aboutsummaryrefslogtreecommitdiffstats
path: root/nikola
diff options
context:
space:
mode:
authorLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:50 -0500
committerLibravatarUnit 193 <unit193@unit193.net>2021-02-03 19:17:50 -0500
commit475d074fd74425efbe783fad08f97f2df0c4909f (patch)
tree2acdae53999b3c74b716efa4edb5b40311fa356a /nikola
parentcd502d52787f666fff3254d7d7e7578930c813c2 (diff)
parent3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (diff)
Update upstream source from tag 'upstream/8.1.2'
Update to upstream version '8.1.2' with Debian dir e5e966a9e6010ef70618dc9a61558fa4db35aceb
Diffstat (limited to 'nikola')
-rw-r--r--nikola/__init__.py10
-rw-r--r--nikola/__main__.py176
-rw-r--r--nikola/conf.py.in591
-rw-r--r--nikola/data/samplesite/galleries/demo/metadata.sample.yml13
-rw-r--r--nikola/data/samplesite/listings/hello.py1
-rw-r--r--nikola/data/samplesite/pages/bootstrap-demo.rst2
-rw-r--r--nikola/data/samplesite/pages/charts.rst (renamed from nikola/data/samplesite/pages/charts.txt)0
l---------nikola/data/samplesite/pages/creating-a-theme.rst2
-rw-r--r--nikola/data/samplesite/pages/dr-nikolas-vendetta.rst936
l---------nikola/data/samplesite/pages/extending.rst1
l---------nikola/data/samplesite/pages/extending.txt1
l---------nikola/data/samplesite/pages/internals.rst1
l---------nikola/data/samplesite/pages/internals.txt1
l---------nikola/data/samplesite/pages/manual.rst2
l---------nikola/data/samplesite/pages/path_handlers.rst1
l---------nikola/data/samplesite/pages/path_handlers.txt1
-rw-r--r--nikola/data/samplesite/pages/quickref.rst6
-rw-r--r--nikola/data/samplesite/pages/quickstart.rst31
-rw-r--r--nikola/data/samplesite/pages/slides-demo.rst17
l---------nikola/data/samplesite/pages/social_buttons.rst1
l---------nikola/data/samplesite/pages/social_buttons.txt1
l---------nikola/data/samplesite/pages/theming.rst2
-rw-r--r--nikola/data/samplesite/posts/1.rst1
-rw-r--r--nikola/data/symlinked.txt167
-rw-r--r--nikola/data/themes/base-jinja/AUTHORS.txt1
-rw-r--r--nikola/data/themes/base-jinja/base-jinja.theme10
-rw-r--r--nikola/data/themes/base-jinja/engine1
-rw-r--r--nikola/data/themes/base-jinja/parent1
-rw-r--r--nikola/data/themes/base-jinja/templates/archive.tmpl1
-rw-r--r--nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl27
-rw-r--r--nikola/data/themes/base-jinja/templates/archiveindex.tmpl21
-rw-r--r--nikola/data/themes/base-jinja/templates/author.tmpl33
-rw-r--r--nikola/data/themes/base-jinja/templates/authorindex.tmpl22
-rw-r--r--nikola/data/themes/base-jinja/templates/authors.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/base.tmpl25
-rw-r--r--nikola/data/themes/base-jinja/templates/base_footer.tmpl1
-rw-r--r--nikola/data/themes/base-jinja/templates/base_header.tmpl21
-rw-r--r--nikola/data/themes/base-jinja/templates/base_helper.tmpl94
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper.tmpl28
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl13
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl2
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl17
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl4
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl14
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl33
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl5
-rw-r--r--nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl23
-rw-r--r--nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl124
-rw-r--r--nikola/data/themes/base-jinja/templates/gallery.tmpl82
-rw-r--r--nikola/data/themes/base-jinja/templates/index.tmpl33
-rw-r--r--nikola/data/themes/base-jinja/templates/index_helper.tmpl31
-rw-r--r--nikola/data/themes/base-jinja/templates/list.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/list_post.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/listing.tmpl10
-rw-r--r--nikola/data/themes/base-jinja/templates/math_helper.tmpl69
-rw-r--r--nikola/data/themes/base-jinja/templates/page.tmpl1
-rw-r--r--nikola/data/themes/base-jinja/templates/pagination_helper.tmpl16
-rw-r--r--nikola/data/themes/base-jinja/templates/post.tmpl9
-rw-r--r--nikola/data/themes/base-jinja/templates/post_header.tmpl25
-rw-r--r--nikola/data/themes/base-jinja/templates/post_helper.tmpl63
-rw-r--r--nikola/data/themes/base-jinja/templates/sectionindex.tmpl21
-rw-r--r--nikola/data/themes/base-jinja/templates/slides.tmpl24
-rw-r--r--nikola/data/themes/base-jinja/templates/story.tmpl3
-rw-r--r--nikola/data/themes/base-jinja/templates/tag.tmpl34
-rw-r--r--nikola/data/themes/base-jinja/templates/tagindex.tmpl13
-rw-r--r--nikola/data/themes/base-jinja/templates/tags.tmpl8
-rw-r--r--nikola/data/themes/base-jinja/templates/ui_helper.tmpl (renamed from nikola/data/themes/base-jinja/templates/crumbs.tmpl)3
l---------nikola/data/themes/base/assets/css/baguetteBox.min.css1
-rw-r--r--nikola/data/themes/base/assets/css/html4css1.css1
-rw-r--r--nikola/data/themes/base/assets/css/ipython.min.css4
-rw-r--r--nikola/data/themes/base/assets/css/ipython.min.css.map1
-rw-r--r--nikola/data/themes/base/assets/css/nikola_ipython.css70
-rw-r--r--nikola/data/themes/base/assets/css/nikola_rst.css79
-rw-r--r--nikola/data/themes/base/assets/css/rst.css332
-rw-r--r--nikola/data/themes/base/assets/css/rst_base.css474
-rw-r--r--nikola/data/themes/base/assets/css/theme.css79
l---------nikola/data/themes/base/assets/js/baguetteBox.min.js1
-rw-r--r--nikola/data/themes/base/assets/js/fancydates.js22
-rw-r--r--nikola/data/themes/base/assets/js/fancydates.min.js1
-rw-r--r--nikola/data/themes/base/assets/js/gallery.js32
-rw-r--r--nikola/data/themes/base/assets/js/gallery.min.js1
l---------[-rw-r--r--]nikola/data/themes/base/assets/js/html5.js9
l---------nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js1
l---------nikola/data/themes/base/assets/js/justified-layout.min.js1
l---------nikola/data/themes/base/assets/js/luxon.min.js1
l---------nikola/data/themes/base/assets/js/moment-with-locales.min.js1
-rw-r--r--nikola/data/themes/base/base.theme9
-rw-r--r--nikola/data/themes/base/bundles21
-rw-r--r--nikola/data/themes/base/engine1
-rw-r--r--nikola/data/themes/base/messages/messages_af.py49
-rw-r--r--nikola/data/themes/base/messages/messages_ar.py39
-rw-r--r--nikola/data/themes/base/messages/messages_az.py13
-rw-r--r--nikola/data/themes/base/messages/messages_bg.py11
-rw-r--r--nikola/data/themes/base/messages/messages_br.py (renamed from nikola/data/themes/base/messages/messages_fil.py)11
-rw-r--r--nikola/data/themes/base/messages/messages_bs.py11
-rw-r--r--nikola/data/themes/base/messages/messages_ca.py43
-rw-r--r--nikola/data/themes/base/messages/messages_cs.py11
-rw-r--r--nikola/data/themes/base/messages/messages_da.py11
-rw-r--r--nikola/data/themes/base/messages/messages_de.py13
-rw-r--r--nikola/data/themes/base/messages/messages_el.py11
-rw-r--r--nikola/data/themes/base/messages/messages_en.py11
-rw-r--r--nikola/data/themes/base/messages/messages_eo.py11
-rw-r--r--nikola/data/themes/base/messages/messages_es.py11
-rw-r--r--nikola/data/themes/base/messages/messages_et.py45
-rw-r--r--nikola/data/themes/base/messages/messages_eu.py17
-rw-r--r--nikola/data/themes/base/messages/messages_fa.py11
-rw-r--r--nikola/data/themes/base/messages/messages_fi.py39
-rw-r--r--nikola/data/themes/base/messages/messages_fr.py13
-rw-r--r--nikola/data/themes/base/messages/messages_fur.py49
-rw-r--r--nikola/data/themes/base/messages/messages_gl.py15
-rw-r--r--nikola/data/themes/base/messages/messages_he.py25
-rw-r--r--nikola/data/themes/base/messages/messages_hi.py51
-rw-r--r--nikola/data/themes/base/messages/messages_hr.py11
-rw-r--r--nikola/data/themes/base/messages/messages_hu.py11
-rw-r--r--nikola/data/themes/base/messages/messages_ia.py49
-rw-r--r--nikola/data/themes/base/messages/messages_id.py13
-rw-r--r--nikola/data/themes/base/messages/messages_it.py15
-rw-r--r--nikola/data/themes/base/messages/messages_ja.py41
-rw-r--r--nikola/data/themes/base/messages/messages_ko.py13
-rw-r--r--nikola/data/themes/base/messages/messages_lt.py11
-rw-r--r--nikola/data/themes/base/messages/messages_mi.py49
-rw-r--r--nikola/data/themes/base/messages/messages_ml.py49
-rw-r--r--nikola/data/themes/base/messages/messages_mr.py49
-rw-r--r--nikola/data/themes/base/messages/messages_nb.py11
-rw-r--r--nikola/data/themes/base/messages/messages_nl.py11
-rw-r--r--nikola/data/themes/base/messages/messages_pa.py13
-rw-r--r--nikola/data/themes/base/messages/messages_pl.py11
-rw-r--r--nikola/data/themes/base/messages/messages_pt.py21
-rw-r--r--nikola/data/themes/base/messages/messages_pt_br.py11
-rw-r--r--nikola/data/themes/base/messages/messages_ru.py13
-rw-r--r--nikola/data/themes/base/messages/messages_si_lk.py44
-rw-r--r--nikola/data/themes/base/messages/messages_sk.py21
-rw-r--r--nikola/data/themes/base/messages/messages_sl.py11
-rw-r--r--nikola/data/themes/base/messages/messages_sq.py11
-rw-r--r--nikola/data/themes/base/messages/messages_sr.py11
-rw-r--r--nikola/data/themes/base/messages/messages_sr_latin.py11
-rw-r--r--nikola/data/themes/base/messages/messages_sv.py11
-rw-r--r--nikola/data/themes/base/messages/messages_te.py23
-rw-r--r--nikola/data/themes/base/messages/messages_th.py49
-rw-r--r--nikola/data/themes/base/messages/messages_tl.py44
-rw-r--r--nikola/data/themes/base/messages/messages_tr.py11
-rw-r--r--nikola/data/themes/base/messages/messages_uk.py17
-rw-r--r--nikola/data/themes/base/messages/messages_ur.py13
-rw-r--r--nikola/data/themes/base/messages/messages_vi.py49
-rw-r--r--nikola/data/themes/base/messages/messages_zh_cn.py41
-rw-r--r--nikola/data/themes/base/messages/messages_zh_tw.py11
-rw-r--r--nikola/data/themes/base/templates/archive.tmpl1
-rw-r--r--nikola/data/themes/base/templates/archive_navigation_helper.tmpl27
-rw-r--r--nikola/data/themes/base/templates/archiveindex.tmpl21
-rw-r--r--nikola/data/themes/base/templates/author.tmpl33
-rw-r--r--nikola/data/themes/base/templates/authorindex.tmpl22
-rw-r--r--nikola/data/themes/base/templates/authors.tmpl8
-rw-r--r--nikola/data/themes/base/templates/base.tmpl25
-rw-r--r--nikola/data/themes/base/templates/base_footer.tmpl1
-rw-r--r--nikola/data/themes/base/templates/base_header.tmpl21
-rw-r--r--nikola/data/themes/base/templates/base_helper.tmpl94
-rw-r--r--nikola/data/themes/base/templates/comments_helper.tmpl28
-rw-r--r--nikola/data/themes/base/templates/comments_helper_commento.tmpl13
-rw-r--r--nikola/data/themes/base/templates/comments_helper_disqus.tmpl2
-rw-r--r--nikola/data/themes/base/templates/comments_helper_googleplus.tmpl17
-rw-r--r--nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl4
-rw-r--r--nikola/data/themes/base/templates/comments_helper_isso.tmpl14
-rw-r--r--nikola/data/themes/base/templates/comments_helper_livefyre.tmpl33
-rw-r--r--nikola/data/themes/base/templates/comments_helper_mustache.tmpl5
-rw-r--r--nikola/data/themes/base/templates/comments_helper_utterances.tmpl23
-rw-r--r--nikola/data/themes/base/templates/feeds_translations_helper.tmpl124
-rw-r--r--nikola/data/themes/base/templates/gallery.tmpl84
-rw-r--r--nikola/data/themes/base/templates/index.tmpl33
-rw-r--r--nikola/data/themes/base/templates/index_helper.tmpl31
-rw-r--r--nikola/data/themes/base/templates/list.tmpl8
-rw-r--r--nikola/data/themes/base/templates/list_post.tmpl8
-rw-r--r--nikola/data/themes/base/templates/listing.tmpl10
-rw-r--r--nikola/data/themes/base/templates/math_helper.tmpl69
-rw-r--r--nikola/data/themes/base/templates/page.tmpl1
-rw-r--r--nikola/data/themes/base/templates/pagination_helper.tmpl16
-rw-r--r--nikola/data/themes/base/templates/post.tmpl9
-rw-r--r--nikola/data/themes/base/templates/post_header.tmpl25
-rw-r--r--nikola/data/themes/base/templates/post_helper.tmpl63
-rw-r--r--nikola/data/themes/base/templates/sectionindex.tmpl21
-rw-r--r--nikola/data/themes/base/templates/slides.tmpl24
-rw-r--r--nikola/data/themes/base/templates/story.tmpl3
-rw-r--r--nikola/data/themes/base/templates/tag.tmpl34
-rw-r--r--nikola/data/themes/base/templates/tagindex.tmpl13
-rw-r--r--nikola/data/themes/base/templates/tags.tmpl8
-rw-r--r--nikola/data/themes/base/templates/ui_helper.tmpl (renamed from nikola/data/themes/base/templates/crumbs.tmpl)3
-rw-r--r--nikola/data/themes/bootblog4-jinja/README.md6
l---------nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css1
-rw-r--r--nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme12
l---------nikola/data/themes/bootblog4-jinja/bundles1
-rw-r--r--nikola/data/themes/bootblog4-jinja/templates/base.tmpl104
-rw-r--r--nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl169
-rw-r--r--nikola/data/themes/bootblog4-jinja/templates/index.tmpl150
-rw-r--r--nikola/data/themes/bootblog4/README.md6
-rw-r--r--nikola/data/themes/bootblog4/assets/css/bootblog.css225
-rw-r--r--nikola/data/themes/bootblog4/bootblog4.theme12
-rw-r--r--nikola/data/themes/bootblog4/bundles28
-rw-r--r--nikola/data/themes/bootblog4/templates/base.tmpl104
-rw-r--r--nikola/data/themes/bootblog4/templates/base_helper.tmpl169
-rw-r--r--nikola/data/themes/bootblog4/templates/index.tmpl150
-rw-r--r--nikola/data/themes/bootstrap3-jinja/AUTHORS.txt1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/README.md8
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map1
l---------nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/docs.css160
l---------nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.pngbin215 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.pngbin217 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.pngbin216 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.pngbin214 -> 0 bytes
l---------nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif1
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot1
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg1
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf1
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff1
l---------nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff21
l---------nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js271
l---------nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js1
l---------nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map1
l---------nikola/data/themes/bootstrap3-jinja/bundles1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/engine1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/parent1
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/base.tmpl94
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl188
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl95
-rw-r--r--nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl24
-rw-r--r--nikola/data/themes/bootstrap3/README.md8
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap.css1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css1
l---------nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map1
l---------nikola/data/themes/bootstrap3/assets/css/colorbox.css1
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/docs.css160
l---------nikola/data/themes/bootstrap3/assets/css/images/controls.png1
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.pngbin215 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.pngbin217 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.pngbin216 -> 0 bytes
-rw-r--r--nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.pngbin214 -> 0 bytes
l---------nikola/data/themes/bootstrap3/assets/css/images/loading.gif1
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot1
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg1
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf1
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff1
l---------nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff21
l---------nikola/data/themes/bootstrap3/assets/js/bootstrap.js1
l---------nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js1
l---------nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js1
-rw-r--r--nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js271
l---------nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js1
l---------nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js1
l---------nikola/data/themes/bootstrap3/assets/js/jquery.js1
l---------nikola/data/themes/bootstrap3/assets/js/jquery.min.js1
l---------nikola/data/themes/bootstrap3/assets/js/jquery.min.map1
-rw-r--r--nikola/data/themes/bootstrap3/bundles4
-rw-r--r--nikola/data/themes/bootstrap3/engine1
-rw-r--r--nikola/data/themes/bootstrap3/parent1
-rw-r--r--nikola/data/themes/bootstrap3/templates/base.tmpl94
-rw-r--r--nikola/data/themes/bootstrap3/templates/base_helper.tmpl188
-rw-r--r--nikola/data/themes/bootstrap3/templates/gallery.tmpl95
-rw-r--r--nikola/data/themes/bootstrap3/templates/slides.tmpl24
-rw-r--r--nikola/data/themes/bootstrap4-jinja/README.md10
l---------nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css1
-rw-r--r--nikola/data/themes/bootstrap4-jinja/assets/css/theme.css (renamed from nikola/data/themes/bootstrap3-jinja/assets/css/theme.css)147
l---------nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js1
l---------nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js1
l---------nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js1
-rw-r--r--nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme12
l---------nikola/data/themes/bootstrap4-jinja/bundles1
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/authors.tmpl (renamed from nikola/data/themes/bootstrap3-jinja/templates/authors.tmpl)8
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/base.tmpl105
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl165
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl13
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/listing.tmpl (renamed from nikola/data/themes/bootstrap3-jinja/templates/listing.tmpl)14
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl40
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/post.tmpl (renamed from nikola/data/themes/bootstrap3-jinja/templates/post.tmpl)14
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/tags.tmpl (renamed from nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl)4
-rw-r--r--nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl24
-rw-r--r--nikola/data/themes/bootstrap4/README.md10
l---------nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css1
-rw-r--r--nikola/data/themes/bootstrap4/assets/css/theme.css (renamed from nikola/data/themes/bootstrap3/assets/css/theme.css)147
l---------nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js1
l---------nikola/data/themes/bootstrap4/assets/js/jquery.min.js1
l---------nikola/data/themes/bootstrap4/assets/js/popper.min.js1
-rw-r--r--nikola/data/themes/bootstrap4/bootstrap4.theme12
-rw-r--r--nikola/data/themes/bootstrap4/bundles26
-rw-r--r--nikola/data/themes/bootstrap4/templates/authors.tmpl (renamed from nikola/data/themes/bootstrap3/templates/authors.tmpl)8
-rw-r--r--nikola/data/themes/bootstrap4/templates/base.tmpl105
-rw-r--r--nikola/data/themes/bootstrap4/templates/base_helper.tmpl165
-rw-r--r--nikola/data/themes/bootstrap4/templates/index_helper.tmpl13
-rw-r--r--nikola/data/themes/bootstrap4/templates/listing.tmpl (renamed from nikola/data/themes/bootstrap3/templates/listing.tmpl)14
-rw-r--r--nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl40
-rw-r--r--nikola/data/themes/bootstrap4/templates/post.tmpl (renamed from nikola/data/themes/bootstrap3/templates/post.tmpl)14
-rw-r--r--nikola/data/themes/bootstrap4/templates/tags.tmpl (renamed from nikola/data/themes/bootstrap3/templates/tags.tmpl)4
-rw-r--r--nikola/data/themes/bootstrap4/templates/ui_helper.tmpl24
-rw-r--r--nikola/filters.py276
-rw-r--r--nikola/hierarchy_utils.py274
-rw-r--r--nikola/image_processing.py241
-rw-r--r--nikola/log.py152
-rw-r--r--nikola/metadata_extractors.py274
-rw-r--r--nikola/nikola.py1720
-rw-r--r--nikola/packages/README.md5
-rw-r--r--nikola/packages/datecond/LICENSE2
-rw-r--r--nikola/packages/datecond/__init__.py23
-rw-r--r--nikola/packages/pygments_better_html/LICENSE30
-rw-r--r--nikola/packages/pygments_better_html/LICENSE.pygments25
-rw-r--r--nikola/packages/pygments_better_html/__init__.py241
-rw-r--r--nikola/packages/tzlocal/__init__.py9
-rw-r--r--nikola/packages/tzlocal/darwin.py43
-rw-r--r--nikola/packages/tzlocal/unix.py184
-rw-r--r--nikola/packages/tzlocal/win32.py31
-rw-r--r--nikola/packages/tzlocal/windows_tz.py1226
-rw-r--r--nikola/plugin_categories.py542
-rw-r--r--nikola/plugins/__init__.py2
-rw-r--r--nikola/plugins/basic_import.py27
-rw-r--r--nikola/plugins/command/__init__.py2
-rw-r--r--nikola/plugins/command/auto.plugin2
-rw-r--r--nikola/plugins/command/auto/__init__.py690
l---------nikola/plugins/command/auto/livereload.js2
-rw-r--r--nikola/plugins/command/bootswatch_theme.py116
-rw-r--r--nikola/plugins/command/check.plugin2
-rw-r--r--nikola/plugins/command/check.py104
-rw-r--r--nikola/plugins/command/console.plugin2
-rw-r--r--nikola/plugins/command/console.py45
-rw-r--r--nikola/plugins/command/default_config.plugin13
-rw-r--r--nikola/plugins/command/default_config.py54
-rw-r--r--nikola/plugins/command/deploy.plugin2
-rw-r--r--nikola/plugins/command/deploy.py54
-rw-r--r--nikola/plugins/command/github_deploy.plugin2
-rw-r--r--nikola/plugins/command/github_deploy.py43
-rw-r--r--nikola/plugins/command/import_wordpress.plugin2
-rw-r--r--nikola/plugins/command/import_wordpress.py283
-rw-r--r--nikola/plugins/command/init.plugin2
-rw-r--r--nikola/plugins/command/init.py88
-rw-r--r--nikola/plugins/command/install_theme.plugin13
-rw-r--r--nikola/plugins/command/install_theme.py91
-rw-r--r--nikola/plugins/command/new_page.plugin2
-rw-r--r--nikola/plugins/command/new_page.py4
-rw-r--r--nikola/plugins/command/new_post.plugin2
-rw-r--r--nikola/plugins/command/new_post.py105
-rw-r--r--nikola/plugins/command/orphans.plugin2
-rw-r--r--nikola/plugins/command/orphans.py3
-rw-r--r--nikola/plugins/command/plugin.plugin2
-rw-r--r--nikola/plugins/command/plugin.py109
-rw-r--r--nikola/plugins/command/rst2html.plugin2
-rw-r--r--nikola/plugins/command/rst2html/__init__.py11
-rw-r--r--nikola/plugins/command/serve.plugin2
-rw-r--r--nikola/plugins/command/serve.py87
-rw-r--r--nikola/plugins/command/status.plugin2
-rw-r--r--nikola/plugins/command/status.py3
-rw-r--r--nikola/plugins/command/subtheme.plugin (renamed from nikola/plugins/command/bootswatch_theme.plugin)10
-rw-r--r--nikola/plugins/command/subtheme.py150
-rw-r--r--nikola/plugins/command/theme.plugin2
-rw-r--r--nikola/plugins/command/theme.py102
-rw-r--r--nikola/plugins/command/version.plugin2
-rw-r--r--nikola/plugins/command/version.py17
-rw-r--r--nikola/plugins/compile/__init__.py2
-rw-r--r--nikola/plugins/compile/html.plugin2
-rw-r--r--nikola/plugins/compile/html.py78
-rw-r--r--nikola/plugins/compile/ipynb.plugin6
-rw-r--r--nikola/plugins/compile/ipynb.py189
-rw-r--r--nikola/plugins/compile/markdown.plugin2
-rw-r--r--nikola/plugins/compile/markdown/__init__.py129
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.plugin2
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.py20
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.plugin2
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.py14
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.plugin2
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.py10
-rw-r--r--nikola/plugins/compile/pandoc.plugin2
-rw-r--r--nikola/plugins/compile/pandoc.py29
-rw-r--r--nikola/plugins/compile/php.plugin2
-rw-r--r--nikola/plugins/compile/php.py22
-rw-r--r--nikola/plugins/compile/rest.plugin4
-rw-r--r--nikola/plugins/compile/rest/__init__.py229
-rw-r--r--nikola/plugins/compile/rest/chart.plugin2
-rw-r--r--nikola/plugins/compile/rest/chart.py58
-rw-r--r--nikola/plugins/compile/rest/doc.plugin2
-rw-r--r--nikola/plugins/compile/rest/doc.py38
-rw-r--r--nikola/plugins/compile/rest/gist.plugin2
-rw-r--r--nikola/plugins/compile/rest/gist.py2
-rw-r--r--nikola/plugins/compile/rest/listing.plugin2
-rw-r--r--nikola/plugins/compile/rest/listing.py25
-rw-r--r--nikola/plugins/compile/rest/media.plugin2
-rw-r--r--nikola/plugins/compile/rest/media.py13
-rw-r--r--nikola/plugins/compile/rest/post_list.plugin6
-rw-r--r--nikola/plugins/compile/rest/post_list.py258
-rw-r--r--nikola/plugins/compile/rest/slides.plugin14
-rw-r--r--nikola/plugins/compile/rest/slides.py78
-rw-r--r--nikola/plugins/compile/rest/soundcloud.plugin2
-rw-r--r--nikola/plugins/compile/rest/soundcloud.py26
-rw-r--r--nikola/plugins/compile/rest/thumbnail.plugin2
-rw-r--r--nikola/plugins/compile/rest/thumbnail.py6
-rw-r--r--nikola/plugins/compile/rest/vimeo.plugin2
-rw-r--r--nikola/plugins/compile/rest/vimeo.py13
-rw-r--r--nikola/plugins/compile/rest/youtube.plugin2
-rw-r--r--nikola/plugins/compile/rest/youtube.py19
-rw-r--r--nikola/plugins/misc/__init__.py2
-rw-r--r--nikola/plugins/misc/scan_posts.py29
-rw-r--r--nikola/plugins/misc/taxonomies_classifier.plugin12
-rw-r--r--nikola/plugins/misc/taxonomies_classifier.py335
-rw-r--r--nikola/plugins/shortcode/chart.plugin13
-rw-r--r--nikola/plugins/shortcode/chart.py90
-rw-r--r--nikola/plugins/shortcode/emoji.plugin13
-rw-r--r--nikola/plugins/shortcode/emoji/__init__.py46
-rw-r--r--nikola/plugins/shortcode/emoji/data/Activity.json418
-rw-r--r--nikola/plugins/shortcode/emoji/data/Flags.json998
-rw-r--r--nikola/plugins/shortcode/emoji/data/Food.json274
-rw-r--r--nikola/plugins/shortcode/emoji/data/LICENSE25
-rw-r--r--nikola/plugins/shortcode/emoji/data/Nature.json594
-rw-r--r--nikola/plugins/shortcode/emoji/data/Objects.json718
-rw-r--r--nikola/plugins/shortcode/emoji/data/People.json1922
-rw-r--r--nikola/plugins/shortcode/emoji/data/Symbols.json1082
-rw-r--r--nikola/plugins/shortcode/emoji/data/Travel.json466
-rw-r--r--nikola/plugins/shortcode/gist.plugin2
-rw-r--r--nikola/plugins/shortcode/gist.py6
-rw-r--r--nikola/plugins/shortcode/listing.plugin13
-rw-r--r--nikola/plugins/shortcode/listing.py77
-rw-r--r--nikola/plugins/shortcode/post_list.plugin13
-rw-r--r--nikola/plugins/shortcode/post_list.py245
-rw-r--r--nikola/plugins/shortcode/thumbnail.plugin12
-rw-r--r--nikola/plugins/shortcode/thumbnail.py69
-rw-r--r--nikola/plugins/task/__init__.py2
-rw-r--r--nikola/plugins/task/archive.plugin4
-rw-r--r--nikola/plugins/task/archive.py409
-rw-r--r--nikola/plugins/task/authors.plugin4
-rw-r--r--nikola/plugins/task/authors.py387
-rw-r--r--nikola/plugins/task/bundles.plugin4
-rw-r--r--nikola/plugins/task/bundles.py83
-rw-r--r--nikola/plugins/task/categories.plugin12
-rw-r--r--nikola/plugins/task/categories.py248
-rw-r--r--nikola/plugins/task/copy_assets.plugin2
-rw-r--r--nikola/plugins/task/copy_assets.py37
-rw-r--r--nikola/plugins/task/copy_files.plugin2
-rw-r--r--nikola/plugins/task/copy_files.py2
-rw-r--r--nikola/plugins/task/galleries.plugin2
-rw-r--r--nikola/plugins/task/galleries.py233
-rw-r--r--nikola/plugins/task/gzip.plugin2
-rw-r--r--nikola/plugins/task/gzip.py2
-rw-r--r--nikola/plugins/task/indexes.plugin5
-rw-r--r--nikola/plugins/task/indexes.py397
-rw-r--r--nikola/plugins/task/listings.plugin2
-rw-r--r--nikola/plugins/task/listings.py48
-rw-r--r--nikola/plugins/task/page_index.plugin12
-rw-r--r--nikola/plugins/task/page_index.py111
-rw-r--r--nikola/plugins/task/pages.plugin2
-rw-r--r--nikola/plugins/task/pages.py20
-rw-r--r--nikola/plugins/task/posts.plugin2
-rw-r--r--nikola/plugins/task/posts.py18
-rw-r--r--nikola/plugins/task/py3_switch.plugin13
-rw-r--r--nikola/plugins/task/py3_switch.py103
-rw-r--r--nikola/plugins/task/redirect.plugin2
-rw-r--r--nikola/plugins/task/redirect.py6
-rw-r--r--nikola/plugins/task/robots.plugin2
-rw-r--r--nikola/plugins/task/robots.py13
-rw-r--r--nikola/plugins/task/rss.plugin13
-rw-r--r--nikola/plugins/task/rss.py117
-rw-r--r--nikola/plugins/task/scale_images.plugin2
-rw-r--r--nikola/plugins/task/scale_images.py32
-rw-r--r--nikola/plugins/task/sitemap.plugin2
-rw-r--r--nikola/plugins/task/sitemap.py (renamed from nikola/plugins/task/sitemap/__init__.py)47
-rw-r--r--nikola/plugins/task/sources.plugin2
-rw-r--r--nikola/plugins/task/sources.py10
-rw-r--r--nikola/plugins/task/tags.plugin5
-rw-r--r--nikola/plugins/task/tags.py570
-rw-r--r--nikola/plugins/task/taxonomies.plugin12
-rw-r--r--nikola/plugins/task/taxonomies.py459
-rw-r--r--nikola/plugins/template/__init__.py2
-rw-r--r--nikola/plugins/template/jinja.plugin2
-rw-r--r--nikola/plugins/template/jinja.py21
-rw-r--r--nikola/plugins/template/mako.plugin2
-rw-r--r--nikola/plugins/template/mako.py33
-rw-r--r--nikola/post.py1035
-rw-r--r--nikola/rc4.py84
-rw-r--r--nikola/shortcodes.py112
-rw-r--r--nikola/state.py11
-rw-r--r--nikola/utils.py1130
-rw-r--r--nikola/winutils.py9
575 files changed, 21985 insertions, 10743 deletions
diff --git a/nikola/__init__.py b/nikola/__init__.py
index a7f6fc6..4ead429 100644
--- a/nikola/__init__.py
+++ b/nikola/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,11 +26,15 @@
"""Nikola -- a modular, fast, simple, static website generator."""
-from __future__ import absolute_import
import os
+import sys
-__version__ = '7.8.1'
+__version__ = '8.1.2'
DEBUG = bool(os.getenv('NIKOLA_DEBUG'))
+SHOW_TRACEBACKS = bool(os.getenv('NIKOLA_SHOW_TRACEBACKS'))
+
+if sys.version_info[0] == 2:
+ raise Exception("Nikola does not support Python 2.")
from .nikola import Nikola # NOQA
from . import plugins # NOQA
diff --git a/nikola/__main__.py b/nikola/__main__.py
index f002768..8330e67 100644
--- a/nikola/__main__.py
+++ b/nikola/__main__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,38 +26,37 @@
"""The main function of Nikola."""
-from __future__ import print_function, unicode_literals
-from collections import defaultdict
+import importlib.util
import os
import shutil
-try:
- import readline # NOQA
-except ImportError:
- pass # This is only so raw_input/input does nicer things if it's available
import sys
+import textwrap
import traceback
+import doit.cmd_base
+from collections import defaultdict
-from doit.loader import generate_tasks
-from doit.cmd_base import TaskLoader
-from doit.reporter import ExecutedOnlyReporter
-from doit.doit_cmd import DoitMain
-from doit.cmd_help import Help as DoitHelp
-from doit.cmd_run import Run as DoitRun
+from blinker import signal
+from doit.cmd_auto import Auto as DoitAuto
+from doit.cmd_base import TaskLoader, _wrap
from doit.cmd_clean import Clean as DoitClean
from doit.cmd_completion import TabCompletion
-from doit.cmd_auto import Auto as DoitAuto
-from logbook import NullHandler
-from blinker import signal
+from doit.cmd_help import Help as DoitHelp
+from doit.cmd_run import Run as DoitRun
+from doit.doit_cmd import DoitMain
+from doit.loader import generate_tasks
+from doit.reporter import ExecutedOnlyReporter
from . import __version__
-from .plugin_categories import Command
from .nikola import Nikola
-from .utils import sys_decode, sys_encode, get_root_dir, req_missing, LOGGER, STRICT_HANDLER, STDERR_HANDLER, ColorfulStderrHandler
+from .plugin_categories import Command
+from .log import configure_logging, LOGGER, ColorfulFormatter, LoggingMode
+from .utils import get_root_dir, req_missing, sys_decode
+
+try:
+ import readline # NOQA
+except ImportError:
+ pass # This is only so raw_input/input does nicer things if it's available
-if sys.version_info[0] == 3:
- import importlib.machinery
-else:
- import imp
config = {}
@@ -68,10 +67,10 @@ _RETURN_DOITNIKOLA = False
def main(args=None):
"""Run Nikola."""
colorful = False
- if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None:
+ if sys.stderr.isatty() and os.name != 'nt' and os.getenv('NIKOLA_MONO') is None and os.getenv('TERM') != 'dumb':
colorful = True
- ColorfulStderrHandler._colorful = colorful
+ ColorfulFormatter._colorful = colorful
if args is None:
args = sys.argv[1:]
@@ -80,29 +79,24 @@ def main(args=None):
args = [sys_decode(arg) for arg in args]
conf_filename = 'conf.py'
- conf_filename_bytes = b'conf.py'
conf_filename_changed = False
for index, arg in enumerate(args):
if arg[:7] == '--conf=':
del args[index]
del oargs[index]
conf_filename = arg[7:]
- conf_filename_bytes = sys_encode(arg[7:])
conf_filename_changed = True
break
quiet = False
- strict = False
if len(args) > 0 and args[0] == 'build' and '--strict' in args:
- LOGGER.notice('Running in strict mode')
- STRICT_HANDLER.push_application()
- strict = True
- if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
- NullHandler().push_application()
+ LOGGER.info('Running in strict mode')
+ configure_logging(LoggingMode.STRICT)
+ elif len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
+ configure_logging(LoggingMode.QUIET)
quiet = True
- if not quiet and not strict:
- NullHandler().push_application()
- STDERR_HANDLER[0].push_application()
+ else:
+ configure_logging()
global config
@@ -119,20 +113,22 @@ def main(args=None):
os.chdir(root)
# Help and imports don't require config, but can use one if it exists
needs_config_file = (argname != 'help') and not argname.startswith('import_')
+ LOGGER.debug("Website root: %r", root)
else:
needs_config_file = False
- sys.path.append('')
+ sys.path.insert(0, os.path.dirname(conf_filename))
try:
- if sys.version_info[0] == 3:
- loader = importlib.machinery.SourceFileLoader("conf", conf_filename)
- conf = loader.load_module()
- else:
- conf = imp.load_source("conf", conf_filename_bytes)
+ spec = importlib.util.spec_from_file_location("conf", conf_filename)
+ conf = importlib.util.module_from_spec(spec)
+ # Preserve caching behavior of `import conf` if the filename matches
+ if os.path.splitext(os.path.basename(conf_filename))[0] == "conf":
+ sys.modules["conf"] = conf
+ spec.loader.exec_module(conf)
config = conf.__dict__
except Exception:
if os.path.exists(conf_filename):
- msg = traceback.format_exc(0)
+ msg = traceback.format_exc()
LOGGER.error('"{0}" cannot be parsed.\n{1}'.format(conf_filename, msg))
return 1
elif needs_config_file and conf_filename_changed:
@@ -155,7 +151,7 @@ def main(args=None):
req_missing(['freezegun'], 'perform invariant builds')
if config:
- if os.path.exists('plugins') and not os.path.exists('plugins/__init__.py'):
+ if os.path.isdir('plugins') and not os.path.exists('plugins/__init__.py'):
with open('plugins/__init__.py', 'w') as fh:
fh.write('# Plugin modules go here.')
@@ -233,19 +229,21 @@ class Build(DoitRun):
}
)
self.cmd_options = tuple(opts)
- super(Build, self).__init__(*args, **kw)
+ super().__init__(*args, **kw)
class Clean(DoitClean):
"""Clean site, including the cache directory."""
- def clean_tasks(self, tasks, dryrun):
+ # The unseemly *a is because this API changed between doit 0.30.1 and 0.31
+ def clean_tasks(self, tasks, dryrun, *a):
"""Clean tasks."""
if not dryrun and config:
cache_folder = config.get('CACHE_FOLDER', 'cache')
if os.path.exists(cache_folder):
shutil.rmtree(cache_folder)
- return super(Clean, self).clean_tasks(tasks, dryrun)
+ return super(Clean, self).clean_tasks(tasks, dryrun, *a)
+
# Nikola has its own "auto" commands that uses livereload.
# Expose original doit "auto" command as "doit_auto".
@@ -274,13 +272,20 @@ class NikolaTaskLoader(TaskLoader):
}
DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render']
DOIT_CONFIG.update(self.nikola._doit_config)
- tasks = generate_tasks(
- 'render_site',
- self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
- latetasks = generate_tasks(
- 'post_render',
- self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
- signal('initialized').send(self.nikola)
+ try:
+ tasks = generate_tasks(
+ 'render_site',
+ self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
+ latetasks = generate_tasks(
+ 'post_render',
+ self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executed after site is rendered.'))
+ signal('initialized').send(self.nikola)
+ except Exception:
+ LOGGER.error('Error loading tasks. An unhandled exception occurred.')
+ if self.nikola.debug or self.nikola.show_tracebacks:
+ raise
+ _print_exception()
+ sys.exit(3)
return tasks + latetasks, DOIT_CONFIG
@@ -293,7 +298,7 @@ class DoitNikola(DoitMain):
def __init__(self, nikola, quiet=False):
"""Initialzie DoitNikola."""
- super(DoitNikola, self).__init__()
+ super().__init__()
self.nikola = nikola
nikola.doit = self
self.task_loader = self.TASK_LOADER(nikola, quiet)
@@ -362,7 +367,14 @@ class DoitNikola(DoitMain):
LOGGER.error("This command needs to run inside an "
"existing Nikola site.")
return 3
- return super(DoitNikola, self).run(cmd_args)
+ try:
+ return super().run(cmd_args)
+ except Exception:
+ LOGGER.error('An unhandled exception occurred.')
+ if self.nikola.debug or self.nikola.show_tracebacks:
+ raise
+ _print_exception()
+ return 1
@staticmethod
def print_version():
@@ -370,6 +382,53 @@ class DoitNikola(DoitMain):
print("Nikola v" + __version__)
+# Override Command.help() to make it more readable and to remove
+# some doit-specific stuff. Based on doit's implementation.
+# (see Issue #3342)
+def _command_help(self: Command):
+ """Return help text for a command."""
+ text = []
+
+ usage = "{} {} {}".format(self.bin_name, self.name, self.doc_usage)
+ text.extend(textwrap.wrap(usage, subsequent_indent=' '))
+ text.extend(_wrap(self.doc_purpose, 4))
+
+ text.append("\nOptions:")
+ options = defaultdict(list)
+ for opt in self.cmdparser.options:
+ options[opt.section].append(opt)
+ for section, opts in sorted(options.items()):
+ if section:
+ section_name = '\n{}'.format(section)
+ text.extend(_wrap(section_name, 2))
+ for opt in opts:
+ # ignore option that cant be modified on cmd line
+ if not (opt.short or opt.long):
+ continue
+ text.extend(_wrap(opt.help_param(), 4))
+ opt_help = opt.help
+ if '%(default)s' in opt_help:
+ opt_help = opt.help % {'default': opt.default}
+ elif opt.default != '' and opt.default is not False and opt.default is not None:
+ opt_help += ' [default: {}]'.format(opt.default)
+ opt_choices = opt.help_choices()
+ desc = '{} {}'.format(opt_help, opt_choices)
+ text.extend(_wrap(desc, 8))
+
+ # print bool inverse option
+ if opt.inverse:
+ text.extend(_wrap('--{}'.format(opt.inverse), 4))
+ text.extend(_wrap('opposite of --{}'.format(opt.long), 8))
+
+ if self.doc_description is not None:
+ text.append("\n\nDescription:")
+ text.extend(_wrap(self.doc_description, 4))
+ return "\n".join(text)
+
+
+doit.cmd_base.Command.help = _command_help
+
+
def levenshtein(s1, s2):
u"""Calculate the Levenshtein distance of two strings.
@@ -398,5 +457,12 @@ def levenshtein(s1, s2):
return previous_row[-1]
+def _print_exception():
+ """Print an exception in a friendlier, shorter style."""
+ etype, evalue, _ = sys.exc_info()
+ LOGGER.error(''.join(traceback.format_exception(etype, evalue, None, limit=0, chain=False)).strip())
+ LOGGER.warning("To see more details, run Nikola in debug mode (set environment variable NIKOLA_DEBUG=1) or use NIKOLA_SHOW_TRACEBACKS=1")
+
+
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
diff --git a/nikola/conf.py.in b/nikola/conf.py.in
index 5010278..4546460 100644
--- a/nikola/conf.py.in
+++ b/nikola/conf.py.in
@@ -1,7 +1,6 @@
## -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
import time
# !! This is the configuration of Nikola. !! #
@@ -57,7 +56,7 @@ TRANSLATIONS = ${TRANSLATIONS}
# this pattern is also used for metadata:
# something.meta -> something.pl.meta
-TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN}
+TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
# Links for the sidebar / navigation bar. (translatable)
# This is a dict. The keys are languages, and values are tuples.
@@ -76,9 +75,9 @@ TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN}
#
# WARNING: Support for submenus is theme-dependent.
# Only one level of submenus is supported.
-# WARNING: Some themes, including the default Bootstrap 3 theme,
+# WARNING: Some themes, including the default Bootstrap 4 theme,
# may present issues if the menu is too large.
-# (in bootstrap3, the navbar can grow too large and cover contents.)
+# (in Bootstrap, the navbar can grow too large and cover contents.)
# WARNING: If you link to directories, make sure to follow
# ``STRIP_INDEXES``. If it’s set to ``True``, end your links
# with a ``/``, otherwise end them with ``/index.html`` — or
@@ -86,14 +85,61 @@ TRANSLATIONS_PATTERN = ${TRANSLATIONS_PATTERN}
NAVIGATION_LINKS = ${NAVIGATION_LINKS}
+# Alternative navigation links. Works the same way NAVIGATION_LINKS does,
+# although themes may not always support them. (translatable)
+# (Bootstrap 4: right-side of navbar, Bootblog 4: right side of title)
+NAVIGATION_ALT_LINKS = {
+ DEFAULT_LANG: ()
+}
+
# Name of the theme to use.
THEME = ${THEME}
-# Primary color of your theme. This will be used to customize your theme and
-# auto-generate related colors in POSTS_SECTION_COLORS. Must be a HEX value.
+# A theme color. In default themes, it might be displayed by some browsers as
+# the browser UI color (eg. Chrome on Android). Other themes might also use it
+# as an accent color (the default ones don’t). Must be a HEX value.
THEME_COLOR = '#5670d4'
+# Theme configuration. Fully theme-dependent. (translatable)
+# Samples for bootblog4 (enabled) and bootstrap4 (commented) follow.
+# bootblog4 supports: featured_large featured_small featured_on_mobile
+# featured_large_image_on_mobile featured_strip_html sidebar
+# bootstrap4 supports: navbar_light (defaults to False)
+# navbar_custom_bg (defaults to '')
+
+# Config for bootblog4:
+THEME_CONFIG = {
+ DEFAULT_LANG: {
+ # Show the latest featured post in a large box, with the previewimage as its background.
+ 'featured_large': False,
+ # Show the first (remaining) two featured posts in small boxes.
+ 'featured_small': False,
+ # Show featured posts on mobile.
+ 'featured_on_mobile': True,
+ # Show image in `featured_large` on mobile.
+ # `featured_small` displays them only on desktop.
+ 'featured_large_image_on_mobile': True,
+ # Strip HTML from featured post text.
+ 'featured_strip_html': False,
+ # Contents of the sidebar, If empty, the sidebar is not displayed.
+ 'sidebar': ''
+ }
+}
+# Config for bootstrap4:
+# THEME_CONFIG = {
+# DEFAULT_LANG: {
+# # Use a light navbar with dark text. Defaults to False.
+# 'navbar_light': False,
+# # Use a custom navbar color. If unset, 'navbar_light' sets text +
+# # background color. If set, navbar_light controls only background
+# # color. Supported values: bg-dark, bg-light, bg-primary, bg-secondary,
+# # bg-success, bg-danger, bg-warning, bg-info, bg-white, bg-transparent.
+# 'navbar_custom_bg': '',
+# }
+# }
+
# POSTS and PAGES contains (wildcard, destination, template) tuples.
+# (translatable)
#
# The wildcard is used to generate a list of source files
# (whatever/thing.rst, for example).
@@ -119,6 +165,12 @@ THEME_COLOR = '#5670d4'
# to feeds, indexes, tag lists and archives and are considered part
# of a blog, while PAGES are just independent HTML pages.
#
+# Finally, note that destination can be translated, i.e. you can
+# specify a different translation folder per language. Example:
+# PAGES = (
+# ("pages/*.rst", {"en": "pages", "de": "seiten"}, "page.tmpl"),
+# ("pages/*.md", {"en": "pages", "de": "seiten"}, "page.tmpl"),
+# )
POSTS = ${POSTS}
PAGES = ${PAGES}
@@ -143,36 +195,29 @@ TIMEZONE = ${TIMEZONE}
# FORCE_ISO8601 = False
# Date format used to display post dates. (translatable)
-# (str used by datetime.datetime.strftime)
-# DATE_FORMAT = '%Y-%m-%d %H:%M'
+# Used by babel.dates, CLDR style: http://cldr.unicode.org/translation/date-time
+# You can also use 'full', 'long', 'medium', or 'short'
+# DATE_FORMAT = 'yyyy-MM-dd HH:mm'
# Date format used to display post dates, if local dates are used. (translatable)
-# (str used by moment.js)
-# JS_DATE_FORMAT = 'YYYY-MM-DD HH:mm'
+# Used by Luxon: https://moment.github.io/luxon/docs/manual/formatting
+# Example for presets: {'preset': True, 'format': 'DATE_FULL'}
+# LUXON_DATE_FORMAT = {
+# DEFAULT_LANG: {'preset': False, 'format': 'yyyy-MM-dd HH:mm'},
+# }
# Date fanciness.
#
-# 0 = using DATE_FORMAT and TIMEZONE
-# 1 = using JS_DATE_FORMAT and local user time (via moment.js)
-# 2 = using a string like “2 days ago”
+# 0 = using DATE_FORMAT and TIMEZONE (without JS)
+# 1 = using LUXON_DATE_FORMAT and local user time (JS, using Luxon)
+# 2 = using a string like “2 days ago” (JS, using Luxon)
#
-# Your theme must support it, bootstrap and bootstrap3 already do.
+# Your theme must support it, Bootstrap already does.
# DATE_FANCINESS = 0
-# While Nikola can select a sensible locale for each language,
-# sometimes explicit control can come handy.
-# In this file we express locales in the string form that
-# python's locales will accept in your OS, by example
-# "en_US.utf8" in Unix-like OS, "English_United States" in Windows.
-# LOCALES = dict mapping language --> explicit locale for the languages
-# in TRANSLATIONS. You can omit one or more keys.
-# LOCALE_FALLBACK = locale to use when an explicit locale is unavailable
-# LOCALE_DEFAULT = locale to use for languages not mentioned in LOCALES; if
-# not set the default Nikola mapping is used.
-
+# Customize the locale/region used for a language.
+# For example, to use British instead of US English: LOCALES = {'en': 'en_GB'}
# LOCALES = {}
-# LOCALE_FALLBACK = None
-# LOCALE_DEFAULT = None
# One or more folders containing files to be copied as-is into the output.
# The format is a dictionary of {source: relative destination}.
@@ -190,20 +235,42 @@ TIMEZONE = ${TIMEZONE}
# Feel free to add or delete extensions to any list, but don't add any new
# compilers unless you write the interface for it yourself.
#
+# The default compiler for `new_post` is the first entry in the POSTS tuple.
+#
# 'rest' is reStructuredText
-# 'markdown' is MarkDown
+# 'markdown' is Markdown
# 'html' assumes the file is HTML and just copies it
COMPILERS = ${COMPILERS}
+# Enable reST directives that insert the contents of external files such
+# as "include" and "raw." This maps directly to the docutils file_insertion_enabled
+# config. See: http://docutils.sourceforge.net/docs/user/config.html#file-insertion-enabled
+# REST_FILE_INSERTION_ENABLED = True
+
# Create by default posts in one file format?
# Set to False for two-file posts, with separate metadata.
# ONE_FILE_POSTS = True
+# Preferred metadata format for new posts
+# "Nikola": reST comments, wrapped in a HTML comment if needed (default)
+# "YAML": YAML wrapped in "---"
+# "TOML": TOML wrapped in "+++"
+# "Pelican": Native markdown metadata or reST docinfo fields. Nikola style for other formats.
+# METADATA_FORMAT = "Nikola"
+
+# Use date-based path when creating posts?
+# Can be enabled on a per-post basis with `nikola new_post -d`.
+# The setting is ignored when creating pages.
+# NEW_POST_DATE_PATH = False
+
+# What format to use when creating posts with date paths?
+# Default is '%Y/%m/%d', other possibilities include '%Y' or '%Y/%m'.
+# NEW_POST_DATE_PATH_FORMAT = '%Y/%m/%d'
+
# If this is set to True, the DEFAULT_LANG version will be displayed for
# untranslated posts.
# If this is set to False, then posts that are not translated to a language
# LANG will not be visible at all in the pages in that language.
-# Formerly known as HIDE_UNTRANSLATED_POSTS (inverse)
# SHOW_UNTRANSLATED_POSTS = True
# Nikola supports logo display. If you have one, you can put the URL here.
@@ -211,77 +278,33 @@ COMPILERS = ${COMPILERS}
# The URL may be relative to the site root.
# LOGO_URL = ''
+# When linking posts to social media, Nikola provides Open Graph metadata
+# which is used to show a nice preview. This includes an image preview
+# taken from the post's previewimage metadata field.
+# This option lets you use an image to be used if the post doesn't have it.
+# The default is None, valid values are URLs or output paths like
+# "/images/foo.jpg"
+# DEFAULT_PREVIEW_IMAGE = None
+
# If you want to hide the title of your website (for example, if your logo
# already contains the text), set this to False.
# SHOW_BLOG_TITLE = True
-# Writes tag cloud data in form of tag_cloud_data.json.
-# Warning: this option will change its default value to False in v8!
-WRITE_TAG_CLOUD = True
-
-# Generate pages for each section. The site must have at least two sections
-# for this option to take effect. It wouldn't build for just one section.
-POSTS_SECTIONS = True
-
-# Setting this to False generates a list page instead of an index. Indexes
-# are the default and will apply GENERATE_ATOM if set.
-# POSTS_SECTIONS_ARE_INDEXES = True
-
-# Each post and section page will have an associated color that can be used
-# to style them with a recognizable color detail across your site. A color
-# is assigned to each section based on shifting the hue of your THEME_COLOR
-# at least 7.5 % while leaving the lightness and saturation untouched in the
-# HUSL colorspace. You can overwrite colors by assigning them colors in HEX.
-# POSTS_SECTION_COLORS = {
-# DEFAULT_LANG: {
-# 'posts': '#49b11bf',
-# 'reviews': '#ffe200',
-# },
-# }
-
-# Associate a description with a section. For use in meta description on
-# section index pages or elsewhere in themes.
-# POSTS_SECTION_DESCRIPTIONS = {
-# DEFAULT_LANG: {
-# 'how-to': 'Learn how-to things properly with these amazing tutorials.',
-# },
-# }
-
-# Sections are determined by their output directory as set in POSTS by default,
-# but can alternatively be determined from file metadata instead.
-# POSTS_SECTION_FROM_META = False
-
-# Names are determined from the output directory name automatically or the
-# metadata label. Unless overwritten below, names will use title cased and
-# hyphens replaced by spaces.
-# POSTS_SECTION_NAME = {
-# DEFAULT_LANG: {
-# 'posts': 'Blog Posts',
-# 'uncategorized': 'Odds and Ends',
-# },
-# }
-
-# Titles for per-section index pages. Can be either one string where "{name}"
-# is substituted or the POSTS_SECTION_NAME, or a dict of sections. Note
-# that the INDEX_PAGES option is also applied to section page titles.
-# POSTS_SECTION_TITLE = {
-# DEFAULT_LANG: {
-# 'how-to': 'How-to and Tutorials',
-# },
-# }
-
# Paths for different autogenerated bits. These are combined with the
# translation paths.
# Final locations are:
# output / TRANSLATION[lang] / TAG_PATH / index.html (list of tags)
# output / TRANSLATION[lang] / TAG_PATH / tag.html (list of posts for a tag)
-# output / TRANSLATION[lang] / TAG_PATH / tag.xml (RSS feed for a tag)
+# output / TRANSLATION[lang] / TAG_PATH / tag RSS_EXTENSION (RSS feed for a tag)
# (translatable)
# TAG_PATH = "categories"
-# See TAG_PATH's "list of tags" for the default setting value. Can be overwritten
-# here any path relative to the output directory.
+# By default, the list of tags is stored in
+# output / TRANSLATION[lang] / TAG_PATH / index.html
+# (see explanation for TAG_PATH). This location can be changed to
+# output / TRANSLATION[lang] / TAGS_INDEX_PATH
+# with an arbitrary relative path TAGS_INDEX_PATH.
# (translatable)
# TAGS_INDEX_PATH = "tags.html"
@@ -292,15 +315,15 @@ POSTS_SECTIONS = True
# Set descriptions for tag pages to make them more interesting. The
# default is no description. The value is used in the meta description
# and displayed underneath the tag list or index page’s title.
-# TAG_PAGES_DESCRIPTIONS = {
+# TAG_DESCRIPTIONS = {
# DEFAULT_LANG: {
-# "blogging": "Meta-blog posts about blogging about blogging.",
+# "blogging": "Meta-blog posts about blogging.",
# "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects."
# },
# }
# Set special titles for tag pages. The default is "Posts about TAG".
-# TAG_PAGES_TITLES = {
+# TAG_TITLES = {
# DEFAULT_LANG: {
# "blogging": "Meta-posts about blogging",
# "open source": "Posts about open source software"
@@ -308,7 +331,7 @@ POSTS_SECTIONS = True
# }
# If you do not want to display a tag publicly, you can mark it as hidden.
-# The tag will not be displayed on the tag list page, the tag cloud and posts.
+# The tag will not be displayed on the tag list page and posts.
# Tag pages will still be generated.
HIDDEN_TAGS = ['mathjax']
@@ -318,14 +341,36 @@ HIDDEN_TAGS = ['mathjax']
# However, more obscure tags can be hidden from the tag index page.
# TAGLIST_MINIMUM_POSTS = 1
+# A list of dictionaries specifying tags which translate to each other.
+# Format: a list of dicts {language: translation, language2: translation2, …}
+# For example:
+# [
+# {'en': 'private', 'de': 'Privat'},
+# {'en': 'work', 'fr': 'travail', 'de': 'Arbeit'},
+# ]
+# TAG_TRANSLATIONS = []
+
+# If set to True, a tag in a language will be treated as a translation
+# of the literally same tag in all other languages. Enable this if you
+# do not translate tags, for example.
+# TAG_TRANSLATIONS_ADD_DEFAULTS = True
+
# Final locations are:
# output / TRANSLATION[lang] / CATEGORY_PATH / index.html (list of categories)
# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.html (list of posts for a category)
-# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category.xml (RSS feed for a category)
+# output / TRANSLATION[lang] / CATEGORY_PATH / CATEGORY_PREFIX category RSS_EXTENSION (RSS feed for a category)
# (translatable)
# CATEGORY_PATH = "categories"
# CATEGORY_PREFIX = "cat_"
+# By default, the list of categories is stored in
+# output / TRANSLATION[lang] / CATEGORY_PATH / index.html
+# (see explanation for CATEGORY_PATH). This location can be changed to
+# output / TRANSLATION[lang] / CATEGORIES_INDEX_PATH
+# with an arbitrary relative path CATEGORIES_INDEX_PATH.
+# (translatable)
+# CATEGORIES_INDEX_PATH = "categories.html"
+
# If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in
# hierarchies. For a post, the whole path in the hierarchy must be specified,
# using a forward slash ('/') to separate paths. Use a backslash ('\') to escape
@@ -343,15 +388,15 @@ CATEGORY_OUTPUT_FLAT_HIERARCHY = ${CATEGORY_OUTPUT_FLAT_HIERARCHY}
# Set descriptions for category pages to make them more interesting. The
# default is no description. The value is used in the meta description
# and displayed underneath the category list or index page’s title.
-# CATEGORY_PAGES_DESCRIPTIONS = {
+# CATEGORY_DESCRIPTIONS = {
# DEFAULT_LANG: {
-# "blogging": "Meta-blog posts about blogging about blogging.",
+# "blogging": "Meta-blog posts about blogging.",
# "open source": "My contributions to my many, varied, ever-changing, and eternal libre software projects."
# },
# }
# Set special titles for category pages. The default is "Posts about CATEGORY".
-# CATEGORY_PAGES_TITLES = {
+# CATEGORY_TITLES = {
# DEFAULT_LANG: {
# "blogging": "Meta-posts about blogging",
# "open source": "Posts about open source software"
@@ -363,14 +408,56 @@ CATEGORY_OUTPUT_FLAT_HIERARCHY = ${CATEGORY_OUTPUT_FLAT_HIERARCHY}
# Category pages will still be generated.
HIDDEN_CATEGORIES = []
+# A list of dictionaries specifying categories which translate to each other.
+# Format: a list of dicts {language: translation, language2: translation2, …}
+# See TAG_TRANSLATIONS example above.
+# CATEGORY_TRANSLATIONS = []
+
+# If set to True, a category in a language will be treated as a translation
+# of the literally same category in all other languages. Enable this if you
+# do not translate categories, for example.
+# CATEGORY_TRANSLATIONS_ADD_DEFAULTS = True
+
+# If no category is specified in a post, the destination path of the post
+# can be used in its place. This replaces the sections feature. Using
+# category hierarchies is recommended.
+# CATEGORY_DESTPATH_AS_DEFAULT = False
+
+# If True, the prefix will be trimmed from the category name, eg. if the
+# POSTS destination is "foo/bar", and the path is "foo/bar/baz/quux",
+# the category will be "baz/quux" (or "baz" if only the first directory is considered).
+# Note that prefixes coming from translations are always ignored.
+# CATEGORY_DESTPATH_TRIM_PREFIX = False
+
+# If True, only the first directory of a path will be used.
+# CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY = True
+
+# Map paths to prettier category names. (translatable)
+# CATEGORY_DESTPATH_NAMES = {
+# DEFAULT_LANG: {
+# 'webdev': 'Web Development',
+# 'webdev/django': 'Web Development/Django',
+# 'random': 'Odds and Ends',
+# },
+# }
+
+# By default, category indexes will appear in CATEGORY_PATH and use
+# CATEGORY_PREFIX. If this is enabled, those settings will be ignored (except
+# for the index) and instead, they will follow destination paths (eg. category
+# 'foo' might appear in 'posts/foo'). If the category does not come from a
+# destpath, first entry in POSTS followed by the category name will be used.
+# For this setting, category hierarchies are required and cannot be flattened.
+# CATEGORY_PAGES_FOLLOW_DESTPATH = False
+
# If ENABLE_AUTHOR_PAGES is set to True and there is more than one
# author, author pages are generated.
# ENABLE_AUTHOR_PAGES = True
-# Final locations are:
-# output / TRANSLATION[lang] / AUTHOR_PATH / index.html (list of tags)
-# output / TRANSLATION[lang] / AUTHOR_PATH / author.html (list of posts for a tag)
-# output / TRANSLATION[lang] / AUTHOR_PATH / author.xml (RSS feed for a tag)
+# Path to author pages. Final locations are:
+# output / TRANSLATION[lang] / AUTHOR_PATH / index.html (list of authors)
+# output / TRANSLATION[lang] / AUTHOR_PATH / author.html (list of posts by an author)
+# output / TRANSLATION[lang] / AUTHOR_PATH / author RSS_EXTENSION (RSS feed for an author)
+# (translatable)
# AUTHOR_PATH = "authors"
# If AUTHOR_PAGES_ARE_INDEXES is set to True, each author's page will contain
@@ -393,8 +480,12 @@ HIDDEN_CATEGORIES = []
# Tag pages will still be generated.
HIDDEN_AUTHORS = ['Guest']
+# Allow multiple, comma-separated authors for a post? (Requires theme support, present in built-in themes)
+# MULTIPLE_AUTHORS_PER_POST = False
+
# Final location for the main blog page and sibling paginated pages is
# output / TRANSLATION[lang] / INDEX_PATH / index-*.html
+# (translatable)
# INDEX_PATH = ""
# Optional HTML that displayed on “main” blog index.html files.
@@ -412,11 +503,14 @@ FRONT_INDEX_HEADER = {
# CREATE_FULL_ARCHIVES = False
# If monthly archives or full archives are created, adds also one archive per day
# CREATE_DAILY_ARCHIVE = False
+# Create previous, up, next navigation links for archives
+# CREATE_ARCHIVE_NAVIGATION = False
# Final locations for the archives are:
# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME
# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / index.html
# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / DAY / index.html
+# (translatable)
# ARCHIVE_PATH = ""
# ARCHIVE_FILENAME = "archive.html"
@@ -431,20 +525,30 @@ FRONT_INDEX_HEADER = {
# absolute: a complete URL (that includes the SITE_URL)
# URL_TYPE = 'rel_path'
-# If USE_BASE_TAG is True, then all HTML files will include
-# something like <base href=http://foo.var.com/baz/bat> to help
-# the browser resolve relative links.
-# Most people don’t need this tag; major websites don’t use it. Use
-# only if you know what you’re doing. If this is True, your website
-# will not be fully usable by manually opening .html files in your web
-# browser (`nikola serve` or `nikola auto` is mandatory). Also, if you
-# have mirrors of your site, they will point to SITE_URL everywhere.
-USE_BASE_TAG = False
+# Extension for RSS feed files
+# RSS_EXTENSION = ".xml"
+
+# RSS filename base (without extension); used for indexes and galleries.
+# (translatable)
+# RSS_FILENAME_BASE = "rss"
# Final location for the blog main RSS feed is:
-# output / TRANSLATION[lang] / RSS_PATH / rss.xml
+# output / TRANSLATION[lang] / RSS_PATH / RSS_FILENAME_BASE RSS_EXTENSION
+# (translatable)
# RSS_PATH = ""
+# Final location for the blog main Atom feed is:
+# output / TRANSLATION[lang] / ATOM_PATH / ATOM_FILENAME_BASE ATOM_EXTENSION
+# (translatable)
+# ATOM_PATH = ""
+
+# Atom filename base (without extension); used for indexes.
+# (translatable)
+ATOM_FILENAME_BASE = "feed"
+
+# Extension for Atom feed files
+# ATOM_EXTENSION = ".atom"
+
# Slug the Tag URL. Easier for users to type, special characters are
# often removed or replaced as well.
# SLUG_TAG_PATH = True
@@ -531,6 +635,35 @@ GITHUB_COMMIT_SOURCE = True
# ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
# }
+# Executable for the "yui_compressor" filter (defaults to 'yui-compressor').
+# YUI_COMPRESSOR_EXECUTABLE = 'yui-compressor'
+
+# Executable for the "closure_compiler" filter (defaults to 'closure-compiler').
+# CLOSURE_COMPILER_EXECUTABLE = 'closure-compiler'
+
+# Executable for the "optipng" filter (defaults to 'optipng').
+# OPTIPNG_EXECUTABLE = 'optipng'
+
+# Executable for the "jpegoptim" filter (defaults to 'jpegoptim').
+# JPEGOPTIM_EXECUTABLE = 'jpegoptim'
+
+# Executable for the "html_tidy_withconfig", "html_tidy_nowrap",
+# "html_tidy_wrap", "html_tidy_wrap_attr" and "html_tidy_mini" filters
+# (defaults to 'tidy5').
+# HTML_TIDY_EXECUTABLE = 'tidy5'
+
+# List of XPath expressions which should be used for finding headers
+# ({hx} is replaced by headers h1 through h6).
+# You must change this if you use a custom theme that does not use
+# "e-content entry-content" as a class for post and page contents.
+# HEADER_PERMALINKS_XPATH_LIST = ['*//div[@class="e-content entry-content"]//{hx}']
+# Include *every* header (not recommended):
+# HEADER_PERMALINKS_XPATH_LIST = ['*//{hx}']
+
+# File blacklist for header permalinks. Contains output path
+# (eg. 'output/index.html')
+# HEADER_PERMALINKS_FILE_BLACKLIST = []
+
# Expert setting! Create a gzipped copy of each generated file. Cheap server-
# side optimization for very high traffic sites or low memory servers.
# GZIP_FILES = False
@@ -544,20 +677,6 @@ GITHUB_COMMIT_SOURCE = True
# return partial content of another representation for these resources. Do not
# use this feature if you do not understand what this means.
-# Compiler to process LESS files.
-# LESS_COMPILER = 'lessc'
-
-# A list of options to pass to the LESS compiler.
-# Final command is: LESS_COMPILER LESS_OPTIONS file.less
-# LESS_OPTIONS = []
-
-# Compiler to process Sass files.
-# SASS_COMPILER = 'sass'
-
-# A list of options to pass to the Sass compiler.
-# Final command is: SASS_COMPILER SASS_OPTIONS file.s(a|c)ss
-# SASS_OPTIONS = []
-
# #############################################################################
# Image Gallery Options
# #############################################################################
@@ -573,7 +692,16 @@ GITHUB_COMMIT_SOURCE = True
# MAX_IMAGE_SIZE = 1280
# USE_FILENAME_AS_TITLE = True
# EXTRA_IMAGE_EXTENSIONS = []
-#
+
+# Use a thumbnail (defined by ".. previewimage:" in the gallery's index) in
+# list of galleries for each gallery
+GALLERIES_USE_THUMBNAIL = False
+
+# Image to use as thumbnail for those galleries that don't have one
+# None: show a grey square
+# '/url/to/file': show the image in that url
+GALLERIES_DEFAULT_THUMBNAIL = None
+
# If set to False, it will sort by filename instead. Defaults to True
# GALLERY_SORT_BY_DATE = True
@@ -613,6 +741,10 @@ GITHUB_COMMIT_SOURCE = True
# Embedded thumbnail information:
# EXIF_WHITELIST['1st'] = ["*"]
+# If set to True, any ICC profile will be copied when an image is thumbnailed or
+# resized.
+# PRESERVE_ICC_PROFILES = False
+
# Folders containing images to be used in normal posts or pages.
# IMAGE_FOLDERS is a dictionary of the form {"source": "destination"},
# where "source" is the folder containing the images to be published, and
@@ -622,17 +754,19 @@ GITHUB_COMMIT_SOURCE = True
# To reference the images in your posts, include a leading slash in the path.
# For example, if IMAGE_FOLDERS = {'images': 'images'}, write
#
-# ..image:: /images/tesla.jpg
+# .. image:: /images/tesla.jpg
#
# See the Nikola Handbook for details (in the “Embedding Images” and
# “Thumbnails” sections)
# Images will be scaled down according to IMAGE_THUMBNAIL_SIZE and MAX_IMAGE_SIZE
# options, but will have to be referenced manually to be visible on the site
-# (the thumbnail has ``.thumbnail`` added before the file extension).
+# (the thumbnail has ``.thumbnail`` added before the file extension by default,
+# but a different naming template can be configured with IMAGE_THUMBNAIL_FORMAT).
IMAGE_FOLDERS = {'images': 'images'}
# IMAGE_THUMBNAIL_SIZE = 400
+# IMAGE_THUMBNAIL_FORMAT = '{name}.thumbnail{ext}'
# #############################################################################
# HTML fragments and diverse things that are used by the templates
@@ -680,49 +814,28 @@ IMAGE_FOLDERS = {'images': 'images'}
# for the full URL with the page number of the main page to the normal (shorter) main
# page URL.
# INDEXES_PRETTY_PAGE_URL = False
+#
+# If the following is true, a page range navigation will be inserted to indices.
+# Please note that this will undo the effect of INDEXES_STATIC, as all index pages
+# must be recreated whenever the number of pages changes.
+# SHOW_INDEX_PAGE_NAVIGATION = False
+
+# If the following is True, a meta name="generator" tag is added to pages. The
+# generator tag is used to specify the software used to generate the page
+# (it promotes Nikola).
+# META_GENERATOR_TAG = True
# Color scheme to be used for code blocks. If your theme provides
-# "assets/css/code.css" this is ignored.
+# "assets/css/code.css" this is ignored. Set to None to disable.
# Can be any of:
-# algol
-# algol_nu
-# arduino
-# autumn
-# borland
-# bw
-# colorful
-# default
-# emacs
-# friendly
-# fruity
-# igor
-# lovelace
-# manni
-# monokai
-# murphy
-# native
-# paraiso_dark
-# paraiso_light
-# pastie
-# perldoc
-# rrt
-# tango
-# trac
-# vim
-# vs
-# xcode
+# algol, algol_nu, autumn, borland, bw, colorful, default, emacs, friendly,
+# fruity, igor, lovelace, manni, monokai, murphy, native, paraiso-dark,
+# paraiso-light, pastie, perldoc, rrt, tango, trac, vim, vs, xcode
# This list MAY be incomplete since pygments adds styles every now and then.
+# Check with list(pygments.styles.get_all_styles()) in an interpreter.
+#
# CODE_COLOR_SCHEME = 'default'
-# If you use 'site-reveal' theme you can select several subthemes
-# THEME_REVEAL_CONFIG_SUBTHEME = 'sky'
-# You can also use: beige/serif/simple/night/default
-
-# Again, if you use 'site-reveal' theme you can select several transitions
-# between the slides
-# THEME_REVEAL_CONFIG_TRANSITION = 'cube'
-# You can also use: page/concave/linear/none/default
-
# FAVICONS contains (name, file, size) tuples.
# Used to create favicon link like this:
# <link rel="name" href="file" sizes="size"/>
@@ -743,6 +856,7 @@ IMAGE_FOLDERS = {'images': 'images'}
# {min_remaining_read} The string “{remaining_reading_time} min remaining to read” in the current language.
# {paragraph_count} The amount of paragraphs in the post.
# {remaining_paragraph_count} The amount of paragraphs in the post, sans the teaser.
+# {post_title} The title of the post.
# {{ A literal { (U+007B LEFT CURLY BRACKET)
# }} A literal } (U+007D RIGHT CURLY BRACKET)
@@ -786,6 +900,8 @@ CONTENT_FOOTER = 'Contents &copy; {date} \
# tuples of tuples of positional arguments and dicts of keyword arguments
# to format(). For example, {'en': (('Hello'), {'target': 'World'})}
# results in CONTENT_FOOTER['en'].format('Hello', target='World').
+# If you need to use the literal braces '{' and '}' in your footer text, use
+# '{{' and '}}' to escape them (str.format is used)
# WARNING: If you do not use multiple languages with CONTENT_FOOTER, this
# still needs to be a dict of this format. (it can be empty if you
# do not need formatting)
@@ -802,6 +918,12 @@ CONTENT_FOOTER_FORMATS = {
)
}
+# A simple copyright tag for inclusion in RSS feeds that works just
+# like CONTENT_FOOTER and CONTENT_FOOTER_FORMATS
+RSS_COPYRIGHT = 'Contents © {date} <a href="mailto:{email}">{author}</a> {license}'
+RSS_COPYRIGHT_PLAIN = 'Contents © {date} {author} {license}'
+RSS_COPYRIGHT_FORMATS = CONTENT_FOOTER_FORMATS
+
# To use comments, you can choose between different third party comment
# systems. The following comment systems are supported by Nikola:
${_SUPPORTED_COMMENT_SYSTEMS}
@@ -813,13 +935,6 @@ COMMENT_SYSTEM = ${COMMENT_SYSTEM}
# is in the manual.
COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
-# Enable annotations using annotateit.org?
-# If set to False, you can still enable them for individual posts and pages
-# setting the "annotations" metadata.
-# If set to True, you can disable them for individual posts and pages using
-# the "noannotations" metadata.
-# ANNOTATIONS = False
-
# Create index.html for page folders?
# WARNING: if a page would conflict with the index file (usually
# caused by setting slug to `index`), the PAGE_INDEX
@@ -839,17 +954,8 @@ COMMENT_SYSTEM_ID = ${COMMENT_SYSTEM_ID}
# http://mysite/foo/bar/index.html => http://mysite/foo/bar/
# (Uses the INDEX_FILE setting, so if that is, say, default.html,
# it will instead /foo/default.html => /foo)
-# (Note: This was briefly STRIP_INDEX_HTML in v 5.4.3 and 5.4.4)
STRIP_INDEXES = ${STRIP_INDEXES}
-# Should the sitemap list directories which only include other directories
-# and no files.
-# Default to True
-# If this is False
-# e.g. /2012 includes only /01, /02, /03, /04, ...: don't add it to the sitemap
-# if /2012 includes any files (including index.html)... add it to the sitemap
-# SITEMAP_INCLUDE_FILELESS_DIRS = True
-
# List of files relative to the server root (!) that will be asked to be excluded
# from indexing and other robotic spidering. * is supported. Will only be effective
# if SITE_URL points to server root. The list is used to exclude resources from
@@ -877,13 +983,14 @@ PRETTY_URLS = ${PRETTY_URLS}
# Allows scheduling of posts using the rule specified here (new_post -s)
# Specify an iCal Recurrence Rule: http://www.kanzaki.com/docs/ical/rrule.html
# SCHEDULE_RULE = ''
-# If True, use the scheduling rule to all posts by default
+# If True, use the scheduling rule to all posts (not pages!) by default
# SCHEDULE_ALL = False
-# Do you want a add a Mathjax config file?
+# Do you want to add a Mathjax config file?
# MATHJAX_CONFIG = ""
-# If you are using the compile-ipynb plugin, just add this one:
+# If you want support for the $.$ syntax (which may conflict with running
+# text!), just use this config:
# MATHJAX_CONFIG = """
# <script type="text/x-mathjax-config">
# MathJax.Hub.Config({
@@ -892,7 +999,7 @@ PRETTY_URLS = ${PRETTY_URLS}
# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ],
# processEscapes: true
# },
-# displayAlign: 'left', // Change this to 'center' to center equations.
+# displayAlign: 'center', // Change this to 'left' if you want left-aligned equations.
# "HTML-CSS": {
# styles: {'.MathJax_Display': {"margin": 0}}
# }
@@ -900,21 +1007,19 @@ PRETTY_URLS = ${PRETTY_URLS}
# </script>
# """
-# Want to use KaTeX instead of MathJax? While KaTeX is less featureful,
-# it's faster and the output looks better.
-# If you set USE_KATEX to True, you also need to add an extra CSS file
-# like this:
-# EXTRA_HEAD_DATA = """<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.css">"""
+# Want to use KaTeX instead of MathJax? While KaTeX may not support every
+# feature yet, it's faster and the output looks better.
# USE_KATEX = False
-# If you want to use the old (buggy) inline math $.$ with KaTeX, then
-# you might want to use this feature.
+# KaTeX auto-render settings. If you want support for the $.$ syntax (which may
+# conflict with running text!), just use this config:
# KATEX_AUTO_RENDER = """
# delimiters: [
# {left: "$$", right: "$$", display: true},
-# {left: "\\\[", right: "\\\]", display: true},
+# {left: "\\\\[", right: "\\\\]", display: true},
+# {left: "\\\\begin{equation*}", right: "\\\\end{equation*}", display: true},
# {left: "$", right: "$", display: false},
-# {left: "\\\(", right: "\\\)", display: false}
+# {left: "\\\\(", right: "\\\\)", display: false}
# ]
# """
@@ -922,17 +1027,23 @@ PRETTY_URLS = ${PRETTY_URLS}
# IPYNB_CONFIG = {}
# With the following example configuration you can use a custom jinja template
# called `toggle.tpl` which has to be located in your site/blog main folder:
-# IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}}
+# IPYNB_CONFIG = {'Exporter': {'template_file': 'toggle'}}
# What Markdown extensions to enable?
# You will also get gist, nikola and podcast because those are
# done in the code, hope you don't mind ;-)
# Note: most Nikola-specific extensions are done via the Nikola plugin system,
# with the MarkdownExtension class and should not be added here.
-# The default is ['fenced_code', 'codehilite']
-MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra']
+# Defaults are markdown.extensions.(fenced_code|codehilite|extra)
+# markdown.extensions.meta is required for Markdown metadata.
+MARKDOWN_EXTENSIONS = ['markdown.extensions.fenced_code', 'markdown.extensions.codehilite', 'markdown.extensions.extra']
-# Extra options to pass to the pandoc comand.
+# Options to be passed to markdown extensions (See https://python-markdown.github.io/reference/)
+# Default is {} (no config at all)
+# MARKDOWN_EXTENSION_CONFIGS = {}
+
+
+# Extra options to pass to the pandoc command.
# by default, it's empty, is a list of strings, for example
# ['-F', 'pandoc-citeproc', '--bibliography=/Users/foo/references.bib']
# Pandoc does not demote headers by default. To enable this, you can use, for example
@@ -958,7 +1069,6 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra']
# """
# Show link to source for the posts?
-# Formerly known as HIDE_SOURCELINK (inverse)
# SHOW_SOURCELINK = True
# Copy the source files for your pages?
# Setting it to False implies SHOW_SOURCELINK = False
@@ -981,22 +1091,16 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra']
# between each other. Old Atom feeds with no changes are marked as archived.
# GENERATE_ATOM = False
-# Only inlclude teasers in Atom and RSS feeds. Disabling include the full
+# Only include teasers in Atom and RSS feeds. Disabling include the full
# content. Defaults to True.
# FEED_TEASERS = True
-# Strip HTML from Atom annd RSS feed summaries and content. Defaults to False.
+# Strip HTML from Atom and RSS feed summaries and content. Defaults to False.
# FEED_PLAIN = False
# Number of posts in Atom and RSS feeds.
# FEED_LENGTH = 10
-# Include preview image as a <figure><img></figure> at the top of the entry.
-# Requires FEED_PLAIN = False. If the preview image is found in the content,
-# it will not be included again. Image will be included as-is, aim to optmize
-# the image source for Feedly, Apple News, Flipboard, and other popular clients.
-# FEED_PREVIEWIMAGE = True
-
# RSS_LINK is a HTML fragment to link the RSS or Atom feeds. If set to None,
# the base.tmpl will use the feed Nikola generates. However, you may want to
# change it for a FeedBurner feed or something else.
@@ -1081,26 +1185,52 @@ MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite', 'extra']
# (Note the '.*\/' in the beginning -- matches source paths relative to conf.py)
# FILE_METADATA_REGEXP = None
-# If you hate "Filenames with Capital Letters and Spaces.md", you should
-# set this to true.
-UNSLUGIFY_TITLES = True
+# Should titles fetched from file metadata be unslugified (made prettier?)
+# FILE_METADATA_UNSLUGIFY_TITLES = True
+
+# If enabled, extract metadata from docinfo fields in reST documents.
+# If your text files start with a level 1 heading, it will be treated as the
+# document title and will be removed from the text.
+# USE_REST_DOCINFO_METADATA = False
+
+# If enabled, hide docinfo fields in reST document output
+# HIDE_REST_DOCINFO = False
+
+# Map metadata from other formats to Nikola names.
+# Supported formats: ${_METADATA_MAPPING_FORMATS}
+# METADATA_MAPPING = {}
+#
+# Example for Pelican compatibility:
+# METADATA_MAPPING = {
+# "rest_docinfo": {"summary": "description", "modified": "updated"},
+# "markdown_metadata": {"summary": "description", "modified": "updated"}
+# }
+# Other examples: https://getnikola.com/handbook.html#mapping-metadata-from-other-formats
+
+# Map metadata between types/values. (Runs after METADATA_MAPPING.)
+# Supported formats: nikola, ${_METADATA_MAPPING_FORMATS}
+# The value on the right should be a dict of callables.
+# METADATA_VALUE_MAPPING = {}
+# Examples:
+# METADATA_VALUE_MAPPING = {
+# "yaml": {"keywords": lambda value: ', '.join(value)}, # yaml: 'keywords' list -> str
+# "nikola": {
+# "widgets": lambda value: value.split(', '), # nikola: 'widgets' comma-separated string -> list
+# "tags": str.lower # nikola: force lowercase 'tags' (input would be string)
+# }
+# }
+
+# Add any post types here that you want to be displayed without a title.
+# If your theme supports it, the titles will not be shown.
+# TYPES_TO_HIDE_TITLE = []
# Additional metadata that is added to a post when creating a new_post
# ADDITIONAL_METADATA = {}
-# Nikola supports Open Graph Protocol data for enhancing link sharing and
-# discoverability of your site on Facebook, Google+, and other services.
-# Open Graph is enabled by default.
-# USE_OPEN_GRAPH = True
-
# Nikola supports Twitter Card summaries, but they are disabled by default.
# They make it possible for you to attach media to Tweets that link
# to your content.
#
-# IMPORTANT:
-# Please note, that you need to opt-in for using Twitter Cards!
-# To do this please visit https://cards-dev.twitter.com/validator
-#
# Uncomment and modify to following lines to match your accounts.
# Images displayed come from the `previewimage` meta tag.
# You can specify the card type by using the `card` parameter in TWITTER_CARD.
@@ -1112,14 +1242,20 @@ UNSLUGIFY_TITLES = True
# # 'creator': '@username', # Username for the content creator / author.
# }
-# If webassets is installed, bundle JS and CSS into single files to make
-# site loading faster in a HTTP/1.1 environment but is not recommended for
-# HTTP/2.0 when caching is used. Defaults to True.
+# Bundle JS and CSS into single files to make site loading faster in a HTTP/1.1
+# environment but is not recommended for HTTP/2.0 when caching is used.
+# Defaults to True.
# USE_BUNDLES = True
# Plugins you don't want to use. Be careful :-)
# DISABLED_PLUGINS = ["render_galleries"]
+# Special settings to disable only parts of the indexes plugin.
+# Use with care.
+# DISABLE_INDEXES = False
+# DISABLE_MAIN_ATOM_FEED = False
+# DISABLE_MAIN_RSS_FEED = False
+
# Add the absolute paths to directories containing plugins to use them.
# For example, the `plugins` directory of your clone of the Nikola plugins
# repository.
@@ -1147,18 +1283,21 @@ UNSLUGIFY_TITLES = True
# (defaults to 1.)
# DEMOTE_HEADERS = 1
-# Docutils, by default, will perform a transform in your documents
-# extracting unique titles at the top of your document and turning
-# them into metadata. This surprises a lot of people, and setting
-# this option to True will prevent it.
-# NO_DOCUTILS_TITLE_TRANSFORM = False
-
# If you don’t like slugified file names ([a-z0-9] and a literal dash),
# and would prefer to use all the characters your file system allows.
# USE WITH CARE! This is also not guaranteed to be perfect, and may
# sometimes crash Nikola, your web server, or eat your cat.
# USE_SLUGIFY = True
+# If set to True, the tags 'draft', 'mathjax' and 'private' have special
+# meaning. If set to False, these tags are handled like regular tags.
+USE_TAG_METADATA = False
+
+# If set to True, a warning is issued if one of the 'draft', 'mathjax'
+# and 'private' tags are found in a post. Useful for checking that
+# migration was successful.
+WARN_ABOUT_TAG_METADATA = False
+
# Templates will use those filters, along with the defaults.
# Consult your engine's documentation on filters if you need help defining
# those.
diff --git a/nikola/data/samplesite/galleries/demo/metadata.sample.yml b/nikola/data/samplesite/galleries/demo/metadata.sample.yml
new file mode 100644
index 0000000..f504573
--- /dev/null
+++ b/nikola/data/samplesite/galleries/demo/metadata.sample.yml
@@ -0,0 +1,13 @@
+---
+name: tesla_tower1_lg.jpg
+caption: Wardenclyffe Tower
+built_in: 1904
+order: 2
+---
+name: tesla4_lg.jpg
+order: 0
+---
+name: tesla_conducts_lg.jpg
+caption: Nikola Tesla conducts electricity
+order: 1
+---
diff --git a/nikola/data/samplesite/listings/hello.py b/nikola/data/samplesite/listings/hello.py
index 885acde..5535df8 100644
--- a/nikola/data/samplesite/listings/hello.py
+++ b/nikola/data/samplesite/listings/hello.py
@@ -7,5 +7,6 @@ def hello(name='world'):
greeting = "hello " + name
print(greeting)
+
if __name__ == "__main__":
hello(*sys.argv[1:])
diff --git a/nikola/data/samplesite/pages/bootstrap-demo.rst b/nikola/data/samplesite/pages/bootstrap-demo.rst
index 481140a..35a0265 100644
--- a/nikola/data/samplesite/pages/bootstrap-demo.rst
+++ b/nikola/data/samplesite/pages/bootstrap-demo.rst
@@ -357,7 +357,7 @@
</blockquote>
</div>
<div class="col-lg-6">
- <blockquote class="pull-right">
+ <blockquote class="float-md-right">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
<small>Someone famous in <cite title="Source Title">Source Title</cite></small>
</blockquote>
diff --git a/nikola/data/samplesite/pages/charts.txt b/nikola/data/samplesite/pages/charts.rst
index 72fedb1..72fedb1 100644
--- a/nikola/data/samplesite/pages/charts.txt
+++ b/nikola/data/samplesite/pages/charts.rst
diff --git a/nikola/data/samplesite/pages/creating-a-theme.rst b/nikola/data/samplesite/pages/creating-a-theme.rst
index 108a192..66d75d1 120000
--- a/nikola/data/samplesite/pages/creating-a-theme.rst
+++ b/nikola/data/samplesite/pages/creating-a-theme.rst
@@ -1 +1 @@
-../../../../docs/creating-a-theme.txt \ No newline at end of file
+../../../../docs/creating-a-theme.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst b/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst
index 6175355..9342f11 100644
--- a/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst
+++ b/nikola/data/samplesite/pages/dr-nikolas-vendetta.rst
@@ -1,468 +1,468 @@
-.. title: A BID FOR FORTUNE OR; DR. NIKOLA'S VENDETTA
-.. template: book.tmpl
-.. hyphenate: yes
-.. filters: filters.typogrify
-
-.. class:: subtitle
-
-By `GUY BOOTHBY <http://www.gutenberg.org/ebooks/author/3587>`__
-
-Author of "Dr. Nikola," "The Beautiful White Devil," etc., etc.
-
-.. figure:: /images/frontispiece.jpg
- :class: bookfig
-
-.. topic:: The Project Gutenberg EBook of A Bid for Fortune, by Guy Boothby
-
- This eBook is for the use of anyone anywhere at no cost and with
- almost no restrictions whatsoever. You may copy it, give it away or
- re-use it under the terms of the Project Gutenberg License included
- with this eBook or online at www.gutenberg.org
-
-
- Title: A Bid for Fortune
- or Dr. Nikola's Vendetta
-
- Author: `Guy Boothby <http://www.gutenberg.org/ebooks/author/3587>`__
-
- Release Date: May 29, 2007 [EBook #21640]
-
- Language: English
-
- Produced by Marilynda Fraser-Cunliffe, Mary Meehan and the
- Online Distributed Proofreading Team at http://www.pgdp.net
-
- Originally published by:
-
- WARD, LOCK & CO., LIMITED
- LONDON, MELBOURNE AND TORONTO
- 1918
-
-.. figure:: /images/illus_001.jpg
- :class: bookfig
-
-PART I
-======
-
-PROLOGUE
---------
-
-.. role:: smallcaps
-
-
-:smallcaps:`The` manager of the new Imperial Restaurant on the Thames Embankment went
-into his luxurious private office and shut the door. Having done so, he
-first scratched his chin reflectively, and then took a letter from the
-drawer in which it had reposed for more than two months and perused it
-carefully. Though he was not aware of it, this was the thirtieth time he
-had read it since breakfast that morning. And yet he was not a whit
-nearer understanding it than he had been at the beginning. He turned it
-over and scrutinized the back, where not a sign of writing was to be
-seen; he held it up to the window, as if he might hope to discover
-something from the water-mark; but there was nothing in either of these
-places of a nature calculated to set his troubled mind at rest. Then he
-took a magnificent repeater watch from his waistcoat pocket and glanced
-at the dial; the hands stood at half-past seven. He immediately threw
-the letter on the table, and as he did so his anxiety found relief in
-words.
-
-"It's really the most extraordinary affair I ever had to do with," he
-remarked. "And as I've been in the business just three-and-thirty years
-at eleven a.m. next Monday morning, I ought to know something about it.
-I only hope I've done right, that's all."
-
-As he spoke, the chief bookkeeper, who had the treble advantage of being
-tall, pretty, and just eight-and-twenty years of age, entered the room.
-She noticed the open letter and the look upon her chief's face, and her
-curiosity was proportionately excited.
-
-"You seem worried, Mr. McPherson," she said tenderly, as she put down
-the papers she had brought in for his signature.
-
-"You have just hit it, Miss O'Sullivan," he answered, pushing them
-farther on to the table. "I am worried about many things, but
-particularly about this letter."
-
-He handed the epistle to her, and she, being desirous of impressing him
-with her business capabilities, read it with ostentatious care. But it
-was noticeable that when she reached the signature she too turned back
-to the beginning, and then deliberately read it over again. The manager
-rose, crossed to the mantelpiece, and rang for the head waiter. Having
-relieved his feelings in this way, he seated himself again at his
-writing-table, put on his glasses, and stared at his companion, while
-waiting for her to speak.
-
-"It's very funny," she said. "Very funny indeed!"
-
-"It's the most extraordinary communication I have ever received," he
-replied with conviction. "You see it is written from Cuyaba, Brazil. The
-date is three months ago to a day. Now I have taken the trouble to find
-out where and what Cuyaba is."
-
-He made this confession with an air of conscious pride, and having done
-so, laid himself back in his chair, stuck his thumbs into the armholes
-of his waistcoat, and looked at his fair subordinate for approval. Nor
-was he destined to be disappointed. He was a bachelor in possession of a
-snug income, and she, besides being pretty, was a lady with a keen eye
-to the main chance.
-
-"And where *is* Cuyaba?" she asked humbly.
-
-"Cuyaba," he replied, rolling his tongue with considerable relish round
-his unconscious mispronunciation of the name, "is a town almost on the
-western or Bolivian border of Brazil. It is of moderate size, is
-situated on the banks of the river Cuyaba, and is considerably connected
-with the famous Brazilian Diamond Fields."
-
-"And does the writer of this letter live there?"
-
-"I cannot say. He writes from there--that is enough for us."
-
-"And he orders dinner for four--here, in a private room overlooking the
-river, three months ahead--punctually at eight o'clock, gives you a list
-of the things he wants, and even arranges the decoration of the table.
-Says he has never seen either of his three friends before; that one of
-them hails from (here she consulted the letter again) Hang-chow, another
-from Bloemfontein, while the third resides, at present, in England. Each
-one is to present an ordinary visiting card with a red dot on it to the
-porter in the hall, and to be shown to the room at once. I don't
-understand it at all."
-
-The manager paused for a moment, and then said deliberately,--"Hang-chow
-is in China, Bloemfontein is in South Africa."
-
-"What a wonderful man you are, to be sure, Mr. McPherson! I never can
-*think* how you manage to carry so much in your head."
-
-There spoke the true woman. And it was a move in the right direction,
-for the manager was susceptible to her gentle influence, as she had
-occasion to know.
-
-At this juncture the head waiter appeared upon the scene, and took up a
-position just inside the doorway, as if he were afraid of injuring the
-carpet by coming farther.
-
-"Is No. 22 ready, Williams?"
-
-"Quite ready, sir. The wine is on the ice, and cook tells me he'll be
-ready to dish punctual to the moment."
-
-"The letter says, 'no electric light; candles with red shades.' Have you
-put on those shades I got this morning?"
-
-"Just seen it done this very minute, sir."
-
-"And let me see, there was one other thing." He took the letter from the
-chief bookkeeper's hand and glanced at it. "Ah, yes, a porcelain saucer,
-and a small jug of new milk upon the mantelpiece. An extraordinary
-request, but has it been attended to?"
-
-"I put it there myself, sir."
-
-"Who wait?"
-
-"Jones, Edmunds, Brooks, and Tomkins."
-
-"Very good. Then I think that will do. Stay! You had better tell the
-hall porter to look out for three gentlemen presenting plain visiting
-cards with a little red spot on them. Let Brooks wait in the hall, and
-when they arrive tell him to show them straight up to the room."
-
-"It shall be done, sir."
-
-The head waiter left the room, and the manager stretched himself in his
-chair, yawned by way of showing his importance, and then said
-solemnly,--
-
-"I don't believe they'll any of them turn up; but if they do, this Dr.
-Nikola, whoever he may be, won't be able to find fault with my
-arrangements."
-
-Then, leaving the dusty high road of Business, he and his companion
-wandered in the shady bridle-paths of Love--to the end that when the
-chief bookkeeper returned to her own department she had forgotten the
-strange dinner party about to take place upstairs, and was busily
-engaged upon a calculation as to how she would look in white satin and
-orange blossoms, and, that settled, fell to wondering whether it was
-true, as Miss Joyce, a subordinate, had been heard to declare, that the
-manager had once shown himself partial to a certain widow with reputed
-savings and a share in an extensive egg and dairy business.
-
-At ten minutes to eight precisely a hansom drew up at the steps of the
-hotel. As soon as it stopped, an undersized gentleman, with a clean
-shaven countenance, a canonical corporation, and bow legs, dressed in a
-decidedly clerical garb, alighted. He paid and discharged his cabman,
-and then took from his ticket pocket an ordinary white visiting card,
-which he presented to the gold-laced individual who had opened the
-apron. The latter, having noted the red spot, called a waiter, and the
-reverend gentleman was immediately escorted upstairs.
-
-Hardly had the attendant time to return to his station in the hall,
-before a second cab made its appearance, closely followed by a third.
-Out of the second jumped a tall, active, well-built man of about thirty
-years of age. He was dressed in evening dress of the latest fashion, and
-to conceal it from the vulgar gaze, wore a large Inverness cape of heavy
-texture. He also in his turn handed a white card to the porter, and,
-having done so, proceeded into the hall, followed by the occupant of the
-last cab, who had closely copied his example. This individual was also
-in evening dress, but it was of a different stamp. It was old-fashioned
-and had seen much use. The wearer, too, was taller than the ordinary run
-of men, while it was noticeable that his hair was snow-white, and that
-his face was deeply pitted with smallpox. After disposing of their hats
-and coats in an ante-room, they reached room No. 22, where they found
-the gentleman in clerical costume pacing impatiently up and down.
-
-Left alone, the tallest of the trio, who for want of a better title we
-may call the Best Dressed Man, took out his watch, and having glanced at
-it, looked at his companions. "Gentlemen," he said, with a slight
-American accent, "it is three minutes to eight o'clock. My name is
-Eastover!"
-
-"I'm glad to hear it, for I'm most uncommonly hungry," said the next
-tallest, whom I have already described as being so marked by disease.
-"My name is Prendergast!"
-
-"We only wait for our friend and host," remarked the clerical gentleman,
-as if he felt he ought to take a share in the conversation, and then, as
-an afterthought, he continued, "My name is Baxter!"
-
-They shook hands all round with marked cordiality, seated themselves
-again, and took it in turns to examine the clock.
-
-"Have you ever had the pleasure of meeting our host before?" asked Mr.
-Baxter of Mr. Prendergast.
-
-"Never," replied that gentleman, with a shake of his head. "Perhaps Mr.
-Eastover has been more fortunate?"
-
-"Not I," was the brief rejoinder. "I've had to do with him off and on
-for longer than I care to reckon, but I've never set eyes on him up to
-date."
-
-"And where may he have been the first time you heard from him?"
-
-"In Nashville, Tennessee," said Eastover. "After that, Tahupapa, New
-Zealand; after that, Papeete, in the Society Islands; then Pekin, China.
-And you?"
-
-"First time, Brussels; second, Monte Video; third, Mandalay, and then
-the Gold Coast, Africa. It's your turn, Mr. Baxter."
-
-The clergyman glanced at the timepiece. It was exactly eight o'clock.
-"First time, Cabul, Afghanistan; second, Nijni Novgorod, Russia; third,
-Wilcannia, Darling River, Australia; fourth, Valparaiso, Chili; fifth,
-Nagasaki, Japan."
-
-"He is evidently a great traveller and a most mysterious person."
-
-"He is more than that," said Eastover with conviction; "he is late for
-dinner!"
-
-Prendergast looked at his watch.
-
-"That clock is two minutes fast. Hark, there goes Big Ben! Eight
-exactly."
-
-As he spoke the door was thrown open and a voice announced "Dr. Nikola."
-
-The three men sprang to their feet simultaneously, with exclamations of
-astonishment, as the man they had been discussing made his appearance.
-
-It would take more time than I can spare the subject to give you an
-adequate and inclusive description of the person who entered the room at
-that moment. In stature he was slightly above the ordinary, his
-shoulders were broad, his limbs perfectly shaped and plainly muscular,
-but very slim. His head, which was magnificently set upon his shoulders,
-was adorned with a profusion of glossy black hair; his face was
-destitute of beard or moustache, and was of oval shape and handsome
-moulding; while his skin was of a dark olive hue, a colour which
-harmonized well with his piercing black eyes and pearly teeth. His hands
-and feet were small, and the greatest dandy must have admitted that he
-was irreproachably dressed, with a neatness that bordered on the
-puritanical. In age he might have been anything from eight-and-twenty to
-forty; in reality he was thirty-three. He advanced into the room and
-walked with out-stretched hand directly across to where Eastover was
-standing by the fireplace.
-
-"Mr. Eastover, I feel certain," he said, fixing his glittering eyes upon
-the man he addressed, and allowing a curious smile to play upon his
-face.
-
-"That is my name, Dr. Nikola," the other answered with evident surprise.
-"But how on earth can you distinguish me from your other guests?"
-
-"Ah! it would surprise you if you knew. And Mr. Prendergast, and Mr.
-Baxter. This is delightful; I hope I am not late. We had a collision in
-the Channel this morning, and I was almost afraid I might not be up to
-time. Dinner seems ready; shall we sit down to it?" They seated
-themselves, and the meal commenced. The Imperial Restaurant has earned
-an enviable reputation for doing things well, and the dinner that night
-did not in any way detract from its lustre. But, delightful as it all
-was, it was noticeable that the three guests paid more attention to
-their host than to his excellent *menu*. As they had said before his
-arrival, they had all had dealings with him for several years, but what
-those dealings were they were careful not to describe. It was more than
-possible that they hardly liked to remember them themselves.
-
-When coffee had been served and the servants had withdrawn, Dr. Nikola
-rose from the table, and went across to the massive sideboard. On it
-stood a basket of very curious shape and workmanship. This he opened,
-and as he did so, to the astonishment of his guests, an enormous cat, as
-black as his master's coat, leaped out on to the floor. The reason for
-the saucer and jug of milk became evident.
-
-Seating himself at the table again, the host followed the example of his
-guests and lit a cigar, blowing a cloud of smoke luxuriously through his
-delicately chiselled nostrils. His eyes wandered round the cornice of
-the room, took in the pictures and decorations, and then came down to
-meet the faces of his companions. As they did so, the black cat, having
-finished its meal, sprang on to his shoulder to crouch there, watching
-the three men through the curling smoke drift with its green blinking,
-fiendish eyes. Dr. Nikola smiled as he noticed the effect the animal had
-upon his guests.
-
-"Now shall we get to business?" he said briskly.
-
-The others almost simultaneously knocked the ashes off their cigars and
-brought themselves to attention. Dr. Nikola's dainty, languid manner
-seemed to drop from him like a cloak, his eyes brightened, and his
-voice, when he spoke, was clean cut as chiselled silver.
-
-"You are doubtless anxious to be informed why I summoned you from all
-parts of the globe to meet me here to-night? And it is very natural you
-should be. But then, from what you know of me, you should not be
-surprised at anything I do."
-
-His voice dropped back into its old tone of gentle languor. He drew in a
-great breath of smoke and then sent it slowly out from his lips again.
-His eyes were half closed, and he drummed with one finger on the table
-edge. The cat looked through the smoke at the three men, and it seemed
-to them that he grew every moment larger and more ferocious. Presently
-his owner took him from his perch, and seating him on his knee fell to
-stroking his fur, from head to tail, with his long slim fingers. It was
-as if he were drawing inspiration for some deadly mischief from the
-uncanny beast.
-
-"To preface what I have to say to you, let me tell you that this is by
-far the most important business for which I have ever required your
-help. (Three slow strokes down the centre of the back, and one round
-each ear.) When it first came into my mind I was at a loss who to trust
-in the matter. I thought of Vendon, but I found Vendon was dead. I
-thought of Brownlow, but Brownlow was no longer faithful. (Two strokes
-down the back and two on the throat.) Then bit by bit I remembered you.
-I was in Brazil at the time. So I sent for you. You came. So far so
-good."
-
-He rose, and crossed over to the fireplace. As he went the cat crawled
-back to its original position on his shoulder. Then his voice changed
-once more to its former business-like tone.
-
-"I am not going to tell you very much about it. But from what I do tell
-you, you will be able to gather a great deal and imagine the rest. To
-begin with, there is a man living in this world to-day who has done me a
-great and lasting injury. What that injury is is no concern of yours.
-You would not understand if I told you. So we'll leave that out of the
-question. He is immensely rich. His cheque for £300,000 would be
-honoured by his bank at any minute. Obviously he is a power. He has had
-reason to know that I am pitting my wits against his, and he flatters
-himself that so far he has got the better of me. That is because I am
-drawing him on. I am maturing a plan which will make him a poor and a
-very miserable man at one and the same time. If that scheme succeeds,
-and I am satisfied with the way you three men have performed the parts I
-shall call on you to play in it, I shall pay to each of you the sum of
-£10,000. If it doesn't succeed, then you will each receive a thousand
-and your expenses. Do you follow me?"
-
-It was evident from their faces that they hung upon his every word.
-
-"But, remember, I demand from you your whole and entire labour. While
-you are serving me you are mine body and soul. I know you are
-trustworthy. I have had good proof that you are--pardon the
-expression--unscrupulous, and I flatter myself you are silent. What is
-more, I shall tell you nothing beyond what is necessary for the carrying
-out of my scheme, so that you could not betray me if you would. Now for
-my plans!"
-
-He sat down again and took a paper from his pocket. Having perused it,
-he turned to Eastover.
-
-"You will leave at once--that is to say, by the boat on Wednesday--for
-Sydney. You will book your passage to-morrow morning, first thing, and
-join her in Plymouth. You will meet me to-morrow evening at an address I
-will send you, and receive your final instructions. Good-night."
-
-Seeing that he was expected to go, Eastover rose, shook hands, and left
-the room without a word. He was too astonished to hesitate or to say
-anything.
-
-Nikola took another letter from his pocket and turned to Prendergast.
-"*You* will go down to Dover to-night, cross to Paris to-morrow morning,
-and leave this letter personally at the address you will find written on
-it. On Thursday, at half-past two precisely, you will deliver me an
-answer in the porch at Charing Cross. You will find sufficient money in
-that envelope to pay all your expenses. Now go!"
-
-"At half-past two you shall have your answer. Good-night."
-
-"Good-night."
-
-When Prendergast had left the room, Dr. Nikola lit another cigar and
-turned his attentions to Mr. Baxter.
-
-"Six months ago, Mr. Baxter, I found for you a situation as tutor to the
-young Marquis of Beckenham. You still hold it, I suppose?"
-
-"I do."
-
-"Is the father well disposed towards you?"
-
-"In every way. I have done my best to ingratiate myself with him. That
-was one of your instructions."
-
-"Yes, yes! But I was not certain that you would succeed. If the old man
-is anything like what he was when I last met him he must still be a
-difficult person to deal with. Does the boy like you?"
-
-"I hope so."
-
-"Have you brought me his photograph as I directed?"
-
-"I have. Here it is."
-
-Baxter took a photograph from his pocket and handed it across the table.
-
-"Good. You have done very well, Mr. Baxter. I am pleased with you.
-To-morrow morning you will go back to Yorkshire----"
-
-"I beg your pardon, Bournemouth. His Grace owns a house near
-Bournemouth, which he occupies during the summer months."
-
-"Very well--then to-morrow morning you will go back to Bournemouth and
-continue to ingratiate yourself with father and son. You will also begin
-to implant in the boy's mind a desire for travel. Don't let him become
-aware that his desire has its source in you--but do not fail to foster
-it all you can. I will communicate with you further in a day or two. Now
-go."
-
-Baxter in his turn left the room. The door closed. Dr. Nikola picked up
-the photograph and studied it.
-
-"The likeness is unmistakable--or it ought to be. My friend, my very
-dear friend, Wetherell, my toils are closing on you. My arrangements are
-perfecting themselves admirably. Presently, when all is complete, I
-shall press the lever, the machinery will be set in motion, and you will
-find yourself being slowly but surely ground into powder. Then you will
-hand over what I want, and be sorry you thought fit to baulk Dr.
-Nikola!"
-
-He rang the bell and ordered his bill. This duty discharged, he placed
-the cat back in its prison, shut the lid, descended with the basket to
-the hall, and called a hansom. The porter inquired to what address he
-should order the cabman to drive. Dr. Nikola did not reply for a moment,
-then he said, as if he had been thinking something out: "The *Green
-Sailor* public-house, East India Dock Road."
-
-
-------------------------
-
-You can read the rest of "A Bid For Fortune; Or, Dr. Nikola's Vendetta" at `Open Library <https://archive.org/stream/bidforfortunenov00bootiala#page/12/mode/2up>`__
+.. title: A BID FOR FORTUNE OR; DR. NIKOLA'S VENDETTA
+.. template: book.tmpl
+.. hyphenate: yes
+.. filters: filters.typogrify
+
+.. class:: subtitle
+
+By `GUY BOOTHBY <http://www.gutenberg.org/ebooks/author/3587>`__
+
+Author of "Dr. Nikola," "The Beautiful White Devil," etc., etc.
+
+.. figure:: /images/frontispiece.jpg
+ :class: bookfig
+
+.. topic:: The Project Gutenberg EBook of A Bid for Fortune, by Guy Boothby
+
+ This eBook is for the use of anyone anywhere at no cost and with
+ almost no restrictions whatsoever. You may copy it, give it away or
+ re-use it under the terms of the Project Gutenberg License included
+ with this eBook or online at www.gutenberg.org
+
+
+ Title: A Bid for Fortune
+ or Dr. Nikola's Vendetta
+
+ Author: `Guy Boothby <http://www.gutenberg.org/ebooks/author/3587>`__
+
+ Release Date: May 29, 2007 [EBook #21640]
+
+ Language: English
+
+ Produced by Marilynda Fraser-Cunliffe, Mary Meehan and the
+ Online Distributed Proofreading Team at http://www.pgdp.net
+
+ Originally published by:
+
+ WARD, LOCK & CO., LIMITED
+ LONDON, MELBOURNE AND TORONTO
+ 1918
+
+.. figure:: /images/illus_001.jpg
+ :class: bookfig
+
+PART I
+======
+
+PROLOGUE
+--------
+
+.. role:: smallcaps
+
+
+:smallcaps:`The` manager of the new Imperial Restaurant on the Thames Embankment went
+into his luxurious private office and shut the door. Having done so, he
+first scratched his chin reflectively, and then took a letter from the
+drawer in which it had reposed for more than two months and perused it
+carefully. Though he was not aware of it, this was the thirtieth time he
+had read it since breakfast that morning. And yet he was not a whit
+nearer understanding it than he had been at the beginning. He turned it
+over and scrutinized the back, where not a sign of writing was to be
+seen; he held it up to the window, as if he might hope to discover
+something from the water-mark; but there was nothing in either of these
+places of a nature calculated to set his troubled mind at rest. Then he
+took a magnificent repeater watch from his waistcoat pocket and glanced
+at the dial; the hands stood at half-past seven. He immediately threw
+the letter on the table, and as he did so his anxiety found relief in
+words.
+
+"It's really the most extraordinary affair I ever had to do with," he
+remarked. "And as I've been in the business just three-and-thirty years
+at eleven a.m. next Monday morning, I ought to know something about it.
+I only hope I've done right, that's all."
+
+As he spoke, the chief bookkeeper, who had the treble advantage of being
+tall, pretty, and just eight-and-twenty years of age, entered the room.
+She noticed the open letter and the look upon her chief's face, and her
+curiosity was proportionately excited.
+
+"You seem worried, Mr. McPherson," she said tenderly, as she put down
+the papers she had brought in for his signature.
+
+"You have just hit it, Miss O'Sullivan," he answered, pushing them
+farther on to the table. "I am worried about many things, but
+particularly about this letter."
+
+He handed the epistle to her, and she, being desirous of impressing him
+with her business capabilities, read it with ostentatious care. But it
+was noticeable that when she reached the signature she too turned back
+to the beginning, and then deliberately read it over again. The manager
+rose, crossed to the mantelpiece, and rang for the head waiter. Having
+relieved his feelings in this way, he seated himself again at his
+writing-table, put on his glasses, and stared at his companion, while
+waiting for her to speak.
+
+"It's very funny," she said. "Very funny indeed!"
+
+"It's the most extraordinary communication I have ever received," he
+replied with conviction. "You see it is written from Cuyaba, Brazil. The
+date is three months ago to a day. Now I have taken the trouble to find
+out where and what Cuyaba is."
+
+He made this confession with an air of conscious pride, and having done
+so, laid himself back in his chair, stuck his thumbs into the armholes
+of his waistcoat, and looked at his fair subordinate for approval. Nor
+was he destined to be disappointed. He was a bachelor in possession of a
+snug income, and she, besides being pretty, was a lady with a keen eye
+to the main chance.
+
+"And where *is* Cuyaba?" she asked humbly.
+
+"Cuyaba," he replied, rolling his tongue with considerable relish round
+his unconscious mispronunciation of the name, "is a town almost on the
+western or Bolivian border of Brazil. It is of moderate size, is
+situated on the banks of the river Cuyaba, and is considerably connected
+with the famous Brazilian Diamond Fields."
+
+"And does the writer of this letter live there?"
+
+"I cannot say. He writes from there--that is enough for us."
+
+"And he orders dinner for four--here, in a private room overlooking the
+river, three months ahead--punctually at eight o'clock, gives you a list
+of the things he wants, and even arranges the decoration of the table.
+Says he has never seen either of his three friends before; that one of
+them hails from (here she consulted the letter again) Hang-chow, another
+from Bloemfontein, while the third resides, at present, in England. Each
+one is to present an ordinary visiting card with a red dot on it to the
+porter in the hall, and to be shown to the room at once. I don't
+understand it at all."
+
+The manager paused for a moment, and then said deliberately,--"Hang-chow
+is in China, Bloemfontein is in South Africa."
+
+"What a wonderful man you are, to be sure, Mr. McPherson! I never can
+*think* how you manage to carry so much in your head."
+
+There spoke the true woman. And it was a move in the right direction,
+for the manager was susceptible to her gentle influence, as she had
+occasion to know.
+
+At this juncture the head waiter appeared upon the scene, and took up a
+position just inside the doorway, as if he were afraid of injuring the
+carpet by coming farther.
+
+"Is No. 22 ready, Williams?"
+
+"Quite ready, sir. The wine is on the ice, and cook tells me he'll be
+ready to dish punctual to the moment."
+
+"The letter says, 'no electric light; candles with red shades.' Have you
+put on those shades I got this morning?"
+
+"Just seen it done this very minute, sir."
+
+"And let me see, there was one other thing." He took the letter from the
+chief bookkeeper's hand and glanced at it. "Ah, yes, a porcelain saucer,
+and a small jug of new milk upon the mantelpiece. An extraordinary
+request, but has it been attended to?"
+
+"I put it there myself, sir."
+
+"Who wait?"
+
+"Jones, Edmunds, Brooks, and Tomkins."
+
+"Very good. Then I think that will do. Stay! You had better tell the
+hall porter to look out for three gentlemen presenting plain visiting
+cards with a little red spot on them. Let Brooks wait in the hall, and
+when they arrive tell him to show them straight up to the room."
+
+"It shall be done, sir."
+
+The head waiter left the room, and the manager stretched himself in his
+chair, yawned by way of showing his importance, and then said
+solemnly,--
+
+"I don't believe they'll any of them turn up; but if they do, this Dr.
+Nikola, whoever he may be, won't be able to find fault with my
+arrangements."
+
+Then, leaving the dusty high road of Business, he and his companion
+wandered in the shady bridle-paths of Love--to the end that when the
+chief bookkeeper returned to her own department she had forgotten the
+strange dinner party about to take place upstairs, and was busily
+engaged upon a calculation as to how she would look in white satin and
+orange blossoms, and, that settled, fell to wondering whether it was
+true, as Miss Joyce, a subordinate, had been heard to declare, that the
+manager had once shown himself partial to a certain widow with reputed
+savings and a share in an extensive egg and dairy business.
+
+At ten minutes to eight precisely a hansom drew up at the steps of the
+hotel. As soon as it stopped, an undersized gentleman, with a clean
+shaven countenance, a canonical corporation, and bow legs, dressed in a
+decidedly clerical garb, alighted. He paid and discharged his cabman,
+and then took from his ticket pocket an ordinary white visiting card,
+which he presented to the gold-laced individual who had opened the
+apron. The latter, having noted the red spot, called a waiter, and the
+reverend gentleman was immediately escorted upstairs.
+
+Hardly had the attendant time to return to his station in the hall,
+before a second cab made its appearance, closely followed by a third.
+Out of the second jumped a tall, active, well-built man of about thirty
+years of age. He was dressed in evening dress of the latest fashion, and
+to conceal it from the vulgar gaze, wore a large Inverness cape of heavy
+texture. He also in his turn handed a white card to the porter, and,
+having done so, proceeded into the hall, followed by the occupant of the
+last cab, who had closely copied his example. This individual was also
+in evening dress, but it was of a different stamp. It was old-fashioned
+and had seen much use. The wearer, too, was taller than the ordinary run
+of men, while it was noticeable that his hair was snow-white, and that
+his face was deeply pitted with smallpox. After disposing of their hats
+and coats in an ante-room, they reached room No. 22, where they found
+the gentleman in clerical costume pacing impatiently up and down.
+
+Left alone, the tallest of the trio, who for want of a better title we
+may call the Best Dressed Man, took out his watch, and having glanced at
+it, looked at his companions. "Gentlemen," he said, with a slight
+American accent, "it is three minutes to eight o'clock. My name is
+Eastover!"
+
+"I'm glad to hear it, for I'm most uncommonly hungry," said the next
+tallest, whom I have already described as being so marked by disease.
+"My name is Prendergast!"
+
+"We only wait for our friend and host," remarked the clerical gentleman,
+as if he felt he ought to take a share in the conversation, and then, as
+an afterthought, he continued, "My name is Baxter!"
+
+They shook hands all round with marked cordiality, seated themselves
+again, and took it in turns to examine the clock.
+
+"Have you ever had the pleasure of meeting our host before?" asked Mr.
+Baxter of Mr. Prendergast.
+
+"Never," replied that gentleman, with a shake of his head. "Perhaps Mr.
+Eastover has been more fortunate?"
+
+"Not I," was the brief rejoinder. "I've had to do with him off and on
+for longer than I care to reckon, but I've never set eyes on him up to
+date."
+
+"And where may he have been the first time you heard from him?"
+
+"In Nashville, Tennessee," said Eastover. "After that, Tahupapa, New
+Zealand; after that, Papeete, in the Society Islands; then Pekin, China.
+And you?"
+
+"First time, Brussels; second, Monte Video; third, Mandalay, and then
+the Gold Coast, Africa. It's your turn, Mr. Baxter."
+
+The clergyman glanced at the timepiece. It was exactly eight o'clock.
+"First time, Cabul, Afghanistan; second, Nijni Novgorod, Russia; third,
+Wilcannia, Darling River, Australia; fourth, Valparaiso, Chili; fifth,
+Nagasaki, Japan."
+
+"He is evidently a great traveller and a most mysterious person."
+
+"He is more than that," said Eastover with conviction; "he is late for
+dinner!"
+
+Prendergast looked at his watch.
+
+"That clock is two minutes fast. Hark, there goes Big Ben! Eight
+exactly."
+
+As he spoke the door was thrown open and a voice announced "Dr. Nikola."
+
+The three men sprang to their feet simultaneously, with exclamations of
+astonishment, as the man they had been discussing made his appearance.
+
+It would take more time than I can spare the subject to give you an
+adequate and inclusive description of the person who entered the room at
+that moment. In stature he was slightly above the ordinary, his
+shoulders were broad, his limbs perfectly shaped and plainly muscular,
+but very slim. His head, which was magnificently set upon his shoulders,
+was adorned with a profusion of glossy black hair; his face was
+destitute of beard or moustache, and was of oval shape and handsome
+moulding; while his skin was of a dark olive hue, a colour which
+harmonized well with his piercing black eyes and pearly teeth. His hands
+and feet were small, and the greatest dandy must have admitted that he
+was irreproachably dressed, with a neatness that bordered on the
+puritanical. In age he might have been anything from eight-and-twenty to
+forty; in reality he was thirty-three. He advanced into the room and
+walked with out-stretched hand directly across to where Eastover was
+standing by the fireplace.
+
+"Mr. Eastover, I feel certain," he said, fixing his glittering eyes upon
+the man he addressed, and allowing a curious smile to play upon his
+face.
+
+"That is my name, Dr. Nikola," the other answered with evident surprise.
+"But how on earth can you distinguish me from your other guests?"
+
+"Ah! it would surprise you if you knew. And Mr. Prendergast, and Mr.
+Baxter. This is delightful; I hope I am not late. We had a collision in
+the Channel this morning, and I was almost afraid I might not be up to
+time. Dinner seems ready; shall we sit down to it?" They seated
+themselves, and the meal commenced. The Imperial Restaurant has earned
+an enviable reputation for doing things well, and the dinner that night
+did not in any way detract from its lustre. But, delightful as it all
+was, it was noticeable that the three guests paid more attention to
+their host than to his excellent *menu*. As they had said before his
+arrival, they had all had dealings with him for several years, but what
+those dealings were they were careful not to describe. It was more than
+possible that they hardly liked to remember them themselves.
+
+When coffee had been served and the servants had withdrawn, Dr. Nikola
+rose from the table, and went across to the massive sideboard. On it
+stood a basket of very curious shape and workmanship. This he opened,
+and as he did so, to the astonishment of his guests, an enormous cat, as
+black as his master's coat, leaped out on to the floor. The reason for
+the saucer and jug of milk became evident.
+
+Seating himself at the table again, the host followed the example of his
+guests and lit a cigar, blowing a cloud of smoke luxuriously through his
+delicately chiselled nostrils. His eyes wandered round the cornice of
+the room, took in the pictures and decorations, and then came down to
+meet the faces of his companions. As they did so, the black cat, having
+finished its meal, sprang on to his shoulder to crouch there, watching
+the three men through the curling smoke drift with its green blinking,
+fiendish eyes. Dr. Nikola smiled as he noticed the effect the animal had
+upon his guests.
+
+"Now shall we get to business?" he said briskly.
+
+The others almost simultaneously knocked the ashes off their cigars and
+brought themselves to attention. Dr. Nikola's dainty, languid manner
+seemed to drop from him like a cloak, his eyes brightened, and his
+voice, when he spoke, was clean cut as chiselled silver.
+
+"You are doubtless anxious to be informed why I summoned you from all
+parts of the globe to meet me here to-night? And it is very natural you
+should be. But then, from what you know of me, you should not be
+surprised at anything I do."
+
+His voice dropped back into its old tone of gentle languor. He drew in a
+great breath of smoke and then sent it slowly out from his lips again.
+His eyes were half closed, and he drummed with one finger on the table
+edge. The cat looked through the smoke at the three men, and it seemed
+to them that he grew every moment larger and more ferocious. Presently
+his owner took him from his perch, and seating him on his knee fell to
+stroking his fur, from head to tail, with his long slim fingers. It was
+as if he were drawing inspiration for some deadly mischief from the
+uncanny beast.
+
+"To preface what I have to say to you, let me tell you that this is by
+far the most important business for which I have ever required your
+help. (Three slow strokes down the centre of the back, and one round
+each ear.) When it first came into my mind I was at a loss who to trust
+in the matter. I thought of Vendon, but I found Vendon was dead. I
+thought of Brownlow, but Brownlow was no longer faithful. (Two strokes
+down the back and two on the throat.) Then bit by bit I remembered you.
+I was in Brazil at the time. So I sent for you. You came. So far so
+good."
+
+He rose, and crossed over to the fireplace. As he went the cat crawled
+back to its original position on his shoulder. Then his voice changed
+once more to its former business-like tone.
+
+"I am not going to tell you very much about it. But from what I do tell
+you, you will be able to gather a great deal and imagine the rest. To
+begin with, there is a man living in this world to-day who has done me a
+great and lasting injury. What that injury is is no concern of yours.
+You would not understand if I told you. So we'll leave that out of the
+question. He is immensely rich. His cheque for £300,000 would be
+honoured by his bank at any minute. Obviously he is a power. He has had
+reason to know that I am pitting my wits against his, and he flatters
+himself that so far he has got the better of me. That is because I am
+drawing him on. I am maturing a plan which will make him a poor and a
+very miserable man at one and the same time. If that scheme succeeds,
+and I am satisfied with the way you three men have performed the parts I
+shall call on you to play in it, I shall pay to each of you the sum of
+£10,000. If it doesn't succeed, then you will each receive a thousand
+and your expenses. Do you follow me?"
+
+It was evident from their faces that they hung upon his every word.
+
+"But, remember, I demand from you your whole and entire labour. While
+you are serving me you are mine body and soul. I know you are
+trustworthy. I have had good proof that you are--pardon the
+expression--unscrupulous, and I flatter myself you are silent. What is
+more, I shall tell you nothing beyond what is necessary for the carrying
+out of my scheme, so that you could not betray me if you would. Now for
+my plans!"
+
+He sat down again and took a paper from his pocket. Having perused it,
+he turned to Eastover.
+
+"You will leave at once--that is to say, by the boat on Wednesday--for
+Sydney. You will book your passage to-morrow morning, first thing, and
+join her in Plymouth. You will meet me to-morrow evening at an address I
+will send you, and receive your final instructions. Good-night."
+
+Seeing that he was expected to go, Eastover rose, shook hands, and left
+the room without a word. He was too astonished to hesitate or to say
+anything.
+
+Nikola took another letter from his pocket and turned to Prendergast.
+"*You* will go down to Dover to-night, cross to Paris to-morrow morning,
+and leave this letter personally at the address you will find written on
+it. On Thursday, at half-past two precisely, you will deliver me an
+answer in the porch at Charing Cross. You will find sufficient money in
+that envelope to pay all your expenses. Now go!"
+
+"At half-past two you shall have your answer. Good-night."
+
+"Good-night."
+
+When Prendergast had left the room, Dr. Nikola lit another cigar and
+turned his attentions to Mr. Baxter.
+
+"Six months ago, Mr. Baxter, I found for you a situation as tutor to the
+young Marquis of Beckenham. You still hold it, I suppose?"
+
+"I do."
+
+"Is the father well disposed towards you?"
+
+"In every way. I have done my best to ingratiate myself with him. That
+was one of your instructions."
+
+"Yes, yes! But I was not certain that you would succeed. If the old man
+is anything like what he was when I last met him he must still be a
+difficult person to deal with. Does the boy like you?"
+
+"I hope so."
+
+"Have you brought me his photograph as I directed?"
+
+"I have. Here it is."
+
+Baxter took a photograph from his pocket and handed it across the table.
+
+"Good. You have done very well, Mr. Baxter. I am pleased with you.
+To-morrow morning you will go back to Yorkshire----"
+
+"I beg your pardon, Bournemouth. His Grace owns a house near
+Bournemouth, which he occupies during the summer months."
+
+"Very well--then to-morrow morning you will go back to Bournemouth and
+continue to ingratiate yourself with father and son. You will also begin
+to implant in the boy's mind a desire for travel. Don't let him become
+aware that his desire has its source in you--but do not fail to foster
+it all you can. I will communicate with you further in a day or two. Now
+go."
+
+Baxter in his turn left the room. The door closed. Dr. Nikola picked up
+the photograph and studied it.
+
+"The likeness is unmistakable--or it ought to be. My friend, my very
+dear friend, Wetherell, my toils are closing on you. My arrangements are
+perfecting themselves admirably. Presently, when all is complete, I
+shall press the lever, the machinery will be set in motion, and you will
+find yourself being slowly but surely ground into powder. Then you will
+hand over what I want, and be sorry you thought fit to baulk Dr.
+Nikola!"
+
+He rang the bell and ordered his bill. This duty discharged, he placed
+the cat back in its prison, shut the lid, descended with the basket to
+the hall, and called a hansom. The porter inquired to what address he
+should order the cabman to drive. Dr. Nikola did not reply for a moment,
+then he said, as if he had been thinking something out: "The *Green
+Sailor* public-house, East India Dock Road."
+
+
+------------------------
+
+You can read the rest of "A Bid For Fortune; Or, Dr. Nikola's Vendetta" at `Open Library <https://archive.org/stream/bidforfortunenov00bootiala#page/12/mode/2up>`__
diff --git a/nikola/data/samplesite/pages/extending.rst b/nikola/data/samplesite/pages/extending.rst
new file mode 120000
index 0000000..aab25e2
--- /dev/null
+++ b/nikola/data/samplesite/pages/extending.rst
@@ -0,0 +1 @@
+../../../../docs/extending.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/extending.txt b/nikola/data/samplesite/pages/extending.txt
deleted file mode 120000
index f545532..0000000
--- a/nikola/data/samplesite/pages/extending.txt
+++ /dev/null
@@ -1 +0,0 @@
-../../../../docs/extending.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/internals.rst b/nikola/data/samplesite/pages/internals.rst
new file mode 120000
index 0000000..23b276d
--- /dev/null
+++ b/nikola/data/samplesite/pages/internals.rst
@@ -0,0 +1 @@
+../../../../docs/internals.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/internals.txt b/nikola/data/samplesite/pages/internals.txt
deleted file mode 120000
index b955b57..0000000
--- a/nikola/data/samplesite/pages/internals.txt
+++ /dev/null
@@ -1 +0,0 @@
-../../../../docs/internals.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/manual.rst b/nikola/data/samplesite/pages/manual.rst
index 9992900..4d5f0a1 120000
--- a/nikola/data/samplesite/pages/manual.rst
+++ b/nikola/data/samplesite/pages/manual.rst
@@ -1 +1 @@
-../../../../docs/manual.txt \ No newline at end of file
+../../../../docs/manual.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/path_handlers.rst b/nikola/data/samplesite/pages/path_handlers.rst
new file mode 120000
index 0000000..23193d7
--- /dev/null
+++ b/nikola/data/samplesite/pages/path_handlers.rst
@@ -0,0 +1 @@
+../../../../docs/path_handlers.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/path_handlers.txt b/nikola/data/samplesite/pages/path_handlers.txt
deleted file mode 120000
index cce056b..0000000
--- a/nikola/data/samplesite/pages/path_handlers.txt
+++ /dev/null
@@ -1 +0,0 @@
-../../../../docs/path_handlers.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/quickref.rst b/nikola/data/samplesite/pages/quickref.rst
index 7cc91bd..152fbd0 100644
--- a/nikola/data/samplesite/pages/quickref.rst
+++ b/nikola/data/samplesite/pages/quickref.rst
@@ -8,7 +8,7 @@
.. raw:: html
- <div class="alert alert-info pull-right" style="margin-left: 2em;">
+ <div class="alert alert-primary float-md-right" style="margin-left: 2em;">
<h2><a name="contents">Contents</a></h2>
<ul>
@@ -1214,11 +1214,11 @@
<td>
<samp>Titles&nbsp;are&nbsp;targets,&nbsp;too</samp>
<br><samp>=======================</samp>
- <br><samp>Implict&nbsp;references,&nbsp;like&nbsp;`Titles&nbsp;are</samp>
+ <br><samp>Implicit&nbsp;references,&nbsp;like&nbsp;`Titles&nbsp;are</samp>
<br><samp>targets,&nbsp;too`_.</samp>
<td>
<font size="+2"><strong><a name="title">Titles are targets, too</a></strong></font>
- <p>Implict references, like <a href="#title">Titles are
+ <p>Implicit references, like <a href="#title">Titles are
targets, too</a>.
</table>
diff --git a/nikola/data/samplesite/pages/quickstart.rst b/nikola/data/samplesite/pages/quickstart.rst
index 5937e56..28a452d 100644
--- a/nikola/data/samplesite/pages/quickstart.rst
+++ b/nikola/data/samplesite/pages/quickstart.rst
@@ -1,12 +1,9 @@
.. title: A reStructuredText Primer
.. slug: quickstart
.. date: 2012-03-30 23:00:00 UTC-03:00
-.. tags:
-.. link:
-.. description:
-
-A ReStructuredText Primer
-=========================
+.. tags:
+.. link:
+.. description:
:Author: Richard Jones
:Version: $Revision: 5801 $
@@ -23,7 +20,7 @@ reference. If these links don't work, please refer to the `master
quick reference`_ document.
__
-.. _Quick reStructuredText: quickref.html
+.. _Quick reStructuredText: ../quickref/
.. _master quick reference:
http://docutils.sourceforge.net/docs/user/rst/quickref.html
@@ -65,7 +62,7 @@ Results in:
This is another one.
-__ quickref.html#paragraphs
+__ ../quickref/#paragraphs
Text styles
@@ -73,7 +70,7 @@ Text styles
(quickref__)
-__ quickref.html#inline-markup
+__ ../quickref/#inline-markup
Inside paragraphs and other bodies of text, you may additionally mark
text for *italics* with "``*italics*``" or **bold** with
@@ -95,7 +92,7 @@ by enclosing it in double back-quotes (inline literals), like this::
``*``
-__ quickref.html#escaping
+__ ../quickref/#escaping
.. Tip:: Think of inline markup as a form of (parentheses) and use it
the same way: immediately before and after the text being marked
@@ -119,7 +116,7 @@ Lists must always start a new paragraph -- that is, they must appear
after a blank line.
**enumerated** lists (numbers, letters or roman numerals; quickref__)
- __ quickref.html#enumerated-lists
+ __ ../quickref/#enumerated-lists
Start a line off with a number or letter followed by a period ".",
right bracket ")" or surrounded by brackets "( )" -- whatever you're
@@ -170,7 +167,7 @@ after a blank line.
1) and again
**bulleted** lists (quickref__)
- __ quickref.html#bullet-lists
+ __ ../quickref/#bullet-lists
Just like enumerated lists, start the line off with a bullet point
character - either "-", "+" or "*"::
@@ -194,7 +191,7 @@ after a blank line.
- another item
**definition** lists (quickref__)
- __ quickref.html#definition-lists
+ __ ../quickref/#definition-lists
Unlike the other two, the definition lists consist of a term, and
the definition of that term. The format of a definition list is::
@@ -222,7 +219,7 @@ Preformatting (code samples)
----------------------------
(quickref__)
-__ quickref.html#literal-blocks
+__ ../quickref/#literal-blocks
To just include a chunk of preformatted, never-to-be-fiddled-with
text, finish the prior paragraph with "``::``". The preformatted
@@ -270,7 +267,7 @@ Sections
(quickref__)
-__ quickref.html#section-structure
+__ ../quickref/#section-structure
To break longer text up into sections, you use **section headers**.
These are a single line of text (one or more words) with adornment: an
@@ -364,9 +361,9 @@ Images
(quickref__)
-__ quickref.html#directives
+__ ../quickref/#directives
-To include an image in your document, you use the the ``image`` directive__.
+To include an image in your document, you use the ``image`` directive__.
For example::
.. image:: /images/nikola.png
diff --git a/nikola/data/samplesite/pages/slides-demo.rst b/nikola/data/samplesite/pages/slides-demo.rst
deleted file mode 100644
index 0d07bbc..0000000
--- a/nikola/data/samplesite/pages/slides-demo.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-.. title: Slides Demo
-.. slug: slides-demo
-.. date: 2012-12-27 10:16:20 UTC-03:00
-.. tags:
-.. link:
-.. description:
-
-Nikola intends to let you do slideshows easily:
-
-.. slides::
-
- /galleries/demo/tesla_conducts_lg.jpg
- /galleries/demo/tesla_lightning2_lg.jpg
- /galleries/demo/tesla4_lg.jpg
- /galleries/demo/tesla_lightning1_lg.jpg
- /galleries/demo/tesla_tower1_lg.jpg
-
diff --git a/nikola/data/samplesite/pages/social_buttons.rst b/nikola/data/samplesite/pages/social_buttons.rst
new file mode 120000
index 0000000..df8d07c
--- /dev/null
+++ b/nikola/data/samplesite/pages/social_buttons.rst
@@ -0,0 +1 @@
+../../../../docs/social_buttons.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/social_buttons.txt b/nikola/data/samplesite/pages/social_buttons.txt
deleted file mode 120000
index b60d598..0000000
--- a/nikola/data/samplesite/pages/social_buttons.txt
+++ /dev/null
@@ -1 +0,0 @@
-../../../../docs/social_buttons.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/pages/theming.rst b/nikola/data/samplesite/pages/theming.rst
index d2dddb6..d004aa8 120000
--- a/nikola/data/samplesite/pages/theming.rst
+++ b/nikola/data/samplesite/pages/theming.rst
@@ -1 +1 @@
-../../../../docs/theming.txt \ No newline at end of file
+../../../../docs/theming.rst \ No newline at end of file
diff --git a/nikola/data/samplesite/posts/1.rst b/nikola/data/samplesite/posts/1.rst
index 386251b..628dfd7 100644
--- a/nikola/data/samplesite/posts/1.rst
+++ b/nikola/data/samplesite/posts/1.rst
@@ -21,7 +21,6 @@ Next steps:
* `Visit the Nikola website to learn more <https://getnikola.com>`__
* `See a demo photo gallery <link://gallery/demo>`__
* :doc:`See a demo listing <listings-demo>`
-* :doc:`See a demo slideshow <slides-demo>`
* :doc:`See a demo of a longer text <dr-nikolas-vendetta>`
Send feedback to info@getnikola.com!
diff --git a/nikola/data/symlinked.txt b/nikola/data/symlinked.txt
index c0d37eb..477335e 100644
--- a/nikola/data/symlinked.txt
+++ b/nikola/data/symlinked.txt
@@ -1,142 +1,37 @@
-docs/sphinx/creating-a-site.txt
-docs/sphinx/creating-a-theme.txt
-docs/sphinx/extending.txt
-docs/sphinx/internals.txt
-docs/sphinx/manual.txt
-docs/sphinx/path_handlers.txt
-docs/sphinx/social_buttons.txt
-docs/sphinx/theming.txt
+docs/sphinx/creating-a-site.rst
+docs/sphinx/creating-a-theme.rst
+docs/sphinx/extending.rst
+docs/sphinx/internals.rst
+docs/sphinx/manual.rst
+docs/sphinx/path_handlers.rst
+docs/sphinx/social_buttons.rst
+docs/sphinx/support.rst
+docs/sphinx/template-variables.rst
+docs/sphinx/theming.rst
nikola/data/samplesite/pages/creating-a-theme.rst
-nikola/data/samplesite/pages/extending.txt
-nikola/data/samplesite/pages/internals.txt
+nikola/data/samplesite/pages/extending.rst
+nikola/data/samplesite/pages/internals.rst
nikola/data/samplesite/pages/manual.rst
-nikola/data/samplesite/pages/path_handlers.txt
-nikola/data/samplesite/pages/social_buttons.txt
+nikola/data/samplesite/pages/path_handlers.rst
+nikola/data/samplesite/pages/social_buttons.rst
nikola/data/samplesite/pages/theming.rst
nikola/data/symlink-test-link.txt
-nikola/data/themes/base/assets/js/moment-with-locales.min.js
+nikola/data/themes/base/assets/css/baguetteBox.min.css
+nikola/data/themes/base/assets/js/baguetteBox.min.js
+nikola/data/themes/base/assets/js/html5.js
+nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js
+nikola/data/themes/base/assets/js/justified-layout.min.js
+nikola/data/themes/base/assets/js/luxon.min.js
nikola/data/themes/base/messages/messages_cz.py
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css
-nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map
-nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css
-nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png
-nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif
-nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot
-nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg
-nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf
-nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff
-nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2
-nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js
-nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
-nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
-nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js
-nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js
-nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js
-nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js
-nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map
-nikola/data/themes/bootstrap3-jinja/bundles
-nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css
-nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map
-nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css
-nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map
-nikola/data/themes/bootstrap3/assets/css/bootstrap.css
-nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map
-nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css
-nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map
-nikola/data/themes/bootstrap3/assets/css/colorbox.css
-nikola/data/themes/bootstrap3/assets/css/images/controls.png
-nikola/data/themes/bootstrap3/assets/css/images/loading.gif
-nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot
-nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg
-nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf
-nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff
-nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2
-nikola/data/themes/bootstrap3/assets/js/bootstrap.js
-nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
-nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
-nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js
-nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js
-nikola/data/themes/bootstrap3/assets/js/jquery.js
-nikola/data/themes/bootstrap3/assets/js/jquery.min.js
-nikola/data/themes/bootstrap3/assets/js/jquery.min.map
+nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css
+nikola/data/themes/bootblog4-jinja/bundles
+nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css
+nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js
+nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js
+nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js
+nikola/data/themes/bootstrap4-jinja/bundles
+nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css
+nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js
+nikola/data/themes/bootstrap4/assets/js/jquery.min.js
+nikola/data/themes/bootstrap4/assets/js/popper.min.js
nikola/plugins/command/auto/livereload.js
diff --git a/nikola/data/themes/base-jinja/AUTHORS.txt b/nikola/data/themes/base-jinja/AUTHORS.txt
deleted file mode 100644
index 043d497..0000000
--- a/nikola/data/themes/base-jinja/AUTHORS.txt
+++ /dev/null
@@ -1 +0,0 @@
-Roberto Alsina <https://github.com/ralsina>
diff --git a/nikola/data/themes/base-jinja/base-jinja.theme b/nikola/data/themes/base-jinja/base-jinja.theme
new file mode 100644
index 0000000..64dc002
--- /dev/null
+++ b/nikola/data/themes/base-jinja/base-jinja.theme
@@ -0,0 +1,10 @@
+[Theme]
+engine = jinja
+parent = base
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+
+[Family]
+family = base
+mako_version = base
diff --git a/nikola/data/themes/base-jinja/engine b/nikola/data/themes/base-jinja/engine
deleted file mode 100644
index 6f04b30..0000000
--- a/nikola/data/themes/base-jinja/engine
+++ /dev/null
@@ -1 +0,0 @@
-jinja
diff --git a/nikola/data/themes/base-jinja/parent b/nikola/data/themes/base-jinja/parent
deleted file mode 100644
index df967b9..0000000
--- a/nikola/data/themes/base-jinja/parent
+++ /dev/null
@@ -1 +0,0 @@
-base
diff --git a/nikola/data/themes/base-jinja/templates/archive.tmpl b/nikola/data/themes/base-jinja/templates/archive.tmpl
new file mode 100644
index 0000000..f2b715d
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/archive.tmpl
@@ -0,0 +1 @@
+{% extends 'list_post.tmpl' %}
diff --git a/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl b/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl
new file mode 100644
index 0000000..ce913a3
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/archive_navigation_helper.tmpl
@@ -0,0 +1,27 @@
+{# -*- coding: utf-8 -*- #}
+
+{% macro archive_navigation() %}
+{% if 'archive_page' in pagekind %}
+ {% if has_archive_navigation %}
+ <nav class="archivenav">
+ <ul class="pager">
+ {% if previous_archive %}
+ <li class="previous"><a href="{{ previous_archive }}" rel="prev">{{ messages("Previous") }}</a></li>
+ {% else %}
+ <li class="previous disabled"><a href="#" rel="prev">{{ messages("Previous") }}</a></li>
+ {% endif %}
+ {% if up_archive %}
+ <li class="up"><a href="{{ up_archive }}" rel="up">{{ messages("Up") }}</a></li>
+ {% else %}
+ <li class="up disabled"><a href="#" rel="up">{{ messages("Up") }}</a></li>
+ {% endif %}
+ {% if next_archive %}
+ <li class="next"><a href="{{ next_archive }}" rel="next">{{ messages("Next") }}</a></li>
+ {% else %}
+ <li class="next disabled"><a href="#" rel="next">{{ messages("Next") }}</a></li>
+ {% endif %}
+ </ul>
+ </nav>
+ {% endif %}
+{% endif %}
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl
index 8b9286e..a8bc9c6 100644
--- a/nikola/data/themes/base-jinja/templates/archiveindex.tmpl
+++ b/nikola/data/themes/base-jinja/templates/archiveindex.tmpl
@@ -1,13 +1,20 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}
+{% import 'archive_navigation_helper.tmpl' as archive_nav with context %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% block extra_head %}
{{ super() }}
- {% if translations|length > 1 and generate_atom %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ archive_name }} section ({{ language }})" href="{{ _link("archive_atom", archive_name, language) }}">
- {% endfor %}
- {% elif generate_atom %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ archive_name }} archive" href="{{ _link("archive_atom", archive_name) }}">
- {% endif %}
+ {{ feeds_translations.head(archive_name, kind, rss_override=False) }}
+{% endblock %}
+
+{% block content_header %}
+ <header>
+ <h1>{{ title|e }}</h1>
+ {{ archive_nav.archive_navigation() }}
+ <div class="metadata">
+ {{ feeds_translations.feed_link(archive, kind) }}
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
+ </header>
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/author.tmpl b/nikola/data/themes/base-jinja/templates/author.tmpl
index 327debe..4d8a876 100644
--- a/nikola/data/themes/base-jinja/templates/author.tmpl
+++ b/nikola/data/themes/base-jinja/templates/author.tmpl
@@ -1,43 +1,28 @@
{# -*- coding: utf-8 -*- #}
{% extends 'list_post.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% block extra_head %}
- {{ super() }}
- {% if translations|length > 1 and generate_rss %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ author|e }} ({{ language }})" href="{{ _link(kind + "_rss", author, language) }}">
- {% endfor %}
- {% elif generate_rss %}
- <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ author|e }}" href="{{ _link(kind + "_rss", author) }}">
- {% endif %}
+ {{ feeds_translations.head(author, kind, rss_override=False) }}
{% endblock %}
-
{% block content %}
<article class="authorpage">
<header>
<h1>{{ title|e }}</h1>
{% if description %}
- <p>{{ description }}</p>
+ <p>{{ description }}</p>
{% endif %}
<div class="metadata">
- {% if translations|length > 1 and generate_rss %}
- {% for language in translations|sort %}
- <p class="feedlink">
- <a href="{{ _link(kind + "_rss", author, language) }}" hreflang="{{ language }}" type="application/rss+xml">{{ messages('RSS feed', language) }} ({{ language }})</a>&nbsp;
- </p>
- {% endfor %}
- {% elif generate_rss %}
- <p class="feedlink"><a href="{{ _link(kind + "_rss", author) }}" type="application/rss+xml">{{ messages('RSS feed') }}</a></p>
- {% endif %}
+ {{ feeds_translations.feed_link(author, kind) }}
</div>
</header>
{% if posts %}
- <ul class="postlist">
- {% for post in posts %}
- <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a></li>
- {% endfor %}
- </ul>
+ <ul class="postlist">
+ {% for post in posts %}
+ <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}</a></li>
+ {% endfor %}
+ </ul>
{% endif %}
</article>
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/authorindex.tmpl b/nikola/data/themes/base-jinja/templates/authorindex.tmpl
index 3c40ee1..4b2fcbf 100644
--- a/nikola/data/themes/base-jinja/templates/authorindex.tmpl
+++ b/nikola/data/themes/base-jinja/templates/authorindex.tmpl
@@ -1,13 +1,21 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block content_header %}
+ <header>
+ <h1>{{ title|e }}</h1>
+ {% if description %}
+ <p>{{ description }}</p>
+ {% endif %}
+ <div class="metadata">
+ {{ feeds_translations.feed_link(author, kind) }}
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
+ </header>
+{% endblock %}
{% block extra_head %}
{{ super() }}
- {% if tranlations|length > 1 and generate_atom %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ author|e }} section ({{ language }})" href="{{ _link(kind + "_atom", author, language) }}">
- {% endfor %}
- {% elif generate_atom %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ author|e }} section" href="{{ _link("author" + "_atom", author) }}">
- {% endif %}
+ {{ feeds_translations.head(author, kind, rss_override=False) }}
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/authors.tmpl b/nikola/data/themes/base-jinja/templates/authors.tmpl
index 8b6ea64..c8e05ff 100644
--- a/nikola/data/themes/base-jinja/templates/authors.tmpl
+++ b/nikola/data/themes/base-jinja/templates/authors.tmpl
@@ -1,10 +1,18 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block extra_head %}
+ {{ feeds_translations.head(kind=kind, feeds=False) }}
+{% endblock %}
{% block content %}
<article class="authorindex">
{% if items %}
<h2>{{ messages("Authors") }}</h2>
+ <div class="metadata">
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
<ul class="postlist">
{% for text, link in items %}
{% if text not in hidden_authors %}
diff --git a/nikola/data/themes/base-jinja/templates/base.tmpl b/nikola/data/themes/base-jinja/templates/base.tmpl
index 5412326..8b057db 100644
--- a/nikola/data/themes/base-jinja/templates/base.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base.tmpl
@@ -2,8 +2,8 @@
{% import 'base_helper.tmpl' as base with context %}
{% import 'base_header.tmpl' as header with context %}
{% import 'base_footer.tmpl' as footer with context %}
-{% import 'annotation_helper.tmpl' as annotations with context %}
{{ set_locale(lang) }}
+{# <html> tag is included by base.html_headstart #}
{{ base.html_headstart() }}
{% block extra_head %}
{# Leave this block alone. #}
@@ -11,16 +11,29 @@
{{ template_hooks['extra_head']() }}
</head>
<body>
-<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
+ <a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
<div id="container">
- {{ header.html_header() }}
- <main id="content">
+ {{ header.html_header() }}
+ <main id="content">
{% block content %}{% endblock %}
- </main>
- {{ footer.html_footer() }}
+ </main>
+ {{ footer.html_footer() }}
</div>
{{ base.late_load_js() }}
+ {% if date_fanciness != 0 %}
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}";
+ fancydates({{ date_fanciness }}, {{ luxon_date_format }});
+ </script>
+ <!-- end fancy dates -->
+ {% endif %}
{% block extra_js %}{% endblock %}
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
{{ body_end }}
{{ template_hooks['body_end']() }}
</body>
diff --git a/nikola/data/themes/base-jinja/templates/base_footer.tmpl b/nikola/data/themes/base-jinja/templates/base_footer.tmpl
index 2e541a6..db44a67 100644
--- a/nikola/data/themes/base-jinja/templates/base_footer.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base_footer.tmpl
@@ -1,5 +1,4 @@
{# -*- coding: utf-8 -*- #}
-{% import 'base_helper.tmpl' as base with context %}
{% macro html_footer() %}
{% if content_footer %}
diff --git a/nikola/data/themes/base-jinja/templates/base_header.tmpl b/nikola/data/themes/base-jinja/templates/base_header.tmpl
index d9370d7..bfbd447 100644
--- a/nikola/data/themes/base-jinja/templates/base_header.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base_header.tmpl
@@ -16,7 +16,7 @@
{% endmacro %}
{% macro html_site_title() %}
- <h1 id="brand"><a href="{{ abs_link(_link("root", None, lang)) }}" title="{{ blog_title|e }}" rel="home">
+ <h1 id="brand"><a href="{{ _link("root", None, lang) }}" title="{{ blog_title|e }}" rel="home">
{% if logo_url %}
<img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo">
{% endif %}
@@ -30,13 +30,22 @@
{% macro html_navigation_links() %}
<nav id="menu">
<ul>
- {% for url, text in navigation_links[lang] %}
+ {{ html_navigation_links_entries(navigation_links) }}
+ {{ html_navigation_links_entries(navigation_alt_links) }}
+ {{ template_hooks['menu']() }}
+ {{ template_hooks['menu_alt']() }}
+ </ul>
+ </nav>
+{% endmacro %}
+
+{% macro html_navigation_links_entries(navigation_links_source) %}
+ {% for url, text in navigation_links_source[lang] %}
{% if isinstance(url, tuple) %}
<li> {{ text }}
<ul>
{% for suburl, text in url %}
{% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a></li>
+ <li class="active"><a href="{{ permalink }}">{{ text }}<span class="sr-only"> {{ messages("(active)", lang) }}</span></a></li>
{% else %}
<li><a href="{{ suburl }}">{{ text }}</a></li>
{% endif %}
@@ -44,16 +53,12 @@
</ul>
{% else %}
{% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a></li>
+ <li class="active"><a href="{{ permalink }}">{{ text }}<span class="sr-only"> {{ messages("(active)", lang) }}</span></a></li>
{% else %}
<li><a href="{{ url }}">{{ text }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
- {{ template_hooks['menu']() }}
- {{ template_hooks['menu_alt']() }}
- </ul>
- </nav>
{% endmacro %}
{% macro html_translation_header() %}
diff --git a/nikola/data/themes/base-jinja/templates/base_helper.tmpl b/nikola/data/themes/base-jinja/templates/base_helper.tmpl
index 04f49fe..a05abb9 100644
--- a/nikola/data/themes/base-jinja/templates/base_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/base_helper.tmpl
@@ -1,31 +1,25 @@
{# -*- coding: utf-8 -*- #}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% macro html_headstart() %}
<!DOCTYPE html>
<html \
-prefix='
-{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %}
-og: http://ogp.me/ns# article: http://ogp.me/ns/article#
-{% endif %}
-{% if comment_system == 'facebook' %}
-fb: http://ogp.me/ns/fb#
-{% endif %}
-' \
-{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %}
-vocab="http://ogp.me/ns" \
-{% endif %}
+ prefix='
+ og: http://ogp.me/ns# article: http://ogp.me/ns/article#
+ {% if comment_system == 'facebook' %}
+ fb: http://ogp.me/ns/fb#
+ {% endif %}
+ ' \
+ vocab="http://ogp.me/ns" \
{% if is_rtl %}
-dir="rtl"
+ dir="rtl"
{% endif %}
lang="{{ lang }}">
<head>
<meta charset="utf-8">
- {% if use_base_tag %}
- <base href="{{ abs_link(permalink) }}">
- {% endif %}
{% if description %}
- <meta name="description" content="{{ description|e }}">
+ <meta name="description" content="{{ description|e }}">
{% endif %}
<meta name="viewport" content="width=device-width">
{% if title == blog_title %}
@@ -35,8 +29,11 @@ lang="{{ lang }}">
{% endif %}
{{ html_stylesheets() }}
- <meta content="{{ theme_color }}" name="theme-color">
- {{ html_feedlinks() }}
+ <meta name="theme-color" content="{{ theme_color }}">
+ {% if meta_generator_tag %}
+ <meta name="generator" content="Nikola (getnikola.com)">
+ {% endif %}
+ {{ feeds_translations.head(classification=None, kind='index', other=False) }}
<link rel="canonical" href="{{ abs_link(permalink) }}">
{% if favicons %}
@@ -56,29 +53,58 @@ lang="{{ lang }}">
<link rel="next" href="{{ nextlink }}" type="text/html">
{% endif %}
- {{ mathjax_config }}
{% if use_cdn %}
- <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ <!--[if lt IE 9]><script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script><![endif]-->
{% else %}
- <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang) }}"></script><![endif]-->
+ <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5shiv-printshiv.min.js', lang, url_type) }}"></script><![endif]-->
{% endif %}
{{ extra_head_data }}
{% endmacro %}
{% macro late_load_js() %}
+ {% if use_bundles %}
+ {% if use_cdn %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ <script src="/assets/js/all.js"></script>
+ {% else %}
+ <script src="/assets/js/all-nocdn.js"></script>
+ {% endif %}
+ {% else %}
+ {% if use_cdn %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ {% else %}
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ {% endif %}
+ {% endif %}
+ {% if date_fanciness != 0 %}
+ {% if date_fanciness == 2 %}
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script>
+ {% endif %}
+ {% if use_cdn %}
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ {% else %}
+ <script src="/assets/js/luxon.min.js"></script>
+ {% endif %}
+ {% if not use_bundles %}
+ <script src="/assets/js/fancydates.min.js"></script>
+ {% endif %}
+ {% endif %}
{{ social_buttons_code }}
{% endmacro %}
{% macro html_stylesheets() %}
{% if use_bundles %}
{% if use_cdn %}
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
{% else %}
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
{% endif %}
{% else %}
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/rst_base.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_rst.css" rel="stylesheet" type="text/css">
<link href="/assets/css/code.css" rel="stylesheet" type="text/css">
<link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
{% if has_custom_css %}
@@ -91,34 +117,16 @@ lang="{{ lang }}">
{% endif %}
{% endmacro %}
+{# This function is deprecated; use feed_helper directly. #}
{% macro html_feedlinks() %}
- {% if rss_link %}
- {{ rss_link }}
- {% elif generate_rss %}
- {% if translations|length > 1 %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}">
- {% endfor %}
- {% else %}
- <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}">
- {% endif %}
- {% endif %}
- {% if generate_atom %}
- {% if translations|length > 1 %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}">
- {% endfor %}
- {% else %}
- <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}">
- {% endif %}
- {% endif %}
+ {{ feeds_translations.head(classification=None, kind='index', other=False) }}
{% endmacro %}
{% macro html_translations() %}
<ul class="translations">
{% for langname in translations|sort %}
{% if langname != lang %}
- <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
+ <li><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
{% endif %}
{% endfor %}
</ul>
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl
index aba7294..2a7d8dc 100644
--- a/nikola/data/themes/base-jinja/templates/comments_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/comments_helper.tmpl
@@ -1,63 +1,63 @@
{# -*- coding: utf-8 -*- #}
{% import 'comments_helper_disqus.tmpl' as disqus with context %}
-{% import 'comments_helper_livefyre.tmpl' as livefyre with context %}
{% import 'comments_helper_intensedebate.tmpl' as intensedebate with context %}
{% import 'comments_helper_muut.tmpl' as muut with context %}
-{% import 'comments_helper_googleplus.tmpl' as googleplus with context %}
{% import 'comments_helper_facebook.tmpl' as facebook with context %}
{% import 'comments_helper_isso.tmpl' as isso with context %}
+{% import 'comments_helper_commento.tmpl' as commento with context %}
+{% import 'comments_helper_utterances.tmpl' as utterances with context %}
{% macro comment_form(url, title, identifier) %}
{% if comment_system == 'disqus' %}
{{ disqus.comment_form(url, title, identifier) }}
- {% elif comment_system == 'livefyre' %}
- {{ livefyre.comment_form(url, title, identifier) }}
{% elif comment_system == 'intensedebate' %}
{{ intensedebate.comment_form(url, title, identifier) }}
{% elif comment_system == 'muut' %}
{{ muut.comment_form(url, title, identifier) }}
- {% elif comment_system == 'googleplus' %}
- {{ googleplus.comment_form(url, title, identifier) }}
{% elif comment_system == 'facebook' %}
{{ facebook.comment_form(url, title, identifier) }}
{% elif comment_system == 'isso' %}
{{ isso.comment_form(url, title, identifier) }}
+ {% elif comment_system == 'commento' %}
+ {{ commento.comment_form(url, title, identifier) }}
+ {% elif comment_system == 'utterances' %}
+ {{ utterances.comment_form(url, title, identifier) }}
{% endif %}
{% endmacro %}
{% macro comment_link(link, identifier) %}
{% if comment_system == 'disqus' %}
{{ disqus.comment_link(link, identifier) }}
- {% elif comment_system == 'livefyre' %}
- {{ livefyre.comment_link(link, identifier) }}
{% elif comment_system == 'intensedebate' %}
{{ intensedebate.comment_link(link, identifier) }}
{% elif comment_system == 'muut' %}
{{ muut.comment_link(link, identifier) }}
- {% elif comment_system == 'googleplus' %}
- {{ googleplus.comment_link(link, identifier) }}
{% elif comment_system == 'facebook' %}
{{ facebook.comment_link(link, identifier) }}
{% elif comment_system == 'isso' %}
{{ isso.comment_link(link, identifier) }}
+ {% elif comment_system == 'commento' %}
+ {{ commento.comment_link(link, identifier) }}
+ {% elif comment_system == 'utterances' %}
+ {{ utterances.comment_link(link, identifier) }}
{% endif %}
{% endmacro %}
{% macro comment_link_script() %}
{% if comment_system == 'disqus' %}
{{ disqus.comment_link_script() }}
- {% elif comment_system == 'livefyre' %}
- {{ livefyre.comment_link_script() }}
{% elif comment_system == 'intensedebate' %}
{{ intensedebate.comment_link_script() }}
{% elif comment_system == 'muut' %}
{{ muut.comment_link_script() }}
- {% elif comment_system == 'googleplus' %}
- {{ googleplus.comment_link_script() }}
{% elif comment_system == 'facebook' %}
{{ facebook.comment_link_script() }}
{% elif comment_system == 'isso' %}
{{ isso.comment_link_script() }}
+ {% elif comment_system == 'commento' %}
+ {{ commento.comment_link_script() }}
+ {% elif comment_system == 'utterances' %}
+ {{ utterances.comment_link_script() }}
{% endif %}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl
new file mode 100644
index 0000000..25857d9
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_commento.tmpl
@@ -0,0 +1,13 @@
+{# -*- coding: utf-8 -*- #}
+{% macro comment_form(url, title, identifier) %}
+ <div id="commento"></div>
+
+ <script defer src="{{ comment_system_id }}/js/commento.js"></script>
+{% endmacro %}
+
+{% macro comment_link(link, identifier) %}
+ <a href="{{ link }}#commento">{{ messages("Comments") }}</a>
+{% endmacro %}
+
+{% macro comment_link_script() %}
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
index 981453d..4aa42fa 100644
--- a/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_disqus.tmpl
@@ -30,7 +30,7 @@
{% macro comment_link(link, identifier) %}
{% if comment_system_id %}
- <a href="{{ link }}#disqus_thread" data-disqus-identifier="{{ identifier }}">Comments</a>
+ <a href="{{ link }}#disqus_thread" data-disqus-identifier="{{ identifier }}">{{ messages("Comments") }}</a>
{% endif %}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl
deleted file mode 100644
index cf153e0..0000000
--- a/nikola/data/themes/base-jinja/templates/comments_helper_googleplus.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% macro comment_form(url, title, identifier) %}
-<script src="https://apis.google.com/js/plusone.js"></script>
-<div class="g-comments"
- data-href="{{ url }}"
- data-first_party_property="BLOGGER"
- data-view_type="FILTERED_POSTMOD">
-</div>
-{% endmacro %}
-
-{% macro comment_link(link, identifier) %}
-<div class="g-commentcount" data-href="{{ link }}"></div>
-<script src="https://apis.google.com/js/plusone.js"></script>
-{% endmacro %}
-
-{% macro comment_link_script() %}
-{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl
index 042409b..d649d31 100644
--- a/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_intensedebate.tmpl
@@ -6,7 +6,7 @@ var idcomments_post_id = "{{ identifier }}";
var idcomments_post_url = "{{ url }}";
</script>
<span id="IDCommentsPostTitle" style="display:none"></span>
-<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script>
+<script src="https://www.intensedebate.com/js/genericCommentWrapperV2.js"></script>
</script>
{% endmacro %}
@@ -17,7 +17,7 @@ var idcomments_acct = '{{ comment_system_id }}';
var idcomments_post_id = "{{ identifier }}";
var idcomments_post_url = "{{ link }}";
</script>
-<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script>
+<script src="https://www.intensedebate.com/js/genericLinkWrapperV2.js"></script>
</a>
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl
index b40b5e4..43995c5 100644
--- a/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_isso.tmpl
@@ -1,20 +1,26 @@
{# -*- coding: utf-8 -*- #}
{% macro comment_form(url, title, identifier) %}
{% if comment_system_id %}
- <div data-title="{{ title|urlencode }}" id="isso-thread"></div>
- <script src="{{ comment_system_id }}js/embed.min.js" data-isso="{{ comment_system_id }}"></script>
+ <div data-title="{{ title|e }}" id="isso-thread"></div>
+ <script src="{{ comment_system_id }}js/embed.min.js" data-isso="{{ comment_system_id }}" data-isso-lang="{{ lang }}"
+ {% if isso_config %}
+ {% for k, v in isso_config.items() %}
+ data-isso-{{ k }}="{{ v }}"
+ {% endfor %}
+ {% endif %}
+ ></script>
{% endif %}
{% endmacro %}
{% macro comment_link(link, identifier) %}
{% if comment_system_id %}
- <a href="{{ link }}#isso-thread">Comments</a>
+ <a href="{{ link }}#isso-thread">{{ messages("Comments") }}</a>
{% endif %}
{% endmacro %}
{% macro comment_link_script() %}
{% if comment_system_id and 'index' in pagekind %}
- <script src="{{ comment_system_id }}js/count.min.js" data-isso="{{ comment_system_id }}"></script>
+ <script src="{{ comment_system_id }}js/count.min.js" data-isso="{{ comment_system_id }}" data-isso-lang="{{ lang }}"></script>
{% endif %}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl
deleted file mode 100644
index 5b01fbf..0000000
--- a/nikola/data/themes/base-jinja/templates/comments_helper_livefyre.tmpl
+++ /dev/null
@@ -1,33 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% macro comment_form(url, title, identifier) %}
-<div id="livefyre-comments"></div>
-<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script>
-<script>
-(function () {
- var articleId = "{{ identifier }}";
- fyre.conv.load({}, [{
- el: 'livefyre-comments',
- network: "livefyre.com",
- siteId: "{{ comment_system_id }}",
- articleId: articleId,
- signed: false,
- collectionMeta: {
- articleId: articleId,
- url: fyre.conv.load.makeCollectionUrl(),
- }
- }], function() {});
-}());
-</script>
-{% endmacro %}
-
-{% macro comment_link(link, identifier) %}
- <a href="{{ link }}">
- <span class="livefyre-commentcount" data-lf-site-id="{{ comment_system_id }}" data-lf-article-id="{{ identifier }}">
- 0 Comments
- </span>
-{% endmacro %}
-
-
-{% macro comment_link_script() %}
-<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script>
-{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl
deleted file mode 100644
index 8912e19..0000000
--- a/nikola/data/themes/base-jinja/templates/comments_helper_mustache.tmpl
+++ /dev/null
@@ -1,5 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% import 'comments_helper.tmpl' as comments with context %}
-{% if not post.meta('nocomments') %}
- {{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }}
-{% endif %}
diff --git a/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl b/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl
new file mode 100644
index 0000000..e1c03c2
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/comments_helper_utterances.tmpl
@@ -0,0 +1,23 @@
+{# -*- coding: utf-8 -*- #}
+{% macro comment_form(url, title, identifier) %}
+ {% if comment_system_id %}
+ <div data-title="{{ title|e }}" id="utterances-thread"></div>
+ <script src="https://utteranc.es/client.js" repo="{{ comment_system_id }}"
+ {% if utterances_config %}
+ {% for k, v in utterances_config.items() %}
+ {{ k }}="{{ v }}"
+ {% endfor %}
+ {% endif %}
+ ></script>
+ {% endif %}
+{% endmacro %}
+
+{% macro comment_link(link, identifier) %}
+ {% if comment_system_id %}
+ <a href="{{ link }}#utterances-thread">{{ messages("Comments") }}</a>
+ {% endif %}
+{% endmacro %}
+
+
+{% macro comment_link_script() %}
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl b/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl
new file mode 100644
index 0000000..278e1c4
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/feeds_translations_helper.tmpl
@@ -0,0 +1,124 @@
+{# -*- coding: utf-8 -*- #}
+
+{% macro _head_feed_link(link_type, link_name, link_postfix, classification, kind, language) %}
+ {% if translations|length > 1 %}
+ <link rel="alternate" type="{{ link_type }}" title="{{ link_name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + '_' + link_postfix, classification, language) }}">
+ {% else %}
+ <link rel="alternate" type="{{ link_type }}" title="{{ link_name|e }}" hreflang="{{ language }}" href="{{ _link(kind + '_' + link_postfix, classification, language) }}">
+ {% endif %}
+{% endmacro %}
+
+{% macro _html_feed_link(link_type, link_name, link_postfix, classification, kind, language, name=None) %}
+ {% if translations|length > 1 %}
+ {% if name and kind != "archive" and kind != "author" %}
+ <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ name|e }}, {{ language }})</a>
+ {% else %}
+ <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ language }})</a>
+ {% endif %}
+ {% else %}
+ {% if name and kind != "archive" and kind != "author" %}
+ <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }} ({{ name|e }})</a>
+ {% else %}
+ <a href="{{ _link(kind + '_' + link_postfix, classification, language) }}" hreflang="{{ language }}" type="{{ link_type }}">{{ messages(link_name, language) }}</a>
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro _html_translation_link(classification, kind, language, name=None) %}
+ {% if name and kind != "archive" and kind != "author" %}
+ <a href="{{ _link(kind, classification, language) }}" hreflang="{{ language }}" rel="alternate">{{ messages("LANGUAGE", language) }} ({{ name|e }})</a>
+ {% else %}
+ <a href="{{ _link(kind, classification, language) }}" hreflang="{{ language }}" rel="alternate">{{ messages("LANGUAGE", language) }}</a>
+ {% endif %}
+{% endmacro %}
+
+{% macro _head_rss(classification=None, kind='index', rss_override=True) %}
+ {% if rss_link and rss_override %}
+ {{ rss_link }}
+ {% endif %}
+ {% if generate_rss and not (rss_link and rss_override) and kind != 'archive' %}
+ {% if translations|length > 1 and has_other_languages and classification and kind != 'index' %}
+ {% for language, classification, name in all_languages %}
+ <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + "_rss", classification, language) }}">
+ {% endfor %}
+ {% else %}
+ {% for language in translations_feedorder %}
+ {% if (classification or classification == '') and kind != 'index' %}
+ {{ _head_feed_link('application/rss+xml', 'RSS for ' + kind + ' ' + classification, 'rss', classification, kind, language) }}
+ {% else %}
+ {{ _head_feed_link('application/rss+xml', 'RSS', 'rss', classification, 'index', language) }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro _head_atom(classification=None, kind='index') %}
+ {% if generate_atom %}
+ {% if translations|length > 1 and has_other_languages and classification and kind != 'index' %}
+ {% for language, classification, name in all_languages %}
+ <link rel="alternate" type="application/atom+xml" title="Atom for {{ kind }} {{ name|e }} ({{ language }})" hreflang="{{ language }}" href="{{ _link(kind + "_atom", classification, language) }}">
+ {% endfor %}
+ {% else %}
+ {% for language in translations_feedorder %}
+ {% if (classification or classification == '') and kind != 'index' %}
+ {{ _head_feed_link('application/atom+xml', 'Atom for ' + kind + ' ' + classification, 'atom', classification, kind, language) }}
+ {% else %}
+ {{ _head_feed_link('application/atom+xml', 'Atom', 'atom', classification, 'index', language) }}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{# Handles both feeds and translations #}
+{% macro head(classification=None, kind='index', feeds=True, other=True, rss_override=True, has_no_feeds=False) %}
+ {% if feeds and not has_no_feeds %}
+ {{ _head_rss(classification, 'index' if (kind == 'archive' and rss_override) else kind, rss_override) }}
+ {{ _head_atom(classification, kind) }}
+ {% endif %}
+ {% if other and has_other_languages and other_languages %}
+ {% for language, classification, _ in other_languages %}
+ <link rel="alternate" hreflang="{{ language }}" href="{{ _link(kind, classification, language) }}">
+ {% endfor %}
+ {% endif %}
+{% endmacro %}
+
+{% macro feed_link(classification, kind) %}
+ {% if generate_atom or generate_rss %}
+ {% if translations|length > 1 and has_other_languages and kind != 'index' %}
+ {% for language, classification, name in all_languages %}
+ <p class="feedlink">
+ {% if generate_atom %}
+ {{ _html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language, name) }}
+ {% endif %}
+ {% if generate_rss and kind != 'archive' %}
+ {{ _html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language, name) }}
+ {% endif %}
+ </p>
+ {% endfor %}
+ {% else %}
+ {% for language in translations_feedorder %}
+ <p class="feedlink">
+ {% if generate_atom %}
+ {{ _html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language) }}
+ {% endif %}
+ {% if generate_rss and kind != 'archive' %}
+ {{ _html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language) }}
+ {% endif %}
+ </p>
+ {% endfor %}
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro translation_link(kind) %}
+ {% if has_other_languages and other_languages %}
+ <div class="translationslist translations">
+ <h3 class="translationslist-intro">{{ messages("Also available in:") }}</h3>
+ {% for language, classification, name in other_languages %}
+ <p>{{ _html_translation_link(classification, kind, language, name) }}</p>
+ {% endfor %}
+ </div>
+ {% endif %}
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/gallery.tmpl b/nikola/data/themes/base-jinja/templates/gallery.tmpl
index 977dea1..d425106 100644
--- a/nikola/data/themes/base-jinja/templates/gallery.tmpl
+++ b/nikola/data/themes/base-jinja/templates/gallery.tmpl
@@ -1,11 +1,12 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
{% import 'comments_helper.tmpl' as comments with context %}
-{% import 'crumbs.tmpl' as ui with context %}
+{% import 'ui_helper.tmpl' as ui with context %}
+{% import 'post_helper.tmpl' as post_helper with context %}
{% block sourcelink %}{% endblock %}
{% block content %}
- {{ ui.bar(crumbs) }}
+ {{ ui.breadcrumbs(crumbs) }}
{% if title %}
<h1>{{ title|e }}</h1>
{% endif %}
@@ -15,21 +16,39 @@
</p>
{% endif %}
{% if folders %}
- <ul>
- {% for folder, ftitle in folders %}
- <li><a href="{{ folder }}"><i
- class="icon-folder-open"></i>&nbsp;{{ ftitle|e }}</a></li>
- {% endfor %}
- </ul>
- {% endif %}
- {% if photo_array %}
- <ul class="thumbnails">
- {% for image in photo_array %}
- <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title'] }}">
- <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" /></a>
- {% endfor %}
- </ul>
+ {% if galleries_use_thumbnail %}
+ {% for (folder, ftitle, fpost) in folders %}
+ <div class="thumnbnail-container">
+ <a href="{{ folder }}" class="thumbnail image-reference" title="{{ ftitle|e }}">
+ {% if fpost and fpost.previewimage %}
+ <img src="{{ fpost.previewimage }}" alt="{{ ftitle|e }}" loading="lazy" style="max-width:{{ thumbnail_size }}px; max-height:{{ thumbnail_size }}px;" />
+ {% else %}
+ <div style="height: {{ thumbnail_size }}px; width: {{ thumbnail_size }}px; background-color: #eee;"></div>
+ {% endif %}
+ <p class="thumbnail-caption">{{ ftitle|e }}</p>
+ </a>
+ </div>
+ {% endfor %}
+ {% else %}
+ <ul>
+ {% for folder, ftitle in folders %}
+ <li><a href="{{ folder }}">📂&nbsp;{{ ftitle|e }}</a></li>
+ {% endfor %}
+ </ul>
+ {% endif %}
{% endif %}
+
+<div id="gallery_container"></div>
+{% if photo_array %}
+<noscript>
+<ul class="thumbnails">
+ {% for image in photo_array %}
+ <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title']|e }}">
+ <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" loading="lazy" /></a>
+ {% endfor %}
+</ul>
+</noscript>
+{% endif %}
{% if site_has_comments and enable_comments %}
{{ comments.comment_form(None, permalink, title) }}
{% endif %}
@@ -38,4 +57,35 @@
{% block extra_head %}
{{ super() }}
<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
+<style type="text/css">
+ #gallery_container {
+ position: relative;
+ }
+ .image-block {
+ position: absolute;
+ }
+</style>
+{% if translations|length > 1 %}
+ {% for langname in translations.keys() %}
+ {% if langname != lang %}
+ <link rel="alternate" hreflang="{{ langname }}" href="{{ _link('gallery', gallery_path, langname) }}">
+ {% endif %}
+ {% endfor %}
+{% endif %}
+<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
+{% if post %}
+ {{ post_helper.open_graph_metadata(post) }}
+ {{ post_helper.twitter_card_information(post) }}
+{% endif %}
+{% endblock %}
+
+{% block extra_js %}
+<script src="/assets/js/justified-layout.min.js"></script>
+<script src="/assets/js/gallery.min.js"></script>
+<script>
+var jsonContent = {{ photo_array_json }};
+var thumbnailSize = {{ thumbnail_size }};
+renderGallery(jsonContent, thumbnailSize);
+window.addEventListener('resize', function(){renderGallery(jsonContent, thumbnailSize)});
+</script>
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/index.tmpl b/nikola/data/themes/base-jinja/templates/index.tmpl
index f982091..55ae9aa 100644
--- a/nikola/data/themes/base-jinja/templates/index.tmpl
+++ b/nikola/data/themes/base-jinja/templates/index.tmpl
@@ -1,6 +1,9 @@
{# -*- coding: utf-8 -*- #}
{% import 'index_helper.tmpl' as helper with context %}
+{% import 'math_helper.tmpl' as math with context %}
{% import 'comments_helper.tmpl' as comments with context %}
+{% import 'pagination_helper.tmpl' as pagination with context %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% extends 'base.tmpl' %}
{% block extra_head %}
@@ -8,27 +11,45 @@
{% if posts and (permalink == '/' or permalink == '/' + index_file) %}
<link rel="prefetch" href="{{ posts[0].permalink() }}" type="text/html">
{% endif %}
+ {{ math.math_styles_ifposts(posts) }}
{% endblock %}
{% block content %}
-{% block content_header %}{% endblock %}
+{% block content_header %}
+ {{ feeds_translations.translation_link(kind) }}
+{% endblock %}
{% if 'main_index' in pagekind %}
{{ front_index_header }}
{% endif %}
+{% if page_links %}
+ {{ pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed) }}
+{% endif %}
<div class="postindex">
{% for post in posts %}
- <article class="h-entry post-{{ post.meta('type') }}">
+ <article class="h-entry post-{{ post.meta('type') }}" itemscope="itemscope" itemtype="http://schema.org/Article">
<header>
<h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title()|e }}</a></h1>
<div class="metadata">
- <p class="byline author vcard"><span class="byline-name fn">
- {% if author_pages_generated %}
+ <p class="byline author vcard"><span class="byline-name fn" itemprop="author">
+ {% if author_pages_generated and multiple_authors_per_post %}
+ {% for author in post.authors() %}
+ <a href="{{ _link('author', author) }}">{{ author|e }}</a>
+ {% endfor %}
+ {% elif author_pages_generated %}
<a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a>
{% else %}
{{ post.author()|e }}
{% endif %}
</span></p>
- <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time></a></p>
+ <p class="dateline">
+ <a href="{{ post.permalink() }}" rel="bookmark">
+ <time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" itemprop="datePublished" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time>
+ {% if post.updated and post.updated != post.date %}
+ <span class="updated"> ({{ messages("updated") }}
+ <time class="dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span>
+ {% endif %}
+ </a>
+ </p>
{% if not post.meta('nocomments') and site_has_comments %}
<p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
{% endif %}
@@ -47,5 +68,5 @@
</div>
{{ helper.html_pager() }}
{{ comments.comment_link_script() }}
-{{ helper.mathjax_script(posts) }}
+{{ math.math_scripts_ifposts(posts) }}
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/index_helper.tmpl b/nikola/data/themes/base-jinja/templates/index_helper.tmpl
index 704c635..bc57734 100644
--- a/nikola/data/themes/base-jinja/templates/index_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/index_helper.tmpl
@@ -1,4 +1,5 @@
{# -*- coding: utf-8 -*- #}
+{% import 'math_helper.tmpl' as math with context %}
{% macro html_pager() %}
{% if prevlink or nextlink %}
<nav class="postindexpager">
@@ -18,33 +19,7 @@
{% endif %}
{% endmacro %}
+{# This function is deprecated; use math_helper directly. #}
{% macro mathjax_script(posts) %}
- {% if posts|selectattr("is_mathjax")|list %}
- {% if use_katex %}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script>
- {% if katex_auto_render %}
- <script>
- renderMathInElement(document.body,
- {
- {{ katex_auto_render }}
- }
- );
- </script>
- {% else %}
- <script>
- renderMathInElement(document.body);
- </script>
- {% endif %}
- {% else %}
- <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
- {% if mathjax_config %}
- {{ mathjax_config }}
- {% else %}
- <script type="text/x-mathjax-config">
- MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
- </script>
- {% endif %}
- {% endif %}
- {% endif %}
+ {{ math.math_scripts_ifposts(posts) }}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/list.tmpl b/nikola/data/themes/base-jinja/templates/list.tmpl
index c9c330b..5f8ddea 100644
--- a/nikola/data/themes/base-jinja/templates/list.tmpl
+++ b/nikola/data/themes/base-jinja/templates/list.tmpl
@@ -1,11 +1,19 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
+{% import 'archive_navigation_helper.tmpl' as archive_nav with context %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block extra_head %}
+ {{ feeds_translations.head(kind=kind, rss_override=False, has_no_feeds=has_no_feeds) }}
+{% endblock %}
{% block content %}
<article class="listpage">
<header>
<h1>{{ title|e }}</h1>
</header>
+ {{ archive_nav.archive_navigation() }}
+ {{ feeds_translations.translation_link(kind) }}
{% if items %}
<ul class="postlist">
{% for text, link, count in items %}
diff --git a/nikola/data/themes/base-jinja/templates/list_post.tmpl b/nikola/data/themes/base-jinja/templates/list_post.tmpl
index 1dd2605..e6b2080 100644
--- a/nikola/data/themes/base-jinja/templates/list_post.tmpl
+++ b/nikola/data/themes/base-jinja/templates/list_post.tmpl
@@ -1,11 +1,19 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
+{% import 'archive_navigation_helper.tmpl' as archive_nav with context %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block extra_head %}
+ {{ feeds_translations.head(kind=kind, rss_override=False) }}
+{% endblock %}
{% block content %}
<article class="listpage">
<header>
<h1>{{ title|e }}</h1>
</header>
+ {{ archive_nav.archive_navigation() }}
+ {{ feeds_translations.translation_link(kind) }}
{% if posts %}
<ul class="postlist">
{% for post in posts %}
diff --git a/nikola/data/themes/base-jinja/templates/listing.tmpl b/nikola/data/themes/base-jinja/templates/listing.tmpl
index 9b6d76d..7b6b3a6 100644
--- a/nikola/data/themes/base-jinja/templates/listing.tmpl
+++ b/nikola/data/themes/base-jinja/templates/listing.tmpl
@@ -1,15 +1,15 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
-{% import 'crumbs.tmpl' as ui with context %}
+{% import 'ui_helper.tmpl' as ui with context %}
{% block content %}
-{{ ui.bar(crumbs) }}
+{{ ui.breadcrumbs(crumbs) }}
{% if folders or files %}
<ul>
{% for name in folders %}
- <li><a href="{{ name|urlencode }}"><i class="icon-folder-open"></i> {{ name|e }}</a>
+ <li><a href="{{ name|e }}" class="listing-folder">{{ name|e }}</a>
{% endfor %}
{% for name in files %}
- <li><a href="{{ name|urlencode }}.html"><i class="icon-file"></i> {{ name|e }}</a>
+ <li><a href="{{ name|e }}.html" class="listing-file">{{ name|e }}</a>
{% endfor %}
</ul>
{% endif %}
@@ -22,5 +22,3 @@
{{ code }}
{% endif %}
{% endblock %}
-
-
diff --git a/nikola/data/themes/base-jinja/templates/math_helper.tmpl b/nikola/data/themes/base-jinja/templates/math_helper.tmpl
new file mode 100644
index 0000000..c16f9b8
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/math_helper.tmpl
@@ -0,0 +1,69 @@
+{# Note: at present, MathJax and KaTeX do not respect the USE_CDN configuration option #}
+{% macro math_scripts() %}
+ {% if use_katex %}
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js" integrity="sha384-9Nhn55MVVN0/4OFx7EE5kpFBPsEMZxKTCnA+4fqDmg12eCTqGi6+BB2LjY8brQxJ" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"></script>
+ {% if katex_auto_render %}
+ <script>
+ renderMathInElement(document.body,
+ {
+ {{ katex_auto_render }}
+ }
+ );
+ </script>
+ {% else %}
+ <script>
+ renderMathInElement(document.body,
+ {
+ delimiters: [
+ {left: "$$", right: "$$", display: true},
+ {left: "\\[", right: "\\]", display: true},
+ {left: "\\begin{equation*}", right: "\\end{equation*}", display: true},
+ {left: "\\(", right: "\\)", display: false}
+ ]
+ }
+ );
+ </script>
+ {% endif %}
+ {% else %}
+{# Note: given the size of MathJax; nikola will retrieve MathJax from a CDN regardless of use_cdn configuration #}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML" integrity="sha384-3lJUsx1TJHt7BA4udB5KPnDrlkO8T6J6v/op7ui0BbCjvZ9WqV4Xm6DTP6kQ/iBH" crossorigin="anonymous"></script>
+ {% if mathjax_config %}
+ {{ mathjax_config }}
+ {% else %}
+ <script type="text/x-mathjax-config">
+ MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
+ </script>
+ {% endif %}
+ {% endif %}
+{% endmacro %}
+
+{% macro math_styles() %}
+ {% if use_katex %}
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css" integrity="sha384-yFRtMMDnQtDRO8rLpMIKrtPCD5jdktao2TV19YiZYWMDkUR5GQZR/NOVTdquEx1j" crossorigin="anonymous">
+ {% endif %}
+{% endmacro %}
+
+{% macro math_scripts_ifpost(post) %}
+ {% if post.has_math %}
+ {{ math_scripts() }}
+ {% endif %}
+{% endmacro %}
+
+{% macro math_scripts_ifposts(posts) %}
+ {% if posts|selectattr("has_math")|list %}
+ {{ math_scripts() }}
+ {% endif %}
+{% endmacro %}
+
+{% macro math_styles_ifpost(post) %}
+ {% if post.has_math %}
+ {{ math_styles() }}
+ {% endif %}
+{% endmacro %}
+
+{% macro math_styles_ifposts(posts) %}
+ {% if posts|selectattr("has_math")|list %}
+ {{ math_styles() }}
+ {% endif %}
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/page.tmpl b/nikola/data/themes/base-jinja/templates/page.tmpl
new file mode 100644
index 0000000..c2f3f7a
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/page.tmpl
@@ -0,0 +1 @@
+{% extends 'story.tmpl' %}
diff --git a/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl b/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl
new file mode 100644
index 0000000..73cf699
--- /dev/null
+++ b/nikola/data/themes/base-jinja/templates/pagination_helper.tmpl
@@ -0,0 +1,16 @@
+{# -*- coding: utf-8 -*- #}
+{% macro page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5) %}
+<div class="page-navigation">
+ {% for i, link in enumerate(page_links) %}
+ {% if (i - current_page)|abs <= surrounding or i == 0 or i == page_links|length - 1 %}
+ {% if i == current_page %}
+ <span class="current-page">{{ i+1 }}</span>
+ {% else %}
+ <a href="{{ page_links[i] }}">{{ i+1 }}</a>
+ {% endif %}
+ {% elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1 %}
+ <span class="ellipsis">…</span>
+ {% endif %}
+ {% endfor %}
+</div>
+{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/post.tmpl b/nikola/data/themes/base-jinja/templates/post.tmpl
index 5e4d9a5..484a2e0 100644
--- a/nikola/data/themes/base-jinja/templates/post.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post.tmpl
@@ -2,15 +2,13 @@
{% import 'post_helper.tmpl' as helper with context %}
{% import 'post_header.tmpl' as pheader with context %}
{% import 'comments_helper.tmpl' as comments with context %}
+{% import 'math_helper.tmpl' as math with context %}
{% extends 'base.tmpl' %}
{% block extra_head %}
{{ super() }}
{% if post.meta('keywords') %}
- <meta name="keywords" content="{{ post.meta('keywords')|e }}">
- {% endif %}
- {% if post.description() %}
- <meta name="description" content="{{ post.description()|e }}">
+ <meta name="keywords" content="{{ smartjoin(', ', post.meta('keywords'))|e }}">
{% endif %}
<meta name="author" content="{{ post.author()|e }}">
{% if post.prev_post %}
@@ -25,6 +23,7 @@
{{ helper.open_graph_metadata(post) }}
{{ helper.twitter_card_information(post) }}
{{ helper.meta_translations(post) }}
+ {{ math.math_styles_ifpost(post) }}
{% endblock %}
{% block content %}
@@ -45,7 +44,7 @@
{{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }}
</section>
{% endif %}
- {{ helper.mathjax_script(post) }}
+ {{ math.math_scripts_ifpost(post) }}
</article>
{{ comments.comment_link_script() }}
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/post_header.tmpl b/nikola/data/themes/base-jinja/templates/post_header.tmpl
index 6b81120..d3c5d3e 100644
--- a/nikola/data/themes/base-jinja/templates/post_header.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post_header.tmpl
@@ -23,7 +23,7 @@
{% macro html_sourcelink() %}
{% if show_sourcelink %}
- <p class="sourceline"><a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a></p>
+ <p class="sourceline"><a href="{{ post.source_link() }}" class="sourcelink">{{ messages("Source") }}</a></p>
{% endif %}
{% endmacro %}
@@ -31,14 +31,26 @@
<header>
{{ html_title() }}
<div class="metadata">
- <p class="byline author vcard"><span class="byline-name fn">
- {% if author_pages_generated %}
- <a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a>
+ <p class="byline author vcard p-author h-card"><span class="byline-name fn p-name" itemprop="author">
+ {% if author_pages_generated and multiple_authors_per_post %}
+ {% for author in post.authors() %}
+ <a class="u-url" href="{{ _link('author', author) }}">{{ author|e }}</a>
+ {% endfor %}
+ {% elif author_pages_generated %}
+ <a class="u-url" href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a>
{% else %}
{{ post.author()|e }}
{% endif %}
</span></p>
- <p class="dateline"><a href="{{ post.permalink() }}" rel="bookmark"><time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" itemprop="datePublished" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time></a></p>
+ <p class="dateline">
+ <a href="{{ post.permalink() }}" rel="bookmark">
+ <time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" itemprop="datePublished" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time>
+ {% if post.updated and post.updated != post.date %}
+ <span class="updated"> ({{ messages("updated") }}
+ <time class="updated dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span>
+ {% endif %}
+ </a>
+ </p>
{% if not post.meta('nocomments') and site_has_comments %}
<p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
{% endif %}
@@ -46,9 +58,6 @@
{% if post.meta('link') %}
<p class="linkline"><a href="{{ post.meta('link') }}">{{ messages("Original site") }}</a></p>
{% endif %}
- {% if post.description() %}
- <meta name="description" itemprop="description" content="{{ post.description()|e }}">
- {% endif %}
</div>
{{ html_translations(post) }}
</header>
diff --git a/nikola/data/themes/base-jinja/templates/post_helper.tmpl b/nikola/data/themes/base-jinja/templates/post_helper.tmpl
index e2dcf59..94b3c05 100644
--- a/nikola/data/themes/base-jinja/templates/post_helper.tmpl
+++ b/nikola/data/themes/base-jinja/templates/post_helper.tmpl
@@ -1,4 +1,5 @@
{# -*- coding: utf-8 -*- #}
+{% import 'math_helper.tmpl' as math with context %}
{% macro meta_translations(post) %}
{% if translations|length > 1 %}
@@ -40,31 +41,29 @@
{% endmacro %}
{% macro open_graph_metadata(post) %}
-{% if use_open_graph %}
- <meta property="og:site_name" content="{{ blog_title|e }}">
- <meta property="og:title" content="{{ post.title()[:70]|e }}">
- <meta property="og:url" content="{{ abs_link(permalink) }}">
- {% if post.description() %}
+<meta property="og:site_name" content="{{ blog_title|e }}">
+<meta property="og:title" content="{{ post.title()[:70]|e }}">
+<meta property="og:url" content="{{ abs_link(permalink) }}">
+{% if post.description() %}
<meta property="og:description" content="{{ post.description()[:200]|e }}">
- {% else %}
+{% else %}
<meta property="og:description" content="{{ post.text(strip_html=True)[:200]|e }}">
- {% endif %}
- {% if post.previewimage %}
+{% endif %}
+{% if post.previewimage %}
<meta property="og:image" content="{{ url_replacer(permalink, post.previewimage, lang, 'absolute') }}">
- {% endif %}
- <meta property="og:type" content="article">
+{% endif %}
+<meta property="og:type" content="article">
{# Will only work with Pintrest and breaks everywhere else who expect a [Facebook] URI. #}
{# %if post.author(): #}
{# <meta property="article:author" content="{{ post.author()|e }}"> #}
{# %endif #}
- {% if post.date.isoformat() %}
+{% if post.date.isoformat() %}
<meta property="article:published_time" content="{{ post.formatted_date('webiso') }}">
- {% endif %}
- {% if post.tags %}
- {% for tag in post.tags %}
- <meta property="article:tag" content="{{ tag|e }}">
- {% endfor %}
- {% endif %}
+{% endif %}
+{% if post.tags %}
+ {% for tag in post.tags %}
+ <meta property="article:tag" content="{{ tag|e }}">
+ {% endfor %}
{% endif %}
{% endmacro %}
@@ -84,33 +83,7 @@
{% endif %}
{% endmacro %}
+{# This function is deprecated; use math_helper directly. #}
{% macro mathjax_script(post) %}
- {% if post.is_mathjax %}
- {% if use_katex %}
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script>
- {% if katex_auto_render %}
- <script>
- renderMathInElement(document.body,
- {
- {{ katex_auto_render }}
- }
- );
- </script>
- {% else %}
- <script>
- renderMathInElement(document.body);
- </script>
- {% endif %}
- {% else %}
- <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
- {% if mathjax_config %}
- {{ mathjax_config }}
- {% else %}
- <script type="text/x-mathjax-config">
- MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
- </script>
- {% endif %}
- {% endif %}
- {% endif %}
+ {{ math.math_scripts_ifpost(post) }}
{% endmacro %}
diff --git a/nikola/data/themes/base-jinja/templates/sectionindex.tmpl b/nikola/data/themes/base-jinja/templates/sectionindex.tmpl
deleted file mode 100644
index f1d3d5b..0000000
--- a/nikola/data/themes/base-jinja/templates/sectionindex.tmpl
+++ /dev/null
@@ -1,21 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% extends 'index.tmpl' %}
-
-{% block extra_head %}
- {{ super() }}
- {% if generate_atom %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ posts[0].section_name()|e }} section" href="{{ _link('section_index_atom', posts[0].section_slug()) }}">
- {% endif %}
-{% endblock %}
-
-{% block content %}
-<div class="sectionindex">
- <header>
- <h2><a href="{{ _link('section_index', posts[0].section_slug()) }}">{{ title|e }}</a></h2>
- {% if generate_atom %}
- <p class="feedlink"><a href="{{ _link('section_index_atom', posts[0].section_slug()) }}" type="application/atom+xml">{{ messages('Updates') }}</a></p>
- {% endif %}
- </header>
- {{ super() }}
-</div>
-{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/slides.tmpl b/nikola/data/themes/base-jinja/templates/slides.tmpl
deleted file mode 100644
index 0ae8fe8..0000000
--- a/nikola/data/themes/base-jinja/templates/slides.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-{% block content %}
-<div id="{{ carousel_id }}" class="carousel slide">
- <ol class="carousel-indicators">
- {% for i in range(slides_content|length) %}
- {% if i == 0 %}
- <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}" class="active"></li>
- {% else %}
- <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}"></li>
- {% endif %}
- {% endfor %}
- </ol>
- <div class="carousel-inner">
- {% for i, image in enumerate(slides_content) %}
- {% if i == 0 %}
- <div class="item active"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div>
- {% else %}
- <div class="item"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div>
- {% endif %}
- {% endfor %}
- </div>
- <a class="left carousel-control" href="#{{ carousel_id }}" data-slide="prev">&lsaquo;</a>
- <a class="right carousel-control" href="#{{ carousel_id }}" data-slide="next">&rsaquo;</a>
-</div>
-{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/story.tmpl b/nikola/data/themes/base-jinja/templates/story.tmpl
index 1269724..5c93256 100644
--- a/nikola/data/themes/base-jinja/templates/story.tmpl
+++ b/nikola/data/themes/base-jinja/templates/story.tmpl
@@ -2,6 +2,7 @@
{% import 'post_helper.tmpl' as helper with context %}
{% import 'post_header.tmpl' as pheader with context %}
{% import 'comments_helper.tmpl' as comments with context %}
+{% import 'math_helper.tmpl' as math with context %}
{% extends 'post.tmpl' %}
{% block content %}
@@ -19,6 +20,6 @@
{{ comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path) }}
</section>
{% endif %}
- {{ helper.mathjax_script(post) }}
+ {{ math.math_scripts_ifpost(post) }}
</article>
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/tag.tmpl b/nikola/data/themes/base-jinja/templates/tag.tmpl
index 363019b..fd5acbf 100644
--- a/nikola/data/themes/base-jinja/templates/tag.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tag.tmpl
@@ -1,24 +1,17 @@
{# -*- coding: utf-8 -*- #}
{% extends 'list_post.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% block extra_head %}
- {{ super() }}
- {% if translations|length > 1 and generate_rss %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ tag|e }} ({{ language }})" href="{{ _link(kind + "_rss", tag, language) }}">
- {% endfor %}
- {% elif generate_rss %}
- <link rel="alternate" type="application/rss+xml" title="RSS for {{ kind }} {{ tag|e }}" href="{{ _link(kind + "_rss", tag) }}">
- {% endif %}
+ {{ feeds_translations.head(tag, kind, rss_override=False) }}
{% endblock %}
-
{% block content %}
<article class="tagpage">
<header>
<h1>{{ title|e }}</h1>
{% if description %}
- <p>{{ description }}</p>
+ <p>{{ description }}</p>
{% endif %}
{% if subcategories %}
{{ messages('Subcategories:') }}
@@ -29,23 +22,16 @@
</ul>
{% endif %}
<div class="metadata">
- {% if translations|length > 1 and generate_rss %}
- {% for language in translations|sort %}
- <p class="feedlink">
- <a href="{{ _link(kind + "_rss", tag, language) }}" hreflang="{{ language }}" type="application/rss+xml">{{ messages('RSS feed', language) }} ({{ language }})</a>&nbsp;
- </p>
- {% endfor %}
- {% elif generate_rss %}
- <p class="feedlink"><a href="{{ _link(kind + "_rss", tag) }}" type="application/rss+xml">{{ messages('RSS feed') }}</a></p>
- {% endif %}
+ {{ feeds_translations.feed_link(tag, kind=kind) }}
</div>
+ {{ feeds_translations.translation_link(kind) }}
</header>
{% if posts %}
- <ul class="postlist">
- {% for post in posts %}
- <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}<a></li>
- {% endfor %}
- </ul>
+ <ul class="postlist">
+ {% for post in posts %}
+ <li><time class="listdate" datetime="{{ post.formatted_date('webiso') }}" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time> <a href="{{ post.permalink() }}" class="listtitle">{{ post.title()|e }}<a></li>
+ {% endfor %}
+ </ul>
{% endif %}
</article>
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/tagindex.tmpl b/nikola/data/themes/base-jinja/templates/tagindex.tmpl
index 624961d..8ea6a4b 100644
--- a/nikola/data/themes/base-jinja/templates/tagindex.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tagindex.tmpl
@@ -1,5 +1,6 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
{% block content_header %}
<header>
@@ -15,16 +16,14 @@
{% endfor %}
</ul>
{% endif %}
+ <div class="metadata">
+ {{ feeds_translations.feed_link(tag, kind) }}
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
</header>
{% endblock %}
{% block extra_head %}
{{ super() }}
- {% if translations|length > 1 and generate_atom %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag|e }} section ({{ language }})" href="{{ _link(kind + "_atom", tag, language) }}">
- {% endfor %}
- {% elif generate_atom %}
- <link rel="alternate" type="application/atom+xml" title="Atom for the {{ tag|e }} section" href="{{ _link("tag" + "_atom", tag) }}">
- {% endif %}
+ {{ feeds_translations.head(tag, kind, rss_override=False) }}
{% endblock %}
diff --git a/nikola/data/themes/base-jinja/templates/tags.tmpl b/nikola/data/themes/base-jinja/templates/tags.tmpl
index 936ed21..931ee71 100644
--- a/nikola/data/themes/base-jinja/templates/tags.tmpl
+++ b/nikola/data/themes/base-jinja/templates/tags.tmpl
@@ -1,10 +1,18 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block extra_head %}
+ {{ feeds_translations.head(kind=kind, feeds=False) }}
+{% endblock %}
{% block content %}
<article class="tagindex">
<header>
<h1>{{ title|e }}</h1>
+ <div class="metadata">
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
</header>
{% if cat_items %}
{% if items %}
diff --git a/nikola/data/themes/base-jinja/templates/crumbs.tmpl b/nikola/data/themes/base-jinja/templates/ui_helper.tmpl
index 970d509..9eb5697 100644
--- a/nikola/data/themes/base-jinja/templates/crumbs.tmpl
+++ b/nikola/data/themes/base-jinja/templates/ui_helper.tmpl
@@ -1,6 +1,5 @@
{# -*- coding: utf-8 -*- #}
-
-{% macro bar(crumbs) %}
+{% macro breadcrumbs(crumbs) %}
{% if crumbs %}
<nav class="breadcrumbs">
<ul class="breadcrumb">
diff --git a/nikola/data/themes/base/assets/css/baguetteBox.min.css b/nikola/data/themes/base/assets/css/baguetteBox.min.css
new file mode 120000
index 0000000..601d72f
--- /dev/null
+++ b/nikola/data/themes/base/assets/css/baguetteBox.min.css
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.css \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/css/html4css1.css b/nikola/data/themes/base/assets/css/html4css1.css
new file mode 100644
index 0000000..cc29335
--- /dev/null
+++ b/nikola/data/themes/base/assets/css/html4css1.css
@@ -0,0 +1 @@
+@import url("rst_base.css");
diff --git a/nikola/data/themes/base/assets/css/ipython.min.css b/nikola/data/themes/base/assets/css/ipython.min.css
index f9934c2..c1c6bc4 100644
--- a/nikola/data/themes/base/assets/css/ipython.min.css
+++ b/nikola/data/themes/base/assets/css/ipython.min.css
@@ -2,8 +2,8 @@
*
* IPython base
*
-*/.modal.fade .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}code{color:#000}pre{font-size:inherit;line-height:inherit}label{font-weight:normal}.border-box-sizing{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.corner-all{border-radius:2px}.no-padding{padding:0}.hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.hbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.vbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.hbox.reverse,.vbox.reverse,.reverse{-webkit-box-direction:reverse;-moz-box-direction:reverse;box-direction:reverse;flex-direction:row-reverse}.hbox.box-flex0,.vbox.box-flex0,.box-flex0{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none;width:auto}.hbox.box-flex1,.vbox.box-flex1,.box-flex1{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex,.vbox.box-flex,.box-flex{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex2,.vbox.box-flex2,.box-flex2{-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2}.box-group1{-webkit-box-flex-group:1;-moz-box-flex-group:1;box-flex-group:1}.box-group2{-webkit-box-flex-group:2;-moz-box-flex-group:2;box-flex-group:2}.hbox.start,.vbox.start,.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;justify-content:flex-start}.hbox.end,.vbox.end,.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;justify-content:flex-end}.hbox.center,.vbox.center,.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;justify-content:center}.hbox.baseline,.vbox.baseline,.baseline{-webkit-box-pack:baseline;-moz-box-pack:baseline;box-pack:baseline;justify-content:baseline}.hbox.stretch,.vbox.stretch,.stretch{-webkit-box-pack:stretch;-moz-box-pack:stretch;box-pack:stretch;justify-content:stretch}.hbox.align-start,.vbox.align-start,.align-start{-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.hbox.align-end,.vbox.align-end,.align-end{-webkit-box-align:end;-moz-box-align:end;box-align:end;align-items:flex-end}.hbox.align-center,.vbox.align-center,.align-center{-webkit-box-align:center;-moz-box-align:center;box-align:center;align-items:center}.hbox.align-baseline,.vbox.align-baseline,.align-baseline{-webkit-box-align:baseline;-moz-box-align:baseline;box-align:baseline;align-items:baseline}.hbox.align-stretch,.vbox.align-stretch,.align-stretch{-webkit-box-align:stretch;-moz-box-align:stretch;box-align:stretch;align-items:stretch}div.error{margin:2em;text-align:center}div.error>h1{font-size:500%;line-height:normal}div.error>p{font-size:200%;line-height:normal}div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}/*!
+*/.modal.fade .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}code{color:#000}pre{font-size:inherit;line-height:inherit}label{font-weight:normal}.border-box-sizing{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.corner-all{border-radius:2px}.no-padding{padding:0}.hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.hbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.vbox>*{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none}.hbox.reverse,.vbox.reverse,.reverse{-webkit-box-direction:reverse;-moz-box-direction:reverse;box-direction:reverse;flex-direction:row-reverse}.hbox.box-flex0,.vbox.box-flex0,.box-flex0{-webkit-box-flex:0;-moz-box-flex:0;box-flex:0;flex:none;width:auto}.hbox.box-flex1,.vbox.box-flex1,.box-flex1{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex,.vbox.box-flex,.box-flex{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.hbox.box-flex2,.vbox.box-flex2,.box-flex2{-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2}.box-group1{-webkit-box-flex-group:1;-moz-box-flex-group:1;box-flex-group:1}.box-group2{-webkit-box-flex-group:2;-moz-box-flex-group:2;box-flex-group:2}.hbox.start,.vbox.start,.start{-webkit-box-pack:start;-moz-box-pack:start;box-pack:start;justify-content:flex-start}.hbox.end,.vbox.end,.end{-webkit-box-pack:end;-moz-box-pack:end;box-pack:end;justify-content:flex-end}.hbox.center,.vbox.center,.center{-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;justify-content:center}.hbox.baseline,.vbox.baseline,.baseline{-webkit-box-pack:baseline;-moz-box-pack:baseline;box-pack:baseline;justify-content:baseline}.hbox.stretch,.vbox.stretch,.stretch{-webkit-box-pack:stretch;-moz-box-pack:stretch;box-pack:stretch;justify-content:stretch}.hbox.align-start,.vbox.align-start,.align-start{-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.hbox.align-end,.vbox.align-end,.align-end{-webkit-box-align:end;-moz-box-align:end;box-align:end;align-items:flex-end}.hbox.align-center,.vbox.align-center,.align-center{-webkit-box-align:center;-moz-box-align:center;box-align:center;align-items:center}.hbox.align-baseline,.vbox.align-baseline,.align-baseline{-webkit-box-align:baseline;-moz-box-align:baseline;box-align:baseline;align-items:baseline}.hbox.align-stretch,.vbox.align-stretch,.align-stretch{-webkit-box-align:stretch;-moz-box-align:stretch;box-align:stretch;align-items:stretch}div.error{margin:2em;text-align:center}div.error>h1{font-size:500%;line-height:normal}div.error>p{font-size:200%;line-height:normal}div.traceback-wrapper{text-align:left;max-width:800px;margin:auto}div.traceback-wrapper pre.traceback{max-height:600px;overflow:auto}/*!
*
* IPython notebook
*
-*/.ansibold{font-weight:bold}.ansiblack{color:black}.ansired{color:darkred}.ansigreen{color:darkgreen}.ansiyellow{color:#c4a000}.ansiblue{color:darkblue}.ansipurple{color:darkviolet}.ansicyan{color:steelblue}.ansigray{color:gray}.ansibgblack{background-color:black}.ansibgred{background-color:red}.ansibggreen{background-color:green}.ansibgyellow{background-color:yellow}.ansibgblue{background-color:blue}.ansibgpurple{background-color:magenta}.ansibgcyan{background-color:cyan}.ansibggray{background-color:gray}div.cell{border:1px solid transparent;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;border-radius:2px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:thin;border-style:solid;width:100%;padding:5px;margin:0;outline:none}div.cell.selected{border-color:#ababab}@media print{div.cell.selected{border-color:transparent}}.edit_mode div.cell.selected{border-color:green}@media print{.edit_mode div.cell.selected{border-color:transparent}}.prompt{min-width:14ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em}@media (max-width:540px){.prompt{text-align:left}}div.inner_cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}@-moz-document url-prefix(){div.inner_cell{overflow-x:hidden}}div.input_area{border:1px solid #cfcfcf;border-radius:2px;background:#f7f7f7;line-height:1.21429em}div.prompt:empty{padding-top:0;padding-bottom:0}div.unrecognized_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.unrecognized_cell .inner_cell{border-radius:2px;padding:5px;font-weight:bold;color:red;border:1px solid #cfcfcf;background:#eaeaea}div.unrecognized_cell .inner_cell a{color:inherit;text-decoration:none}div.unrecognized_cell .inner_cell a:hover{color:inherit;text-decoration:none}@media (max-width:540px){div.unrecognized_cell>div.prompt{display:none}}@media print{div.code_cell{page-break-inside:avoid}}div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media (max-width:540px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:navy;border-top:1px solid transparent}div.input_area>div.highlight{margin:.4em;border:none;padding:0;background-color:transparent}div.input_area>div.highlight>pre{margin:0;border:none;padding:0;background-color:transparent}.CodeMirror{line-height:1.21429em;font-size:14px;height:auto;background:none}.CodeMirror-scroll{overflow-y:hidden;overflow-x:auto}.CodeMirror-lines{padding:.4em}.CodeMirror-linenumber{padding:0 8px 0 4px}.CodeMirror-gutters{border-bottom-left-radius:2px;border-top-left-radius:2px}.CodeMirror pre{padding:0;border:0;border-radius:0}.highlight-base{color:#000}.highlight-variable{color:#000}.highlight-variable-2{color:#1a1a1a}.highlight-variable-3{color:#333}.highlight-string{color:#ba2121}.highlight-comment{color:#408080;font-style:italic}.highlight-number{color:#080}.highlight-atom{color:#88f}.highlight-keyword{color:#008000;font-weight:bold}.highlight-builtin{color:#008000}.highlight-error{color:#f00}.highlight-operator{color:#a2f;font-weight:bold}.highlight-meta{color:#a2f}.highlight-def{color:#00f}.highlight-string-2{color:#f50}.highlight-qualifier{color:#555}.highlight-bracket{color:#997}.highlight-tag{color:#170}.highlight-attribute{color:#00c}.highlight-header{color:blue}.highlight-quote{color:#090}.highlight-link{color:#00c}.cm-s-ipython span.cm-keyword{color:#008000;font-weight:bold}.cm-s-ipython span.cm-atom{color:#88f}.cm-s-ipython span.cm-number{color:#080}.cm-s-ipython span.cm-def{color:#00f}.cm-s-ipython span.cm-variable{color:#000}.cm-s-ipython span.cm-operator{color:#a2f;font-weight:bold}.cm-s-ipython span.cm-variable-2{color:#1a1a1a}.cm-s-ipython span.cm-variable-3{color:#333}.cm-s-ipython span.cm-comment{color:#408080;font-style:italic}.cm-s-ipython span.cm-string{color:#ba2121}.cm-s-ipython span.cm-string-2{color:#f50}.cm-s-ipython span.cm-meta{color:#a2f}.cm-s-ipython span.cm-qualifier{color:#555}.cm-s-ipython span.cm-builtin{color:#008000}.cm-s-ipython span.cm-bracket{color:#997}.cm-s-ipython span.cm-tag{color:#170}.cm-s-ipython span.cm-attribute{color:#00c}.cm-s-ipython span.cm-header{color:blue}.cm-s-ipython span.cm-quote{color:#090}.cm-s-ipython span.cm-link{color:#00c}.cm-s-ipython span.cm-error{color:#f00}.cm-s-ipython span.cm-tab{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);background-position:right;background-repeat:no-repeat}div.output_wrapper{position:relative;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;z-index:1}div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:2px;-webkit-box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);display:block}div.output_collapsed{margin:0;padding:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}div.out_prompt_overlay{height:100%;padding:0 .4em;position:absolute;border-radius:2px}div.out_prompt_overlay:hover{-webkit-box-shadow:inset 0 0 1px #000;box-shadow:inset 0 0 1px #000;background:rgba(240,240,240,0.5)}div.output_prompt{color:darkred}div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.output_area .MathJax_Display{text-align:left !important}div.output_area .rendered_html table{margin-left:0;margin-right:0}div.output_area .rendered_html img{margin-left:0;margin-right:0}div.output_area img,div.output_area svg{max-width:100%;height:auto}div.output_area img.unconfined,div.output_area svg.unconfined{max-width:none}.output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}@media (max-width:540px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;vertical-align:baseline;color:black;background-color:transparent;border-radius:0}div.output_subarea{overflow-x:auto;padding:.4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1;max-width:calc(100% - 14ex)}div.output_text{text-align:left;color:#000;line-height:1.21429em}div.output_stderr{background:#fdd}div.output_latex{text-align:left}div.output_javascript:empty{padding:0}.js-error{color:darkred}div.raw_input_container{font-family:monospace;padding-top:5px}input.raw_input{font-family:inherit;font-size:inherit;color:inherit;width:auto;vertical-align:baseline;padding:0 .25em;margin:0 .25em}input.raw_input:focus{box-shadow:none}p.p-space{margin-bottom:10px}div.output_unrecognized{padding:5px;font-weight:bold;color:red}div.output_unrecognized a{color:inherit;text-decoration:none}div.output_unrecognized a:hover{color:inherit;text-decoration:none}.rendered_html{color:#000}.rendered_html em{font-style:italic}.rendered_html strong{font-weight:bold}.rendered_html u{text-decoration:underline}.rendered_html :link{text-decoration:underline}.rendered_html :visited{text-decoration:underline}.rendered_html h1{font-size:185.7%;margin:1.08em 0 0 0;font-weight:bold;line-height:1}.rendered_html h2{font-size:157.1%;margin:1.27em 0 0 0;font-weight:bold;line-height:1}.rendered_html h3{font-size:128.6%;margin:1.55em 0 0 0;font-weight:bold;line-height:1}.rendered_html h4{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1}.rendered_html h5{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1;font-style:italic}.rendered_html h6{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1;font-style:italic}.rendered_html h1:first-child{margin-top:.538em}.rendered_html h2:first-child{margin-top:.636em}.rendered_html h3:first-child{margin-top:.777em}.rendered_html h4:first-child{margin-top:1em}.rendered_html h5:first-child{margin-top:1em}.rendered_html h6:first-child{margin-top:1em}.rendered_html ul{list-style:disc;margin:0 2em;padding-left:0}.rendered_html ul ul{list-style:square;margin:0 2em}.rendered_html ul ul ul{list-style:circle;margin:0 2em}.rendered_html ol{list-style:decimal;margin:0 2em;padding-left:0}.rendered_html ol ol{list-style:upper-alpha;margin:0 2em}.rendered_html ol ol ol{list-style:lower-alpha;margin:0 2em}.rendered_html ol ol ol ol{list-style:lower-roman;margin:0 2em}.rendered_html ol ol ol ol ol{list-style:decimal;margin:0 2em}.rendered_html *+ul{margin-top:1em}.rendered_html *+ol{margin-top:1em}.rendered_html hr{color:black;background-color:black}.rendered_html pre{margin:1em 2em}.rendered_html pre,.rendered_html code{border:0;background-color:#fff;color:#000;font-size:100%;padding:0}.rendered_html blockquote{margin:1em 2em}.rendered_html table{margin-left:auto;margin-right:auto;border:1px solid black;border-collapse:collapse}.rendered_html tr,.rendered_html th,.rendered_html td{border:1px solid black;border-collapse:collapse;margin:1em 2em}.rendered_html td,.rendered_html th{text-align:left;vertical-align:middle;padding:4px}.rendered_html th{font-weight:bold}.rendered_html *+table{margin-top:1em}.rendered_html p{text-align:left}.rendered_html *+p{margin-top:1em}.rendered_html img{display:block;margin-left:auto;margin-right:auto}.rendered_html *+img{margin-top:1em}.rendered_html img,.rendered_html svg{max-width:100%;height:auto}.rendered_html img.unconfined,.rendered_html svg.unconfined{max-width:none}div.text_cell{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media (max-width:540px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:none;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden}h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible}.text_cell.rendered .input_area{display:none}.text_cell.rendered .rendered_html{overflow-x:auto}.text_cell.unrendered .text_cell_render{display:none}.cm-header-1,.cm-header-2,.cm-header-3,.cm-header-4,.cm-header-5,.cm-header-6{font-weight:bold;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.cm-header-1{font-size:185.7%}.cm-header-2{font-size:157.1%}.cm-header-3{font-size:128.6%}.cm-header-4{font-size:110%}.cm-header-5{font-size:100%;font-style:italic}.cm-header-6{font-size:100%;font-style:italic}.widget-interact>div,.widget-interact>input{padding:2.5px}.widget-area{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-area .widget-subarea{padding:.44em .4em .4em 1px;margin-left:6px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:2;-moz-box-flex:2;box-flex:2;flex:2;-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.widget-area.connection-problems .prompt:after{content:"\f127";font-family:'FontAwesome';color:#d9534f;font-size:14px;top:3px;padding:3px}.slide-track{border:1px solid #ccc;background:#fff;border-radius:2px}.widget-hslider{padding-left:8px;padding-right:2px;overflow:visible;width:350px;height:5px;max-height:5px;margin-top:13px;margin-bottom:10px;border:1px solid #ccc;background:#fff;border-radius:2px;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-hslider .ui-slider{border:0;background:none;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.widget-hslider .ui-slider .ui-slider-handle{width:12px;height:28px;margin-top:-8px;border-radius:2px}.widget-hslider .ui-slider .ui-slider-range{height:12px;margin-top:-4px;background:#eee}.widget-vslider{padding-bottom:5px;overflow:visible;width:5px;max-width:5px;height:250px;margin-left:12px;border:1px solid #ccc;background:#fff;border-radius:2px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.widget-vslider .ui-slider{border:0;background:none;margin-left:-4px;margin-top:5px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.widget-vslider .ui-slider .ui-slider-handle{width:28px;height:12px;margin-left:-9px;border-radius:2px}.widget-vslider .ui-slider .ui-slider-range{width:12px;margin-left:-1px;background:#eee}.widget-text{width:350px;margin:0}.widget-listbox{width:350px;margin-bottom:0}.widget-numeric-text{width:150px;margin:0}.widget-progress{margin-top:6px;min-width:350px}.widget-progress .progress-bar{-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none}.widget-combo-btn{min-width:125px}.widget_item .dropdown-menu li a{color:inherit}.widget-hbox{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}.widget-hbox input[type="checkbox"]{margin-top:9px;margin-bottom:10px}.widget-hbox .widget-label{min-width:10ex;padding-right:8px;padding-top:5px;text-align:right;vertical-align:text-top}.widget-hbox .widget-readout{padding-left:8px;padding-top:5px;text-align:left;vertical-align:text-top}.widget-vbox{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}.widget-vbox .widget-label{padding-bottom:5px;text-align:center;vertical-align:text-bottom}.widget-vbox .widget-readout{padding-top:5px;text-align:center;vertical-align:text-top}.widget-box{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-webkit-box-align:start;-moz-box-align:start;box-align:start;align-items:flex-start}.widget-radio-box{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding-top:4px}.widget-radio-box label{margin-top:0;margin-left:20px}/*# sourceMappingURL=ipython.min.css.map */ \ No newline at end of file
+*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-fg{color:#282c36}.ansi-black-intense-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-fg{color:#b22b31}.ansi-red-intense-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-fg{color:#007427}.ansi-green-intense-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-fg{color:#b27d12}.ansi-yellow-intense-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-fg{color:#0065ca}.ansi-blue-intense-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-fg{color:#a03196}.ansi-magenta-intense-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-fg{color:#258f8f}.ansi-cyan-intense-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-fg{color:#a1a6b2}.ansi-white-intense-bg{background-color:#a1a6b2}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}.ansi-inverse{outline:.5px dotted}.ansibold{font-weight:bold}.ansiblack{color:black}.ansired{color:darkred}.ansigreen{color:darkgreen}.ansiyellow{color:#c4a000}.ansiblue{color:darkblue}.ansipurple{color:darkviolet}.ansicyan{color:steelblue}.ansigray{color:gray}.ansibgblack{background-color:black}.ansibgred{background-color:red}.ansibggreen{background-color:green}.ansibgyellow{background-color:yellow}.ansibgblue{background-color:blue}.ansibgpurple{background-color:magenta}.ansibgcyan{background-color:cyan}.ansibggray{background-color:gray}div.cell{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;border-radius:2px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;border-width:1px;border-style:solid;border-color:transparent;width:100%;padding:5px;margin:0;outline:0;position:relative;overflow:visible}div.cell:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:transparent}div.cell.jupyter-soft-selected{border-left-color:#e3f2fd;border-left-width:1px;padding-left:5px;border-right-color:#e3f2fd;border-right-width:1px;background:#e3f2fd}@media print{div.cell.jupyter-soft-selected{border-color:transparent}}div.cell.selected,div.cell.selected.jupyter-soft-selected{border-color:#ababab}div.cell.selected:before,div.cell.selected.jupyter-soft-selected:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:#42a5f5}@media print{div.cell.selected,div.cell.selected.jupyter-soft-selected{border-color:transparent}}.edit_mode div.cell.selected{border-color:#66bb6a}.edit_mode div.cell.selected:before{position:absolute;display:block;top:-1px;left:-1px;width:5px;height:calc(100%+2px);content:'';background:#66bb6a}@media print{.edit_mode div.cell.selected{border-color:transparent}}.prompt{min-width:14ex;padding:.4em;margin:0;font-family:monospace;text-align:right;line-height:1.21429em;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@media(max-width:540px){.prompt{text-align:left}}div.inner_cell{min-width:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}div.input_area>div.highlight>pre{border:1px solid #cfcfcf;line-height:1.21429em}div.prompt:empty{padding-top:0;padding-bottom:0}div.unrecognized_cell{padding:5px 5px 5px 0;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.unrecognized_cell .inner_cell{border-radius:2px;padding:5px;font-weight:bold;color:red;border:1px solid #cfcfcf;background:#eaeaea}div.unrecognized_cell .inner_cell a{color:inherit;text-decoration:none}div.unrecognized_cell .inner_cell a:hover{color:inherit;text-decoration:none}@media(max-width:540px){div.unrecognized_cell>div.prompt{display:none}}@media print{div.code_cell{page-break-inside:avoid}}div.input{page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media(max-width:540px){div.input{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.input_prompt{color:#303f9f;border-top:1px solid transparent}div.input_area>div.highlight{margin:0;border:0;padding:0;background-color:transparent}div.input_area>div.highlight>pre{margin:0;border:0;padding:.4em}.CodeMirror{line-height:1.21429em;font-size:14px;height:auto;background:0}.CodeMirror-scroll{overflow-y:hidden;overflow-x:auto}.CodeMirror-lines{padding:.4em}.CodeMirror-linenumber{padding:0 8px 0 4px}.CodeMirror-gutters{border-bottom-left-radius:2px;border-top-left-radius:2px}.CodeMirror pre{padding:0;border:0;border-radius:0}.highlight-base{color:#000}.highlight-variable{color:#000}.highlight-variable-2{color:#1a1a1a}.highlight-variable-3{color:#333}.highlight-string{color:#ba2121}.highlight-comment{color:#408080;font-style:italic}.highlight-number{color:#080}.highlight-atom{color:#88F}.highlight-keyword{color:#008000;font-weight:bold}.highlight-builtin{color:#008000}.highlight-error{color:red}.highlight-operator{color:#a2f;font-weight:bold}.highlight-meta{color:#a2f}.highlight-def{color:#00f}.highlight-string-2{color:#f50}.highlight-qualifier{color:#555}.highlight-bracket{color:#997}.highlight-tag{color:#170}.highlight-attribute{color:#00c}.highlight-header{color:blue}.highlight-quote{color:#090}.highlight-link{color:#00c}.cm-s-ipython span.cm-keyword{color:#008000;font-weight:bold}.cm-s-ipython span.cm-atom{color:#88F}.cm-s-ipython span.cm-number{color:#080}.cm-s-ipython span.cm-def{color:#00f}.cm-s-ipython span.cm-variable{color:#000}.cm-s-ipython span.cm-operator{color:#a2f;font-weight:bold}.cm-s-ipython span.cm-variable-2{color:#1a1a1a}.cm-s-ipython span.cm-variable-3{color:#333}.cm-s-ipython span.cm-comment{color:#408080;font-style:italic}.cm-s-ipython span.cm-string{color:#ba2121}.cm-s-ipython span.cm-string-2{color:#f50}.cm-s-ipython span.cm-meta{color:#a2f}.cm-s-ipython span.cm-qualifier{color:#555}.cm-s-ipython span.cm-builtin{color:#008000}.cm-s-ipython span.cm-bracket{color:#997}.cm-s-ipython span.cm-tag{color:#170}.cm-s-ipython span.cm-attribute{color:#00c}.cm-s-ipython span.cm-header{color:blue}.cm-s-ipython span.cm-quote{color:#090}.cm-s-ipython span.cm-link{color:#00c}.cm-s-ipython span.cm-error{color:red}.cm-s-ipython span.cm-tab{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);background-position:right;background-repeat:no-repeat}div.output_wrapper{position:relative;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch;z-index:1}div.output_scroll{height:24em;width:100%;overflow:auto;border-radius:2px;-webkit-box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);box-shadow:inset 0 2px 8px rgba(0,0,0,0.8);display:block}div.output_collapsed{margin:0;padding:0;display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}div.out_prompt_overlay{height:100%;padding:0 .4em;position:absolute;border-radius:2px}div.out_prompt_overlay:hover{-webkit-box-shadow:inset 0 0 1px #000;box-shadow:inset 0 0 1px #000;background:rgba(240,240,240,0.5)}div.output_prompt{color:#d84315}div.output_area{padding:0;page-break-inside:avoid;display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}div.output_area .MathJax_Display{text-align:left !important}div.output_area .rendered_html table{margin-left:0;margin-right:0}div.output_area .rendered_html img{margin-left:0;margin-right:0}div.output_area img,div.output_area svg{max-width:100%;height:auto}div.output_area img.unconfined,div.output_area svg.unconfined{max-width:none}div.output_area .mglyph>img{max-width:none}.output{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}@media(max-width:540px){div.output_area{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:vertical;-moz-box-align:stretch;display:box;box-orient:vertical;box-align:stretch;display:flex;flex-direction:column;align-items:stretch}}div.output_area pre{margin:0;padding:0;border:0;vertical-align:baseline;color:black;background-color:transparent;border-radius:0}div.output_subarea{overflow-x:auto;padding:.4em;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1;max-width:calc(100% - 14ex)}div.output_scroll div.output_subarea{overflow-x:visible}div.output_text{text-align:left;color:#000;line-height:1.21429em}div.output_stderr{background:#fdd}div.output_latex{text-align:left}div.output_javascript:empty{padding:0}.js-error{color:darkred}div.raw_input_container{line-height:1.21429em;padding-top:5px}input.raw_input{font-family:monospace;font-size:inherit;color:inherit;width:auto;vertical-align:baseline;padding:0 .25em;margin:0 .25em}input.raw_input:focus{box-shadow:none}p.p-space{margin-bottom:10px}div.output_unrecognized{padding:5px;font-weight:bold;color:red}div.output_unrecognized a{color:inherit;text-decoration:none}div.output_unrecognized a:hover{color:inherit;text-decoration:none}.rendered_html{color:#000}.rendered_html em{font-style:italic}.rendered_html strong{font-weight:bold}.rendered_html u{text-decoration:underline}.rendered_html :link{text-decoration:underline}.rendered_html :visited{text-decoration:underline}.rendered_html h1{font-size:185.7%;margin:1.08em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h2{font-size:157.1%;margin:1.27em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h3{font-size:128.6%;margin:1.55em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h4{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0}.rendered_html h5{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0;font-style:italic}.rendered_html h6{font-size:100%;margin:2em 0 0 0;font-weight:bold;line-height:1.0;font-style:italic}.rendered_html h1:first-child{margin-top:.538em}.rendered_html h2:first-child{margin-top:.636em}.rendered_html h3:first-child{margin-top:.777em}.rendered_html h4:first-child{margin-top:1em}.rendered_html h5:first-child{margin-top:1em}.rendered_html h6:first-child{margin-top:1em}.rendered_html ul:not(.list-inline),.rendered_html ol:not(.list-inline){padding-left:2em}.rendered_html ul{list-style:disc}.rendered_html ul ul{list-style:square}.rendered_html ul ul ul{list-style:circle}.rendered_html ol{list-style:decimal}.rendered_html ol ol{list-style:upper-alpha}.rendered_html ol ol ol{list-style:lower-alpha}.rendered_html ol ol ol ol{list-style:lower-roman}.rendered_html ol ol ol ol ol{list-style:decimal}.rendered_html *+ul{margin-top:1em}.rendered_html *+ol{margin-top:1em}.rendered_html hr{color:black;background-color:black}.rendered_html pre{margin:1em 2em}.rendered_html pre,.rendered_html code{border:0;background-color:#fff;color:#000;font-size:100%;padding:0}.rendered_html blockquote{margin:1em 2em}.rendered_html table{margin-left:auto;margin-right:auto;border:0;border-collapse:collapse;border-spacing:0;color:black;font-size:12px;table-layout:fixed}.rendered_html thead{border-bottom:1px solid black;vertical-align:bottom}.rendered_html tr,.rendered_html th,.rendered_html td{text-align:right;vertical-align:middle;padding:.5em .5em;line-height:normal;white-space:normal;max-width:none;border:0}.rendered_html th{font-weight:bold}.rendered_html tbody tr:nth-child(odd){background:#f5f5f5}.rendered_html tbody tr:hover{background:rgba(66,165,245,0.2)}.rendered_html *+table{margin-top:1em}.rendered_html p{text-align:left}.rendered_html *+p{margin-top:1em}.rendered_html img{display:block;margin-left:auto;margin-right:auto}.rendered_html *+img{margin-top:1em}.rendered_html img,.rendered_html svg{max-width:100%;height:auto}.rendered_html img.unconfined,.rendered_html svg.unconfined{max-width:none}.rendered_html .alert{margin-bottom:initial}.rendered_html *+.alert{margin-top:1em}div.text_cell{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch}@media(max-width:540px){div.text_cell>div.prompt{display:none}}div.text_cell_render{outline:0;resize:none;width:inherit;border-style:none;padding:.5em .5em .5em .4em;color:#000;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}a.anchor-link:link{text-decoration:none;padding:0 20px;visibility:hidden}h1:hover .anchor-link,h2:hover .anchor-link,h3:hover .anchor-link,h4:hover .anchor-link,h5:hover .anchor-link,h6:hover .anchor-link{visibility:visible}.text_cell.rendered .input_area{display:none}.text_cell.rendered .rendered_html{overflow-x:auto;overflow-y:hidden}.text_cell.rendered .rendered_html tr,.text_cell.rendered .rendered_html th,.text_cell.rendered .rendered_html td{max-width:none}.text_cell.unrendered .text_cell_render{display:none}.text_cell .dropzone .input_area{border:2px dashed #bababa;margin:-1px}.cm-header-1,.cm-header-2,.cm-header-3,.cm-header-4,.cm-header-5,.cm-header-6{font-weight:bold;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.cm-header-1{font-size:185.7%}.cm-header-2{font-size:157.1%}.cm-header-3{font-size:128.6%}.cm-header-4{font-size:110%}.cm-header-5{font-size:100%;font-style:italic}.cm-header-6{font-size:100%;font-style:italic}
diff --git a/nikola/data/themes/base/assets/css/ipython.min.css.map b/nikola/data/themes/base/assets/css/ipython.min.css.map
deleted file mode 100644
index 3e36e5e..0000000
--- a/nikola/data/themes/base/assets/css/ipython.min.css.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../base/less/variables.less","../components/bootstrap/less/mixins/vendor-prefixes.less","../base/less/mixins.less","../base/less/flexbox.less","../base/less/error.less","../notebook/less/ansicolors.less","../notebook/less/cell.less","../notebook/less/codecell.less","../notebook/less/codemirror.less","../notebook/less/highlight.less","../components/codemirror/lib/codemirror.css","../notebook/less/outputarea.less","../notebook/less/renderedhtml.less","../notebook/less/textcell.less","../components/bootstrap/less/variables.less","../widgets/less/widgets.less","../components/font-awesome/less/variables.less"],"names":[],"mappings":";;;;EAqBE,MAAC,KAAM,eCyHP,kBAAmB,eAAnB,CACI,cAAe,eAAf,CACC,aAAc,eAAd,CACG,UAAW,gBDtHrB,KACE,WAIF,IAGE,iBAAA,CACA,oBAIF,MACI,mBEvCJ,mBACI,qBAAA,CACA,0BAAA,CACA,8BAGJ,YACI,kBAOJ,YACI,UCGJ,MAEI,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBAGJ,KAAM,GAEF,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,UAGJ,MAEI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBAGJ,KAAM,GAEF,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,UAGJ,KAAK,SACL,KAAK,SACL,SAEI,6BAAA,CACA,0BAAA,CACA,qBAAA,CAGA,2BAGJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,SAAA,CACA,WAGJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAGJ,KAAK,UACL,KAAK,UACL,UAVI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAUJ,KAAK,WACL,KAAK,WACL,WAEI,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OAGJ,YAEI,wBAAA,CACA,qBAAA,CACA,iBAGJ,YAEI,wBAAA,CACA,qBAAA,CACA,iBAGJ,KAAK,OACL,KAAK,OACL,OAEI,sBAAA,CACA,mBAAA,CACA,cAAA,CAGA,2BAGJ,KAAK,KACL,KAAK,KACL,KAEI,oBAAA,CACA,iBAAA,CACA,YAAA,CAGA,yBAGJ,KAAK,QACL,KAAK,QACL,QAEI,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBAGJ,KAAK,UACL,KAAK,UACL,UAEI,yBAAA,CACA,sBAAA,CACA,iBAAA,CAGA,yBAGJ,KAAK,SACL,KAAK,SACL,SAEI,wBAAA,CACA,qBAAA,CACA,gBAAA,CAGA,wBAGJ,KAAK,aACL,KAAK,aACL,aAEI,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBAGJ,KAAK,WACL,KAAK,WACL,WAEI,qBAAA,CACA,kBAAA,CACA,aAAA,CAGA,qBAGJ,KAAK,cACL,KAAK,cACL,cAEI,wBAAA,CACA,qBAAA,CACA,gBAAA,CAGA,mBAGJ,KAAK,gBACL,KAAK,gBACL,gBAEI,0BAAA,CACA,uBAAA,CACA,kBAAA,CAGA,qBAGJ,KAAK,eACL,KAAK,eACL,eAEI,yBAAA,CACA,sBAAA,CACA,iBAAA,CAGA,oBC3QJ,GAAG,OACD,UAAA,CACA,kBAGF,GAAG,MAAO,IACN,cAAA,CACA,mBAGJ,GAAG,MAAO,GACN,cAAA,CACA,mBAGJ,GAAG,mBACC,eAAA,CACA,eAAA,CACA;;;;EChBJ,UAAW,iBAGX,WAAY,YACZ,SAAU,cACV,WAAY,gBACZ,YAAa,cACb,UAAW,eACX,YAAa,iBACb,UAAW,gBACX,UAAW,WAGX,aAAc,uBACd,WAAY,qBACZ,aAAc,uBACd,cAAe,wBACf,YAAa,sBACb,cAAe,yBACf,YAAa,sBACb,YAAa,sBCtBb,GAAG,MACC,4BAAA,CHmDA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CD1DA,iBAAA,CANA,qBAAA,CACA,0BAAA,CACA,6BAAA,CIAA,iBAAA,CACA,kBAAA,CAkBA,UAAA,CACA,WAAA,CAEA,QAAA,CACA,aApBA,GARD,KAQE,UACG,qBAKJ,aAAA,GAdD,KAQE,UAIO,0BAIR,UAAW,IAhBZ,KAgBa,UACR,mBAKJ,aAAA,UANW,IAhBZ,KAgBa,UAIJ,0BAWZ,QAEI,cAAA,CAEA,YAAA,CACA,QAAA,CACA,qBAAA,CACA,gBAAA,CAEA,sBAWJ,QARmC,iBAG/B,QACI,iBAIR,GAAG,YHCC,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA0CA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OG1DJ,4BACI,GAAG,YAGC,mBAKR,GAAG,YACC,wBAAA,CJzDA,iBAAA,CI2DA,kBAAA,CACA,sBAMJ,GAAG,OAAO,OACN,aAAA,CACA,iBAGJ,GAAG,mBAEC,qBAAA,CH5DA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBG2CJ,GAAG,kBAKC,aJxEF,iBAAA,CI0EM,WAAA,CACA,gBAAA,CACA,SAAA,CACA,wBAAA,CACA,mBAXR,GAAG,kBAKC,YAQI,GACI,aAAA,CACA,qBAEA,GAjBT,kBAKC,YAQI,EAIK,OACG,aAAA,CACA,qBAWhB,QANmC,iBAE/B,GAAG,kBAAmB,IAAK,QACvB,cCtGR,aAAA,GALG,WAGK,yBAQR,GAAG,OACC,uBAAA,CJUA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBIbJ,QARmC,iBAE/B,GAAG,OJkCH,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,qBI3CJ,GAAG,cACC,UAAA,CACA,iCAQJ,GAAG,WAAY,IAAK,WAChB,WAAA,CACA,WAAA,CACA,SAAA,CACA,6BAGJ,GAAG,WAAY,IAAK,UAAW,KAC3B,QAAA,CACA,WAAA,CACA,SAAA,CACA,6BClCJ,YACI,qBAAA,CACA,cAAA,CACA,WAAA,CACA,gBAGJ,mBAGI,iBAAA,CACA,gBAGJ,kBAGI,aAGJ,uBAGI,oBAGJ,oBAGI,6BAAA,CACA,2BAGJ,WAAY,KAGR,SAAA,CACA,QAAA,CNnCF,gBOFF,gBACE,WAGF,oBAHE,WAOF,sBACE,cAGF,sBACE,WAGF,kBACE,cAGF,mBACE,aAAA,CACA,kBAGF,kBACE,WAGF,gBACE,WAGF,mBACE,aAAA,CACA,iBAGF,mBACE,cAGF,iBACE,WAGF,oBACE,UAAA,CACA,iBAGF,gBACE,WAIF,eC8BuB,WD3BvB,oBCoC4B,WDnC5B,qBCqC6B,WDpC7B,mBCsC2B,WDrC3B,eCsCuB,WDrCvB,qBCsC6B,WDrC7B,kBCsC0B,WDrC1B,iBCsCyB,WDrCzB,gBCuCwB,WDlCtB,aADY,KACX,YArCD,aAAA,CACA,iBAqCA,aAFY,KAEX,SA1CD,WA2CA,aAHY,KAGX,WA/CD,WAgDA,aAJY,KAIX,QCYoB,WDXrB,aALY,KAKX,aA1ED,WA6EA,aARY,KAQX,aA/BD,UAAA,CACA,iBA+BA,aATY,KASX,eAtED,cAuEA,aAVY,KAUX,eAnED,WAoEA,aAXY,KAWX,YA5DD,aAAA,CACA,kBA4DA,aAZY,KAYX,WAjED,cAkEA,aAbY,KAaX,aCYyB,WDX1B,aAdY,KAcX,SAhCD,WAiCA,aAfY,KAeX,cCY0B,WDX3B,aAhBY,KAgBX,YA/CD,cAgDA,aAjBY,KAiBX,YCYwB,WDXzB,aAlBY,KAkBX,QCYoB,WDXrB,aAnBY,KAmBX,cCY0B,WDX3B,aApBY,KAoBX,WCYuB,WDXxB,aArBY,KAqBX,UCYsB,WDXvB,aAtBY,KAsBX,SCaqB,WDZtB,aAvBY,KAuBX,UAlDD,WAoDA,aAzBY,KAyBX,QACC,sQAAA,CACA,yBAAA,CACA,4BE7GJ,GAAG,gBAEC,iBAAA,CRkDA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CQ9DA,UAIJ,GAAG,eAEC,WAAA,CAEA,UAAA,CAEA,aAAA,CTNA,iBAAA,CD2DF,kDAAA,CACQ,0CAAA,CUnDN,cAIJ,GAAG,kBACC,QAAA,CACA,SAAA,CR4BA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBQvCJ,GAAG,oBACC,WAAA,CACA,cAAA,CACA,iBAAA,CTtBA,kBS0BJ,GAAG,mBAAmB,OViCpB,qCAAA,CACQ,6BAAA,CU/BN,iCAGJ,GAAG,eACC,cAIJ,GAAG,aACC,SAAA,CACA,uBAAA,CR1BA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBQSJ,GAAG,YAKC,kBAEI,eAAA,YAPR,GAAG,YAUC,eAEI,OACI,aAAA,CACA,eAdZ,GAAG,YAUC,eAOI,KACI,aAAA,CACA,eAnBZ,GAAG,YAuBC,KAvBJ,GAAG,YAuBM,KACD,cAAA,CACA,YACA,GA1BL,YAuBC,IAGK,YAAD,GA1BL,YAuBM,IAGA,YACG,eAOZ,QR5BI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBQwBJ,QAPmC,iBAE/B,GAAG,aRlCH,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,qBQwBJ,GAAG,YAAa,KACZ,QAAA,CACA,SAAA,CACA,QAAA,CACA,uBAAA,CACA,WAAA,CACA,4BAAA,CTpFF,gBS0FF,GAAG,gBAEC,eAAA,CACA,YAAA,CRGA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,MAAA,CQLA,UAAW,kBAOf,GAAG,aACC,eAAA,CACA,UAAA,CAEA,sBAUJ,GAAG,eACC,gBAGJ,GAAG,cACC,gBAaJ,GAAG,kBAAkB,OACjB,UAGJ,UACI,cAKJ,GAAG,qBACC,qBAAA,CAGA,gBAOJ,KAAK,WACD,mBAAA,CACA,iBAAA,CACA,aAAA,CACA,UAAA,CAEA,uBAAA,CAEA,eAAA,CACA,eAGJ,KAAK,UAAU,OACX,gBAGJ,CAAC,SACG,mBAGJ,GAAG,qBACD,WAAA,CACA,gBAAA,CACA,UAHF,GAAG,oBAKD,GACI,aAAA,CACA,qBAEA,GATH,oBAKD,EAIK,OACG,aAAA,CACA,qBCxMV,eAEI,WAFJ,cAGI,IAAI,kBAHR,cAII,QAAQ,iBAJZ,cAKI,GAAG,0BALP,cAMI,OAAO,0BANX,cAOI,UAAU,0BAPd,cAYI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAZpE,cAaI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAbpE,cAcI,IAAI,gBAAA,CAAmB,mBAAA,CAAsB,gBAAA,CAAmB,cAdpE,cAeI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,cAf/D,cAgBI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,aAAA,CAAkB,kBAhBjF,cAiBI,IAAI,cAAA,CAAiB,gBAAA,CAAmB,gBAAA,CAAmB,aAAA,CAAkB,kBAjBjF,cAoBI,GAAE,aAAc,kBApBpB,cAqBI,GAAE,aAAc,kBArBpB,cAsBI,GAAE,aAAc,kBAtBpB,cAuBI,GAAE,aAAc,eAvBpB,cAwBI,GAAE,aAAc,eAxBpB,cAyBI,GAAE,aAAc,eAzBpB,cA2BI,IAAI,eAAA,CAAiB,YAAA,CAAiB,eA3B1C,cA4BI,GAAG,IAAI,iBAAA,CAAmB,aA5B9B,cA6BI,GAAG,GAAG,IAAI,iBAAA,CAAmB,aA7BjC,cA8BI,IAAI,kBAAA,CAAoB,YAAA,CAAiB,eA9B7C,cA+BI,GAAG,IAAI,sBAAA,CAAwB,aA/BnC,cAgCI,GAAG,GAAG,IAAI,sBAAA,CAAwB,aAhCtC,cAiCI,GAAG,GAAG,GAAG,IAAI,sBAAA,CAAwB,aAjCzC,cAmCI,GAAG,GAAG,GAAG,GAAG,IAAI,kBAAA,CAAoB,aAnCxC,cAoCI,EAAE,IAAM,eApCZ,cAqCI,EAAE,IAAM,eArCZ,cAuCI,IACI,WAAA,CACA,uBAzCR,cA4CI,KAAK,eA5CT,cA8CI,KA9CJ,cA8CS,MACD,QAAA,CACA,qBAAA,CACA,UAAA,CACA,cAAA,CACA,UAnDR,cAsDI,YAAY,eAtDhB,cAwDI,OACI,gBAAA,CACA,iBAAA,CACA,sBAAA,CACA,yBA5DR,cA8DI,IA9DJ,cA8DQ,IA9DR,cA8DY,IACJ,sBAAA,CACA,wBAAA,CACA,eAjER,cAmEI,IAnEJ,cAmEQ,IACA,eAAA,CACA,qBAAA,CACA,YAtER,cAwEI,IAAI,iBAxER,cAyEI,EAAE,OAAS,eAzEf,cA2EI,GAAG,gBA3EP,cA4EI,EAAE,GAAK,eA5EX,cA8EI,KACI,aAAA,CACA,gBAAA,CACA,kBAjFR,cAmFI,EAAE,KAAO,eAnFb,cAqFI,KArFJ,cAqFS,KACD,cAAA,CACA,YACA,cAHJ,IAGK,YAAD,cAHC,IAGA,YACG,eCzFZ,GAAG,WVsBC,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBU3BJ,QAPmC,iBAE/B,GAAG,UAAW,IAAK,QACf,cAIR,GAAG,kBAEC,YAAA,CACA,WAAA,CACA,aAAA,CACA,iBAAA,CACA,2BAAA,CACA,UAAA,CXdA,qBAAA,CACA,0BAAA,CACA,8BWgBJ,CAAC,YAAY,MACX,oBAAA,CACA,cAAA,CACA,kBAIE,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cAAR,EAAC,MAAO,cACJ,mBAIR,UAAU,SAAU,aAChB,aAGJ,UAAU,SAAU,gBAChB,gBAGJ,UAAU,WAAY,mBAClB,aAGJ,aACA,aACA,aACA,aACA,aACA,aACI,gBAAA,CACA,YCRsB,4CDW1B,aAAe,iBACf,aAAe,iBACf,aAAe,iBACf,aAAe,eACf,aACI,cAAA,CACA,kBAEJ,aACI,cAAA,CACA,kBE7DJ,gBACI,KADJ,gBACU,OACF,cAIR,aAgBI,uBAAA,CZJA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBY3BJ,YAmBI,iBACI,2BAAA,CACA,eAAA,Cb5BJ,qBAAA,CACA,0BAAA,CACA,6BAAA,CC+CA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA6DA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,MAAA,CAiFA,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBYpLA,YAAC,oBAAqB,QAAO,OACzB,QC0Ec,OD1Ed,CACA,YAAa,aAAb,CACA,aAAA,CACA,cAAA,CACA,OAAA,CACA,YAOR,aAEI,qBAAA,CACA,eAAA,Cb9CA,kBamDJ,gBAuBI,gBAAA,CACA,iBAAA,CACA,gBAAA,CAGA,WAAA,CACA,UAAA,CACA,cAAA,CACA,eAAA,CACA,kBAAA,CAtCA,qBAAA,CACA,eAAA,Cb9CA,iBAAA,CCaA,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBYuBJ,eAwCI,YAEI,QAAA,CACA,eAAA,CZjFJ,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,mBAAA,CAwEA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OYtDJ,eAwCI,WAQI,mBACI,UAAA,CACA,WAAA,CACA,eAAA,CACA,kBApDZ,eAwCI,WAeI,kBACI,WAAA,CACA,eAAA,CACA,gBAKZ,gBAKI,kBAAA,CACA,gBAAA,CAGA,SAAA,CACA,aAAA,CACA,YAAA,CACA,gBAAA,CAjFA,qBAAA,CACA,eAAA,Cb9CA,iBAAA,CC2CA,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBYwDJ,eAoBI,YAEI,QAAA,CACA,eAAA,CACA,gBAAA,CACA,cAAA,CZhGJ,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CA0CA,kBAAA,CACA,eAAA,CACA,UAAA,CAGA,OYSJ,eAoBI,WAUI,mBACI,UAAA,CACA,WAAA,CACA,gBAAA,CACA,kBAlCZ,eAoBI,WAiBI,kBACI,UAAA,CACA,gBAAA,CACA,gBAKZ,aAEI,WAAA,CACA,SAGJ,gBAEI,WAAA,CACA,gBAGJ,qBAEI,WAAA,CACA,SAGJ,iBAEI,cAAA,CACA,gBAHJ,gBAKI,eAEI,uBAAA,CACA,oBAAA,CACA,mBAAA,CACA,kBAAA,CACA,gBAIR,kBAGI,gBAGJ,YAAa,eAAe,GAAG,GAC3B,cAGJ,aZ7LI,mBAAA,CACA,6BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,0BAAA,CACA,sBAAA,CAEA,WAAA,CACA,qBAAA,CACA,iBAAA,CAGA,YAAA,CACA,kBAAA,CACA,oBY8KJ,YAII,MAAK,kBACD,cAAA,CACA,mBANR,YASI,eAEI,cAAA,CACA,iBAAA,CACA,eAAA,CACA,gBAAA,CACA,wBAfR,YAkBI,iBACI,gBAAA,CACA,eAAA,CACA,eAAA,CACA,wBAIR,aZzLI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,oBY0KJ,YAKI,eAEI,kBAAA,CACA,iBAAA,CACA,2BATR,YAYI,iBAEI,eAAA,CACA,iBAAA,CACA,wBAKR,Yb/PI,qBAAA,CACA,0BAAA,CACA,6BAAA,CCiNA,uBAAA,CACA,oBAAA,CACA,eAAA,CAGA,uBY6CJ,kBZpNI,mBAAA,CACA,2BAAA,CACA,yBAAA,CAEA,gBAAA,CACA,wBAAA,CACA,sBAAA,CAEA,WAAA,CACA,mBAAA,CACA,iBAAA,CAGA,YAAA,CACA,qBAAA,CACA,mBAAA,CDhEA,qBAAA,CACA,0BAAA,CACA,6BAAA,CawQA,gBALJ,iBAOI,OACI,YAAA,CACA","file":"ipython.min.css"} \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/css/nikola_ipython.css b/nikola/data/themes/base/assets/css/nikola_ipython.css
index 5ae5189..aba37da 100644
--- a/nikola/data/themes/base/assets/css/nikola_ipython.css
+++ b/nikola/data/themes/base/assets/css/nikola_ipython.css
@@ -40,77 +40,17 @@ div.text_cell_render {
.rendered_html pre, .rendered_html code {
background-color: #DDDDDD;
+ margin: 1em 0em;
+ font-size: 14px;
+}
+
+.rendered_html pre {
padding-left: 0.5em;
padding-right: 0.5em;
padding-top: 0.05em;
padding-bottom: 0.05em;
- margin: 1em 0em;
- font-size: 14px;
}
.page-content > .content p {
margin: 0 0 0px;
}
-
-.highlight .hll { background-color: #ffffcc }
-.highlight { background: #f8f8f8; }
-.highlight .c { color: #408080; font-style: italic } /* Comment */
-.highlight .err { border: 1px solid #FF0000 } /* Error */
-.highlight .k { color: #008000; font-weight: bold } /* Keyword */
-.highlight .o { color: #666666 } /* Operator */
-.highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.highlight .cp { color: #BC7A00 } /* Comment.Preproc */
-.highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.highlight .cs { color: #408080; font-style: italic } /* Comment.Special */
-.highlight .gd { color: #A00000 } /* Generic.Deleted */
-.highlight .ge { font-style: italic } /* Generic.Emph */
-.highlight .gr { color: #FF0000 } /* Generic.Error */
-.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.highlight .gi { color: #00A000 } /* Generic.Inserted */
-.highlight .go { color: #888888 } /* Generic.Output */
-.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
-.highlight .gs { font-weight: bold } /* Generic.Strong */
-.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.highlight .gt { color: #0044DD } /* Generic.Traceback */
-.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
-.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
-.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
-.highlight .kp { color: #008000 } /* Keyword.Pseudo */
-.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
-.highlight .kt { color: #B00040 } /* Keyword.Type */
-.highlight .m { color: #666666 } /* Literal.Number */
-.highlight .s { color: #BA2121 } /* Literal.String */
-.highlight .na { color: #7D9029 } /* Name.Attribute */
-.highlight .nb { color: #008000 } /* Name.Builtin */
-.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */
-.highlight .no { color: #880000 } /* Name.Constant */
-.highlight .nd { color: #AA22FF } /* Name.Decorator */
-.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
-.highlight .nf { color: #0000FF } /* Name.Function */
-.highlight .nl { color: #A0A000 } /* Name.Label */
-.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
-.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */
-.highlight .nv { color: #19177C } /* Name.Variable */
-.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.highlight .w { color: #bbbbbb } /* Text.Whitespace */
-.highlight .mf { color: #666666 } /* Literal.Number.Float */
-.highlight .mh { color: #666666 } /* Literal.Number.Hex */
-.highlight .mi { color: #666666 } /* Literal.Number.Integer */
-.highlight .mo { color: #666666 } /* Literal.Number.Oct */
-.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */
-.highlight .sc { color: #BA2121 } /* Literal.String.Char */
-.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
-.highlight .s2 { color: #BA2121 } /* Literal.String.Double */
-.highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
-.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
-.highlight .sx { color: #008000 } /* Literal.String.Other */
-.highlight .sr { color: #BB6688 } /* Literal.String.Regex */
-.highlight .s1 { color: #BA2121 } /* Literal.String.Single */
-.highlight .ss { color: #19177C } /* Literal.String.Symbol */
-.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */
-.highlight .vc { color: #19177C } /* Name.Variable.Class */
-.highlight .vg { color: #19177C } /* Name.Variable.Global */
-.highlight .vi { color: #19177C } /* Name.Variable.Instance */
-.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/nikola/data/themes/base/assets/css/nikola_rst.css b/nikola/data/themes/base/assets/css/nikola_rst.css
new file mode 100644
index 0000000..71a0f84
--- /dev/null
+++ b/nikola/data/themes/base/assets/css/nikola_rst.css
@@ -0,0 +1,79 @@
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning, div.sidebar,
+div.system-message {
+/* stolen from Boostrap 4 (.card) */
+ margin-bottom: 2rem;
+ position: relative;
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff;
+ color: #212529;
+ background-clip: border-box;
+ border: 1px solid rgba(0,0,0,.125);
+ border-radius: .25rem;
+ padding: 0;
+}
+
+div.attention, div.caution, div.danger, div.error, div.warning {
+ /* stolen from Boostrap 3 (.border-danger) */
+ border-color: #dc3545!important;
+}
+
+div.admonition p, div.hint p,
+div.important p, div.note p,
+div.tip p, div.sidebar p,
+div.attention p, div.caution p,
+div.danger p, div.error p,
+div.warning p, div.system-message p {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title, div.sidebar p.sidebar-title,
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, div.system-message p.system-message-title {
+/* stolen from Boostrap 4 (.card .card-header) */
+ font-weight: 400;
+ font-size: 1.25rem;
+ padding: .75rem 1.25rem;
+ margin-bottom: 1rem;
+ background-color: rgba(0,0,0,.03);
+ border-bottom: 1px solid rgba(0,0,0,.125);
+}
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title, div.system-message p.system-message-title {
+ /* stolen from Boostrap 4 (.card .card-header .bg-danger) */
+ background-color: #dc3545;
+ color: white;
+}
+
+div.sidebar {
+ margin-right: 0;
+}
+
+/* Improved margin overrides */
+div.topic,
+pre.literal-block,
+pre.doctest-block,
+pre.math,
+pre.code,
+div.code {
+ margin-left: 1rem;
+ margin-right: 1rem;
+}
+
+div.code {
+ margin-bottom: 1rem;
+}
diff --git a/nikola/data/themes/base/assets/css/rst.css b/nikola/data/themes/base/assets/css/rst.css
index a1efa1a..03424a8 100644
--- a/nikola/data/themes/base/assets/css/rst.css
+++ b/nikola/data/themes/base/assets/css/rst.css
@@ -1,330 +1,2 @@
-/*
-:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 7614 2013-02-21 15:55:51Z milde $
-:Copyright: This stylesheet has been placed in the public domain.
-
-Default cascading style sheet for the HTML output of Docutils.
-
-See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
-customize this style sheet.
-*/
-
-/* used to remove borders from tables and images */
-.borderless, table.borderless td, table.borderless th {
- border: 0 }
-
-table.borderless td, table.borderless th {
- /* Override padding for "table.docutils td" with "! important".
- The right padding separates the table cells. */
- padding: 0 0.5em 0 0 ! important }
-
-.first {
- /* Override more specific margin styles with "! important". */
- margin-top: 0 ! important }
-
-.last, .with-subtitle {
- margin-bottom: 0 ! important }
-
-.hidden {
- display: none }
-
-a.toc-backref {
- text-decoration: none ;
- color: black }
-
-blockquote.epigraph {
- margin: 2em 5em ; }
-
-object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
- overflow: hidden;
-}
-
-div.abstract {
- margin: 2em 5em }
-
-div.abstract p.topic-title {
- font-weight: bold ;
- text-align: center }
-
-div.admonition, div.attention, div.caution, div.danger, div.error,
-div.hint, div.important, div.note, div.tip, div.warning, div.sidebar {
-/* stolen from Boostrap 3 (.panel .panel-default) */
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid #ddd;
- border-radius: 4px;
- -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
- box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
- padding: 0 15px 15px 15px;
-}
-
-div.attention, div.caution, div.danger, div.error, div.warning {
- /* stolen from Boostrap 3 (.panel .panel-danger) */
- border-color: #EBCCD1;
-}
-
-div.admonition p.admonition-title, div.hint p.admonition-title,
-div.important p.admonition-title, div.note p.admonition-title,
-div.tip p.admonition-title, div.sidebar p.sidebar-title,
-div.attention p.admonition-title, div.caution p.admonition-title,
-div.danger p.admonition-title, div.error p.admonition-title,
-div.warning p.admonition-title {
-/* stolen from Boostrap 3 (.panel .panel-default .panel-heading) */
- font-size: 16px;
- color: #333;
- background-color: #F5F5F5;
- padding: 10px 15px;
- margin-left: -15px;
- margin-right: -15px;
- border-bottom: 1px solid rgba(0, 0, 0, 0);
- border-top-left-radius: 3px;
- border-top-right-radius: 3px;
- color: #333;
- background-color: #F5F5F5;
- border-color: #DDD;
-}
-
-div.attention p.admonition-title, div.caution p.admonition-title,
-div.danger p.admonition-title, div.error p.admonition-title,
-div.warning p.admonition-title {
- /* stolen from Boostrap 3 (.panel .panel-danger) */
- color: #A94442;
- background-color: #F2DEDE;
- border-color: #EBCCD1;
-}
-
-/* Uncomment (and remove this text!) to get reduced vertical space in
- compound paragraphs.
-div.compound .compound-first, div.compound .compound-middle {
- margin-bottom: 0.5em }
-
-div.compound .compound-last, div.compound .compound-middle {
- margin-top: 0.5em }
-*/
-
-div.dedication {
- margin: 2em 5em ;
- text-align: center ;
- font-style: italic }
-
-div.dedication p.topic-title {
- font-weight: bold ;
- font-style: normal }
-
-div.figure {
- margin-left: 2em ;
- margin-right: 2em }
-
-div.footer, div.header {
- clear: both;
- font-size: smaller }
-
-div.line-block {
- display: block ;
- margin-top: 1em ;
- margin-bottom: 1em }
-
-div.line-block div.line-block {
- margin-top: 0 ;
- margin-bottom: 0 ;
- margin-left: 1.5em }
-
-
-html[dir="rtl"] div.line-block div.line-block {
- margin-top: 0 ;
- margin-bottom: 0 ;
- margin-right: 1.5em ;
- margin-left: 0 ;
-}
-
-div.sidebar {
- margin-left: 2em;
- min-height: 20px;
- width: 40% ;
- float: right ;
- clear: right }
-
-div.sidebar p.rubric {
- font-size: medium }
-
-div.system-messages {
- margin: 5em }
-
-div.system-messages h1 {
- color: #a94442 }
-
-div.system-message {
- border: 1px solid #ebccd1;
- padding: 1em }
-
-div.system-message p.system-message-title {
- color: #a94442 ;
- font-weight: bold }
-
-div.topic {
- margin: 2em }
-
-img.align-left, .figure.align-left, object.align-left {
- clear: left ;
- float: left ;
- margin-right: 1em }
-
-img.align-right, .figure.align-right, object.align-right {
- clear: right ;
- float: right ;
- margin-left: 1em }
-
-img.align-center, .figure.align-center, object.align-center {
- display: block;
- margin-left: auto;
- margin-right: auto;
-}
-
-.align-left {
- text-align: left }
-
-.align-center {
- clear: both ;
- text-align: center }
-
-.align-right {
- text-align: right }
-
-/* reset inner alignment in figures */
-.figure.align-right {
- text-align: inherit }
-
-/* div.align-center * { */
-/* text-align: left } */
-
-ol.simple, ul.simple {
- margin-bottom: 1em }
-
-ol.arabic {
- list-style: decimal }
-
-ol.loweralpha {
- list-style: lower-alpha }
-
-ol.upperalpha {
- list-style: upper-alpha }
-
-ol.lowerroman {
- list-style: lower-roman }
-
-ol.upperroman {
- list-style: upper-roman }
-
-p.attribution {
- text-align: right ;
- margin-left: 50% }
-
-p.caption {
- font-style: italic }
-
-p.credits {
- font-style: italic ;
- font-size: smaller }
-
-p.label {
- white-space: nowrap }
-
-p.rubric {
- font-weight: bold ;
- font-size: larger ;
- color: maroon ;
- text-align: center }
-
-p.sidebar-subtitle {
- font-weight: bold }
-
-p.topic-title {
- font-weight: bold }
-
-pre.address {
- margin-bottom: 0 ;
- margin-top: 0 ;
- font: inherit }
-
-pre.code .ln { color: grey; } /* line numbers */
-/*
-pre.code, code { background-color: #eeeeee }
-pre.code .comment, code .comment { color: #5C6576 }
-pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
-pre.code .literal.string, code .literal.string { color: #0C5404 }
-pre.code .name.builtin, code .name.builtin { color: #352B84 }
-pre.code .deleted, code .deleted { background-color: #DEB0A1}
-pre.code .inserted, code .inserted { background-color: #A3D289}
-*/
-
-span.classifier {
- font-style: italic }
-
-span.classifier-delimiter {
- font-weight: bold }
-
-span.option {
- white-space: nowrap }
-
-span.pre {
- white-space: pre }
-
-span.problematic {
- color: red }
-
-span.section-subtitle {
- /* font-size relative to parent (h1..h6 element) */
- font-size: 80% }
-
-table.citation {
- border-left: solid 1px gray;
- margin-left: 1px }
-
-table.docinfo {
- margin: 2em 4em }
-
-table.docutils {
- margin-top: 0.5em ;
- margin-bottom: 0.5em }
-
-table.footnote {
- border-left: solid 1px black;
- margin-left: 1px }
-
-table.docutils td, table.docutils th,
-table.docinfo td, table.docinfo th {
- padding-left: 0.5em ;
- padding-right: 0.5em ;
- vertical-align: top }
-
-table.docutils th.field-name, table.docinfo th.docinfo-name {
- font-weight: bold ;
- text-align: left ;
- white-space: nowrap ;
- padding-left: 0 }
-
-/* "booktabs" style (no vertical lines) */
-table.docutils.booktabs {
- border: 0px;
- border-top: 2px solid;
- border-bottom: 2px solid;
- border-collapse: collapse;
-}
-table.docutils.booktabs * {
- border: 0px;
-}
-table.docutils.booktabs th {
- border-bottom: thin solid;
- text-align: left;
-}
-
-h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
-h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
- font-size: 100% }
-
-ul.auto-toc {
- list-style-type: none }
-
-a.footnote-reference {
- line-height: 0px;
-}
+@import url("rst_base.css");
+@import url("nikola_rst.css");
diff --git a/nikola/data/themes/base/assets/css/rst_base.css b/nikola/data/themes/base/assets/css/rst_base.css
new file mode 100644
index 0000000..429f7b5
--- /dev/null
+++ b/nikola/data/themes/base/assets/css/rst_base.css
@@ -0,0 +1,474 @@
+/* Minimal style sheet for the HTML output of Docutils. */
+/* */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger */
+/* :Id: $Id: minimal.css 7952 2016-07-26 18:15:59Z milde $ */
+/* :Copyright: © 2015 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+
+/* This CSS2.1_ stylesheet defines rules for Docutils elements without */
+/* HTML equivalent. It is required to make the document semantic visible. */
+/* */
+/* .. _CSS2.1: http://www.w3.org/TR/CSS2 */
+/* .. _validates: http://jigsaw.w3.org/css-validator/validator$link */
+
+/* alignment of text and inline objects inside block objects*/
+.align-left { text-align: left; }
+.align-right { text-align: right; }
+.align-center { clear: both; text-align: center; }
+.align-top { vertical-align: top; }
+.align-middle { vertical-align: middle; }
+.align-bottom { vertical-align: bottom; }
+
+/* titles */
+h1.title, p.subtitle {
+ text-align: center;
+}
+p.admonition-title,
+p.topic-title,
+p.sidebar-title,
+p.rubric,
+p.system-message-title {
+ font-weight: bold;
+}
+h1 + p.subtitle,
+h1 + p.section-subtitle {
+ font-size: 1.6em;
+}
+h2 + p.section-subtitle { font-size: 1.28em; }
+p.subtitle,
+p.section-subtitle,
+p.sidebar-subtitle {
+ font-weight: bold;
+ margin-top: -0.5em;
+}
+p.sidebar-title,
+p.rubric {
+ font-size: larger;
+}
+p.rubric { color: maroon; }
+a.toc-backref {
+ color: black;
+ text-decoration: none; }
+
+/* Warnings, Errors */
+div.caution p.admonition-title,
+div.attention p.admonition-title,
+div.danger p.admonition-title,
+div.error p.admonition-title,
+div.warning p.admonition-title,
+div.system-messages h1,
+div.error,
+span.problematic,
+p.system-message-title {
+ color: red;
+}
+
+/* inline literals */
+span.docutils.literal {
+ font-family: monospace;
+ white-space: pre-wrap;
+}
+/* do not wraph at hyphens and similar: */
+.literal > span.pre { white-space: nowrap; }
+
+/* Lists */
+
+/* compact and simple lists: no margin between items */
+.simple li, .compact li,
+.simple ul, .compact ul,
+.simple ol, .compact ol,
+.simple > li p, .compact > li p,
+dl.simple > dd, dl.compact > dd {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+/* Table of Contents */
+/*div.topic.contents { margin: 0; }*/
+ul.auto-toc {
+ list-style-type: none;
+ padding-left: 1.5em; }
+
+/* Enumerated Lists */
+ol.arabic { list-style: decimal }
+ol.loweralpha { list-style: lower-alpha }
+ol.upperalpha { list-style: upper-alpha }
+ol.lowerroman { list-style: lower-roman }
+ol.upperroman { list-style: upper-roman }
+
+dt span.classifier { font-style: italic }
+dt span.classifier:before {
+ font-style: normal;
+ margin: 0.5em;
+ content: ":";
+}
+
+/* Field Lists and drivatives */
+/* bold field name, content starts on the same line */
+dl.field-list > dt,
+dl.option-list > dt,
+dl.docinfo > dt,
+dl.footnote > dt,
+dl.citation > dt {
+ font-weight: bold;
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.5em;
+}
+/* Offset for field content (corresponds to the --field-name-limit option) */
+dl.field-list > dd,
+dl.option-list > dd,
+dl.docinfo > dd {
+ margin-left: 9em; /* ca. 14 chars in the test examples */
+}
+/* start field-body on a new line after long field names */
+dl.field-list > dd > *:first-child,
+dl.option-list > dd > *:first-child
+{
+ display: inline-block;
+ width: 100%;
+ margin: 0;
+}
+/* field names followed by a colon */
+dl.field-list > dt:after,
+dl.docinfo > dt:after {
+ content: ":";
+}
+
+/* Bibliographic Fields (docinfo) */
+pre.address { font: inherit; }
+dd.authors > p { margin: 0; }
+
+/* Option Lists */
+dl.option-list { margin-left: 40px; }
+dl.option-list > dt { font-weight: normal; }
+span.option { white-space: nowrap; }
+
+/* Footnotes and Citations */
+dl.footnote.superscript > dd {margin-left: 1em; }
+dl.footnote.brackets > dd {margin-left: 2em; }
+dl > dt.label { font-weight: normal; }
+a.footnote-reference.brackets:before,
+dt.label > span.brackets:before { content: "["; }
+a.footnote-reference.brackets:after,
+dt.label > span.brackets:after { content: "]"; }
+a.footnote-reference.superscript,
+dl.footnote.superscript > dt.label {
+ vertical-align: super;
+ font-size: smaller;
+}
+dt.label > span.fn-backref { margin-left: 0.2em; }
+dt.label > span.fn-backref > a { font-style: italic; }
+
+/* Line Blocks */
+div.line-block { display: block; }
+div.line-block div.line-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: 40px;
+}
+
+/* Figures, Images, and Tables */
+.figure.align-left,
+img.align-left,
+object.align-left,
+table.align-left {
+ margin-right: auto;
+}
+.figure.align-center,
+img.align-center,
+object.align-center {
+ margin-left: auto;
+ margin-right: auto;
+ display: block;
+}
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+.figure.align-right,
+img.align-right,
+object.align-right,
+table.align-right {
+ margin-left: auto;
+}
+/* reset inner alignment in figures and tables */
+div.align-left, div.align-center, div.align-right,
+table.align-left, table.align-center, table.align-right
+{ text-align: inherit }
+
+/* Admonitions and System Messages */
+div.admonition,
+div.system-message,
+div.sidebar{
+ margin: 40px;
+ border: medium outset;
+ padding-right: 1em;
+ padding-left: 1em;
+}
+
+/* Sidebar */
+div.sidebar {
+ width: 30%;
+ max-width: 26em;
+ float: right;
+ clear: right;
+}
+
+/* Text Blocks */
+div.topic,
+pre.literal-block,
+pre.doctest-block,
+pre.math,
+pre.code {
+ margin-right: 40px;
+ margin-left: 40px;
+}
+pre.code .ln { color: gray; } /* line numbers */
+
+/* Tables */
+table.docutils { border-collapse: collapse; }
+table.docutils > td, table.docutils > th {
+ border-style: solid;
+ border-color: silver;
+ padding: 0 1ex;
+ border-width: thin;
+}
+table.docutils > td > p:first-child, table.docutils > th > p:first-child { margin-top: 0; }
+table.docutils > td > p, table.docutils > th > p { margin-bottom: 0; }
+
+table.docutils > caption {
+ text-align: left;
+ margin-bottom: 0.25em
+}
+
+table.borderless td, table.borderless th {
+ border: 0;
+ padding: 0;
+ padding-right: 0.5em /* separate table cells */
+}
+
+/* CSS31_ style sheet for the output of Docutils HTML writers. */
+/* Rules for easy reading and pre-defined style variants. */
+/* */
+/* :Author: Günter Milde, based on html4css1.css by David Goodger */
+/* :Id: $Id: plain.css 7952 2016-07-26 18:15:59Z milde $ */
+/* :Copyright: © 2015 Günter Milde. */
+/* :License: Released under the terms of the `2-Clause BSD license`_, */
+/* in short: */
+/* */
+/* Copying and distribution of this file, with or without modification, */
+/* are permitted in any medium without royalty provided the copyright */
+/* notice and this notice are preserved. */
+/* */
+/* This file is offered as-is, without any warranty. */
+/* */
+/* .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause */
+/* .. _CSS3: http://www.w3.org/TR/CSS3 */
+
+
+/* Document Structure */
+/* ****************** */
+
+/* Sections */
+
+/* Transitions */
+
+hr.docutils {
+ width: 80%;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ clear: both;
+}
+
+/* Paragraphs */
+/* ========== */
+
+/* vertical space (parskip) */
+/*p, ol, ul, dl,*/
+/*div.line-block,*/
+/*table{*/
+ /*margin-top: 0.5em;*/
+ /*margin-bottom: 0.5em;*/
+/*}*/
+/*h1, h2, h3, h4, h5, h6, */
+dl > dd {
+ margin-bottom: 0.5em;
+}
+
+/* Lists */
+/* ========== */
+
+/* Definition Lists */
+
+dl > dd p:first-child { margin-top: 0; }
+/* :last-child is not part of CSS 2.1 (introduced in CSS 3) */
+/* dl > dd p:last-child { margin-bottom: 0; } */
+
+/* lists nested in definition lists */
+/* :only-child is not part of CSS 2.1 (introduced in CSS 3) */
+dd > ul:only-child, dd > ol:only-child { padding-left: 1em; }
+
+/* Description Lists */
+/* styled like in most dictionaries, encyclopedias etc. */
+dl.description > dt {
+ font-weight: bold;
+ clear: left;
+ float: left;
+ margin: 0;
+ padding: 0;
+ padding-right: 0.5em;
+}
+
+/* Field Lists */
+
+/* example for custom field-name width */
+dl.field-list.narrow > dd {
+ margin-left: 5em;
+}
+/* run-in: start field-body on same line after long field names */
+dl.field-list.run-in > dd p {
+ display: block;
+}
+
+/* Bibliographic Fields */
+
+/* generally, bibliographic fields use special definition list dl.docinfo */
+/* but dedication and abstract are placed into "topic" divs */
+div.abstract p.topic-title {
+ text-align: center;
+}
+div.dedication {
+ margin: 2em 5em;
+ text-align: center;
+ font-style: italic;
+}
+div.dedication p.topic-title {
+ font-style: normal;
+}
+
+/* Citations */
+dl.citation dt.label {
+ font-weight: bold;
+}
+span.fn-backref {
+ font-weight: normal;
+}
+
+/* Text Blocks */
+/* ============ */
+
+/* Literal Blocks */
+pre.literal-block, pre.doctest-block,
+pre.math, pre.code {
+ margin-left: 1.5em;
+ margin-right: 1.5em
+}
+
+/* Block Quotes */
+
+blockquote,
+div.topic {
+ margin-left: 1.5em;
+ margin-right: 1.5em
+}
+blockquote > table,
+div.topic > table {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+blockquote p.attribution,
+div.topic p.attribution {
+ text-align: right;
+ margin-left: 20%;
+}
+
+/* Tables */
+/* ====== */
+
+/* th { vertical-align: bottom; } */
+
+table tr { text-align: left; }
+
+/* "booktabs" style (no vertical lines) */
+table.booktabs {
+ border: 0;
+ border-top: 2px solid;
+ border-bottom: 2px solid;
+ border-collapse: collapse;
+}
+table.booktabs * {
+ border: 0;
+}
+table.booktabs th {
+ border-bottom: thin solid;
+}
+
+/* numbered tables (counter defined in div.document) */
+table.numbered > caption:before {
+ counter-increment: table;
+ content: "Table " counter(table) ": ";
+ font-weight: bold;
+}
+
+/* Explicit Markup Blocks */
+/* ====================== */
+
+/* Footnotes and Citations */
+/* ----------------------- */
+
+/* line on the left */
+dl.footnote {
+ padding-left: 1ex;
+ border-left: solid;
+ border-left-width: thin;
+}
+
+/* Directives */
+/* ---------- */
+
+/* Body Elements */
+/* ~~~~~~~~~~~~~ */
+
+/* Images and Figures */
+
+/* let content flow to the side of aligned images and figures */
+.figure.align-left,
+img.align-left,
+object.align-left {
+ display: block;
+ clear: left;
+ float: left;
+ margin-right: 1em
+}
+.figure.align-right,
+img.align-right,
+object.align-right {
+ display: block;
+ clear: right;
+ float: right;
+ margin-left: 1em
+}
+
+/* Sidebar */
+
+/* Move into the margin. In a layout with fixed margins, */
+/* it can be moved into the margin completely. */
+div.sidebar {
+ width: 30%;
+ max-width: 26em;
+ margin-left: 1em;
+ margin-right: -5.5%;
+ background-color: #ffffee ;
+}
diff --git a/nikola/data/themes/base/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css
index 4842a3f..076351f 100644
--- a/nikola/data/themes/base/assets/css/theme.css
+++ b/nikola/data/themes/base/assets/css/theme.css
@@ -1,7 +1,7 @@
@charset "UTF-8";
/*
- Copyright © 2014-2016 Daniel Aleksandersen and others.
+ Copyright © 2014-2020 Daniel Aleksandersen and others.
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@@ -67,9 +67,9 @@ body {
margin-right: 1.5em;
}
-#menu ul li:dir(rtl),
-#toptranslations ul li:dir(rtl),
-#toptranslations h2:dir(rtl) {
+html[dir="rtl"] #menu ul li,
+html[dir="rtl"] #toptranslations ul li,
+html[dir="rtl"] #toptranslations h2 {
margin-left: 1.5em;
margin-right: 0;
}
@@ -79,12 +79,12 @@ body {
float: right;
}
-#toptranslations:dir(rtl) {
+html[dir="rtl"] #toptranslations {
text-align: left;
float: left;
}
-.posttranslations h3 {
+.posttranslations h3, .translationslist h3 {
display: inline;
font-size: 1em;
}
@@ -93,7 +93,7 @@ body {
font-size: 2em;
}
-.posttranslations h3:last-child {
+.posttranslations h3:last-child, .translationslist h3:last-child {
display: none;
}
@@ -120,35 +120,50 @@ body {
}
.metadata p:before,
.postpromonav .tags li:before,
-.postlist .listdate:after {
+.postlist .listdate:after,
+.translationslist p:before {
content: " — ";
}
.postlist li {
margin-bottom: .33em;
}
+.byline a:not(:last-child):after {
+ content: ",";
+}
/* Post and archive pagers */
.postindexpager .pager .next:before {
content: "↓ ";
}
-.postindexpager .pager .previous:before {
+.postindexpager .pager .previous:before,
+.archivenav .pager .up:before {
content: "↑ ";
}
-.postpromonav .pager .next:after {
+.postpromonav .pager .next:after,
+.archivenav .pager .next:after {
content: " →";
}
-.postpromonav .pager .previous:dir(rtl):after {
+html[dir="rtl"] .postpromonav .pager .previous:after,
+html[dir="rtl"] .archivenav .pager .previous:after {
content: " →";
}
-.postpromonav .pager .previous:before {
+.postpromonav .pager .previous:before,
+.archivenav .pager .previous:before {
content: "← ";
}
-.postpromonav .pager .next:dir(rtl):before {
+html[dir="rtl"] .postpromonav .pager .next:before,
+html[dir="rtl"] .archivenav .pager .next:before {
content: "← ";
}
-
+html[dir="rtl"] .postpromonav .pager .next:after,
+html[dir="rtl"] .archivenav .pager .next:after,
+html[dir="rtl"] .postpromonav .pager .previous:before,
+html[dir="rtl"] .archivenav .pager .previous:before {
+ content: "";
+}
.metadata p:first-of-type:before,
-.postpromonav .tags li:first-of-type:before {
+.postpromonav .tags li:first-of-type:before,
+.translationslist p:first-of-type:before {
content: "";
}
.postpromonav .pager {
@@ -156,24 +171,40 @@ body {
height: 1em;
}
.postpromonav .tags li,
-.postpromonav .pager li {
+.postpromonav .pager li,
+.archivenav .pager li {
display: inline-block;
}
-.postpromonav .pager .next {
+.archivenav .pager {
+ text-align: center
+}
+.postpromonav .pager .next,
+.archivenav .pager .next {
float: right;
}
-.postpromonav .pager .next:dir(rtl) {
+html[dir="rtl"] .postpromonav .pager .next,
+html[dir="rtl"] .archivenav .pager .next {
float: left;
}
-.postpromonav .pager .previous {
+.postpromonav .pager .previous,
+.archivenav .pager .previous {
float: left;
}
-.postpromonav .pager .previous:dir(rtl) {
+html[dir="rtl"] .postpromonav .pager .previous,
+html[dir="rtl"] .archivenav .pager .previous {
float: right;
}
-.metadata p {
+.archivenav .pager .disabled,
+.archivenav .pager .disabled a,
+.archivenav .pager .disabled:link {
+ color: #888;
+ cursor: not-allowed;
+}
+
+.metadata p,
+.translationslist p {
display: inline;
}
@@ -254,10 +285,6 @@ img {
margin-right: 0;
}
-.codetable .linenos {
- padding-right: 10px;
-}
-
.sr-only {
position: absolute;
width: 1px;
@@ -280,7 +307,7 @@ img {
}
pre.code, code {
- white-space: pre;
+ white-space: pre-wrap;
word-wrap: normal;
overflow: auto;
}
diff --git a/nikola/data/themes/base/assets/js/baguetteBox.min.js b/nikola/data/themes/base/assets/js/baguetteBox.min.js
new file mode 120000
index 0000000..dda9b55
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/baguetteBox.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/baguettebox.js/dist/baguetteBox.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/js/fancydates.js b/nikola/data/themes/base/assets/js/fancydates.js
index d13b11b..dc7906d 100644
--- a/nikola/data/themes/base/assets/js/fancydates.js
+++ b/nikola/data/themes/base/assets/js/fancydates.js
@@ -1,19 +1,21 @@
-function fancydates(fanciness, date_format) {
- if (fanciness == 0) {
+function fancydates(fanciness, luxonDateFormat) {
+ if (fanciness === 0) {
return;
}
- dates = $('time.published.dt-published');
+ var dates = document.querySelectorAll('.dt-published, .dt-updated, .listdate');
- i = 0;
- l = dates.length;
+ var l = dates.length;
- for (i = 0; i < l; i++) {
- d = moment(dates[i].attributes.datetime.value);
- if (fanciness == 1) {
- o = d.local().format(date_format);
+ for (var i = 0; i < l; i++) {
+ var d = luxon.DateTime.fromISO(dates[i].attributes.datetime.value);
+ var o;
+ if (fanciness === 1 && luxonDateFormat.preset) {
+ o = d.toLocal().toLocaleString(luxon.DateTime[luxonDateFormat.format]);
+ } else if (fanciness === 1) {
+ o = d.toLocal().toFormat(luxonDateFormat.format);
} else {
- o = d.fromNow();
+ o = d.toRelative();
}
dates[i].innerHTML = o;
}
diff --git a/nikola/data/themes/base/assets/js/fancydates.min.js b/nikola/data/themes/base/assets/js/fancydates.min.js
new file mode 100644
index 0000000..bb0b07b
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/fancydates.min.js
@@ -0,0 +1 @@
+function fancydates(t,e){if(0!==t)for(var a=document.querySelectorAll(".dt-published, .dt-updated, .listdate"),o=a.length,l=0;l<o;l++){var r,i=luxon.DateTime.fromISO(a[l].attributes.datetime.value);r=1===t&&e.preset?i.toLocal().toLocaleString(luxon.DateTime[e.format]):1===t?i.toLocal().toFormat(e.format):i.toRelative(),a[l].innerHTML=r}}
diff --git a/nikola/data/themes/base/assets/js/gallery.js b/nikola/data/themes/base/assets/js/gallery.js
new file mode 100644
index 0000000..af29f47
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/gallery.js
@@ -0,0 +1,32 @@
+function renderGallery(jsonContent, thumbnailSize) {
+ var container = document.getElementById("gallery_container");
+ container.innerHTML = '';
+ var layoutGeometry = require('justified-layout')(jsonContent, {
+ "containerWidth": container.offsetWidth,
+ "targetRowHeight": thumbnailSize * 0.6,
+ "boxSpacing": 5});
+ container.style.height = layoutGeometry.containerHeight + 'px';
+ var boxes = layoutGeometry.boxes;
+ for (var i = 0; i < boxes.length; i++) {
+ var img = document.createElement("img");
+ img.setAttribute('src', jsonContent[i].url_thumb);
+ img.setAttribute('alt', jsonContent[i].title);
+ img.style.width = boxes[i].width + 'px';
+ img.style.height = boxes[i].height + 'px';
+ link = document.createElement("a");
+ link.setAttribute('href', jsonContent[i].url);
+ link.setAttribute('class', 'image-reference');
+ div = document.createElement("div");
+ div.setAttribute('class', 'image-block');
+ div.setAttribute('title', jsonContent[i].title);
+ div.setAttribute('data-toggle', "tooltip")
+ div.style.width = boxes[i].width + 'px';
+ div.style.height = boxes[i].height + 'px';
+ div.style.top = boxes[i].top + 'px';
+ div.style.left = boxes[i].left + 'px';
+ link.appendChild(img);
+ div.appendChild(link);
+ container.appendChild(div);
+ }
+}
+
diff --git a/nikola/data/themes/base/assets/js/gallery.min.js b/nikola/data/themes/base/assets/js/gallery.min.js
new file mode 100644
index 0000000..c434155
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/gallery.min.js
@@ -0,0 +1 @@
+function renderGallery(t,e){var i=document.getElementById("gallery_container");i.innerHTML="";var l=require("justified-layout")(t,{containerWidth:i.offsetWidth,targetRowHeight:.6*e,boxSpacing:5});i.style.height=l.containerHeight+"px";for(var n=l.boxes,r=0;r<n.length;r++){var a=document.createElement("img");a.setAttribute("src",t[r].url_thumb),a.setAttribute("alt",t[r].title),a.style.width=n[r].width+"px",a.style.height=n[r].height+"px",link=document.createElement("a"),link.setAttribute("href",t[r].url),link.setAttribute("class","image-reference"),div=document.createElement("div"),div.setAttribute("class","image-block"),div.setAttribute("title",t[r].title),div.setAttribute("data-toggle","tooltip"),div.style.width=n[r].width+"px",div.style.height=n[r].height+"px",div.style.top=n[r].top+"px",div.style.left=n[r].left+"px",link.appendChild(a),div.appendChild(link),i.appendChild(div)}}
diff --git a/nikola/data/themes/base/assets/js/html5.js b/nikola/data/themes/base/assets/js/html5.js
index 448cebd..31340f0 100644..120000
--- a/nikola/data/themes/base/assets/js/html5.js
+++ b/nikola/data/themes/base/assets/js/html5.js
@@ -1,8 +1 @@
-/*
- HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
-*/
-(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag();
-a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x<style>article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}</style>";
-c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="<xyz></xyz>";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||
-"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f);
-if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d<h;d++)c.createElement(e[d]);return c}};l.html5=e;q(f)})(this,document);
+../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js b/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js
new file mode 120000
index 0000000..31340f0
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/html5shiv-printshiv.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/html5shiv/dist/html5shiv-printshiv.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/js/justified-layout.min.js b/nikola/data/themes/base/assets/js/justified-layout.min.js
new file mode 120000
index 0000000..d067ee6
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/justified-layout.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/justified-layout/dist/justified-layout.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/js/luxon.min.js b/nikola/data/themes/base/assets/js/luxon.min.js
new file mode 120000
index 0000000..a8a639d
--- /dev/null
+++ b/nikola/data/themes/base/assets/js/luxon.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/luxon/build/global/luxon.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/assets/js/moment-with-locales.min.js b/nikola/data/themes/base/assets/js/moment-with-locales.min.js
deleted file mode 120000
index 1caedc6..0000000
--- a/nikola/data/themes/base/assets/js/moment-with-locales.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/moment/min/moment-with-locales.min.js \ No newline at end of file
diff --git a/nikola/data/themes/base/base.theme b/nikola/data/themes/base/base.theme
new file mode 100644
index 0000000..3dd0fd1
--- /dev/null
+++ b/nikola/data/themes/base/base.theme
@@ -0,0 +1,9 @@
+[Theme]
+engine = mako
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+
+[Family]
+family = base
+jinja_version = base-jinja
diff --git a/nikola/data/themes/base/bundles b/nikola/data/themes/base/bundles
index 4760181..186e40e 100644
--- a/nikola/data/themes/base/bundles
+++ b/nikola/data/themes/base/bundles
@@ -1,2 +1,19 @@
-assets/css/all.css=rst.css,code.css,theme.css
-assets/css/all-nocdn.css=rst.css,code.css,theme.css
+; css bundles
+assets/css/all.css=
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ theme.css,
+assets/css/all-nocdn.css=
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ theme.css,
+ baguetteBox.min.css,
+
+; javascript bundles
+assets/js/all.js=
+ fancydates.js,
+assets/js/all-nocdn.js=
+ baguetteBox.min.js,
+ fancydates.js,
diff --git a/nikola/data/themes/base/engine b/nikola/data/themes/base/engine
deleted file mode 100644
index 2951cdd..0000000
--- a/nikola/data/themes/base/engine
+++ /dev/null
@@ -1 +0,0 @@
-mako
diff --git a/nikola/data/themes/base/messages/messages_af.py b/nikola/data/themes/base/messages/messages_af.py
new file mode 100644
index 0000000..650676b
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_af.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "%d min oor om te lees",
+ "(active)": "(aktief)",
+ "Also available in:": "Ook beskikbaar in:",
+ "Archive": "Argief",
+ "Atom feed": "Atom-voer",
+ "Authors": "Outeurs",
+ "Categories": "Kategorieë",
+ "Comments": "Opmerkings",
+ "LANGUAGE": "Afrikaans",
+ "Languages:": "Tale:",
+ "More posts about %s": "Meer plasings oor %s",
+ "Newer posts": "Jonger plasings",
+ "Next post": "Volgende plasing",
+ "Next": "Volgende",
+ "No posts found.": "Geen plasings gevind nie.",
+ "Nothing found.": "Niks gevind nie.",
+ "Older posts": "Ouer plasings",
+ "Original site": "Oorspronklike werf",
+ "Posted:": "Geplaas:",
+ "Posts about %s": "Plasings oor %s",
+ "Posts by %s": "Plasings deur %s",
+ "Posts for year %s": "Plasings vir %s",
+ "Posts for {month_day_year}": "Plasings vir {month_day_year}",
+ "Posts for {month_year}": "Plasings vir {month_year}",
+ "Previous post": "Vorige plasing",
+ "Previous": "Vorige",
+ "Publication date": "Publikasiedatum",
+ "RSS feed": "RSS-voer",
+ "Read in English": "Lees in Afrikaans",
+ "Read more": "Lees meer",
+ "Skip to main content": "Spring na die hoofinhoud",
+ "Source": "Bron",
+ "Subcategories:": "Subkategorieë:",
+ "Tags and Categories": "Etikette en kategorieë",
+ "Tags": "Etikette",
+ "Toggle navigation": "Wissel navigasie",
+ "Uncategorized": "Ongekategoriseerd",
+ "Up": "Op",
+ "Updates": "Bywerkings",
+ "Write your page here.": "Skryf die bladsy hier.",
+ "Write your post here.": "Skryf die plasing hier.",
+ "old posts, page %d": "ou plasings, bladsy %d",
+ "page %d": "bladsy %d",
+ "updated": "bygewerk",
+}
diff --git a/nikola/data/themes/base/messages/messages_ar.py b/nikola/data/themes/base/messages/messages_ar.py
index 4990ad4..72c137b 100644
--- a/nikola/data/themes/base/messages/messages_ar.py
+++ b/nikola/data/themes/base/messages/messages_ar.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "",
- "(active)": "",
+ "%d min remaining to read": "%d دقائق متبقية للقراءة",
+ "(active)": "(نشط)",
"Also available in:": "أيضا متوفر في:",
"Archive": "الأرشيف",
- "Authors": "",
+ "Atom feed": "روابط Atom",
+ "Authors": "المؤلفون",
"Categories": "فئات",
"Comments": "التّعليقات",
"LANGUAGE": "العربيّة",
- "Languages:": "اللغات",
+ "Languages:": "اللُغَات",
"More posts about %s": "المزيد من المقالات حول %s",
"Newer posts": "مقالات أحدث",
"Next post": "المقالة التالية",
+ "Next": "التالي",
"No posts found.": "لم يوجد مقالات.",
- "Nothing found.": "لم يوجد شيء.",
+ "Nothing found.": "لا يوجد شيء.",
"Older posts": "مقالات أقدم",
"Original site": "الموقع الأصلي",
"Posted:": "نشر:",
"Posts about %s": "مقالات عن %s",
- "Posts by %s": "",
+ "Posts by %s": "مقالات بواسطة %s",
"Posts for year %s": "مقالات سنة %s",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "",
+ "Posts for {month_day_year}": "مقال لـ {شَهْر - يَوْم - سَنَة}",
+ "Posts for {month_year}": "مقالـ {شَهْر - سَنَة }",
"Previous post": "المقالة السابقة",
- "Publication date": "تاريخ النشر",
- "RSS feed": "",
+ "Previous": "السابق",
+ "Publication date": "تاريخ النَشْر",
+ "RSS feed": "روابط التغذية ",
"Read in English": "اقرأ بالعربية",
"Read more": "قراءة المزيد",
"Skip to main content": "انتقل إلى المحتوى الرئيسي",
"Source": "المصدر",
- "Subcategories:": "",
+ "Subcategories:": "فئات فرعية",
"Tags and Categories": "تصنيفات و فئات",
"Tags": "تصنيفات",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
- "Write your page here.": "",
- "Write your post here.": "",
+ "Toggle navigation": "تَفْعيل وَضْع التَنَقُلْ",
+ "Uncategorized": "غير مصنف",
+ "Up": "أعلى",
+ "Updates": "التَحْدِيثَات",
+ "Write your page here.": "اكْتُب صَفْحَتك هُنَا ",
+ "Write your post here.": "اكْتُب مَقَالَك هُنَا",
"old posts, page %d": "مقالات قديمة, صفحة %d",
"page %d": "صفحة %d",
+ "updated": "مُحدَّثة",
}
diff --git a/nikola/data/themes/base/messages/messages_az.py b/nikola/data/themes/base/messages/messages_az.py
index 11e45ba..b7913f2 100644
--- a/nikola/data/themes/base/messages/messages_az.py
+++ b/nikola/data/themes/base/messages/messages_az.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d dəqiqəlik oxuma",
"(active)": "(aktiv)",
"Also available in:": "Həmçinin mövcuddur:",
"Archive": "Arxiv",
+ "Atom feed": "Atom feed",
"Authors": "Müəlliflər",
"Categories": "Kateqoriyalar",
"Comments": "Şərhlər",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "%s ilə bağlı digər yazılar",
"Newer posts": "Yeni yazılar",
"Next post": "Növbəti yazı",
+ "Next": "Növbəti",
"No posts found.": "Heç bir yazı tapılmadı",
"Nothing found.": "Heç nə tapılmadı",
"Older posts": "Köhnə yazılar",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "%s ilə bağlı yazılar",
"Posts by %s": "%s tərəfindən yazılmış yazılar",
"Posts for year %s": "%s ilindəki yazılar",
- "Posts for {month} {day}, {year}": "{month} {day}, {year} üçün yazılar",
- "Posts for {month} {year}": "{month} {year} üçün yazılar",
+ "Posts for {month_day_year}": "{month_day_year} üçün yazılar",
+ "Posts for {month_year}": "{month_year} üçün yazılar",
"Previous post": "Əvvəlki yazı",
+ "Previous": "Öncəki",
"Publication date": "Buraxılış tarixi",
"RSS feed": "RSS",
"Read in English": "Azərbaycan dilində oxu",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Subkateqoriyalar",
"Tags and Categories": "Teqlər və Kateqoriyalar",
"Tags": "Teqlər",
- "Toggle navigation": "",
+ "Toggle navigation": "Naviqasiya keçidi",
"Uncategorized": "Kateqoriyasız",
+ "Up": "Yuxarı",
"Updates": "Yenilənmələr",
"Write your page here.": "Öz səhifəni bura yaz",
"Write your post here.": "Öz məqaləni bura yaz",
"old posts, page %d": "köhnə yazılar, səhifə %s",
"page %d": "səhifə %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py
index 2e4d400..3344e04 100644
--- a/nikola/data/themes/base/messages/messages_bg.py
+++ b/nikola/data/themes/base/messages/messages_bg.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d минути до прочитане",
"(active)": "(активно)",
"Also available in:": "Достъпно също на:",
"Archive": "Архив",
+ "Atom feed": "",
"Authors": "Автори",
"Categories": "Категории",
"Comments": "Коментари",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Още публикации относно %s",
"Newer posts": "Нови публикации",
"Next post": "Следваща публикация",
+ "Next": "",
"No posts found.": "Не са намерени публикации.",
"Nothing found.": "Нищо не е намерено.",
"Older posts": "Стари публикации",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Публикации относно %s",
"Posts by %s": "Публикации от %s",
"Posts for year %s": "Публикации за %s година",
- "Posts for {month} {day}, {year}": "Публикации от {day} {month} {year}",
- "Posts for {month} {year}": "Публикации за {month} {year}",
+ "Posts for {month_day_year}": "Публикации от {month_day_year}",
+ "Posts for {month_year}": "Публикации за {month_year}",
"Previous post": "Предишна публикация",
+ "Previous": "",
"Publication date": "Дата на публикуване",
"RSS feed": "RSS поток",
"Read in English": "Прочетете на български",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Тагове",
"Toggle navigation": "",
"Uncategorized": "Без категория",
+ "Up": "",
"Updates": "Обновления",
"Write your page here.": "Напиши тук текста на твоята страница.",
"Write your post here.": "Напиши тук текста на твоята публикация.",
"old posts, page %d": "стари публикации, страница %d",
"page %d": "страница %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_fil.py b/nikola/data/themes/base/messages/messages_br.py
index 6107c54..316e4ec 100644
--- a/nikola/data/themes/base/messages/messages_fil.py
+++ b/nikola/data/themes/base/messages/messages_br.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "",
"(active)": "",
"Also available in:": "",
"Archive": "",
+ "Atom feed": "",
"Authors": "",
"Categories": "",
"Comments": "",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "",
"Newer posts": "",
"Next post": "",
+ "Next": "",
"No posts found.": "",
"Nothing found.": "",
"Older posts": "",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "",
"Posts by %s": "",
"Posts for year %s": "",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "",
+ "Posts for {month_day_year}": "",
+ "Posts for {month_year}": "",
"Previous post": "",
+ "Previous": "",
"Publication date": "",
"RSS feed": "",
"Read in English": "",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "",
"Write your post here.": "",
"old posts, page %d": "",
"page %d": "",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_bs.py b/nikola/data/themes/base/messages/messages_bs.py
index 9d7ac6f..2d537e6 100644
--- a/nikola/data/themes/base/messages/messages_bs.py
+++ b/nikola/data/themes/base/messages/messages_bs.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minuta preostalo za čitanje",
"(active)": "(aktivno)",
"Also available in:": "Takođe dostupan u:",
"Archive": "Arhiva",
+ "Atom feed": "",
"Authors": "Autori",
"Categories": "Kategorije",
"Comments": "Komentari",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Više članaka o %s",
"Newer posts": "Novije objave",
"Next post": "Naredni članak",
+ "Next": "",
"No posts found.": "Nema članaka.",
"Nothing found.": "Ništa nije pronađeno.",
"Older posts": "Starije objave",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Objave o %s",
"Posts by %s": "Objave prema %s",
"Posts for year %s": "Objave u godini %s",
- "Posts for {month} {day}, {year}": "Objave za {day}.{month}.{year}",
- "Posts for {month} {year}": "Objave za {month} {year}",
+ "Posts for {month_day_year}": "Objave za {month_day_year}",
+ "Posts for {month_year}": "Objave za {month_year}",
"Previous post": "Prethodni članak",
+ "Previous": "",
"Publication date": "Datum objavljivanja",
"RSS feed": "RSS feed",
"Read in English": "Pročitaj na bosanskom",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Oznake",
"Toggle navigation": "",
"Uncategorized": "Bez kategorije",
+ "Up": "",
"Updates": "Ažuriranja",
"Write your page here.": "Vašu stranicu napišite ovdje.",
"Write your post here.": "Vaš članak napišite ovdje.",
"old posts, page %d": "stare objave, strana %d",
"page %d": "strana %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py
index f3aebdf..0143a85 100644
--- a/nikola/data/themes/base/messages/messages_ca.py
+++ b/nikola/data/themes/base/messages/messages_ca.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "% min recordar per a llegir",
- "(active)": "",
- "Also available in:": "També disponibles en:",
+ "%d min remaining to read": "% min restants per a llegir",
+ "(active)": "(actiu)",
+ "Also available in:": "També disponible en:",
"Archive": "Arxiu",
- "Authors": "",
+ "Atom feed": "Canal Atom",
+ "Authors": "Autors",
"Categories": "Categories",
"Comments": "Comentaris",
"LANGUAGE": "Català",
- "Languages:": "Llenguatges:",
+ "Languages:": "Idiomes:",
"More posts about %s": "Més entrades sobre %s",
- "Newer posts": "Entrades posteriors",
+ "Newer posts": "Entrades més recents",
"Next post": "Entrada següent",
+ "Next": "Següent",
"No posts found.": "Publicació no trobada",
"Nothing found.": "No trobat",
- "Older posts": "Entrades anteriors",
+ "Older posts": "Entrades més antigues",
"Original site": "Lloc original",
"Posted:": "Publicat:",
"Posts about %s": "Entrades sobre %s",
- "Posts by %s": "",
+ "Posts by %s": "Entrades per %s",
"Posts for year %s": "Entrades de l'any %s",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "Publicat en {month} {year}",
+ "Posts for {month_day_year}": "Entrades per {month_day_year}",
+ "Posts for {month_year}": "Publicat en {month_year}",
"Previous post": "Entrada anterior",
+ "Previous": "Anterior",
"Publication date": "Data de publicació",
- "RSS feed": "Feed RSS",
+ "RSS feed": "Canal RSS",
"Read in English": "Llegeix-ho en català",
"Read more": "Llegeix-ne més",
"Skip to main content": "Vés al comentari principal",
"Source": "Codi",
- "Subcategories:": "",
- "Tags and Categories": "Etiquetes i Categories",
+ "Subcategories:": "Subcategories:",
+ "Tags and Categories": "Etiquetes i categories",
"Tags": "Etiquetes",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
- "Write your page here.": "",
- "Write your post here.": "",
+ "Toggle navigation": "Commuta la navegació",
+ "Uncategorized": "Sense categoria",
+ "Up": "Amunt",
+ "Updates": "Actualitzacions",
+ "Write your page here.": "Escriviu la vostra pàgina aquí.",
+ "Write your post here.": "Escriviu la vostra entrada aquí.",
"old posts, page %d": "entrades antigues, pàgina %d",
"page %d": "pàgina %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_cs.py b/nikola/data/themes/base/messages/messages_cs.py
index 42fb1c1..dcff7e6 100644
--- a/nikola/data/themes/base/messages/messages_cs.py
+++ b/nikola/data/themes/base/messages/messages_cs.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min zbývajících",
"(active)": "",
"Also available in:": "Dostupné také v",
"Archive": "Archiv",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorie",
"Comments": "Komentáře",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Další příspěvky o %s",
"Newer posts": "Novější příspěvky",
"Next post": "Další příspěvek",
+ "Next": "",
"No posts found.": "Nebyly nalezeny žádné příspěvky.",
"Nothing found.": "Nic nebylo nalezeno.",
"Older posts": "Starší příspěvky",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Příspěvky o %s",
"Posts by %s": "",
"Posts for year %s": "Příspěvky v roce %s",
- "Posts for {month} {day}, {year}": "Příspěvky v {month} {day}, {year}",
- "Posts for {month} {year}": "Příspěvky v {month} {year}",
+ "Posts for {month_day_year}": "Příspěvky v {month_day_year}",
+ "Posts for {month_year}": "Příspěvky v {month_year}",
"Previous post": "Předchozí příspěvek",
+ "Previous": "",
"Publication date": "Datum zveřejnění",
"RSS feed": "RSS zdroj",
"Read in English": "Číst v češtině",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Štítky",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "",
"Write your post here.": "",
"old posts, page %d": "staré příspěvky, strana %d",
"page %d": "strana %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_da.py b/nikola/data/themes/base/messages/messages_da.py
index 08b3d15..b070db5 100644
--- a/nikola/data/themes/base/messages/messages_da.py
+++ b/nikola/data/themes/base/messages/messages_da.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min. tilbage at læse",
"(active)": "",
"Also available in:": "Fås også i:",
"Archive": "Arkiv",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorier",
"Comments": "Kommentarer",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Yderligere indlæg om %s",
"Newer posts": "Nyere indlæg",
"Next post": "Næste indlæg",
+ "Next": "",
"No posts found.": "Søgningen gav ingen resultater.",
"Nothing found.": "Søgningen gav ingen resultater.",
"Older posts": "Ældre indlæg",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Indlæg om %s",
"Posts by %s": "",
"Posts for year %s": "Indlæg for %s",
- "Posts for {month} {day}, {year}": "Indlæs for {month} {day}, {year}",
- "Posts for {month} {year}": "Indlæg for {month} {year}",
+ "Posts for {month_day_year}": "Indlæs for {month_day_year}",
+ "Posts for {month_year}": "Indlæg for {month_year}",
"Previous post": "Tidligere indlæg",
+ "Previous": "",
"Publication date": "Udgivelsesdato",
"RSS feed": "RSS-nyhedskilde",
"Read in English": "Læs på dansk",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Nøgleord",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "",
"Write your post here.": "",
"old posts, page %d": "gamle indlæg, side %d",
"page %d": "side %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py
index 7981c69..1ba4dd4 100644
--- a/nikola/data/themes/base/messages/messages_de.py
+++ b/nikola/data/themes/base/messages/messages_de.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min verbleiben zum Lesen",
"(active)": "(aktiv)",
"Also available in:": "Auch verfügbar in:",
"Archive": "Archiv",
+ "Atom feed": "Atom-Feed",
"Authors": "Autoren",
"Categories": "Kategorien",
"Comments": "Kommentare",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Weitere Einträge über %s",
"Newer posts": "Neuere Einträge",
"Next post": "Nächster Eintrag",
+ "Next": "Nächster Eintrag",
"No posts found.": "Keine Einträge gefunden.",
"Nothing found.": "Nichts gefunden.",
"Older posts": "Ältere Einträge",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Einträge über %s",
"Posts by %s": "Einträge von %s",
"Posts for year %s": "Einträge aus dem Jahr %s",
- "Posts for {month} {day}, {year}": "Einträge vom {day}. {month} {year}",
- "Posts for {month} {year}": "Einträge aus {month} {year}",
+ "Posts for {month_day_year}": "Einträge vom {month_day_year}",
+ "Posts for {month_year}": "Einträge aus {month_year}",
"Previous post": "Vorheriger Eintrag",
+ "Previous": "Vorheriger Eintrag",
"Publication date": "Veröffentlichungsdatum",
"RSS feed": "RSS-Feed",
"Read in English": "Auf Deutsch lesen",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Unterkategorien:",
"Tags and Categories": "Tags und Kategorien",
"Tags": "Tags",
- "Toggle navigation": "",
+ "Toggle navigation": "Navigation umschalten",
"Uncategorized": "Nicht kategorisiert",
+ "Up": "Nach oben",
"Updates": "Updates",
"Write your page here.": "Schreibe hier deinen Seiteninhalt hin.",
"Write your post here.": "Schreibe hier deinen Eintrag hin.",
"old posts, page %d": "Ältere Einträge, Seite %d",
"page %d": "Seite %d",
+ "updated": "aktualisiert",
}
diff --git a/nikola/data/themes/base/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py
index 7e32afa..7021d84 100644
--- a/nikola/data/themes/base/messages/messages_el.py
+++ b/nikola/data/themes/base/messages/messages_el.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "",
"(active)": "",
"Also available in:": "Διαθέσιμο και στα:",
"Archive": "Αρχείο",
+ "Atom feed": "",
"Authors": "",
"Categories": "Κατηγορίες",
"Comments": "Σχόλια",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Περισσότερες αναρτήσεις για %s",
"Newer posts": "Νεότερες αναρτήσεις",
"Next post": "Επόμενη ανάρτηση",
+ "Next": "",
"No posts found.": "Δε βρέθηκαν αναρτήσεις",
"Nothing found.": "Δε βρέθηκε περιεχόμενο",
"Older posts": "Παλαιότερες αναρτήσεις",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Αναρτήσεις για %s",
"Posts by %s": "",
"Posts for year %s": "Αναρτήσεις για το έτος %s",
- "Posts for {month} {day}, {year}": "Αναρτήσεις στις {day} {month}, {year}",
- "Posts for {month} {year}": "Αναρτήσεις για τον {month} του {year}",
+ "Posts for {month_day_year}": "Αναρτήσεις στις {month_day_year}",
+ "Posts for {month_year}": "Αναρτήσεις για τον {month_year}",
"Previous post": "Προηγούμενη ανάρτηση",
+ "Previous": "",
"Publication date": "Ημερομηνία δημοσίευσης",
"RSS feed": "",
"Read in English": "Διαβάστε στα Ελληνικά",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Ετικέτες",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "",
"Write your post here.": "",
"old posts, page %d": "σελίδα παλαιότερων αναρτήσεων %d",
"page %d": "σελίδα %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py
index 3c39f55..a6beb5e 100644
--- a/nikola/data/themes/base/messages/messages_en.py
+++ b/nikola/data/themes/base/messages/messages_en.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min remaining to read",
"(active)": "(active)",
"Also available in:": "Also available in:",
"Archive": "Archive",
+ "Atom feed": "Atom feed",
"Authors": "Authors",
"Categories": "Categories",
"Comments": "Comments",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "More posts about %s",
"Newer posts": "Newer posts",
"Next post": "Next post",
+ "Next": "Next",
"No posts found.": "No posts found.",
"Nothing found.": "Nothing found.",
"Older posts": "Older posts",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Posts about %s",
"Posts by %s": "Posts by %s",
"Posts for year %s": "Posts for year %s",
- "Posts for {month} {day}, {year}": "Posts for {month} {day}, {year}",
- "Posts for {month} {year}": "Posts for {month} {year}",
+ "Posts for {month_day_year}": "Posts for {month_day_year}",
+ "Posts for {month_year}": "Posts for {month_year}",
"Previous post": "Previous post",
+ "Previous": "Previous",
"Publication date": "Publication date",
"RSS feed": "RSS feed",
"Read in English": "Read in English",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Tags",
"Toggle navigation": "Toggle navigation",
"Uncategorized": "Uncategorized",
+ "Up": "Up",
"Updates": "Updates",
"Write your page here.": "Write your page here.",
"Write your post here.": "Write your post here.",
"old posts, page %d": "old posts, page %d",
"page %d": "page %d",
+ "updated": "updated",
}
diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py
index 9d1ae72..2793ff5 100644
--- a/nikola/data/themes/base/messages/messages_eo.py
+++ b/nikola/data/themes/base/messages/messages_eo.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minutoj por legi",
"(active)": "(aktiva)",
"Also available in:": "Ankaŭ disponebla en:",
"Archive": "Arkivo",
+ "Atom feed": "Atom fluo",
"Authors": "Aŭtoroj",
"Categories": "Kategorioj",
"Comments": "Komentoj",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Pli da artikoloj pri %s",
"Newer posts": "Pli novaj artikoloj",
"Next post": "Venonta artikolo",
+ "Next": "Venonta",
"No posts found.": "Neniu artikoloj trovitaj.",
"Nothing found.": "Nenio trovita.",
"Older posts": "Pli malnovaj artikoloj",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Artikoloj pri %s",
"Posts by %s": "Artikoloj de %s",
"Posts for year %s": "Artikoloj de la jaro %s",
- "Posts for {month} {day}, {year}": "Artikoloj de la {day}a de {month} {year}",
- "Posts for {month} {year}": "Artikoloj de {month} {year}",
+ "Posts for {month_day_year}": "Artikoloj de la {month_day_year}",
+ "Posts for {month_year}": "Artikoloj de {month_year}",
"Previous post": "Antaŭa artikolo",
+ "Previous": "Antaŭa",
"Publication date": "Eldona dato",
"RSS feed": "RSS fluo",
"Read in English": "Legu ĝin en Esperanto",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etikedoj",
"Toggle navigation": "Ŝalti menuon",
"Uncategorized": "Sen kategorioj",
+ "Up": "Supren",
"Updates": "Ĝisdatigoj",
"Write your page here.": "Skribu tie vian paĝon.",
"Write your post here.": "Skribu tie vian artikolon.",
"old posts, page %d": "%da paĝo de malnovaj artikoloj",
"page %d": "paĝo %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py
index c590a64..bb58c82 100644
--- a/nikola/data/themes/base/messages/messages_es.py
+++ b/nikola/data/themes/base/messages/messages_es.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "quedan %d minutos de lectura",
"(active)": "(activo)",
"Also available in:": "También disponible en:",
"Archive": "Archivo",
+ "Atom feed": "Canal Atom",
"Authors": "Autores",
"Categories": "Categorías",
"Comments": "Comentarios",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Más publicaciones sobre %s",
"Newer posts": "Publicaciones posteriores",
"Next post": "Siguiente publicación",
+ "Next": "Siguiente",
"No posts found.": "No se encontraron publicaciones.",
"Nothing found.": "No se encontró nada.",
"Older posts": "Publicaciones anteriores",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Publicaciones sobre %s",
"Posts by %s": "Publicaciones de %s",
"Posts for year %s": "Publicaciones del año %s",
- "Posts for {month} {day}, {year}": "Publicaciones del {day} de {month} de {year}",
- "Posts for {month} {year}": "Posts de {month} de {year}",
+ "Posts for {month_day_year}": "Publicaciones del {month_day_year}",
+ "Posts for {month_year}": "Posts de {month_year}",
"Previous post": "Publicación anterior",
+ "Previous": "Anterior",
"Publication date": "Fecha de publicación",
"RSS feed": "Canal RSS",
"Read in English": "Leer en español",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etiquetas",
"Toggle navigation": "Mostrar navegación",
"Uncategorized": "Sin categoría",
+ "Up": "Arriba",
"Updates": "Actualizaciones",
"Write your page here.": "Escriba su página aquí.",
"Write your post here.": "Escriba su publicación aquí.",
"old posts, page %d": "publicaciones antiguas, página %d",
"page %d": "página %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_et.py b/nikola/data/themes/base/messages/messages_et.py
index 9b3ba9c..e2a6e30 100644
--- a/nikola/data/themes/base/messages/messages_et.py
+++ b/nikola/data/themes/base/messages/messages_et.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "",
- "(active)": "",
+ "%d min remaining to read": "%d minutit veel lugeda",
+ "(active)": "(aktiivne)",
"Also available in:": "Saadaval ka:",
"Archive": "Arhiiv",
- "Authors": "",
+ "Atom feed": "Atom uudisvoog",
+ "Authors": "Autorid",
"Categories": "Kategooriad",
- "Comments": "",
+ "Comments": "Kommentaarid",
"LANGUAGE": "Eesti",
- "Languages:": "",
+ "Languages:": "Keeled:",
"More posts about %s": "Veel postitusi %s kohta",
"Newer posts": "Uued postitused",
"Next post": "Järgmine postitus",
- "No posts found.": "",
- "Nothing found.": "",
+ "Next": "Järgmine",
+ "No posts found.": "Postitusi ei leitud.",
+ "Nothing found.": "Midagi ei leitud.",
"Older posts": "Vanemad postitused",
"Original site": "Algallikas",
"Posted:": "Postitatud:",
"Posts about %s": "Postitused %s kohta",
- "Posts by %s": "",
+ "Posts by %s": "Postitus kasutajalt %s",
"Posts for year %s": "Postitused aastast %s",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "Postitused {year} aasta kuust {month} ",
+ "Posts for {month_day_year}": "Postitused {month_day_year}",
+ "Posts for {month_year}": "Postitused {month_year}",
"Previous post": "Eelmine postitus",
- "Publication date": "",
- "RSS feed": "",
+ "Previous": "Eelmine",
+ "Publication date": "Avaldamise kuupäev",
+ "RSS feed": "RSS uudisvoog",
"Read in English": "Loe eesti keeles",
"Read more": "Loe veel",
- "Skip to main content": "",
+ "Skip to main content": "Otse peamise sisu juurde",
"Source": "Lähtekood",
- "Subcategories:": "",
+ "Subcategories:": "Alamkategooriad:",
"Tags and Categories": "Sildid ja kategooriad",
"Tags": "Märksõnad",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
- "Write your page here.": "",
- "Write your post here.": "",
+ "Toggle navigation": "Näita või peida menüüd",
+ "Uncategorized": "Kategooriata",
+ "Up": "Üles",
+ "Updates": "Uuendused",
+ "Write your page here.": "Kirjuta oma lehekülje sisu siia.",
+ "Write your post here.": "Kirjuta oma postitus siia.",
"old posts, page %d": "vanade postituste, leht %d",
"page %d": "leht %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_eu.py b/nikola/data/themes/base/messages/messages_eu.py
index 68d7162..dd39991 100644
--- a/nikola/data/themes/base/messages/messages_eu.py
+++ b/nikola/data/themes/base/messages/messages_eu.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minutu gelditzen dira irakurtzeko",
"(active)": "(aktibo)",
"Also available in:": "Eskuragarria hizkuntza hauetan ere:",
"Archive": "Artxiboa",
+ "Atom feed": "",
"Authors": "Egileak",
"Categories": "Kategoriak",
"Comments": "Iruzkinak",
@@ -14,17 +15,19 @@ MESSAGES = {
"More posts about %s": "%s-ri buruzko argitalpen gehiago",
"Newer posts": "Argitalpen berriagoak",
"Next post": "Hurrengo argitalpena",
+ "Next": "",
"No posts found.": "Ez da argitalpenik aurkitu",
"Nothing found.": "Ez da ezer aurkitu",
- "Older posts": "Post zaharragoak",
- "Original site": "Jatorrizko orria",
+ "Older posts": "Argitalpen zaharragoak",
+ "Original site": "Jatorrizko gunea",
"Posted:": "Argitaratuta:",
"Posts about %s": "%s-ri buruzko argitalpenak",
"Posts by %s": "%s-ek idatzitako argitalpenak",
"Posts for year %s": "%s. urteko argitalpenak",
- "Posts for {month} {day}, {year}": "{year}ko {month}aren {day}ko argitalpenak",
- "Posts for {month} {year}": "{year}ko {month}ren argitalpenak",
+ "Posts for {month_day_year}": "{month_day_year} argitalpenak",
+ "Posts for {month_year}": "{month_year} argitalpenak",
"Previous post": "Aurreko argitalpena",
+ "Previous": "",
"Publication date": "Argitaratze-data",
"RSS feed": "RSS jarioa",
"Read in English": "Euskaraz irakurri",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etiketak",
"Toggle navigation": "",
"Uncategorized": "Kategorizatu-gabeak",
+ "Up": "",
"Updates": "Eguneraketak",
"Write your page here.": "Idatzi zure orria hemen",
"Write your post here.": "Idatzi zure argitalpena hemen",
- "old posts, page %d": "Argitalpen zaharragoak,%d. orria",
+ "old posts, page %d": "Argitalpen zaharragoak, %d. orria",
"page %d": "%d. orria",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py
index b2abad6..ade3276 100644
--- a/nikola/data/themes/base/messages/messages_fa.py
+++ b/nikola/data/themes/base/messages/messages_fa.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d دقیقه برای خواندن باقی مانده",
"(active)": "(فعال)",
"Also available in:": "همچنین قابل دسترس از:",
"Archive": "آرشیو",
+ "Atom feed": "",
"Authors": "نویسنده‌ها",
"Categories": "دسته‌ها",
"Comments": "دیدگاه‌‌‌ها",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "ارسال‌های بیشتر دربارهٔ%s",
"Newer posts": "ارسال‌های جدید‌تر",
"Next post": "ارسال بعدی",
+ "Next": "بعدی",
"No posts found.": "هیچ پستی پیدا نشد.",
"Nothing found.": "هیچ‌چیزی پیدا نشد.",
"Older posts": "پست‌های قدیمی‌تر",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "ارسال‌ها دربارهٔ %s",
"Posts by %s": "ارسال‌های %s",
"Posts for year %s": "ارسال‌ها برای سال %s",
- "Posts for {month} {day}, {year}": "ارسال برای {month} {day}. {year}",
- "Posts for {month} {year}": "ارسال برای {month} {year}",
+ "Posts for {month_day_year}": "ارسال برای {month_day_year}",
+ "Posts for {month_year}": "ارسال برای {month_year}",
"Previous post": "ارسال پیشین",
+ "Previous": "قبلی",
"Publication date": "تاریخ انتشار",
"RSS feed": "خوراک",
"Read in English": "به فارسی بخوانید",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "برچسب‌ها",
"Toggle navigation": "",
"Uncategorized": "دسته‌بندی نشده",
+ "Up": "",
"Updates": "بروزرسانی‌ها",
"Write your page here.": "من صفحه را این‌جا بنویسید. ",
"Write your post here.": "متن پست‌تان را این‌جا بنویسید.",
"old posts, page %d": "صفحهٔ ارسال‌های قدیمی %d",
"page %d": "برگه %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py
index 9a70ede..e29db48 100644
--- a/nikola/data/themes/base/messages/messages_fi.py
+++ b/nikola/data/themes/base/messages/messages_fi.py
@@ -1,30 +1,33 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minuuttia lukuaikaa",
"(active)": "(aktiivinen)",
"Also available in:": "Saatavilla myös:",
"Archive": "Arkisto",
+ "Atom feed": "Atom-syöte",
"Authors": "Kirjoittajat",
"Categories": "Kategoriat",
"Comments": "Kommentit",
"LANGUAGE": "Suomi",
"Languages:": "Kielet:",
- "More posts about %s": "Lisää postauksia aiheesta %s",
- "Newer posts": "Uudempia postauksia",
- "Next post": "Seuraava postaus",
- "No posts found.": "Postauksia ei löytynyt.",
+ "More posts about %s": "Lisää kirjoituksia aiheesta %s",
+ "Newer posts": "Uudempia kirjoituksia",
+ "Next post": "Seuraava kirjoitus",
+ "Next": "Seuraava",
+ "No posts found.": "Kirjoituksia ei löytynyt.",
"Nothing found.": "Ei hakutuloksia.",
- "Older posts": "Vanhempia postauksia",
+ "Older posts": "Vanhempia kirjoituksia",
"Original site": "Alkuperäinen sivusto",
- "Posted:": "Postattu:",
- "Posts about %s": "Postauksia aiheesta %s",
- "Posts by %s": "Postaukset kirjoittajalta %s",
- "Posts for year %s": "Postauksia vuodelta %s",
- "Posts for {month} {day}, {year}": "Kirjoituksia ajalta {day}. {month}ta {year}",
- "Posts for {month} {year}": "Postauksia ajalle {month} {year}",
- "Previous post": "Edellinen postaus",
+ "Posted:": "Kirjoitettu:",
+ "Posts about %s": "Kirjoituksia aiheesta %s",
+ "Posts by %s": "Artikkelit kirjoittajalta %s",
+ "Posts for year %s": "Kirjoituksia vuodelta %s",
+ "Posts for {month_day_year}": "Kirjoituksia ajalta {month_day_year}",
+ "Posts for {month_year}": "Kirjoituksia ajalta {month_year}",
+ "Previous post": "Edellinen kirjoitus",
+ "Previous": "Edellinen",
"Publication date": "Julkaisupäivämäärä",
"RSS feed": "RSS-syöte",
"Read in English": "Lue suomeksi",
@@ -32,13 +35,15 @@ MESSAGES = {
"Skip to main content": "Hyppää sisältöön",
"Source": "Lähde",
"Subcategories:": "Alakategoriat:",
- "Tags and Categories": "Tagit ja kategoriat",
- "Tags": "Tagit",
- "Toggle navigation": "",
+ "Tags and Categories": "Avainsanat ja kategoriat",
+ "Tags": "Avainsanat",
+ "Toggle navigation": "Vaihda navigointia",
"Uncategorized": "Luokittelematon",
+ "Up": "Ylös",
"Updates": "Päivitykset",
"Write your page here.": "Kirjoita sisältö tähän.",
"Write your post here.": "Kirjoita sisältö tähän.",
- "old posts, page %d": "vanhoja postauksia, sivu %d",
+ "old posts, page %d": "vanhoja kirjoituksia, sivu %d",
"page %d": "sivu %d",
+ "updated": "päivitetty",
}
diff --git a/nikola/data/themes/base/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py
index 6be1422..971d607 100644
--- a/nikola/data/themes/base/messages/messages_fr.py
+++ b/nikola/data/themes/base/messages/messages_fr.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "Il reste encore %d min. de lecture",
"(active)": "(actif)",
"Also available in:": "Également disponible en :",
"Archive": "Archives",
+ "Atom feed": "Flux Atom",
"Authors": "Auteurs",
"Categories": "Catégories",
"Comments": "Commentaires",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Plus d'articles sur %s",
"Newer posts": "Articles récents",
"Next post": "Article suivant",
+ "Next": "Article suivant",
"No posts found.": "Pas d'articles.",
"Nothing found.": "Pas de résultats.",
"Older posts": "Anciens articles",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Articles sur %s",
"Posts by %s": "Publiés par %s",
"Posts for year %s": "Articles de l'année %s",
- "Posts for {month} {day}, {year}": "Articles du {day} {month} {year}",
- "Posts for {month} {year}": "Articles de {month} {year}",
+ "Posts for {month_day_year}": "Articles du {month_day_year}",
+ "Posts for {month_year}": "Articles de {month_year}",
"Previous post": "Article précédent",
+ "Previous": "Article précédent",
"Publication date": "Date de publication",
"RSS feed": "Flux RSS",
"Read in English": "Lire en français",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Sous-catégories",
"Tags and Categories": "Étiquettes et catégories",
"Tags": "Étiquettes",
- "Toggle navigation": "",
+ "Toggle navigation": "Basculer en navigation",
"Uncategorized": "Sans catégorie",
+ "Up": "Retour en haut",
"Updates": "Mises à jour",
"Write your page here.": "Écrivez votre page ici.",
"Write your post here.": "Écrivez votre billet ici.",
"old posts, page %d": "anciens articles, page %d",
"page %d": "page %d",
+ "updated": "mis à jour",
}
diff --git a/nikola/data/themes/base/messages/messages_fur.py b/nikola/data/themes/base/messages/messages_fur.py
new file mode 100644
index 0000000..6f75b66
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_fur.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "a restin altris %d minûts di leture",
+ "(active)": "(atîf)",
+ "Also available in:": "Disponibil ancje in:",
+ "Archive": "Archivi",
+ "Atom feed": "Feed Atom",
+ "Authors": "Autôrs",
+ "Categories": "Categoriis",
+ "Comments": "Coments",
+ "LANGUAGE": "Furlan",
+ "Languages:": "Lenghis:",
+ "More posts about %s": "Altris articui su %s",
+ "Newer posts": "Articui plui resints",
+ "Next post": "Prossim articul",
+ "Next": "Prossim",
+ "No posts found.": "Nissun articul cjatât.",
+ "Nothing found.": "Nol è stât cjatât nuie.",
+ "Older posts": "Articui precedents",
+ "Original site": "Sît originâl",
+ "Posted:": "Publicât:",
+ "Posts about %s": "Articui su %s",
+ "Posts by %s": "Articui di %s",
+ "Posts for year %s": "Articui par an %s",
+ "Posts for {month_day_year}": "Articui par {month_day_year}",
+ "Posts for {month_year}": "Articui par {month_year}",
+ "Previous post": "Articul precedent",
+ "Previous": "Precedent",
+ "Publication date": "Date di publicazion",
+ "RSS feed": "Feed RSS",
+ "Read in English": "Lei par furlan",
+ "Read more": "Continue la leture",
+ "Skip to main content": "Va al test principâl",
+ "Source": "Document origjinâl",
+ "Subcategories:": "Subcategoriis:",
+ "Tags and Categories": "Tags e categoriis",
+ "Tags": "Tags",
+ "Toggle navigation": "Ativâ la navigazion",
+ "Uncategorized": "Cence categorie",
+ "Up": "Su",
+ "Updates": "Inzornaments",
+ "Write your page here.": "Scrîf ca la tô pagjine.",
+ "Write your post here.": "Scrîf ca il to articul.",
+ "old posts, page %d": "articui vecjos, pagjine %d",
+ "page %d": "pagjine %d",
+ "updated": "inzornât",
+}
diff --git a/nikola/data/themes/base/messages/messages_gl.py b/nikola/data/themes/base/messages/messages_gl.py
index 11ac1cf..c41d61f 100644
--- a/nikola/data/themes/base/messages/messages_gl.py
+++ b/nikola/data/themes/base/messages/messages_gl.py
@@ -1,19 +1,21 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min restantes para ler",
"(active)": "(activo)",
"Also available in:": "Tamén dispoñible en:",
"Archive": "Arquivo",
+ "Atom feed": "",
"Authors": "Autores",
"Categories": "Categorías",
"Comments": "Comentarios",
- "LANGUAGE": "Inglés",
+ "LANGUAGE": "Galego",
"Languages:": "Linguas:",
"More posts about %s": "Máis artigos sobre %s",
"Newer posts": "Últimos artigos",
"Next post": "Seguinte artigo",
+ "Next": "",
"No posts found.": "Non se atoparon artigos.",
"Nothing found.": "Non se atopou nada.",
"Older posts": "Artigos vellos",
@@ -22,12 +24,13 @@ MESSAGES = {
"Posts about %s": "Artigos sobre %s",
"Posts by %s": "Publicacións de %s",
"Posts for year %s": "Artigos do ano %s",
- "Posts for {month} {day}, {year}": "Artigos de {month} {day}, {year}",
- "Posts for {month} {year}": "Artigos de {month} {year}",
+ "Posts for {month_day_year}": "Artigos de {month_day_year}",
+ "Posts for {month_year}": "Artigos de {month_year}",
"Previous post": "Artigo anterior",
+ "Previous": "",
"Publication date": "Data de publicación",
"RSS feed": "Sindicación RSS",
- "Read in English": "Ler en Inglés",
+ "Read in English": "Ler en Galego",
"Read more": "Ler máis",
"Skip to main content": "Saltar ó contido principal",
"Source": "Fonte",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etiquetas",
"Toggle navigation": "",
"Uncategorized": "Sen categoría",
+ "Up": "",
"Updates": "Actualizacións",
"Write your page here.": "Escribe a túa páxina aquí.",
"Write your post here.": "Escribe o teu artigo aquí.",
"old posts, page %d": "Artigos vellos, páxina %d",
"page %d": "páxina %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_he.py b/nikola/data/themes/base/messages/messages_he.py
index 106e35e..2de9796 100644
--- a/nikola/data/themes/base/messages/messages_he.py
+++ b/nikola/data/themes/base/messages/messages_he.py
@@ -1,30 +1,33 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "d% דקות נותרים לסיום קריאה",
+ "%d min remaining to read": "%d דקות נותרים לסיום קריאה",
"(active)": "(פעיל)",
"Also available in:": "זמין גם ב:",
"Archive": "ארכיב",
+ "Atom feed": "",
"Authors": "מחברים",
"Categories": "קטגוריות",
"Comments": "הערות",
"LANGUAGE": "אנגלית",
"Languages:": "שפות:",
- "More posts about %s": "עוד פוסטים אודות s%",
+ "More posts about %s": "עוד פוסטים אודות %s",
"Newer posts": "פוסטים חדשים",
"Next post": "לפוסט הבא",
+ "Next": "",
"No posts found.": "לא נמצאו פוסטים",
"Nothing found.": "לא נמצא",
"Older posts": "פוסטים ישנים",
"Original site": "אתר המקורי",
"Posted:": "פורסם:",
- "Posts about %s": "פוסטים אודות s%",
- "Posts by %s": "פוסטים ע״י s%",
- "Posts for year %s": "פוסטים לשנת s%",
- "Posts for {month} {day}, {year}": "פוסטים עבוד {year},{day}{month}",
- "Posts for {month} {year}": "פוסטים עבוד {year}{month}",
+ "Posts about %s": "פוסטים אודות %s",
+ "Posts by %s": "פוסטים ע״י %s",
+ "Posts for year %s": "פוסטים לשנת %s",
+ "Posts for {month_day_year}": "פוסטים עבוד {month_day_year}",
+ "Posts for {month_year}": "פוסטים עבוד {month_year}",
"Previous post": "פוסט הקודם",
+ "Previous": "",
"Publication date": "תאריך פרסום",
"RSS feed": "פיד RSS",
"Read in English": "קרא באנגלית",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "תגים",
"Toggle navigation": "החלף מצב ניווט",
"Uncategorized": "לא משויך לקטגוריה",
+ "Up": "",
"Updates": "עדכונים",
"Write your page here.": "תכתוב את העמוד שלך פה.",
"Write your post here.": "תכתוב את הפוסט שלך פה.",
- "old posts, page %d": "פוסטים קודמים, דף d%",
- "page %d": "עמוד d%",
+ "old posts, page %d": "פוסטים קודמים, דף %d",
+ "page %d": "עמוד %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_hi.py b/nikola/data/themes/base/messages/messages_hi.py
index 551a9df..25c3d2e 100644
--- a/nikola/data/themes/base/messages/messages_hi.py
+++ b/nikola/data/themes/base/messages/messages_hi.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "पढ़ने में %d मिनट बाकी",
+ "%d min remaining to read": "पढ़ने के लिए %d मिनट शेष",
"(active)": "(सक्रिय)",
"Also available in:": "उपलब्ध भाषाएँ:",
- "Archive": "आर्काइव",
+ "Archive": "संग्रह",
+ "Atom feed": "एटम फीड",
"Authors": "लेखक",
"Categories": "श्रेणियाँ",
"Comments": "टिप्पणियाँ",
"LANGUAGE": "हिन्दी",
"Languages:": "भाषाएँ:",
- "More posts about %s": "%s के बारे में अौर पोस्टें",
- "Newer posts": "नई पोस्टें",
- "Next post": "अगली पोस्ट",
- "No posts found.": "कोई पोस्ट नहीं मिल सकी",
- "Nothing found.": "कुछ नहीं मिल सका",
- "Older posts": "पुरानी पोस्टें",
- "Original site": "असली साइट",
+ "More posts about %s": "%s के बारे में और पोस्ट",
+ "Newer posts": "नई पोस्ट",
+ "Next post": "अगला पोस्ट",
+ "Next": "अगला",
+ "No posts found.": "कोई पोस्ट नहीं मिला",
+ "Nothing found.": "कुछ नहीं मिला",
+ "Older posts": "पुरानी पोस्ट",
+ "Original site": "मूल साइट",
"Posted:": "पोस्टेड:",
- "Posts about %s": "%s के बारे में पोस्टें",
- "Posts by %s": "%s की पोस्टें",
- "Posts for year %s": "साल %s की पोस्टें",
- "Posts for {month} {day}, {year}": "{day} {month} {year} की पोस्टें",
- "Posts for {month} {year}": "{month} {year} की पोस्टें",
+ "Posts about %s": "%s के बारे में पोस्ट",
+ "Posts by %s": "%s द्वारा पोस्ट",
+ "Posts for year %s": "वर्ष %s के पोस्ट",
+ "Posts for {month_day_year}": "{month_day_year} के पोस्ट",
+ "Posts for {month_year}": "{month_year} के पोस्ट",
"Previous post": "पिछली पोस्ट",
- "Publication date": "प्रकाशन की तारीख",
+ "Previous": "पिछला",
+ "Publication date": "प्रकाशन तिथि",
"RSS feed": "आर एस एस फ़ीड",
"Read in English": "हिन्दी में पढ़िए",
"Read more": "और पढ़िए",
- "Skip to main content": "मुख्य सामग्री पर जाएँ",
- "Source": "सोर्स",
+ "Skip to main content": "मुख्य विषयवस्तु में जाएं",
+ "Source": "स्रोत",
"Subcategories:": "उपश्रेणी",
"Tags and Categories": "टैग्स और श्रेणियाँ",
"Tags": "टैग्स",
- "Toggle navigation": "",
- "Uncategorized": "बिना श्रेणी",
+ "Toggle navigation": "टॉगल नेविगेशन",
+ "Uncategorized": "अवर्गीकृत",
+ "Up": "ऊपर",
"Updates": "अपडेट्स",
- "Write your page here.": "अपना पेज यहाँ लिखिए",
- "Write your post here.": "अपनी पोस्ट यहाँ लिखिए",
- "old posts, page %d": "पुरानी पोस्टें, पृष्‍ठ %d",
+ "Write your page here.": "अपना पेज यहाँ लिखें",
+ "Write your post here.": "अपनी पोस्ट यहाँ लिखें",
+ "old posts, page %d": "पुराने पोस्ट, पृष्‍ठ %d",
"page %d": "पृष्‍ठ %d",
+ "updated": "संशोधित",
}
diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py
index 933bafc..7c72f3b 100644
--- a/nikola/data/themes/base/messages/messages_hr.py
+++ b/nikola/data/themes/base/messages/messages_hr.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minuta preostalo za čitanje",
"(active)": "(aktivno)",
"Also available in:": "Također dostupno i u:",
"Archive": "Arhiva",
+ "Atom feed": "",
"Authors": "Autori",
"Categories": "Kategorije",
"Comments": "Komentari",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Više postova o %s",
"Newer posts": "Noviji postovi",
"Next post": "Sljedeći post",
+ "Next": "",
"No posts found.": "Nema postova.",
"Nothing found.": "Nema ničeg.",
"Older posts": "Stariji postovi",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Postovi o %s",
"Posts by %s": "Objave od %s",
"Posts for year %s": "Postovi za godinu %s",
- "Posts for {month} {day}, {year}": "Objave za {month} {day}, {year}",
- "Posts for {month} {year}": "Postovi za {month} {year}",
+ "Posts for {month_day_year}": "Postovi za {month_day_year}",
+ "Posts for {month_year}": "Postovi za {month_year}",
"Previous post": "Prethodni post",
+ "Previous": "",
"Publication date": "Nadnevak objave",
"RSS feed": "RSS kanal",
"Read in English": "Čitaj na hrvatskom",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Tagovi",
"Toggle navigation": "",
"Uncategorized": "Nekategorizirano",
+ "Up": "",
"Updates": "Nadopune",
"Write your page here.": "Napiši svoju stranicu ovdje",
"Write your post here.": "Napiši svoju objavu ovdje",
"old posts, page %d": "stari postovi, stranice %d",
"page %d": "stranice %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_hu.py b/nikola/data/themes/base/messages/messages_hu.py
index 0b137fc..8b0ac86 100644
--- a/nikola/data/themes/base/messages/messages_hu.py
+++ b/nikola/data/themes/base/messages/messages_hu.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d perc van hátra olvasni",
"(active)": "(aktív)",
"Also available in:": "Olvasható még:",
"Archive": "Archív",
+ "Atom feed": "Atom",
"Authors": "Szerzők",
"Categories": "Kategóriák",
"Comments": "Hozzászólások",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Több bejegyzés erről: %s",
"Newer posts": "Újabb bejegyzések",
"Next post": "A következő bejegyzés",
+ "Next": "Következő",
"No posts found.": "Nincs ilyen bejegyzés.",
"Nothing found.": "Nincs találat.",
"Older posts": "Régebbi bejegyzések",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Bejegyzések erről: %s",
"Posts by %s": "Bejegyzések %s által",
"Posts for year %s": "%s. bejegyzések",
- "Posts for {month} {day}, {year}": "{year}. {month}. {day}.-i bejegyzések",
- "Posts for {month} {year}": "{year}. {month}.-i bejegyzések",
+ "Posts for {month_day_year}": "{month_day_year} bejegyzések",
+ "Posts for {month_year}": "{month_year} bejegyzések",
"Previous post": "Az előző bejegyzés ",
+ "Previous": "Előző",
"Publication date": "A megjelenés dátuma",
"RSS feed": "RSS",
"Read in English": "Olvass magyarul",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Címkék",
"Toggle navigation": "",
"Uncategorized": "Nincs kategorizálva",
+ "Up": "Fel",
"Updates": "Frissítések",
"Write your page here.": "Ide írd az oldalad.",
"Write your post here.": "Ide írd a bejegyzésed.",
"old posts, page %d": "régi bejegyzések, %d. oldal",
"page %d": "%d. oldal",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_ia.py b/nikola/data/themes/base/messages/messages_ia.py
new file mode 100644
index 0000000..34868ba
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_ia.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "%dminutas de lectura remanente",
+ "(active)": "(active)",
+ "Also available in:": "Anque disponibile in:",
+ "Archive": "Archivo",
+ "Atom feed": "Fluxo Atom",
+ "Authors": "Authores",
+ "Categories": "Categorias",
+ "Comments": "Commentos",
+ "LANGUAGE": "Interlingua",
+ "Languages:": "Linguas:",
+ "More posts about %s": "Plure entratas super %s",
+ "Newer posts": "Entratas plus recente",
+ "Next post": "Entrata successive",
+ "Next": "Successive",
+ "No posts found.": "Nulle entrata esseva trovate.",
+ "Nothing found.": "Nihil esseva trovate.",
+ "Older posts": "Entratas plus vetule",
+ "Original site": "Sito original",
+ "Posted:": "Publicate:",
+ "Posts about %s": "Entratas super %s",
+ "Posts by %s": "Entratas per %s",
+ "Posts for year %s": "Entratas del anno %s",
+ "Posts for {month_day_year}": "Entratas de {month_day_year}",
+ "Posts for {month_year}": "Entratas de {month_year}",
+ "Previous post": "Entrata precedente",
+ "Previous": "Precendente",
+ "Publication date": "Data de publication",
+ "RSS feed": "Fluxo RSS",
+ "Read in English": "Lege in interlingua",
+ "Read more": "Lege plus",
+ "Skip to main content": "Salta al contento principal",
+ "Source": "Sorgente",
+ "Subcategories:": "Subcategorias:",
+ "Tags and Categories": "Etiquettas e categorias",
+ "Tags": "Etiquettas",
+ "Toggle navigation": "Commuta navigation",
+ "Uncategorized": "Sin categoria",
+ "Up": "In alto",
+ "Updates": "Actualisationes",
+ "Write your page here.": "Scribe tu pagina hic.",
+ "Write your post here.": "Scribe tu entrata hic.",
+ "old posts, page %d": "Vetule entratas, pagina %d",
+ "page %d": "pagina %d",
+ "updated": "actualisate",
+}
diff --git a/nikola/data/themes/base/messages/messages_id.py b/nikola/data/themes/base/messages/messages_id.py
index ec60f9a..d6c53ae 100644
--- a/nikola/data/themes/base/messages/messages_id.py
+++ b/nikola/data/themes/base/messages/messages_id.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d menit tersisa untuk membaca",
"(active)": "(aktif)",
"Also available in:": "Juga tersedia dalam:",
"Archive": "Arsip",
+ "Atom feed": "Umpan Atom",
"Authors": "Penulis",
"Categories": "Kategori",
"Comments": "Komentar",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Lebih banyak tulisan tentang %s",
"Newer posts": "Tulisan lebih baru",
"Next post": "Tulisan berikutnya",
+ "Next": "Sesudah",
"No posts found.": "Tidak ada tulisan yang ditemukan.",
"Nothing found.": "Tidak ditemukan.",
"Older posts": "Tulisan lebih lama",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Tulisan tentang %s",
"Posts by %s": "Tulisan oleh %s",
"Posts for year %s": "Tulisan untuk tahun %s",
- "Posts for {month} {day}, {year}": "Tulisan untuk {month} {day}, {year}",
- "Posts for {month} {year}": "Tulisan untuk {month} {year}",
+ "Posts for {month_day_year}": "Tulisan untuk {month_day_year}",
+ "Posts for {month_year}": "Tulisan untuk {month_year}",
"Previous post": "Tulisan sebelumnya",
+ "Previous": "Sebelum",
"Publication date": "Tanggal publikasi",
"RSS feed": "Sindikasi RSS",
"Read in English": "Baca dalam Bahasa Indonesia",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Sub kategori:",
"Tags and Categories": "Tag dan Kategori",
"Tags": "Tag",
- "Toggle navigation": "",
+ "Toggle navigation": "Alih navigasi",
"Uncategorized": "Tanpa kategori",
+ "Up": "Atas",
"Updates": "Update",
"Write your page here.": "Tulis halaman Anda disini.",
"Write your post here.": "Tulis tulisan Anda disini.",
"old posts, page %d": "tulisan lama, halaman %d",
"page %d": "halaman %d",
+ "updated": "diperbarui",
}
diff --git a/nikola/data/themes/base/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py
index 08a65d5..2af1a62 100644
--- a/nikola/data/themes/base/messages/messages_it.py
+++ b/nikola/data/themes/base/messages/messages_it.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "ulteriori %d minuti di lettura",
"(active)": "(attivo)",
"Also available in:": "Disponibile anche in:",
"Archive": "Archivio",
+ "Atom feed": "Feed Atom",
"Authors": "Autori",
"Categories": "Categorie",
"Comments": "Commenti",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Altri articoli collegati %s",
"Newer posts": "Articoli più recenti",
"Next post": "Articolo successivo",
+ "Next": "Successivo",
"No posts found.": "Nessun articolo trovato.",
"Nothing found.": "Non trovato.",
"Older posts": "Articoli precedenti",
@@ -22,23 +24,26 @@ MESSAGES = {
"Posts about %s": "Articoli su %s",
"Posts by %s": "Articoli di %s",
"Posts for year %s": "Articoli per l'anno %s",
- "Posts for {month} {day}, {year}": "Articoli per il {day} {month} {year}",
- "Posts for {month} {year}": "Articoli per {month} {year}",
+ "Posts for {month_day_year}": "Articoli per il {month_day_year}",
+ "Posts for {month_year}": "Articoli per {month_year}",
"Previous post": "Articolo precedente",
+ "Previous": "Precedente",
"Publication date": "Data di pubblicazione",
"RSS feed": "Feed RSS",
- "Read in English": "Leggi in inglese",
+ "Read in English": "Leggi in italiano",
"Read more": "Continua la lettura",
"Skip to main content": "Vai al testo principale",
"Source": "Sorgente",
"Subcategories:": "Sottocategorie:",
"Tags and Categories": "Tag e categorie",
"Tags": "Tag",
- "Toggle navigation": "",
+ "Toggle navigation": "Attiva la navigazione",
"Uncategorized": "Senza categorie",
+ "Up": "Su",
"Updates": "Aggiornamenti",
"Write your page here.": "Scrivi qui la tua pagina.",
"Write your post here.": "Scrivi qui il tuo post.",
"old posts, page %d": "vecchi articoli, pagina %d",
"page %d": "pagina %d",
+ "updated": "aggiornato",
}
diff --git a/nikola/data/themes/base/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py
index f0d752e..fd563ad 100644
--- a/nikola/data/themes/base/messages/messages_ja.py
+++ b/nikola/data/themes/base/messages/messages_ja.py
@@ -1,30 +1,33 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "残りを読むのに必要な時間は%d分",
"(active)": "(有効)",
"Also available in:": "他の言語で読む:",
- "Archive": "文書一覧",
+ "Archive": "過去記事一覧",
+ "Atom feed": "Atomフィード",
"Authors": "著者一覧",
"Categories": "カテゴリ",
"Comments": "コメント",
"LANGUAGE": "日本語",
"Languages:": "言語:",
- "More posts about %s": "%sに関する文書一覧",
- "Newer posts": "新しい文書",
- "Next post": "次の文書",
- "No posts found.": "文書はありません。",
+ "More posts about %s": "%sに関する記事一覧",
+ "Newer posts": "新しい記事",
+ "Next post": "次の記事",
+ "Next": "次",
+ "No posts found.": "記事はありません。",
"Nothing found.": "なにも見つかりませんでした。",
- "Older posts": "過去の文書",
+ "Older posts": "過去の記事",
"Original site": "翻訳元のサイト",
"Posted:": "公開日時:",
- "Posts about %s": "%sについての文書",
- "Posts by %s": "%sの文書一覧",
- "Posts for year %s": "%s年の文書",
- "Posts for {month} {day}, {year}": "{year}年{month}{day}日の文書",
- "Posts for {month} {year}": "{year}年{month}の文書",
- "Previous post": "一つ前の文書",
+ "Posts about %s": "%sについての記事",
+ "Posts by %s": "%sの記事一覧",
+ "Posts for year %s": "%s年の記事",
+ "Posts for {month_day_year}": "{month_day_year}の記事",
+ "Posts for {month_year}": "{month_year}の記事",
+ "Previous post": "一つ前の記事",
+ "Previous": "前",
"Publication date": "公開日",
"RSS feed": "RSSフィード",
"Read in English": "日本語で読む",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "サブカテゴリ",
"Tags and Categories": "カテゴリおよびタグ一覧",
"Tags": "タグ",
- "Toggle navigation": "",
- "Uncategorized": "uncategorized",
+ "Toggle navigation": "ナビゲーションを隠す",
+ "Uncategorized": "カテゴリなし",
+ "Up": "上",
"Updates": "フィード",
- "Write your page here.": "ここに文書を記述してください。",
- "Write your post here.": "ここに文書を記述してください。",
- "old posts, page %d": "過去の文書 %dページ目",
+ "Write your page here.": "ここに記事を記述してください。",
+ "Write your post here.": "ここに記事を記述してください。",
+ "old posts, page %d": "過去の記事 %dページ目",
"page %d": "ページ%d",
+ "updated": "更新日時",
}
diff --git a/nikola/data/themes/base/messages/messages_ko.py b/nikola/data/themes/base/messages/messages_ko.py
index 9a87aef..57b6cb9 100644
--- a/nikola/data/themes/base/messages/messages_ko.py
+++ b/nikola/data/themes/base/messages/messages_ko.py
@@ -1,19 +1,21 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "읽기 %d분 남음.",
"(active)": "(활성됨)",
"Also available in:": "이곳에서도 가능함:",
"Archive": "저장소",
+ "Atom feed": "",
"Authors": "작성자",
"Categories": "분류",
"Comments": "댓글",
- "LANGUAGE": "영어",
+ "LANGUAGE": "한국어",
"Languages:": "언어:",
"More posts about %s": "%s에 대한 또다른 포스트",
"Newer posts": "최신 포스트",
"Next post": "다음 포스트",
+ "Next": "다음",
"No posts found.": "검색된 포스트 없음.",
"Nothing found.": "검색 결과 없음.",
"Older posts": "옛날 포스트",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "%s에 대한 포스트",
"Posts by %s": "%s에 의해 작성된 글",
"Posts for year %s": "%s년도 포스트",
- "Posts for {month} {day}, {year}": " {year}년 {month}월 {day}일에 작성된 포스트",
- "Posts for {month} {year}": "{year}년 {month}월에 쓴 포스트",
+ "Posts for {month_day_year}": "{month_day_year}에 작성된 포스트",
+ "Posts for {month_year}": "{month_year}에 쓴 포스트",
"Previous post": "이전 포스트",
+ "Previous": "이전",
"Publication date": "발간일",
"RSS feed": "RSS 목록",
"Read in English": "영어로 읽기",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "태그",
"Toggle navigation": "",
"Uncategorized": "카테고리 없음",
+ "Up": "",
"Updates": "업데이트",
"Write your page here.": "여기에 페이지를 작성하세요.",
"Write your post here.": "이곳에 글을 작성하세요.",
"old posts, page %d": "이전 포스트, 페이지 %d",
"page %d": "페이지 %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_lt.py b/nikola/data/themes/base/messages/messages_lt.py
index 54b61d1..d79b940 100644
--- a/nikola/data/themes/base/messages/messages_lt.py
+++ b/nikola/data/themes/base/messages/messages_lt.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "liko %d min skaitymo",
"(active)": "(aktyvi)",
"Also available in:": "Taip pat turimas šiomis kalbomis:",
"Archive": "Archyvas",
+ "Atom feed": "",
"Authors": "Autoriai",
"Categories": "Kategorijos",
"Comments": "Komentarai",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Daugiau įrašų apie %s",
"Newer posts": "Naujesni įrašai",
"Next post": "Sekantis įrašas",
+ "Next": "",
"No posts found.": "Įrašų nerasta.",
"Nothing found.": "Nieko nerasta.",
"Older posts": "Senesni įrašai",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Įrašai apie %s",
"Posts by %s": "Autoriaus %s įrašai",
"Posts for year %s": "%s metų įrašai",
- "Posts for {month} {day}, {year}": "{month} {day}, {year} įrašai",
- "Posts for {month} {year}": "{month} {year} įrašai",
+ "Posts for {month_day_year}": "{month_day_year} įrašai",
+ "Posts for {month_year}": "{month_year} įrašai",
"Previous post": "Ankstesnis įrašas",
+ "Previous": "",
"Publication date": "Publikavimo data",
"RSS feed": "RSS srautas",
"Read in English": "Skaityti lietuviškai",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Žymės",
"Toggle navigation": "",
"Uncategorized": "Nekategorizuota",
+ "Up": "",
"Updates": "Atnaujinimai",
"Write your page here.": "Čia rašykite puslapio tekstą.",
"Write your post here.": "Čia rašykite įrašo tekstą.",
"old posts, page %d": "seni įrašai, %d puslapis",
"page %d": "%d puslapis",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_mi.py b/nikola/data/themes/base/messages/messages_mi.py
new file mode 100644
index 0000000..21f9739
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_mi.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "",
+ "(active)": "(mātātoa)",
+ "Also available in:": "",
+ "Archive": "Pūranga",
+ "Atom feed": "",
+ "Authors": "Kaituhi",
+ "Categories": "Kāwai",
+ "Comments": "Kōrerohia",
+ "LANGUAGE": " Māori",
+ "Languages:": "Reo",
+ "More posts about %s": "",
+ "Newer posts": "",
+ "Next post": "",
+ "Next": "Panuku",
+ "No posts found.": "",
+ "Nothing found.": "Kīhai i kitea",
+ "Older posts": "",
+ "Original site": "",
+ "Posted:": "",
+ "Posts about %s": "",
+ "Posts by %s": "",
+ "Posts for year %s": "",
+ "Posts for {month_day_year}": "",
+ "Posts for {month_year}": "",
+ "Previous post": "",
+ "Previous": "Whakamuri",
+ "Publication date": "",
+ "RSS feed": "Whāngai RSS",
+ "Read in English": "",
+ "Read more": "",
+ "Skip to main content": "",
+ "Source": "Matapuna",
+ "Subcategories:": "",
+ "Tags and Categories": "Tūtohu me nga kāwai",
+ "Tags": "Tūtohu",
+ "Toggle navigation": "",
+ "Uncategorized": "",
+ "Up": "Ki runga",
+ "Updates": "Whakahou",
+ "Write your page here.": "",
+ "Write your post here.": "",
+ "old posts, page %d": "",
+ "page %d": "whārangi %d",
+ "updated": "whakahoutia",
+}
diff --git a/nikola/data/themes/base/messages/messages_ml.py b/nikola/data/themes/base/messages/messages_ml.py
new file mode 100644
index 0000000..a818320
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_ml.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "%d മിനിട്ട് കൂടി വായിച്ചു തീരാന്‍ ",
+ "(active)": "(സജീവം)",
+ "Also available in:": "ലഭ്യമായ ഭാഷകള്‍:",
+ "Archive": "ആര്‍കൈവ്",
+ "Atom feed": "ആറ്റം ഫിഡ്",
+ "Authors": "രചയിതാക്കള്‍",
+ "Categories": "വിഭാഗങ്ങള്‍",
+ "Comments": "കമന്റുകള്‍",
+ "LANGUAGE": "മലയാളം",
+ "Languages:": "ഭാഷകള്‍:",
+ "More posts about %s": "%s-നെ കുറിച്ചുള്ള കൂടുതല്‍ രചനകള്‍",
+ "Newer posts": "പുതിയ രചനകള്‍",
+ "Next post": "അടുത്ത രചന",
+ "Next": "അടുത്തത്",
+ "No posts found.": "രചനകള്‍ ലഭ്യമല്ല.",
+ "Nothing found.": "ഒന്നും ലഭ്യമല്ല.",
+ "Older posts": "പഴയ രചനകള്‍",
+ "Original site": "യഥാര്‍ഥ സൈറ്റ്",
+ "Posted:": "എഴുതിയത്:",
+ "Posts about %s": "%s -നെ കുറിച്ചുള്ള രചനകള്‍",
+ "Posts by %s": "%s എഴുതിയ രചനകള്‍",
+ "Posts for year %s": "%s-ാം ആണ്ടിലെ രചനകള്‍",
+ "Posts for {month_day_year}": "{month_day_year} ലെ രചനകള്‍",
+ "Posts for {month_year}": "{month_year} ലെ രചനകള്‍",
+ "Previous post": "മുമ്പിലെ രചന",
+ "Previous": "കഴിഞ്ഞത്",
+ "Publication date": "പ്രസിദ്ധീകരിച്ച തീയതി",
+ "RSS feed": "ആര്‍ എസ് എസ് ഫീഡ്",
+ "Read in English": "മലയാളത്തില്‍ വായിക്കുക",
+ "Read more": "കൂടുതല്‍ വായിക്കുക",
+ "Skip to main content": "പ്രധാന ഉള്ളടക്കത്തിലേക്ക് നേരെ പോവുക",
+ "Source": "സ്രോതസ്സ്‌",
+ "Subcategories:": "ഉപവിഭാഗങ്ങള്‍:",
+ "Tags and Categories": "ടാഗുകളും വിഭാഗങ്ങളും",
+ "Tags": "ടാഗുകള്‍",
+ "Toggle navigation": "നാവിഗേഷൻ മാറ്റുക",
+ "Uncategorized": "വേര്‍തിരിക്കാത്തവ",
+ "Up": "മുകളിലേക്ക്",
+ "Updates": "അപ്ഡേറ്റുകൾ",
+ "Write your page here.": "താങ്കളുടെ താള്‍ ഇവിടെ എഴുതുക.",
+ "Write your post here.": "താങ്കളുടെ രചന ഇവിടെ എഴുതുക.",
+ "old posts, page %d": "പഴയ രചനകള്‍, താള്‍ %d",
+ "page %d": "താള്‍ %d",
+ "updated": "പുതുക്കിയത്",
+}
diff --git a/nikola/data/themes/base/messages/messages_mr.py b/nikola/data/themes/base/messages/messages_mr.py
new file mode 100644
index 0000000..49e4fb6
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_mr.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "वाचण्यासाठी %d मिनिटे शिल्लक आहेत",
+ "(active)": "(सक्रिय)",
+ "Also available in:": "मध्ये देखील उपलब्ध:",
+ "Archive": "संग्रह",
+ "Atom feed": "अ‍ॅटम फीड",
+ "Authors": "लेखक",
+ "Categories": "श्रेणी",
+ "Comments": "टिप्पण्या",
+ "LANGUAGE": "मराठी",
+ "Languages:": "भाषा:",
+ "More posts about %s": "%s बद्दल अधिक पोस्ट",
+ "Newer posts": "नवीन पोस्ट्स",
+ "Next post": "पुढील पोस्ट",
+ "Next": "पुढे",
+ "No posts found.": "कोणतीही पोस्ट आढळली नाहीत",
+ "Nothing found.": "काहीही सापडले नाही.",
+ "Older posts": "जुने पोस्ट",
+ "Original site": "मूळ साइट",
+ "Posted:": "पोस्ट केले:",
+ "Posts about %s": "%s बद्दल पोस्ट",
+ "Posts by %s": "%s द्वारा पोस्ट केलेले",
+ "Posts for year %s": "%s वर्षासाठी पोस्ट",
+ "Posts for {month_day_year}": "{month_day_year} साठी पोस्ट्स",
+ "Posts for {month_year}": "{month_year} साठी पोस्ट्स",
+ "Previous post": "मागील पोस्ट",
+ "Previous": "मागील",
+ "Publication date": "प्रकाशन तारीख",
+ "RSS feed": "आरएसएस फीड",
+ "Read in English": "मराठी मध्ये वाचा",
+ "Read more": "अधिक वाचा",
+ "Skip to main content": "मुख्य सामग्रीकडे जा",
+ "Source": "स्रोत",
+ "Subcategories:": "उपश्रेणी:",
+ "Tags and Categories": "टॅग्ज आणि श्रेण्या",
+ "Tags": "टॅग्ज",
+ "Toggle navigation": "नेव्हिगेशन टॉगल करा",
+ "Uncategorized": "अवर्गीकृत",
+ "Up": "वर",
+ "Updates": "अद्यतने",
+ "Write your page here.": "आपले पृष्ठ येथे लिहा.",
+ "Write your post here.": "आपले पोस्ट येथे लिहा.",
+ "old posts, page %d": "जुने पोस्ट, पृष्ठ %d",
+ "page %d": "पृष्ठ %d",
+ "updated": "अद्यतनित",
+}
diff --git a/nikola/data/themes/base/messages/messages_nb.py b/nikola/data/themes/base/messages/messages_nb.py
index 8ab0911..599d6ed 100644
--- a/nikola/data/themes/base/messages/messages_nb.py
+++ b/nikola/data/themes/base/messages/messages_nb.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min gjenstår å lese",
"(active)": "(aktiv)",
"Also available in:": "Også tilgjengelig på:",
"Archive": "Arkiv",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorier",
"Comments": "Kommentarer",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Flere innlegg om %s",
"Newer posts": "Nyere innlegg",
"Next post": "Neste innlegg",
+ "Next": "",
"No posts found.": "Fant ingen innlegg.",
"Nothing found.": "Fant ingenting.",
"Older posts": "Eldre innlegg",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Innlegg om %s",
"Posts by %s": "",
"Posts for year %s": "Innlegg fra %s",
- "Posts for {month} {day}, {year}": "Innlegg fra {day}. {month} {year}",
- "Posts for {month} {year}": "Innlegg fra {month} {year}",
+ "Posts for {month_day_year}": "Innlegg fra {month_day_year}",
+ "Posts for {month_year}": "Innlegg fra {month_year}",
"Previous post": "Forrige innlegg",
+ "Previous": "",
"Publication date": "Publiseringsdato",
"RSS feed": "RSS-nyhetskanal",
"Read in English": "Les på norsk",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Merker",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "Skriv siden din her.",
"Write your post here.": "Skriv innlegget din her.",
"old posts, page %d": "eldre innlegg, side %d",
"page %d": "side %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py
index 7c5698d..ee98121 100644
--- a/nikola/data/themes/base/messages/messages_nl.py
+++ b/nikola/data/themes/base/messages/messages_nl.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min resterende leestijd ",
"(active)": "(actief)",
"Also available in:": "Ook beschikbaar in:",
"Archive": "Archief",
+ "Atom feed": "Atom-feed",
"Authors": "Auteurs",
"Categories": "Categorieën",
"Comments": "Commentaar",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Meer berichten over %s",
"Newer posts": "Nieuwere berichten",
"Next post": "Volgend bericht",
+ "Next": "Volgende",
"No posts found.": "Geen berichten gevonden.",
"Nothing found.": "Niets gevonden.",
"Older posts": "Oudere berichten",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Berichten over %s",
"Posts by %s": "Berichten van %s",
"Posts for year %s": "Berichten voor het jaar %s",
- "Posts for {month} {day}, {year}": "Berichten voor {month} {day}, {year}",
- "Posts for {month} {year}": "Berichten voor {month} {year}",
+ "Posts for {month_day_year}": "Berichten voor {month_day_year}",
+ "Posts for {month_year}": "Berichten voor {month_year}",
"Previous post": "Vorig bericht",
+ "Previous": "Vorige",
"Publication date": "Publicatiedatum",
"RSS feed": "RSS-feed",
"Read in English": "Lees in het Nederlands",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Tags",
"Toggle navigation": "Toggle navigatie",
"Uncategorized": "Ongeordend",
+ "Up": "Omhoog",
"Updates": "Bijgewerkte versies",
"Write your page here.": "Schrijf hier je pagina.",
"Write your post here.": "Schrijf hier je bericht.",
"old posts, page %d": "oude berichten, pagina %d",
"page %d": "pagina %d",
+ "updated": "bijgewerkt",
}
diff --git a/nikola/data/themes/base/messages/messages_pa.py b/nikola/data/themes/base/messages/messages_pa.py
index 5eb76f1..8a0be76 100644
--- a/nikola/data/themes/base/messages/messages_pa.py
+++ b/nikola/data/themes/base/messages/messages_pa.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "ਪੜਣ ਲਈ %d ਮਿੰਟ ਬਾਕੀ",
"(active)": "(ਚਲੰਤ)",
"Also available in:": "ਹੋਰ ਉਪਲਬਧ ਬੋਲੀਆਂ:",
"Archive": "ਆਰਕਾਈਵ",
+ "Atom feed": "ਐਟਮ ਫੀਡ",
"Authors": "ਲੇਖਕ",
"Categories": "ਸ਼੍ਰੇਣੀ",
"Comments": "ਟਿੱਪਣੀਆਂ",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "%s ਬਾਰੇ ਹੋਰ ਲਿਖਤਾਂ",
"Newer posts": "ਨਵੀਆਂ ਲਿਖਤਾਂ",
"Next post": "ਅਗਲੀ ਲਿਖਤ",
+ "Next": "ਅੱਗੇ ",
"No posts found.": "ਕੋਈ ਲਿਖਤ ਨਹੀਂ ਲੱਭੀ |",
"Nothing found.": "ਕੁਝ ਨਹੀਂ ਲੱਭਿਆ |",
"Older posts": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "%s ਬਾਰੇ ਲਿਖਤਾਂ",
"Posts by %s": "%s ਦੀ ਲਿਖਤਾਂ",
"Posts for year %s": "ਸਾਲ %s ਦੀਆਂ ਲਿਖਤਾਂ",
- "Posts for {month} {day}, {year}": "{day} {month} {year} ਦੀਆਂ ਲਿਖਤਾਂ",
- "Posts for {month} {year}": "{month} {year} ਦੀਆਂ ਲਿਖਤਾਂ",
+ "Posts for {month_day_year}": "{month_day_year} ਦੀਆਂ ਲਿਖਤਾਂ",
+ "Posts for {month_year}": "{month_year} ਦੀਆਂ ਲਿਖਤਾਂ",
"Previous post": "ਪਿਛਲੀ ਲਿਖਤ",
+ "Previous": "ਪਿੱਛੇ ",
"Publication date": "ਛਪਾਈ ਦੀ ਤਰੀਕ",
"RSS feed": "ਆਰ ਐੱਸ ਐੱਸ ਫੀਡ",
"Read in English": "ਪੰਜਾਬੀ ਵਿੱਚ ਪੜ੍ਹੋ",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "ਉਪਸ਼੍ਰੇਣੀਆਂ:",
"Tags and Categories": "ਟੈਗ ਅਤੇ ਸ਼੍ਰੇਣੀਆਂ",
"Tags": "ਟੈਗ",
- "Toggle navigation": "",
+ "Toggle navigation": "ਨੈਵੀਗੇਸ਼ਨ ਬਦਲੋ ",
"Uncategorized": "ਇਤਾਹਾਸ",
+ "Up": "ਉੱਤੇ ",
"Updates": "ਅੱਪਡੇਟਸ",
"Write your page here.": "ਆਪਣਾ ਸਫ਼ਾ ਏਥੇ ਲਿਖੋ |",
"Write your post here.": "ਆਪਣੀ ਲਿਖਤ ਏਥੇ ਲਿਖੋ |",
"old posts, page %d": "ਪੁਰਾਣੀਆਂ ਲਿਖਤਾਂ , ਸਫ਼ਾ %d",
"page %d": "ਸਫ਼ਾ %d",
+ "updated": "ਅੱਪਡੇਟ ਕੀਤਾ",
}
diff --git a/nikola/data/themes/base/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py
index 257a31a..2d13e32 100644
--- a/nikola/data/themes/base/messages/messages_pl.py
+++ b/nikola/data/themes/base/messages/messages_pl.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "zostało %d minut czytania",
"(active)": "(aktywne)",
"Also available in:": "Również dostępny w językach:",
"Archive": "Archiwum",
+ "Atom feed": "Kanał Atom",
"Authors": "Autorzy",
"Categories": "Kategorie",
"Comments": "Komentarze",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Więcej postów o %s",
"Newer posts": "Nowsze posty",
"Next post": "Następny post",
+ "Next": "Następne",
"No posts found.": "Nie znaleziono żadnych postów.",
"Nothing found.": "Nic nie znaleziono.",
"Older posts": "Starsze posty",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Posty o %s",
"Posts by %s": "Posty autora %s",
"Posts for year %s": "Posty z roku %s",
- "Posts for {month} {day}, {year}": "Posty z {day} {month} {year}",
- "Posts for {month} {year}": "Posty z {month} {year}",
+ "Posts for {month_day_year}": "Posty z {month_day_year}",
+ "Posts for {month_year}": "Posty z {month_year:MMMM yyyy}",
"Previous post": "Poprzedni post",
+ "Previous": "Poprzednie",
"Publication date": "Data publikacji",
"RSS feed": "Kanał RSS",
"Read in English": "Czytaj po polsku",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Tagi",
"Toggle navigation": "Pokaż/ukryj menu",
"Uncategorized": "Nieskategoryzowane",
+ "Up": "Do góry",
"Updates": "Aktualności",
"Write your page here.": "Tu wpisz treść strony.",
"Write your post here.": "Tu wpisz treść postu.",
"old posts, page %d": "stare posty, strona %d",
"page %d": "strona %d",
+ "updated": "aktualizacja",
}
diff --git a/nikola/data/themes/base/messages/messages_pt.py b/nikola/data/themes/base/messages/messages_pt.py
index 4a184bd..92c56a2 100644
--- a/nikola/data/themes/base/messages/messages_pt.py
+++ b/nikola/data/themes/base/messages/messages_pt.py
@@ -1,12 +1,13 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minutos restante para leitura",
"(active)": "(ativo)",
"Also available in:": "Também disponível em:",
"Archive": "Arquivo",
- "Authors": "",
+ "Atom feed": "Feed Atom",
+ "Authors": "Autores",
"Categories": "Categorias",
"Comments": "Comentários",
"LANGUAGE": "Português",
@@ -14,17 +15,19 @@ MESSAGES = {
"More posts about %s": "Mais textos publicados sobre %s",
"Newer posts": "Textos publicados mais recentes",
"Next post": "Próximo texto publicado",
+ "Next": "Próximo",
"No posts found.": "Nenhum texto publicado foi encontrado",
"Nothing found.": "Nada encontrado.",
"Older posts": "Textos publicados mais antigos",
"Original site": "Sítio original",
"Posted:": "Publicado:",
"Posts about %s": "Textos publicados sobre %s",
- "Posts by %s": "",
+ "Posts by %s": "Textos publicados por %s",
"Posts for year %s": "Textos publicados do ano %s",
- "Posts for {month} {day}, {year}": "Textos publicados de {day} {month} {year}",
- "Posts for {month} {year}": "Textos publicados de {month} {year}",
+ "Posts for {month_day_year}": "Textos publicados de {month_day_year}",
+ "Posts for {month_year}": "Textos publicados de {month_year}",
"Previous post": "Texto publicado anterior",
+ "Previous": "Anterior",
"Publication date": "Data de publicação",
"RSS feed": "Feed RSS",
"Read in English": "Ler em português",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Sub-Categorias:",
"Tags and Categories": "Etiquetas e Categorias",
"Tags": "Etiqueta",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
+ "Toggle navigation": "Alternar Navegação",
+ "Uncategorized": "Sem Categoria",
+ "Up": "Cima",
+ "Updates": "Actualizaçōes",
"Write your page here.": "Escreva a sua página aqui.",
"Write your post here.": "Escreva o seu texto para publicar aqui.",
"old posts, page %d": "Textos publicados antigos, página %d",
"page %d": "página %d",
+ "updated": "Actualizado",
}
diff --git a/nikola/data/themes/base/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py
index b2877e4..f33abcc 100644
--- a/nikola/data/themes/base/messages/messages_pt_br.py
+++ b/nikola/data/themes/base/messages/messages_pt_br.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d mín restante para leitura",
"(active)": "(ativo)",
"Also available in:": "Também disponível em:",
"Archive": "Arquivo",
+ "Atom feed": "Feed Atom",
"Authors": "Autores",
"Categories": "Categorias",
"Comments": "Comentários",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Mais posts sobre %s",
"Newer posts": "Posts mais recentes",
"Next post": "Próximo post",
+ "Next": "Próximo",
"No posts found.": "Nenhum tópico encontrado.",
"Nothing found.": "Nada encontrado.",
"Older posts": "Posts mais antigos",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Posts sobre %s",
"Posts by %s": "Postado por %s",
"Posts for year %s": "Posts do ano %s",
- "Posts for {month} {day}, {year}": "Posts do {day} {month}, {year}",
- "Posts for {month} {year}": "Posts de {month} {year}",
+ "Posts for {month_day_year}": "Posts do {month_day_year}",
+ "Posts for {month_year}": "Posts de {month_year}",
"Previous post": "Post anterior",
+ "Previous": "Anterior",
"Publication date": "Data de publicação",
"RSS feed": "Feed RSS",
"Read in English": "Ler em português",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Tags",
"Toggle navigation": "Alternar navegação",
"Uncategorized": "Sem categoria",
+ "Up": "acima",
"Updates": "Atualizações",
"Write your page here.": "Insira a sua página aqui",
"Write your post here.": "Escreva o seu comentário aqui.",
"old posts, page %d": "Posts antigos, página %d",
"page %d": "página %d",
+ "updated": "Atualizado",
}
diff --git a/nikola/data/themes/base/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py
index 0070b94..f2f33cc 100644
--- a/nikola/data/themes/base/messages/messages_ru.py
+++ b/nikola/data/themes/base/messages/messages_ru.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d минут чтения осталось",
"(active)": "(активная)",
"Also available in:": "Также доступно на:",
"Archive": "Архив",
+ "Atom feed": "",
"Authors": "Разработчики",
"Categories": "Категории",
"Comments": "Комментарии",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Больше записей о %s",
"Newer posts": "Новые записи",
"Next post": "Следующая запись",
+ "Next": "Следующая",
"No posts found.": "Записей не найдено.",
"Nothing found.": "Ничего не найдено.",
"Older posts": "Старые записи",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Записи о %s",
"Posts by %s": "Запись %s",
"Posts for year %s": "Записи за %s год",
- "Posts for {month} {day}, {year}": "Записи за {day} {month} {year}",
- "Posts for {month} {year}": "Записи за {month} {year}",
+ "Posts for {month_day_year}": "Записи за {month_day_year}",
+ "Posts for {month_year}": "Записи за {month_year}",
"Previous post": "Предыдущая запись",
+ "Previous": "Предыдущая",
"Publication date": "Дата опубликования",
"RSS feed": "RSS лента",
"Read in English": "Прочесть по-русски",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Подкатегории:",
"Tags and Categories": "Тэги и категории",
"Tags": "Тэги",
- "Toggle navigation": "",
+ "Toggle navigation": "Включить навигацию",
"Uncategorized": "Несортированное",
+ "Up": "Наверх",
"Updates": "Обновления",
"Write your page here.": "Создайте Вашу страницу здесь.",
"Write your post here.": "Создайте Вашу запись здесь.",
"old posts, page %d": "%d страница со старыми записями",
"page %d": "%d страница",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_si_lk.py b/nikola/data/themes/base/messages/messages_si_lk.py
deleted file mode 100644
index 6107c54..0000000
--- a/nikola/data/themes/base/messages/messages_si_lk.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
-
-MESSAGES = {
- "%d min remaining to read": "",
- "(active)": "",
- "Also available in:": "",
- "Archive": "",
- "Authors": "",
- "Categories": "",
- "Comments": "",
- "LANGUAGE": "",
- "Languages:": "",
- "More posts about %s": "",
- "Newer posts": "",
- "Next post": "",
- "No posts found.": "",
- "Nothing found.": "",
- "Older posts": "",
- "Original site": "",
- "Posted:": "",
- "Posts about %s": "",
- "Posts by %s": "",
- "Posts for year %s": "",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "",
- "Previous post": "",
- "Publication date": "",
- "RSS feed": "",
- "Read in English": "",
- "Read more": "",
- "Skip to main content": "",
- "Source": "",
- "Subcategories:": "",
- "Tags and Categories": "",
- "Tags": "",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
- "Write your page here.": "",
- "Write your post here.": "",
- "old posts, page %d": "",
- "page %d": "",
-}
diff --git a/nikola/data/themes/base/messages/messages_sk.py b/nikola/data/themes/base/messages/messages_sk.py
index 7b7df6f..aea46f2 100644
--- a/nikola/data/themes/base/messages/messages_sk.py
+++ b/nikola/data/themes/base/messages/messages_sk.py
@@ -1,12 +1,13 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "zostáva %d minút na čítanie",
"(active)": "(aktívne)",
"Also available in:": "Tiež dostupné v:",
"Archive": "Archív",
- "Authors": "",
+ "Atom feed": "Atom kanál",
+ "Authors": "Autori",
"Categories": "Kategórie",
"Comments": "Komentáre",
"LANGUAGE": "Slovenčina",
@@ -14,17 +15,19 @@ MESSAGES = {
"More posts about %s": "Viac príspevkov o %s",
"Newer posts": "Novšie príspevky",
"Next post": "Nasledujúci príspevok",
+ "Next": "Nasledujúci",
"No posts found.": "Žiadne príspevky nenájdené",
"Nothing found.": "Nič nenájdené.",
"Older posts": "Staršie príspevky",
"Original site": "Pôvodná stránka",
"Posted:": "Zverejnené:",
"Posts about %s": "Príspevky o %s",
- "Posts by %s": "",
+ "Posts by %s": "Príspevky od %s",
"Posts for year %s": "Príspevky z roku %s",
- "Posts for {month} {day}, {year}": "Príspevky zo dňa {day}. {month} {year}",
- "Posts for {month} {year}": "Príspevky za mesiac {month} z roku {year}",
+ "Posts for {month_day_year}": "Príspevky zo dňa {day}. {month_year}",
+ "Posts for {month_year}": "Príspevky za mesiac {month} z roku {year}",
"Previous post": "Predchádzajúci príspevok",
+ "Previous": "Predchádzajúci",
"Publication date": "Dátum zverejnenia",
"RSS feed": "RSS kanál",
"Read in English": "Čítať v slovenčine",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Podkategórie:",
"Tags and Categories": "Štítky a kategórie",
"Tags": "Štítky",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
+ "Toggle navigation": "Prepnúť navigáciu",
+ "Uncategorized": "Nekategorizované",
+ "Up": "Dohora",
+ "Updates": "Aktualizácie",
"Write your page here.": "Tu napíšte svoju stránku.",
"Write your post here.": "Tu napíšte svoj príspevok.",
"old posts, page %d": "staré príspevky, strana %d",
"page %d": "stránka %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py
index 31f3a58..6bd6370 100644
--- a/nikola/data/themes/base/messages/messages_sl.py
+++ b/nikola/data/themes/base/messages/messages_sl.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "še %d min za branje preostanka",
"(active)": "",
"Also available in:": "Na voljo tudi v:",
"Archive": "Arhiv",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorije",
"Comments": "Komentarji",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Več objav o %s",
"Newer posts": "Novejše objave",
"Next post": "Naslednja objava",
+ "Next": "",
"No posts found.": "Ni najdenih objav.",
"Nothing found.": "Brez zadetkov.",
"Older posts": "Starejše objave",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Objave o %s",
"Posts by %s": "",
"Posts for year %s": "Objave za leto %s",
- "Posts for {month} {day}, {year}": "Objave za {day}. {month}, {year}",
- "Posts for {month} {year}": "Objave za {month} {year}",
+ "Posts for {month_day_year}": "Objave za {month_day_year}",
+ "Posts for {month_year}": "Objave za {month_year}",
"Previous post": "Prejšnja objava",
+ "Previous": "",
"Publication date": "Datum objave",
"RSS feed": "vir RSS",
"Read in English": "Beri v slovenščini",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Značke",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "",
"Write your post here.": "",
"old posts, page %d": "stare objave, stran %d",
"page %d": "stran %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sq.py b/nikola/data/themes/base/messages/messages_sq.py
index 6d5e39a..088cdcb 100644
--- a/nikola/data/themes/base/messages/messages_sq.py
+++ b/nikola/data/themes/base/messages/messages_sq.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d min ngelen për tu lexuar",
"(active)": "(aktiv)",
"Also available in:": "Gjithashtu e disponueshme në:",
"Archive": "Arkiva",
+ "Atom feed": "",
"Authors": "Autorë",
"Categories": "Kategori",
"Comments": "Komente",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Më shumë postime rreth %s",
"Newer posts": "Postime më të reja",
"Next post": "Postimi i rradhës",
+ "Next": "",
"No posts found.": "Nuk është gjetur asnjë post.",
"Nothing found.": "Nuk është gjetur asgjë.",
"Older posts": "Postime më të vjetra",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Postime rreth %s",
"Posts by %s": "Postime nga %s",
"Posts for year %s": "Postime për vitin %s",
- "Posts for {month} {day}, {year}": "Postime për {month} {day}, {year}",
- "Posts for {month} {year}": "Postime për {month} {year}",
+ "Posts for {month_day_year}": "Postime për {month_day_year}",
+ "Posts for {month_year}": "Postime për {month_year}",
"Previous post": "Postim i kaluar",
+ "Previous": "",
"Publication date": "Data e publikimit",
"RSS feed": "Furnizim RSS",
"Read in English": "Lexo në Shqip",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etiketa",
"Toggle navigation": "",
"Uncategorized": "E pa kategorizuar",
+ "Up": "",
"Updates": "Përditësime",
"Write your page here.": "Shkruaj faqen tënde këtu.",
"Write your post here.": "Shkruaj postin tënd këtu.",
"old posts, page %d": "Postime të kaluara, faqe %d",
"page %d": "faqe %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sr.py b/nikola/data/themes/base/messages/messages_sr.py
index 03f1024..98e023e 100644
--- a/nikola/data/themes/base/messages/messages_sr.py
+++ b/nikola/data/themes/base/messages/messages_sr.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d минута је преостало за читање",
"(active)": "(активно)",
"Also available in:": "Такође доступан у:",
"Archive": "Архива",
+ "Atom feed": "",
"Authors": "",
"Categories": "Категорије",
"Comments": "Коментари",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Више постова о %s",
"Newer posts": "Новији постови",
"Next post": "Следећи пост",
+ "Next": "",
"No posts found.": "Нема постова.",
"Nothing found.": "Није ништа пронађено.",
"Older posts": "Старији постови",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Постови о %s",
"Posts by %s": "",
"Posts for year %s": "Постови за годину %s",
- "Posts for {month} {day}, {year}": "Објаве за {month} {day}, {year}",
- "Posts for {month} {year}": "Постови за {month} {year}",
+ "Posts for {month_day_year}": "Објаве за {month_day_year}",
+ "Posts for {month_year}": "Постови за {month_year}",
"Previous post": "Претходни пост",
+ "Previous": "",
"Publication date": "Датум објаве",
"RSS feed": "RSS feed",
"Read in English": "Прочитај на српском",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Тагови",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "Вашу страницу напишите овдје.",
"Write your post here.": "Вашу објаву напишите овдје.",
"old posts, page %d": "стари постови, страна %d",
"page %d": "страна %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sr_latin.py b/nikola/data/themes/base/messages/messages_sr_latin.py
index f7f3727..4b1e827 100644
--- a/nikola/data/themes/base/messages/messages_sr_latin.py
+++ b/nikola/data/themes/base/messages/messages_sr_latin.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minuta preostalo za čitanje",
"(active)": "(aktivno)",
"Also available in:": "Takođe dostupan u:",
"Archive": "Arhiva",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorije",
"Comments": "Komentari",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Više objava o %s",
"Newer posts": "Novije objave",
"Next post": "Naredna objava",
+ "Next": "",
"No posts found.": "Nema objava.",
"Nothing found.": "Ništa nije pronađeno.",
"Older posts": "Starije objave",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Objave o %s",
"Posts by %s": "",
"Posts for year %s": "Objave u godini %s",
- "Posts for {month} {day}, {year}": "Objave za {month} {day}, {year}",
- "Posts for {month} {year}": "Objave za {month} {year}",
+ "Posts for {month_day_year}": "Objave za {month_day_year}",
+ "Posts for {month_year}": "Objave za {month_year}",
"Previous post": "Prethodne objave",
+ "Previous": "",
"Publication date": "Datum objavljivanja",
"RSS feed": "RSS feed",
"Read in English": "Pročitaj na bosanskom",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Oznake",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "Vašu stranicu napišite ovdje.",
"Write your post here.": "Vašu objavu napišite ovdje.",
"old posts, page %d": "stare objave, strana %d",
"page %d": "strana %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_sv.py b/nikola/data/themes/base/messages/messages_sv.py
index 106138f..ce56b11 100644
--- a/nikola/data/themes/base/messages/messages_sv.py
+++ b/nikola/data/themes/base/messages/messages_sv.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d minuter kvar att läsa",
"(active)": "(aktiv)",
"Also available in:": "Även tillgänglig på:",
"Archive": "Arkiv",
+ "Atom feed": "",
"Authors": "",
"Categories": "Kategorier",
"Comments": "Kommentarer",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "Fler inlägg om %s",
"Newer posts": "Nya inlägg",
"Next post": "Nästa inlägg",
+ "Next": "",
"No posts found.": "Inga inlägg hittade.",
"Nothing found.": "Inget hittat.",
"Older posts": "Äldre inlägg",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "Inlägg om %s",
"Posts by %s": "",
"Posts for year %s": "Inlägg för år %s",
- "Posts for {month} {day}, {year}": "Inlägg för {month} {day}, {year}",
- "Posts for {month} {year}": "Inlägg för {month} {year}",
+ "Posts for {month_day_year}": "Inlägg för {month_day_year}",
+ "Posts for {month_year}": "Inlägg för {month_year}",
"Previous post": "Föregående inlägg",
+ "Previous": "",
"Publication date": "Publiceringsdatum",
"RSS feed": "RSS-flöde",
"Read in English": "Läs på svenska",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Taggar",
"Toggle navigation": "",
"Uncategorized": "",
+ "Up": "",
"Updates": "",
"Write your page here.": "Skriv din sida här.",
"Write your post here.": "Skriv ditt inlägg här.",
"old posts, page %d": "gamla inlägg, sida %d",
"page %d": "sida %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_te.py b/nikola/data/themes/base/messages/messages_te.py
index 70d8150..385ff0b 100644
--- a/nikola/data/themes/base/messages/messages_te.py
+++ b/nikola/data/themes/base/messages/messages_te.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d నిమిషాలు చదవడానికి కావలెను ",
"(active)": "(క్రియాశీల)",
"Also available in:": "ఇందులో కూడా లభించును:",
"Archive": "అభిలేఖలు ",
+ "Atom feed": "అటామ్ ఫీడ్",
"Authors": "రచయితలు",
"Categories": "వర్గాలు",
"Comments": "వ్యాఖ్యలు",
- "LANGUAGE": "ఆంగ్లం ",
+ "LANGUAGE": "తెలుగు",
"Languages:": "భాషలు:",
"More posts about %s": "%s గూర్చి మరిన్ని టపాలు",
"Newer posts": "కొత్త టపాలు",
- "Next post": "తరువాత టపా",
- "No posts found.": "",
- "Nothing found.": "",
+ "Next post": "తరువాతి టపా",
+ "Next": "తరువాతి",
+ "No posts found.": "టపాలు ఏవీ కనుగొనబడలేదు.",
+ "Nothing found.": "ఏదీ కనుగొనబడలేదు.",
"Older posts": "పాత టపాలు",
"Original site": "వాస్తవ సైట్",
"Posted:": "ప్రచురుంచిన తేదీ:",
"Posts about %s": "%s గూర్చి టపాలు",
"Posts by %s": "%s యొక్క టపాలు",
"Posts for year %s": "%s సంవత్సర టపాలు",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "",
+ "Posts for {month_day_year}": "{month_day_year} యొక్క టపాలు",
+ "Posts for {month_year}": "{month_year} యొక్క టపాలు",
"Previous post": "మునుపటి టపా",
+ "Previous": "మునుపటి",
"Publication date": "ప్రచురణ తేదీ",
"RSS feed": "RSS ఫీడ్",
- "Read in English": "ఆంగ్లంలో చదవండి",
+ "Read in English": "తెలుగులో చదవండి",
"Read more": "ఇంకా చదవండి",
"Skip to main content": "ప్రధాన విషయానికి వెళ్ళు",
"Source": "మూలం",
"Subcategories:": "ఉపవర్గాలు:",
"Tags and Categories": "ట్యాగ్లు మరియు వర్గాలు",
"Tags": "ట్యాగ్లు",
- "Toggle navigation": "",
+ "Toggle navigation": "నావిగేషన్‌ను టోగుల్ చేయండి",
"Uncategorized": "వర్గీకరించని",
+ "Up": "పైకి",
"Updates": "నవీకరణలు",
"Write your page here.": "మీ పేజీ ఇక్కడ రాయండి.",
"Write your post here.": "ఇక్కడ మీ టపా ను వ్రాయండి.",
"old posts, page %d": "పాత టపాలు, పేజీ %d",
"page %d": "పేజీ %d",
+ "updated": "నవీకరించబడింది",
}
diff --git a/nikola/data/themes/base/messages/messages_th.py b/nikola/data/themes/base/messages/messages_th.py
new file mode 100644
index 0000000..a46f9ca
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_th.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "ใช้เวลาอ่านอีก %d นาที",
+ "(active)": "(ถูกใช้งาน)",
+ "Also available in:": "ภาษาอื่นๆ:",
+ "Archive": "คลังโพสต์",
+ "Atom feed": "ฟีด Atom",
+ "Authors": "ผู้เขียน",
+ "Categories": "หมวดหมู่",
+ "Comments": "ความคิดเห็น",
+ "LANGUAGE": "ภาษาไทย",
+ "Languages:": "ภาษา:",
+ "More posts about %s": "โพสต์เพิ่มเติมเกี่ยวกับ %s",
+ "Newer posts": "โพสต์มาใหม่",
+ "Next post": "โพสต์ถัดไป",
+ "Next": "ถัดไป",
+ "No posts found.": "ไม่พบโพสต์.",
+ "Nothing found.": "ไม่พบ",
+ "Older posts": "โพสต์เก่า",
+ "Original site": "เว็บไซต์ที่มา",
+ "Posted:": "โพสต์เมื่อ:",
+ "Posts about %s": "โพสต์เกี่ยวกับ %s",
+ "Posts by %s": "โพสต์โดย %s",
+ "Posts for year %s": "โพสต์เมื่อปี %s",
+ "Posts for {month_day_year}": "โพสต์เมื่อ {month_day_year}",
+ "Posts for {month_year}": "โพสต์เมื่อ {month_year}",
+ "Previous post": "โพสต์ก่อนหน้า",
+ "Previous": "ก่อนหน้า",
+ "Publication date": "วันที่ตีพิมพ์",
+ "RSS feed": "ฟีด RSS",
+ "Read in English": "อ่านเป็นภาษาไทย",
+ "Read more": "อ่านเพิ่มเติม",
+ "Skip to main content": "ข้ามไปหน้าหลัก",
+ "Source": "แหล่งที่มา",
+ "Subcategories:": "หมวดหมู่ย่อย:",
+ "Tags and Categories": "แท็กและหมวดหมู่",
+ "Tags": "แท็ก",
+ "Toggle navigation": "เมนูบาร์",
+ "Uncategorized": "ไม่มีหมวดหมู่",
+ "Up": "ขึ้นด้านบน",
+ "Updates": "อัพเดด",
+ "Write your page here.": "เขียนเพจที่นี่",
+ "Write your post here.": "เขียนโพสต์ที่นี่",
+ "old posts, page %d": "โพสต์เก่า, หน้า %d",
+ "page %d": "หน้า %d",
+ "updated": "",
+}
diff --git a/nikola/data/themes/base/messages/messages_tl.py b/nikola/data/themes/base/messages/messages_tl.py
deleted file mode 100644
index 1b85eb4..0000000
--- a/nikola/data/themes/base/messages/messages_tl.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
-
-MESSAGES = {
- "%d min remaining to read": "",
- "(active)": "",
- "Also available in:": "",
- "Archive": "",
- "Authors": "",
- "Categories": "",
- "Comments": "",
- "LANGUAGE": "Ingles",
- "Languages:": "Mga Wika:",
- "More posts about %s": "",
- "Newer posts": "",
- "Next post": "Susunod",
- "No posts found.": "",
- "Nothing found.": "",
- "Older posts": "",
- "Original site": "",
- "Posted:": "",
- "Posts about %s": "",
- "Posts by %s": "",
- "Posts for year %s": "",
- "Posts for {month} {day}, {year}": "",
- "Posts for {month} {year}": "",
- "Previous post": "",
- "Publication date": "",
- "RSS feed": "",
- "Read in English": "",
- "Read more": "",
- "Skip to main content": "",
- "Source": "",
- "Subcategories:": "",
- "Tags and Categories": "",
- "Tags": "Mga Tag",
- "Toggle navigation": "",
- "Uncategorized": "",
- "Updates": "",
- "Write your page here.": "",
- "Write your post here.": "",
- "old posts, page %d": "",
- "page %d": "",
-}
diff --git a/nikola/data/themes/base/messages/messages_tr.py b/nikola/data/themes/base/messages/messages_tr.py
index e09dcb8..8249f42 100644
--- a/nikola/data/themes/base/messages/messages_tr.py
+++ b/nikola/data/themes/base/messages/messages_tr.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d dakikalık okuma",
"(active)": "(etkin)",
"Also available in:": "Şu dilde de mevcut:",
"Archive": "Arşiv",
+ "Atom feed": "",
"Authors": "Yazarlar",
"Categories": "Kategoriler",
"Comments": "Yorumlar",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "%s hakkında diğer yazılar",
"Newer posts": "Daha yeni yazılar",
"Next post": "Sonraki yazı",
+ "Next": "",
"No posts found.": "Yazı bulunamadı.",
"Nothing found.": "Hiçbir şey bulunamadı.",
"Older posts": "Daha eski yazılar",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "%s ile ilgili yazılar",
"Posts by %s": "%s tarafından yazılanlar",
"Posts for year %s": "%s yılındaki yazılar",
- "Posts for {month} {day}, {year}": "{month} {day}, {year} tarihinden itibaren yazılar",
- "Posts for {month} {year}": "{month} {year} göre yazılar",
+ "Posts for {month_day_year}": "{month_day_year} tarihinden itibaren yazılar",
+ "Posts for {month_year}": "{month_year} göre yazılar",
"Previous post": "Önceki yazı",
+ "Previous": "",
"Publication date": "Yayınlanma tarihi",
"RSS feed": "RSS kaynağı",
"Read in English": "Türkçe olarak oku",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "Etiketler",
"Toggle navigation": "",
"Uncategorized": "Kategorisiz",
+ "Up": "",
"Updates": "Güncellemeler",
"Write your page here.": "Sayfanızı buraya yazın.",
"Write your post here.": "Yazınızı buraya yazın.",
"old posts, page %d": "eski yazılar, sayfa %d",
"page %d": "sayfa %d",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/messages/messages_uk.py b/nikola/data/themes/base/messages/messages_uk.py
index 327a5d9..2a6d84f 100644
--- a/nikola/data/themes/base/messages/messages_uk.py
+++ b/nikola/data/themes/base/messages/messages_uk.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "Залишилось читати %d хвилин",
"(active)": "(активне)",
"Also available in:": "Іншою мовою:",
"Archive": "Архів",
+ "Atom feed": "Стрічка у форматі Atom",
"Authors": "Автори",
"Categories": "Категорії",
"Comments": "Коментарі",
@@ -14,19 +15,21 @@ MESSAGES = {
"More posts about %s": "Більше статей про %s",
"Newer posts": "Новіші статті",
"Next post": "Наступна стаття",
+ "Next": "Вперед",
"No posts found.": "Не знайдено жодної статті",
"Nothing found.": "Нічого не знайдено",
- "Older posts": "Більш старі статті",
+ "Older posts": "Старіші статті",
"Original site": "Оригінал сайту",
"Posted:": "Опублікована:",
"Posts about %s": "Статті про %s",
"Posts by %s": "Статті %s",
"Posts for year %s": "Статті за %s рік",
- "Posts for {month} {day}, {year}": "Статті за {month} {day}, {year}",
- "Posts for {month} {year}": "Статті за {month} {year}",
+ "Posts for {month_day_year}": "Статті за {month_day_year}",
+ "Posts for {month_year}": "Статті за {month_year}",
"Previous post": "Попередня стаття",
+ "Previous": "Назад",
"Publication date": "Дата публікації",
- "RSS feed": "RSS-стрічка",
+ "RSS feed": "Стрічка у форматі RSS",
"Read in English": "Читати українською",
"Read more": "Читати далі",
"Skip to main content": "Перейти до основного матеріалу",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "Підкатегорії:",
"Tags and Categories": "Теги і категорії",
"Tags": "Теги",
- "Toggle navigation": "",
+ "Toggle navigation": "Перемкнути навігацію",
"Uncategorized": "Без категорії",
+ "Up": "Нагору",
"Updates": "Підписки",
"Write your page here.": "Напишіть Вашу сторінку тут.",
"Write your post here.": "Напишить Вашу статтю тут.",
"old posts, page %d": "старі статті, сторінка %d",
"page %d": "сторінка %d",
+ "updated": "оновлено",
}
diff --git a/nikola/data/themes/base/messages/messages_ur.py b/nikola/data/themes/base/messages/messages_ur.py
index 055f72e..2e6cd49 100644
--- a/nikola/data/themes/base/messages/messages_ur.py
+++ b/nikola/data/themes/base/messages/messages_ur.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "%d منٹ کا مطالعہ باقی",
"(active)": "(فعال)",
"Also available in:": "ان میں بھی دستیاب:",
"Archive": "آرکائیو",
+ "Atom feed": "ایٹم فِیڈ",
"Authors": "مصنفین",
"Categories": "زمرے",
"Comments": "تبصرے",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "%s کے بارے میں مزید تحاریر",
"Newer posts": "نئی تحاریر",
"Next post": "اگلی تحریر",
+ "Next": "آگے",
"No posts found.": "کوئی تحریر نہیں مل سکی۔",
"Nothing found.": "کچھ نہیں مل سکا۔",
"Older posts": "پرانی تحاریر",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "%s کے بارے میں تحاریر",
"Posts by %s": "%s کی تحاریر",
"Posts for year %s": "سال %s کی تحاریر",
- "Posts for {month} {day}, {year}": "{day} {month}، {year} کی تحاریر",
- "Posts for {month} {year}": "{month} {year} کی تحاریر",
+ "Posts for {month_day_year}": "{month_day_year} کی تحاریر",
+ "Posts for {month_year}": "{month_year} کی تحاریر",
"Previous post": "پچھلی تحریر",
+ "Previous": "پیچھے",
"Publication date": "تاریخِ اشاعت",
"RSS feed": "آر ایس ایس فیڈ",
"Read in English": "اردو میں پڑھیں",
@@ -34,11 +37,13 @@ MESSAGES = {
"Subcategories:": "ذیلی زمرے",
"Tags and Categories": "ٹیگز اور زمرے",
"Tags": "ٹیگز",
- "Toggle navigation": "",
+ "Toggle navigation": "نیویگیشن ہٹائیں/دکھائیں",
"Uncategorized": "بے زمرہ",
+ "Up": "اوپر",
"Updates": "تازہ ترین",
"Write your page here.": "اپنے صفحے کا متن یہاں لکھیں۔",
"Write your post here.": "اپنی تحریر یہاں لکھیں۔",
"old posts, page %d": "پرانی تحاریر صفحہ %d",
"page %d": "صفحہ %d",
+ "updated": "تازہ کاری",
}
diff --git a/nikola/data/themes/base/messages/messages_vi.py b/nikola/data/themes/base/messages/messages_vi.py
new file mode 100644
index 0000000..5ec8bbe
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_vi.py
@@ -0,0 +1,49 @@
+# -*- encoding:utf-8 -*-
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
+
+MESSAGES = {
+ "%d min remaining to read": "Cần %d phút để đọc",
+ "(active)": "(active)",
+ "Also available in:": "Cũng có sẵn trong:",
+ "Archive": "Kho",
+ "Atom feed": "Nguồn cung cấp dữ liệu Atom",
+ "Authors": "Tác giả",
+ "Categories": "Thể loại",
+ "Comments": "Bình luận",
+ "LANGUAGE": "Tiếng Việt",
+ "Languages:": "Ngôn ngữ:",
+ "More posts about %s": "Các bài đăng khác về %s",
+ "Newer posts": "Bài viết gần đây",
+ "Next post": "Bài viết tiếp theo",
+ "Next": "Kế tiếp",
+ "No posts found.": "Không tìm thấy bài viết.",
+ "Nothing found.": "Không có kết quả.",
+ "Older posts": "Bài viết trước đây",
+ "Original site": "Trang gốc",
+ "Posted:": "Đã đăng:",
+ "Posts about %s": "Bài viết về %s",
+ "Posts by %s": "Bài đăng bởi %s",
+ "Posts for year %s": "Các bài viết trong năm %s",
+ "Posts for {month_day_year}": "Các bài đã đăng {month_day_year}",
+ "Posts for {month_year}": "Các bài đã đăng {month_year}",
+ "Previous post": "Bài viết trước",
+ "Previous": "Trước",
+ "Publication date": "Ngày phát hành",
+ "RSS feed": "Nguồn cung cấp dữ liệu RSS",
+ "Read in English": "Phiên bản Tiếng Việt",
+ "Read more": "Đọc thêm",
+ "Skip to main content": "Chuyển đến nội dung chính",
+ "Source": "Nguồn",
+ "Subcategories:": "Thể loại con:",
+ "Tags and Categories": "Thẻ và Thể loại",
+ "Tags": "Thẻ",
+ "Toggle navigation": "Chuyển điều hướng",
+ "Uncategorized": "Chưa được phân loại",
+ "Up": "Trở lên",
+ "Updates": "Cập nhật",
+ "Write your page here.": "Bắt đầu viết nội dung của trang ở đây.",
+ "Write your post here.": "Bắt đầu viết bài ở đây.",
+ "old posts, page %d": "các bài viết trước đây, trang %d",
+ "page %d": "trang %d",
+ "updated": "Đã được cập nhật",
+}
diff --git a/nikola/data/themes/base/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py
index 84e4317..df8570b 100644
--- a/nikola/data/themes/base/messages/messages_zh_cn.py
+++ b/nikola/data/themes/base/messages/messages_zh_cn.py
@@ -1,44 +1,49 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
- "%d min remaining to read": "剩余 %d 分钟去阅读",
+ "%d min remaining to read": "剩余阅读时间 %d 分钟",
"(active)": "(活跃)",
- "Also available in:": "其他语言版本:",
- "Archive": "文章存档",
+ "Also available in:": "也可用于:",
+ "Archive": "文章归档",
+ "Atom feed": "Atom 源",
"Authors": "作者",
"Categories": "分类",
- "Comments": "注释",
+ "Comments": "评论",
"LANGUAGE": "简体中文",
"Languages:": "语言:",
- "More posts about %s": "更多 %s 相关文章",
- "Newer posts": "新一篇",
- "Next post": "后一篇",
+ "More posts about %s": "关于%s 的更多文章 ",
+ "Newer posts": "较新的文章",
+ "Next post": "下一篇文章",
+ "Next": "项下",
"No posts found.": "没有找到文章",
"Nothing found.": "没有找到。",
- "Older posts": "旧一篇",
+ "Older posts": "以前的文章",
"Original site": "原文地址",
"Posted:": "发表于:",
- "Posts about %s": "文章分类:%s",
- "Posts by %s": "%s 发布",
+ "Posts about %s": "关于文章 %s",
+ "Posts by %s": "文章有%s发布",
"Posts for year %s": "%s年文章",
- "Posts for {month} {day}, {year}": "{year}年{month}月{day}日文章",
- "Posts for {month} {year}": "{year}年{month}月文章",
- "Previous post": "前一篇",
+ "Posts for {month_day_year}": "{month_day_year}文章",
+ "Posts for {month_year}": "{month_year}文章",
+ "Previous post": "上一篇文章",
+ "Previous": "以前",
"Publication date": "发布日期",
"RSS feed": "RSS 源",
- "Read in English": "中文版",
- "Read more": "更多",
+ "Read in English": "阅读简体中文",
+ "Read more": "阅读更多",
"Skip to main content": "跳到主内容",
- "Source": "源代码",
+ "Source": "源文件",
"Subcategories:": "子类别:",
"Tags and Categories": "标签和分类",
"Tags": "标签",
- "Toggle navigation": "",
+ "Toggle navigation": "展开导航栏",
"Uncategorized": "未分类",
+ "Up": "向上",
"Updates": "更新",
"Write your page here.": "在这里书写你的页面。",
"Write your post here.": "在这里书写你的文章。",
"old posts, page %d": "旧文章页 %d",
"page %d": "第 %d 页",
+ "updated": "更新",
}
diff --git a/nikola/data/themes/base/messages/messages_zh_tw.py b/nikola/data/themes/base/messages/messages_zh_tw.py
index e6de0ff..ee4befd 100644
--- a/nikola/data/themes/base/messages/messages_zh_tw.py
+++ b/nikola/data/themes/base/messages/messages_zh_tw.py
@@ -1,11 +1,12 @@
# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
+"""Autogenerated file, do not edit. Submit translations on Transifex."""
MESSAGES = {
"%d min remaining to read": "尚餘 %d 分鐘",
"(active)": "(啟用)",
"Also available in:": "其他語言版本:",
"Archive": "彙整",
+ "Atom feed": "",
"Authors": "作者",
"Categories": "分類",
"Comments": "迴響",
@@ -14,6 +15,7 @@ MESSAGES = {
"More posts about %s": "更多 %s 的文章",
"Newer posts": "較新的文章",
"Next post": "下一篇",
+ "Next": "下一篇",
"No posts found.": "沒有找到文章。",
"Nothing found.": "沒有找到。",
"Older posts": "較舊的文章",
@@ -22,9 +24,10 @@ MESSAGES = {
"Posts about %s": "文章分類:%s",
"Posts by %s": "%s 發佈",
"Posts for year %s": "%s 年的文章",
- "Posts for {month} {day}, {year}": "{year}年{month}月{day}日的文章",
- "Posts for {month} {year}": "{year}年{month}月的文章",
+ "Posts for {month_day_year}": "{month_day_year}的文章",
+ "Posts for {month_year}": "{month_year}的文章",
"Previous post": "上一篇",
+ "Previous": "上一篇",
"Publication date": "發佈日期",
"RSS feed": "RSS 訂閱",
"Read in English": "繁體中文版",
@@ -36,9 +39,11 @@ MESSAGES = {
"Tags": "標籤",
"Toggle navigation": "切換導航",
"Uncategorized": "未分類",
+ "Up": "向上",
"Updates": "更新",
"Write your page here.": "從這裡開始編輯頁面",
"Write your post here.": "從這裡開始編輯文章",
"old posts, page %d": "舊文章,第 %d 頁",
"page %d": "第 %d 頁",
+ "updated": "",
}
diff --git a/nikola/data/themes/base/templates/archive.tmpl b/nikola/data/themes/base/templates/archive.tmpl
new file mode 100644
index 0000000..d6f107c
--- /dev/null
+++ b/nikola/data/themes/base/templates/archive.tmpl
@@ -0,0 +1 @@
+<%inherit file="list_post.tmpl"/>
diff --git a/nikola/data/themes/base/templates/archive_navigation_helper.tmpl b/nikola/data/themes/base/templates/archive_navigation_helper.tmpl
new file mode 100644
index 0000000..506629f
--- /dev/null
+++ b/nikola/data/themes/base/templates/archive_navigation_helper.tmpl
@@ -0,0 +1,27 @@
+## -*- coding: utf-8 -*-
+
+<%def name="archive_navigation()">
+%if 'archive_page' in pagekind:
+ %if has_archive_navigation:
+ <nav class="archivenav">
+ <ul class="pager">
+ %if previous_archive:
+ <li class="previous"><a href="${previous_archive}" rel="prev">${messages("Previous")}</a></li>
+ %else:
+ <li class="previous disabled"><a href="#" rel="prev">${messages("Previous")}</a></li>
+ % endif
+ %if up_archive:
+ <li class="up"><a href="${up_archive}" rel="up">${messages("Up")}</a></li>
+ %else:
+ <li class="up disabled"><a href="#" rel="up">${messages("Up")}</a></li>
+ %endif
+ %if next_archive:
+ <li class="next"><a href="${next_archive}" rel="next">${messages("Next")}</a></li>
+ %else:
+ <li class="next disabled"><a href="#" rel="next">${messages("Next")}</a></li>
+ %endif
+ </ul>
+ </nav>
+ %endif
+% endif
+</%def>
diff --git a/nikola/data/themes/base/templates/archiveindex.tmpl b/nikola/data/themes/base/templates/archiveindex.tmpl
index 8c58f13..eb4dd27 100644
--- a/nikola/data/themes/base/templates/archiveindex.tmpl
+++ b/nikola/data/themes/base/templates/archiveindex.tmpl
@@ -1,13 +1,20 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>
+<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%block name="extra_head">
${parent.extra_head()}
- %if len(translations) > 1 and generate_atom:
- %for language in sorted(translations):
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${archive_name} section (${language})" href="${_link("archive_atom", archive_name, language)}">
- %endfor
- %elif generate_atom:
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${archive_name} archive" href="${_link("archive_atom", archive_name)}">
- %endif
+ ${feeds_translations.head(archive_name, kind, rss_override=False)}
+</%block>
+
+<%block name="content_header">
+ <header>
+ <h1>${title|h}</h1>
+ ${archive_nav.archive_navigation()}
+ <div class="metadata">
+ ${feeds_translations.feed_link(archive, kind)}
+ ${feeds_translations.translation_link(kind)}
+ </div>
+ </header>
</%block>
diff --git a/nikola/data/themes/base/templates/author.tmpl b/nikola/data/themes/base/templates/author.tmpl
index 21d8d64..983f118 100644
--- a/nikola/data/themes/base/templates/author.tmpl
+++ b/nikola/data/themes/base/templates/author.tmpl
@@ -1,43 +1,28 @@
## -*- coding: utf-8 -*-
<%inherit file="list_post.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%block name="extra_head">
- ${parent.extra_head()}
- %if len(translations) > 1 and generate_rss:
- %for language in sorted(translations):
- <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${author|h} (${language})" href="${_link(kind + "_rss", author, language)}">
- %endfor
- %elif generate_rss:
- <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${author|h}" href="${_link(kind + "_rss", author)}">
- %endif
+ ${feeds_translations.head(author, kind, rss_override=False)}
</%block>
-
<%block name="content">
<article class="authorpage">
<header>
<h1>${title|h}</h1>
%if description:
- <p>${description}</p>
+ <p>${description}</p>
%endif
<div class="metadata">
- %if len(translations) > 1 and generate_rss:
- %for language in sorted(translations):
- <p class="feedlink">
- <a href="${_link(kind + "_rss", author, language)}" hreflang="${language}" type="application/rss+xml">${messages('RSS feed', language)} (${language})</a>&nbsp;
- </p>
- %endfor
- %elif generate_rss:
- <p class="feedlink"><a href="${_link(kind + "_rss", author)}" type="application/rss+xml">${messages('RSS feed')}</a></p>
- %endif
+ ${feeds_translations.feed_link(author, kind)}
</div>
</header>
%if posts:
- <ul class="postlist">
- % for post in posts:
- <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}</a></li>
- % endfor
- </ul>
+ <ul class="postlist">
+ % for post in posts:
+ <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}</a></li>
+ % endfor
+ </ul>
%endif
</article>
</%block>
diff --git a/nikola/data/themes/base/templates/authorindex.tmpl b/nikola/data/themes/base/templates/authorindex.tmpl
index 34cb20b..fe9d39e 100644
--- a/nikola/data/themes/base/templates/authorindex.tmpl
+++ b/nikola/data/themes/base/templates/authorindex.tmpl
@@ -1,13 +1,21 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="content_header">
+ <header>
+ <h1>${title|h}</h1>
+ %if description:
+ <p>${description}</p>
+ %endif
+ <div class="metadata">
+ ${feeds_translations.feed_link(author, kind)}
+ ${feeds_translations.translation_link(kind)}
+ </div>
+ </header>
+</%block>
<%block name="extra_head">
${parent.extra_head()}
- %if len(tranlations) > 1 and generate_atom:
- %for language in sorted(translations):
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${author|h} section (${language})" href="${_link(kind + "_atom", author, language)}">
- %endfor
- %elif generate_atom:
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${author|h} section" href="${_link("author" + "_atom", author)}">
- %endif
+ ${feeds_translations.head(author, kind, rss_override=False)}
</%block>
diff --git a/nikola/data/themes/base/templates/authors.tmpl b/nikola/data/themes/base/templates/authors.tmpl
index 141c560..8503bd4 100644
--- a/nikola/data/themes/base/templates/authors.tmpl
+++ b/nikola/data/themes/base/templates/authors.tmpl
@@ -1,10 +1,18 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="extra_head">
+ ${feeds_translations.head(kind=kind, feeds=False)}
+</%block>
<%block name="content">
<article class="authorindex">
%if items:
<h2>${messages("Authors")}</h2>
+ <div class="metadata">
+ ${feeds_translations.translation_link(kind)}
+ </div>
<ul class="postlist">
% for text, link in items:
% if text not in hidden_authors:
diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl
index 2b0cbfd..f071c95 100644
--- a/nikola/data/themes/base/templates/base.tmpl
+++ b/nikola/data/themes/base/templates/base.tmpl
@@ -2,8 +2,8 @@
<%namespace name="base" file="base_helper.tmpl" import="*"/>
<%namespace name="header" file="base_header.tmpl" import="*"/>
<%namespace name="footer" file="base_footer.tmpl" import="*"/>
-<%namespace name="annotations" file="annotation_helper.tmpl"/>
${set_locale(lang)}
+### <html> tag is included by base.html_headstart
${base.html_headstart()}
<%block name="extra_head">
### Leave this block alone.
@@ -11,16 +11,29 @@ ${base.html_headstart()}
${template_hooks['extra_head']()}
</head>
<body>
-<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
+ <a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
<div id="container">
- ${header.html_header()}
- <main id="content">
+ ${header.html_header()}
+ <main id="content">
<%block name="content"></%block>
- </main>
- ${footer.html_footer()}
+ </main>
+ ${footer.html_footer()}
</div>
${base.late_load_js()}
+ % if date_fanciness != 0:
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "${luxon_locales[lang]}";
+ fancydates(${date_fanciness}, ${luxon_date_format});
+ </script>
+ <!-- end fancy dates -->
+ % endif
<%block name="extra_js"></%block>
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
${body_end}
${template_hooks['body_end']()}
</body>
diff --git a/nikola/data/themes/base/templates/base_footer.tmpl b/nikola/data/themes/base/templates/base_footer.tmpl
index cd41d37..7e44c75 100644
--- a/nikola/data/themes/base/templates/base_footer.tmpl
+++ b/nikola/data/themes/base/templates/base_footer.tmpl
@@ -1,5 +1,4 @@
## -*- coding: utf-8 -*-
-<%namespace name="base" file="base_helper.tmpl" import="*"/>
<%def name="html_footer()">
%if content_footer:
diff --git a/nikola/data/themes/base/templates/base_header.tmpl b/nikola/data/themes/base/templates/base_header.tmpl
index 2ffcfee..b45744a 100644
--- a/nikola/data/themes/base/templates/base_header.tmpl
+++ b/nikola/data/themes/base/templates/base_header.tmpl
@@ -16,7 +16,7 @@
</%def>
<%def name="html_site_title()">
- <h1 id="brand"><a href="${abs_link(_link("root", None, lang))}" title="${blog_title|h}" rel="home">
+ <h1 id="brand"><a href="${_link("root", None, lang)}" title="${blog_title|h}" rel="home">
%if logo_url:
<img src="${logo_url}" alt="${blog_title|h}" id="logo">
%endif
@@ -30,13 +30,22 @@
<%def name="html_navigation_links()">
<nav id="menu">
<ul>
- %for url, text in navigation_links[lang]:
+ ${html_navigation_links_entries(navigation_links)}
+ ${html_navigation_links_entries(navigation_alt_links)}
+ ${template_hooks['menu']()}
+ ${template_hooks['menu_alt']()}
+ </ul>
+ </nav>
+</%def>
+
+<%def name="html_navigation_links_entries(navigation_links_source)">
+ %for url, text in navigation_links_source[lang]:
% if isinstance(url, tuple):
<li> ${text}
<ul>
%for suburl, text in url:
% if rel_link(permalink, suburl) == "#":
- <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a></li>
+ <li class="active"><a href="${permalink}">${text}<span class="sr-only"> ${messages("(active)", lang)}</span></a></li>
%else:
<li><a href="${suburl}">${text}</a></li>
%endif
@@ -44,16 +53,12 @@
</ul>
% else:
% if rel_link(permalink, url) == "#":
- <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a></li>
+ <li class="active"><a href="${permalink}">${text}<span class="sr-only"> ${messages("(active)", lang)}</span></a></li>
%else:
<li><a href="${url}">${text}</a></li>
%endif
% endif
%endfor
- ${template_hooks['menu']()}
- ${template_hooks['menu_alt']()}
- </ul>
- </nav>
</%def>
<%def name="html_translation_header()">
diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl
index e2ffab2..18801ed 100644
--- a/nikola/data/themes/base/templates/base_helper.tmpl
+++ b/nikola/data/themes/base/templates/base_helper.tmpl
@@ -1,31 +1,25 @@
## -*- coding: utf-8 -*-
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%def name="html_headstart()">
<!DOCTYPE html>
<html \
-prefix='\
-%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']):
-og: http://ogp.me/ns# article: http://ogp.me/ns/article# \
-%endif
-%if comment_system == 'facebook':
-fb: http://ogp.me/ns/fb#
-%endif
-' \
-%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']):
-vocab="http://ogp.me/ns" \
-%endif
+ prefix='\
+ og: http://ogp.me/ns# article: http://ogp.me/ns/article# \
+ %if comment_system == 'facebook':
+ fb: http://ogp.me/ns/fb# \
+ %endif
+ ' \
+ vocab="http://ogp.me/ns" \
% if is_rtl:
-dir="rtl" \
+ dir="rtl" \
% endif
\
lang="${lang}">
<head>
<meta charset="utf-8">
- % if use_base_tag:
- <base href="${abs_link(permalink)}">
- % endif
%if description:
- <meta name="description" content="${description|h}">
+ <meta name="description" content="${description|h}">
%endif
<meta name="viewport" content="width=device-width">
%if title == blog_title:
@@ -35,8 +29,11 @@ lang="${lang}">
%endif
${html_stylesheets()}
- <meta content="${theme_color}" name="theme-color">
- ${html_feedlinks()}
+ <meta name="theme-color" content="${theme_color}">
+ % if meta_generator_tag:
+ <meta name="generator" content="Nikola (getnikola.com)">
+ % endif
+ ${feeds_translations.head(classification=None, kind='index', other=False)}
<link rel="canonical" href="${abs_link(permalink)}">
%if favicons:
@@ -56,29 +53,58 @@ lang="${lang}">
<link rel="next" href="${nextlink}" type="text/html">
%endif
- ${mathjax_config}
%if use_cdn:
- <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ <!--[if lt IE 9]><script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script><![endif]-->
%else:
- <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang)}"></script><![endif]-->
+ <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5shiv-printshiv.min.js', lang, url_type)}"></script><![endif]-->
%endif
${extra_head_data}
</%def>
<%def name="late_load_js()">
+ % if use_bundles:
+ % if use_cdn:
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ <script src="/assets/js/all.js"></script>
+ % else:
+ <script src="/assets/js/all-nocdn.js"></script>
+ % endif
+ % else:
+ % if use_cdn:
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ % else:
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ % endif
+ % endif
+ % if date_fanciness != 0:
+ % if date_fanciness == 2:
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script>
+ % endif
+ % if use_cdn:
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ % else:
+ <script src="/assets/js/luxon.min.js"></script>
+ % endif
+ % if not use_bundles:
+ <script src="/assets/js/fancydates.min.js"></script>
+ % endif
+ % endif
${social_buttons_code}
</%def>
<%def name="html_stylesheets()">
%if use_bundles:
%if use_cdn:
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
<link href="/assets/css/all.css" rel="stylesheet" type="text/css">
%else:
<link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
%endif
%else:
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/rst_base.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_rst.css" rel="stylesheet" type="text/css">
<link href="/assets/css/code.css" rel="stylesheet" type="text/css">
<link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
%if has_custom_css:
@@ -91,34 +117,16 @@ lang="${lang}">
% endif
</%def>
+### This function is deprecated; use feed_helper directly.
<%def name="html_feedlinks()">
- %if rss_link:
- ${rss_link}
- %elif generate_rss:
- %if len(translations) > 1:
- %for language in sorted(translations):
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
- %endfor
- %else:
- <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
- %endif
- %endif
- %if generate_atom:
- %if len(translations) > 1:
- %for language in sorted(translations):
- <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
- %endfor
- %else:
- <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
- %endif
- %endif
+ ${feeds_translations.head(classification=None, kind='index', other=False)}
</%def>
<%def name="html_translations()">
<ul class="translations">
%for langname in sorted(translations):
%if langname != lang:
- <li><a href="${abs_link(_link("root", None, langname))}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
+ <li><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
%endif
%endfor
</ul>
diff --git a/nikola/data/themes/base/templates/comments_helper.tmpl b/nikola/data/themes/base/templates/comments_helper.tmpl
index 1459888..002499e 100644
--- a/nikola/data/themes/base/templates/comments_helper.tmpl
+++ b/nikola/data/themes/base/templates/comments_helper.tmpl
@@ -1,63 +1,63 @@
## -*- coding: utf-8 -*-
<%namespace name="disqus" file="comments_helper_disqus.tmpl"/>
-<%namespace name="livefyre" file="comments_helper_livefyre.tmpl"/>
<%namespace name="intensedebate" file="comments_helper_intensedebate.tmpl"/>
<%namespace name="muut" file="comments_helper_muut.tmpl"/>
-<%namespace name="googleplus" file="comments_helper_googleplus.tmpl"/>
<%namespace name="facebook" file="comments_helper_facebook.tmpl"/>
<%namespace name="isso" file="comments_helper_isso.tmpl"/>
+<%namespace name="commento" file="comments_helper_commento.tmpl"/>
+<%namespace name="utterances" file="comments_helper_utterances.tmpl"/>
<%def name="comment_form(url, title, identifier)">
%if comment_system == 'disqus':
${disqus.comment_form(url, title, identifier)}
- % elif comment_system == 'livefyre':
- ${livefyre.comment_form(url, title, identifier)}
% elif comment_system == 'intensedebate':
${intensedebate.comment_form(url, title, identifier)}
% elif comment_system == 'muut':
${muut.comment_form(url, title, identifier)}
- % elif comment_system == 'googleplus':
- ${googleplus.comment_form(url, title, identifier)}
% elif comment_system == 'facebook':
${facebook.comment_form(url, title, identifier)}
% elif comment_system == 'isso':
${isso.comment_form(url, title, identifier)}
+ % elif comment_system == 'commento':
+ ${commento.comment_form(url, title, identifier)}
+ % elif comment_system == 'utterances':
+ ${utterances.comment_form(url, title, identifier)}
%endif
</%def>
<%def name="comment_link(link, identifier)">
%if comment_system == 'disqus':
${disqus.comment_link(link, identifier)}
- % elif comment_system == 'livefyre':
- ${livefyre.comment_link(link, identifier)}
% elif comment_system == 'intensedebate':
${intensedebate.comment_link(link, identifier)}
% elif comment_system == 'muut':
${muut.comment_link(link, identifier)}
- % elif comment_system == 'googleplus':
- ${googleplus.comment_link(link, identifier)}
% elif comment_system == 'facebook':
${facebook.comment_link(link, identifier)}
% elif comment_system == 'isso':
${isso.comment_link(link, identifier)}
+ % elif comment_system == 'commento':
+ ${commento.comment_link(link, identifier)}
+ % elif comment_system == 'utterances':
+ ${utterances.comment_link(link, identifier)}
%endif
</%def>
<%def name="comment_link_script()">
%if comment_system == 'disqus':
${disqus.comment_link_script()}
- % elif comment_system == 'livefyre':
- ${livefyre.comment_link_script()}
% elif comment_system == 'intensedebate':
${intensedebate.comment_link_script()}
% elif comment_system == 'muut':
${muut.comment_link_script()}
- % elif comment_system == 'googleplus':
- ${googleplus.comment_link_script()}
% elif comment_system == 'facebook':
${facebook.comment_link_script()}
% elif comment_system == 'isso':
${isso.comment_link_script()}
+ % elif comment_system == 'commento':
+ ${commento.comment_link_script()}
+ % elif comment_system == 'utterances':
+ ${utterances.comment_link_script()}
%endif
</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_commento.tmpl b/nikola/data/themes/base/templates/comments_helper_commento.tmpl
new file mode 100644
index 0000000..793a6d4
--- /dev/null
+++ b/nikola/data/themes/base/templates/comments_helper_commento.tmpl
@@ -0,0 +1,13 @@
+## -*- coding: utf-8 -*-
+<%def name="comment_form(url, title, identifier)">
+ <div id="commento"></div>
+
+ <script defer src="${comment_system_id}/js/commento.js"></script>
+</%def>
+
+<%def name="comment_link(link, identifier)">
+ <a href="${link}#commento">${messages("Comments")}</a>
+</%def>
+
+<%def name="comment_link_script()">
+</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_disqus.tmpl b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
index b842871..f17777c 100644
--- a/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
+++ b/nikola/data/themes/base/templates/comments_helper_disqus.tmpl
@@ -32,7 +32,7 @@
<%def name="comment_link(link, identifier)">
%if comment_system_id:
- <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">Comments</a>
+ <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">${messages("Comments")}</a>
%endif
</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl b/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl
deleted file mode 100644
index 5a5c4d7..0000000
--- a/nikola/data/themes/base/templates/comments_helper_googleplus.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="comment_form(url, title, identifier)">
-<script src="https://apis.google.com/js/plusone.js"></script>
-<div class="g-comments"
- data-href="${url}"
- data-first_party_property="BLOGGER"
- data-view_type="FILTERED_POSTMOD">
-</div>
-</%def>
-
-<%def name="comment_link(link, identifier)">
-<div class="g-commentcount" data-href="${link}"></div>
-<script src="https://apis.google.com/js/plusone.js"></script>
-</%def>
-
-<%def name="comment_link_script()">
-</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl
index c47b6c7..0a51328 100644
--- a/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl
+++ b/nikola/data/themes/base/templates/comments_helper_intensedebate.tmpl
@@ -6,7 +6,7 @@ var idcomments_post_id = "${identifier}";
var idcomments_post_url = "${url}";
</script>
<span id="IDCommentsPostTitle" style="display:none"></span>
-<script src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script>
+<script src="https://www.intensedebate.com/js/genericCommentWrapperV2.js"></script>
</script>
</%def>
@@ -17,7 +17,7 @@ var idcomments_acct = '${comment_system_id}';
var idcomments_post_id = "${identifier}";
var idcomments_post_url = "${link}";
</script>
-<script src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script>
+<script src="https://www.intensedebate.com/js/genericLinkWrapperV2.js"></script>
</a>
</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_isso.tmpl b/nikola/data/themes/base/templates/comments_helper_isso.tmpl
index 95f93ae..c733bbe 100644
--- a/nikola/data/themes/base/templates/comments_helper_isso.tmpl
+++ b/nikola/data/themes/base/templates/comments_helper_isso.tmpl
@@ -1,20 +1,26 @@
## -*- coding: utf-8 -*-
<%def name="comment_form(url, title, identifier)">
%if comment_system_id:
- <div data-title="${title|u}" id="isso-thread"></div>
- <script src="${comment_system_id}js/embed.min.js" data-isso="${comment_system_id}"></script>
+ <div data-title="${title|h}" id="isso-thread"></div>
+ <script src="${comment_system_id}js/embed.min.js" data-isso="${comment_system_id}" data-isso-lang="${lang}"
+ % if isso_config:
+ % for k, v in isso_config.items():
+ data-isso-${k}="${v}"
+ % endfor
+ % endif
+ ></script>
%endif
</%def>
<%def name="comment_link(link, identifier)">
%if comment_system_id:
- <a href="${link}#isso-thread">Comments</a>
+ <a href="${link}#isso-thread">${messages("Comments")}</a>
%endif
</%def>
<%def name="comment_link_script()">
%if comment_system_id and 'index' in pagekind:
- <script src="${comment_system_id}js/count.min.js" data-isso="${comment_system_id}"></script>
+ <script src="${comment_system_id}js/count.min.js" data-isso="${comment_system_id}" data-isso-lang="${lang}"></script>
%endif
</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl b/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl
deleted file mode 100644
index 68d99e5..0000000
--- a/nikola/data/themes/base/templates/comments_helper_livefyre.tmpl
+++ /dev/null
@@ -1,33 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="comment_form(url, title, identifier)">
-<div id="livefyre-comments"></div>
-<script src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script>
-<script>
-(function () {
- var articleId = "${identifier}";
- fyre.conv.load({}, [{
- el: 'livefyre-comments',
- network: "livefyre.com",
- siteId: "${comment_system_id}",
- articleId: articleId,
- signed: false,
- collectionMeta: {
- articleId: articleId,
- url: fyre.conv.load.makeCollectionUrl(),
- }
- }], function() {});
-}());
-</script>
-</%def>
-
-<%def name="comment_link(link, identifier)">
- <a href="${link}">
- <span class="livefyre-commentcount" data-lf-site-id="${comment_system_id}" data-lf-article-id="${identifier}">
- 0 Comments
- </span>
-</%def>
-
-
-<%def name="comment_link_script()">
-<script src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js"></script>
-</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper_mustache.tmpl b/nikola/data/themes/base/templates/comments_helper_mustache.tmpl
deleted file mode 100644
index 593d0aa..0000000
--- a/nikola/data/themes/base/templates/comments_helper_mustache.tmpl
+++ /dev/null
@@ -1,5 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="comments" file="comments_helper.tmpl"/>
-% if not post.meta('nocomments'):
- ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)}
-% endif
diff --git a/nikola/data/themes/base/templates/comments_helper_utterances.tmpl b/nikola/data/themes/base/templates/comments_helper_utterances.tmpl
new file mode 100644
index 0000000..9b68917
--- /dev/null
+++ b/nikola/data/themes/base/templates/comments_helper_utterances.tmpl
@@ -0,0 +1,23 @@
+## -*- coding: utf-8 -*-
+<%def name="comment_form(url, title, identifier)">
+ %if comment_system_id:
+ <div data-title="${title|h}" id="utterances-thread"></div>
+ <script src="https://utteranc.es/client.js" repo="${comment_system_id}"
+ % if utterances_config:
+ % for k, v in utterances_config.items():
+ ${k}="${v}"
+ % endfor
+ % endif
+ ></script>
+ %endif
+</%def>
+
+<%def name="comment_link(link, identifier)">
+ %if comment_system_id:
+ <a href="${link}#utterances-thread">${messages("Comments")}</a>
+ %endif
+</%def>
+
+
+<%def name="comment_link_script()">
+</%def>
diff --git a/nikola/data/themes/base/templates/feeds_translations_helper.tmpl b/nikola/data/themes/base/templates/feeds_translations_helper.tmpl
new file mode 100644
index 0000000..10e704d
--- /dev/null
+++ b/nikola/data/themes/base/templates/feeds_translations_helper.tmpl
@@ -0,0 +1,124 @@
+## -*- coding: utf-8 -*-
+
+<%def name="_head_feed_link(link_type, link_name, link_postfix, classification, kind, language)">
+ % if len(translations) > 1:
+ <link rel="alternate" type="${link_type}" title="${link_name|h} (${language})" hreflang="${language}" href="${_link(kind + '_' + link_postfix, classification, language)}">
+ % else:
+ <link rel="alternate" type="${link_type}" title="${link_name|h}" hreflang="${language}" href="${_link(kind + '_' + link_postfix, classification, language)}">
+ % endif
+</%def>
+
+<%def name="_html_feed_link(link_type, link_name, link_postfix, classification, kind, language, name=None)">
+ % if len(translations) > 1:
+ % if name and kind != "archive" and kind != "author":
+ <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${name|h}, ${language})</a>
+ % else:
+ <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${language})</a>
+ % endif
+ % else:
+ % if name and kind != "archive" and kind != "author":
+ <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)} (${name|h})</a>
+ % else:
+ <a href="${_link(kind + '_' + link_postfix, classification, language)}" hreflang="${language}" type="${link_type}">${messages(link_name, language)}</a>
+ % endif
+ % endif
+</%def>
+
+<%def name="_html_translation_link(classification, kind, language, name=None)">
+ % if name and kind != "archive" and kind != "author":
+ <a href="${_link(kind, classification, language)}" hreflang="${language}" rel="alternate">${messages("LANGUAGE", language)} (${name|h})</a>
+ % else:
+ <a href="${_link(kind, classification, language)}" hreflang="${language}" rel="alternate">${messages("LANGUAGE", language)}</a>
+ % endif
+</%def>
+
+<%def name="_head_rss(classification=None, kind='index', rss_override=True)">
+ % if rss_link and rss_override:
+ ${rss_link}
+ % endif
+ % if generate_rss and not (rss_link and rss_override) and kind != 'archive':
+ % if len(translations) > 1 and has_other_languages and classification and kind != 'index':
+ % for language, classification, name in all_languages:
+ <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${name|h} (${language})" hreflang="${language}" href="${_link(kind + "_rss", classification, language)}">
+ % endfor
+ % else:
+ % for language in translations_feedorder:
+ % if (classification or classification == '') and kind != 'index':
+ ${_head_feed_link('application/rss+xml', 'RSS for ' + kind + ' ' + classification, 'rss', classification, kind, language)}
+ % else:
+ ${_head_feed_link('application/rss+xml', 'RSS', 'rss', classification, 'index', language)}
+ % endif
+ % endfor
+ % endif
+ % endif
+</%def>
+
+<%def name="_head_atom(classification=None, kind='index')">
+ % if generate_atom:
+ % if len(translations) > 1 and has_other_languages and classification and kind != 'index':
+ % for language, classification, name in all_languages:
+ <link rel="alternate" type="application/atom+xml" title="Atom for ${kind} ${name|h} (${language})" hreflang="${language}" href="${_link(kind + "_atom", classification, language)}">
+ % endfor
+ % else:
+ % for language in translations_feedorder:
+ % if (classification or classification == '') and kind != 'index':
+ ${_head_feed_link('application/atom+xml', 'Atom for ' + kind + ' ' + classification, 'atom', classification, kind, language)}
+ % else:
+ ${_head_feed_link('application/atom+xml', 'Atom', 'atom', classification, 'index', language)}
+ % endif
+ % endfor
+ % endif
+ % endif
+</%def>
+
+## Handles both feeds and translations
+<%def name="head(classification=None, kind='index', feeds=True, other=True, rss_override=True, has_no_feeds=False)">
+ % if feeds and not has_no_feeds:
+ ${_head_rss(classification, 'index' if (kind == 'archive' and rss_override) else kind, rss_override)}
+ ${_head_atom(classification, kind)}
+ % endif
+ % if other and has_other_languages and other_languages:
+ % for language, classification, _ in other_languages:
+ <link rel="alternate" hreflang="${language}" href="${_link(kind, classification, language)}">
+ % endfor
+ % endif
+</%def>
+
+<%def name="feed_link(classification, kind)">
+ % if generate_atom or generate_rss:
+ % if len(translations) > 1 and has_other_languages and kind != 'index':
+ % for language, classification, name in all_languages:
+ <p class="feedlink">
+ % if generate_atom:
+ ${_html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language, name)}
+ % endif
+ % if generate_rss and kind != 'archive':
+ ${_html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language, name)}
+ % endif
+ </p>
+ % endfor
+ % else:
+ % for language in translations_feedorder:
+ <p class="feedlink">
+ % if generate_atom:
+ ${_html_feed_link('application/atom+xml', 'Atom feed', 'atom', classification, kind, language)}
+ % endif
+ % if generate_rss and kind != 'archive':
+ ${_html_feed_link('application/rss+xml', 'RSS feed', 'rss', classification, kind, language)}
+ % endif
+ </p>
+ % endfor
+ % endif
+ % endif
+</%def>
+
+<%def name="translation_link(kind)">
+ % if has_other_languages and other_languages:
+ <div class="translationslist translations">
+ <h3 class="translationslist-intro">${messages("Also available in:")}</h3>
+ % for language, classification, name in other_languages:
+ <p>${_html_translation_link(classification, kind, language, name)}</p>
+ % endfor
+ </div>
+ % endif
+</%def>
diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl
index f9bbd1b..fef3a86 100644
--- a/nikola/data/themes/base/templates/gallery.tmpl
+++ b/nikola/data/themes/base/templates/gallery.tmpl
@@ -1,11 +1,12 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
<%namespace name="comments" file="comments_helper.tmpl"/>
-<%namespace name="ui" file="crumbs.tmpl" import="bar"/>
+<%namespace name="ui" file="ui_helper.tmpl"/>
+<%namespace name="post_helper" file="post_helper.tmpl"/>
<%block name="sourcelink"></%block>
<%block name="content">
- ${ui.bar(crumbs)}
+ ${ui.breadcrumbs(crumbs)}
%if title:
<h1>${title|h}</h1>
%endif
@@ -15,21 +16,39 @@
</p>
%endif
%if folders:
- <ul>
- % for folder, ftitle in folders:
- <li><a href="${folder}"><i
- class="icon-folder-open"></i>&nbsp;${ftitle|h}</a></li>
- % endfor
- </ul>
- %endif
- %if photo_array:
- <ul class="thumbnails">
- %for image in photo_array:
- <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}">
- <img src="${image['url_thumb']}" alt="${image['title']|h}" /></a>
- %endfor
- </ul>
- %endif
+ % if galleries_use_thumbnail:
+ % for (folder, ftitle, fpost) in folders:
+ <div class="thumnbnail-container">
+ <a href="${folder}" class="thumbnail image-reference" title="${ftitle|h}">
+ % if fpost and fpost.previewimage:
+ <img src="${fpost.previewimage}" alt="${ftitle|h}" loading="lazy" style="max-width:${thumbnail_size}px; max-height:${thumbnail_size}px;" />
+ % else:
+ <div style="height: ${thumbnail_size}px; width: ${thumbnail_size}px; background-color: #eee;"></div>
+ % endif
+ <p class="thumbnail-caption">${ftitle|h}</p>
+ </a>
+ </div>
+ % endfor
+ % else:
+ <ul>
+ % for folder, ftitle in folders:
+ <li><a href="${folder}">📂&nbsp;${ftitle|h}</a></li>
+ % endfor
+ </ul>
+ % endif
+ % endif
+
+<div id="gallery_container"></div>
+%if photo_array:
+<noscript>
+<ul class="thumbnails">
+ %for image in photo_array:
+ <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']|h}">
+ <img src="${image['url_thumb']}" alt="${image['title']|h}" loading="lazy" /></a>
+ %endfor
+</ul>
+</noscript>
+%endif
%if site_has_comments and enable_comments:
${comments.comment_form(None, permalink, title)}
%endif
@@ -38,4 +57,35 @@
<%block name="extra_head">
${parent.extra_head()}
<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
+<style type="text/css">
+ #gallery_container {
+ position: relative;
+ }
+ .image-block {
+ position: absolute;
+ }
+</style>
+%if len(translations) > 1:
+ %for langname in translations.keys():
+ %if langname != lang:
+ <link rel="alternate" hreflang="${langname}" href="${_link('gallery', gallery_path, langname)}">
+ %endif
+ %endfor
+%endif
+<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
+%if post:
+ ${post_helper.open_graph_metadata(post)}
+ ${post_helper.twitter_card_information(post)}
+%endif
+</%block>
+
+<%block name="extra_js">
+<script src="/assets/js/justified-layout.min.js"></script>
+<script src="/assets/js/gallery.min.js"></script>
+<script>
+var jsonContent = ${photo_array_json};
+var thumbnailSize = ${thumbnail_size};
+renderGallery(jsonContent, thumbnailSize);
+window.addEventListener('resize', function(){renderGallery(jsonContent, thumbnailSize)});
+</script>
</%block>
diff --git a/nikola/data/themes/base/templates/index.tmpl b/nikola/data/themes/base/templates/index.tmpl
index f74d2e4..b8e4f8c 100644
--- a/nikola/data/themes/base/templates/index.tmpl
+++ b/nikola/data/themes/base/templates/index.tmpl
@@ -1,6 +1,9 @@
## -*- coding: utf-8 -*-
<%namespace name="helper" file="index_helper.tmpl"/>
+<%namespace name="math" file="math_helper.tmpl"/>
<%namespace name="comments" file="comments_helper.tmpl"/>
+<%namespace name="pagination" file="pagination_helper.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%inherit file="base.tmpl"/>
<%block name="extra_head">
@@ -8,27 +11,45 @@
% if posts and (permalink == '/' or permalink == '/' + index_file):
<link rel="prefetch" href="${posts[0].permalink()}" type="text/html">
% endif
+ ${math.math_styles_ifposts(posts)}
</%block>
<%block name="content">
-<%block name="content_header"></%block>
+<%block name="content_header">
+ ${feeds_translations.translation_link(kind)}
+</%block>
% if 'main_index' in pagekind:
${front_index_header}
% endif
+% if page_links:
+ ${pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed)}
+% endif
<div class="postindex">
% for post in posts:
- <article class="h-entry post-${post.meta('type')}">
+ <article class="h-entry post-${post.meta('type')}" itemscope="itemscope" itemtype="http://schema.org/Article">
<header>
<h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()|h}</a></h1>
<div class="metadata">
- <p class="byline author vcard"><span class="byline-name fn">
- % if author_pages_generated:
+ <p class="byline author vcard"><span class="byline-name fn" itemprop="author">
+ % if author_pages_generated and multiple_authors_per_post:
+ % for author in post.authors():
+ <a href="${_link('author', author)}">${author|h}</a>
+ % endfor
+ % elif author_pages_generated:
<a href="${_link('author', post.author())}">${post.author()|h}</a>
% else:
${post.author()|h}
% endif
</span></p>
- <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time></a></p>
+ <p class="dateline">
+ <a href="${post.permalink()}" rel="bookmark">
+ <time class="published dt-published" datetime="${post.formatted_date('webiso')}" itemprop="datePublished" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time>
+ % if post.updated and post.updated != post.date:
+ <span class="updated"> (${messages("updated")}
+ <time class="dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span>
+ % endif
+ </a>
+ </p>
% if not post.meta('nocomments') and site_has_comments:
<p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}
% endif
@@ -47,5 +68,5 @@
</div>
${helper.html_pager()}
${comments.comment_link_script()}
-${helper.mathjax_script(posts)}
+${math.math_scripts_ifposts(posts)}
</%block>
diff --git a/nikola/data/themes/base/templates/index_helper.tmpl b/nikola/data/themes/base/templates/index_helper.tmpl
index 0e98016..e400e3b 100644
--- a/nikola/data/themes/base/templates/index_helper.tmpl
+++ b/nikola/data/themes/base/templates/index_helper.tmpl
@@ -1,4 +1,5 @@
## -*- coding: utf-8 -*-
+<%namespace name="math" file="math_helper.tmpl"/>
<%def name="html_pager()">
%if prevlink or nextlink:
<nav class="postindexpager">
@@ -18,33 +19,7 @@
%endif
</%def>
+### This function is deprecated; use math_helper directly.
<%def name="mathjax_script(posts)">
- %if any(post.is_mathjax for post in posts):
- %if use_katex:
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script>
- % if katex_auto_render:
- <script>
- renderMathInElement(document.body,
- {
- ${katex_auto_render}
- }
- );
- </script>
- % else:
- <script>
- renderMathInElement(document.body);
- </script>
- % endif
- %else:
- <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
- % if mathjax_config:
- ${mathjax_config}
- % else:
- <script type="text/x-mathjax-config">
- MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
- </script>
- % endif
- %endif
- %endif
+ ${math.math_scripts_ifposts(posts)}
</%def>
diff --git a/nikola/data/themes/base/templates/list.tmpl b/nikola/data/themes/base/templates/list.tmpl
index 5a8843d..ca6c421 100644
--- a/nikola/data/themes/base/templates/list.tmpl
+++ b/nikola/data/themes/base/templates/list.tmpl
@@ -1,11 +1,19 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="extra_head">
+ ${feeds_translations.head(kind=kind, rss_override=False, has_no_feeds=has_no_feeds)}
+</%block>
<%block name="content">
<article class="listpage">
<header>
<h1>${title|h}</h1>
</header>
+ ${archive_nav.archive_navigation()}
+ ${feeds_translations.translation_link(kind)}
%if items:
<ul class="postlist">
% for text, link, count in items:
diff --git a/nikola/data/themes/base/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl
index bc52385..8cd9336 100644
--- a/nikola/data/themes/base/templates/list_post.tmpl
+++ b/nikola/data/themes/base/templates/list_post.tmpl
@@ -1,11 +1,19 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="extra_head">
+ ${feeds_translations.head(kind=kind, rss_override=False)}
+</%block>
<%block name="content">
<article class="listpage">
<header>
<h1>${title|h}</h1>
</header>
+ ${archive_nav.archive_navigation()}
+ ${feeds_translations.translation_link(kind)}
%if posts:
<ul class="postlist">
% for post in posts:
diff --git a/nikola/data/themes/base/templates/listing.tmpl b/nikola/data/themes/base/templates/listing.tmpl
index fae7607..ef2dfd6 100644
--- a/nikola/data/themes/base/templates/listing.tmpl
+++ b/nikola/data/themes/base/templates/listing.tmpl
@@ -1,15 +1,15 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
-<%namespace name="ui" file="crumbs.tmpl" import="bar"/>
+<%namespace name="ui" file="ui_helper.tmpl"/>
<%block name="content">
-${ui.bar(crumbs)}
+${ui.breadcrumbs(crumbs)}
%if folders or files:
<ul>
% for name in folders:
- <li><a href="${name|u}"><i class="icon-folder-open"></i> ${name|h}</a>
+ <li><a href="${name|h}" class="listing-folder">${name|h}</a>
% endfor
% for name in files:
- <li><a href="${name|u}.html"><i class="icon-file"></i> ${name|h}</a>
+ <li><a href="${name|h}.html" class="listing-file">${name|h}</a>
% endfor
</ul>
%endif
@@ -22,5 +22,3 @@ ${ui.bar(crumbs)}
${code}
% endif
</%block>
-
-
diff --git a/nikola/data/themes/base/templates/math_helper.tmpl b/nikola/data/themes/base/templates/math_helper.tmpl
new file mode 100644
index 0000000..961b7ce
--- /dev/null
+++ b/nikola/data/themes/base/templates/math_helper.tmpl
@@ -0,0 +1,69 @@
+### Note: at present, MathJax and KaTeX do not respect the USE_CDN configuration option
+<%def name="math_scripts()">
+ %if use_katex:
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.js" integrity="sha384-9Nhn55MVVN0/4OFx7EE5kpFBPsEMZxKTCnA+4fqDmg12eCTqGi6+BB2LjY8brQxJ" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/contrib/auto-render.min.js" integrity="sha384-kWPLUVMOks5AQFrykwIup5lo0m3iMkkHrD0uJ4H5cjeGihAutqP0yW0J6dpFiVkI" crossorigin="anonymous"></script>
+ % if katex_auto_render:
+ <script>
+ renderMathInElement(document.body,
+ {
+ ${katex_auto_render}
+ }
+ );
+ </script>
+ % else:
+ <script>
+ renderMathInElement(document.body,
+ {
+ delimiters: [
+ {left: "$$", right: "$$", display: true},
+ {left: "\\[", right: "\\]", display: true},
+ {left: "\\begin{equation*}", right: "\\end{equation*}", display: true},
+ {left: "\\(", right: "\\)", display: false}
+ ]
+ }
+ );
+ </script>
+ % endif
+ %else:
+### Note: given the size of MathJax; nikola will retrieve MathJax from a CDN regardless of use_cdn configuration
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_HTMLorMML" integrity="sha384-3lJUsx1TJHt7BA4udB5KPnDrlkO8T6J6v/op7ui0BbCjvZ9WqV4Xm6DTP6kQ/iBH" crossorigin="anonymous"></script>
+ % if mathjax_config:
+ ${mathjax_config}
+ % else:
+ <script type="text/x-mathjax-config">
+ MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
+ </script>
+ % endif
+ %endif
+</%def>
+
+<%def name="math_styles()">
+ % if use_katex:
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.10.2/dist/katex.min.css" integrity="sha384-yFRtMMDnQtDRO8rLpMIKrtPCD5jdktao2TV19YiZYWMDkUR5GQZR/NOVTdquEx1j" crossorigin="anonymous">
+ % endif
+</%def>
+
+<%def name="math_scripts_ifpost(post)">
+ %if post.has_math:
+ ${math_scripts()}
+ %endif
+</%def>
+
+<%def name="math_scripts_ifposts(posts)">
+ %if any(post.has_math for post in posts):
+ ${math_scripts()}
+ %endif
+</%def>
+
+<%def name="math_styles_ifpost(post)">
+ %if post.has_math:
+ ${math_styles()}
+ %endif
+</%def>
+
+<%def name="math_styles_ifposts(posts)">
+ %if any(post.has_math for post in posts):
+ ${math_styles()}
+ %endif
+</%def>
diff --git a/nikola/data/themes/base/templates/page.tmpl b/nikola/data/themes/base/templates/page.tmpl
new file mode 100644
index 0000000..b2cd756
--- /dev/null
+++ b/nikola/data/themes/base/templates/page.tmpl
@@ -0,0 +1 @@
+<%inherit file="story.tmpl"/>
diff --git a/nikola/data/themes/base/templates/pagination_helper.tmpl b/nikola/data/themes/base/templates/pagination_helper.tmpl
new file mode 100644
index 0000000..91c1115
--- /dev/null
+++ b/nikola/data/themes/base/templates/pagination_helper.tmpl
@@ -0,0 +1,16 @@
+## -*- coding: utf-8 -*-
+<%def name="page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5)">
+<div class="page-navigation">
+ % for i, link in enumerate(page_links):
+ % if abs(i - current_page) <= surrounding or i == 0 or i == len(page_links) - 1:
+ % if i == current_page:
+ <span class="current-page">${i+1}</span>
+ % else:
+ <a href="${page_links[i]}">${i+1}</a>
+ % endif
+ % elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1:
+ <span class="ellipsis">…</span>
+ % endif
+ % endfor
+</div>
+</%def>
diff --git a/nikola/data/themes/base/templates/post.tmpl b/nikola/data/themes/base/templates/post.tmpl
index da616bf..1f2f0a4 100644
--- a/nikola/data/themes/base/templates/post.tmpl
+++ b/nikola/data/themes/base/templates/post.tmpl
@@ -2,16 +2,14 @@
<%namespace name="helper" file="post_helper.tmpl"/>
<%namespace name="pheader" file="post_header.tmpl"/>
<%namespace name="comments" file="comments_helper.tmpl"/>
+<%namespace name="math" file="math_helper.tmpl"/>
<%inherit file="base.tmpl"/>
<%block name="extra_head">
${parent.extra_head()}
% if post.meta('keywords'):
- <meta name="keywords" content="${post.meta('keywords')|h}">
+ <meta name="keywords" content="${smartjoin(', ', post.meta('keywords'))|h}">
% endif
- %if post.description():
- <meta name="description" content="${post.description()|h}">
- %endif
<meta name="author" content="${post.author()|h}">
%if post.prev_post:
<link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html">
@@ -25,6 +23,7 @@
${helper.open_graph_metadata(post)}
${helper.twitter_card_information(post)}
${helper.meta_translations(post)}
+ ${math.math_styles_ifpost(post)}
</%block>
<%block name="content">
@@ -45,7 +44,7 @@
${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)}
</section>
% endif
- ${helper.mathjax_script(post)}
+ ${math.math_scripts_ifpost(post)}
</article>
${comments.comment_link_script()}
</%block>
diff --git a/nikola/data/themes/base/templates/post_header.tmpl b/nikola/data/themes/base/templates/post_header.tmpl
index 480c36a..617a156 100644
--- a/nikola/data/themes/base/templates/post_header.tmpl
+++ b/nikola/data/themes/base/templates/post_header.tmpl
@@ -23,7 +23,7 @@
<%def name="html_sourcelink()">
% if show_sourcelink:
- <p class="sourceline"><a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a></p>
+ <p class="sourceline"><a href="${post.source_link()}" class="sourcelink">${messages("Source")}</a></p>
% endif
</%def>
@@ -31,14 +31,26 @@
<header>
${html_title()}
<div class="metadata">
- <p class="byline author vcard"><span class="byline-name fn">
- % if author_pages_generated:
- <a href="${_link('author', post.author())}">${post.author()|h}</a>
+ <p class="byline author vcard p-author h-card"><span class="byline-name fn p-name" itemprop="author">
+ % if author_pages_generated and multiple_authors_per_post:
+ % for author in post.authors():
+ <a class="u-url" href="${_link('author', author)}">${author|h}</a>
+ % endfor
+ % elif author_pages_generated:
+ <a class="u-url" href="${_link('author', post.author())}">${post.author()|h}</a>
% else:
${post.author()|h}
% endif
</span></p>
- <p class="dateline"><a href="${post.permalink()}" rel="bookmark"><time class="published dt-published" datetime="${post.formatted_date('webiso')}" itemprop="datePublished" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time></a></p>
+ <p class="dateline">
+ <a href="${post.permalink()}" rel="bookmark">
+ <time class="published dt-published" datetime="${post.formatted_date('webiso')}" itemprop="datePublished" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time>
+ % if post.updated and post.updated != post.date:
+ <span class="updated"> (${messages("updated")}
+ <time class="updated dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span>
+ % endif
+ </a>
+ </p>
% if not post.meta('nocomments') and site_has_comments:
<p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}
% endif
@@ -46,9 +58,6 @@
% if post.meta('link'):
<p class="linkline"><a href="${post.meta('link')}">${messages("Original site")}</a></p>
% endif
- %if post.description():
- <meta name="description" itemprop="description" content="${post.description()|h}">
- %endif
</div>
${html_translations(post)}
</header>
diff --git a/nikola/data/themes/base/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl
index 47bf9b3..9ae4489 100644
--- a/nikola/data/themes/base/templates/post_helper.tmpl
+++ b/nikola/data/themes/base/templates/post_helper.tmpl
@@ -1,4 +1,5 @@
## -*- coding: utf-8 -*-
+<%namespace name="math" file="math_helper.tmpl"/>
<%def name="meta_translations(post)">
%if len(translations) > 1:
@@ -40,31 +41,29 @@
</%def>
<%def name="open_graph_metadata(post)">
-%if use_open_graph:
- <meta property="og:site_name" content="${blog_title|h}">
- <meta property="og:title" content="${post.title()[:70]|h}">
- <meta property="og:url" content="${abs_link(permalink)}">
- %if post.description():
+<meta property="og:site_name" content="${blog_title|h}">
+<meta property="og:title" content="${post.title()[:70]|h}">
+<meta property="og:url" content="${abs_link(permalink)}">
+%if post.description():
<meta property="og:description" content="${post.description()[:200]|h}">
- %else:
+%else:
<meta property="og:description" content="${post.text(strip_html=True)[:200]|h}">
- %endif
- %if post.previewimage:
+%endif
+%if post.previewimage:
<meta property="og:image" content="${url_replacer(permalink, post.previewimage, lang, 'absolute')}">
- %endif
- <meta property="og:type" content="article">
+%endif
+<meta property="og:type" content="article">
### Will only work with Pintrest and breaks everywhere else who expect a [Facebook] URI.
### %if post.author():
### <meta property="article:author" content="${post.author()|h}">
### %endif
- %if post.date.isoformat():
+%if post.date.isoformat():
<meta property="article:published_time" content="${post.formatted_date('webiso')}">
- %endif
- %if post.tags:
- %for tag in post.tags:
- <meta property="article:tag" content="${tag|h}">
- %endfor
- %endif
+%endif
+%if post.tags:
+ %for tag in post.tags:
+ <meta property="article:tag" content="${tag|h}">
+ %endfor
%endif
</%def>
@@ -84,33 +83,7 @@
%endif
</%def>
+### This function is deprecated; use math_helper directly.
<%def name="mathjax_script(post)">
- %if post.is_mathjax:
- %if use_katex:
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/katex.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.6.0/contrib/auto-render.min.js"></script>
- % if katex_auto_render:
- <script>
- renderMathInElement(document.body,
- {
- ${katex_auto_render}
- }
- );
- </script>
- % else:
- <script>
- renderMathInElement(document.body);
- </script>
- % endif
- %else:
- <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"> </script>
- % if mathjax_config:
- ${mathjax_config}
- % else:
- <script type="text/x-mathjax-config">
- MathJax.Hub.Config({tex2jax: {inlineMath: [['$latex ','$'], ['\\(','\\)']]}});
- </script>
- % endif
- %endif
- %endif
+ ${math.math_scripts_ifpost(post)}
</%def>
diff --git a/nikola/data/themes/base/templates/sectionindex.tmpl b/nikola/data/themes/base/templates/sectionindex.tmpl
deleted file mode 100644
index 7fb4f1e..0000000
--- a/nikola/data/themes/base/templates/sectionindex.tmpl
+++ /dev/null
@@ -1,21 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="index.tmpl"/>
-
-<%block name="extra_head">
- ${parent.extra_head()}
- % if generate_atom:
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${posts[0].section_name()|h} section" href="${_link('section_index_atom', posts[0].section_slug())}">
- % endif
-</%block>
-
-<%block name="content">
-<div class="sectionindex">
- <header>
- <h2><a href="${_link('section_index', posts[0].section_slug())}">${title|h}</a></h2>
- % if generate_atom:
- <p class="feedlink"><a href="${_link('section_index_atom', posts[0].section_slug())}" type="application/atom+xml">${messages('Updates')}</a></p>
- % endif
- </header>
- ${parent.content()}
-</div>
-</%block>
diff --git a/nikola/data/themes/base/templates/slides.tmpl b/nikola/data/themes/base/templates/slides.tmpl
deleted file mode 100644
index 048fb7e..0000000
--- a/nikola/data/themes/base/templates/slides.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-<%block name="content">
-<div id="${carousel_id}" class="carousel slide">
- <ol class="carousel-indicators">
- % for i in range(len(slides_content)):
- % if i == 0:
- <li data-target="#${carousel_id}" data-slide-to="${i}" class="active"></li>
- % else:
- <li data-target="#${carousel_id}" data-slide-to="${i}"></li>
- % endif
- % endfor
- </ol>
- <div class="carousel-inner">
- % for i, image in enumerate(slides_content):
- % if i == 0:
- <div class="item active"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div>
- % else:
- <div class="item"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div>
- % endif
- % endfor
- </div>
- <a class="left carousel-control" href="#${carousel_id}" data-slide="prev">&lsaquo;</a>
- <a class="right carousel-control" href="#${carousel_id}" data-slide="next">&rsaquo;</a>
-</div>
-</%block>
diff --git a/nikola/data/themes/base/templates/story.tmpl b/nikola/data/themes/base/templates/story.tmpl
index b8fb7ed..aeac04f 100644
--- a/nikola/data/themes/base/templates/story.tmpl
+++ b/nikola/data/themes/base/templates/story.tmpl
@@ -2,6 +2,7 @@
<%namespace name="helper" file="post_helper.tmpl"/>
<%namespace name="pheader" file="post_header.tmpl"/>
<%namespace name="comments" file="comments_helper.tmpl"/>
+<%namespace name="math" file="math_helper.tmpl"/>
<%inherit file="post.tmpl"/>
<%block name="content">
@@ -19,6 +20,6 @@
${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)}
</section>
%endif
- ${helper.mathjax_script(post)}
+ ${math.math_scripts_ifpost(post)}
</article>
</%block>
diff --git a/nikola/data/themes/base/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl
index 50c5bf2..ac40876 100644
--- a/nikola/data/themes/base/templates/tag.tmpl
+++ b/nikola/data/themes/base/templates/tag.tmpl
@@ -1,24 +1,17 @@
## -*- coding: utf-8 -*-
<%inherit file="list_post.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%block name="extra_head">
- ${parent.extra_head()}
- %if len(translations) > 1 and generate_rss:
- %for language in sorted(translations):
- <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${tag|h} (${language})" href="${_link(kind + "_rss", tag, language)}">
- %endfor
- %elif generate_rss:
- <link rel="alternate" type="application/rss+xml" title="RSS for ${kind} ${tag|h}" href="${_link(kind + "_rss", tag)}">
- %endif
+ ${feeds_translations.head(tag, kind, rss_override=False)}
</%block>
-
<%block name="content">
<article class="tagpage">
<header>
<h1>${title|h}</h1>
%if description:
- <p>${description}</p>
+ <p>${description}</p>
%endif
%if subcategories:
${messages('Subcategories:')}
@@ -29,23 +22,16 @@
</ul>
%endif
<div class="metadata">
- %if len(translations) > 1 and generate_rss:
- %for language in sorted(translations):
- <p class="feedlink">
- <a href="${_link(kind + "_rss", tag, language)}" hreflang="${language}" type="application/rss+xml">${messages('RSS feed', language)} (${language})</a>&nbsp;
- </p>
- %endfor
- %elif generate_rss:
- <p class="feedlink"><a href="${_link(kind + "_rss", tag)}" type="application/rss+xml">${messages('RSS feed')}</a></p>
- %endif
+ ${feeds_translations.feed_link(tag, kind=kind)}
</div>
+ ${feeds_translations.translation_link(kind)}
</header>
%if posts:
- <ul class="postlist">
- % for post in posts:
- <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}<a></li>
- % endfor
- </ul>
+ <ul class="postlist">
+ % for post in posts:
+ <li><time class="listdate" datetime="${post.formatted_date('webiso')}" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time> <a href="${post.permalink()}" class="listtitle">${post.title()|h}<a></li>
+ % endfor
+ </ul>
%endif
</article>
</%block>
diff --git a/nikola/data/themes/base/templates/tagindex.tmpl b/nikola/data/themes/base/templates/tagindex.tmpl
index c3c51b0..232d093 100644
--- a/nikola/data/themes/base/templates/tagindex.tmpl
+++ b/nikola/data/themes/base/templates/tagindex.tmpl
@@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
<%block name="content_header">
<header>
@@ -15,16 +16,14 @@
%endfor
</ul>
%endif
+ <div class="metadata">
+ ${feeds_translations.feed_link(tag, kind)}
+ ${feeds_translations.translation_link(kind)}
+ </div>
</header>
</%block>
<%block name="extra_head">
${parent.extra_head()}
- %if len(translations) > 1 and generate_atom:
- %for language in sorted(translations):
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag|h} section (${language})" href="${_link(kind + "_atom", tag, language)}">
- %endfor
- %elif generate_atom:
- <link rel="alternate" type="application/atom+xml" title="Atom for the ${tag|h} section" href="${_link("tag" + "_atom", tag)}">
- %endif
+ ${feeds_translations.head(tag, kind, rss_override=False)}
</%block>
diff --git a/nikola/data/themes/base/templates/tags.tmpl b/nikola/data/themes/base/templates/tags.tmpl
index 6c329d9..c54559a 100644
--- a/nikola/data/themes/base/templates/tags.tmpl
+++ b/nikola/data/themes/base/templates/tags.tmpl
@@ -1,10 +1,18 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="extra_head">
+ ${feeds_translations.head(kind=kind, feeds=False)}
+</%block>
<%block name="content">
<article class="tagindex">
<header>
<h1>${title|h}</h1>
+ <div class="metadata">
+ ${feeds_translations.translation_link(kind)}
+ </div>
</header>
% if cat_items:
% if items:
diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/ui_helper.tmpl
index 49c5e1e..173027c 100644
--- a/nikola/data/themes/base/templates/crumbs.tmpl
+++ b/nikola/data/themes/base/templates/ui_helper.tmpl
@@ -1,6 +1,5 @@
## -*- coding: utf-8 -*-
-
-<%def name="bar(crumbs)">
+<%def name="breadcrumbs(crumbs)">
%if crumbs:
<nav class="breadcrumbs">
<ul class="breadcrumb">
diff --git a/nikola/data/themes/bootblog4-jinja/README.md b/nikola/data/themes/bootblog4-jinja/README.md
new file mode 100644
index 0000000..6a9226e
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/README.md
@@ -0,0 +1,6 @@
+This is a theme based on Bootstrap 4 and the [blog example](https://getbootstrap.com/docs/4.0/examples/blog/) by @mdo.
+
+Note that unlike previous versions of Bootstrap, icon fonts are not built-in.
+You can use Font Awesome for this.
+
+This theme **does not** support Bootswatch font/color schemes.
diff --git a/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css b/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css
new file mode 120000
index 0000000..c8bd66a
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/assets/css/bootblog.css
@@ -0,0 +1 @@
+../../../bootblog4/assets/css/bootblog.css \ No newline at end of file
diff --git a/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme b/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme
new file mode 100644
index 0000000..8a5e55f
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/bootblog4-jinja.theme
@@ -0,0 +1,12 @@
+[Theme]
+engine = jinja
+parent = bootstrap4-jinja
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+based_on = Bootstrap 4 <http://getbootstrap.com/>, Bootstrap 4 blog example <http://getbootstrap.com/docs/4.0/examples/blog/>
+tags = bootstrap
+
+[Family]
+family = bootblog4
+mako-version = bootstrap4
diff --git a/nikola/data/themes/bootblog4-jinja/bundles b/nikola/data/themes/bootblog4-jinja/bundles
new file mode 120000
index 0000000..94a0160
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/bundles
@@ -0,0 +1 @@
+../bootblog4/bundles \ No newline at end of file
diff --git a/nikola/data/themes/bootblog4-jinja/templates/base.tmpl b/nikola/data/themes/bootblog4-jinja/templates/base.tmpl
new file mode 100644
index 0000000..0adf447
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/templates/base.tmpl
@@ -0,0 +1,104 @@
+{# -*- coding: utf-8 -*- #}
+{% import 'base_helper.tmpl' as base with context %}
+{% import 'annotation_helper.tmpl' as notes with context %}
+{{ set_locale(lang) }}
+{{ base.html_headstart() }}
+{% block extra_head %}
+{# Leave this block alone. #}
+{% endblock %}
+{{ template_hooks['extra_head']() }}
+</head>
+<body>
+<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
+
+<!-- Header and menu bar -->
+<div class="container">
+ <header class="blog-header py-3">
+ <div class="row nbb-header align-items-center">
+ <div class="col-md-3 col-xs-2 col-sm-2" style="width: auto;">
+ <button class="navbar-toggler navbar-light bg-light nbb-navbar-toggler" type="button" data-toggle="collapse" data-target=".bs-nav-collapsible" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="collapse bs-nav-collapsible bootblog4-search-form-holder">
+ {{ search_form }}
+ </div>
+ </div>
+ <div class="col-md-6 col-xs-10 col-sm-10 bootblog4-brand" style="width: auto;">
+ <a class="navbar-brand blog-header-logo text-dark" href="{{ _link("root", None, lang) }}">
+ {% if logo_url %}
+ <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo" class="d-inline-block align-top">
+ {% endif %}
+
+ {% if show_blog_title %}
+ <span id="blog-title">{{ blog_title|e }}</span>
+ {% endif %}
+ </a>
+ </div>
+ <div class="col-md-3 justify-content-end align-items-center bs-nav-collapsible collapse flex-collapse bootblog4-right-nav">
+ <nav class="navbar navbar-light bg-white">
+ <ul class="navbar-nav bootblog4-right-nav">
+ {{ base.html_navigation_links_entries(navigation_alt_links) }}
+ {% block belowtitle %}
+ {% if translations|length > 1 %}
+ {{ base.html_translations() }}
+ {% endif %}
+ {% endblock %}
+ {% block sourcelink %}{% endblock %}
+ {{ template_hooks['menu_alt']() }}
+ </ul></nav>
+ </div>
+ </div>
+</header>
+
+<nav class="navbar navbar-expand-md navbar-light bg-white static-top">
+ <div class="collapse navbar-collapse bs-nav-collapsible" id="bs-navbar">
+ <ul class="navbar-nav nav-fill d-flex w-100">
+ {{ base.html_navigation_links_entries(navigation_links) }}
+ {{ template_hooks['menu']() }}
+ </ul>
+ </div><!-- /.navbar-collapse -->
+</nav>
+{% block before_content %}{% endblock %}
+</div>
+
+<div class="container" id="content" role="main">
+ <div class="body-content">
+ {% if theme_config.get('sidebar') %}
+ <div class="row"><div class="col-md-8 blog-main">
+ {% endif %}
+ <!--Body content-->
+ {{ template_hooks['page_header']() }}
+ {% block extra_header %}{% endblock %}
+ {% block content %}{% endblock %}
+ <!--End of body content-->
+ {% if theme_config.get('sidebar') %}
+ </div><aside class="col-md-4 blog-sidebar">{{ theme_config.get('sidebar') }}</aside></div>
+ {% endif %}
+
+ <footer id="footer">
+ {{ content_footer }}
+ {{ template_hooks['page_footer']() }}
+ {% block extra_footer %}{% endblock %}
+ </footer>
+ </div>
+</div>
+
+{{ base.late_load_js() }}
+ {% if date_fanciness != 0 %}
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}";
+ fancydates({{ date_fanciness }}, {{ luxon_date_format }});
+ </script>
+ <!-- end fancy dates -->
+ {% endif %}
+ {% block extra_js %}{% endblock %}
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
+{{ body_end }}
+{{ template_hooks['body_end']() }}
+</body>
+</html>
diff --git a/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl
new file mode 100644
index 0000000..0b74696
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/templates/base_helper.tmpl
@@ -0,0 +1,169 @@
+{# -*- coding: utf-8 -*- #}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% macro html_headstart() %}
+<!DOCTYPE html>
+<html
+
+prefix='
+og: http://ogp.me/ns# article: http://ogp.me/ns/article#
+{% if comment_system == 'facebook' %}
+fb: http://ogp.me/ns/fb#
+{% endif %}
+'
+{% if is_rtl %}
+dir="rtl"
+{% endif %}
+
+lang="{{ lang }}">
+ <head>
+ <meta charset="utf-8">
+ {% if description %}
+ <meta name="description" content="{{ description|e }}">
+ {% endif %}
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ {% if title == blog_title %}
+ <title>{{ blog_title|e }}</title>
+ {% else %}
+ <title>{{ title|e }} | {{ blog_title|e }}</title>
+ {% endif %}
+
+ {{ html_stylesheets() }}
+ <meta name="theme-color" content="{{ theme_color }}">
+ {% if meta_generator_tag %}
+ <meta name="generator" content="Nikola (getnikola.com)">
+ {% endif %}
+ {{ html_feedlinks() }}
+ <link rel="canonical" href="{{ abs_link(permalink) }}">
+
+ {% if favicons %}
+ {% for name, file, size in favicons %}
+ <link rel="{{ name }}" href="{{ file }}" sizes="{{ size }}"/>
+ {% endfor %}
+ {% endif %}
+
+ {% if comment_system == 'facebook' %}
+ <meta property="fb:app_id" content="{{ comment_system_id }}">
+ {% endif %}
+
+ {% if prevlink %}
+ <link rel="prev" href="{{ prevlink }}" type="text/html">
+ {% endif %}
+ {% if nextlink %}
+ <link rel="next" href="{{ nextlink }}" type="text/html">
+ {% endif %}
+
+ {% if use_cdn %}
+ <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ {% else %}
+ <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang, url_type) }}"></script><![endif]-->
+ {% endif %}
+
+ {{ extra_head_data }}
+{% endmacro %}
+
+{% macro late_load_js() %}
+ {% if use_cdn %}
+ <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ {% endif %}
+ {% if use_bundles and use_cdn %}
+ <script src="/assets/js/all.js"></script>
+ {% elif use_bundles %}
+ <script src="/assets/js/all-nocdn.js"></script>
+ {% else %}
+ {% if not use_cdn %}
+ <script src="/assets/js/jquery.min.js"></script>
+ <script src="/assets/js/popper.min.js"></script>
+ <script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ {% endif %}
+ {% endif %}
+ {% if date_fanciness != 0 %}
+ {% if date_fanciness == 2 %}
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script>
+ {% endif %}
+ {% if use_cdn %}
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ {% else %}
+ <script src="/assets/js/luxon.min.js"></script>
+ {% endif %}
+ {% if not use_bundles %}
+ <script src="/assets/js/fancydates.min.js"></script>
+ {% endif %}
+ {% endif %}
+ {{ social_buttons_code }}
+{% endmacro %}
+
+
+{% macro html_stylesheets() %}
+ {% if use_cdn %}
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
+ {% endif %}
+ {% if use_bundles and use_cdn %}
+ <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
+ {% elif use_bundles %}
+ <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
+ {% else %}
+ {% if not use_cdn %}
+ <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ {% endif %}
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/bootblog.css" rel="stylesheet" type="text/css">
+ {% if has_custom_css %}
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ {% endif %}
+ {% endif %}
+ {% if needs_ipython_css %}
+ <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
+ {% endif %}
+ <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet">
+{% endmacro %}
+
+{% macro html_navigation_links() %}
+ {{ html_navigation_links_entries(navigation_links) }}
+{% endmacro %}
+
+{% macro html_navigation_links_entries(navigation_links_source) %}
+ {% for url, text in navigation_links_source[lang] %}
+ {% if isinstance(url, tuple) %}
+ <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }}</a>
+ <div class="dropdown-menu">
+ {% for suburl, text in url %}
+ {% if rel_link(permalink, suburl) == "#" %}
+ <a href="{{ permalink }}" class="dropdown-item active">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
+ {% else %}
+ <a href="{{ suburl }}" class="dropdown-item">{{ text }}</a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% else %}
+ {% if rel_link(permalink, url) == "#" %}
+ <li class="nav-item active"><a href="{{ permalink }}" class="nav-link">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
+ {% else %}
+ <li class="nav-item"><a href="{{ url }}" class="nav-link">{{ text }}</a>
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+
+
+
+{% macro html_feedlinks() %}
+ {{ feeds_translations.head(classification=None, kind='index', other=False) }}
+{% endmacro %}
+
+{% macro html_translations() %}
+ {% for langname in translations|sort %}
+ {% if langname != lang %}
+ <li class="nav-item"><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}" class="nav-link">{{ messages("LANGUAGE", langname) }}</a></li>
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
diff --git a/nikola/data/themes/bootblog4-jinja/templates/index.tmpl b/nikola/data/themes/bootblog4-jinja/templates/index.tmpl
new file mode 100644
index 0000000..efc4e58
--- /dev/null
+++ b/nikola/data/themes/bootblog4-jinja/templates/index.tmpl
@@ -0,0 +1,150 @@
+{# -*- coding: utf-8 -*- #}
+{% import 'index_helper.tmpl' as helper with context %}
+{% import 'math_helper.tmpl' as math with context %}
+{% import 'comments_helper.tmpl' as comments with context %}
+{% import 'pagination_helper.tmpl' as pagination with context %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+{% extends 'base.tmpl' %}
+
+{% block extra_head %}
+ {{ super() }}
+ {% if posts and (permalink == '/' or permalink == '/' + index_file) %}
+ <link rel="prefetch" href="{{ posts[0].permalink() }}" type="text/html">
+ {% endif %}
+ {{ math.math_styles_ifposts(posts) }}
+{% endblock %}
+
+{% block content %}
+ {% block content_header %}
+ {{ feeds_translations.translation_link(kind) }}
+ {% endblock %}
+ {% if 'main_index' in pagekind %}
+ {{ front_index_header }}
+ {% endif %}
+ {% if page_links %}
+ {{ pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed) }}
+ {% endif %}
+ <div class="postindex">
+ {% for post in posts %}
+ <article class="h-entry post-{{ post.meta('type') }}" itemscope="itemscope" itemtype="http://schema.org/Article">
+ <header>
+ <h1 class="p-name entry-title"><a href="{{ post.permalink() }}" class="u-url">{{ post.title()|e }}</a></h1>
+ <div class="metadata">
+ <p class="byline author vcard"><span class="byline-name fn" itemprop="author">
+ {% if author_pages_generated and multiple_authors_per_post %}
+ {% for author in post.authors() %}
+ <a href="{{ _link('author', author) }}">{{ author|e }}</a>
+ {% endfor %}
+ {% elif author_pages_generated %}
+ <a href="{{ _link('author', post.author()) }}">{{ post.author()|e }}</a>
+ {% else %}
+ {{ post.author()|e }}
+ {% endif %}
+ </span></p>
+ <p class="dateline">
+ <a href="{{ post.permalink() }}" rel="bookmark">
+ <time class="published dt-published" datetime="{{ post.formatted_date('webiso') }}" itemprop="datePublished" title="{{ post.formatted_date(date_format)|e }}">{{ post.formatted_date(date_format)|e }}</time>
+ {% if post.updated and post.updated != post.date %}
+ <span class="updated"> ({{ messages("updated") }}
+ <time class="dt-updated" datetime="{{ post.formatted_updated('webiso') }}" itemprop="dateUpdated" title="{{ post.formatted_updated(date_format)|e }}">{{ post.formatted_updated(date_format)|e }}</time>)</span>
+ {% endif %}
+ </a>
+ </p>
+ {% if not post.meta('nocomments') and site_has_comments %}
+ <p class="commentline">{{ comments.comment_link(post.permalink(), post._base_path) }}
+ {% endif %}
+ </div>
+ </header>
+ {% if index_teasers %}
+ <div class="p-summary entry-summary">
+ {{ post.text(teaser_only=True) }}
+ </div>
+ {% else %}
+ <div class="e-content entry-content">
+ {{ post.text(teaser_only=False) }}
+ </div>
+ {% endif %}
+ </article>
+ {% endfor %}
+ </div>
+ {{ helper.html_pager() }}
+ {{ comments.comment_link_script() }}
+ {{ math.math_scripts_ifposts(posts) }}
+{% endblock %}
+
+{% block before_content %}
+ {% if 'main_index' in pagekind and is_frontmost_index and featured and (theme_config.get('featured_large') or theme_config.get('featured_small')) %}
+ {% if theme_config.get('featured_on_mobile') %}
+ <div class="d-block">
+ {% else %}
+ <div class="d-none d-md-block">
+ {% endif %}
+ {% if featured and theme_config.get('featured_large') %}
+ <div class="jumbotron p-0 text-white rounded bg-dark">
+ <div class="row bootblog4-featured-jumbotron-row">
+ <div class="col-md-6 p-3 p-md-4 pr-0 h-md-250 bootblog4-featured-text">
+ <h1 class="display-4 font-italic"><a class="text-white" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a></h1>
+ {% if featured[0].previewimage %}
+ <div class="lead my-3 mb-0">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ {% if theme_config.get('featured_large_image_on_mobile') %}
+ <div class="col-md-6 p-0 h-md-250 text-right">
+ {% else %}
+ <div class="col-md-6 p-0 h-md-250 text-right d-none d-md-block">
+ {% endif %}
+ <img class="bootblog4-featured-large-image" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}">
+ </div>
+ {% else %}
+ <div class="lead my-3 mb-0">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+
+ {% if featured and theme_config.get('featured_small') %}
+ <div class="row mb-2">
+ {% if featured|length == 1 %}
+ <div class="col-md-12">
+ {% else %}
+ <div class="col-md-6">
+ {% endif %}
+ <div class="card flex-md-row mb-4 box-shadow h-md-250">
+ <div class="card-body d-flex flex-column align-items-start">
+ <h3 class="mb-0">
+ <a class="text-dark" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a>
+ </h3>
+ {% if featured[0].previewimage %}
+ <div class="card-text mb-auto bootblog4-featured-text">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ <img class="card-img-right flex-auto d-none d-lg-block" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}">
+ {% else %}
+ <div class="card-text mb-auto bootblog4-featured-text">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+
+ {% if featured %}
+ <div class="col-md-6">
+ <div class="card flex-md-row mb-4 box-shadow h-md-250">
+ <div class="card-body d-flex flex-column align-items-start">
+ <h3 class="mb-0">
+ <a class="text-dark" href="{{ featured[0].permalink() }}">{{ featured[0].title() }}</a>
+ </h3>
+ {% if featured[0].previewimage %}
+ <div class="card-text mb-auto bootblog4-featured-text">{{ featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ <img class="card-img-right flex-auto d-none d-lg-block" src="{{ featured[0].previewimage }}" alt="{{ featured.pop(0).title() }}">
+ {% else %}
+ <div class="card-text mb-auto bootblog4-featured-text">{{ featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True)) }}</div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+{% endif %}
+{% endblock %}
diff --git a/nikola/data/themes/bootblog4/README.md b/nikola/data/themes/bootblog4/README.md
new file mode 100644
index 0000000..6a9226e
--- /dev/null
+++ b/nikola/data/themes/bootblog4/README.md
@@ -0,0 +1,6 @@
+This is a theme based on Bootstrap 4 and the [blog example](https://getbootstrap.com/docs/4.0/examples/blog/) by @mdo.
+
+Note that unlike previous versions of Bootstrap, icon fonts are not built-in.
+You can use Font Awesome for this.
+
+This theme **does not** support Bootswatch font/color schemes.
diff --git a/nikola/data/themes/bootblog4/assets/css/bootblog.css b/nikola/data/themes/bootblog4/assets/css/bootblog.css
new file mode 100644
index 0000000..96d4b92
--- /dev/null
+++ b/nikola/data/themes/bootblog4/assets/css/bootblog.css
@@ -0,0 +1,225 @@
+/* stylelint-disable selector-list-comma-newline-after */
+
+.blog-header {
+ line-height: 1;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.blog-header-logo {
+ font-family: "Playfair Display", Georgia, "Times New Roman", serif;
+ font-size: 2.25rem;
+}
+
+.blog-header-logo:hover {
+ text-decoration: none;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: "Playfair Display", Georgia, "Times New Roman", serif;
+}
+
+.display-4 {
+ font-size: 2.5rem;
+}
+@media (min-width: 768px) {
+ .display-4 {
+ font-size: 3rem;
+ }
+}
+
+.nav-scroller {
+ position: relative;
+ z-index: 2;
+ height: 2.75rem;
+ overflow-y: hidden;
+}
+
+.nav-scroller .nav {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ padding-bottom: 1rem;
+ margin-top: -1px;
+ overflow-x: auto;
+ text-align: center;
+ white-space: nowrap;
+ -webkit-overflow-scrolling: touch;
+}
+
+.nav-scroller .nav-link {
+ padding-top: .75rem;
+ padding-bottom: .75rem;
+ font-size: .875rem;
+}
+
+.card-img-right {
+ height: 100%;
+ border-radius: 0 3px 3px 0;
+}
+
+.flex-auto {
+ -ms-flex: 0 0 auto;
+ -webkit-box-flex: 0;
+ flex: 0 0 auto;
+}
+
+.h-150 { height: 150px; }
+@media (min-width: 768px) {
+ .h-md-150 { height: 150px; }
+}
+
+.h-250 { height: 250px; }
+@media (min-width: 768px) {
+ .h-md-250 { height: 250px; }
+}
+
+.border-top { border-top: 1px solid #e5e5e5; }
+.border-bottom { border-bottom: 1px solid #e5e5e5; }
+
+.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
+
+/*
+ * Blog name and description
+ */
+.blog-title {
+ margin-bottom: 0;
+ font-size: 2rem;
+ font-weight: 400;
+}
+.blog-description {
+ font-size: 1.1rem;
+ color: #999;
+}
+
+@media (min-width: 40em) {
+ .blog-title {
+ font-size: 3.5rem;
+ }
+}
+
+/* Pagination */
+.blog-pagination {
+ margin-bottom: 4rem;
+}
+.blog-pagination > .btn {
+ border-radius: 2rem;
+}
+
+/*
+ * Blog posts
+ */
+article {
+ margin-bottom: 4rem;
+}
+article:last-child {
+ margin-bottom: 0;
+}
+.entry-title {
+ margin-bottom: .25rem;
+ font-size: 2.5rem;
+}
+article .metadata {
+ margin-bottom: 1.25rem;
+ color: #999;
+}
+
+/*
+ * Footer
+ */
+.blog-footer {
+ padding: 2.5rem 0;
+ color: #999;
+ text-align: center;
+ background-color: #f9f9f9;
+ border-top: .05rem solid #e5e5e5;
+}
+.blog-footer p:last-child {
+ margin-bottom: 0;
+}
+
+@media (min-width: 576px) {
+ .nbb-navbar-toggler {
+ display: none;
+ }
+
+ .nbb-header {
+ -webkit-box-pack: justify!important;
+ -ms-flex-pack: justify!important;
+ justify-content: space-between!important;
+ }
+}
+
+/* Various fixes that make this theme look better for Nikola's needs */
+.navbar-brand {
+ padding: 0;
+ white-space: initial;
+}
+
+.bootblog4-featured-large-image {
+ height: 100%;
+ border-top-right-radius: .25rem!important;
+ border-bottom-right-radius: .25rem!important;
+}
+
+.bootblog4-featured-jumbotron-row {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.bootblog4-right-nav {
+ flex-direction: row;
+}
+
+.bootblog4-right-nav .nav-link {
+ padding-right: .5rem;
+ padding-left: .5rem;
+}
+
+.bootblog4-featured-text {
+ overflow: auto;
+}
+/* extend the mobile appearance to `sm`, because otherwise weird things happen */
+@media (min-width: 576px) {
+ .nbb-navbar-toggler {
+ display: block;
+ }
+}
+
+@media (max-width: 767px) {
+ .bootblog4-right-nav {
+ margin-top: 1rem;
+ }
+
+ .bootblog4-search-form-holder {
+ position: absolute;
+ top: 2.75rem;
+ }
+
+ .bootblog4-search-form-holder input.form-control {
+ width: 6rem;
+ }
+
+ .bootblog4-brand {
+ text-align: left;
+ }
+}
+
+@media (min-width: 768px) {
+ .nbb-navbar-toggler {
+ display: none;
+ }
+
+ .flex-collapse {
+ display: flex !important;
+ }
+
+ .bootblog4-search-form-holder {
+ display: block !important;
+ }
+
+ .bootblog4-brand {
+ text-align: center;
+ }
+}
diff --git a/nikola/data/themes/bootblog4/bootblog4.theme b/nikola/data/themes/bootblog4/bootblog4.theme
new file mode 100644
index 0000000..46db4ea
--- /dev/null
+++ b/nikola/data/themes/bootblog4/bootblog4.theme
@@ -0,0 +1,12 @@
+[Theme]
+engine = mako
+parent = bootstrap4
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+based_on = Bootstrap 4 <http://getbootstrap.com/>, Bootstrap 4 blog example <http://getbootstrap.com/docs/4.0/examples/blog/>
+tags = bootstrap
+
+[Family]
+family = bootblog4
+jinja_version = bootblog4-jinja
diff --git a/nikola/data/themes/bootblog4/bundles b/nikola/data/themes/bootblog4/bundles
new file mode 100644
index 0000000..76ffd4b
--- /dev/null
+++ b/nikola/data/themes/bootblog4/bundles
@@ -0,0 +1,28 @@
+; css bundles
+assets/css/all-nocdn.css=
+ bootstrap.min.css,
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ baguetteBox.min.css,
+ theme.css,
+ bootblog.css,
+ custom.css,
+assets/css/all.css=
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ baguetteBox.min.css,
+ theme.css,
+ bootblog.css,
+ custom.css,
+
+; javascript bundles
+assets/js/all-nocdn.js=
+ jquery.min.js,
+ popper.min.js,
+ bootstrap.min.js,
+ baguetteBox.min.js,
+ fancydates.min.js,
+assets/js/all.js=
+ fancydates.min.js,
diff --git a/nikola/data/themes/bootblog4/templates/base.tmpl b/nikola/data/themes/bootblog4/templates/base.tmpl
new file mode 100644
index 0000000..69b9d30
--- /dev/null
+++ b/nikola/data/themes/bootblog4/templates/base.tmpl
@@ -0,0 +1,104 @@
+## -*- coding: utf-8 -*-
+<%namespace name="base" file="base_helper.tmpl" import="*" />
+<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
+${set_locale(lang)}
+${base.html_headstart()}
+<%block name="extra_head">
+### Leave this block alone.
+</%block>
+${template_hooks['extra_head']()}
+</head>
+<body>
+<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
+
+<!-- Header and menu bar -->
+<div class="container">
+ <header class="blog-header py-3">
+ <div class="row nbb-header align-items-center">
+ <div class="col-md-3 col-xs-2 col-sm-2" style="width: auto;">
+ <button class="navbar-toggler navbar-light bg-light nbb-navbar-toggler" type="button" data-toggle="collapse" data-target=".bs-nav-collapsible" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="collapse bs-nav-collapsible bootblog4-search-form-holder">
+ ${search_form}
+ </div>
+ </div>
+ <div class="col-md-6 col-xs-10 col-sm-10 bootblog4-brand" style="width: auto;">
+ <a class="navbar-brand blog-header-logo text-dark" href="${_link("root", None, lang)}">
+ %if logo_url:
+ <img src="${logo_url}" alt="${blog_title|h}" id="logo" class="d-inline-block align-top">
+ %endif
+
+ % if show_blog_title:
+ <span id="blog-title">${blog_title|h}</span>
+ % endif
+ </a>
+ </div>
+ <div class="col-md-3 justify-content-end align-items-center bs-nav-collapsible collapse flex-collapse bootblog4-right-nav">
+ <nav class="navbar navbar-light bg-white">
+ <ul class="navbar-nav bootblog4-right-nav">
+ ${base.html_navigation_links_entries(navigation_alt_links)}
+ <%block name="belowtitle">
+ %if len(translations) > 1:
+ ${base.html_translations()}
+ %endif
+ </%block>
+ <%block name="sourcelink"></%block>
+ ${template_hooks['menu_alt']()}
+ </ul></nav>
+ </div>
+ </div>
+</header>
+
+<nav class="navbar navbar-expand-md navbar-light bg-white static-top">
+ <div class="collapse navbar-collapse bs-nav-collapsible" id="bs-navbar">
+ <ul class="navbar-nav nav-fill d-flex w-100">
+ ${base.html_navigation_links_entries(navigation_links)}
+ ${template_hooks['menu']()}
+ </ul>
+ </div><!-- /.navbar-collapse -->
+</nav>
+<%block name="before_content"></%block>
+</div>
+
+<div class="container" id="content" role="main">
+ <div class="body-content">
+ % if theme_config.get('sidebar'):
+ <div class="row"><div class="col-md-8 blog-main">
+ % endif
+ <!--Body content-->
+ ${template_hooks['page_header']()}
+ <%block name="extra_header"></%block>
+ <%block name="content"></%block>
+ <!--End of body content-->
+ % if theme_config.get('sidebar'):
+ </div><aside class="col-md-4 blog-sidebar">${theme_config.get('sidebar')}</aside></div>
+ % endif
+
+ <footer id="footer">
+ ${content_footer}
+ ${template_hooks['page_footer']()}
+ <%block name="extra_footer"></%block>
+ </footer>
+ </div>
+</div>
+
+${base.late_load_js()}
+ %if date_fanciness != 0:
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "${luxon_locales[lang]}";
+ fancydates(${date_fanciness}, ${luxon_date_format});
+ </script>
+ <!-- end fancy dates -->
+ %endif
+ <%block name="extra_js"></%block>
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
+${body_end}
+${template_hooks['body_end']()}
+</body>
+</html>
diff --git a/nikola/data/themes/bootblog4/templates/base_helper.tmpl b/nikola/data/themes/bootblog4/templates/base_helper.tmpl
new file mode 100644
index 0000000..3c919b4
--- /dev/null
+++ b/nikola/data/themes/bootblog4/templates/base_helper.tmpl
@@ -0,0 +1,169 @@
+## -*- coding: utf-8 -*-
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%def name="html_headstart()">
+<!DOCTYPE html>
+<html
+\
+prefix='\
+og: http://ogp.me/ns# article: http://ogp.me/ns/article# \
+%if comment_system == 'facebook':
+fb: http://ogp.me/ns/fb# \
+%endif
+'\
+% if is_rtl:
+dir="rtl" \
+% endif
+\
+lang="${lang}">
+ <head>
+ <meta charset="utf-8">
+ %if description:
+ <meta name="description" content="${description|h}">
+ %endif
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ %if title == blog_title:
+ <title>${blog_title|h}</title>
+ %else:
+ <title>${title|h} | ${blog_title|h}</title>
+ %endif
+
+ ${html_stylesheets()}
+ <meta name="theme-color" content="${theme_color}">
+ % if meta_generator_tag:
+ <meta name="generator" content="Nikola (getnikola.com)">
+ % endif
+ ${html_feedlinks()}
+ <link rel="canonical" href="${abs_link(permalink)}">
+
+ %if favicons:
+ %for name, file, size in favicons:
+ <link rel="${name}" href="${file}" sizes="${size}"/>
+ %endfor
+ %endif
+
+ % if comment_system == 'facebook':
+ <meta property="fb:app_id" content="${comment_system_id}">
+ % endif
+
+ %if prevlink:
+ <link rel="prev" href="${prevlink}" type="text/html">
+ %endif
+ %if nextlink:
+ <link rel="next" href="${nextlink}" type="text/html">
+ %endif
+
+ %if use_cdn:
+ <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ %else:
+ <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang, url_type)}"></script><![endif]-->
+ %endif
+
+ ${extra_head_data}
+</%def>
+
+<%def name="late_load_js()">
+ %if use_cdn:
+ <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ % endif
+ %if use_bundles and use_cdn:
+ <script src="/assets/js/all.js"></script>
+ %elif use_bundles:
+ <script src="/assets/js/all-nocdn.js"></script>
+ %else:
+ %if not use_cdn:
+ <script src="/assets/js/jquery.min.js"></script>
+ <script src="/assets/js/popper.min.js"></script>
+ <script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ %endif
+ %endif
+ %if date_fanciness != 0:
+ %if date_fanciness == 2:
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script>
+ %endif
+ %if use_cdn:
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ %else:
+ <script src="/assets/js/luxon.min.js"></script>
+ %endif
+ %if not use_bundles:
+ <script src="/assets/js/fancydates.min.js"></script>
+ %endif
+ %endif
+ ${social_buttons_code}
+</%def>
+
+
+<%def name="html_stylesheets()">
+ % if use_cdn:
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
+ % endif
+ %if use_bundles and use_cdn:
+ <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
+ %elif use_bundles:
+ <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
+ %else:
+ %if not use_cdn:
+ <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ %endif
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/bootblog.css" rel="stylesheet" type="text/css">
+ %if has_custom_css:
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ %endif
+ %endif
+ % if needs_ipython_css:
+ <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
+ % endif
+ <link href="https://fonts.googleapis.com/css?family=Playfair+Display:700,900" rel="stylesheet">
+</%def>
+
+<%def name="html_navigation_links()">
+ ${html_navigation_links_entries(navigation_links)}
+</%def>
+
+<%def name="html_navigation_links_entries(navigation_links_source)">
+ %for url, text in navigation_links_source[lang]:
+ % if isinstance(url, tuple):
+ <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text}</a>
+ <div class="dropdown-menu">
+ %for suburl, text in url:
+ % if rel_link(permalink, suburl) == "#":
+ <a href="${permalink}" class="dropdown-item active">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
+ %else:
+ <a href="${suburl}" class="dropdown-item">${text}</a>
+ %endif
+ %endfor
+ </div>
+ % else:
+ % if rel_link(permalink, url) == "#":
+ <li class="nav-item active"><a href="${permalink}" class="nav-link">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
+ %else:
+ <li class="nav-item"><a href="${url}" class="nav-link">${text}</a>
+ %endif
+ % endif
+ %endfor
+</%def>
+
+
+
+<%def name="html_feedlinks()">
+ ${feeds_translations.head(classification=None, kind='index', other=False)}
+</%def>
+
+<%def name="html_translations()">
+ %for langname in sorted(translations):
+ %if langname != lang:
+ <li class="nav-item"><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}" class="nav-link">${messages("LANGUAGE", langname)}</a></li>
+ %endif
+ %endfor
+</%def>
diff --git a/nikola/data/themes/bootblog4/templates/index.tmpl b/nikola/data/themes/bootblog4/templates/index.tmpl
new file mode 100644
index 0000000..449c5ec
--- /dev/null
+++ b/nikola/data/themes/bootblog4/templates/index.tmpl
@@ -0,0 +1,150 @@
+## -*- coding: utf-8 -*-
+<%namespace name="helper" file="index_helper.tmpl"/>
+<%namespace name="math" file="math_helper.tmpl"/>
+<%namespace name="comments" file="comments_helper.tmpl"/>
+<%namespace name="pagination" file="pagination_helper.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+<%inherit file="base.tmpl"/>
+
+<%block name="extra_head">
+ ${parent.extra_head()}
+ % if posts and (permalink == '/' or permalink == '/' + index_file):
+ <link rel="prefetch" href="${posts[0].permalink()}" type="text/html">
+ % endif
+ ${math.math_styles_ifposts(posts)}
+</%block>
+
+<%block name="content">
+ <%block name="content_header">
+ ${feeds_translations.translation_link(kind)}
+ </%block>
+ % if 'main_index' in pagekind:
+ ${front_index_header}
+ % endif
+ % if page_links:
+ ${pagination.page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed)}
+ % endif
+ <div class="postindex">
+ % for post in posts:
+ <article class="h-entry post-${post.meta('type')}" itemscope="itemscope" itemtype="http://schema.org/Article">
+ <header>
+ <h1 class="p-name entry-title"><a href="${post.permalink()}" class="u-url">${post.title()|h}</a></h1>
+ <div class="metadata">
+ <p class="byline author vcard"><span class="byline-name fn" itemprop="author">
+ % if author_pages_generated and multiple_authors_per_post:
+ % for author in post.authors():
+ <a href="${_link('author', author)}">${author|h}</a>
+ % endfor
+ % elif author_pages_generated:
+ <a href="${_link('author', post.author())}">${post.author()|h}</a>
+ % else:
+ ${post.author()|h}
+ % endif
+ </span></p>
+ <p class="dateline">
+ <a href="${post.permalink()}" rel="bookmark">
+ <time class="published dt-published" datetime="${post.formatted_date('webiso')}" itemprop="datePublished" title="${post.formatted_date(date_format)|h}">${post.formatted_date(date_format)|h}</time>
+ % if post.updated and post.updated != post.date:
+ <span class="updated"> (${messages("updated")}
+ <time class="dt-updated" datetime="${post.formatted_updated('webiso')}" itemprop="dateUpdated" title="${post.formatted_updated(date_format)|h}">${post.formatted_updated(date_format)|h}</time>)</span>
+ % endif
+ </a>
+ </p>
+ % if not post.meta('nocomments') and site_has_comments:
+ <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}
+ % endif
+ </div>
+ </header>
+ %if index_teasers:
+ <div class="p-summary entry-summary">
+ ${post.text(teaser_only=True)}
+ </div>
+ %else:
+ <div class="e-content entry-content">
+ ${post.text(teaser_only=False)}
+ </div>
+ %endif
+ </article>
+ % endfor
+ </div>
+ ${helper.html_pager()}
+ ${comments.comment_link_script()}
+ ${math.math_scripts_ifposts(posts)}
+</%block>
+
+<%block name="before_content">
+ % if 'main_index' in pagekind and is_frontmost_index and featured and (theme_config.get('featured_large') or theme_config.get('featured_small')):
+ % if theme_config.get('featured_on_mobile'):
+ <div class="d-block">
+ % else:
+ <div class="d-none d-md-block">
+ % endif
+ % if featured and theme_config.get('featured_large'):
+ <div class="jumbotron p-0 text-white rounded bg-dark">
+ <div class="row bootblog4-featured-jumbotron-row">
+ <div class="col-md-6 p-3 p-md-4 pr-0 h-md-250 bootblog4-featured-text">
+ <h1 class="display-4 font-italic"><a class="text-white" href="${featured[0].permalink()}">${featured[0].title()}</a></h1>
+ % if featured[0].previewimage:
+ <div class="lead my-3 mb-0">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ % if theme_config.get('featured_large_image_on_mobile'):
+ <div class="col-md-6 p-0 h-md-250 text-right">
+ % else:
+ <div class="col-md-6 p-0 h-md-250 text-right d-none d-md-block">
+ % endif
+ <img class="bootblog4-featured-large-image" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}">
+ </div>
+ % else:
+ <div class="lead my-3 mb-0">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ % endif
+ </div>
+ </div>
+ % endif
+
+ % if featured and theme_config.get('featured_small'):
+ <div class="row mb-2">
+ % if len(featured) == 1:
+ <div class="col-md-12">
+ % else:
+ <div class="col-md-6">
+ % endif
+ <div class="card flex-md-row mb-4 box-shadow h-md-250">
+ <div class="card-body d-flex flex-column align-items-start">
+ <h3 class="mb-0">
+ <a class="text-dark" href="${featured[0].permalink()}">${featured[0].title()}</a>
+ </h3>
+ % if featured[0].previewimage:
+ <div class="card-text mb-auto bootblog4-featured-text">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ <img class="card-img-right flex-auto d-none d-lg-block" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}">
+ % else:
+ <div class="card-text mb-auto bootblog4-featured-text">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ % endif
+ </div>
+ </div>
+
+ % if featured:
+ <div class="col-md-6">
+ <div class="card flex-md-row mb-4 box-shadow h-md-250">
+ <div class="card-body d-flex flex-column align-items-start">
+ <h3 class="mb-0">
+ <a class="text-dark" href="${featured[0].permalink()}">${featured[0].title()}</a>
+ </h3>
+ % if featured[0].previewimage:
+ <div class="card-text mb-auto bootblog4-featured-text">${featured[0].text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ <img class="card-img-right flex-auto d-none d-lg-block" src="${featured[0].previewimage}" alt="${featured.pop(0).title()}">
+ % else:
+ <div class="card-text mb-auto bootblog4-featured-text">${featured.pop(0).text(teaser_only=True, strip_html=theme_config.get('featured_strip_html', True))}</div>
+ </div>
+ % endif
+ </div>
+ </div>
+ % endif
+ </div>
+ %endif
+ </div>
+% endif
+</%block>
diff --git a/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt b/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt
deleted file mode 100644
index 043d497..0000000
--- a/nikola/data/themes/bootstrap3-jinja/AUTHORS.txt
+++ /dev/null
@@ -1 +0,0 @@
-Roberto Alsina <https://github.com/ralsina>
diff --git a/nikola/data/themes/bootstrap3-jinja/README.md b/nikola/data/themes/bootstrap3-jinja/README.md
deleted file mode 100644
index 10e673a..0000000
--- a/nikola/data/themes/bootstrap3-jinja/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-A theme based on Bootstrap 3.
-
-There is a variant called bootstrap3-gradients which uses an extra CSS
-file for a *visually enhanced experience* (according to Bootstrap
-developers at least). This one uses the default bootstrap3 flat look.
-
-This theme supports Bootswtach font/color schemes (unlike
-bootstrap3-gradients) through the `nikola bootswatch_theme` command.
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css
deleted file mode 120000
index 78d39af..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map
deleted file mode 120000
index 639bdc1..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css
deleted file mode 120000
index 200c765..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map
deleted file mode 120000
index fcd3722..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap-theme.min.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css
deleted file mode 120000
index 013623e..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map
deleted file mode 120000
index 8448a3d..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css
deleted file mode 120000
index 5bc6076..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map b/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map
deleted file mode 120000
index 5914aca..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/bootstrap.min.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css b/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css
deleted file mode 120000
index 5f8b3b0..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/colorbox.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/example3/colorbox.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css b/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
deleted file mode 100644
index 189ea89..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/docs.css
+++ /dev/null
@@ -1,160 +0,0 @@
-body {
- font-weight: 300;
-}
-
-a:hover,
-a:focus {
- text-decoration: none;
-}
-
-.container {
- max-width: 700px;
-}
-
-h2 {
- text-align: center;
- font-weight: 300;
-}
-
-
-/* Header
--------------------------------------------------- */
-
-.jumbotron {
- position: relative;
- font-size: 16px;
- color: #fff;
- color: rgba(255,255,255,.75);
- text-align: center;
- background-color: #b94a48;
- border-radius: 0;
-}
-.jumbotron h1,
-.jumbotron .glyphicon-ok {
- margin-bottom: 15px;
- font-weight: 300;
- letter-spacing: -1px;
- color: #fff;
-}
-.jumbotron .glyphicon-ok {
- font-size: 40px;
- line-height: 1;
-}
-.btn-outline {
- margin-top: 15px;
- margin-bottom: 15px;
- padding: 18px 24px;
- font-size: inherit;
- font-weight: 500;
- color: #fff; /* redeclare to override the `.jumbotron a` */
- background-color: transparent;
- border-color: #fff;
- border-color: rgba(255,255,255,.5);
- transition: all .1s ease-in-out;
-}
-.btn-outline:hover,
-.btn-outline:active {
- color: #b94a48;
- background-color: #fff;
- border-color: #fff;
-}
-
-.jumbotron:after {
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 10;
- display: block;
- content: "";
- height: 30px;
- background-image: -moz-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1));
- background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1));
-}
-
-.jumbotron p a,
-.jumbotron-links a {
- font-weight: 500;
- color: #fff;
- transition: all .1s ease-in-out;
-}
-.jumbotron p a:hover,
-.jumbotron-links a:hover {
- text-shadow: 0 0 10px rgba(255,255,255,.55);
-}
-
-/* Textual links */
-.jumbotron-links {
- margin-top: 15px;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- font-size: 14px;
-}
-.jumbotron-links li {
- display: inline;
-}
-.jumbotron-links li + li {
- margin-left: 20px;
-}
-
-@media (min-width: 768px) {
- .jumbotron {
- padding-top: 100px;
- padding-bottom: 100px;
- font-size: 21px;
- }
- .jumbotron h1,
- .jumbotron .glyphicon-ok {
- font-size: 50px;
- }
-}
-
-/* Steps for setup
--------------------------------------------------- */
-
-.how-to {
- padding: 50px 20px;
- border-top: 1px solid #eee;
-}
-.how-to li {
- font-size: 21px;
- line-height: 1.5;
- margin-top: 20px;
-}
-.how-to li p {
- font-size: 16px;
- color: #555;
-}
-.how-to code {
- font-size: 85%;
- color: #b94a48;
- background-color: #fcf3f2;
- word-wrap: break-word;
- white-space: normal;
-}
-
-/* Icons
--------------------------------------------------- */
-
-.the-icons {
- padding: 40px 10px;
- font-size: 20px;
- line-height: 2;
- color: #333;
- text-align: center;
-}
-.the-icons .glyphicon {
- padding-left: 15px;
- padding-right: 15px;
-}
-
-/* Footer
--------------------------------------------------- */
-
-.footer {
- padding: 50px 30px;
- color: #777;
- text-align: center;
- border-top: 1px solid #eee;
-}
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png
deleted file mode 120000
index 841a726..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/controls.png
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/example3/images/controls.png \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png
deleted file mode 100644
index 0d4475e..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomCenter.png
+++ /dev/null
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
deleted file mode 100644
index 2775eba..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomLeft.png
+++ /dev/null
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
deleted file mode 100644
index f7f5137..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderBottomRight.png
+++ /dev/null
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
deleted file mode 100644
index a2d63d1..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleLeft.png
+++ /dev/null
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
deleted file mode 100644
index fd7c3e8..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderMiddleRight.png
+++ /dev/null
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
deleted file mode 100644
index 2937a9c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopCenter.png
+++ /dev/null
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
deleted file mode 100644
index f9d458b..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopLeft.png
+++ /dev/null
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
deleted file mode 100644
index 74b8583..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/ie6/borderTopRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif
deleted file mode 120000
index b192a75..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/images/loading.gif
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/example3/images/loading.gif \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot
deleted file mode 120000
index c2dfd17..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.eot
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg
deleted file mode 120000
index 30abe9d..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.svg
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf
deleted file mode 120000
index 93e3bf3..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.ttf
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff
deleted file mode 120000
index f7595ae..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2
deleted file mode 120000
index 8c1e4d3..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/fonts/glyphicons-halflings-regular.woff2
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js b/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js
deleted file mode 120000
index 26aa1fd..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/js/bootstrap.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js
deleted file mode 120000
index c4cdf6c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/bootstrap.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/js/bootstrap.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js
deleted file mode 120000
index f83073f..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ar.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js
deleted file mode 120000
index bafc4e0..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bg.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
deleted file mode 120000
index 9b995d8..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-bn.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js
deleted file mode 120000
index a749232..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ca.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js
deleted file mode 120000
index e4a595c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-cs.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js
deleted file mode 120000
index 1e9a1d6..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-da.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js
deleted file mode 120000
index 748f53b..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-de.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js
deleted file mode 120000
index 1154fb5..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-es.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js
deleted file mode 120000
index 483e192..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-et.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js
deleted file mode 120000
index a30b13c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fa.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js
deleted file mode 120000
index 2a7e8ad..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fi.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js
deleted file mode 120000
index e359290..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-fr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js
deleted file mode 120000
index 04fa276..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js
deleted file mode 120000
index d8105ab..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-gr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js
deleted file mode 120000
index 72dddf5..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-he.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js
deleted file mode 120000
index 34aa3c0..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js
deleted file mode 120000
index a87f03c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-hu.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js
deleted file mode 120000
index 31053b8..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-id.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js
deleted file mode 120000
index aad9d22..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-it.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js
deleted file mode 120000
index 3ea27c2..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ja.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js
deleted file mode 120000
index 3e23b4a..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-kr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js
deleted file mode 120000
index 374b9bb..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lt.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js
deleted file mode 120000
index 101b476..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-lv.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js
deleted file mode 120000
index 8e14f15..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-my.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js
deleted file mode 120000
index 2d03d48..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-nl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js
deleted file mode 120000
index 9af0ba7..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-no.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js
deleted file mode 120000
index 34f8ab1..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
deleted file mode 120000
index e20bd38..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js
deleted file mode 120000
index 555f2e6..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ro.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js
deleted file mode 120000
index bac4855..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-ru.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js
deleted file mode 120000
index 65b0492..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-si.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js
deleted file mode 120000
index 99859fd..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sk.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js
deleted file mode 120000
index c4fd9d5..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js
deleted file mode 120000
index d7f26e0..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-sv.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js
deleted file mode 120000
index 86fd98f..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-tr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js
deleted file mode 120000
index 7cd1336..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-uk.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
deleted file mode 120000
index e6c5965..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
deleted file mode 120000
index bd2254c..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js
deleted file mode 100644
index 732fa3d..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/flowr.plugin.js
+++ /dev/null
@@ -1,271 +0,0 @@
-/**
- * Flowr.js - Simple jQuery plugin to emulate Flickr's justified view
- * For usage information refer to http://github.com/kalyan02/flowr-js
- *
- *
- * @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net)
- * @version: v0.1
- */
-(function($) {
- //$("#container2").css( 'border', '1px solid #ccc');
- $.fn.flowr = function(options) {
-
- $this = this;
- var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy
- var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed
- var NO_COPY_FIELDS = ['complete', 'data', 'responsive']; // these attributes will not be carried forward for append related calls
- var DEFAULTS = {
- 'data': [],
- 'padding': 5, // whats the padding between flowy items
- 'height': 240, // Minimum height an image row should take
- 'render': null, // callback function to get the tag
- 'append': false, // TODO
- 'widthAttr': 'width', // a custom data structure can specify which attribute refers to height/width
- 'heightAttr': 'height',
- 'maxScale': 1.5, // In case there is only 1 elment in last row
- 'maxWidth': this.width() - 1, // 1px is just for offset
- 'itemWidth': null, // callback function for width
- 'itemHeight': null, // callback function for height
- 'complete': null, // complete callback
- 'rowClassName': ROW_CLASS_NAME,
- 'rows': -1, // Maximum number of rows to render. -1 for no limit.
- 'responsive': true // make content responsive
- };
- var settings = $.extend(DEFAULTS, options);
-
- // If data is being appended, we already have settings
- // If we already have settings, retrieve them
- if (settings.append && $this.data('lastSettings')) {
- lastSettings = $this.data('lastSettings');
-
- // Copy over the settings from previous init
- for (attr in DEFAULTS) {
- if (NO_COPY_FIELDS.indexOf(attr) < 0 && settings[attr] == DEFAULTS[attr]) {
- settings[attr] = lastSettings[attr];
- }
- }
-
- // Check if we have an incomplete last row
- lastRow = $this.data('lastRow');
- if (lastRow.data.length > 0 && settings.maxWidth - lastRow.width > MAX_LAST_ROW_GAP) {
- // Prepend the incomplete row to newly loaded data and redraw
- lastRowData = lastSettings.data.slice(lastSettings.data.length - lastRow.data.length - 1);
- settings.data = lastRowData.concat(settings.data);
-
- // Remove the incomplete row
- // TODO: Don't reload this stuff later. Reattach to new row.
- $('.' + settings.rowClassName + ':last', $this).detach();
- } else {
- // console.log( lastRow.data.length );
- // console.log( lastRow.width );
- }
- }
-
- // only on the first initial call
- if (!settings.responsive && !settings.append)
- $this.width($this.width());
-
- // Basic sanity checks
- if (!(settings.data instanceof Array))
- return;
-
- if (typeof(settings.padding) != 'number')
- settings.padding = parseInt(settings.padding);
-
- if (typeof(settings.itemWidth) != 'function') {
- settings.itemWidth = function(data) {
- return data[settings.widthAttr];
- }
- }
-
- if (typeof(settings.itemHeight) != 'function') {
- settings.itemHeight = function(data) {
- return data[settings.heightAttr];
- }
- }
-
- // A standalone utility to calculate the item widths for a particular row
- // Returns rowWidth: width occupied & data : the items in the new row
- var utils = {
- getNextRow: function(data, settings) {
- var itemIndex = 0;
- var itemsLength = data.length;
- var lineItems = [];
- var lineWidth = 0;
- var maxWidth = settings.maxWidth;
- var paddingSize = settings.padding;
-
- // console.log( 'maxItems=' + data.length );
-
- requiredPadding = function() {
- var extraPads = arguments.length == 1 ? arguments[0] : 0;
- return (lineItems.length - 1 + extraPads) * settings.padding;
- }
-
- while (lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength)) {
- var itemData = data[itemIndex];
- var itemWidth = settings.itemWidth.call($this, itemData);
- var itemHeight = settings.itemHeight.call($this, itemData);
-
- var minHeight = settings.height;
- var minWidth = Math.floor(itemWidth * settings.height / itemHeight);
-
-
- if (minWidth > settings.maxWidth) {
- // very short+wide images like panoramas
- // show them even if ugly, as wide as possible
- minWidth = settings.maxWidth - 1 - requiredPadding(1);
- minHeight = settings.height * minHeight / minWidth;
- }
- var newLineWidth = lineWidth + minWidth;
-
- // console.log( 'lineWidth = ' + lineWidth );
- // console.log( 'newLineWidth = ' + newLineWidth );
- if (newLineWidth < settings.maxWidth) {
- lineItems.push({
- 'height': minHeight,
- 'width': minWidth,
- 'itemData': itemData
- });
-
- lineWidth += minWidth;
- itemIndex++;
- } else {
- // We'd have exceeded width. So break off to scale.
- // console.log( 'breaking off = ' + itemIndex );
- // console.log( 'leave off size = ' + lineItems.length );
- break;
- }
- } //while
-
- // Scale the size to max width
- testWidth = 0;
- if (lineWidth < settings.maxWidth) {
- var fullScaleWidth = settings.maxWidth - requiredPadding() - 10;
- var currScaleWidth = lineWidth;
- var scaleFactor = fullScaleWidth / currScaleWidth;
- if (scaleFactor > settings.maxScale)
- scaleFactor = 1;
-
- var newHeight = Math.round(settings.height * scaleFactor);
- for (i = 0; i < lineItems.length; i++) {
- var lineItem = lineItems[i];
- lineItem.width = Math.floor(lineItem.width * scaleFactor);
- lineItem.height = newHeight;
-
- testWidth += lineItem.width;
- }
- }
-
- return {
- data: lineItems,
- width: testWidth + requiredPadding()
- };
- }, //getNextRow
- reorderContent: function() {
- /*
- TODO: optimize for faster resizing by reusing dom objects instead of killing the dom
- */
- var _initialWidth = $this.data('width');
- var _newWidth = $this.width();
- var _change = _initialWidth - _newWidth;
-
- if (_initialWidth != _newWidth) {
- $this.html('');
- var _settings = $this.data('lastSettings');
- _settings.data = $this.data('data');
- _settings.maxWidth = $this.width() - 1;
- $this.flowr(_settings);
- }
- }
- } //utils
-
- // If the responsive var is set to true then listen for resize method
- // and prevent resizing from happening twice if responsive is set again during append phase!
- if (settings.responsive && !$this.data('__responsive')) {
- $(window).resize(function() {
- initialWidth = $this.data('width');
- newWidth = $this.width();
-
- //initiate resize
- if (initialWidth != newWidth) {
- var task_id = $this.data('task_id');
- if (task_id) {
- task_id = clearTimeout(task_id);
- task_id = null;
- }
- task_id = setTimeout(utils.reorderContent, 80);
- $this.data('task_id', task_id);
- }
- });
- $this.data('__responsive', true);
- }
-
-
- return this.each(function() {
-
- // Get a copy of original data. 1 level deep copy is sufficient.
- var data = settings.data.slice(0);
- var rowData = null;
- var currentRow = 0;
- var currentItem = 0;
-
- // Store all the data
- var allData = [];
- for (i = 0; i < data.length; i++) {
- allData.push(data[i]);
- }
- $this.data('data', allData);
-
- // While we have a new row
- while ((rowData = utils.getNextRow(data, settings)) != null && rowData.data.length > 0) {
- if (settings.rows > 0 && currentRow >= settings.rows)
- break;
- // remove the number of elements in the new row from the top of data stack
- data.splice(0, rowData.data.length);
-
- // Create a new row div, add class, append the htmls and insert the flowy items
- var $row = $('<div>').addClass(settings.rowClassName);
- var slack = $this[0].clientWidth - rowData.width - 2 * settings.padding
- for (i = 0; i < rowData.data.length; i++) {
- var displayData = rowData.data[i];
- // Get the HTML object from custom render function passed as argument
- var displayObject = settings.render.call($this, displayData);
- displayObject = $(displayObject);
- extraw = Math.floor(slack/rowData.data.length)
- if (i == 0) {
- extraw += slack % rowData.data.length
- }
- // Set some basic stuff
- displayObject
- .css('width', displayData.width + extraw)
- .css('height', displayData.height)
- .css('margin-bottom', settings.padding + "px")
- .css('margin-left', i == 0 ? '0' : settings.padding + "px"); //TODO:Refactor
- $row.append(displayObject);
-
- currentItem++;
- }
- $this.append($row);
- // console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width );
-
- currentRow++;
- $this.data('lastRow', rowData);
- }
- // store the current state of settings and the items in last row
- // we'll need this info when we append more items
- $this.data('lastSettings', settings);
-
- // onComplete callback
- // pass back info about list of rows and items rendered
- if (typeof(settings.complete) == 'function') {
- var completeData = {
- renderedRows: currentRow,
- renderedItems: currentItem
- }
- settings.complete.call($this, completeData);
- }
- });
- };
-
-})(jQuery);
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js
deleted file mode 120000
index 9e40fd4..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox-min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/jquery.colorbox-min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js
deleted file mode 120000
index 5ee7a90..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.colorbox.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js
deleted file mode 120000
index 966173b..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js
deleted file mode 120000
index 5c080da..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map b/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map
deleted file mode 120000
index 7e2c217..0000000
--- a/nikola/data/themes/bootstrap3-jinja/assets/js/jquery.min.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.min.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/bundles b/nikola/data/themes/bootstrap3-jinja/bundles
deleted file mode 120000
index 8cb3e06..0000000
--- a/nikola/data/themes/bootstrap3-jinja/bundles
+++ /dev/null
@@ -1 +0,0 @@
-../bootstrap3/bundles \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/engine b/nikola/data/themes/bootstrap3-jinja/engine
deleted file mode 100644
index 6f04b30..0000000
--- a/nikola/data/themes/bootstrap3-jinja/engine
+++ /dev/null
@@ -1 +0,0 @@
-jinja
diff --git a/nikola/data/themes/bootstrap3-jinja/parent b/nikola/data/themes/bootstrap3-jinja/parent
deleted file mode 100644
index e9ed660..0000000
--- a/nikola/data/themes/bootstrap3-jinja/parent
+++ /dev/null
@@ -1 +0,0 @@
-base-jinja
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl
deleted file mode 100644
index 4ce46d0..0000000
--- a/nikola/data/themes/bootstrap3-jinja/templates/base.tmpl
+++ /dev/null
@@ -1,94 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% import 'base_helper.tmpl' as base with context %}
-{% import 'annotation_helper.tmpl' as notes with context %}
-{{ set_locale(lang) }}
-{{ base.html_headstart() }}
-{% block extra_head %}
-{# Leave this block alone. #}
-{% endblock %}
-{{ template_hooks['extra_head']() }}
-</head>
-<body>
-<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
-
-<!-- Menubar -->
-
-<nav class="navbar navbar-inverse navbar-static-top">
- <div class="container"><!-- This keeps the margins nice -->
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false">
- <span class="sr-only">{{ messages("Toggle navigation") }}</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href="{{ abs_link(_link("root", None, lang)) }}">
- {% if logo_url %}
- <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo">
- {% endif %}
-
- {% if show_blog_title %}
- <span id="blog-title">{{ blog_title|e }}</span>
- {% endif %}
- </a>
- </div><!-- /.navbar-header -->
- <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false">
- <ul class="nav navbar-nav">
- {{ base.html_navigation_links() }}
- {{ template_hooks['menu']() }}
- </ul>
- {% if search_form %}
- {{ search_form }}
- {% endif %}
-
- <ul class="nav navbar-nav navbar-right">
- {% block belowtitle %}
- {% if translations|length > 1 %}
- <li>{{ base.html_translations() }}</li>
- {% endif %}
- {% endblock %}
- {% if show_sourcelink %}
- {% block sourcelink %}{% endblock %}
- {% endif %}
- {{ template_hooks['menu_alt']() }}
- </ul>
- </div><!-- /.navbar-collapse -->
- </div><!-- /.container -->
-</nav>
-
-<!-- End of Menubar -->
-
-<div class="container" id="content" role="main">
- <div class="body-content">
- <!--Body content-->
- <div class="row">
- {{ template_hooks['page_header']() }}
- {% block content %}{% endblock %}
- </div>
- <!--End of body content-->
-
- <footer id="footer">
- {{ content_footer }}
- {{ template_hooks['page_footer']() }}
- </footer>
- </div>
-</div>
-
-{{ base.late_load_js() }}
- <script>$('a.image-reference:not(.islink) img:not(.islink)').parent().colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
- <!-- fancy dates -->
- <script>
- moment.locale("{{ momentjs_locales[lang] }}");
- fancydates({{ date_fanciness }}, {{ js_date_format }});
- </script>
- <!-- end fancy dates -->
- {% block extra_js %}{% endblock %}
- {% if annotations and post and not post.meta('noannotations') %}
- {{ notes.code() }}
- {% elif not annotations and post and post.meta('annotations') %}
- {{ notes.code() }}
- {% endif %}
-{{ body_end }}
-{{ template_hooks['body_end']() }}
-</body>
-</html>
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl
deleted file mode 100644
index 1d1802f..0000000
--- a/nikola/data/themes/bootstrap3-jinja/templates/base_helper.tmpl
+++ /dev/null
@@ -1,188 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-
-{% import 'annotation_helper.tmpl' as notes with context %}
-{% macro html_headstart() %}
-<!DOCTYPE html>
-<html
-
-{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook') %}
-prefix='
-{% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) %}
-og: http://ogp.me/ns#
-{% endif %}
-{% if use_open_graph %}
-article: http://ogp.me/ns/article#
-{% endif %}
-{% if comment_system == 'facebook' %}
-fb: http://ogp.me/ns/fb#
-{% endif %}
-'
-{% endif %}
-
-{% if is_rtl %}
-dir="rtl"
-{% endif %}
-
-lang="{{ lang }}">
- <head>
- <meta charset="utf-8">
- {% if use_base_tag %}
- <base href="{{ abs_link(permalink) }}">
- {% endif %}
- {% if description %}
- <meta name="description" content="{{ description|e }}">
- {% endif %}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- {% if title == blog_title %}
- <title>{{ blog_title|e }}</title>
- {% else %}
- <title>{{ title|e }} | {{ blog_title|e }}</title>
- {% endif %}
-
- {{ html_stylesheets() }}
- <meta content="{{ theme_color }}" name="theme-color">
- {{ html_feedlinks() }}
- <link rel="canonical" href="{{ abs_link(permalink) }}">
-
- {% if favicons %}
- {% for name, file, size in favicons %}
- <link rel="{{ name }}" href="{{ file }}" sizes="{{ size }}"/>
- {% endfor %}
- {% endif %}
-
- {% if comment_system == 'facebook' %}
- <meta property="fb:app_id" content="{{ comment_system_id }}">
- {% endif %}
-
- {% if prevlink %}
- <link rel="prev" href="{{ prevlink }}" type="text/html">
- {% endif %}
- {% if nextlink %}
- <link rel="next" href="{{ nextlink }}" type="text/html">
- {% endif %}
-
- {{ mathjax_config }}
- {% if use_cdn %}
- <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
- {% else %}
- <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang) }}"></script><![endif]-->
- {% endif %}
-
- {{ extra_head_data }}
-{% endmacro %}
-
-{% macro late_load_js() %}
- {% if use_bundles %}
- {% if use_cdn %}
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
-
- <script src="/assets/js/all.js"></script>
- {% else %}
- <script src="/assets/js/all-nocdn.js"></script>
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
- {% else %}
- <script src="/assets/js/jquery.min.js"></script>
- <script src="/assets/js/bootstrap.min.js"></script>
- <script src="/assets/js/moment-with-locales.min.js"></script>
- <script src="/assets/js/fancydates.js"></script>
- {% endif %}
- <script src="/assets/js/jquery.colorbox-min.js"></script>
- {% endif %}
- {% if colorbox_locales[lang] %}
- <script src="/assets/js/colorbox-i18n/jquery.colorbox-{{ colorbox_locales[lang] }}.js"></script>
- {% endif %}
- {{ social_buttons_code }}
-{% endmacro %}
-
-
-{% macro html_stylesheets() %}
- {% if use_bundles %}
- {% if use_cdn %}
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- {% else %}
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
- {% else %}
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- {% endif %}
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
- {% if has_custom_css %}
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% endif %}
- {% if needs_ipython_css %}
- <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% if annotations and post and not post.meta('noannotations') %}
- {{ notes.css() }}
- {% elif not annotations and post and post.meta('annotations') %}
- {{ notes.css() }}
- {% endif %}
-{% endmacro %}
-
-{% macro html_navigation_links() %}
- {% for url, text in navigation_links[lang] %}
- {% if isinstance(url, tuple) %}
- <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }} <b class="caret"></b></a>
- <ul class="dropdown-menu">
- {% for suburl, text in url %}
- {% if rel_link(permalink, suburl) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
- {% else %}
- <li><a href="{{ suburl }}">{{ text }}</a>
- {% endif %}
- {% endfor %}
- </ul>
- {% else %}
- {% if rel_link(permalink, url) == "#" %}
- <li class="active"><a href="{{ permalink }}">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
- {% else %}
- <li><a href="{{ url }}">{{ text }}</a>
- {% endif %}
- {% endif %}
- {% endfor %}
-{% endmacro %}
-
-{% macro html_feedlinks() %}
- {% if rss_link %}
- {{ rss_link }}
- {% elif generate_rss %}
- {% if translations|length > 1 %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/rss+xml" title="RSS ({{ language }})" href="{{ _link('rss', None, language) }}">
- {% endfor %}
- {% else %}
- <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ _link('rss', None) }}">
- {% endif %}
- {% endif %}
- {% if generate_atom %}
- {% if translations|length > 1 %}
- {% for language in translations|sort %}
- <link rel="alternate" type="application/atom+xml" title="Atom ({{ language }})" href="{{ _link('index_atom', None, language) }}">
- {% endfor %}
- {% else %}
- <link rel="alternate" type="application/atom+xml" title="Atom" href="{{ _link('index_atom', None) }}">
- {% endif %}
- {% endif %}
-{% endmacro %}
-
-{% macro html_translations() %}
- {% for langname in translations|sort %}
- {% if langname != lang %}
- <li><a href="{{ abs_link(_link("root", None, langname)) }}" rel="alternate" hreflang="{{ langname }}">{{ messages("LANGUAGE", langname) }}</a></li>
- {% endif %}
- {% endfor %}
-{% endmacro %}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl
deleted file mode 100644
index cd9a5ed..0000000
--- a/nikola/data/themes/bootstrap3-jinja/templates/gallery.tmpl
+++ /dev/null
@@ -1,95 +0,0 @@
-{# -*- coding: utf-8 -*- #}
-{% extends 'base.tmpl' %}
-{% import 'comments_helper.tmpl' as comments with context %}
-{% import 'crumbs.tmpl' as ui with context %}
-{% block sourcelink %}{% endblock %}
-
-{% block content %}
- {{ ui.bar(crumbs) }}
- {% if title %}
- <h1>{{ title|e }}</h1>
- {% endif %}
- {% if post %}
- <p>
- {{ post.text() }}
- </p>
- {% endif %}
- {% if folders %}
- <ul>
- {% for folder, ftitle in folders %}
- <li><a href="{{ folder }}"><i class="glyphicon glyphicon-folder-open"></i>&nbsp;{{ ftitle|e }}</a></li>
- {% endfor %}
- </ul>
- {% endif %}
-
-<div id="gallery_container"></div>
-{% if photo_array %}
-<noscript>
-<ul class="thumbnails">
- {% for image in photo_array %}
- <li><a href="{{ image['url'] }}" class="thumbnail image-reference" title="{{ image['title']|e }}">
- <img src="{{ image['url_thumb'] }}" alt="{{ image['title']|e }}" /></a>
- {% endfor %}
-</ul>
-</noscript>
-{% endif %}
-{% if site_has_comments and enable_comments %}
-{{ comments.comment_form(None, permalink, title) }}
-{% endif %}
-{% endblock %}
-
-{% block extra_head %}
-{{ super() }}
-<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
-<style type="text/css">
- .image-block {
- display: inline-block;
- }
- .flowr_row {
- width: 100%;
- }
- </style>
-{% endblock %}
-
-
-{% block extra_js %}
-<script src="/assets/js/flowr.plugin.js"></script>
-<script>
-jsonContent = {{ photo_array_json }};
-$("#gallery_container").flowr({
- data : jsonContent,
- height : {{ thumbnail_size }}*.6,
- padding: 5,
- rows: -1,
- render : function(params) {
- // Just return a div, string or a dom object, anything works fine
- img = $("<img />").attr({
- 'src': params.itemData.url_thumb,
- 'width' : params.width,
- 'height' : params.height
- }).css('max-width', '100%');
- link = $( "<a></a>").attr({
- 'href': params.itemData.url,
- 'class': 'image-reference'
- });
- div = $("<div />").addClass('image-block').attr({
- 'title': params.itemData.title,
- 'data-toggle': "tooltip",
- });
- link.append(img);
- div.append(link);
- div.hover(div.tooltip());
- return div;
- },
- itemWidth : function(data) { return data.size.w; },
- itemHeight : function(data) { return data.size.h; },
- complete : function(params) {
- if( jsonContent.length > params.renderedItems ) {
- nextRenderList = jsonContent.slice( params.renderedItems );
- }
- }
- });
-$("a.image-reference").colorbox({rel:"gal", maxWidth:"100%",maxHeight:"100%",scalePhotos:true});
-$('a.image-reference[href="'+window.location.hash.substring(1,1000)+'"]').click();
-</script>
-{% endblock %}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl b/nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl
deleted file mode 100644
index 342ed27..0000000
--- a/nikola/data/themes/bootstrap3-jinja/templates/slides.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-{% block content %}
-<div id="{{ carousel_id }}" class="carousel slide">
- <ol class="carousel-indicators">
- {% for i in range(slides_content|length) %}
- {% if i == 0 %}
- <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}" class="active"></li>
- {% else %}
- <li data-target="#{{ carousel_id }}" data-slide-to="{{ i }}"></li>
- {% endif %}
- {% endfor %}
- </ol>
- <div class="carousel-inner">
- {% for i, image in enumerate(slides_content) %}
- {% if i == 0 %}
- <div class="item active"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div>
- {% else %}
- <div class="item"><img src="{{ image }}" alt="" style="margin: 0 auto 0 auto;"></div>
- {% endif %}
- {% endfor %}
- </div>
- <a class="left carousel-control" href="#{{ carousel_id }}" data-slide="prev"><span class="icon-prev"></span></a>
- <a class="right carousel-control" href="#{{ carousel_id }}" data-slide="next"><span class="icon-next"></span></a>
-</div>
-{% endblock %}
diff --git a/nikola/data/themes/bootstrap3/README.md b/nikola/data/themes/bootstrap3/README.md
deleted file mode 100644
index 10e673a..0000000
--- a/nikola/data/themes/bootstrap3/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-A theme based on Bootstrap 3.
-
-There is a variant called bootstrap3-gradients which uses an extra CSS
-file for a *visually enhanced experience* (according to Bootstrap
-developers at least). This one uses the default bootstrap3 flat look.
-
-This theme supports Bootswtach font/color schemes (unlike
-bootstrap3-gradients) through the `nikola bootswatch_theme` command.
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css
deleted file mode 120000
index 78d39af..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map
deleted file mode 120000
index 639bdc1..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css
deleted file mode 120000
index 200c765..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map
deleted file mode 120000
index fcd3722..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap-theme.min.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap-theme.min.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap.css
deleted file mode 120000
index 013623e..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map
deleted file mode 120000
index 8448a3d..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css
deleted file mode 120000
index 5bc6076..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map b/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map
deleted file mode 120000
index 5914aca..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/bootstrap.min.css.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/css/bootstrap.min.css.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/colorbox.css b/nikola/data/themes/bootstrap3/assets/css/colorbox.css
deleted file mode 120000
index 5f8b3b0..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/colorbox.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/example3/colorbox.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/docs.css b/nikola/data/themes/bootstrap3/assets/css/docs.css
deleted file mode 100644
index 189ea89..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/docs.css
+++ /dev/null
@@ -1,160 +0,0 @@
-body {
- font-weight: 300;
-}
-
-a:hover,
-a:focus {
- text-decoration: none;
-}
-
-.container {
- max-width: 700px;
-}
-
-h2 {
- text-align: center;
- font-weight: 300;
-}
-
-
-/* Header
--------------------------------------------------- */
-
-.jumbotron {
- position: relative;
- font-size: 16px;
- color: #fff;
- color: rgba(255,255,255,.75);
- text-align: center;
- background-color: #b94a48;
- border-radius: 0;
-}
-.jumbotron h1,
-.jumbotron .glyphicon-ok {
- margin-bottom: 15px;
- font-weight: 300;
- letter-spacing: -1px;
- color: #fff;
-}
-.jumbotron .glyphicon-ok {
- font-size: 40px;
- line-height: 1;
-}
-.btn-outline {
- margin-top: 15px;
- margin-bottom: 15px;
- padding: 18px 24px;
- font-size: inherit;
- font-weight: 500;
- color: #fff; /* redeclare to override the `.jumbotron a` */
- background-color: transparent;
- border-color: #fff;
- border-color: rgba(255,255,255,.5);
- transition: all .1s ease-in-out;
-}
-.btn-outline:hover,
-.btn-outline:active {
- color: #b94a48;
- background-color: #fff;
- border-color: #fff;
-}
-
-.jumbotron:after {
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 10;
- display: block;
- content: "";
- height: 30px;
- background-image: -moz-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1));
- background-image: -webkit-linear-gradient(rgba(0, 0, 0, 0), rgba(0,0,0,.1));
-}
-
-.jumbotron p a,
-.jumbotron-links a {
- font-weight: 500;
- color: #fff;
- transition: all .1s ease-in-out;
-}
-.jumbotron p a:hover,
-.jumbotron-links a:hover {
- text-shadow: 0 0 10px rgba(255,255,255,.55);
-}
-
-/* Textual links */
-.jumbotron-links {
- margin-top: 15px;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- font-size: 14px;
-}
-.jumbotron-links li {
- display: inline;
-}
-.jumbotron-links li + li {
- margin-left: 20px;
-}
-
-@media (min-width: 768px) {
- .jumbotron {
- padding-top: 100px;
- padding-bottom: 100px;
- font-size: 21px;
- }
- .jumbotron h1,
- .jumbotron .glyphicon-ok {
- font-size: 50px;
- }
-}
-
-/* Steps for setup
--------------------------------------------------- */
-
-.how-to {
- padding: 50px 20px;
- border-top: 1px solid #eee;
-}
-.how-to li {
- font-size: 21px;
- line-height: 1.5;
- margin-top: 20px;
-}
-.how-to li p {
- font-size: 16px;
- color: #555;
-}
-.how-to code {
- font-size: 85%;
- color: #b94a48;
- background-color: #fcf3f2;
- word-wrap: break-word;
- white-space: normal;
-}
-
-/* Icons
--------------------------------------------------- */
-
-.the-icons {
- padding: 40px 10px;
- font-size: 20px;
- line-height: 2;
- color: #333;
- text-align: center;
-}
-.the-icons .glyphicon {
- padding-left: 15px;
- padding-right: 15px;
-}
-
-/* Footer
--------------------------------------------------- */
-
-.footer {
- padding: 50px 30px;
- color: #777;
- text-align: center;
- border-top: 1px solid #eee;
-}
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/controls.png b/nikola/data/themes/bootstrap3/assets/css/images/controls.png
deleted file mode 120000
index 841a726..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/controls.png
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/example3/images/controls.png \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png
deleted file mode 100644
index 0d4475e..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomCenter.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png
deleted file mode 100644
index 2775eba..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png
deleted file mode 100644
index f7f5137..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderBottomRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png
deleted file mode 100644
index a2d63d1..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png
deleted file mode 100644
index fd7c3e8..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderMiddleRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png
deleted file mode 100644
index 2937a9c..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopCenter.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png
deleted file mode 100644
index f9d458b..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png b/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png
deleted file mode 100644
index 74b8583..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/ie6/borderTopRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/bootstrap3/assets/css/images/loading.gif b/nikola/data/themes/bootstrap3/assets/css/images/loading.gif
deleted file mode 120000
index b192a75..0000000
--- a/nikola/data/themes/bootstrap3/assets/css/images/loading.gif
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/example3/images/loading.gif \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot
deleted file mode 120000
index c2dfd17..0000000
--- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.eot
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg
deleted file mode 120000
index 30abe9d..0000000
--- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.svg
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf
deleted file mode 120000
index 93e3bf3..0000000
--- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.ttf
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff
deleted file mode 120000
index f7595ae..0000000
--- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2 b/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2
deleted file mode 120000
index 8c1e4d3..0000000
--- a/nikola/data/themes/bootstrap3/assets/fonts/glyphicons-halflings-regular.woff2
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/bootstrap.js b/nikola/data/themes/bootstrap3/assets/js/bootstrap.js
deleted file mode 120000
index 26aa1fd..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/bootstrap.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/js/bootstrap.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js
deleted file mode 120000
index c4cdf6c..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/bootstrap.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/bootstrap/dist/js/bootstrap.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js
deleted file mode 120000
index f83073f..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ar.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ar.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js
deleted file mode 120000
index bafc4e0..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bg.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bg.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js
deleted file mode 120000
index 9b995d8..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-bn.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-bn.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js
deleted file mode 120000
index a749232..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ca.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ca.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js
deleted file mode 120000
index e4a595c..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-cs.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-cs.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js
deleted file mode 120000
index 1e9a1d6..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-da.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-da.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js
deleted file mode 120000
index 748f53b..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-de.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-de.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js
deleted file mode 120000
index 1154fb5..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-es.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-es.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js
deleted file mode 120000
index 483e192..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-et.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-et.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js
deleted file mode 120000
index a30b13c..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fa.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fa.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js
deleted file mode 120000
index 2a7e8ad..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fi.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fi.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js
deleted file mode 120000
index e359290..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-fr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-fr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js
deleted file mode 120000
index 04fa276..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js
deleted file mode 120000
index d8105ab..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-gr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-gr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js
deleted file mode 120000
index 72dddf5..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-he.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-he.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js
deleted file mode 120000
index 34aa3c0..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js
deleted file mode 120000
index a87f03c..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-hu.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-hu.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js
deleted file mode 120000
index 31053b8..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-id.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-id.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js
deleted file mode 120000
index aad9d22..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-it.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-it.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js
deleted file mode 120000
index 3ea27c2..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ja.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ja.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js
deleted file mode 120000
index 3e23b4a..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-kr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-kr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js
deleted file mode 120000
index 374b9bb..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lt.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lt.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js
deleted file mode 120000
index 101b476..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-lv.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-lv.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js
deleted file mode 120000
index 8e14f15..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-my.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-my.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js
deleted file mode 120000
index 2d03d48..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-nl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-nl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js
deleted file mode 120000
index 9af0ba7..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-no.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-no.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js
deleted file mode 120000
index 34f8ab1..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pl.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pl.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
deleted file mode 120000
index e20bd38..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-pt-BR.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-pt-BR.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js
deleted file mode 120000
index 555f2e6..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ro.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ro.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js
deleted file mode 120000
index bac4855..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-ru.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-ru.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js
deleted file mode 120000
index 65b0492..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-si.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-si.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js
deleted file mode 120000
index 99859fd..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sk.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sk.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js
deleted file mode 120000
index c4fd9d5..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js
deleted file mode 120000
index d7f26e0..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-sv.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-sv.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js
deleted file mode 120000
index 86fd98f..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-tr.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-tr.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js
deleted file mode 120000
index 7cd1336..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-uk.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-uk.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
deleted file mode 120000
index e6c5965..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-CN.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-CN.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js b/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
deleted file mode 120000
index bd2254c..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/colorbox-i18n/jquery.colorbox-zh-TW.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../../bower_components/jquery-colorbox/i18n/jquery.colorbox-zh-TW.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js
deleted file mode 100644
index 732fa3d..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/flowr.plugin.js
+++ /dev/null
@@ -1,271 +0,0 @@
-/**
- * Flowr.js - Simple jQuery plugin to emulate Flickr's justified view
- * For usage information refer to http://github.com/kalyan02/flowr-js
- *
- *
- * @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net)
- * @version: v0.1
- */
-(function($) {
- //$("#container2").css( 'border', '1px solid #ccc');
- $.fn.flowr = function(options) {
-
- $this = this;
- var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy
- var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed
- var NO_COPY_FIELDS = ['complete', 'data', 'responsive']; // these attributes will not be carried forward for append related calls
- var DEFAULTS = {
- 'data': [],
- 'padding': 5, // whats the padding between flowy items
- 'height': 240, // Minimum height an image row should take
- 'render': null, // callback function to get the tag
- 'append': false, // TODO
- 'widthAttr': 'width', // a custom data structure can specify which attribute refers to height/width
- 'heightAttr': 'height',
- 'maxScale': 1.5, // In case there is only 1 elment in last row
- 'maxWidth': this.width() - 1, // 1px is just for offset
- 'itemWidth': null, // callback function for width
- 'itemHeight': null, // callback function for height
- 'complete': null, // complete callback
- 'rowClassName': ROW_CLASS_NAME,
- 'rows': -1, // Maximum number of rows to render. -1 for no limit.
- 'responsive': true // make content responsive
- };
- var settings = $.extend(DEFAULTS, options);
-
- // If data is being appended, we already have settings
- // If we already have settings, retrieve them
- if (settings.append && $this.data('lastSettings')) {
- lastSettings = $this.data('lastSettings');
-
- // Copy over the settings from previous init
- for (attr in DEFAULTS) {
- if (NO_COPY_FIELDS.indexOf(attr) < 0 && settings[attr] == DEFAULTS[attr]) {
- settings[attr] = lastSettings[attr];
- }
- }
-
- // Check if we have an incomplete last row
- lastRow = $this.data('lastRow');
- if (lastRow.data.length > 0 && settings.maxWidth - lastRow.width > MAX_LAST_ROW_GAP) {
- // Prepend the incomplete row to newly loaded data and redraw
- lastRowData = lastSettings.data.slice(lastSettings.data.length - lastRow.data.length - 1);
- settings.data = lastRowData.concat(settings.data);
-
- // Remove the incomplete row
- // TODO: Don't reload this stuff later. Reattach to new row.
- $('.' + settings.rowClassName + ':last', $this).detach();
- } else {
- // console.log( lastRow.data.length );
- // console.log( lastRow.width );
- }
- }
-
- // only on the first initial call
- if (!settings.responsive && !settings.append)
- $this.width($this.width());
-
- // Basic sanity checks
- if (!(settings.data instanceof Array))
- return;
-
- if (typeof(settings.padding) != 'number')
- settings.padding = parseInt(settings.padding);
-
- if (typeof(settings.itemWidth) != 'function') {
- settings.itemWidth = function(data) {
- return data[settings.widthAttr];
- }
- }
-
- if (typeof(settings.itemHeight) != 'function') {
- settings.itemHeight = function(data) {
- return data[settings.heightAttr];
- }
- }
-
- // A standalone utility to calculate the item widths for a particular row
- // Returns rowWidth: width occupied & data : the items in the new row
- var utils = {
- getNextRow: function(data, settings) {
- var itemIndex = 0;
- var itemsLength = data.length;
- var lineItems = [];
- var lineWidth = 0;
- var maxWidth = settings.maxWidth;
- var paddingSize = settings.padding;
-
- // console.log( 'maxItems=' + data.length );
-
- requiredPadding = function() {
- var extraPads = arguments.length == 1 ? arguments[0] : 0;
- return (lineItems.length - 1 + extraPads) * settings.padding;
- }
-
- while (lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength)) {
- var itemData = data[itemIndex];
- var itemWidth = settings.itemWidth.call($this, itemData);
- var itemHeight = settings.itemHeight.call($this, itemData);
-
- var minHeight = settings.height;
- var minWidth = Math.floor(itemWidth * settings.height / itemHeight);
-
-
- if (minWidth > settings.maxWidth) {
- // very short+wide images like panoramas
- // show them even if ugly, as wide as possible
- minWidth = settings.maxWidth - 1 - requiredPadding(1);
- minHeight = settings.height * minHeight / minWidth;
- }
- var newLineWidth = lineWidth + minWidth;
-
- // console.log( 'lineWidth = ' + lineWidth );
- // console.log( 'newLineWidth = ' + newLineWidth );
- if (newLineWidth < settings.maxWidth) {
- lineItems.push({
- 'height': minHeight,
- 'width': minWidth,
- 'itemData': itemData
- });
-
- lineWidth += minWidth;
- itemIndex++;
- } else {
- // We'd have exceeded width. So break off to scale.
- // console.log( 'breaking off = ' + itemIndex );
- // console.log( 'leave off size = ' + lineItems.length );
- break;
- }
- } //while
-
- // Scale the size to max width
- testWidth = 0;
- if (lineWidth < settings.maxWidth) {
- var fullScaleWidth = settings.maxWidth - requiredPadding() - 10;
- var currScaleWidth = lineWidth;
- var scaleFactor = fullScaleWidth / currScaleWidth;
- if (scaleFactor > settings.maxScale)
- scaleFactor = 1;
-
- var newHeight = Math.round(settings.height * scaleFactor);
- for (i = 0; i < lineItems.length; i++) {
- var lineItem = lineItems[i];
- lineItem.width = Math.floor(lineItem.width * scaleFactor);
- lineItem.height = newHeight;
-
- testWidth += lineItem.width;
- }
- }
-
- return {
- data: lineItems,
- width: testWidth + requiredPadding()
- };
- }, //getNextRow
- reorderContent: function() {
- /*
- TODO: optimize for faster resizing by reusing dom objects instead of killing the dom
- */
- var _initialWidth = $this.data('width');
- var _newWidth = $this.width();
- var _change = _initialWidth - _newWidth;
-
- if (_initialWidth != _newWidth) {
- $this.html('');
- var _settings = $this.data('lastSettings');
- _settings.data = $this.data('data');
- _settings.maxWidth = $this.width() - 1;
- $this.flowr(_settings);
- }
- }
- } //utils
-
- // If the responsive var is set to true then listen for resize method
- // and prevent resizing from happening twice if responsive is set again during append phase!
- if (settings.responsive && !$this.data('__responsive')) {
- $(window).resize(function() {
- initialWidth = $this.data('width');
- newWidth = $this.width();
-
- //initiate resize
- if (initialWidth != newWidth) {
- var task_id = $this.data('task_id');
- if (task_id) {
- task_id = clearTimeout(task_id);
- task_id = null;
- }
- task_id = setTimeout(utils.reorderContent, 80);
- $this.data('task_id', task_id);
- }
- });
- $this.data('__responsive', true);
- }
-
-
- return this.each(function() {
-
- // Get a copy of original data. 1 level deep copy is sufficient.
- var data = settings.data.slice(0);
- var rowData = null;
- var currentRow = 0;
- var currentItem = 0;
-
- // Store all the data
- var allData = [];
- for (i = 0; i < data.length; i++) {
- allData.push(data[i]);
- }
- $this.data('data', allData);
-
- // While we have a new row
- while ((rowData = utils.getNextRow(data, settings)) != null && rowData.data.length > 0) {
- if (settings.rows > 0 && currentRow >= settings.rows)
- break;
- // remove the number of elements in the new row from the top of data stack
- data.splice(0, rowData.data.length);
-
- // Create a new row div, add class, append the htmls and insert the flowy items
- var $row = $('<div>').addClass(settings.rowClassName);
- var slack = $this[0].clientWidth - rowData.width - 2 * settings.padding
- for (i = 0; i < rowData.data.length; i++) {
- var displayData = rowData.data[i];
- // Get the HTML object from custom render function passed as argument
- var displayObject = settings.render.call($this, displayData);
- displayObject = $(displayObject);
- extraw = Math.floor(slack/rowData.data.length)
- if (i == 0) {
- extraw += slack % rowData.data.length
- }
- // Set some basic stuff
- displayObject
- .css('width', displayData.width + extraw)
- .css('height', displayData.height)
- .css('margin-bottom', settings.padding + "px")
- .css('margin-left', i == 0 ? '0' : settings.padding + "px"); //TODO:Refactor
- $row.append(displayObject);
-
- currentItem++;
- }
- $this.append($row);
- // console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width );
-
- currentRow++;
- $this.data('lastRow', rowData);
- }
- // store the current state of settings and the items in last row
- // we'll need this info when we append more items
- $this.data('lastSettings', settings);
-
- // onComplete callback
- // pass back info about list of rows and items rendered
- if (typeof(settings.complete) == 'function') {
- var completeData = {
- renderedRows: currentRow,
- renderedItems: currentItem
- }
- settings.complete.call($this, completeData);
- }
- });
- };
-
-})(jQuery);
diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js
deleted file mode 120000
index 9e40fd4..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox-min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/jquery.colorbox-min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js b/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js
deleted file mode 120000
index 5ee7a90..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/jquery.colorbox.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery-colorbox/jquery.colorbox.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.js b/nikola/data/themes/bootstrap3/assets/js/jquery.js
deleted file mode 120000
index 966173b..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/jquery.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.min.js b/nikola/data/themes/bootstrap3/assets/js/jquery.min.js
deleted file mode 120000
index 5c080da..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/jquery.min.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/js/jquery.min.map b/nikola/data/themes/bootstrap3/assets/js/jquery.min.map
deleted file mode 120000
index 7e2c217..0000000
--- a/nikola/data/themes/bootstrap3/assets/js/jquery.min.map
+++ /dev/null
@@ -1 +0,0 @@
-../../../../../../bower_components/jquery/dist/jquery.min.map \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/bundles b/nikola/data/themes/bootstrap3/bundles
deleted file mode 100644
index 8bdc591..0000000
--- a/nikola/data/themes/bootstrap3/bundles
+++ /dev/null
@@ -1,4 +0,0 @@
-assets/css/all-nocdn.css=bootstrap.css,rst.css,code.css,colorbox.css,theme.css,custom.css
-assets/css/all.css=rst.css,code.css,colorbox.css,theme.css,custom.css
-assets/js/all-nocdn.js=jquery.min.js,bootstrap.min.js,jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js
-assets/js/all.js=jquery.colorbox-min.js,moment-with-locales.min.js,fancydates.js
diff --git a/nikola/data/themes/bootstrap3/engine b/nikola/data/themes/bootstrap3/engine
deleted file mode 100644
index 2951cdd..0000000
--- a/nikola/data/themes/bootstrap3/engine
+++ /dev/null
@@ -1 +0,0 @@
-mako
diff --git a/nikola/data/themes/bootstrap3/parent b/nikola/data/themes/bootstrap3/parent
deleted file mode 100644
index df967b9..0000000
--- a/nikola/data/themes/bootstrap3/parent
+++ /dev/null
@@ -1 +0,0 @@
-base
diff --git a/nikola/data/themes/bootstrap3/templates/base.tmpl b/nikola/data/themes/bootstrap3/templates/base.tmpl
deleted file mode 100644
index 2f7a290..0000000
--- a/nikola/data/themes/bootstrap3/templates/base.tmpl
+++ /dev/null
@@ -1,94 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="base" file="base_helper.tmpl" import="*" />
-<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
-${set_locale(lang)}
-${base.html_headstart()}
-<%block name="extra_head">
-### Leave this block alone.
-</%block>
-${template_hooks['extra_head']()}
-</head>
-<body>
-<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
-
-<!-- Menubar -->
-
-<nav class="navbar navbar-inverse navbar-static-top">
- <div class="container"><!-- This keeps the margins nice -->
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false">
- <span class="sr-only">${messages("Toggle navigation")}</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href="${abs_link(_link("root", None, lang))}">
- %if logo_url:
- <img src="${logo_url}" alt="${blog_title|h}" id="logo">
- %endif
-
- % if show_blog_title:
- <span id="blog-title">${blog_title|h}</span>
- % endif
- </a>
- </div><!-- /.navbar-header -->
- <div class="collapse navbar-collapse" id="bs-navbar" aria-expanded="false">
- <ul class="nav navbar-nav">
- ${base.html_navigation_links()}
- ${template_hooks['menu']()}
- </ul>
- %if search_form:
- ${search_form}
- %endif
-
- <ul class="nav navbar-nav navbar-right">
- <%block name="belowtitle">
- %if len(translations) > 1:
- <li>${base.html_translations()}</li>
- %endif
- </%block>
- % if show_sourcelink:
- <%block name="sourcelink"></%block>
- %endif
- ${template_hooks['menu_alt']()}
- </ul>
- </div><!-- /.navbar-collapse -->
- </div><!-- /.container -->
-</nav>
-
-<!-- End of Menubar -->
-
-<div class="container" id="content" role="main">
- <div class="body-content">
- <!--Body content-->
- <div class="row">
- ${template_hooks['page_header']()}
- <%block name="content"></%block>
- </div>
- <!--End of body content-->
-
- <footer id="footer">
- ${content_footer}
- ${template_hooks['page_footer']()}
- </footer>
- </div>
-</div>
-
-${base.late_load_js()}
- <script>$('a.image-reference:not(.islink) img:not(.islink)').parent().colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
- <!-- fancy dates -->
- <script>
- moment.locale("${momentjs_locales[lang]}");
- fancydates(${date_fanciness}, ${js_date_format});
- </script>
- <!-- end fancy dates -->
- <%block name="extra_js"></%block>
- % if annotations and post and not post.meta('noannotations'):
- ${notes.code()}
- % elif not annotations and post and post.meta('annotations'):
- ${notes.code()}
- % endif
-${body_end}
-${template_hooks['body_end']()}
-</body>
-</html>
diff --git a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl b/nikola/data/themes/bootstrap3/templates/base_helper.tmpl
deleted file mode 100644
index 20b135b..0000000
--- a/nikola/data/themes/bootstrap3/templates/base_helper.tmpl
+++ /dev/null
@@ -1,188 +0,0 @@
-## -*- coding: utf-8 -*-
-
-<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
-<%def name="html_headstart()">
-<!DOCTYPE html>
-<html
-\
-% if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']) or (comment_system == 'facebook'):
-prefix='\
-%if use_open_graph or (twitter_card and twitter_card['use_twitter_cards']):
-og: http://ogp.me/ns# \
-%endif
-%if use_open_graph:
-article: http://ogp.me/ns/article# \
-%endif
-%if comment_system == 'facebook':
-fb: http://ogp.me/ns/fb# \
-%endif
-'\
-%endif
-\
-% if is_rtl:
-dir="rtl" \
-% endif
-\
-lang="${lang}">
- <head>
- <meta charset="utf-8">
- % if use_base_tag:
- <base href="${abs_link(permalink)}">
- % endif
- %if description:
- <meta name="description" content="${description|h}">
- %endif
- <meta name="viewport" content="width=device-width, initial-scale=1">
- %if title == blog_title:
- <title>${blog_title|h}</title>
- %else:
- <title>${title|h} | ${blog_title|h}</title>
- %endif
-
- ${html_stylesheets()}
- <meta content="${theme_color}" name="theme-color">
- ${html_feedlinks()}
- <link rel="canonical" href="${abs_link(permalink)}">
-
- %if favicons:
- %for name, file, size in favicons:
- <link rel="${name}" href="${file}" sizes="${size}"/>
- %endfor
- %endif
-
- % if comment_system == 'facebook':
- <meta property="fb:app_id" content="${comment_system_id}">
- % endif
-
- %if prevlink:
- <link rel="prev" href="${prevlink}" type="text/html">
- %endif
- %if nextlink:
- <link rel="next" href="${nextlink}" type="text/html">
- %endif
-
- ${mathjax_config}
- %if use_cdn:
- <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
- %else:
- <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang)}"></script><![endif]-->
- %endif
-
- ${extra_head_data}
-</%def>
-
-<%def name="late_load_js()">
- %if use_bundles:
- %if use_cdn:
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
-
- <script src="/assets/js/all.js"></script>
- %else:
- <script src="/assets/js/all-nocdn.js"></script>
- %endif
- %else:
- %if use_cdn:
- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
- <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
- %else:
- <script src="/assets/js/jquery.min.js"></script>
- <script src="/assets/js/bootstrap.min.js"></script>
- <script src="/assets/js/moment-with-locales.min.js"></script>
- <script src="/assets/js/fancydates.js"></script>
- %endif
- <script src="/assets/js/jquery.colorbox-min.js"></script>
- %endif
- %if colorbox_locales[lang]:
- <script src="/assets/js/colorbox-i18n/jquery.colorbox-${colorbox_locales[lang]}.js"></script>
- %endif
- ${social_buttons_code}
-</%def>
-
-
-<%def name="html_stylesheets()">
- %if use_bundles:
- %if use_cdn:
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- %else:
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- %endif
- %else:
- %if use_cdn:
- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
- %else:
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- %endif
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
- %if has_custom_css:
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- %endif
- %endif
- % if needs_ipython_css:
- <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
- % endif
- % if annotations and post and not post.meta('noannotations'):
- ${notes.css()}
- % elif not annotations and post and post.meta('annotations'):
- ${notes.css()}
- % endif
-</%def>
-
-<%def name="html_navigation_links()">
- %for url, text in navigation_links[lang]:
- % if isinstance(url, tuple):
- <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text} <b class="caret"></b></a>
- <ul class="dropdown-menu">
- %for suburl, text in url:
- % if rel_link(permalink, suburl) == "#":
- <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
- %else:
- <li><a href="${suburl}">${text}</a>
- %endif
- %endfor
- </ul>
- % else:
- % if rel_link(permalink, url) == "#":
- <li class="active"><a href="${permalink}">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
- %else:
- <li><a href="${url}">${text}</a>
- %endif
- % endif
- %endfor
-</%def>
-
-<%def name="html_feedlinks()">
- %if rss_link:
- ${rss_link}
- %elif generate_rss:
- %if len(translations) > 1:
- %for language in sorted(translations):
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
- %endfor
- %else:
- <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
- %endif
- %endif
- %if generate_atom:
- %if len(translations) > 1:
- %for language in sorted(translations):
- <link rel="alternate" type="application/atom+xml" title="Atom (${language})" href="${_link('index_atom', None, language)}">
- %endfor
- %else:
- <link rel="alternate" type="application/atom+xml" title="Atom" href="${_link('index_atom', None)}">
- %endif
- %endif
-</%def>
-
-<%def name="html_translations()">
- %for langname in sorted(translations):
- %if langname != lang:
- <li><a href="${abs_link(_link("root", None, langname))}" rel="alternate" hreflang="${langname}">${messages("LANGUAGE", langname)}</a></li>
- %endif
- %endfor
-</%def>
diff --git a/nikola/data/themes/bootstrap3/templates/gallery.tmpl b/nikola/data/themes/bootstrap3/templates/gallery.tmpl
deleted file mode 100644
index 3dbfa82..0000000
--- a/nikola/data/themes/bootstrap3/templates/gallery.tmpl
+++ /dev/null
@@ -1,95 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%namespace name="comments" file="comments_helper.tmpl"/>
-<%namespace name="ui" file="crumbs.tmpl" import="bar"/>
-<%block name="sourcelink"></%block>
-
-<%block name="content">
- ${ui.bar(crumbs)}
- %if title:
- <h1>${title|h}</h1>
- %endif
- %if post:
- <p>
- ${post.text()}
- </p>
- %endif
- %if folders:
- <ul>
- % for folder, ftitle in folders:
- <li><a href="${folder}"><i class="glyphicon glyphicon-folder-open"></i>&nbsp;${ftitle|h}</a></li>
- % endfor
- </ul>
- %endif
-
-<div id="gallery_container"></div>
-%if photo_array:
-<noscript>
-<ul class="thumbnails">
- %for image in photo_array:
- <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']|h}">
- <img src="${image['url_thumb']}" alt="${image['title']|h}" /></a>
- %endfor
-</ul>
-</noscript>
-%endif
-%if site_has_comments and enable_comments:
-${comments.comment_form(None, permalink, title)}
-%endif
-</%block>
-
-<%block name="extra_head">
-${parent.extra_head()}
-<link rel="alternate" type="application/rss+xml" title="RSS" href="rss.xml">
-<style type="text/css">
- .image-block {
- display: inline-block;
- }
- .flowr_row {
- width: 100%;
- }
- </style>
-</%block>
-
-
-<%block name="extra_js">
-<script src="/assets/js/flowr.plugin.js"></script>
-<script>
-jsonContent = ${photo_array_json};
-$("#gallery_container").flowr({
- data : jsonContent,
- height : ${thumbnail_size}*.6,
- padding: 5,
- rows: -1,
- render : function(params) {
- // Just return a div, string or a dom object, anything works fine
- img = $("<img />").attr({
- 'src': params.itemData.url_thumb,
- 'width' : params.width,
- 'height' : params.height
- }).css('max-width', '100%');
- link = $( "<a></a>").attr({
- 'href': params.itemData.url,
- 'class': 'image-reference'
- });
- div = $("<div />").addClass('image-block').attr({
- 'title': params.itemData.title,
- 'data-toggle': "tooltip",
- });
- link.append(img);
- div.append(link);
- div.hover(div.tooltip());
- return div;
- },
- itemWidth : function(data) { return data.size.w; },
- itemHeight : function(data) { return data.size.h; },
- complete : function(params) {
- if( jsonContent.length > params.renderedItems ) {
- nextRenderList = jsonContent.slice( params.renderedItems );
- }
- }
- });
-$("a.image-reference").colorbox({rel:"gal", maxWidth:"100%",maxHeight:"100%",scalePhotos:true});
-$('a.image-reference[href="'+window.location.hash.substring(1,1000)+'"]').click();
-</script>
-</%block>
diff --git a/nikola/data/themes/bootstrap3/templates/slides.tmpl b/nikola/data/themes/bootstrap3/templates/slides.tmpl
deleted file mode 100644
index a73848a..0000000
--- a/nikola/data/themes/bootstrap3/templates/slides.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-<%block name="content">
-<div id="${carousel_id}" class="carousel slide">
- <ol class="carousel-indicators">
- % for i in range(len(slides_content)):
- % if i == 0:
- <li data-target="#${carousel_id}" data-slide-to="${i}" class="active"></li>
- % else:
- <li data-target="#${carousel_id}" data-slide-to="${i}"></li>
- % endif
- % endfor
- </ol>
- <div class="carousel-inner">
- % for i, image in enumerate(slides_content):
- % if i == 0:
- <div class="item active"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div>
- % else:
- <div class="item"><img src="${image}" alt="" style="margin: 0 auto 0 auto;"></div>
- % endif
- % endfor
- </div>
- <a class="left carousel-control" href="#${carousel_id}" data-slide="prev"><span class="icon-prev"></span></a>
- <a class="right carousel-control" href="#${carousel_id}" data-slide="next"><span class="icon-next"></span></a>
-</div>
-</%block>
diff --git a/nikola/data/themes/bootstrap4-jinja/README.md b/nikola/data/themes/bootstrap4-jinja/README.md
new file mode 100644
index 0000000..bb1b484
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/README.md
@@ -0,0 +1,10 @@
+This is a theme based on Bootstrap 4.
+
+The theme is a good building block for a site. It is based on a simple navbar +
+content layout. For a more blog-style layout, check out `bootblog4`.
+
+Note that unlike previous versions of Bootstrap, icon fonts are not built-in.
+You can use Font Awesome for this.
+
+This theme supports Bootswatch font/color schemes through the `nikola
+bootwatch_theme` command.
diff --git a/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css
new file mode 120000
index 0000000..8c8dc62
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/assets/css/bootstrap.min.css
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css b/nikola/data/themes/bootstrap4-jinja/assets/css/theme.css
index 52466de..20eee8e 100644
--- a/nikola/data/themes/bootstrap3-jinja/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap4-jinja/assets/css/theme.css
@@ -1,29 +1,11 @@
-#container {
- width: 960px;
- margin: 0 auto;
-}
-
-#contentcolumn {
- max-width: 760px;
-}
-#q {
- width: 150px;
-}
-
img {
- max-width: 90%;
-}
-
-.postbox {
- border-bottom: 2px solid darkgrey;
- margin-bottom: 12px;
+ max-width: 100%;
}
.titlebox {
text-align: right;
}
-#addthisbox {margin-bottom: 12px;}
td.label {
/* Issue #290 */
@@ -36,7 +18,6 @@ td.label {
font-size: xx-small;
}
-
.caption {
/* Issue 292 */
text-align: center;
@@ -52,7 +33,7 @@ div.figure > a > img {
}
blockquote p, blockquote {
- font-size: 17.5px;
+ font-size: 1.25rem;
font-weight: 300;
line-height: 1.25;
}
@@ -67,10 +48,6 @@ ul.bricks > li {
margin: 3px;
}
-.at300b, .stMainServices, .stButton, .stButton_gradient {
- box-sizing: content-box;
-}
-
pre, pre code {
white-space: pre;
word-wrap: normal;
@@ -86,10 +63,6 @@ article.post-micro {
display: inline-block;
}
-.flowr_row {
- width: 100%;
-}
-
.tags {
padding-left: 0;
margin-left: -5px;
@@ -100,21 +73,25 @@ article.post-micro {
.tags > li {
display: inline-block;
- min-width: 10px;
- padding: 3px 7px;
- font-size: 12px;
- font-weight: bold;
+}
+.tags > li a {
+ display: inline-block;
+ padding: .25em .4em;
+ font-size: 75%;
+ font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
- background-color: #999;
- border-radius: 10px;
+ border-radius: .25rem;
+ background-color: #868e96;
}
-.tags > li a {
+.tags > li a:hover {
color: #fff;
+ text-decoration: none;
+ background-color: #6c757d;
}
.metadata p:before,
@@ -144,27 +121,6 @@ article.post-micro {
margin-top: 1em;
}
-.navbar-brand {
- padding: 0 15px;
-}
-
-.navbar-brand #blog-title {
- padding: 15px 0;
- display: inline-block;
-}
-
-.navbar-brand #logo {
- max-width: 100%;
-}
-
-.navbar-brand>img {
- display: inline;
-}
-
-.row {
- margin: 0;
-}
-
/* for alignment with Bootstrap's .entry-content styling */
.entry-summary {
margin-top: 1em;
@@ -177,14 +133,6 @@ article.post-micro {
border-top: 1px solid #e5e5e5;
}
-.codetable {
- table-layout: fixed;
-}
-
-.codetable pre {
- overflow-x: scroll;
-}
-
/* hat tip bootstrap/html5 boilerplate */
@media print {
*, *:before, *:after {
@@ -213,3 +161,72 @@ article.post-micro {
display: none;
}
}
+
+pre, .codetable {
+ border: 1px solid #ccc;
+ border-radius: 0.25rem;
+ margin-bottom: 1rem;
+}
+
+pre {
+ padding: 0.75rem;
+}
+
+.codetable tr:first-child td.linenos {
+ border-top-left-radius: 0.25rem;
+}
+
+.codetable tr:last-child td.linenos {
+ border-bottom-left-radius: 0.25rem;
+}
+
+.postindexpager {
+ padding-bottom: 1rem;
+}
+
+ul.navbar-nav {
+ margin-top: 0;
+}
+
+ul.pager {
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+ border-radius: .25rem;
+ padding-left: 0;
+ margin: 0.5rem 0;
+}
+
+ul.pager li.previous {
+ margin-right: auto;
+ display: inline;
+}
+
+ul.pager li.next {
+ margin-left: auto;
+ display: inline;
+}
+
+
+ul.pager li a {
+ display: inline;
+ position: relative;
+ padding: .5rem .75rem;
+ margin-left: -1px;
+ line-height: 1.25;
+ border: 1px solid #ddd;
+ border-radius: .25rem;
+}
+
+pre.code {
+ white-space: pre-wrap;
+}
+
+.byline a:not(:last-child):after {
+ content: ",";
+}
+
+/* Override incorrect Bootstrap 4 default */
+html[dir="rtl"] body {
+ text-align: right;
+}
diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js
new file mode 120000
index 0000000..593bffb
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/assets/js/bootstrap.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/bootstrap/dist/js/bootstrap.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js
new file mode 120000
index 0000000..2a592f6
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/assets/js/jquery.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/jquery/dist/jquery.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js b/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js
new file mode 120000
index 0000000..43fca04
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/assets/js/popper.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/popper.js/dist/umd/popper.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme b/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme
new file mode 100644
index 0000000..be27ebc
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/bootstrap4-jinja.theme
@@ -0,0 +1,12 @@
+[Theme]
+engine = jinja
+parent = base-jinja
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+based_on = Bootstrap 4 <http://getbootstrap.com/>
+tags = bootstrap
+
+[Family]
+family = bootstrap4
+mako-version = bootstrap4
diff --git a/nikola/data/themes/bootstrap4-jinja/bundles b/nikola/data/themes/bootstrap4-jinja/bundles
new file mode 120000
index 0000000..500c93e
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/bundles
@@ -0,0 +1 @@
+../bootstrap4/bundles \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/authors.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/authors.tmpl
index d65c727..922de74 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/authors.tmpl
+++ b/nikola/data/themes/bootstrap4-jinja/templates/authors.tmpl
@@ -1,9 +1,17 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% block extra_head %}
+ {{ feeds_translations.head(kind=kind, feeds=False) }}
+{% endblock %}
{% block content %}
{% if items %}
<h2>{{ messages("Authors") }}</h2>
+ <div class="metadata">
+ {{ feeds_translations.translation_link(kind) }}
+ </div>
{% endif %}
{% if items %}
<ul class="list-inline">
diff --git a/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl
new file mode 100644
index 0000000..0748bb2
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/templates/base.tmpl
@@ -0,0 +1,105 @@
+{# -*- coding: utf-8 -*- #}
+{% import 'base_helper.tmpl' as base with context %}
+{% import 'annotation_helper.tmpl' as notes with context %}
+{{ set_locale(lang) }}
+{{ base.html_headstart() }}
+{% block extra_head %}
+{# Leave this block alone. #}
+{% endblock %}
+{{ template_hooks['extra_head']() }}
+</head>
+<body>
+<a href="#content" class="sr-only sr-only-focusable">{{ messages("Skip to main content") }}</a>
+
+<!-- Menubar -->
+
+<nav class="navbar navbar-expand-md static-top mb-4
+{% if theme_config.get('navbar_light') %}
+navbar-light
+{% else %}
+navbar-dark
+{% endif %}
+{% if theme_config.get('navbar_custom_bg') %}
+{{ theme_config['navbar_custom_bg'] }}
+{% elif theme_config.get('navbar_light') %}
+bg-light
+{% else %}
+bg-dark
+{% endif %}
+">
+ <div class="container"><!-- This keeps the margins nice -->
+ <a class="navbar-brand" href="{{ _link("root", None, lang) }}">
+ {% if logo_url %}
+ <img src="{{ logo_url }}" alt="{{ blog_title|e }}" id="logo" class="d-inline-block align-top">
+ {% endif %}
+
+ {% if show_blog_title %}
+ <span id="blog-title">{{ blog_title|e }}</span>
+ {% endif %}
+ </a>
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+
+ <div class="collapse navbar-collapse" id="bs-navbar">
+ <ul class="navbar-nav mr-auto">
+ {{ base.html_navigation_links_entries(navigation_links) }}
+ {{ template_hooks['menu']() }}
+ </ul>
+ {% if search_form %}
+ {{ search_form }}
+ {% endif %}
+
+ <ul class="navbar-nav navbar-right">
+ {{ base.html_navigation_links_entries(navigation_alt_links) }}
+ {% block belowtitle %}
+ {% if translations|length > 1 %}
+ <li>{{ base.html_translations() }}</li>
+ {% endif %}
+ {% endblock %}
+ {% if show_sourcelink %}
+ {% block sourcelink %}{% endblock %}
+ {% endif %}
+ {{ template_hooks['menu_alt']() }}
+ </ul>
+ </div><!-- /.navbar-collapse -->
+ </div><!-- /.container -->
+</nav>
+
+<!-- End of Menubar -->
+
+<div class="container" id="content" role="main">
+ <div class="body-content">
+ <!--Body content-->
+ {{ template_hooks['page_header']() }}
+ {% block extra_header %}{% endblock %}
+ {% block content %}{% endblock %}
+ <!--End of body content-->
+
+ <footer id="footer">
+ {{ content_footer }}
+ {{ template_hooks['page_footer']() }}
+ {% block extra_footer %}{% endblock %}
+ </footer>
+ </div>
+</div>
+
+{{ base.late_load_js() }}
+ {% if date_fanciness != 0 %}
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "{{ luxon_locales[lang] }}";
+ fancydates({{ date_fanciness }}, {{ luxon_date_format }});
+ </script>
+ <!-- end fancy dates -->
+ {% endif %}
+ {% block extra_js %}{% endblock %}
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
+{{ body_end }}
+{{ template_hooks['body_end']() }}
+</body>
+</html>
diff --git a/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl
new file mode 100644
index 0000000..b4bcf85
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/templates/base_helper.tmpl
@@ -0,0 +1,165 @@
+{# -*- coding: utf-8 -*- #}
+{% import 'feeds_translations_helper.tmpl' as feeds_translations with context %}
+
+{% macro html_headstart() %}
+<!DOCTYPE html>
+<html
+
+prefix='
+og: http://ogp.me/ns# article: http://ogp.me/ns/article#
+{% if comment_system == 'facebook' %}
+fb: http://ogp.me/ns/fb#
+{% endif %}
+'
+{% if is_rtl %}
+dir="rtl"
+{% endif %}
+
+lang="{{ lang }}">
+ <head>
+ <meta charset="utf-8">
+ {% if description %}
+ <meta name="description" content="{{ description|e }}">
+ {% endif %}
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ {% if title == blog_title %}
+ <title>{{ blog_title|e }}</title>
+ {% else %}
+ <title>{{ title|e }} | {{ blog_title|e }}</title>
+ {% endif %}
+
+ {{ html_stylesheets() }}
+ <meta name="theme-color" content="{{ theme_color }}">
+ {% if meta_generator_tag %}
+ <meta name="generator" content="Nikola (getnikola.com)">
+ {% endif %}
+ {{ html_feedlinks() }}
+ <link rel="canonical" href="{{ abs_link(permalink) }}">
+
+ {% if favicons %}
+ {% for name, file, size in favicons %}
+ <link rel="{{ name }}" href="{{ file }}" sizes="{{ size }}"/>
+ {% endfor %}
+ {% endif %}
+
+ {% if comment_system == 'facebook' %}
+ <meta property="fb:app_id" content="{{ comment_system_id }}">
+ {% endif %}
+
+ {% if prevlink %}
+ <link rel="prev" href="{{ prevlink }}" type="text/html">
+ {% endif %}
+ {% if nextlink %}
+ <link rel="next" href="{{ nextlink }}" type="text/html">
+ {% endif %}
+
+ {% if use_cdn %}
+ <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ {% else %}
+ <!--[if lt IE 9]><script src="{{ url_replacer(permalink, '/assets/js/html5.js', lang, url_type) }}"></script><![endif]-->
+ {% endif %}
+
+ {{ extra_head_data }}
+{% endmacro %}
+
+{% macro late_load_js() %}
+ {% if use_cdn %}
+ <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ {% endif %}
+ {% if use_bundles and use_cdn %}
+ <script src="/assets/js/all.js"></script>
+ {% elif use_bundles %}
+ <script src="/assets/js/all-nocdn.js"></script>
+ {% else %}
+ {% if not use_cdn %}
+ <script src="/assets/js/jquery.min.js"></script>
+ <script src="/assets/js/popper.min.js"></script>
+ <script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ {% endif %}
+ {% endif %}
+ {% if date_fanciness != 0 %}
+ {% if date_fanciness == 2 %}
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.{{ luxon_locales[lang] }}"></script>
+ {% endif %}
+ {% if use_cdn %}
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ {% else %}
+ <script src="/assets/js/luxon.min.js"></script>
+ {% endif %}
+ {% if not use_bundles %}
+ <script src="/assets/js/fancydates.min.js"></script>
+ {% endif %}
+ {% endif %}
+ {{ social_buttons_code }}
+{% endmacro %}
+
+
+{% macro html_stylesheets() %}
+ {% if use_cdn %}
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
+ {% endif %}
+ {% if use_bundles and use_cdn %}
+ <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
+ {% elif use_bundles %}
+ <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
+ {% else %}
+ {% if not use_cdn %}
+ <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ {% endif %}
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
+ {% if has_custom_css %}
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ {% endif %}
+ {% endif %}
+ {% if needs_ipython_css %}
+ <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
+ {% endif %}
+{% endmacro %}
+
+{% macro html_navigation_links() %}
+ {{ html_navigation_links_entries(navigation_links) }}
+{% endmacro %}
+
+{% macro html_navigation_links_entries(navigation_links_source) %}
+ {% for url, text in navigation_links_source[lang] %}
+ {% if isinstance(url, tuple) %}
+ <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ text }}</a>
+ <div class="dropdown-menu">
+ {% for suburl, text in url %}
+ {% if rel_link(permalink, suburl) == "#" %}
+ <a href="{{ permalink }}" class="dropdown-item active">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
+ {% else %}
+ <a href="{{ suburl }}" class="dropdown-item">{{ text }}</a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% else %}
+ {% if rel_link(permalink, url) == "#" %}
+ <li class="nav-item active"><a href="{{ permalink }}" class="nav-link">{{ text }} <span class="sr-only">{{ messages("(active)", lang) }}</span></a>
+ {% else %}
+ <li class="nav-item"><a href="{{ url }}" class="nav-link">{{ text }}</a>
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+
+{% macro html_feedlinks() %}
+ {{ feeds_translations.head(classification=None, kind='index', other=False) }}
+{% endmacro %}
+
+{% macro html_translations() %}
+ {% for langname in translations|sort %}
+ {% if langname != lang %}
+ <li class="nav-item"><a href="{{ _link("root", None, langname) }}" rel="alternate" hreflang="{{ langname }}" class="nav-link">{{ messages("LANGUAGE", langname) }}</a></li>
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
diff --git a/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl
new file mode 100644
index 0000000..2fec2c6
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/templates/index_helper.tmpl
@@ -0,0 +1,13 @@
+{# -*- coding: utf-8 -*- #}
+{% macro html_pager() %}
+ {% if prevlink or nextlink %}
+ <ul class="pager postindexpager clearfix">
+ {% if prevlink %}
+ <li class="previous"><a href="{{ prevlink }}" rel="prev">{{ messages("Newer posts") }}</a></li>
+ {% endif %}
+ {% if nextlink %}
+ <li class="next"><a href="{{ nextlink }}" rel="next">{{ messages("Older posts") }}</a></li>
+ {% endif %}
+ </ul>
+ {% endif %}
+{% endmacro %}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/listing.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/listing.tmpl
index ed99410..56a1b4f 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/listing.tmpl
+++ b/nikola/data/themes/bootstrap4-jinja/templates/listing.tmpl
@@ -1,15 +1,15 @@
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
-{% import 'crumbs.tmpl' as ui with context %}
+{% import 'ui_helper.tmpl' as ui with context %}
{% block content %}
-{{ ui.bar(crumbs) }}
+{{ ui.breadcrumbs(crumbs) }}
{% if folders or files %}
<ul>
{% for name in folders %}
- <li><a href="{{ name|urlencode }}"><i class="glyphicon glyphicon-folder-open"></i> {{ name|e }}</a>
+ <li><a href="{{ name|e }}">📂&nbsp;{{ name|e }}</a>
{% endfor %}
{% for name in files %}
- <li><a href="{{ name|urlencode }}.html"><i class="glyphicon glyphicon-file"></i> {{ name|e }}</a>
+ <li><a href="{{ name|e }}.html">📄&nbsp;{{ name|e }}</a>
{% endfor %}
</ul>
{% endif %}
@@ -24,9 +24,7 @@
{% endblock %}
{% block sourcelink %}
-{% if source_link %}
- <li>
- <a href="{{ source_link }}" id="sourcelink">{{ messages("Source") }}</a>
- </li>
+{% if source_link and show_sourcelink %}
+ {{ ui.show_sourcelink(source_link) }}
{% endif %}
{% endblock %}
diff --git a/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl
new file mode 100644
index 0000000..30fe534
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/templates/pagination_helper.tmpl
@@ -0,0 +1,40 @@
+{# -*- coding: utf-8 -*- #}
+{% macro page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5) %}
+<nav aria-label="Page navigation">
+ <ul class="pagination">
+ {% if prev_next_links_reversed %}
+ {% if nextlink %}
+ <li class="page-item"><a href="{{ nextlink }}" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">&laquo;</span></a></li>
+ {% else %}
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">&laquo;</span></a></li>
+ {% endif %}
+ {% else %}
+ {% if prevlink %}
+ <li class="page-item"><a href="{{ prevlink }}" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">&laquo;</span></a></li>
+ {% else %}
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">&laquo;</span></a></li>
+ {% endif %}
+ {% endif %}
+ {% for i, link in enumerate(page_links) %}
+ {% if (i - current_page)|abs <= surrounding or i == 0 or i == page_links|length - 1 %}
+ <li class="page-item {{ 'active' if i == current_page else '' }}"><a href="{{ link }}" class="page-link">{{ i + 1 }}{{ ' <span class="sr-only">(current)</span>' if i == current_page else '' }}</a></li>
+ {% elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1 %}
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="…"><span aria-hidden="true">…</span></a></li>
+ {% endif %}
+ {% endfor %}
+ {% if prev_next_links_reversed %}
+ {% if prevlink %}
+ <li class="page-item"><a href="{{ prevlink }}" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">&raquo;</span></a></li>
+ {% else %}
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Newer posts") }}"><span aria-hidden="true">&raquo;</span></a></li>
+ {% endif %}
+ {% else %}
+ {% if nextlink %}
+ <li class="page-item"><a href="{{ nextlink }}" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">&raquo;</span></a></li>
+ {% else %}
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="{{ messages("Older posts") }}"><span aria-hidden="true">&raquo;</span></a></li>
+ {% endif %}
+ {% endif %}
+ </ul>
+</nav>
+{% endmacro %}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/post.tmpl
index 3cf4c4b..7e18f90 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/post.tmpl
+++ b/nikola/data/themes/bootstrap4-jinja/templates/post.tmpl
@@ -2,15 +2,14 @@
{% import 'post_helper.tmpl' as helper with context %}
{% import 'post_header.tmpl' as pheader with context %}
{% import 'comments_helper.tmpl' as comments with context %}
+{% import 'math_helper.tmpl' as math with context %}
+{% import 'ui_helper.tmpl' as ui with context %}
{% extends 'base.tmpl' %}
{% block extra_head %}
{{ super() }}
{% if post.meta('keywords') %}
- <meta name="keywords" content="{{ post.meta('keywords')|e }}">
- {% endif %}
- {% if post.description() %}
- <meta name="description" itemprop="description" content="{{ post.description()|e }}">
+ <meta name="keywords" content="{{ smartjoin(', ', post.meta('keywords'))|e }}">
{% endif %}
<meta name="author" content="{{ post.author()|e }}">
{% if post.prev_post %}
@@ -25,6 +24,7 @@
{{ helper.open_graph_metadata(post) }}
{{ helper.twitter_card_information(post) }}
{{ helper.meta_translations(post) }}
+ {{ math.math_styles_ifpost(post) }}
{% endblock %}
{% block content %}
@@ -45,15 +45,13 @@
{{ comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path) }}
</section>
{% endif %}
- {{ helper.mathjax_script(post) }}
+ {{ math.math_scripts_ifpost(post) }}
</article>
{{ comments.comment_link_script() }}
{% endblock %}
{% block sourcelink %}
{% if show_sourcelink %}
- <li>
- <a href="{{ post.source_link() }}" id="sourcelink">{{ messages("Source") }}</a>
- </li>
+ {{ ui.show_sourcelink(post.source_link()) }}
{% endif %}
{% endblock %}
diff --git a/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/tags.tmpl
index 4afd4d2..0eadff6 100644
--- a/nikola/data/themes/bootstrap3-jinja/templates/tags.tmpl
+++ b/nikola/data/themes/bootstrap4-jinja/templates/tags.tmpl
@@ -11,7 +11,7 @@
{% for i in range(indent_change_before) %}
<ul class="list-inline">
{% endfor %}
- <li><a class="reference badge" href="{{ link }}">{{ text|e }}</a>
+ <li class="list-inline-item"><a class="reference badge badge-secondary" href="{{ link }}">{{ text|e }}</a>
{% if indent_change_after <= 0 %}
</li>
{% endif %}
@@ -30,7 +30,7 @@
<ul class="list-inline">
{% for text, link in items %}
{% if text not in hidden_tags %}
- <li><a class="reference badge" href="{{ link }}">{{ text|e }}</a></li>
+ <li class="list-inline-item"><a class="reference badge badge-secondary" href="{{ link }}">{{ text|e }}</a></li>
{% endif %}
{% endfor %}
</ul>
diff --git a/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl b/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl
new file mode 100644
index 0000000..d8c8d4d
--- /dev/null
+++ b/nikola/data/themes/bootstrap4-jinja/templates/ui_helper.tmpl
@@ -0,0 +1,24 @@
+{# -*- coding: utf-8 -*- #}
+{% macro breadcrumbs(crumbs) %}
+{% if crumbs %}
+<nav class="breadcrumbs">
+<ul class="breadcrumb">
+ {% for link, text in crumbs %}
+ {% if text != index_file %}
+ {% if link == '#' %}
+ <li class="breadcrumb-item active">{{ text.rsplit('.html', 1)[0] }}</li>
+ {% else %}
+ <li class="breadcrumb-item"><a href="{{ link }}">{{ text }}</a></li>
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+</ul>
+</nav>
+{% endif %}
+{% endmacro %}
+
+{% macro show_sourcelink(sourcelink_href) %}
+ <li class="nav-item">
+ <a href="{{ sourcelink_href }}" id="sourcelink" class="nav-link">{{ messages("Source") }}</a>
+ </li>
+{% endmacro %}
diff --git a/nikola/data/themes/bootstrap4/README.md b/nikola/data/themes/bootstrap4/README.md
new file mode 100644
index 0000000..bb1b484
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/README.md
@@ -0,0 +1,10 @@
+This is a theme based on Bootstrap 4.
+
+The theme is a good building block for a site. It is based on a simple navbar +
+content layout. For a more blog-style layout, check out `bootblog4`.
+
+Note that unlike previous versions of Bootstrap, icon fonts are not built-in.
+You can use Font Awesome for this.
+
+This theme supports Bootswatch font/color schemes through the `nikola
+bootwatch_theme` command.
diff --git a/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css b/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css
new file mode 120000
index 0000000..8c8dc62
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/assets/css/bootstrap.min.css
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/bootstrap/dist/css/bootstrap.min.css \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap3/assets/css/theme.css b/nikola/data/themes/bootstrap4/assets/css/theme.css
index 52466de..20eee8e 100644
--- a/nikola/data/themes/bootstrap3/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap4/assets/css/theme.css
@@ -1,29 +1,11 @@
-#container {
- width: 960px;
- margin: 0 auto;
-}
-
-#contentcolumn {
- max-width: 760px;
-}
-#q {
- width: 150px;
-}
-
img {
- max-width: 90%;
-}
-
-.postbox {
- border-bottom: 2px solid darkgrey;
- margin-bottom: 12px;
+ max-width: 100%;
}
.titlebox {
text-align: right;
}
-#addthisbox {margin-bottom: 12px;}
td.label {
/* Issue #290 */
@@ -36,7 +18,6 @@ td.label {
font-size: xx-small;
}
-
.caption {
/* Issue 292 */
text-align: center;
@@ -52,7 +33,7 @@ div.figure > a > img {
}
blockquote p, blockquote {
- font-size: 17.5px;
+ font-size: 1.25rem;
font-weight: 300;
line-height: 1.25;
}
@@ -67,10 +48,6 @@ ul.bricks > li {
margin: 3px;
}
-.at300b, .stMainServices, .stButton, .stButton_gradient {
- box-sizing: content-box;
-}
-
pre, pre code {
white-space: pre;
word-wrap: normal;
@@ -86,10 +63,6 @@ article.post-micro {
display: inline-block;
}
-.flowr_row {
- width: 100%;
-}
-
.tags {
padding-left: 0;
margin-left: -5px;
@@ -100,21 +73,25 @@ article.post-micro {
.tags > li {
display: inline-block;
- min-width: 10px;
- padding: 3px 7px;
- font-size: 12px;
- font-weight: bold;
+}
+.tags > li a {
+ display: inline-block;
+ padding: .25em .4em;
+ font-size: 75%;
+ font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
- background-color: #999;
- border-radius: 10px;
+ border-radius: .25rem;
+ background-color: #868e96;
}
-.tags > li a {
+.tags > li a:hover {
color: #fff;
+ text-decoration: none;
+ background-color: #6c757d;
}
.metadata p:before,
@@ -144,27 +121,6 @@ article.post-micro {
margin-top: 1em;
}
-.navbar-brand {
- padding: 0 15px;
-}
-
-.navbar-brand #blog-title {
- padding: 15px 0;
- display: inline-block;
-}
-
-.navbar-brand #logo {
- max-width: 100%;
-}
-
-.navbar-brand>img {
- display: inline;
-}
-
-.row {
- margin: 0;
-}
-
/* for alignment with Bootstrap's .entry-content styling */
.entry-summary {
margin-top: 1em;
@@ -177,14 +133,6 @@ article.post-micro {
border-top: 1px solid #e5e5e5;
}
-.codetable {
- table-layout: fixed;
-}
-
-.codetable pre {
- overflow-x: scroll;
-}
-
/* hat tip bootstrap/html5 boilerplate */
@media print {
*, *:before, *:after {
@@ -213,3 +161,72 @@ article.post-micro {
display: none;
}
}
+
+pre, .codetable {
+ border: 1px solid #ccc;
+ border-radius: 0.25rem;
+ margin-bottom: 1rem;
+}
+
+pre {
+ padding: 0.75rem;
+}
+
+.codetable tr:first-child td.linenos {
+ border-top-left-radius: 0.25rem;
+}
+
+.codetable tr:last-child td.linenos {
+ border-bottom-left-radius: 0.25rem;
+}
+
+.postindexpager {
+ padding-bottom: 1rem;
+}
+
+ul.navbar-nav {
+ margin-top: 0;
+}
+
+ul.pager {
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+ border-radius: .25rem;
+ padding-left: 0;
+ margin: 0.5rem 0;
+}
+
+ul.pager li.previous {
+ margin-right: auto;
+ display: inline;
+}
+
+ul.pager li.next {
+ margin-left: auto;
+ display: inline;
+}
+
+
+ul.pager li a {
+ display: inline;
+ position: relative;
+ padding: .5rem .75rem;
+ margin-left: -1px;
+ line-height: 1.25;
+ border: 1px solid #ddd;
+ border-radius: .25rem;
+}
+
+pre.code {
+ white-space: pre-wrap;
+}
+
+.byline a:not(:last-child):after {
+ content: ",";
+}
+
+/* Override incorrect Bootstrap 4 default */
+html[dir="rtl"] body {
+ text-align: right;
+}
diff --git a/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js b/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js
new file mode 120000
index 0000000..593bffb
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/assets/js/bootstrap.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/bootstrap/dist/js/bootstrap.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4/assets/js/jquery.min.js b/nikola/data/themes/bootstrap4/assets/js/jquery.min.js
new file mode 120000
index 0000000..2a592f6
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/assets/js/jquery.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/jquery/dist/jquery.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4/assets/js/popper.min.js b/nikola/data/themes/bootstrap4/assets/js/popper.min.js
new file mode 120000
index 0000000..43fca04
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/assets/js/popper.min.js
@@ -0,0 +1 @@
+../../../../../../npm_assets/node_modules/popper.js/dist/umd/popper.min.js \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap4/bootstrap4.theme b/nikola/data/themes/bootstrap4/bootstrap4.theme
new file mode 100644
index 0000000..0f3f9ef
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/bootstrap4.theme
@@ -0,0 +1,12 @@
+[Theme]
+engine = mako
+parent = base
+author = The Nikola Contributors
+author_url = https://getnikola.com/
+license = MIT
+based_on = Bootstrap 4 <http://getbootstrap.com/>
+tags = bootstrap
+
+[Family]
+family = bootstrap4
+jinja_version = bootstrap4-jinja
diff --git a/nikola/data/themes/bootstrap4/bundles b/nikola/data/themes/bootstrap4/bundles
new file mode 100644
index 0000000..71d458b
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/bundles
@@ -0,0 +1,26 @@
+; css bundles
+assets/css/all-nocdn.css=
+ bootstrap.min.css,
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ baguetteBox.min.css,
+ theme.css,
+ custom.css,
+assets/css/all.css=
+ rst_base.css,
+ nikola_rst.css,
+ code.css,
+ baguetteBox.min.css,
+ theme.css,
+ custom.css,
+
+; javascript bundles
+assets/js/all-nocdn.js=
+ jquery.min.js,
+ popper.min.js,
+ bootstrap.min.js,
+ baguetteBox.min.js,
+ fancydates.min.js,
+assets/js/all.js=
+ fancydates.min.js
diff --git a/nikola/data/themes/bootstrap3/templates/authors.tmpl b/nikola/data/themes/bootstrap4/templates/authors.tmpl
index 2d3bbf5..300377d 100644
--- a/nikola/data/themes/bootstrap3/templates/authors.tmpl
+++ b/nikola/data/themes/bootstrap4/templates/authors.tmpl
@@ -1,9 +1,17 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%block name="extra_head">
+ ${feeds_translations.head(kind=kind, feeds=False)}
+</%block>
<%block name="content">
% if items:
<h2>${messages("Authors")}</h2>
+ <div class="metadata">
+ ${feeds_translations.translation_link(kind)}
+ </div>
% endif
% if items:
<ul class="list-inline">
diff --git a/nikola/data/themes/bootstrap4/templates/base.tmpl b/nikola/data/themes/bootstrap4/templates/base.tmpl
new file mode 100644
index 0000000..21b6141
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/templates/base.tmpl
@@ -0,0 +1,105 @@
+## -*- coding: utf-8 -*-
+<%namespace name="base" file="base_helper.tmpl" import="*" />
+<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
+${set_locale(lang)}
+${base.html_headstart()}
+<%block name="extra_head">
+### Leave this block alone.
+</%block>
+${template_hooks['extra_head']()}
+</head>
+<body>
+<a href="#content" class="sr-only sr-only-focusable">${messages("Skip to main content")}</a>
+
+<!-- Menubar -->
+
+<nav class="navbar navbar-expand-md static-top mb-4
+% if theme_config.get('navbar_light'):
+navbar-light
+% else:
+navbar-dark
+% endif
+% if theme_config.get('navbar_custom_bg'):
+${theme_config['navbar_custom_bg']}
+% elif theme_config.get('navbar_light'):
+bg-light
+% else:
+bg-dark
+%endif
+">
+ <div class="container"><!-- This keeps the margins nice -->
+ <a class="navbar-brand" href="${_link("root", None, lang)}">
+ %if logo_url:
+ <img src="${logo_url}" alt="${blog_title|h}" id="logo" class="d-inline-block align-top">
+ %endif
+
+ % if show_blog_title:
+ <span id="blog-title">${blog_title|h}</span>
+ % endif
+ </a>
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#bs-navbar" aria-controls="bs-navbar" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+
+ <div class="collapse navbar-collapse" id="bs-navbar">
+ <ul class="navbar-nav mr-auto">
+ ${base.html_navigation_links_entries(navigation_links)}
+ ${template_hooks['menu']()}
+ </ul>
+ %if search_form:
+ ${search_form}
+ %endif
+
+ <ul class="navbar-nav navbar-right">
+ ${base.html_navigation_links_entries(navigation_alt_links)}
+ <%block name="belowtitle">
+ %if len(translations) > 1:
+ <li>${base.html_translations()}</li>
+ %endif
+ </%block>
+ % if show_sourcelink:
+ <%block name="sourcelink"></%block>
+ %endif
+ ${template_hooks['menu_alt']()}
+ </ul>
+ </div><!-- /.navbar-collapse -->
+ </div><!-- /.container -->
+</nav>
+
+<!-- End of Menubar -->
+
+<div class="container" id="content" role="main">
+ <div class="body-content">
+ <!--Body content-->
+ ${template_hooks['page_header']()}
+ <%block name="extra_header"></%block>
+ <%block name="content"></%block>
+ <!--End of body content-->
+
+ <footer id="footer">
+ ${content_footer}
+ ${template_hooks['page_footer']()}
+ <%block name="extra_footer"></%block>
+ </footer>
+ </div>
+</div>
+
+${base.late_load_js()}
+ %if date_fanciness != 0:
+ <!-- fancy dates -->
+ <script>
+ luxon.Settings.defaultLocale = "${luxon_locales[lang]}";
+ fancydates(${date_fanciness}, ${luxon_date_format});
+ </script>
+ <!-- end fancy dates -->
+ %endif
+ <%block name="extra_js"></%block>
+ <script>
+ baguetteBox.run('div#content', {
+ ignoreClass: 'islink',
+ captions: function(element){var i=element.getElementsByTagName('img')[0];return i===undefined?'':i.alt;}});
+ </script>
+${body_end}
+${template_hooks['body_end']()}
+</body>
+</html>
diff --git a/nikola/data/themes/bootstrap4/templates/base_helper.tmpl b/nikola/data/themes/bootstrap4/templates/base_helper.tmpl
new file mode 100644
index 0000000..f551116
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/templates/base_helper.tmpl
@@ -0,0 +1,165 @@
+## -*- coding: utf-8 -*-
+<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>
+
+<%def name="html_headstart()">
+<!DOCTYPE html>
+<html
+\
+prefix='\
+og: http://ogp.me/ns# article: http://ogp.me/ns/article#
+%if comment_system == 'facebook':
+fb: http://ogp.me/ns/fb# \
+%endif
+'\
+% if is_rtl:
+dir="rtl" \
+% endif
+\
+lang="${lang}">
+ <head>
+ <meta charset="utf-8">
+ %if description:
+ <meta name="description" content="${description|h}">
+ %endif
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ %if title == blog_title:
+ <title>${blog_title|h}</title>
+ %else:
+ <title>${title|h} | ${blog_title|h}</title>
+ %endif
+
+ ${html_stylesheets()}
+ <meta name="theme-color" content="${theme_color}">
+ % if meta_generator_tag:
+ <meta name="generator" content="Nikola (getnikola.com)">
+ % endif
+ ${html_feedlinks()}
+ <link rel="canonical" href="${abs_link(permalink)}">
+
+ %if favicons:
+ %for name, file, size in favicons:
+ <link rel="${name}" href="${file}" sizes="${size}"/>
+ %endfor
+ %endif
+
+ % if comment_system == 'facebook':
+ <meta property="fb:app_id" content="${comment_system_id}">
+ % endif
+
+ %if prevlink:
+ <link rel="prev" href="${prevlink}" type="text/html">
+ %endif
+ %if nextlink:
+ <link rel="next" href="${nextlink}" type="text/html">
+ %endif
+
+ %if use_cdn:
+ <!--[if lt IE 9]><script src="https://html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
+ %else:
+ <!--[if lt IE 9]><script src="${url_replacer(permalink, '/assets/js/html5.js', lang, url_type)}"></script><![endif]-->
+ %endif
+
+ ${extra_head_data}
+</%def>
+
+<%def name="late_load_js()">
+ %if use_cdn:
+ <script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.js" integrity="sha256-ULQV01VS9LCI2ePpLsmka+W0mawFpEA0rtxnezUj4A4=" crossorigin="anonymous"></script>
+ % endif
+ %if use_bundles and use_cdn:
+ <script src="/assets/js/all.js"></script>
+ %elif use_bundles:
+ <script src="/assets/js/all-nocdn.js"></script>
+ %else:
+ %if not use_cdn:
+ <script src="/assets/js/jquery.min.js"></script>
+ <script src="/assets/js/popper.min.js"></script>
+ <script src="/assets/js/bootstrap.min.js"></script>
+ <script src="/assets/js/baguetteBox.min.js"></script>
+ %endif
+ %endif
+ %if date_fanciness != 0:
+ %if date_fanciness == 2:
+ <script src="https://polyfill.io/v3/polyfill.js?features=Intl.RelativeTimeFormat.%7Elocale.${luxon_locales[lang]}"></script>
+ %endif
+ %if use_cdn:
+ <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js" integrity="sha256-OVk2fwTRcXYlVFxr/ECXsakqelJbOg5WCj1dXSIb+nU=" crossorigin="anonymous"></script>
+ %else:
+ <script src="/assets/js/luxon.min.js"></script>
+ %endif
+ %if not use_bundles:
+ <script src="/assets/js/fancydates.min.js"></script>
+ %endif
+ %endif
+ ${social_buttons_code}
+</%def>
+
+
+<%def name="html_stylesheets()">
+ %if use_cdn:
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.11.1/baguetteBox.min.css" integrity="sha256-cLMYWYYutHkt+KpNqjg7NVkYSQ+E2VbrXsEvOqU7mL0=" crossorigin="anonymous">
+ % endif
+ %if use_bundles and use_cdn:
+ <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
+ %elif use_bundles:
+ <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
+ %else:
+ %if not use_cdn:
+ <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/baguetteBox.min.css" rel="stylesheet" type="text/css">
+ %endif
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css">
+ %if has_custom_css:
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ %endif
+ %endif
+ % if needs_ipython_css:
+ <link href="/assets/css/ipython.min.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/nikola_ipython.css" rel="stylesheet" type="text/css">
+ % endif
+</%def>
+
+<%def name="html_navigation_links()">
+ ${html_navigation_links_entries(navigation_links)}
+</%def>
+
+<%def name="html_navigation_links_entries(navigation_links_source)">
+ %for url, text in navigation_links_source[lang]:
+ % if isinstance(url, tuple):
+ <li class="nav-item dropdown"><a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${text}</a>
+ <div class="dropdown-menu">
+ %for suburl, text in url:
+ % if rel_link(permalink, suburl) == "#":
+ <a href="${permalink}" class="dropdown-item active">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
+ %else:
+ <a href="${suburl}" class="dropdown-item">${text}</a>
+ %endif
+ %endfor
+ </div>
+ % else:
+ % if rel_link(permalink, url) == "#":
+ <li class="nav-item active"><a href="${permalink}" class="nav-link">${text} <span class="sr-only">${messages("(active)", lang)}</span></a>
+ %else:
+ <li class="nav-item"><a href="${url}" class="nav-link">${text}</a>
+ %endif
+ % endif
+ %endfor
+</%def>
+
+<%def name="html_feedlinks()">
+ ${feeds_translations.head(classification=None, kind='index', other=False)}
+</%def>
+
+<%def name="html_translations()">
+ %for langname in sorted(translations):
+ %if langname != lang:
+ <li class="nav-item"><a href="${_link("root", None, langname)}" rel="alternate" hreflang="${langname}" class="nav-link">${messages("LANGUAGE", langname)}</a></li>
+ %endif
+ %endfor
+</%def>
diff --git a/nikola/data/themes/bootstrap4/templates/index_helper.tmpl b/nikola/data/themes/bootstrap4/templates/index_helper.tmpl
new file mode 100644
index 0000000..e6b0089
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/templates/index_helper.tmpl
@@ -0,0 +1,13 @@
+## -*- coding: utf-8 -*-
+<%def name="html_pager()">
+ %if prevlink or nextlink:
+ <ul class="pager postindexpager clearfix">
+ %if prevlink:
+ <li class="previous"><a href="${prevlink}" rel="prev">${messages("Newer posts")}</a></li>
+ %endif
+ %if nextlink:
+ <li class="next"><a href="${nextlink}" rel="next">${messages("Older posts")}</a></li>
+ %endif
+ </ul>
+ %endif
+</%def>
diff --git a/nikola/data/themes/bootstrap3/templates/listing.tmpl b/nikola/data/themes/bootstrap4/templates/listing.tmpl
index 44809d0..d9a4c56 100644
--- a/nikola/data/themes/bootstrap3/templates/listing.tmpl
+++ b/nikola/data/themes/bootstrap4/templates/listing.tmpl
@@ -1,15 +1,15 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
-<%namespace name="ui" file="crumbs.tmpl" import="bar"/>
+<%namespace name="ui" file="ui_helper.tmpl"/>
<%block name="content">
-${ui.bar(crumbs)}
+${ui.breadcrumbs(crumbs)}
%if folders or files:
<ul>
% for name in folders:
- <li><a href="${name|u}"><i class="glyphicon glyphicon-folder-open"></i> ${name|h}</a>
+ <li><a href="${name|h}">📂&nbsp;${name|h}</a>
% endfor
% for name in files:
- <li><a href="${name|u}.html"><i class="glyphicon glyphicon-file"></i> ${name|h}</a>
+ <li><a href="${name|h}.html">📄&nbsp;${name|h}</a>
% endfor
</ul>
%endif
@@ -24,9 +24,7 @@ ${ui.bar(crumbs)}
</%block>
<%block name="sourcelink">
-% if source_link:
- <li>
- <a href="${source_link}" id="sourcelink">${messages("Source")}</a>
- </li>
+% if source_link and show_sourcelink:
+ ${ui.show_sourcelink(source_link)}
% endif
</%block>
diff --git a/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl b/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl
new file mode 100644
index 0000000..da0e920
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/templates/pagination_helper.tmpl
@@ -0,0 +1,40 @@
+## -*- coding: utf-8 -*-
+<%def name="page_navigation(current_page, page_links, prevlink, nextlink, prev_next_links_reversed, surrounding=5)">
+<nav aria-label="Page navigation">
+ <ul class="pagination">
+ % if prev_next_links_reversed:
+ % if nextlink:
+ <li class="page-item"><a href="${nextlink}" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">&laquo;</span></a></li>
+ % else:
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">&laquo;</span></a></li>
+ % endif
+ % else:
+ % if prevlink:
+ <li class="page-item"><a href="${prevlink}" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">&laquo;</span></a></li>
+ % else:
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">&laquo;</span></a></li>
+ % endif
+ % endif
+ % for i, link in enumerate(page_links):
+ % if abs(i - current_page) <= surrounding or i == 0 or i == len(page_links) - 1:
+ <li class="page-item ${'active' if i == current_page else ''}"><a href="${link}" class="page-link">${i + 1}${' <span class="sr-only">(current)</span>' if i == current_page else ''}</a></li>
+ % elif i == current_page - surrounding - 1 or i == current_page + surrounding + 1:
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="…"><span aria-hidden="true">…</span></a></li>
+ % endif
+ % endfor
+ % if prev_next_links_reversed:
+ % if prevlink:
+ <li class="page-item"><a href="${prevlink}" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">&raquo;</span></a></li>
+ % else:
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Newer posts")}"><span aria-hidden="true">&raquo;</span></a></li>
+ % endif
+ % else:
+ % if nextlink:
+ <li class="page-item"><a href="${nextlink}" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">&raquo;</span></a></li>
+ % else:
+ <li class="page-item disabled"><a href="#" class="page-link" aria-label="${messages("Older posts")}"><span aria-hidden="true">&raquo;</span></a></li>
+ % endif
+ % endif
+ </ul>
+</nav>
+</%def>
diff --git a/nikola/data/themes/bootstrap3/templates/post.tmpl b/nikola/data/themes/bootstrap4/templates/post.tmpl
index 469c1e1..0d4248e 100644
--- a/nikola/data/themes/bootstrap3/templates/post.tmpl
+++ b/nikola/data/themes/bootstrap4/templates/post.tmpl
@@ -2,16 +2,15 @@
<%namespace name="helper" file="post_helper.tmpl"/>
<%namespace name="pheader" file="post_header.tmpl"/>
<%namespace name="comments" file="comments_helper.tmpl"/>
+<%namespace name="math" file="math_helper.tmpl"/>
+<%namespace name="ui" file="ui_helper.tmpl"/>
<%inherit file="base.tmpl"/>
<%block name="extra_head">
${parent.extra_head()}
% if post.meta('keywords'):
- <meta name="keywords" content="${post.meta('keywords')|h}">
+ <meta name="keywords" content="${smartjoin(', ', post.meta('keywords'))|h}">
% endif
- %if post.description():
- <meta name="description" itemprop="description" content="${post.description()|h}">
- %endif
<meta name="author" content="${post.author()|h}">
%if post.prev_post:
<link rel="prev" href="${post.prev_post.permalink()}" title="${post.prev_post.title()|h}" type="text/html">
@@ -25,6 +24,7 @@
${helper.open_graph_metadata(post)}
${helper.twitter_card_information(post)}
${helper.meta_translations(post)}
+ ${math.math_styles_ifpost(post)}
</%block>
<%block name="content">
@@ -45,15 +45,13 @@
${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)}
</section>
% endif
- ${helper.mathjax_script(post)}
+ ${math.math_scripts_ifpost(post)}
</article>
${comments.comment_link_script()}
</%block>
<%block name="sourcelink">
% if show_sourcelink:
- <li>
- <a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a>
- </li>
+ ${ui.show_sourcelink(post.source_link())}
% endif
</%block>
diff --git a/nikola/data/themes/bootstrap3/templates/tags.tmpl b/nikola/data/themes/bootstrap4/templates/tags.tmpl
index 061bb39..f1870f6 100644
--- a/nikola/data/themes/bootstrap3/templates/tags.tmpl
+++ b/nikola/data/themes/bootstrap4/templates/tags.tmpl
@@ -11,7 +11,7 @@
% for i in range(indent_change_before):
<ul class="list-inline">
% endfor
- <li><a class="reference badge" href="${link}">${text|h}</a>
+ <li class="list-inline-item"><a class="reference badge badge-secondary" href="${link}">${text|h}</a>
% if indent_change_after <= 0:
</li>
% endif
@@ -30,7 +30,7 @@
<ul class="list-inline">
% for text, link in items:
% if text not in hidden_tags:
- <li><a class="reference badge" href="${link}">${text|h}</a></li>
+ <li class="list-inline-item"><a class="reference badge badge-secondary" href="${link}">${text|h}</a></li>
% endif
% endfor
</ul>
diff --git a/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl b/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl
new file mode 100644
index 0000000..7e884f9
--- /dev/null
+++ b/nikola/data/themes/bootstrap4/templates/ui_helper.tmpl
@@ -0,0 +1,24 @@
+## -*- coding: utf-8 -*-
+<%def name="breadcrumbs(crumbs)">
+%if crumbs:
+<nav class="breadcrumbs">
+<ul class="breadcrumb">
+ % for link, text in crumbs:
+ % if text != index_file:
+ % if link == '#':
+ <li class="breadcrumb-item active">${text.rsplit('.html', 1)[0]}</li>
+ % else:
+ <li class="breadcrumb-item"><a href="${link}">${text}</a></li>
+ % endif
+ % endif
+ % endfor
+</ul>
+</nav>
+%endif
+</%def>
+
+<%def name="show_sourcelink(sourcelink_href)">
+ <li class="nav-item">
+ <a href="${sourcelink_href}" id="sourcelink" class="nav-link">${messages("Source")}</a>
+ </li>
+</%def>
diff --git a/nikola/filters.py b/nikola/filters.py
index b53e605..9d7e492 100644
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,25 +24,43 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Utility functions to help run filters on files."""
+"""Utility functions to help run filters on files.
+
+All filters defined in this module are registered in Nikola.__init__.
+"""
-from functools import wraps
-import os
import io
import json
+import os
+import re
import shutil
+import shlex
import subprocess
import tempfile
-import shlex
+from functools import wraps
import lxml
+import requests
+
+from .utils import req_missing, LOGGER, slugify
+
try:
import typogrify.filters as typo
except ImportError:
- typo = None # NOQA
-import requests
+ typo = None
+
-from .utils import req_missing, LOGGER
+class _ConfigurableFilter(object):
+ """Allow Nikola to configure filter with site's config."""
+
+ def __init__(self, **configuration_variables):
+ """Define which arguments to configure from which configuration variables."""
+ self.configuration_variables = configuration_variables
+
+ def __call__(self, f):
+ """Store configuration_variables as attribute of function."""
+ f.configuration_variables = self.configuration_variables
+ return f
def apply_to_binary_file(f):
@@ -53,10 +71,10 @@ def apply_to_binary_file(f):
in place. Reads files in binary mode.
"""
@wraps(f)
- def f_in_file(fname):
+ def f_in_file(fname, *args, **kwargs):
with open(fname, 'rb') as inf:
data = inf.read()
- data = f(data)
+ data = f(data, *args, **kwargs)
with open(fname, 'wb+') as outf:
outf.write(data)
@@ -71,10 +89,10 @@ def apply_to_text_file(f):
in place. Reads files in UTF-8.
"""
@wraps(f)
- def f_in_file(fname):
- with io.open(fname, 'r', encoding='utf-8') as inf:
+ def f_in_file(fname, *args, **kwargs):
+ with io.open(fname, 'r', encoding='utf-8-sig') as inf:
data = inf.read()
- data = f(data)
+ data = f(data, *args, **kwargs)
with io.open(fname, 'w+', encoding='utf-8') as outf:
outf.write(data)
@@ -126,60 +144,76 @@ def runinplace(command, infile):
shutil.rmtree(tmpdir)
-def yui_compressor(infile):
+@_ConfigurableFilter(executable='YUI_COMPRESSOR_EXECUTABLE')
+def yui_compressor(infile, executable=None):
"""Run YUI Compressor on a file."""
- yuicompressor = False
- try:
- subprocess.call('yui-compressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
- yuicompressor = 'yui-compressor'
- except Exception:
- pass
+ yuicompressor = executable
+ if not yuicompressor:
+ try:
+ subprocess.call('yui-compressor', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ yuicompressor = 'yui-compressor'
+ except Exception:
+ pass
if not yuicompressor:
try:
- subprocess.call('yuicompressor', stdout=open(os.devnull, 'w'), stderr=open(os.devnull, 'w'))
+ subprocess.call('yuicompressor', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
yuicompressor = 'yuicompressor'
- except:
+ except Exception:
raise Exception("yui-compressor is not installed.")
return False
return runinplace('{} --nomunge %1 -o %2'.format(yuicompressor), infile)
-def closure_compiler(infile):
+@_ConfigurableFilter(executable='CLOSURE_COMPILER_EXECUTABLE')
+def closure_compiler(infile, executable='closure-compiler'):
"""Run closure-compiler on a file."""
- return runinplace('closure-compiler --warning_level QUIET --js %1 --js_output_file %2', infile)
+ return runinplace('{} --warning_level QUIET --js %1 --js_output_file %2'.format(executable), infile)
-def optipng(infile):
+@_ConfigurableFilter(executable='OPTIPNG_EXECUTABLE')
+def optipng(infile, executable='optipng'):
"""Run optipng on a file."""
- return runinplace("optipng -preserve -o2 -quiet %1", infile)
+ return runinplace("{} -preserve -o2 -quiet %1".format(executable), infile)
-def jpegoptim(infile):
+@_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE')
+def jpegoptim(infile, executable='jpegoptim'):
"""Run jpegoptim on a file."""
- return runinplace("jpegoptim -p --strip-all -q %1", infile)
+ return runinplace("{} -p --strip-all -q %1".format(executable), infile)
+@_ConfigurableFilter(executable='JPEGOPTIM_EXECUTABLE')
+def jpegoptim_progressive(infile, executable='jpegoptim'):
+ """Run jpegoptim on a file and convert to progressive."""
+ return runinplace("{} -p --strip-all --all-progressive -q %1".format(executable), infile)
+
+
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
def html_tidy_withconfig(infile, executable='tidy5'):
"""Run HTML Tidy with tidy5.conf as config file."""
return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent -config tidy5.conf -modify %1", executable=executable)
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
def html_tidy_nowrap(infile, executable='tidy5'):
"""Run HTML Tidy without line wrapping."""
return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
def html_tidy_wrap(infile, executable='tidy5'):
"""Run HTML Tidy with line wrapping."""
return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes no --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
def html_tidy_wrap_attr(infile, executable='tidy5'):
"""Run HTML tidy with line wrapping and attribute indentation."""
return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 -indent --indent-attributes yes --sort-attributes alpha --wrap 80 --wrap-sections no --drop-empty-elements no --tidy-mark no -modify %1", executable=executable)
+@_ConfigurableFilter(executable='HTML_TIDY_EXECUTABLE')
def html_tidy_mini(infile, executable='tidy5'):
"""Run HTML tidy with minimal settings."""
return _html_tidy_runner(infile, "-quiet --show-info no --show-warnings no -utf8 --indent-attributes no --sort-attributes alpha --wrap 0 --wrap-sections no --tidy-mark no --drop-empty-elements no -modify %1", executable=executable)
@@ -202,7 +236,7 @@ def html5lib_minify(data):
import html5lib.serializer
data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'),
tree='lxml',
- quote_attr_values=False,
+ quote_attr_values='spec',
omit_optional_tags=True,
minimize_boolean_attributes=True,
strip_whitespace=True,
@@ -218,7 +252,7 @@ def html5lib_xmllike(data):
import html5lib.serializer
data = html5lib.serializer.serialize(html5lib.parse(data, treebuilder='lxml'),
tree='lxml',
- quote_attr_values=True,
+ quote_attr_values='always',
omit_optional_tags=False,
strip_whitespace=False,
alphabetical_attributes=True,
@@ -232,21 +266,53 @@ def minify_lines(data):
return data
+def _run_typogrify(data, typogrify_filters, ignore_tags=None):
+ """Run typogrify with ignore support."""
+ if ignore_tags is None:
+ ignore_tags = ["title"]
+
+ data = _normalize_html(data)
+
+ section_list = typo.process_ignores(data, ignore_tags)
+
+ rendered_text = ""
+ for text_item, should_process in section_list:
+ if should_process:
+ for f in typogrify_filters:
+ text_item = f(text_item)
+
+ rendered_text += text_item
+
+ return rendered_text
+
+
@apply_to_text_file
def typogrify(data):
"""Prettify text with typogrify."""
if typo is None:
req_missing(['typogrify'], 'use the typogrify filter', optional=True)
return data
+ return _run_typogrify(data, [typo.amp, typo.widont, typo.smartypants, typo.caps, typo.initial_quotes])
- data = _normalize_html(data)
- data = typo.amp(data)
- data = typo.widont(data)
- data = typo.smartypants(data)
- # Disabled because of typogrify bug where it breaks <title>
- # data = typo.caps(data)
- data = typo.initial_quotes(data)
- return data
+
+def _smarty_oldschool(text):
+ try:
+ import smartypants
+ except ImportError:
+ raise typo.TypogrifyError("Error in {% smartypants %} filter: The Python smartypants library isn't installed.")
+ else:
+ output = smartypants.convert_dashes_oldschool(text)
+ return output
+
+
+@apply_to_text_file
+def typogrify_oldschool(data):
+ """Prettify text with typogrify."""
+ if typo is None:
+ req_missing(['typogrify'], 'use the typogrify_oldschool filter', optional=True)
+ return data
+
+ return _run_typogrify(data, [typo.amp, typo.widont, _smarty_oldschool, typo.smartypants, typo.caps, typo.initial_quotes])
@apply_to_text_file
@@ -256,28 +322,31 @@ def typogrify_sans_widont(data):
# wrapping, see issue #1465
if typo is None:
req_missing(['typogrify'], 'use the typogrify_sans_widont filter')
+ return data
- data = _normalize_html(data)
- data = typo.amp(data)
- data = typo.smartypants(data)
- # Disabled because of typogrify bug where it breaks <title>
- # data = typo.caps(data)
- data = typo.initial_quotes(data)
- return data
+ return _run_typogrify(data, [typo.amp, typo.smartypants, typo.caps, typo.initial_quotes])
+
+
+@apply_to_text_file
+def typogrify_custom(data, typogrify_filters, ignore_tags=None):
+ """Run typogrify with a custom list of fliter functions."""
+ if typo is None:
+ req_missing(['typogrify'], 'use the typogrify filter', optional=True)
+ return data
+ return _run_typogrify(data, typogrify_filters, ignore_tags)
@apply_to_text_file
def php_template_injection(data):
"""Insert PHP code into Nikola templates."""
- import re
- template = re.search('<\!-- __NIKOLA_PHP_TEMPLATE_INJECTION source\:(.*) checksum\:(.*)__ -->', data)
+ template = re.search(r'<\!-- __NIKOLA_PHP_TEMPLATE_INJECTION source\:(.*) checksum\:(.*)__ -->', data)
if template:
source = template.group(1)
- with io.open(source, "r", encoding="utf-8") as in_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
phpdata = in_file.read()
_META_SEPARATOR = '(' + os.linesep * 2 + '|' + ('\n' * 2) + '|' + ("\r\n" * 2) + ')'
phpdata = re.split(_META_SEPARATOR, phpdata, maxsplit=1)[-1]
- phpdata = re.sub(template.group(0), phpdata, data)
+ phpdata = data.replace(template.group(0), phpdata)
return phpdata
else:
return data
@@ -285,9 +354,9 @@ def php_template_injection(data):
@apply_to_text_file
def cssminify(data):
- """Minify CSS using http://cssminifier.com/."""
+ """Minify CSS using https://cssminifier.com/."""
try:
- url = 'http://cssminifier.com/raw'
+ url = 'https://cssminifier.com/raw'
_data = {'input': data}
response = requests.post(url, data=_data)
if response.status_code != 200:
@@ -301,9 +370,9 @@ def cssminify(data):
@apply_to_text_file
def jsminify(data):
- """Minify JS using http://javascript-minifier.com/."""
+ """Minify JS using https://javascript-minifier.com/."""
try:
- url = 'http://javascript-minifier.com/raw'
+ url = 'https://javascript-minifier.com/raw'
_data = {'input': data}
response = requests.post(url, data=_data)
if response.status_code != 200:
@@ -334,9 +403,108 @@ def _normalize_html(data):
"""Pass HTML through LXML to clean it up, if possible."""
try:
data = lxml.html.tostring(lxml.html.fromstring(data), encoding='unicode')
- except:
+ except Exception:
pass
return '<!DOCTYPE html>\n' + data
+# The function is used in other filters, so the decorator cannot be used directly.
normalize_html = apply_to_text_file(_normalize_html)
+
+
+@_ConfigurableFilter(xpath_list='HEADER_PERMALINKS_XPATH_LIST', file_blacklist='HEADER_PERMALINKS_FILE_BLACKLIST')
+def add_header_permalinks(fname, xpath_list=None, file_blacklist=None):
+ """Post-process HTML via lxml to add header permalinks Sphinx-style."""
+ # Blacklist requires custom file handling
+ file_blacklist = file_blacklist or []
+ if fname in file_blacklist:
+ return
+ with io.open(fname, 'r', encoding='utf-8-sig') as inf:
+ data = inf.read()
+ doc = lxml.html.document_fromstring(data)
+ # Get language for slugify
+ try:
+ lang = doc.attrib['lang'] # <html lang="…">
+ except KeyError:
+ # Circular import workaround (utils imports filters)
+ from nikola.utils import LocaleBorg
+ lang = LocaleBorg().current_lang
+
+ xpath_set = set()
+ if not xpath_list:
+ xpath_list = ['*//div[@class="e-content entry-content"]//{hx}']
+ for xpath_expr in xpath_list:
+ for hx in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
+ xpath_set.add(xpath_expr.format(hx=hx))
+ for x in xpath_set:
+ nodes = doc.findall(x)
+ for node in nodes:
+ parent = node.getparent()
+ if 'id' in node.attrib:
+ hid = node.attrib['id']
+ elif 'id' in parent.attrib:
+ # docutils: <div> has an ID and contains the header
+ hid = parent.attrib['id']
+ else:
+ # Using force-mode, because not every character can appear in a
+ # HTML id
+ node.attrib['id'] = slugify(node.text_content(), lang, True)
+ hid = node.attrib['id']
+
+ new_node = lxml.html.fragment_fromstring('<a href="#{0}" class="headerlink" title="Permalink to this heading">¶</a>'.format(hid))
+ node.append(new_node)
+
+ with io.open(fname, 'w', encoding='utf-8') as outf:
+ outf.write('<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding="unicode"))
+
+
+@_ConfigurableFilter(top_classes='DEDUPLICATE_IDS_TOP_CLASSES')
+@apply_to_text_file
+def deduplicate_ids(data, top_classes=None):
+ """Post-process HTML via lxml to deduplicate IDs."""
+ if not top_classes:
+ top_classes = ('postpage', 'storypage')
+ doc = lxml.html.document_fromstring(data)
+ elements = doc.xpath('//*')
+ all_ids = [element.attrib.get('id') for element in elements]
+ seen_ids = set()
+ duplicated_ids = set()
+ for i in all_ids:
+ if i is not None and i in seen_ids:
+ duplicated_ids.add(i)
+ else:
+ seen_ids.add(i)
+
+ if duplicated_ids:
+ # Well, that sucks.
+ for i in duplicated_ids:
+ # Results are ordered the same way they are ordered in document
+ offending_elements = doc.xpath('//*[@id="{}"]'.format(i))
+ counter = 2
+ # If this is a story or a post, do it from top to bottom, because
+ # updates to those are more likely to appear at the bottom of pages.
+ # For anything else, including indexes, do it from bottom to top,
+ # because new posts appear at the top of pages.
+ # We also leave the first result out, so there is one element with
+ # "plain" ID
+ if any(doc.find_class(c) for c in top_classes):
+ off = offending_elements[1:]
+ else:
+ off = offending_elements[-2::-1]
+ for e in off:
+ new_id = i
+ while new_id in seen_ids:
+ new_id = '{0}-{1}'.format(i, counter)
+ counter += 1
+ e.attrib['id'] = new_id
+ seen_ids.add(new_id)
+ # Find headerlinks that we can fix.
+ headerlinks = e.find_class('headerlink')
+ for hl in headerlinks:
+ # We might get headerlinks of child elements
+ if hl.attrib['href'] == '#' + i:
+ hl.attrib['href'] = '#' + new_id
+ break
+ return '<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='unicode')
+ else:
+ return data
diff --git a/nikola/hierarchy_utils.py b/nikola/hierarchy_utils.py
new file mode 100644
index 0000000..8993518
--- /dev/null
+++ b/nikola/hierarchy_utils.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Hierarchy utility functions."""
+
+import natsort
+
+__all__ = ('TreeNode', 'clone_treenode', 'flatten_tree_structure',
+ 'sort_classifications', 'join_hierarchical_category_path',
+ 'parse_escaped_hierarchical_category_name',)
+
+
+class TreeNode(object):
+ """A tree node."""
+
+ indent_levels = None # use for formatting comments as tree
+ indent_change_before = 0 # use for formatting comments as tree
+ indent_change_after = 0 # use for formatting comments as tree
+
+ # The indent levels and changes allow to render a tree structure
+ # without keeping track of all that information during rendering.
+ #
+ # The indent_change_before is the different between the current
+ # comment's level and the previous comment's level; if the number
+ # is positive, the current level is indented further in, and if it
+ # is negative, it is indented further out. Positive values can be
+ # used to open HTML tags for each opened level.
+ #
+ # The indent_change_after is the difference between the next
+ # comment's level and the current comment's level. Negative values
+ # can be used to close HTML tags for each closed level.
+ #
+ # The indent_levels list contains one entry (index, count) per
+ # level, informing about the index of the current comment on that
+ # level and the count of comments on that level (before a comment
+ # of a higher level comes). This information can be used to render
+ # tree indicators, for example to generate a tree such as:
+ #
+ # +--- [(0,3)]
+ # +-+- [(1,3)]
+ # | +--- [(1,3), (0,2)]
+ # | +-+- [(1,3), (1,2)]
+ # | +--- [(1,3), (1,2), (0, 1)]
+ # +-+- [(2,3)]
+ # +- [(2,3), (0,1)]
+ #
+ # (The lists used as labels represent the content of the
+ # indent_levels property for that node.)
+
+ def __init__(self, name, parent=None):
+ """Initialize node."""
+ self.name = name
+ self.parent = parent
+ self.children = []
+
+ def get_path(self):
+ """Get path."""
+ path = []
+ curr = self
+ while curr is not None:
+ path.append(curr)
+ curr = curr.parent
+ return reversed(path)
+
+ def get_children(self):
+ """Get children of a node."""
+ return self.children
+
+ def __str__(self):
+ """Stringify node (return name)."""
+ return self.name
+
+ def _repr_partial(self):
+ """Return partial representation."""
+ if self.parent:
+ return "{0}/{1!r}".format(self.parent._repr_partial(), self.name)
+ else:
+ return repr(self.name)
+
+ def __repr__(self):
+ """Return programmer-friendly node representation."""
+ return "<TreeNode {0}>".format(self._repr_partial())
+
+
+def clone_treenode(treenode, parent=None, acceptor=lambda x: True):
+ """Clone a TreeNode.
+
+ Children are only cloned if `acceptor` returns `True` when
+ applied on them.
+
+ Returns the cloned node if it has children or if `acceptor`
+ applied to it returns `True`. In case neither applies, `None`
+ is returned.
+ """
+ # Copy standard TreeNode stuff
+ node_clone = TreeNode(treenode.name, parent)
+ node_clone.children = [clone_treenode(node, parent=node_clone, acceptor=acceptor) for node in treenode.children]
+ node_clone.children = [node for node in node_clone.children if node]
+ node_clone.indent_levels = treenode.indent_levels
+ node_clone.indent_change_before = treenode.indent_change_before
+ node_clone.indent_change_after = treenode.indent_change_after
+ if hasattr(treenode, 'classification_path'):
+ # Copy stuff added by taxonomies_classifier plugin
+ node_clone.classification_path = treenode.classification_path
+ node_clone.classification_name = treenode.classification_name
+
+ # Accept this node if there are no children (left) and acceptor fails
+ if not node_clone.children and not acceptor(treenode):
+ return None
+ return node_clone
+
+
+def flatten_tree_structure(root_list):
+ """Flatten a tree."""
+ elements = []
+
+ def generate(input_list, indent_levels_so_far):
+ """Generate flat list of nodes."""
+ for index, element in enumerate(input_list):
+ # add to destination
+ elements.append(element)
+ # compute and set indent levels
+ indent_levels = indent_levels_so_far + [(index, len(input_list))]
+ element.indent_levels = indent_levels
+ # add children
+ children = element.get_children()
+ element.children_count = len(children)
+ generate(children, indent_levels)
+
+ generate(root_list, [])
+ # Add indent change counters
+ level = 0
+ last_element = None
+ for element in elements:
+ new_level = len(element.indent_levels)
+ # Compute level change before this element
+ change = new_level - level
+ if last_element is not None:
+ last_element.indent_change_after = change
+ element.indent_change_before = change
+ # Update variables
+ level = new_level
+ last_element = element
+ # Set level change after last element
+ if last_element is not None:
+ last_element.indent_change_after = -level
+ return elements
+
+
+def parse_escaped_hierarchical_category_name(category_name):
+ """Parse a category name."""
+ result = []
+ current = None
+ index = 0
+ next_backslash = category_name.find('\\', index)
+ next_slash = category_name.find('/', index)
+ while index < len(category_name):
+ if next_backslash == -1 and next_slash == -1:
+ current = (current if current else "") + category_name[index:]
+ index = len(category_name)
+ elif next_slash >= 0 and (next_backslash == -1 or next_backslash > next_slash):
+ result.append((current if current else "") + category_name[index:next_slash])
+ current = ''
+ index = next_slash + 1
+ next_slash = category_name.find('/', index)
+ else:
+ if len(category_name) == next_backslash + 1:
+ raise Exception("Unexpected '\\' in '{0}' at last position!".format(category_name))
+ esc_ch = category_name[next_backslash + 1]
+ if esc_ch not in {'/', '\\'}:
+ raise Exception("Unknown escape sequence '\\{0}' in '{1}'!".format(esc_ch, category_name))
+ current = (current if current else "") + category_name[index:next_backslash] + esc_ch
+ index = next_backslash + 2
+ next_backslash = category_name.find('\\', index)
+ if esc_ch == '/':
+ next_slash = category_name.find('/', index)
+ if current is not None:
+ result.append(current)
+ return result
+
+
+def join_hierarchical_category_path(category_path):
+ """Join a category path."""
+ def escape(s):
+ """Espace one part of category path."""
+ return s.replace('\\', '\\\\').replace('/', '\\/')
+
+ return '/'.join([escape(p) for p in category_path])
+
+
+def sort_classifications(taxonomy, classifications, lang):
+ """Sort the given list of classifications of the given taxonomy and language.
+
+ ``taxonomy`` must be a ``Taxonomy`` plugin.
+ ``classifications`` must be an iterable collection of
+ classification strings for that taxonomy.
+ ``lang`` is the language the classifications are for.
+
+ The result will be returned as a sorted list. Sorting will
+ happen according to the way the complete classification
+ hierarchy for the taxonomy is sorted.
+ """
+ if taxonomy.has_hierarchy:
+ # To sort a hierarchy of classifications correctly, we first
+ # build a tree out of them (and mark for each node whether it
+ # appears in the list), then sort the tree node-wise, and finally
+ # collapse the tree into a list of recombined classifications.
+
+ # Step 1: build hierarchy. Here, each node consists of a boolean
+ # flag (node appears in list) and a dictionary mapping path elements
+ # to nodes.
+ root = [False, {}]
+ for classification in classifications:
+ node = root
+ for elt in taxonomy.extract_hierarchy(classification):
+ if elt not in node[1]:
+ node[1][elt] = [False, {}]
+ node = node[1][elt]
+ node[0] = True
+ # Step 2: sort hierarchy. The result for a node is a pair
+ # (flag, subnodes), where subnodes is a list of pairs (name, subnode).
+
+ def sort_node(node, level=0):
+ """Return sorted node, with children as `(name, node)` list instead of a dictionary."""
+ keys = natsort.natsorted(node[1].keys(), alg=natsort.ns.F | natsort.ns.IC)
+ taxonomy.sort_classifications(keys, lang, level)
+ subnodes = []
+ for key in keys:
+ subnodes.append((key, sort_node(node[1][key], level + 1)))
+ return (node[0], subnodes)
+
+ root = sort_node(root)
+ # Step 3: collapse the tree structure into a linear sorted list,
+ # with a node coming before its children.
+
+ def append_node(classifications, node, path=()):
+ """Append the node and then its children to the classifications list."""
+ if node[0]:
+ classifications.append(taxonomy.recombine_classification_from_hierarchy(path))
+ for key, subnode in node[1]:
+ append_node(classifications, subnode, path + (key, ))
+
+ classifications = []
+ append_node(classifications, root)
+ return classifications
+ else:
+ # Sorting a flat hierarchy is simpler. We pre-sort with
+ # natsorted and call taxonomy.sort_classifications.
+ classifications = natsort.natsorted(classifications, alg=natsort.ns.F | natsort.ns.IC)
+ taxonomy.sort_classifications(classifications, lang)
+ return classifications
diff --git a/nikola/image_processing.py b/nikola/image_processing.py
index e0096b2..04d4e64 100644
--- a/nikola/image_processing.py
+++ b/nikola/image_processing.py
@@ -26,35 +26,24 @@
"""Process images."""
-from __future__ import unicode_literals
import datetime
+import gzip
import os
-import lxml
import re
-import gzip
+import lxml
import piexif
+from PIL import ExifTags, Image
from nikola import utils
-Image = None
-try:
- from PIL import ExifTags, Image # NOQA
-except ImportError:
- try:
- import ExifTags
- import Image as _Image
- Image = _Image
- except ImportError:
- pass
-
EXIF_TAG_NAMES = {}
class ImageProcessor(object):
"""Apply image operations."""
- image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff']
+ image_ext_list_builtin = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.svgz', '.bmp', '.tiff', '.webp']
def _fill_exif_tag_names(self):
"""Connect EXIF tag names to numeric values."""
@@ -92,101 +81,136 @@ class ImageProcessor(object):
return exif or None
- def resize_image(self, src, dst, max_size, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}):
- """Make a copy of the image in the requested size."""
- if not Image or os.path.splitext(src)[1] in ['.svg', '.svgz']:
- self.resize_svg(src, dst, max_size, bigger_panoramas)
+ def resize_image(self, src, dst=None, max_size=None, bigger_panoramas=True, preserve_exif_data=False, exif_whitelist={}, preserve_icc_profiles=False, dst_paths=None, max_sizes=None):
+ """Make a copy of the image in the requested size(s).
+
+ max_sizes should be a list of sizes, and the image would be resized to fit in a
+ square of each size (preserving aspect ratio).
+
+ dst_paths is a list of the destination paths, and should be the same length as max_sizes.
+
+ Backwards compatibility:
+
+ * If max_sizes is None, it's set to [max_size]
+ * If dst_paths is None, it's set to [dst]
+ * Either max_size or max_sizes should be set
+ * Either dst or dst_paths should be set
+ """
+ if dst_paths is None:
+ dst_paths = [dst]
+ if max_sizes is None:
+ max_sizes = [max_size]
+ if len(max_sizes) != len(dst_paths):
+ raise ValueError('resize_image called with incompatible arguments: {} / {}'.format(dst_paths, max_sizes))
+ extension = os.path.splitext(src)[1].lower()
+ if extension in {'.svg', '.svgz'}:
+ self.resize_svg(src, dst_paths, max_sizes, bigger_panoramas)
return
- im = Image.open(src)
- size = w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
-
- # Panoramas get larger thumbnails because they look *awful*
- if bigger_panoramas and w > 2 * h:
- size = min(w, max_size * 4), min(w, max_size * 4)
-
- try:
- exif = piexif.load(im.info["exif"])
- except KeyError:
- exif = None
- # Inside this if, we can manipulate exif as much as
- # we want/need and it will be preserved if required
- if exif is not None:
+
+ _im = Image.open(src)
+
+ # The jpg exclusion is Issue #3332
+ is_animated = hasattr(_im, 'n_frames') and _im.n_frames > 1 and extension not in {'.jpg', '.jpeg'}
+
+ exif = None
+ if "exif" in _im.info:
+ exif = piexif.load(_im.info["exif"])
# Rotate according to EXIF
- value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)
- if value in (3, 4):
- im = im.transpose(Image.ROTATE_180)
- elif value in (5, 6):
- im = im.transpose(Image.ROTATE_270)
- elif value in (7, 8):
- im = im.transpose(Image.ROTATE_90)
- if value in (2, 4, 5, 7):
- im = im.transpose(Image.FLIP_LEFT_RIGHT)
- exif['0th'][piexif.ImageIFD.Orientation] = 1
-
- try:
- im.thumbnail(size, Image.ANTIALIAS)
- if exif is not None and preserve_exif_data:
- # Put right size in EXIF data
- w, h = im.size
- if '0th' in exif:
- exif["0th"][piexif.ImageIFD.ImageWidth] = w
- exif["0th"][piexif.ImageIFD.ImageLength] = h
- if 'Exif' in exif:
- exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
- exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
- # Filter EXIF data as required
- exif = self.filter_exif(exif, exif_whitelist)
- im.save(dst, exif=piexif.dump(exif))
- else:
- im.save(dst)
- except Exception as e:
- self.logger.warn("Can't process {0}, using original "
- "image! ({1})".format(src, e))
- utils.copy_file(src, dst)
-
- def resize_svg(self, src, dst, max_size, bigger_panoramas):
- """Make a copy of an svg at the requested size."""
- try:
- # Resize svg based on viewport hacking.
- # note that this can also lead to enlarged svgs
- if src.endswith('.svgz'):
- with gzip.GzipFile(src, 'rb') as op:
- xml = op.read()
- else:
- with open(src, 'rb') as op:
- xml = op.read()
- tree = lxml.etree.XML(xml)
- width = tree.attrib['width']
- height = tree.attrib['height']
- w = int(re.search("[0-9]+", width).group(0))
- h = int(re.search("[0-9]+", height).group(0))
- # calculate new size preserving aspect ratio.
- ratio = float(w) / h
- # Panoramas get larger thumbnails because they look *awful*
- if bigger_panoramas and w > 2 * h:
- max_size = max_size * 4
- if w > h:
- w = max_size
- h = max_size / ratio
- else:
- w = max_size * ratio
- h = max_size
- w = int(w)
- h = int(h)
- tree.attrib.pop("width")
- tree.attrib.pop("height")
- tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
- if dst.endswith('.svgz'):
- op = gzip.GzipFile(dst, 'wb')
- else:
- op = open(dst, 'wb')
- op.write(lxml.etree.tostring(tree))
- op.close()
- except (KeyError, AttributeError) as e:
- self.logger.warn("No width/height in %s. Original exception: %s" % (src, e))
- utils.copy_file(src, dst)
+ if "0th" in exif:
+ value = exif['0th'].get(piexif.ImageIFD.Orientation, 1)
+ if value in (3, 4):
+ _im = _im.transpose(Image.ROTATE_180)
+ elif value in (5, 6):
+ _im = _im.transpose(Image.ROTATE_270)
+ elif value in (7, 8):
+ _im = _im.transpose(Image.ROTATE_90)
+ if value in (2, 4, 5, 7):
+ _im = _im.transpose(Image.FLIP_LEFT_RIGHT)
+ exif['0th'][piexif.ImageIFD.Orientation] = 1
+ exif = self.filter_exif(exif, exif_whitelist)
+
+ icc_profile = _im.info.get('icc_profile') if preserve_icc_profiles else None
+
+ for dst, max_size in zip(dst_paths, max_sizes):
+ if is_animated: # Animated gif, leave as-is
+ utils.copy_file(src, dst)
+ continue
+
+ im = _im.copy()
+
+ size = w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+ # Panoramas get larger thumbnails because they look *awful*
+ if bigger_panoramas and w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ save_args = {}
+ if icc_profile:
+ save_args['icc_profile'] = icc_profile
+
+ if exif is not None and preserve_exif_data:
+ # Put right size in EXIF data
+ w, h = im.size
+ if '0th' in exif:
+ exif["0th"][piexif.ImageIFD.ImageWidth] = w
+ exif["0th"][piexif.ImageIFD.ImageLength] = h
+ if 'Exif' in exif:
+ exif["Exif"][piexif.ExifIFD.PixelXDimension] = w
+ exif["Exif"][piexif.ExifIFD.PixelYDimension] = h
+ # Filter EXIF data as required
+ save_args['exif'] = piexif.dump(exif)
+
+ im.save(dst, **save_args)
+ except Exception as e:
+ self.logger.warning("Can't process {0}, using original "
+ "image! ({1})".format(src, e))
+ utils.copy_file(src, dst)
+
+ def resize_svg(self, src, dst_paths, max_sizes, bigger_panoramas):
+ """Make a copy of an svg at the requested sizes."""
+ # Resize svg based on viewport hacking.
+ # note that this can also lead to enlarged svgs
+ if src.endswith('.svgz'):
+ with gzip.GzipFile(src, 'rb') as op:
+ xml = op.read()
+ else:
+ with open(src, 'rb') as op:
+ xml = op.read()
+
+ for dst, max_size in zip(dst_paths, max_sizes):
+ try:
+ tree = lxml.etree.XML(xml)
+ width = tree.attrib['width']
+ height = tree.attrib['height']
+ w = int(re.search("[0-9]+", width).group(0))
+ h = int(re.search("[0-9]+", height).group(0))
+ # calculate new size preserving aspect ratio.
+ ratio = float(w) / h
+ # Panoramas get larger thumbnails because they look *awful*
+ if bigger_panoramas and w > 2 * h:
+ max_size = max_size * 4
+ if w > h:
+ w = max_size
+ h = max_size / ratio
+ else:
+ w = max_size * ratio
+ h = max_size
+ w = int(w)
+ h = int(h)
+ tree.attrib.pop("width")
+ tree.attrib.pop("height")
+ tree.attrib['viewport'] = "0 0 %ipx %ipx" % (w, h)
+ if dst.endswith('.svgz'):
+ op = gzip.GzipFile(dst, 'wb')
+ else:
+ op = open(dst, 'wb')
+ op.write(lxml.etree.tostring(tree))
+ op.close()
+ except (KeyError, AttributeError) as e:
+ self.logger.warning("No width/height in %s. Original exception: %s" % (src, e))
+ utils.copy_file(src, dst)
def image_date(self, src):
"""Try to figure out the date of the image."""
@@ -194,6 +218,7 @@ class ImageProcessor(object):
try:
im = Image.open(src)
exif = im._getexif()
+ im.close()
except Exception:
exif = None
if exif is not None:
diff --git a/nikola/log.py b/nikola/log.py
new file mode 100644
index 0000000..9960ba1
--- /dev/null
+++ b/nikola/log.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Logging support."""
+
+import enum
+import logging
+import warnings
+
+from nikola import DEBUG
+
+__all__ = (
+ "get_logger",
+ "LOGGER",
+)
+
+
+# Handlers/formatters
+class ApplicationWarning(Exception):
+ """An application warning, raised in strict mode."""
+
+ pass
+
+
+class StrictModeExceptionHandler(logging.StreamHandler):
+ """A logging handler that raises an exception on warnings."""
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """Emit a logging record."""
+ if record.levelno >= logging.WARNING:
+ raise ApplicationWarning(self.format(record))
+
+
+class ColorfulFormatter(logging.Formatter):
+ """Stream handler with colors."""
+
+ _colorful = False
+
+ def format(self, record: logging.LogRecord) -> str:
+ """Format a message and add colors to it."""
+ message = super().format(record)
+ return self.wrap_in_color(record).format(message)
+
+ def wrap_in_color(self, record: logging.LogRecord) -> str:
+ """Return the colorized string for this record."""
+ if not self._colorful:
+ return "{}"
+ if record.levelno >= logging.ERROR:
+ return "\033[1;31m{}\033[0m"
+ elif record.levelno >= logging.WARNING:
+ return "\033[1;33m{}\033[0m"
+ elif record.levelno >= logging.INFO:
+ return "\033[1m{}\033[0m"
+ return "\033[37m{}\033[0m"
+
+
+# Initial configuration
+class LoggingMode(enum.Enum):
+ """Logging mode options."""
+
+ NORMAL = 0
+ STRICT = 1
+ QUIET = 2
+
+
+def configure_logging(logging_mode: LoggingMode = LoggingMode.NORMAL) -> None:
+ """Configure logging for Nikola.
+
+ This method can be called multiple times, previous configuration will be overridden.
+ """
+ if DEBUG:
+ logging.root.level = logging.DEBUG
+ else:
+ logging.root.level = logging.INFO
+
+ if logging_mode == LoggingMode.QUIET:
+ logging.root.handlers = []
+ return
+
+ handler = logging.StreamHandler()
+ handler.setFormatter(
+ ColorfulFormatter(
+ fmt="[%(asctime)s] %(levelname)s: %(name)s: %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+ )
+
+ handlers = [handler]
+ if logging_mode == LoggingMode.STRICT:
+ handlers.append(StrictModeExceptionHandler())
+
+ logging.root.handlers = handlers
+
+
+configure_logging()
+
+
+# For compatibility with old code written with Logbook in mind
+# TODO remove in v9
+def patch_notice_level(logger: logging.Logger) -> logging.Logger:
+ """Patch logger to issue WARNINGs with logger.notice."""
+ logger.notice = logger.warning
+ return logger
+
+
+# User-facing loggers
+def get_logger(name: str, handlers=None) -> logging.Logger:
+ """Get a logger with handlers attached."""
+ logger = logging.getLogger(name)
+ if handlers is not None:
+ for h in handlers:
+ logger.addHandler(h)
+ return patch_notice_level(logger)
+
+
+LOGGER = get_logger("Nikola")
+
+
+# Push warnings to logging
+def showwarning(message, category, filename, lineno, file=None, line=None):
+ """Show a warning (from the warnings module) to the user."""
+ try:
+ n = category.__name__
+ except AttributeError:
+ n = str(category)
+ get_logger(n).warning("{0}:{1}: {2}".format(filename, lineno, message))
+
+
+warnings.showwarning = showwarning
diff --git a/nikola/metadata_extractors.py b/nikola/metadata_extractors.py
new file mode 100644
index 0000000..2377dc2
--- /dev/null
+++ b/nikola/metadata_extractors.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Default metadata extractors and helper functions."""
+
+import re
+from enum import Enum
+from io import StringIO
+
+import natsort
+
+from nikola.plugin_categories import MetadataExtractor
+from nikola.utils import unslugify
+
+__all__ = ('MetaCondition', 'MetaPriority', 'MetaSource', 'check_conditions')
+_default_extractors = []
+DEFAULT_EXTRACTOR_NAME = 'nikola'
+DEFAULT_EXTRACTOR = None
+
+
+class MetaCondition(Enum):
+ """Conditions for extracting metadata."""
+
+ config_bool = 1
+ config_present = 2
+ extension = 3
+ compiler = 4
+ first_line = 5
+ never = -1
+
+
+class MetaPriority(Enum):
+ """Priority of metadata.
+
+ An extractor is used if and only if the higher-priority extractors returned nothing.
+ """
+
+ override = 1
+ specialized = 2
+ normal = 3
+ fallback = 4
+
+
+class MetaSource(Enum):
+ """Source of metadata."""
+
+ text = 1
+ filename = 2
+
+
+def check_conditions(post, filename: str, conditions: list, config: dict, source_text: str) -> bool:
+ """Check the conditions for a metadata extractor."""
+ for ct, arg in conditions:
+ if any((
+ ct == MetaCondition.config_bool and not config.get(arg, False),
+ ct == MetaCondition.config_present and arg not in config,
+ ct == MetaCondition.extension and not filename.endswith(arg),
+ ct == MetaCondition.compiler and (post is None or post.compiler.name != arg),
+ ct == MetaCondition.never
+ )):
+ return False
+ elif ct == MetaCondition.first_line:
+ if not source_text or not source_text.startswith(arg + '\n'):
+ return False
+ return True
+
+
+def classify_extractor(extractor: MetadataExtractor, metadata_extractors_by: dict):
+ """Classify an extractor and add it to the metadata_extractors_by dict."""
+ global DEFAULT_EXTRACTOR
+ if extractor.name == DEFAULT_EXTRACTOR_NAME:
+ DEFAULT_EXTRACTOR = extractor
+ metadata_extractors_by['priority'][extractor.priority].append(extractor)
+ metadata_extractors_by['source'][extractor.source].append(extractor)
+ metadata_extractors_by['name'][extractor.name] = extractor
+ metadata_extractors_by['all'].append(extractor)
+
+
+def load_defaults(site, metadata_extractors_by: dict):
+ """Load default metadata extractors."""
+ for extractor in _default_extractors:
+ extractor.site = site
+ classify_extractor(extractor, metadata_extractors_by)
+
+
+def is_extractor(extractor) -> bool: # pragma: no cover
+ """Check if a given class is an extractor."""
+ return isinstance(extractor, MetadataExtractor)
+
+
+def default_metadata_extractors_by() -> dict:
+ """Return the default metadata_extractors_by dictionary."""
+ d = {
+ 'priority': {},
+ 'source': {},
+ 'name': {},
+ 'all': []
+ }
+
+ for i in MetaPriority:
+ d['priority'][i] = []
+ for i in MetaSource:
+ d['source'][i] = []
+
+ return d
+
+
+def _register_default(extractor: type) -> type:
+ """Register a default extractor."""
+ _default_extractors.append(extractor())
+ return extractor
+
+
+@_register_default
+class NikolaMetadata(MetadataExtractor):
+ """Extractor for Nikola-style metadata."""
+
+ name = 'nikola'
+ source = MetaSource.text
+ priority = MetaPriority.normal
+ supports_write = True
+ split_metadata_re = re.compile('\n\n')
+ nikola_re = re.compile(r'^\s*\.\. (.*?): (.*)')
+ map_from = 'nikola' # advertised in values mapping only
+
+ def _extract_metadata_from_text(self, source_text: str) -> dict:
+ """Extract metadata from text."""
+ outdict = {}
+ for line in source_text.split('\n'):
+ match = self.nikola_re.match(line)
+ if match:
+ k, v = match.group(1), match.group(2)
+ if v:
+ outdict[k] = v
+ return outdict
+
+ def write_metadata(self, metadata: dict, comment_wrap=False) -> str:
+ """Write metadata in this extractor’s format."""
+ metadata = metadata.copy()
+ order = ('title', 'slug', 'date', 'tags', 'category', 'link', 'description', 'type')
+ f = '.. {0}: {1}'
+ meta = []
+ for k in order:
+ try:
+ meta.append(f.format(k, metadata.pop(k)))
+ except KeyError:
+ pass
+ # Leftover metadata (user-specified/non-default).
+ for k in natsort.natsorted(list(metadata.keys()), alg=natsort.ns.F | natsort.ns.IC):
+ meta.append(f.format(k, metadata[k]))
+ data = '\n'.join(meta)
+ if comment_wrap is True:
+ comment_wrap = ('<!--', '-->')
+ if comment_wrap:
+ return '\n'.join((comment_wrap[0], data, comment_wrap[1], '', ''))
+ else:
+ return data + '\n\n'
+
+
+@_register_default
+class YAMLMetadata(MetadataExtractor):
+ """Extractor for YAML metadata."""
+
+ name = 'yaml'
+ source = MetaSource.text
+ conditions = ((MetaCondition.first_line, '---'),)
+ requirements = [('ruamel.yaml', 'ruamel.yaml', 'YAML')]
+ supports_write = True
+ split_metadata_re = re.compile('\n---\n')
+ map_from = 'yaml'
+ priority = MetaPriority.specialized
+
+ def _extract_metadata_from_text(self, source_text: str) -> dict:
+ """Extract metadata from text."""
+ from ruamel.yaml import YAML
+ yaml = YAML(typ='safe')
+ meta = yaml.load(source_text[4:])
+ # We expect empty metadata to be '', not None
+ for k in meta:
+ if meta[k] is None:
+ meta[k] = ''
+ return meta
+
+ def write_metadata(self, metadata: dict, comment_wrap=False) -> str:
+ """Write metadata in this extractor’s format."""
+ from ruamel.yaml import YAML
+ yaml = YAML(typ='safe')
+ yaml.default_flow_style = False
+ stream = StringIO()
+ yaml.dump(metadata, stream)
+ stream.seek(0)
+ return '\n'.join(('---', stream.read().strip(), '---', ''))
+
+
+@_register_default
+class TOMLMetadata(MetadataExtractor):
+ """Extractor for TOML metadata."""
+
+ name = 'toml'
+ source = MetaSource.text
+ conditions = ((MetaCondition.first_line, '+++'),)
+ requirements = [('toml', 'toml', 'TOML')]
+ supports_write = True
+ split_metadata_re = re.compile('\n\\+\\+\\+\n')
+ map_from = 'toml'
+ priority = MetaPriority.specialized
+
+ def _extract_metadata_from_text(self, source_text: str) -> dict:
+ """Extract metadata from text."""
+ import toml
+ return toml.loads(source_text[4:])
+
+ def write_metadata(self, metadata: dict, comment_wrap=False) -> str:
+ """Write metadata in this extractor’s format."""
+ import toml
+ return '\n'.join(('+++', toml.dumps(metadata).strip(), '+++', ''))
+
+
+@_register_default
+class FilenameRegexMetadata(MetadataExtractor):
+ """Extractor for filename metadata."""
+
+ name = 'filename_regex'
+ source = MetaSource.filename
+ priority = MetaPriority.fallback
+ conditions = [(MetaCondition.config_bool, 'FILE_METADATA_REGEXP')]
+
+ def _extract_metadata_from_text(self, source_text: str) -> dict:
+ """Extract metadata from text."""
+ # This extractor does not use the source text, and as such, this method returns an empty dict.
+ return {}
+
+ def extract_filename(self, filename: str, lang: str) -> dict:
+ """Try to read the metadata from the filename based on the given re.
+
+ This requires to use symbolic group names in the pattern.
+ The part to read the metadata from the filename based on a regular
+ expression is taken from Pelican - pelican/readers.py
+ """
+ match = re.match(self.site.config['FILE_METADATA_REGEXP'], filename)
+ meta = {}
+
+ if match:
+ for key, value in match.groupdict().items():
+ k = key.lower().strip() # metadata must be lowercase
+ if k == 'title' and self.site.config['FILE_METADATA_UNSLUGIFY_TITLES']:
+ meta[k] = unslugify(value, lang, discard_numbers=False)
+ else:
+ meta[k] = value
+
+ return meta
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 0a62360..86d81e6 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,39 +26,32 @@
"""The main Nikola site object."""
-from __future__ import print_function, unicode_literals
-import io
-from collections import defaultdict
-from copy import copy
-from pkg_resources import resource_filename
import datetime
-import locale
-import os
+import io
import json
+import functools
+import logging
+import operator
+import os
import sys
-import natsort
import mimetypes
-try:
- from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote
-except ImportError:
- from urllib.parse import urlparse, urlsplit, urlunsplit, urljoin, unquote # NOQA
-
-try:
- import pyphen
-except ImportError:
- pyphen = None
+from collections import defaultdict
+from copy import copy
+from urllib.parse import urlparse, urlsplit, urlunsplit, urljoin, unquote, parse_qs
import dateutil.tz
-import logging
-import PyRSS2Gen as rss
import lxml.etree
import lxml.html
-from yapsy.PluginManager import PluginManager
+import natsort
+import PyRSS2Gen as rss
+from pkg_resources import resource_filename
from blinker import signal
+from yapsy.PluginManager import PluginManager
+from . import DEBUG, SHOW_TRACEBACKS, filters, utils, hierarchy_utils, shortcodes
+from . import metadata_extractors
+from .metadata_extractors import default_metadata_extractors_by
from .post import Post # NOQA
-from .state import Persistor
-from . import DEBUG, utils, shortcodes
from .plugin_categories import (
Command,
LateTask,
@@ -66,6 +59,7 @@ from .plugin_categories import (
CompilerExtension,
MarkdownExtension,
RestExtension,
+ MetadataExtractor,
ShortcodePlugin,
Task,
TaskMultiplier,
@@ -73,7 +67,14 @@ from .plugin_categories import (
SignalHandler,
ConfigPlugin,
PostScanner,
+ Taxonomy,
)
+from .state import Persistor
+
+try:
+ import pyphen
+except ImportError:
+ pyphen = None
if DEBUG:
logging.basicConfig(level=logging.DEBUG)
@@ -84,26 +85,25 @@ else:
DEFAULT_INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
DEFAULT_FEED_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>'
-# Default pattern for translation files' names
-DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
-
config_changed = utils.config_changed
__all__ = ('Nikola',)
-# We store legal values for some setting here. For internal use.
+# We store legal values for some settings here. For internal use.
LEGAL_VALUES = {
+ 'DEFAULT_THEME': 'bootblog4',
'COMMENT_SYSTEM': [
'disqus',
'facebook',
- 'googleplus',
'intensedebate',
'isso',
- 'livefyre',
'muut',
+ 'commento',
+ 'utterances',
],
'TRANSLATIONS': {
+ 'af': 'Afrikaans',
'ar': 'Arabic',
'az': 'Azerbaijani',
'bg': 'Bulgarian',
@@ -121,16 +121,20 @@ LEGAL_VALUES = {
'fa': 'Persian',
'fi': 'Finnish',
'fr': 'French',
+ 'fur': 'Friulian',
'gl': 'Galician',
'he': 'Hebrew',
'hi': 'Hindi',
'hr': 'Croatian',
'hu': 'Hungarian',
+ 'ia': 'Interlingua',
'id': 'Indonesian',
'it': 'Italian',
('ja', '!jp'): 'Japanese',
'ko': 'Korean',
'lt': 'Lithuanian',
+ 'ml': 'Malayalam',
+ 'mr': 'Marathi',
'nb': 'Norwegian (Bokmål)',
'nl': 'Dutch',
'pa': 'Punjabi',
@@ -145,61 +149,14 @@ LEGAL_VALUES = {
'sr_latin': 'Serbian (Latin)',
'sv': 'Swedish',
'te': 'Telugu',
+ 'th': 'Thai',
('tr', '!tr_TR'): 'Turkish',
- 'ur': 'Urdu',
'uk': 'Ukrainian',
+ 'ur': 'Urdu',
+ 'vi': 'Vietnamese',
'zh_cn': 'Chinese (Simplified)',
'zh_tw': 'Chinese (Traditional)'
},
- '_WINDOWS_LOCALE_GUESSES': {
- # TODO incomplete
- # some languages may need that the appropriate Microsoft Language Pack be installed.
- "ar": "Arabic",
- "az": "Azeri (Latin)",
- "bg": "Bulgarian",
- "bs": "Bosnian",
- "ca": "Catalan",
- "cs": "Czech",
- "da": "Danish",
- "de": "German",
- "el": "Greek",
- "en": "English",
- # "eo": "Esperanto", # Not available
- "es": "Spanish",
- "et": "Estonian",
- "eu": "Basque",
- "fa": "Persian", # Persian
- "fi": "Finnish",
- "fr": "French",
- "gl": "Galician",
- "he": "Hebrew",
- "hi": "Hindi",
- "hr": "Croatian",
- "hu": "Hungarian",
- "id": "Indonesian",
- "it": "Italian",
- "ja": "Japanese",
- "ko": "Korean",
- "nb": "Norwegian", # Not Bokmål, as Windows doesn't find it for unknown reasons.
- "nl": "Dutch",
- "pa": "Punjabi",
- "pl": "Polish",
- "pt": "Portuguese_Portugal",
- "pt_br": "Portuguese_Brazil",
- "ru": "Russian",
- "sk": "Slovak",
- "sl": "Slovenian",
- "sq": "Albanian",
- "sr": "Serbian",
- "sr_latin": "Serbian (Latin)",
- "sv": "Swedish",
- "te": "Telugu",
- "tr": "Turkish",
- "uk": "Ukrainian",
- "ur": "Urdu",
- "zh_cn": "Chinese_China", # Chinese (Simplified)
- "zh_tw": "Chinese_Taiwan", # Chinese (Traditional)
- },
'_TRANSLATIONS_WITH_COUNTRY_SPECIFIERS': {
# This dict is used in `init` in case of locales that exist with a
# country specifier. If there is no other locale that has the same
@@ -208,95 +165,126 @@ LEGAL_VALUES = {
# This dict is currently empty.
},
+ 'LOCALES_BASE': {
+ # A list of locale mappings to apply for every site. Can be overridden in the config.
+ 'sr_latin': 'sr_Latn',
+ },
'RTL_LANGUAGES': ('ar', 'fa', 'he', 'ur'),
- 'COLORBOX_LOCALES': defaultdict(
- str,
- ar='ar',
- bg='bg',
- ca='ca',
- cs='cs',
- cz='cs',
- da='da',
- de='de',
- en='',
- es='es',
- et='et',
- fa='fa',
- fi='fi',
- fr='fr',
- he='he',
- hr='hr',
- hu='hu',
- id='id',
- it='it',
- ja='ja',
- ko='kr', # kr is South Korea, ko is the Korean language
- lt='lt',
- nb='no',
- nl='nl',
- pl='pl',
- pt='pt-BR', # hope nobody will mind
- pt_br='pt-BR',
- ru='ru',
- sk='sk',
- sl='si', # country code is si, language code is sl, colorbox is wrong
- sr='sr', # warning: this is serbian in Latin alphabet
- sr_latin='sr',
- sv='sv',
- tr='tr',
- uk='uk',
- zh_cn='zh-CN',
- zh_tw='zh-TW'
- ),
- 'MOMENTJS_LOCALES': defaultdict(
- str,
- ar='ar',
- az='az',
- bg='bg',
- bn='bn',
- bs='bs',
- ca='ca',
- cs='cs',
- cz='cs',
- da='da',
- de='de',
- el='el',
- en='en',
- eo='eo',
- es='es',
- et='et',
- eu='eu',
- fa='fa',
- fi='fi',
- fr='fr',
- gl='gl',
- hi='hi',
- he='he',
- hr='hr',
- hu='hu',
- id='id',
- it='it',
- ja='ja',
- ko='ko',
- lt='lt',
- nb='nb',
- nl='nl',
- pl='pl',
- pt='pt',
- pt_br='pt-br',
- ru='ru',
- sk='sk',
- sl='sl',
- sq='sq',
- sr='sr-cyrl',
- sr_latin='sr',
- sv='sv',
- tr='tr',
- uk='uk',
- zh_cn='zh-cn',
- zh_tw='zh-tw'
- ),
+ 'LUXON_LOCALES': defaultdict(lambda: 'en', **{
+ 'af': 'af',
+ 'ar': 'ar',
+ 'az': 'az',
+ 'bg': 'bg',
+ 'bn': 'bn',
+ 'bs': 'bs',
+ 'ca': 'ca',
+ 'cs': 'cs',
+ 'cz': 'cs',
+ 'da': 'da',
+ 'de': 'de',
+ 'el': 'el',
+ 'en': 'en',
+ 'eo': 'eo',
+ 'es': 'es',
+ 'et': 'et',
+ 'eu': 'eu',
+ 'fa': 'fa',
+ 'fi': 'fi',
+ 'fr': 'fr',
+ 'fur': 'fur',
+ 'gl': 'gl',
+ 'hi': 'hi',
+ 'he': 'he',
+ 'hr': 'hr',
+ 'hu': 'hu',
+ 'ia': 'ia',
+ 'id': 'id',
+ 'it': 'it',
+ 'ja': 'ja',
+ 'ko': 'ko',
+ 'lt': 'lt',
+ 'ml': 'ml',
+ 'mr': 'mr',
+ 'nb': 'nb',
+ 'nl': 'nl',
+ 'pa': 'pa',
+ 'pl': 'pl',
+ 'pt': 'pt',
+ 'pt_br': 'pt-BR',
+ 'ru': 'ru',
+ 'sk': 'sk',
+ 'sl': 'sl',
+ 'sq': 'sq',
+ 'sr': 'sr-Cyrl',
+ 'sr_latin': 'sr-Latn',
+ 'sv': 'sv',
+ 'te': 'te',
+ 'tr': 'tr',
+ 'th': 'th',
+ 'uk': 'uk',
+ 'ur': 'ur',
+ 'vi': 'vi',
+ 'zh_cn': 'zh-CN',
+ 'zh_tw': 'zh-TW'
+ }),
+ # TODO: remove in v9
+ 'MOMENTJS_LOCALES': defaultdict(lambda: 'en', **{
+ 'af': 'af',
+ 'ar': 'ar',
+ 'az': 'az',
+ 'bg': 'bg',
+ 'bn': 'bn',
+ 'bs': 'bs',
+ 'ca': 'ca',
+ 'cs': 'cs',
+ 'cz': 'cs',
+ 'da': 'da',
+ 'de': 'de',
+ 'el': 'el',
+ 'en': 'en',
+ 'eo': 'eo',
+ 'es': 'es',
+ 'et': 'et',
+ 'eu': 'eu',
+ 'fa': 'fa',
+ 'fi': 'fi',
+ 'fr': 'fr',
+ 'gl': 'gl',
+ 'hi': 'hi',
+ 'he': 'he',
+ 'hr': 'hr',
+ 'hu': 'hu',
+ 'id': 'id',
+ 'it': 'it',
+ 'ja': 'ja',
+ 'ko': 'ko',
+ 'lt': 'lt',
+ 'ml': 'ml',
+ 'mr': 'mr',
+ 'nb': 'nb',
+ 'nl': 'nl',
+ 'pa': 'pa-in',
+ 'pl': 'pl',
+ 'pt': 'pt',
+ 'pt_br': 'pt-br',
+ 'ru': 'ru',
+ 'sk': 'sk',
+ 'sl': 'sl',
+ 'sq': 'sq',
+ 'sr': 'sr-cyrl',
+ 'sr_latin': 'sr',
+ 'sv': 'sv',
+ 'te': 'te',
+ 'tr': 'tr',
+ 'th': 'th',
+ 'uk': 'uk',
+ 'ur': 'ur',
+ 'vi': 'vi',
+ 'zh_cn': 'zh-cn',
+ 'zh_tw': 'zh-tw'
+ }),
'PYPHEN_LOCALES': {
+ 'af': 'af',
'bg': 'bg',
'ca': 'ca',
'cs': 'cs',
@@ -326,12 +314,14 @@ LEGAL_VALUES = {
'uk': 'uk',
},
'DOCUTILS_LOCALES': {
+ 'af': 'af',
'ca': 'ca',
'da': 'da',
'de': 'de',
'en': 'en',
'eo': 'eo',
'es': 'es',
+ 'fa': 'fa',
'fi': 'fi',
'fr': 'fr',
'gl': 'gl',
@@ -339,6 +329,7 @@ LEGAL_VALUES = {
'it': 'it',
'ja': 'ja',
'lt': 'lt',
+ 'nl': 'nl',
'pl': 'pl',
'pt': 'pt_br', # hope nobody will mind
'pt_br': 'pt_br',
@@ -347,9 +338,21 @@ LEGAL_VALUES = {
'sv': 'sv',
'zh_cn': 'zh_cn',
'zh_tw': 'zh_tw'
- }
+ },
+ "METADATA_MAPPING": ["yaml", "toml", "rest_docinfo", "markdown_metadata"],
+}
+
+# Mapping old pre-taxonomy plugin names to new post-taxonomy plugin names
+TAXONOMY_COMPATIBILITY_PLUGIN_NAME_MAP = {
+ "render_archive": ["classify_archive"],
+ "render_authors": ["classify_authors"],
+ "render_indexes": ["classify_page_index", "classify_sections"], # "classify_indexes" removed from list (see #2591 and special-case logic below)
+ "render_tags": ["classify_categories", "classify_tags"],
}
+# Default value for the pattern used to name translated files
+DEFAULT_TRANSLATIONS_PATTERN = '{path}.{lang}.{ext}'
+
def _enclosure(post, lang):
"""Add an enclosure to RSS."""
@@ -360,7 +363,7 @@ def _enclosure(post, lang):
except KeyError:
length = 0
except ValueError:
- utils.LOGGER.warn("Invalid enclosure length for post {0}".format(post.source_path))
+ utils.LOGGER.warning("Invalid enclosure length for post {0}".format(post.source_path))
length = 0
url = enclosure
mime = mimetypes.guess_type(url)[0]
@@ -374,7 +377,7 @@ class Nikola(object):
"""
def __init__(self, **config):
- """Setup proper environment for running tasks."""
+ """Initialize proper environment for running tasks."""
# Register our own path handlers
self.path_handlers = {
'slug': self.slug_path,
@@ -398,8 +401,9 @@ class Nikola(object):
self._template_system = None
self._THEMES = None
self._MESSAGES = None
+ self.filters = {}
self.debug = DEBUG
- self.loghandlers = utils.STDERR_HANDLER # TODO remove on v8
+ self.show_tracebacks = SHOW_TRACEBACKS
self.colorful = config.pop('__colorful__', False)
self.invariant = config.pop('__invariant__', False)
self.quiet = config.pop('__quiet__', False)
@@ -409,6 +413,7 @@ class Nikola(object):
self.configured = bool(config)
self.injected_deps = defaultdict(list)
self.shortcode_registry = {}
+ self.metadata_extractors_by = default_metadata_extractors_by()
self.rst_transforms = []
self.template_hooks = {
@@ -425,7 +430,6 @@ class Nikola(object):
# This is the default config
self.config = {
- 'ANNOTATIONS': False,
'ARCHIVE_PATH': "",
'ARCHIVE_FILENAME': "archive.html",
'ARCHIVES_ARE_INDEXES': False,
@@ -435,16 +439,25 @@ class Nikola(object):
'AUTHORLIST_MINIMUM_POSTS': 1,
'BLOG_AUTHOR': 'Default Author',
'BLOG_TITLE': 'Default Title',
+ 'BLOG_EMAIL': '',
'BLOG_DESCRIPTION': 'Default Description',
'BODY_END': "",
'CACHE_FOLDER': 'cache',
+ 'CATEGORIES_INDEX_PATH': '',
'CATEGORY_PATH': None, # None means: same as TAG_PATH
'CATEGORY_PAGES_ARE_INDEXES': None, # None means: same as TAG_PAGES_ARE_INDEXES
- 'CATEGORY_PAGES_DESCRIPTIONS': {},
- 'CATEGORY_PAGES_TITLES': {},
+ 'CATEGORY_DESCRIPTIONS': {},
+ 'CATEGORY_TITLES': {},
'CATEGORY_PREFIX': 'cat_',
'CATEGORY_ALLOW_HIERARCHIES': False,
'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
+ 'CATEGORY_DESTPATH_AS_DEFAULT': False,
+ 'CATEGORY_DESTPATH_TRIM_PREFIX': False,
+ 'CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY': True,
+ 'CATEGORY_DESTPATH_NAMES': {},
+ 'CATEGORY_PAGES_FOLLOW_DESTPATH': False,
+ 'CATEGORY_TRANSLATIONS': [],
+ 'CATEGORY_TRANSLATIONS_ADD_DEFAULTS': False,
'CODE_COLOR_SCHEME': 'default',
'COMMENT_SYSTEM': 'disqus',
'COMMENTS_IN_GALLERIES': False,
@@ -461,13 +474,21 @@ class Nikola(object):
},
'CONTENT_FOOTER': '',
'CONTENT_FOOTER_FORMATS': {},
+ 'RSS_COPYRIGHT': '',
+ 'RSS_COPYRIGHT_PLAIN': '',
+ 'RSS_COPYRIGHT_FORMATS': {},
'COPY_SOURCES': True,
+ 'CREATE_ARCHIVE_NAVIGATION': False,
'CREATE_MONTHLY_ARCHIVE': False,
'CREATE_SINGLE_ARCHIVE': False,
'CREATE_FULL_ARCHIVES': False,
'CREATE_DAILY_ARCHIVE': False,
- 'DATE_FORMAT': '%Y-%m-%d %H:%M',
- 'JS_DATE_FORMAT': 'YYYY-MM-DD HH:mm',
+ 'DATE_FORMAT': 'yyyy-MM-dd HH:mm',
+ 'DISABLE_INDEXES': False,
+ 'DISABLE_MAIN_ATOM_FEED': False,
+ 'DISABLE_MAIN_RSS_FEED': False,
+ 'MOMENTJS_DATE_FORMAT': 'YYYY-MM-DD HH:mm',
+ 'LUXON_DATE_FORMAT': {},
'DATE_FANCINESS': 0,
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': {'default': []},
@@ -481,6 +502,7 @@ class Nikola(object):
'FAVICONS': (),
'FEED_LENGTH': 10,
'FILE_METADATA_REGEXP': None,
+ 'FILE_METADATA_UNSLUGIFY_TITLES': True,
'ADDITIONAL_METADATA': {},
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
@@ -488,12 +510,15 @@ class Nikola(object):
'FRONT_INDEX_HEADER': '',
'GALLERY_FOLDERS': {'galleries': 'galleries'},
'GALLERY_SORT_BY_DATE': True,
+ 'GALLERIES_USE_THUMBNAIL': False,
+ 'GALLERIES_DEFAULT_THUMBNAIL': None,
'GLOBAL_CONTEXT_FILLER': [],
'GZIP_COMMAND': None,
'GZIP_FILES': False,
'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'),
'HIDDEN_AUTHORS': [],
'HIDDEN_TAGS': [],
+ 'HIDE_REST_DOCINFO': False,
'HIDDEN_CATEGORIES': [],
'HYPHENATE': False,
'IMAGE_FOLDERS': {'images': ''},
@@ -501,6 +526,7 @@ class Nikola(object):
'INDEX_FILE': 'index.html',
'INDEX_TEASERS': False,
'IMAGE_THUMBNAIL_SIZE': 400,
+ 'IMAGE_THUMBNAIL_FORMAT': '{name}.thumbnail{ext}',
'INDEXES_TITLE': "",
'INDEXES_PAGES': "",
'INDEXES_PAGES_MAIN': False,
@@ -509,48 +535,50 @@ class Nikola(object):
'INDEX_PATH': '',
'IPYNB_CONFIG': {},
'KATEX_AUTO_RENDER': '',
- 'LESS_COMPILER': 'lessc',
- 'LESS_OPTIONS': [],
'LICENSE': '',
'LINK_CHECK_WHITELIST': [],
'LISTINGS_FOLDERS': {'listings': 'listings'},
'LOGO_URL': '',
+ 'DEFAULT_PREVIEW_IMAGE': None,
'NAVIGATION_LINKS': {},
- 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'], # FIXME: Add 'extras' in v8
+ 'NAVIGATION_ALT_LINKS': {},
+ 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite', 'extra'],
+ 'MARKDOWN_EXTENSION_CONFIGS': {},
'MAX_IMAGE_SIZE': 1280,
'MATHJAX_CONFIG': '',
+ 'METADATA_FORMAT': 'nikola',
+ 'METADATA_MAPPING': {},
+ 'MULTIPLE_AUTHORS_PER_POST': False,
+ 'NEW_POST_DATE_PATH': False,
+ 'NEW_POST_DATE_PATH_FORMAT': '%Y/%m/%d',
'OLD_THEME_SUPPORT': True,
'OUTPUT_FOLDER': 'output',
'POSTS': (("posts/*.txt", "posts", "post.tmpl"),),
- 'POSTS_SECTIONS': True,
- 'POSTS_SECTION_COLORS': {},
- 'POSTS_SECTION_ARE_INDEXES': True,
- 'POSTS_SECTION_DESCRIPTIONS': "",
- 'POSTS_SECTION_FROM_META': False,
- 'POSTS_SECTION_NAME': "",
- 'POSTS_SECTION_TITLE': "{name}",
'PRESERVE_EXIF_DATA': False,
- # TODO: change in v8
- 'PAGES': (("stories/*.txt", "stories", "story.tmpl"),),
+ 'PRESERVE_ICC_PROFILES': False,
+ 'PAGES': (("pages/*.txt", "pages", "page.tmpl"),),
'PANDOC_OPTIONS': [],
- 'PRETTY_URLS': False,
+ 'PRETTY_URLS': True,
'FUTURE_IS_NOW': False,
'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK,
'REDIRECTIONS': [],
'ROBOTS_EXCLUSIONS': [],
'GENERATE_ATOM': False,
+ 'ATOM_EXTENSION': '.atom',
+ 'ATOM_PATH': '',
+ 'ATOM_FILENAME_BASE': 'index',
'FEED_TEASERS': True,
'FEED_PLAIN': False,
- 'FEED_PREVIEWIMAGE': True,
'FEED_READ_MORE_LINK': DEFAULT_FEED_READ_MORE_LINK,
'FEED_LINKS_APPEND_QUERY': False,
'GENERATE_RSS': True,
+ 'RSS_EXTENSION': '.xml',
'RSS_LINK': None,
'RSS_PATH': '',
- 'SASS_COMPILER': 'sass',
- 'SASS_OPTIONS': [],
+ 'RSS_FILENAME_BASE': 'rss',
'SEARCH_FORM': '',
'SHOW_BLOG_TITLE': True,
+ 'SHOW_INDEX_PAGE_NAVIGATION': False,
'SHOW_SOURCELINK': True,
'SHOW_UNTRANSLATED_POSTS': True,
'SLUG_AUTHOR_PATH': True,
@@ -558,47 +586,53 @@ class Nikola(object):
'SOCIAL_BUTTONS_CODE': '',
'SITE_URL': 'https://example.com/',
'PAGE_INDEX': False,
- 'STRIP_INDEXES': False,
- 'SITEMAP_INCLUDE_FILELESS_DIRS': True,
+ 'SECTION_PATH': '',
+ 'STRIP_INDEXES': True,
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
- 'TAG_PAGES_DESCRIPTIONS': {},
- 'TAG_PAGES_TITLES': {},
+ 'TAG_DESCRIPTIONS': {},
+ 'TAG_TITLES': {},
+ 'TAG_TRANSLATIONS': [],
+ 'TAG_TRANSLATIONS_ADD_DEFAULTS': False,
'TAGS_INDEX_PATH': '',
'TAGLIST_MINIMUM_POSTS': 1,
'TEMPLATE_FILTERS': {},
- 'THEME': 'bootstrap3',
+ 'THEME': LEGAL_VALUES['DEFAULT_THEME'],
'THEME_COLOR': '#5670d4', # light "corporate blue"
- 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky',
- 'THEME_REVEAL_CONFIG_TRANSITION': 'cube',
+ 'THEME_CONFIG': {},
'THUMBNAIL_SIZE': 180,
- 'UNSLUGIFY_TITLES': False, # WARNING: conf.py.in overrides this with True for backwards compatibility
+ 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN,
'URL_TYPE': 'rel_path',
- 'USE_BASE_TAG': False,
'USE_BUNDLES': True,
'USE_CDN': False,
'USE_CDN_WARNING': True,
+ 'USE_REST_DOCINFO_METADATA': False,
'USE_FILENAME_AS_TITLE': True,
'USE_KATEX': False,
- 'USE_OPEN_GRAPH': True,
'USE_SLUGIFY': True,
+ 'USE_TAG_METADATA': True,
'TIMEZONE': 'UTC',
- 'WRITE_TAG_CLOUD': True,
+ 'WARN_ABOUT_TAG_METADATA': True,
'DEPLOY_DRAFTS': True,
'DEPLOY_FUTURE': False,
'SCHEDULE_ALL': False,
'SCHEDULE_RULE': '',
- 'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}},
'DEMOTE_HEADERS': 1,
'GITHUB_SOURCE_BRANCH': 'master',
'GITHUB_DEPLOY_BRANCH': 'gh-pages',
'GITHUB_REMOTE_NAME': 'origin',
'GITHUB_COMMIT_SOURCE': False, # WARNING: conf.py.in overrides this with True for backwards compatibility
+ 'META_GENERATOR_TAG': True,
+ 'REST_FILE_INSERTION_ENABLED': True,
+ 'TYPES_TO_HIDE_TITLE': [],
}
# set global_context for template rendering
self._GLOBAL_CONTEXT = {}
+ # dependencies for all pages, not included in global context
+ self.ALL_PAGE_DEPS = {}
+
self.config.update(config)
# __builtins__ contains useless cruft
@@ -612,13 +646,22 @@ class Nikola(object):
self.config['__invariant__'] = self.invariant
self.config['__quiet__'] = self.quiet
- # Make sure we have sane NAVIGATION_LINKS.
+ # Use ATOM_PATH when set
+ self.config['ATOM_PATH'] = self.config['ATOM_PATH'] or self.config['INDEX_PATH']
+
+ # Make sure we have sane NAVIGATION_LINKS and NAVIGATION_ALT_LINKS.
if not self.config['NAVIGATION_LINKS']:
self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()}
+ if not self.config['NAVIGATION_ALT_LINKS']:
+ self.config['NAVIGATION_ALT_LINKS'] = {self.config['DEFAULT_LANG']: ()}
# Translatability configuration.
self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
{self.config['DEFAULT_LANG']: ''})
+ for k, v in self.config['TRANSLATIONS'].items():
+ if os.path.isabs(v):
+ self.config['TRANSLATIONS'][k] = os.path.relpath(v, '/')
+
utils.TranslatableSetting.default_lang = self.config['DEFAULT_LANG']
self.TRANSLATABLE_SETTINGS = ('BLOG_AUTHOR',
@@ -631,27 +674,40 @@ class Nikola(object):
'BODY_END',
'EXTRA_HEAD_DATA',
'NAVIGATION_LINKS',
+ 'NAVIGATION_ALT_LINKS',
'FRONT_INDEX_HEADER',
'INDEX_READ_MORE_LINK',
'FEED_READ_MORE_LINK',
'INDEXES_TITLE',
- 'POSTS_SECTION_COLORS',
- 'POSTS_SECTION_DESCRIPTIONS',
- 'POSTS_SECTION_NAME',
- 'POSTS_SECTION_TITLE',
+ 'CATEGORY_DESTPATH_NAMES',
'INDEXES_PAGES',
'INDEXES_PRETTY_PAGE_URL',
+ 'THEME_CONFIG',
# PATH options (Issue #1914)
+ 'ARCHIVE_PATH',
+ 'ARCHIVE_FILENAME',
'TAG_PATH',
'TAGS_INDEX_PATH',
'CATEGORY_PATH',
+ 'CATEGORIES_INDEX_PATH',
+ 'SECTION_PATH',
+ 'INDEX_PATH',
+ 'ATOM_PATH',
+ 'RSS_PATH',
+ 'RSS_FILENAME_BASE',
+ 'ATOM_FILENAME_BASE',
+ 'AUTHOR_PATH',
'DATE_FORMAT',
- 'JS_DATE_FORMAT',
+ 'LUXON_DATE_FORMAT',
+ 'MOMENTJS_DATE_FORMAT', # TODO: remove in v9
+ 'RSS_COPYRIGHT',
+ 'RSS_COPYRIGHT_PLAIN',
+ # Issue #2970
+ 'MARKDOWN_EXTENSION_CONFIGS',
)
self._GLOBAL_CONTEXT_TRANSLATABLE = ('blog_author',
'blog_title',
- 'blog_desc', # TODO: remove in v8
'blog_description',
'license',
'content_footer',
@@ -661,22 +717,40 @@ class Nikola(object):
'extra_head_data',
'date_format',
'js_date_format',
- 'posts_section_colors',
- 'posts_section_descriptions',
- 'posts_section_name',
- 'posts_section_title',
+ 'luxon_date_format',
'front_index_header',
+ 'theme_config',
)
- # WARNING: navigation_links SHOULD NOT be added to the list above.
+
+ self._ALL_PAGE_DEPS_TRANSLATABLE = ('atom_path',
+ 'rss_path',
+ 'rss_filename_base',
+ 'atom_filename_base',
+ )
+ # WARNING: navigation_(alt_)links SHOULD NOT be added to the list above.
# Themes ask for [lang] there and we should provide it.
- # We first have to massage JS_DATE_FORMAT, otherwise we run into trouble
+ # Luxon setup is a dict of dicts, so we need to set up the default here.
+ if not self.config['LUXON_DATE_FORMAT']:
+ self.config['LUXON_DATE_FORMAT'] = {self.config['DEFAULT_LANG']: {'preset': False, 'format': 'yyyy-MM-dd HH:mm'}}
+ # TODO: remove Moment.js stuff in v9
if 'JS_DATE_FORMAT' in self.config:
- if isinstance(self.config['JS_DATE_FORMAT'], dict):
- for k in self.config['JS_DATE_FORMAT']:
- self.config['JS_DATE_FORMAT'][k] = json.dumps(self.config['JS_DATE_FORMAT'][k])
+ utils.LOGGER.warning("Moment.js was replaced by Luxon in the default themes, which uses different date formats.")
+ utils.LOGGER.warning("If you’re using a built-in theme, set LUXON_DATE_FORMAT. If your theme uses Moment.js, you can silence this warning by renaming JS_DATE_FORMAT to MOMENTJS_DATE_FORMAT.")
+ utils.LOGGER.warning("Sample Luxon config: LUXON_DATE_FORMAT = " + str(self.config['LUXON_DATE_FORMAT']))
+ self.config['MOMENTJS_DATE_FORMAT'] = self.config['LUXON_DATE_FORMAT']
+
+ # We first have to massage MOMENTJS_DATE_FORMAT and LUXON_DATE_FORMAT, otherwise we run into trouble
+ if 'MOMENTJS_DATE_FORMAT' in self.config:
+ if isinstance(self.config['MOMENTJS_DATE_FORMAT'], dict):
+ for k in self.config['MOMENTJS_DATE_FORMAT']:
+ self.config['MOMENTJS_DATE_FORMAT'][k] = json.dumps(self.config['MOMENTJS_DATE_FORMAT'][k])
else:
- self.config['JS_DATE_FORMAT'] = json.dumps(self.config['JS_DATE_FORMAT'])
+ self.config['MOMENTJS_DATE_FORMAT'] = json.dumps(self.config['MOMENTJS_DATE_FORMAT'])
+
+ if 'LUXON_DATE_FORMAT' in self.config:
+ for k in self.config['LUXON_DATE_FORMAT']:
+ self.config['LUXON_DATE_FORMAT'][k] = json.dumps(self.config['LUXON_DATE_FORMAT'][k])
for i in self.TRANSLATABLE_SETTINGS:
try:
@@ -686,25 +760,104 @@ class Nikola(object):
# A EXIF_WHITELIST implies you want to keep EXIF data
if self.config['EXIF_WHITELIST'] and not self.config['PRESERVE_EXIF_DATA']:
- utils.LOGGER.warn('Setting EXIF_WHITELIST implies PRESERVE_EXIF_DATA is set to True')
+ utils.LOGGER.warning('Setting EXIF_WHITELIST implies PRESERVE_EXIF_DATA is set to True')
self.config['PRESERVE_EXIF_DATA'] = True
# Setting PRESERVE_EXIF_DATA with an empty EXIF_WHITELIST implies 'keep everything'
if self.config['PRESERVE_EXIF_DATA'] and not self.config['EXIF_WHITELIST']:
- utils.LOGGER.warn('You are setting PRESERVE_EXIF_DATA and not EXIF_WHITELIST so EXIF data is not really kept.')
+ utils.LOGGER.warning('You are setting PRESERVE_EXIF_DATA and not EXIF_WHITELIST so EXIF data is not really kept.')
+
+ if 'UNSLUGIFY_TITLES' in self.config:
+ utils.LOGGER.warning('The UNSLUGIFY_TITLES setting was renamed to FILE_METADATA_UNSLUGIFY_TITLES.')
+ self.config['FILE_METADATA_UNSLUGIFY_TITLES'] = self.config['UNSLUGIFY_TITLES']
+
+ if 'TAG_PAGES_TITLES' in self.config:
+ utils.LOGGER.warning('The TAG_PAGES_TITLES setting was renamed to TAG_TITLES.')
+ self.config['TAG_TITLES'] = self.config['TAG_PAGES_TITLES']
+
+ if 'TAG_PAGES_DESCRIPTIONS' in self.config:
+ utils.LOGGER.warning('The TAG_PAGES_DESCRIPTIONS setting was renamed to TAG_DESCRIPTIONS.')
+ self.config['TAG_DESCRIPTIONS'] = self.config['TAG_PAGES_DESCRIPTIONS']
+
+ if 'CATEGORY_PAGES_TITLES' in self.config:
+ utils.LOGGER.warning('The CATEGORY_PAGES_TITLES setting was renamed to CATEGORY_TITLES.')
+ self.config['CATEGORY_TITLES'] = self.config['CATEGORY_PAGES_TITLES']
+
+ if 'CATEGORY_PAGES_DESCRIPTIONS' in self.config:
+ utils.LOGGER.warning('The CATEGORY_PAGES_DESCRIPTIONS setting was renamed to CATEGORY_DESCRIPTIONS.')
+ self.config['CATEGORY_DESCRIPTIONS'] = self.config['CATEGORY_PAGES_DESCRIPTIONS']
+
+ if 'DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED' in self.config:
+ utils.LOGGER.warning('The DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED setting was renamed and split to DISABLE_INDEXES and DISABLE_MAIN_ATOM_FEED.')
+ self.config['DISABLE_INDEXES'] = self.config['DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED']
+ self.config['DISABLE_MAIN_ATOM_FEED'] = self.config['DISABLE_INDEXES_PLUGIN_INDEX_AND_ATOM_FEED']
+
+ if 'DISABLE_INDEXES_PLUGIN_RSS_FEED' in self.config:
+ utils.LOGGER.warning('The DISABLE_INDEXES_PLUGIN_RSS_FEED setting was renamed to DISABLE_MAIN_RSS_FEED.')
+ self.config['DISABLE_MAIN_RSS_FEED'] = self.config['DISABLE_INDEXES_PLUGIN_RSS_FEED']
+
+ for val in self.config['DATE_FORMAT'].values.values():
+ if '%' in val:
+ utils.LOGGER.error('The DATE_FORMAT setting needs to be upgraded.')
+ utils.LOGGER.warning("Nikola now uses CLDR-style date strings. http://cldr.unicode.org/translation/date-time")
+ utils.LOGGER.warning("Example: %Y-%m-%d %H:%M ==> yyyy-MM-dd HH:mm")
+ utils.LOGGER.warning("(note it’s different to what moment.js uses!)")
+ sys.exit(1)
+
+ # Silently upgrade LOCALES (remove encoding)
+ locales = LEGAL_VALUES['LOCALES_BASE']
+ if 'LOCALES' in self.config:
+ for k, v in self.config['LOCALES'].items():
+ self.config['LOCALES'][k] = v.split('.')[0]
+ locales.update(self.config['LOCALES'])
+ self.config['LOCALES'] = locales
+
+ if self.config.get('POSTS_SECTIONS'):
+ utils.LOGGER.warning("The sections feature has been removed and its functionality has been merged into categories.")
+ utils.LOGGER.warning("For more information on how to migrate, please read: https://getnikola.com/blog/upgrading-to-nikola-v8.html#sections-were-replaced-by-categories")
+
+ for section_config_suffix, cat_config_suffix in (
+ ('DESCRIPTIONS', 'DESCRIPTIONS'),
+ ('TITLE', 'TITLES'),
+ ('TRANSLATIONS', 'TRANSLATIONS')
+ ):
+ section_config = 'POSTS_SECTION_' + section_config_suffix
+ cat_config = 'CATEGORY_' + cat_config_suffix
+ if section_config in self.config:
+ self.config[section_config].update(self.config[cat_config])
+ self.config[cat_config] = self.config[section_config]
+
+ self.config['CATEGORY_DESTPATH_NAMES'] = self.config.get('POSTS_SECTION_NAME', {})
+ # Need to mark this translatable manually.
+ self.config['CATEGORY_DESTPATH_NAMES'] = utils.TranslatableSetting('CATEGORY_DESTPATH_NAMES', self.config['CATEGORY_DESTPATH_NAMES'], self.config['TRANSLATIONS'])
+
+ self.config['CATEGORY_DESTPATH_AS_DEFAULT'] = not self.config.get('POSTS_SECTION_FROM_META')
+ utils.LOGGER.info("Setting CATEGORY_DESTPATH_AS_DEFAULT = " + str(self.config['CATEGORY_DESTPATH_AS_DEFAULT']))
+
+ if self.config.get('CATEGORY_PAGES_FOLLOW_DESTPATH') and (not self.config.get('CATEGORY_ALLOW_HIERARCHIES') or self.config.get('CATEGORY_OUTPUT_FLAT_HIERARCHY')):
+ utils.LOGGER.error('CATEGORY_PAGES_FOLLOW_DESTPATH requires CATEGORY_ALLOW_HIERARCHIES = True, CATEGORY_OUTPUT_FLAT_HIERARCHY = False.')
+ sys.exit(1)
+
+ # The Utterances comment system has a required configuration value
+ if self.config.get('COMMENT_SYSTEM') == 'utterances':
+ utterances_config = self.config.get('GLOBAL_CONTEXT', {}).get('utterances_config', {})
+ if not ('issue-term' in utterances_config or 'issue-number' in utterances_config):
+ utils.LOGGER.error("COMMENT_SYSTEM = 'utterances' must have either GLOBAL_CONTEXT['utterances_config']['issue-term'] or GLOBAL_CONTEXT['utterances_config']['issue-term'] defined.")
- # Handle CONTENT_FOOTER properly.
- # We provide the arguments to format in CONTENT_FOOTER_FORMATS.
+ # Handle CONTENT_FOOTER and RSS_COPYRIGHT* properly.
+ # We provide the arguments to format in CONTENT_FOOTER_FORMATS and RSS_COPYRIGHT_FORMATS.
self.config['CONTENT_FOOTER'].langformat(self.config['CONTENT_FOOTER_FORMATS'])
+ self.config['RSS_COPYRIGHT'].langformat(self.config['RSS_COPYRIGHT_FORMATS'])
+ self.config['RSS_COPYRIGHT_PLAIN'].langformat(self.config['RSS_COPYRIGHT_FORMATS'])
# propagate USE_SLUGIFY
utils.USE_SLUGIFY = self.config['USE_SLUGIFY']
# Make sure we have pyphen installed if we are using it
if self.config.get('HYPHENATE') and pyphen is None:
- utils.LOGGER.warn('To use the hyphenation, you have to install '
- 'the "pyphen" package.')
- utils.LOGGER.warn('Setting HYPHENATE to False.')
+ utils.LOGGER.warning('To use the hyphenation, you have to install '
+ 'the "pyphen" package.')
+ utils.LOGGER.warning('Setting HYPHENATE to False.')
self.config['HYPHENATE'] = False
# FIXME: Internally, we still use post_pages because it's a pain to change it
@@ -714,114 +867,40 @@ class Nikola(object):
for i1, i2, i3 in self.config['PAGES']:
self.config['post_pages'].append([i1, i2, i3, False])
- # RSS_TEASERS has been replaced with FEED_TEASERS
- # TODO: remove on v8
- if 'RSS_TEASERS' in config:
- utils.LOGGER.warn('The RSS_TEASERS option is deprecated, use FEED_TEASERS instead.')
- if 'FEED_TEASERS' in config:
- utils.LOGGER.warn('FEED_TEASERS conflicts with RSS_TEASERS, ignoring RSS_TEASERS.')
- self.config['FEED_TEASERS'] = config['RSS_TEASERS']
-
- # RSS_PLAIN has been replaced with FEED_PLAIN
- # TODO: remove on v8
- if 'RSS_PLAIN' in config:
- utils.LOGGER.warn('The RSS_PLAIN option is deprecated, use FEED_PLAIN instead.')
- if 'FEED_PLAIN' in config:
- utils.LOGGER.warn('FEED_PLIN conflicts with RSS_PLAIN, ignoring RSS_PLAIN.')
- self.config['FEED_PLAIN'] = config['RSS_PLAIN']
-
- # RSS_LINKS_APPEND_QUERY has been replaced with FEED_LINKS_APPEND_QUERY
- # TODO: remove on v8
- if 'RSS_LINKS_APPEND_QUERY' in config:
- utils.LOGGER.warn('The RSS_LINKS_APPEND_QUERY option is deprecated, use FEED_LINKS_APPEND_QUERY instead.')
- if 'FEED_LINKS_APPEND_QUERY' in config:
- utils.LOGGER.warn('FEED_LINKS_APPEND_QUERY conflicts with RSS_LINKS_APPEND_QUERY, ignoring RSS_LINKS_APPEND_QUERY.')
- self.config['FEED_LINKS_APPEND_QUERY'] = config['RSS_LINKS_APPEND_QUERY']
-
- # RSS_READ_MORE_LINK has been replaced with FEED_READ_MORE_LINK
- # TODO: remove on v8
- if 'RSS_READ_MORE_LINK' in config:
- utils.LOGGER.warn('The RSS_READ_MORE_LINK option is deprecated, use FEED_READ_MORE_LINK instead.')
- if 'FEED_READ_MORE_LINK' in config:
- utils.LOGGER.warn('FEED_READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring RSS_READ_MORE_LINK')
- self.config['FEED_READ_MORE_LINK'] = utils.TranslatableSetting('FEED_READ_MORE_LINK', config['RSS_READ_MORE_LINK'], self.config['TRANSLATIONS'])
-
- # DEFAULT_TRANSLATIONS_PATTERN was changed from "p.e.l" to "p.l.e"
- # TODO: remove on v8
- if 'TRANSLATIONS_PATTERN' not in self.config:
- if len(self.config.get('TRANSLATIONS', {})) > 1:
- utils.LOGGER.warn('You do not have a TRANSLATIONS_PATTERN set in your config, yet you have multiple languages.')
- utils.LOGGER.warn('Setting TRANSLATIONS_PATTERN to the pre-v6 default ("{path}.{ext}.{lang}").')
- utils.LOGGER.warn('Please add the proper pattern to your conf.py. (The new default in v7 is "{0}".)'.format(DEFAULT_TRANSLATIONS_PATTERN))
- self.config['TRANSLATIONS_PATTERN'] = "{path}.{ext}.{lang}"
- else:
- # use v7 default there
- self.config['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN
-
- # HIDE_SOURCELINK has been replaced with the inverted SHOW_SOURCELINK
- # TODO: remove on v8
- if 'HIDE_SOURCELINK' in config:
- utils.LOGGER.warn('The HIDE_SOURCELINK option is deprecated, use SHOW_SOURCELINK instead.')
- if 'SHOW_SOURCELINK' in config:
- utils.LOGGER.warn('HIDE_SOURCELINK conflicts with SHOW_SOURCELINK, ignoring HIDE_SOURCELINK.')
- self.config['SHOW_SOURCELINK'] = not config['HIDE_SOURCELINK']
-
- # HIDE_UNTRANSLATED_POSTS has been replaced with the inverted SHOW_UNTRANSLATED_POSTS
- # TODO: remove on v8
- if 'HIDE_UNTRANSLATED_POSTS' in config:
- utils.LOGGER.warn('The HIDE_UNTRANSLATED_POSTS option is deprecated, use SHOW_UNTRANSLATED_POSTS instead.')
- if 'SHOW_UNTRANSLATED_POSTS' in config:
- utils.LOGGER.warn('HIDE_UNTRANSLATED_POSTS conflicts with SHOW_UNTRANSLATED_POSTS, ignoring HIDE_UNTRANSLATED_POSTS.')
- self.config['SHOW_UNTRANSLATED_POSTS'] = not config['HIDE_UNTRANSLATED_POSTS']
-
- # READ_MORE_LINK has been split into INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK
- # TODO: remove on v8
- if 'READ_MORE_LINK' in config:
- utils.LOGGER.warn('The READ_MORE_LINK option is deprecated, use INDEX_READ_MORE_LINK and RSS_READ_MORE_LINK instead.')
- if 'INDEX_READ_MORE_LINK' in config:
- utils.LOGGER.warn('READ_MORE_LINK conflicts with INDEX_READ_MORE_LINK, ignoring READ_MORE_LINK.')
- else:
- self.config['INDEX_READ_MORE_LINK'] = utils.TranslatableSetting('INDEX_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS'])
-
- if 'RSS_READ_MORE_LINK' in config:
- utils.LOGGER.warn('READ_MORE_LINK conflicts with RSS_READ_MORE_LINK, ignoring READ_MORE_LINK.')
+ # Handle old plugin names (from before merging the taxonomy PR #2535)
+ for old_plugin_name, new_plugin_names in TAXONOMY_COMPATIBILITY_PLUGIN_NAME_MAP.items():
+ if old_plugin_name in self.config['DISABLED_PLUGINS']:
+ missing_plugins = []
+ for plugin_name in new_plugin_names:
+ if plugin_name not in self.config['DISABLED_PLUGINS']:
+ missing_plugins.append(plugin_name)
+ if missing_plugins:
+ utils.LOGGER.warning('The "{}" plugin was replaced by several taxonomy plugins (see PR #2535): {}'.format(old_plugin_name, ', '.join(new_plugin_names)))
+ utils.LOGGER.warning('You are currently disabling "{}", but not the following new taxonomy plugins: {}'.format(old_plugin_name, ', '.join(missing_plugins)))
+ utils.LOGGER.warning('Please also disable these new plugins or remove "{}" from the DISABLED_PLUGINS list.'.format(old_plugin_name))
+ self.config['DISABLED_PLUGINS'].extend(missing_plugins)
+ # Special-case logic for "render_indexes" to fix #2591
+ if 'render_indexes' in self.config['DISABLED_PLUGINS']:
+ if 'generate_rss' in self.config['DISABLED_PLUGINS'] or self.config['GENERATE_RSS'] is False:
+ if 'classify_indexes' not in self.config['DISABLED_PLUGINS']:
+ utils.LOGGER.warning('You are disabling the "render_indexes" plugin, as well as disabling the "generate_rss" plugin or setting GENERATE_RSS to False. To achieve the same effect, please disable the "classify_indexes" plugin in the future.')
+ self.config['DISABLED_PLUGINS'].append('classify_indexes')
else:
- self.config['RSS_READ_MORE_LINK'] = utils.TranslatableSetting('RSS_READ_MORE_LINK', config['READ_MORE_LINK'], self.config['TRANSLATIONS'])
-
- # Moot.it renamed themselves to muut.io
- # TODO: remove on v8?
- if self.config.get('COMMENT_SYSTEM') == 'moot':
- utils.LOGGER.warn('The moot comment system has been renamed to muut by the upstream. Setting COMMENT_SYSTEM to "muut".')
- self.config['COMMENT_SYSTEM'] = 'muut'
+ if not self.config['DISABLE_INDEXES']:
+ utils.LOGGER.warning('You are disabling the "render_indexes" plugin, but not the generation of RSS feeds. Please put "DISABLE_INDEXES = True" into your configuration instead.')
+ self.config['DISABLE_INDEXES'] = True
# Disable RSS. For a successful disable, we must have both the option
# false and the plugin disabled through the official means.
if 'generate_rss' in self.config['DISABLED_PLUGINS'] and self.config['GENERATE_RSS'] is True:
+ utils.LOGGER.warning('Please use GENERATE_RSS to disable RSS feed generation, instead of mentioning generate_rss in DISABLED_PLUGINS.')
self.config['GENERATE_RSS'] = False
-
- if not self.config['GENERATE_RSS'] and 'generate_rss' not in self.config['DISABLED_PLUGINS']:
- self.config['DISABLED_PLUGINS'].append('generate_rss')
+ self.config['DISABLE_MAIN_RSS_FEED'] = True
# PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled
if self.config.get('PRETTY_URLS') and 'STRIP_INDEXES' not in config:
self.config['STRIP_INDEXES'] = True
- if 'LISTINGS_FOLDER' in config:
- if 'LISTINGS_FOLDERS' not in config:
- utils.LOGGER.warn("The LISTINGS_FOLDER option is deprecated, use LISTINGS_FOLDERS instead.")
- self.config['LISTINGS_FOLDERS'] = {self.config['LISTINGS_FOLDER']: self.config['LISTINGS_FOLDER']}
- utils.LOGGER.warn("LISTINGS_FOLDERS = {0}".format(self.config['LISTINGS_FOLDERS']))
- else:
- utils.LOGGER.warn("Both LISTINGS_FOLDER and LISTINGS_FOLDERS are specified, ignoring LISTINGS_FOLDER.")
-
- if 'GALLERY_PATH' in config:
- if 'GALLERY_FOLDERS' not in config:
- utils.LOGGER.warn("The GALLERY_PATH option is deprecated, use GALLERY_FOLDERS instead.")
- self.config['GALLERY_FOLDERS'] = {self.config['GALLERY_PATH']: self.config['GALLERY_PATH']}
- utils.LOGGER.warn("GALLERY_FOLDERS = {0}".format(self.config['GALLERY_FOLDERS']))
- else:
- utils.LOGGER.warn("Both GALLERY_PATH and GALLERY_FOLDERS are specified, ignoring GALLERY_PATH.")
-
if not self.config.get('COPY_SOURCES'):
self.config['SHOW_SOURCELINK'] = False
@@ -833,18 +912,14 @@ class Nikola(object):
self.default_lang = self.config['DEFAULT_LANG']
self.translations = self.config['TRANSLATIONS']
- locale_fallback, locale_default, locales = sanitized_locales(
- self.config.get('LOCALE_FALLBACK', None),
- self.config.get('LOCALE_DEFAULT', None),
- self.config.get('LOCALES', {}), self.translations)
- utils.LocaleBorg.initialize(locales, self.default_lang)
+ utils.LocaleBorg.initialize(self.config.get('LOCALES', {}), self.default_lang)
# BASE_URL defaults to SITE_URL
if 'BASE_URL' not in self.config:
self.config['BASE_URL'] = self.config.get('SITE_URL')
# BASE_URL should *always* end in /
if self.config['BASE_URL'] and self.config['BASE_URL'][-1] != '/':
- utils.LOGGER.warn("Your BASE_URL doesn't end in / -- adding it, but please fix it in your config file!")
+ utils.LOGGER.warning("Your BASE_URL doesn't end in / -- adding it, but please fix it in your config file!")
self.config['BASE_URL'] += '/'
try:
@@ -856,35 +931,26 @@ class Nikola(object):
utils.LOGGER.error("Punycode of {}: {}".format(_bnl, _bnl.encode('idna')))
sys.exit(1)
- # TODO: remove in v8
- if not isinstance(self.config['DEPLOY_COMMANDS'], dict):
- utils.LOGGER.warn("A single list as DEPLOY_COMMANDS is deprecated. DEPLOY_COMMANDS should be a dict, with deploy preset names as keys and lists of commands as values.")
- utils.LOGGER.warn("The key `default` is used by `nikola deploy`:")
- self.config['DEPLOY_COMMANDS'] = {'default': self.config['DEPLOY_COMMANDS']}
- utils.LOGGER.warn("DEPLOY_COMMANDS = {0}".format(self.config['DEPLOY_COMMANDS']))
- utils.LOGGER.info("(The above can be used with `nikola deploy` or `nikola deploy default`. Multiple presets are accepted.)")
-
- # TODO: remove and change default in v8
- if 'BLOG_TITLE' in config and 'WRITE_TAG_CLOUD' not in config:
- # BLOG_TITLE is a hack, otherwise the warning would be displayed
- # when conf.py does not exist
- utils.LOGGER.warn("WRITE_TAG_CLOUD is not set in your config. Defaulting to True (== writing tag_cloud_data.json).")
- utils.LOGGER.warn("Please explicitly add the setting to your conf.py with the desired value, as the setting will default to False in the future.")
-
- # Rename stories to pages (#1891, #2518)
- # TODO: remove in v8
- if 'COMMENTS_IN_STORIES' in config:
- utils.LOGGER.warn('The COMMENTS_IN_STORIES option is deprecated, use COMMENTS_IN_PAGES instead.')
- self.config['COMMENTS_IN_PAGES'] = config['COMMENTS_IN_STORIES']
- if 'STORY_INDEX' in config:
- utils.LOGGER.warn('The STORY_INDEX option is deprecated, use PAGE_INDEX instead.')
- self.config['PAGE_INDEX'] = config['STORY_INDEX']
+ # Load built-in metadata extractors
+ metadata_extractors.load_defaults(self, self.metadata_extractors_by)
+ if metadata_extractors.DEFAULT_EXTRACTOR is None:
+ utils.LOGGER.error("Could not find default meta extractor ({})".format(
+ metadata_extractors.DEFAULT_EXTRACTOR_NAME))
+ sys.exit(1)
+
+ # The Pelican metadata format requires a markdown extension
+ if config.get('METADATA_FORMAT', 'nikola').lower() == 'pelican':
+ if 'markdown.extensions.meta' not in config.get('MARKDOWN_EXTENSIONS', []) \
+ and 'markdown' in self.config['COMPILERS']:
+ utils.LOGGER.warning(
+ 'To use the Pelican metadata format, you need to add '
+ '"markdown.extensions.meta" to your MARKDOWN_EXTENSIONS setting.')
# We use one global tzinfo object all over Nikola.
try:
self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])
except Exception as exc:
- utils.LOGGER.warn("Error getting TZ: {}", exc)
+ utils.LOGGER.warning("Error getting TZ: {}", exc)
self.tzinfo = dateutil.tz.gettz()
self.config['__tzinfo__'] = self.tzinfo
@@ -893,33 +959,23 @@ class Nikola(object):
for k, v in self.config['COMPILERS'].items():
self.config['_COMPILERS_RAW'][k] = list(v)
- compilers = defaultdict(set)
- # Also add aliases for combinations with TRANSLATIONS_PATTERN
- for compiler, exts in self.config['COMPILERS'].items():
- for ext in exts:
- compilers[compiler].add(ext)
- for lang in self.config['TRANSLATIONS'].keys():
- candidate = utils.get_translation_candidate(self.config, "f" + ext, lang)
- compilers[compiler].add(candidate)
-
# Get search path for themes
self.themes_dirs = ['themes'] + self.config['EXTRA_THEMES_DIRS']
- # Avoid redundant compilers
- # Remove compilers that match nothing in POSTS/PAGES
- # And put them in "bad compilers"
- pp_exts = set([os.path.splitext(x[0])[1] for x in self.config['post_pages']])
- self.config['COMPILERS'] = {}
- self.disabled_compilers = {}
- self.bad_compilers = set([])
- for k, v in compilers.items():
- if pp_exts.intersection(v):
- self.config['COMPILERS'][k] = sorted(list(v))
- else:
- self.bad_compilers.add(k)
+ # Register default filters
+ filter_name_format = 'filters.{0}'
+ for filter_name, filter_definition in filters.__dict__.items():
+ # Ignore objects whose name starts with an underscore, or which are not callable
+ if filter_name.startswith('_') or not callable(filter_definition):
+ continue
+ # Register all other objects as filters
+ self.register_filter(filter_name_format.format(filter_name), filter_definition)
self._set_global_context_from_config()
- self._set_global_context_from_data()
+ self._set_all_page_deps_from_config()
+ # Read data files only if a site exists (Issue #2708)
+ if self.configured:
+ self._set_global_context_from_data()
# Set persistent state facility
self.state = Persistor('state_data.json')
@@ -932,6 +988,30 @@ class Nikola(object):
self.state._set_site(self)
self.cache._set_site(self)
+ def _filter_duplicate_plugins(self, plugin_list):
+ """Find repeated plugins and discard the less local copy."""
+ def plugin_position_in_places(plugin):
+ # plugin here is a tuple:
+ # (path to the .plugin file, path to plugin module w/o .py, plugin metadata)
+ for i, place in enumerate(self._plugin_places):
+ if plugin[0].startswith(place):
+ return i
+ utils.LOGGER.warn("Duplicate plugin found in unexpected location: {}".format(plugin[0]))
+ return len(self._plugin_places)
+
+ plugin_dict = defaultdict(list)
+ for data in plugin_list:
+ plugin_dict[data[2].name].append(data)
+ result = []
+ for _, plugins in plugin_dict.items():
+ if len(plugins) > 1:
+ # Sort by locality
+ plugins.sort(key=plugin_position_in_places)
+ utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format(
+ plugins[-1][2].name, plugins[-1][0]))
+ result.append(plugins[-1])
+ return result
+
def init_plugins(self, commands_only=False, load_all=False):
"""Load plugins as needed."""
self.plugin_manager = PluginManager(categories_filter={
@@ -944,25 +1024,37 @@ class Nikola(object):
"CompilerExtension": CompilerExtension,
"MarkdownExtension": MarkdownExtension,
"RestExtension": RestExtension,
+ "MetadataExtractor": MetadataExtractor,
"ShortcodePlugin": ShortcodePlugin,
"SignalHandler": SignalHandler,
"ConfigPlugin": ConfigPlugin,
"PostScanner": PostScanner,
+ "Taxonomy": Taxonomy,
})
self.plugin_manager.getPluginLocator().setPluginInfoExtension('plugin')
extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS']
- if sys.version_info[0] == 3:
- self._plugin_places = [
- resource_filename('nikola', 'plugins'),
- os.path.expanduser('~/.nikola/plugins'),
- os.path.join(os.getcwd(), 'plugins'),
- ] + [path for path in extra_plugins_dirs if path]
- else:
- self._plugin_places = [
- resource_filename('nikola', utils.sys_encode('plugins')),
- os.path.join(os.getcwd(), utils.sys_encode('plugins')),
- os.path.expanduser('~/.nikola/plugins'),
- ] + [utils.sys_encode(path) for path in extra_plugins_dirs if path]
+ self._plugin_places = [
+ resource_filename('nikola', 'plugins'),
+ os.path.expanduser(os.path.join('~', '.nikola', 'plugins')),
+ os.path.join(os.getcwd(), 'plugins'),
+ ] + [path for path in extra_plugins_dirs if path]
+
+ compilers = defaultdict(set)
+ # Also add aliases for combinations with TRANSLATIONS_PATTERN
+ for compiler, exts in self.config['COMPILERS'].items():
+ for ext in exts:
+ compilers[compiler].add(ext)
+ for lang in self.config['TRANSLATIONS'].keys():
+ candidate = utils.get_translation_candidate(self.config, "f" + ext, lang)
+ compilers[compiler].add(candidate)
+
+ # Avoid redundant compilers (if load_all is False):
+ # Remove compilers (and corresponding compiler extensions) that are not marked as
+ # needed by any PostScanner plugin and put them into self.disabled_compilers
+ # (respectively self.disabled_compiler_extensions).
+ self.config['COMPILERS'] = {}
+ self.disabled_compilers = {}
+ self.disabled_compiler_extensions = defaultdict(list)
self.plugin_manager.getPluginLocator().setPluginPlaces(self._plugin_places)
self.plugin_manager.locatePlugins()
@@ -970,55 +1062,80 @@ class Nikola(object):
if not load_all:
for p in self.plugin_manager._candidates:
if commands_only:
- if p[-1].details.has_option('Nikola', 'plugincategory'):
+ if p[-1].details.has_option('Nikola', 'PluginCategory'):
# FIXME TemplateSystem should not be needed
if p[-1].details.get('Nikola', 'PluginCategory') not in {'Command', 'Template'}:
bad_candidates.add(p)
else:
bad_candidates.add(p)
elif self.configured: # Not commands-only, and configured
- # Remove compilers we don't use
- if p[-1].name in self.bad_compilers:
- bad_candidates.add(p)
- self.disabled_compilers[p[-1].name] = p
- utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name)
- if p[-1].name not in self.config['COMPILERS'] and \
- p[-1].details.has_option('Nikola', 'plugincategory') and p[-1].details.get('Nikola', 'PluginCategory') == 'Compiler':
- bad_candidates.add(p)
- self.disabled_compilers[p[-1].name] = p
- utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name)
# Remove blacklisted plugins
if p[-1].name in self.config['DISABLED_PLUGINS']:
bad_candidates.add(p)
utils.LOGGER.debug('Not loading disabled plugin {}', p[-1].name)
+ # Remove compilers we don't use
+ if p[-1].details.has_option('Nikola', 'PluginCategory') and p[-1].details.get('Nikola', 'PluginCategory') in ('Compiler', 'PageCompiler'):
+ bad_candidates.add(p)
+ self.disabled_compilers[p[-1].name] = p
# Remove compiler extensions we don't need
if p[-1].details.has_option('Nikola', 'compiler') and p[-1].details.get('Nikola', 'compiler') in self.disabled_compilers:
bad_candidates.add(p)
- utils.LOGGER.debug('Not loading compiler extension {}', p[-1].name)
+ self.disabled_compiler_extensions[p[-1].details.get('Nikola', 'compiler')].append(p)
self.plugin_manager._candidates = list(set(self.plugin_manager._candidates) - bad_candidates)
- # Find repeated plugins and discard the less local copy
- def plugin_position_in_places(plugin):
- # plugin here is a tuple:
- # (path to the .plugin file, path to plugin module w/o .py, plugin metadata)
- for i, place in enumerate(self._plugin_places):
- if plugin[0].startswith(place):
- return i
-
- plugin_dict = defaultdict(list)
- for data in self.plugin_manager._candidates:
- plugin_dict[data[2].name].append(data)
- self.plugin_manager._candidates = []
- for name, plugins in plugin_dict.items():
- if len(plugins) > 1:
- # Sort by locality
- plugins.sort(key=plugin_position_in_places)
- utils.LOGGER.debug("Plugin {} exists in multiple places, using {}".format(
- plugins[-1][2].name, plugins[-1][0]))
- self.plugin_manager._candidates.append(plugins[-1])
-
+ self.plugin_manager._candidates = self._filter_duplicate_plugins(self.plugin_manager._candidates)
self.plugin_manager.loadPlugins()
+ # Search for compiler plugins which we disabled but shouldn't have
+ self._activate_plugins_of_category("PostScanner")
+ if not load_all:
+ file_extensions = set()
+ for post_scanner in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('PostScanner')]:
+ exts = post_scanner.supported_extensions()
+ if exts is not None:
+ file_extensions.update(exts)
+ else:
+ # Stop scanning for more: once we get None, we have to load all compilers anyway
+ utils.LOGGER.debug("Post scanner {0!r} does not implement `supported_extensions`, loading all compilers".format(post_scanner))
+ file_extensions = None
+ break
+ to_add = []
+ for k, v in compilers.items():
+ if file_extensions is None or file_extensions.intersection(v):
+ self.config['COMPILERS'][k] = sorted(list(v))
+ p = self.disabled_compilers.pop(k, None)
+ if p:
+ to_add.append(p)
+ for p in self.disabled_compiler_extensions.pop(k, []):
+ to_add.append(p)
+ for _, p in self.disabled_compilers.items():
+ utils.LOGGER.debug('Not loading unneeded compiler {}', p[-1].name)
+ for _, plugins in self.disabled_compiler_extensions.items():
+ for p in plugins:
+ utils.LOGGER.debug('Not loading compiler extension {}', p[-1].name)
+ if to_add:
+ self.plugin_manager._candidates = self._filter_duplicate_plugins(to_add)
+ self.plugin_manager.loadPlugins()
+
+ # Jupyter theme configuration. If a website has ipynb enabled in post_pages
+ # we should enable the Jupyter CSS (leaving that up to the theme itself).
+ if 'needs_ipython_css' not in self._GLOBAL_CONTEXT:
+ self._GLOBAL_CONTEXT['needs_ipython_css'] = 'ipynb' in self.config['COMPILERS']
+
+ # Activate metadata extractors and prepare them for use
+ for p in self._activate_plugins_of_category("MetadataExtractor"):
+ metadata_extractors.classify_extractor(p.plugin_object, self.metadata_extractors_by)
+
+ self._activate_plugins_of_category("Taxonomy")
+ self.taxonomy_plugins = {}
+ for taxonomy in [p.plugin_object for p in self.plugin_manager.getPluginsOfCategory('Taxonomy')]:
+ if not taxonomy.is_enabled():
+ continue
+ if taxonomy.classification_name in self.taxonomy_plugins:
+ utils.LOGGER.error("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name))
+ sys.exit(1)
+ self.taxonomy_plugins[taxonomy.classification_name] = taxonomy
+
self._activate_plugins_of_category("SignalHandler")
# Emit signal for SignalHandlers which need to start running immediately.
@@ -1031,7 +1148,6 @@ class Nikola(object):
plugin_info.plugin_object.short_help = plugin_info.description
self._commands[plugin_info.name] = plugin_info.plugin_object
- self._activate_plugins_of_category("PostScanner")
self._activate_plugins_of_category("Task")
self._activate_plugins_of_category("LateTask")
self._activate_plugins_of_category("TaskMultiplier")
@@ -1055,8 +1171,28 @@ class Nikola(object):
self.compilers[plugin_info.name] = \
plugin_info.plugin_object
+ # Load config plugins and register templated shortcodes
self._activate_plugins_of_category("ConfigPlugin")
self._register_templated_shortcodes()
+
+ # Check with registered filters and configure filters
+ for actions in self.config['FILTERS'].values():
+ for i, f in enumerate(actions):
+ if isinstance(f, str):
+ # Check whether this denotes a registered filter
+ _f = self.filters.get(f)
+ if _f is not None:
+ f = _f
+ actions[i] = f
+ if hasattr(f, 'configuration_variables'):
+ args = {}
+ for arg, config in f.configuration_variables.items():
+ if config in self.config:
+ args[arg] = self.config[config]
+ if args:
+ actions[i] = functools.partial(f, **args)
+
+ # Signal that we are configured
signal('configured').send(self)
def _set_global_context_from_config(self):
@@ -1075,30 +1211,24 @@ class Nikola(object):
self._GLOBAL_CONTEXT['rel_link'] = self.rel_link
self._GLOBAL_CONTEXT['abs_link'] = self.abs_link
self._GLOBAL_CONTEXT['exists'] = self.file_exists
- self._GLOBAL_CONTEXT['SLUG_AUTHOR_PATH'] = self.config['SLUG_AUTHOR_PATH']
- self._GLOBAL_CONTEXT['SLUG_TAG_PATH'] = self.config['SLUG_TAG_PATH']
- self._GLOBAL_CONTEXT['annotations'] = self.config['ANNOTATIONS']
self._GLOBAL_CONTEXT['index_display_post_count'] = self.config[
'INDEX_DISPLAY_POST_COUNT']
self._GLOBAL_CONTEXT['index_file'] = self.config['INDEX_FILE']
- self._GLOBAL_CONTEXT['use_base_tag'] = self.config['USE_BASE_TAG']
self._GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
self._GLOBAL_CONTEXT['theme_color'] = self.config.get("THEME_COLOR")
+ self._GLOBAL_CONTEXT['theme_config'] = self.config.get("THEME_CONFIG")
self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
self._GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT')
self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
self._GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE')
+ self._GLOBAL_CONTEXT['blog_email'] = self.config.get('BLOG_EMAIL')
self._GLOBAL_CONTEXT['show_blog_title'] = self.config.get('SHOW_BLOG_TITLE')
self._GLOBAL_CONTEXT['logo_url'] = self.config.get('LOGO_URL')
self._GLOBAL_CONTEXT['blog_description'] = self.config.get('BLOG_DESCRIPTION')
self._GLOBAL_CONTEXT['front_index_header'] = self.config.get('FRONT_INDEX_HEADER')
self._GLOBAL_CONTEXT['color_hsl_adjust_hex'] = utils.color_hsl_adjust_hex
self._GLOBAL_CONTEXT['colorize_str_from_base_color'] = utils.colorize_str_from_base_color
-
- # TODO: remove in v8
- self._GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
-
self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL')
self._GLOBAL_CONTEXT['template_hooks'] = self.template_hooks
self._GLOBAL_CONTEXT['body_end'] = self.config.get('BODY_END')
@@ -1113,19 +1243,15 @@ class Nikola(object):
'MATHJAX_CONFIG')
self._GLOBAL_CONTEXT['use_katex'] = self.config.get('USE_KATEX')
self._GLOBAL_CONTEXT['katex_auto_render'] = self.config.get('KATEX_AUTO_RENDER')
- self._GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONFIG_SUBTHEME')
- self._GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONFIG_TRANSITION')
self._GLOBAL_CONTEXT['content_footer'] = self.config.get(
'CONTENT_FOOTER')
self._GLOBAL_CONTEXT['generate_atom'] = self.config.get('GENERATE_ATOM')
self._GLOBAL_CONTEXT['generate_rss'] = self.config.get('GENERATE_RSS')
- self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
self._GLOBAL_CONTEXT['navigation_links'] = self.config.get('NAVIGATION_LINKS')
+ self._GLOBAL_CONTEXT['navigation_alt_links'] = self.config.get('NAVIGATION_ALT_LINKS')
- self._GLOBAL_CONTEXT['use_open_graph'] = self.config.get(
- 'USE_OPEN_GRAPH', True)
self._GLOBAL_CONTEXT['twitter_card'] = self.config.get(
'TWITTER_CARD', {})
self._GLOBAL_CONTEXT['hide_sourcelink'] = not self.config.get(
@@ -1134,25 +1260,24 @@ class Nikola(object):
'SHOW_SOURCELINK')
self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
self._GLOBAL_CONTEXT['date_fanciness'] = self.config.get('DATE_FANCINESS')
- self._GLOBAL_CONTEXT['js_date_format'] = self.config.get('JS_DATE_FORMAT')
- self._GLOBAL_CONTEXT['colorbox_locales'] = LEGAL_VALUES['COLORBOX_LOCALES']
+ self._GLOBAL_CONTEXT['luxon_locales'] = LEGAL_VALUES['LUXON_LOCALES']
+ self._GLOBAL_CONTEXT['luxon_date_format'] = self.config.get('LUXON_DATE_FORMAT')
+ # TODO: remove in v9
+ self._GLOBAL_CONTEXT['js_date_format'] = self.config.get('MOMENTJS_DATE_FORMAT')
self._GLOBAL_CONTEXT['momentjs_locales'] = LEGAL_VALUES['MOMENTJS_LOCALES']
+ # Patch missing locales into momentjs defaulting to English (Issue #3216)
+ for l in self._GLOBAL_CONTEXT['translations']:
+ if l not in self._GLOBAL_CONTEXT['momentjs_locales']:
+ self._GLOBAL_CONTEXT['momentjs_locales'][l] = ""
self._GLOBAL_CONTEXT['hidden_tags'] = self.config.get('HIDDEN_TAGS')
self._GLOBAL_CONTEXT['hidden_categories'] = self.config.get('HIDDEN_CATEGORIES')
self._GLOBAL_CONTEXT['hidden_authors'] = self.config.get('HIDDEN_AUTHORS')
self._GLOBAL_CONTEXT['url_replacer'] = self.url_replacer
- self._GLOBAL_CONTEXT['posts_sections'] = self.config.get('POSTS_SECTIONS')
- self._GLOBAL_CONTEXT['posts_section_are_indexes'] = self.config.get('POSTS_SECTION_ARE_INDEXES')
- self._GLOBAL_CONTEXT['posts_section_colors'] = self.config.get('POSTS_SECTION_COLORS')
- self._GLOBAL_CONTEXT['posts_section_descriptions'] = self.config.get('POSTS_SECTION_DESCRIPTIONS')
- self._GLOBAL_CONTEXT['posts_section_from_meta'] = self.config.get('POSTS_SECTION_FROM_META')
- self._GLOBAL_CONTEXT['posts_section_name'] = self.config.get('POSTS_SECTION_NAME')
- self._GLOBAL_CONTEXT['posts_section_title'] = self.config.get('POSTS_SECTION_TITLE')
-
- # IPython theme configuration. If a website has ipynb enabled in post_pages
- # we should enable the IPython CSS (leaving that up to the theme itself).
-
- self._GLOBAL_CONTEXT['needs_ipython_css'] = 'ipynb' in self.config['COMPILERS']
+ self._GLOBAL_CONTEXT['sort_posts'] = utils.sort_posts
+ self._GLOBAL_CONTEXT['smartjoin'] = utils.smartjoin
+ self._GLOBAL_CONTEXT['colorize_str'] = utils.colorize_str
+ self._GLOBAL_CONTEXT['meta_generator_tag'] = self.config.get('META_GENERATOR_TAG')
+ self._GLOBAL_CONTEXT['multiple_authors_per_post'] = self.config.get('MULTIPLE_AUTHORS_PER_POST')
self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
@@ -1165,6 +1290,24 @@ class Nikola(object):
data = utils.load_data(fname)
key = os.path.splitext(fname.split(os.sep, 1)[1])[0]
self._GLOBAL_CONTEXT['data'][key] = data
+ # Offer global_data as an alias for data (Issue #2488)
+ self._GLOBAL_CONTEXT['global_data'] = self._GLOBAL_CONTEXT['data']
+
+ def _set_all_page_deps_from_config(self):
+ """Save dependencies for all pages from configuration.
+
+ Changes of values in this dict will force a rebuild of all pages.
+ Unlike global context, contents are NOT available to templates.
+ """
+ self.ALL_PAGE_DEPS['atom_extension'] = self.config.get('ATOM_EXTENSION')
+ self.ALL_PAGE_DEPS['atom_path'] = self.config.get('ATOM_PATH')
+ self.ALL_PAGE_DEPS['rss_extension'] = self.config.get('RSS_EXTENSION')
+ self.ALL_PAGE_DEPS['rss_path'] = self.config.get('RSS_PATH')
+ self.ALL_PAGE_DEPS['rss_filename_base'] = self.config.get('RSS_FILENAME_BASE')
+ self.ALL_PAGE_DEPS['atom_filename_base'] = self.config.get('ATOM_FILENAME_BASE')
+ self.ALL_PAGE_DEPS['slug_author_path'] = self.config.get('SLUG_AUTHOR_PATH')
+ self.ALL_PAGE_DEPS['slug_tag_path'] = self.config.get('SLUG_TAG_PATH')
+ self.ALL_PAGE_DEPS['locale'] = self.config.get('LOCALE')
def _activate_plugins_of_category(self, category):
"""Activate all the plugins of a given category and return them."""
@@ -1181,17 +1324,18 @@ class Nikola(object):
try:
self._THEMES = utils.get_theme_chain(self.config['THEME'], self.themes_dirs)
except Exception:
- if self.config['THEME'] != 'bootstrap3':
- utils.LOGGER.warn('''Cannot load theme "{0}", using 'bootstrap3' instead.'''.format(self.config['THEME']))
- self.config['THEME'] = 'bootstrap3'
+ if self.config['THEME'] != LEGAL_VALUES['DEFAULT_THEME']:
+ utils.LOGGER.warning('''Cannot load theme "{0}", using '{1}' instead.'''.format(
+ self.config['THEME'], LEGAL_VALUES['DEFAULT_THEME']))
+ self.config['THEME'] = LEGAL_VALUES['DEFAULT_THEME']
return self._get_themes()
raise
# Check consistency of USE_CDN and the current THEME (Issue #386)
if self.config['USE_CDN'] and self.config['USE_CDN_WARNING']:
bootstrap_path = utils.get_asset_path(os.path.join(
'assets', 'css', 'bootstrap.min.css'), self._THEMES)
- if bootstrap_path and bootstrap_path.split(os.sep)[-4] not in ['bootstrap', 'bootstrap3']:
- utils.LOGGER.warn('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.')
+ if bootstrap_path and bootstrap_path.split(os.sep)[-4] not in ['bootstrap', 'bootstrap3', 'bootstrap4']:
+ utils.LOGGER.warning('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.')
return self._THEMES
@@ -1260,7 +1404,7 @@ class Nikola(object):
"""
ext = os.path.splitext(source_name)[1]
try:
- compile_html = self.inverse_compilers[ext]
+ compiler = self.inverse_compilers[ext]
except KeyError:
# Find the correct compiler for this files extension
lang_exts_tab = list(self.config['COMPILERS'].items())
@@ -1268,26 +1412,28 @@ class Nikola(object):
len([ext_ for ext_ in exts if source_name.endswith(ext_)]) > 0]
if len(langs) != 1:
if len(set(langs)) > 1:
- exit("Your file extension->compiler definition is "
- "ambiguous.\nPlease remove one of the file extensions "
- "from 'COMPILERS' in conf.py\n(The error is in "
- "one of {0})".format(', '.join(langs)))
+ sys.exit("Your file extension->compiler definition is "
+ "ambiguous.\nPlease remove one of the file "
+ "extensions from 'COMPILERS' in conf.py\n(The "
+ "error is in one of {0})".format(', '.join(langs)))
elif len(langs) > 1:
langs = langs[:1]
else:
- exit("COMPILERS in conf.py does not tell me how to "
- "handle '{0}' extensions.".format(ext))
+ sys.exit("COMPILERS in conf.py does not tell me how to "
+ "handle '{0}' extensions.".format(ext))
lang = langs[0]
try:
- compile_html = self.compilers[lang]
+ compiler = self.compilers[lang]
except KeyError:
- exit("Cannot find '{0}' compiler; it might require an extra plugin -- do you have it installed?".format(lang))
- self.inverse_compilers[ext] = compile_html
+ sys.exit("Cannot find '{0}' compiler; "
+ "it might require an extra plugin -- "
+ "do you have it installed?".format(lang))
+ self.inverse_compilers[ext] = compiler
- return compile_html
+ return compiler
- def render_template(self, template_name, output_name, context, url_type=None):
+ def render_template(self, template_name, output_name, context, url_type=None, is_fragment=False):
"""Render a template with the global context.
If ``output_name`` is None, will return a string and all URL
@@ -1298,6 +1444,9 @@ class Nikola(object):
The argument ``url_type`` allows to override the ``URL_TYPE``
configuration.
+
+ If ``is_fragment`` is set to ``True``, a HTML fragment will
+ be rendered and not a whole HTML document.
"""
local_context = {}
local_context["template_name"] = template_name
@@ -1306,6 +1455,11 @@ class Nikola(object):
for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
local_context[k] = local_context[k](local_context['lang'])
local_context['is_rtl'] = local_context['lang'] in LEGAL_VALUES['RTL_LANGUAGES']
+ local_context['url_type'] = self.config['URL_TYPE'] if url_type is None else url_type
+ local_context["translations_feedorder"] = sorted(
+ local_context["translations"],
+ key=lambda x: (int(x != local_context['lang']), x)
+ )
# string, arguments
local_context["formatmsg"] = lambda s, *a: s % a
for h in local_context['template_hooks'].values():
@@ -1320,8 +1474,8 @@ class Nikola(object):
if output_name is None:
return data
- assert output_name.startswith(
- self.config["OUTPUT_FOLDER"])
+ if not output_name.startswith(self.config["OUTPUT_FOLDER"]):
+ raise ValueError("Output path for templates must start with OUTPUT_FOLDER")
url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:]
# Treat our site as if output/ is "/" and then make all URLs relative,
@@ -1333,9 +1487,18 @@ class Nikola(object):
utils.makedirs(os.path.dirname(output_name))
parser = lxml.html.HTMLParser(remove_blank_text=True)
- doc = lxml.html.document_fromstring(data, parser)
+ if is_fragment:
+ doc = lxml.html.fragment_fromstring(data.strip(), parser)
+ else:
+ doc = lxml.html.document_fromstring(data.strip(), parser)
self.rewrite_links(doc, src, context['lang'], url_type)
- data = b'<!DOCTYPE html>\n' + lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True)
+ if is_fragment:
+ # doc.text contains text before the first HTML, or None if there was no text
+ # The text after HTML elements is added by tostring() (because its implicit
+ # argument with_tail has default value True).
+ data = (doc.text or '').encode('utf-8') + b''.join([lxml.html.tostring(child, encoding='utf-8', method='html') for child in doc.iterchildren()])
+ else:
+ data = lxml.html.tostring(doc, encoding='utf8', method='html', pretty_print=True, doctype='<!DOCTYPE html>')
with open(output_name, "wb+") as post_file:
post_file.write(data)
@@ -1345,7 +1508,7 @@ class Nikola(object):
doc.rewrite_links(lambda dst: self.url_replacer(src, dst, lang, url_type), resolve_base_href=False)
# lxml ignores srcset in img and source elements, so do that by hand
- objs = list(doc.xpath('(*//img|*//source)'))
+ objs = list(doc.xpath('(//img|//source)'))
for obj in objs:
if 'srcset' in obj.attrib:
urls = [u.strip() for u in obj.attrib['srcset'].split(',')]
@@ -1366,6 +1529,10 @@ class Nikola(object):
lang is used for language-sensitive URLs in link://
url_type is used to determine final link appearance, defaulting to URL_TYPE from config
"""
+ # Avoid mangling links within the page
+ if dst.startswith('#'):
+ return dst
+
parsed_src = urlsplit(src)
src_elems = parsed_src.path.split('/')[1:]
dst_url = urlparse(dst)
@@ -1380,7 +1547,17 @@ class Nikola(object):
# Refuse to replace links that are full URLs.
if dst_url.netloc:
if dst_url.scheme == 'link': # Magic link
- dst = self.link(dst_url.netloc, dst_url.path.lstrip('/'), lang)
+ if dst_url.query:
+ # If query strings are used in magic link, they will be
+ # passed to the path handler as keyword arguments (strings)
+ link_kwargs = {unquote(k): unquote(v[-1]) for k, v in parse_qs(dst_url.query).items()}
+ else:
+ link_kwargs = {}
+
+ # unquote from issue #2934
+ dst = self.link(dst_url.netloc, unquote(dst_url.path.lstrip('/')), lang, **link_kwargs)
+ if dst_url.fragment:
+ dst += '#' + dst_url.fragment
# Assuming the site is served over one of these, and
# since those are the only URLs we want to rewrite...
else:
@@ -1393,7 +1570,7 @@ class Nikola(object):
# python 3: already unicode
pass
nl = nl.encode('idna')
- if isinstance(nl, utils.bytes_str):
+ if isinstance(nl, bytes):
nl = nl.decode('latin-1') # so idna stays unchanged
dst = urlunsplit((dst_url.scheme,
nl,
@@ -1421,7 +1598,7 @@ class Nikola(object):
return dst
elif url_type == 'full_path':
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
- return urlparse(dst).path
+ return utils.full_path_from_urlparse(urlparse(dst))
else:
return "#"
@@ -1436,10 +1613,7 @@ class Nikola(object):
dst = urljoin(self.config['BASE_URL'], dst.lstrip('/'))
if url_type == 'full_path':
parsed = urlparse(urljoin(self.config['BASE_URL'], dst.lstrip('/')))
- if parsed.fragment:
- dst = '{0}#{1}'.format(parsed.path, parsed.fragment)
- else:
- dst = parsed.path
+ dst = utils.full_path_from_urlparse(parsed)
return dst
# Now both paths are on the same site and absolute
@@ -1463,7 +1637,8 @@ class Nikola(object):
if parsed_dst.fragment:
result += "#" + parsed_dst.fragment
- assert result, (src, dst, i, src_elems, dst_elems)
+ if not result:
+ raise ValueError("Failed to parse link: {0}".format((src, dst, i, src_elems, dst_elems)))
return result
@@ -1476,6 +1651,12 @@ class Nikola(object):
keyword argument dict and then the latter provides the template
context.
+ Global context keys are made available as part of the context,
+ respecting locale.
+
+ As a special quirk, the "data" key from global_context is
+ available only as "global_data" because of name clobbering.
+
"""
def render_shortcode(*args, **kw):
context = self.GLOBAL_CONTEXT.copy()
@@ -1533,30 +1714,58 @@ class Nikola(object):
def register_shortcode(self, name, f):
"""Register function f to handle shortcode "name"."""
if name in self.shortcode_registry:
- utils.LOGGER.warn('Shortcode name conflict: {}', name)
+ utils.LOGGER.warning('Shortcode name conflict: {}', name)
return
self.shortcode_registry[name] = f
- # XXX in v8, get rid of with_dependencies
- def apply_shortcodes(self, data, filename=None, lang=None, with_dependencies=False, extra_context={}):
+ def apply_shortcodes(self, data, filename=None, lang=None, extra_context=None):
"""Apply shortcodes from the registry on data."""
+ if extra_context is None:
+ extra_context = {}
if lang is None:
lang = utils.LocaleBorg().current_lang
- return shortcodes.apply_shortcodes(data, self.shortcode_registry, self, filename, lang=lang, with_dependencies=with_dependencies, extra_context=extra_context)
+ return shortcodes.apply_shortcodes(data, self.shortcode_registry, self, filename, lang=lang, extra_context=extra_context)
- def generic_rss_renderer(self, lang, title, link, description, timeline, output_path,
- rss_teasers, rss_plain, feed_length=10, feed_url=None,
- enclosure=_enclosure, rss_links_append_query=None):
- """Take all necessary data, and render a RSS feed in output_path."""
+ def apply_shortcodes_uuid(self, data, _shortcodes, filename=None, lang=None, extra_context=None):
+ """Apply shortcodes from the registry on data."""
+ if lang is None:
+ lang = utils.LocaleBorg().current_lang
+ if extra_context is None:
+ extra_context = {}
+ deps = []
+ for k, v in _shortcodes.items():
+ replacement, _deps = shortcodes.apply_shortcodes(v, self.shortcode_registry, self, filename, lang=lang, extra_context=extra_context)
+ data = data.replace(k, replacement)
+ deps.extend(_deps)
+ return data, deps
+
+ def _get_rss_copyright(self, lang, rss_plain):
+ if rss_plain:
+ return (
+ self.config['RSS_COPYRIGHT_PLAIN'](lang) or
+ lxml.html.fromstring(self.config['RSS_COPYRIGHT'](lang)).text_content().strip())
+ else:
+ return self.config['RSS_COPYRIGHT'](lang)
+
+ def generic_rss_feed(self, lang, title, link, description, timeline,
+ rss_teasers, rss_plain, feed_length=10, feed_url=None,
+ enclosure=_enclosure, rss_links_append_query=None, copyright_=None):
+ """Generate an ExtendedRSS2 feed object for later use."""
rss_obj = utils.ExtendedRSS2(
title=title,
link=utils.encodelink(link),
description=description,
lastBuildDate=datetime.datetime.utcnow(),
- generator='https://getnikola.com/',
+ generator='Nikola (getnikola.com)',
language=lang
)
+ if copyright_ is None:
+ copyright_ = self._get_rss_copyright(lang, rss_plain)
+ # Use the configured or specified copyright string if present.
+ if copyright_:
+ rss_obj.copyright = copyright_
+
if feed_url:
absurl = '/' + feed_url[len(self.config['BASE_URL']):]
rss_obj.xsl_stylesheet_href = self.url_replacer(absurl, "/assets/xml/rss.xsl")
@@ -1565,6 +1774,8 @@ class Nikola(object):
feed_append_query = None
if rss_links_append_query:
+ if rss_links_append_query is True:
+ raise ValueError("RSS_LINKS_APPEND_QUERY (or FEED_LINKS_APPEND_QUERY) cannot be True. Valid values are False or a formattable string.")
feed_append_query = rss_links_append_query.format(
feedRelUri='/' + feed_url[len(self.config['BASE_URL']):],
feedFormat="rss")
@@ -1575,7 +1786,7 @@ class Nikola(object):
if feed_url is not None and data:
# Massage the post's HTML (unless plain)
if not rss_plain:
- if self.config["FEED_PREVIEWIMAGE"] and 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in data:
+ if 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in data:
data = "<figure><img src=\"{}\"></figure> {}".format(post.meta[lang]['previewimage'], data)
# FIXME: this is duplicated with code in Post.text()
try:
@@ -1592,9 +1803,9 @@ class Nikola(object):
if str(e) == "Document is empty":
data = ""
else: # let other errors raise
- raise(e)
+ raise
args = {
- 'title': post.title(lang),
+ 'title': post.title(lang) if post.should_show_title() else None,
'link': post.permalink(lang, absolute=True, query=feed_append_query),
'description': data,
# PyRSS2Gen's pubDate is GMT time.
@@ -1602,7 +1813,7 @@ class Nikola(object):
post.date.astimezone(dateutil.tz.tzutc())),
'categories': post._tags.get(lang, []),
'creator': post.author(lang),
- 'guid': post.permalink(lang, absolute=True),
+ 'guid': post.guid(lang),
}
if post.author(lang):
@@ -1620,16 +1831,18 @@ class Nikola(object):
rss_obj.items = items
rss_obj.self_url = feed_url
rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
+ return rss_obj
- dst_dir = os.path.dirname(output_path)
- utils.makedirs(dst_dir)
- with io.open(output_path, "w+", encoding="utf-8") as rss_file:
- data = rss_obj.to_xml(encoding='utf-8')
- if isinstance(data, utils.bytes_str):
- data = data.decode('utf-8')
- rss_file.write(data)
+ def generic_rss_renderer(self, lang, title, link, description, timeline, output_path,
+ rss_teasers, rss_plain, feed_length=10, feed_url=None,
+ enclosure=_enclosure, rss_links_append_query=None, copyright_=None):
+ """Take all necessary data, and render a RSS feed in output_path."""
+ rss_obj = self.generic_rss_feed(lang, title, link, description, timeline,
+ rss_teasers, rss_plain, feed_length=feed_length, feed_url=feed_url,
+ enclosure=enclosure, rss_links_append_query=rss_links_append_query, copyright_=copyright_)
+ utils.rss_writer(rss_obj, output_path)
- def path(self, kind, name, lang=None, is_link=False):
+ def path(self, kind, name, lang=None, is_link=False, **kwargs):
r"""Build the path to a certain kind of page.
These are mostly defined by plugins by registering via the
@@ -1653,35 +1866,47 @@ class Nikola(object):
* slug (name is the slug of a post or page)
* filename (name is the source filename of a post/page, in DEFAULT_LANG, relative to conf.py)
- The returned value is always a path relative to output, like
- "categories/whatever.html"
+ The returned value is either a path relative to output, like "categories/whatever.html", or
+ an absolute URL ("https://getnikola.com/"), if path handler returns a string.
If is_link is True, the path is absolute and uses "/" as separator
(ex: "/archive/index.html").
If is_link is False, the path is relative to output and uses the
platform's separator.
(ex: "archive\index.html")
+ If the registered path handler returns a string instead of path component list - it's
+ considered to be an absolute URL and returned as is.
+
"""
if lang is None:
lang = utils.LocaleBorg().current_lang
try:
- path = self.path_handlers[kind](name, lang)
- path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028
- if is_link:
- link = '/' + ('/'.join(path))
- index_len = len(self.config['INDEX_FILE'])
- if self.config['STRIP_INDEXES'] and \
- link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']:
- return link[:-index_len]
- else:
- return link
- else:
- return os.path.join(*path)
+ path = self.path_handlers[kind](name, lang, **kwargs)
except KeyError:
- utils.LOGGER.warn("Unknown path request of kind: {0}".format(kind))
+ utils.LOGGER.warning("Unknown path request of kind: {0}".format(kind))
return ""
+ # If path handler returns a string we consider it to be an absolute URL not requiring any
+ # further processing, i.e 'https://getnikola.com/'. See Issue #2876.
+ if isinstance(path, str):
+ return path
+
+ if path is None:
+ path = "#"
+ else:
+ path = [os.path.normpath(p) for p in path if p != '.'] # Fix Issue #1028
+ if is_link:
+ link = '/' + ('/'.join(path))
+ index_len = len(self.config['INDEX_FILE'])
+ if self.config['STRIP_INDEXES'] and \
+ link[-(1 + index_len):] == '/' + self.config['INDEX_FILE']:
+ return link[:-index_len]
+ else:
+ return link
+ else:
+ return os.path.join(*path)
+
def post_path(self, name, lang):
"""Link to the destination of an element in the POSTS/PAGES settings.
@@ -1709,7 +1934,7 @@ class Nikola(object):
return []
def slug_path(self, name, lang):
- """A link to a post with given slug, if not ambiguous.
+ """Return a link to a post with given slug, if not ambiguous.
Example:
@@ -1721,7 +1946,7 @@ class Nikola(object):
else:
if len(results) > 1:
utils.LOGGER.warning('Ambiguous path request for slug: {0}'.format(name))
- return [_f for _f in results[0].permalink(lang).split('/') if _f]
+ return [_f for _f in results[0].permalink(lang).split('/')]
def filename_path(self, name, lang):
"""Link to post or page by source filename.
@@ -1745,9 +1970,9 @@ class Nikola(object):
else:
self.path_handlers[kind] = f
- def link(self, *args):
+ def link(self, *args, **kwargs):
"""Create a link."""
- url = self.path(*args, is_link=True)
+ url = self.path(*args, is_link=True, **kwargs)
url = utils.encodelink(url)
return url
@@ -1791,6 +2016,17 @@ class Nikola(object):
url = utils.encodelink(url)
return url
+ def register_filter(self, filter_name, filter_definition):
+ """Register a filter.
+
+ filter_name should be a name not confusable with an actual
+ executable. filter_definition should be a callable accepting
+ one argument (the filename).
+ """
+ if filter_name in self.filters:
+ utils.LOGGER.warning('''The filter "{0}" is defined more than once.'''.format(filter_name))
+ self.filters[filter_name] = filter_definition
+
def file_exists(self, path, not_empty=False):
"""Check if the file exists. If not_empty is True, it also must not be empty."""
exists = os.path.exists(path)
@@ -1819,7 +2055,8 @@ class Nikola(object):
task_dep = []
for pluginInfo in self.plugin_manager.getPluginsOfCategory(plugin_category):
for task in flatten(pluginInfo.plugin_object.gen_tasks()):
- assert 'basename' in task
+ if 'basename' not in task:
+ raise ValueError("Task {0} does not have a basename".format(task))
task = self.clean_task_paths(task)
if 'task_dep' not in task:
task['task_dep'] = []
@@ -1846,7 +2083,7 @@ class Nikola(object):
"""Parse a category name into a hierarchy."""
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
try:
- return utils.parse_escaped_hierarchical_category_name(category_name)
+ return hierarchy_utils.parse_escaped_hierarchical_category_name(category_name)
except Exception as e:
utils.LOGGER.error(str(e))
sys.exit(1)
@@ -1856,7 +2093,7 @@ class Nikola(object):
def category_path_to_category_name(self, category_path):
"""Translate a category path to a category name."""
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
- return utils.join_hierarchical_category_path(category_path)
+ return hierarchy_utils.join_hierarchical_category_path(category_path)
else:
return ''.join(category_path)
@@ -1881,7 +2118,7 @@ class Nikola(object):
"""Create category hierarchy."""
result = []
for name, children in cat_hierarchy.items():
- node = utils.TreeNode(name, parent)
+ node = hierarchy_utils.TreeNode(name, parent)
node.children = create_hierarchy(children, node)
node.category_path = [pn.name for pn in node.get_path()]
node.category_name = self.category_path_to_category_name(node.category_path)
@@ -1892,7 +2129,25 @@ class Nikola(object):
root_list = create_hierarchy(self.category_hierarchy)
# Next, flatten the hierarchy
- self.category_hierarchy = utils.flatten_tree_structure(root_list)
+ self.category_hierarchy = hierarchy_utils.flatten_tree_structure(root_list)
+
+ @staticmethod
+ def sort_posts_chronologically(posts, lang=None):
+ """Sort a list of posts chronologically.
+
+ This function also takes priority, title and source path into account.
+ """
+ # Last tie breaker: sort by source path (A-Z)
+ posts = sorted(posts, key=lambda p: p.source_path)
+ # Next tie breaker: sort by title if language is given (A-Z)
+ if lang is not None:
+ posts = natsort.natsorted(posts, key=lambda p: p.title(lang), alg=natsort.ns.F | natsort.ns.IC)
+ # Next tie breaker: sort by date (reverse chronological order)
+ posts = sorted(posts, key=lambda p: p.date, reverse=True)
+ # Finally, sort by priority meta value (descending)
+ posts = sorted(posts, key=lambda p: int(p.meta('priority')) if p.meta('priority') else 0, reverse=True)
+ # Return result
+ return posts
def scan_posts(self, really=False, ignore_quit=False, quiet=False):
"""Scan all the posts.
@@ -1916,8 +2171,12 @@ class Nikola(object):
self.timeline = []
self.pages = []
- for p in self.plugin_manager.getPluginsOfCategory('PostScanner'):
- timeline = p.plugin_object.scan()
+ for p in sorted(self.plugin_manager.getPluginsOfCategory('PostScanner'), key=operator.attrgetter('name')):
+ try:
+ timeline = p.plugin_object.scan()
+ except Exception:
+ utils.LOGGER.error('Error reading timeline')
+ raise
# FIXME: can there be conflicts here?
self.timeline.extend(timeline)
@@ -1933,16 +2192,7 @@ class Nikola(object):
for lang in self.config['TRANSLATIONS'].keys():
for tag in post.tags_for_language(lang):
_tag_slugified = utils.slugify(tag, lang)
- if _tag_slugified in slugged_tags[lang]:
- if tag not in self.posts_per_tag:
- # Tags that differ only in case
- other_tag = [existing for existing in self.posts_per_tag.keys() if utils.slugify(existing, lang) == _tag_slugified][0]
- utils.LOGGER.error('You have tags that are too similar: {0} and {1}'.format(tag, other_tag))
- utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path))
- utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join([p.source_path for p in self.posts_per_tag[other_tag]])))
- quit = True
- else:
- slugged_tags[lang].add(_tag_slugified)
+ slugged_tags[lang].add(_tag_slugified)
if post not in self.posts_per_tag[tag]:
self.posts_per_tag[tag].append(post)
self.tags_per_language[lang].extend(post.tags_for_language(lang))
@@ -1972,17 +2222,17 @@ class Nikola(object):
quit = True
self.post_per_file[dest] = post
self.post_per_file[src_dest] = post
- self.post_per_input_file[src_file] = post
+ if src_file is not None:
+ self.post_per_input_file[src_file] = post
# deduplicate tags_per_language
self.tags_per_language[lang] = list(set(self.tags_per_language[lang]))
# Sort everything.
- for thing in self.timeline, self.posts, self.all_posts, self.pages:
- thing.sort(key=lambda p:
- (int(p.meta('priority')) if p.meta('priority') else 0,
- p.date, p.source_path))
- thing.reverse()
+ self.timeline = self.sort_posts_chronologically(self.timeline)
+ self.posts = self.sort_posts_chronologically(self.posts)
+ self.all_posts = self.sort_posts_chronologically(self.all_posts)
+ self.pages = self.sort_posts_chronologically(self.pages)
self._sort_category_hierarchy()
for i, p in enumerate(self.posts[1:]):
@@ -1996,8 +2246,8 @@ class Nikola(object):
sys.exit(1)
signal('scanned').send(self)
- def generic_renderer(self, lang, output_name, template_name, filters, file_deps=None, uptodate_deps=None, context=None, context_deps_remove=None, post_deps_dict=None, url_type=None):
- """Helper function for rendering pages and post lists and other related pages.
+ def generic_renderer(self, lang, output_name, template_name, filters, file_deps=None, uptodate_deps=None, context=None, context_deps_remove=None, post_deps_dict=None, url_type=None, is_fragment=False):
+ """Create tasks for rendering pages and post lists and other related pages.
lang is the current language.
output_name is the destination file name.
@@ -2008,7 +2258,8 @@ class Nikola(object):
context (optional) a dict used as a basis for the template context. The lang parameter will always be added.
context_deps_remove (optional) is a list of keys to remove from the context after using it as an uptodate dependency. This should name all keys containing non-trivial Python objects; they can be replaced by adding JSON-style dicts in post_deps_dict.
post_deps_dict (optional) is a dict merged into the copy of context which is used as an uptodate dependency.
- url_type (optional) allows to override the ``URL_TYPE`` configuration
+ url_type (optional) allows to override the ``URL_TYPE`` configuration.
+ is_fragment (optional) allows to write a HTML fragment instead of a HTML document.
"""
utils.LocaleBorg().set_locale(lang)
@@ -2026,23 +2277,27 @@ class Nikola(object):
deps_dict['OUTPUT_FOLDER'] = self.config['OUTPUT_FOLDER']
deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS']
deps_dict['global'] = self.GLOBAL_CONTEXT
+ deps_dict['all_page_deps'] = self.ALL_PAGE_DEPS
if post_deps_dict:
deps_dict.update(post_deps_dict)
for k, v in self.GLOBAL_CONTEXT['template_hooks'].items():
- deps_dict['||template_hooks|{0}||'.format(k)] = v._items
+ deps_dict['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
deps_dict[k] = deps_dict['global'][k](lang)
+ for k in self._ALL_PAGE_DEPS_TRANSLATABLE:
+ deps_dict[k] = deps_dict['all_page_deps'][k](lang)
deps_dict['navigation_links'] = deps_dict['global']['navigation_links'](lang)
+ deps_dict['navigation_alt_links'] = deps_dict['global']['navigation_alt_links'](lang)
task = {
'name': os.path.normpath(output_name),
'targets': [output_name],
'file_dep': file_deps,
'actions': [(self.render_template, [template_name, output_name,
- context, url_type])],
+ context, url_type, is_fragment])],
'clean': True,
'uptodate': [config_changed(deps_dict, 'nikola.nikola.Nikola.generic_renderer')] + ([] if uptodate_deps is None else uptodate_deps)
}
@@ -2051,19 +2306,30 @@ class Nikola(object):
def generic_page_renderer(self, lang, post, filters, context=None):
"""Render post fragments to final HTML pages."""
- extension = self.get_compiler(post.source_path).extension()
+ extension = post.compiler.extension()
output_name = os.path.join(self.config['OUTPUT_FOLDER'],
post.destination_path(lang, extension))
deps = post.deps(lang)
uptodate_deps = post.deps_uptodate(lang)
deps.extend(utils.get_asset_path(x, self.THEMES) for x in ('bundles', 'parent', 'engine'))
+ _theme_ini = utils.get_asset_path(self.config['THEME'] + '.theme', self.THEMES)
+ if _theme_ini:
+ deps.append(_theme_ini)
context = copy(context) if context else {}
context['post'] = post
context['title'] = post.title(lang)
context['description'] = post.description(lang)
context['permalink'] = post.permalink(lang)
+ if 'crumbs' not in context:
+ crumb_path = post.permalink(lang).lstrip('/')
+ if crumb_path.endswith(self.config['INDEX_FILE']):
+ crumb_path = crumb_path[:-len(self.config['INDEX_FILE'])]
+ if crumb_path.endswith('/'):
+ context['crumbs'] = utils.get_crumbs(crumb_path.rstrip('/'), is_file=False)
+ else:
+ context['crumbs'] = utils.get_crumbs(crumb_path, is_file=True)
if 'pagekind' not in context:
context['pagekind'] = ['generic_page']
if post.use_in_feeds:
@@ -2080,12 +2346,21 @@ class Nikola(object):
if post:
deps_dict['post_translations'] = post.translated_to
+ signal('render_post').send({
+ 'site': self,
+ 'post': post,
+ 'lang': lang,
+ 'context': context,
+ 'deps_dict': deps_dict,
+ })
+
yield self.generic_renderer(lang, output_name, post.template_name, filters,
file_deps=deps,
uptodate_deps=uptodate_deps,
context=context,
context_deps_remove=['post'],
- post_deps_dict=deps_dict)
+ post_deps_dict=deps_dict,
+ url_type=post.url_type)
def generic_post_list_renderer(self, lang, posts, output_name, template_name, filters, extra_context):
"""Render pages with lists of posts."""
@@ -2103,6 +2378,8 @@ class Nikola(object):
context["nextlink"] = None
if extra_context:
context.update(extra_context)
+ if 'has_other_languages' not in context:
+ context['has_other_languages'] = False
post_deps_dict = {}
post_deps_dict["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in posts]
@@ -2132,30 +2409,30 @@ class Nikola(object):
for post in posts:
deps += post.deps(lang)
uptodate_deps += post.deps_uptodate(lang)
+
context = {}
+ blog_title = self.config['BLOG_TITLE'](lang)
context["posts"] = posts
- context["title"] = self.config['BLOG_TITLE'](lang)
+ context["title"] = blog_title
context["description"] = self.config['BLOG_DESCRIPTION'](lang)
context["lang"] = lang
- context["prevlink"] = None
- context["nextlink"] = None
- context["is_feed_stale"] = None
context.update(extra_context)
+
+ context["title"] = "{0} ({1})".format(blog_title, context["title"]) if blog_title != context["title"] else blog_title
+
deps_context = copy(context)
deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in
posts]
deps_context["global"] = self.GLOBAL_CONTEXT
+ deps_context["all_page_deps"] = self.ALL_PAGE_DEPS
for k in self._GLOBAL_CONTEXT_TRANSLATABLE:
deps_context[k] = deps_context['global'][k](lang)
+ for k in self._ALL_PAGE_DEPS_TRANSLATABLE:
+ deps_context[k] = deps_context['all_page_deps'][k](lang)
- deps_context['navigation_links'] = deps_context['global']['navigation_links'](lang)
-
- nslist = {}
- if context["is_feed_stale"] or "feedpagenum" in context and (not context["feedpagenum"] == context["feedpagecount"] - 1 and not context["feedpagenum"] == 0):
- nslist["fh"] = "http://purl.org/syndication/history/1.0"
feed_xsl_link = self.abs_link("/assets/xml/atom.xsl")
- feed_root = lxml.etree.Element("feed", nsmap=nslist)
+ feed_root = lxml.etree.Element("feed")
feed_root.addprevious(lxml.etree.ProcessingInstruction(
"xml-stylesheet",
'href="' + utils.encodelink(feed_xsl_link) + '" type="text/xsl media="all"'))
@@ -2172,25 +2449,6 @@ class Nikola(object):
feed_author_name.text = self.config["BLOG_AUTHOR"](lang)
feed_root.append(atom_link("self", "application/atom+xml",
self.abs_link(context["feedlink"])))
- # Older is "next" and newer is "previous" in paginated feeds (opposite of archived)
- if "nextfeedlink" in context:
- feed_root.append(atom_link("next", "application/atom+xml",
- self.abs_link(context["nextfeedlink"])))
- if "prevfeedlink" in context:
- feed_root.append(atom_link("previous", "application/atom+xml",
- self.abs_link(context["prevfeedlink"])))
- if context["is_feed_stale"] or "feedpagenum" in context and not context["feedpagenum"] == 0:
- feed_root.append(atom_link("current", "application/atom+xml",
- self.abs_link(context["currentfeedlink"])))
- # Older is "prev-archive" and newer is "next-archive" in archived feeds (opposite of paginated)
- if "prevfeedlink" in context and (context["is_feed_stale"] or "feedpagenum" in context and not context["feedpagenum"] == context["feedpagecount"] - 1):
- feed_root.append(atom_link("next-archive", "application/atom+xml",
- self.abs_link(context["prevfeedlink"])))
- if "nextfeedlink" in context:
- feed_root.append(atom_link("prev-archive", "application/atom+xml",
- self.abs_link(context["nextfeedlink"])))
- if context["is_feed_stale"] or "feedpagenum" and not context["feedpagenum"] == context["feedpagecount"] - 1:
- lxml.etree.SubElement(feed_root, "{http://purl.org/syndication/history/1.0}archive")
feed_root.append(atom_link("alternate", "text/html",
self.abs_link(context["permalink"])))
feed_generator = lxml.etree.SubElement(feed_root, "generator")
@@ -2205,7 +2463,7 @@ class Nikola(object):
def atom_post_text(post, text):
if not self.config["FEED_PLAIN"]:
- if self.config["FEED_PREVIEWIMAGE"] and 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in text:
+ if 'previewimage' in post.meta[lang] and post.meta[lang]['previewimage'] not in text:
text = "<figure><img src=\"{}\"></figure> {}".format(post.meta[lang]['previewimage'], text)
# FIXME: this is duplicated with code in Post.text() and generic_rss_renderer
@@ -2223,7 +2481,7 @@ class Nikola(object):
if str(e) == "Document is empty":
text = ""
else: # let other errors raise
- raise(e)
+ raise
return text.strip()
for post in posts:
@@ -2251,8 +2509,8 @@ class Nikola(object):
entry_author_name = lxml.etree.SubElement(entry_author, "name")
entry_author_name.text = post.author(lang)
entry_root.append(atom_link("alternate", "text/html",
- post.permalink(lang, absolute=True,
- query=feed_append_query)))
+ post.permalink(lang, absolute=True,
+ query=feed_append_query)))
entry_summary = lxml.etree.SubElement(entry_root, "summary")
if not self.config["FEED_PLAIN"]:
entry_summary.set("type", "html")
@@ -2275,11 +2533,11 @@ class Nikola(object):
utils.makedirs(dst_dir)
with io.open(output_path, "w+", encoding="utf-8") as atom_file:
data = lxml.etree.tostring(feed_root.getroottree(), encoding="UTF-8", pretty_print=True, xml_declaration=True)
- if isinstance(data, utils.bytes_str):
+ if isinstance(data, bytes):
data = data.decode('utf-8')
atom_file.write(data)
- def generic_index_renderer(self, lang, posts, indexes_title, template_name, context_source, kw, basename, page_link, page_path, additional_dependencies=[]):
+ def generic_index_renderer(self, lang, posts, indexes_title, template_name, context_source, kw, basename, page_link, page_path, additional_dependencies=None):
"""Create an index page.
lang: The language
@@ -2304,6 +2562,9 @@ class Nikola(object):
as the ones for page_link.
additional_dependencies: a list of dependencies which will be added
to task['uptodate']
+
+ Note: if context['featured'] is present, it must be a list of posts,
+ whose dependencies will be taken added to task['uptodate'].
"""
# Update kw
kw = kw.copy()
@@ -2313,11 +2574,11 @@ class Nikola(object):
kw["indexes_pages"] = self.config['INDEXES_PAGES'](lang)
kw["indexes_pages_main"] = self.config['INDEXES_PAGES_MAIN']
kw["indexes_static"] = self.config['INDEXES_STATIC']
- kw['indexes_prety_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"]
- kw['demote_headers'] = self.config['DEMOTE_HEADERS']
- kw['generate_atom'] = self.config["GENERATE_ATOM"]
- kw['feed_link_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"]
- kw['currentfeed'] = None
+ kw['indexes_pretty_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"]
+ kw['show_index_page_navigation'] = self.config['SHOW_INDEX_PAGE_NAVIGATION']
+
+ if additional_dependencies is None:
+ additional_dependencies = []
# Split in smaller lists
lists = []
@@ -2331,12 +2592,29 @@ class Nikola(object):
while posts:
lists.append(posts[:kw["index_display_post_count"]])
posts = posts[kw["index_display_post_count"]:]
+ if not lists:
+ lists.append([])
num_pages = len(lists)
+ displayed_page_numbers = [utils.get_displayed_page_number(i, num_pages, self) for i in range(num_pages)]
+ page_links = [page_link(i, page_number, num_pages, False) for i, page_number in enumerate(displayed_page_numbers)]
+ if kw['show_index_page_navigation']:
+ # Since the list displayed_page_numbers is not necessarily
+ # sorted -- in case INDEXES_STATIC is True, it is of the
+ # form [num_pages, 1, 2, ..., num_pages - 1] -- we order it
+ # via a map. This allows to not replicate the logic of
+ # utils.get_displayed_page_number() here.
+ if not kw["indexes_pages_main"] and not kw["indexes_static"]:
+ temp_map = {page_number: link for page_number, link in zip(displayed_page_numbers, page_links)}
+ else:
+ temp_map = {page_number - 1: link for page_number, link in zip(displayed_page_numbers, page_links)}
+ page_links_context = [temp_map[i] for i in range(num_pages)]
for i, post_list in enumerate(lists):
context = context_source.copy()
if 'pagekind' not in context:
context['pagekind'] = ['index']
- ipages_i = utils.get_displayed_page_number(i, num_pages, self)
+ if 'has_other_languages' not in context:
+ context['has_other_languages'] = False
+ ipages_i = displayed_page_numbers[i]
if kw["indexes_pages"]:
indexes_pages = kw["indexes_pages"] % ipages_i
else:
@@ -2372,20 +2650,29 @@ class Nikola(object):
if i < num_pages - 1:
nextlink = i + 1
if prevlink is not None:
- context["prevlink"] = page_link(prevlink,
- utils.get_displayed_page_number(prevlink, num_pages, self),
- num_pages, False)
- context["prevfeedlink"] = page_link(prevlink,
- utils.get_displayed_page_number(prevlink, num_pages, self),
+ context["prevlink"] = page_links[prevlink]
+ context["prevfeedlink"] = page_link(prevlink, displayed_page_numbers[prevlink],
num_pages, False, extension=".atom")
if nextlink is not None:
- context["nextlink"] = page_link(nextlink,
- utils.get_displayed_page_number(nextlink, num_pages, self),
- num_pages, False)
- context["nextfeedlink"] = page_link(nextlink,
- utils.get_displayed_page_number(nextlink, num_pages, self),
+ context["nextlink"] = page_links[nextlink]
+ context["nextfeedlink"] = page_link(nextlink, displayed_page_numbers[nextlink],
num_pages, False, extension=".atom")
- context["permalink"] = page_link(i, ipages_i, num_pages, False)
+ context['show_index_page_navigation'] = kw['show_index_page_navigation']
+ if kw['show_index_page_navigation']:
+ context['page_links'] = page_links_context
+ if not kw["indexes_pages_main"] and not kw["indexes_static"]:
+ context['current_page'] = ipages_i
+ else:
+ context['current_page'] = ipages_i - 1
+ context['prev_next_links_reversed'] = kw['indexes_static']
+ context["permalink"] = page_links[i]
+ context["is_frontmost_index"] = i == 0
+
+ # Add dependencies to featured posts
+ if 'featured' in context:
+ for post in context['featured']:
+ additional_dependencies += post.deps_uptodate(lang)
+
output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False))
task = self.generic_post_list_renderer(
lang,
@@ -2399,38 +2686,10 @@ class Nikola(object):
task['basename'] = basename
yield task
- if kw['generate_atom']:
- atom_output_name = os.path.join(kw['output_folder'], page_path(i, ipages_i, num_pages, False, extension=".atom"))
- context["feedlink"] = page_link(i, ipages_i, num_pages, False, extension=".atom")
- if not kw["currentfeed"]:
- kw["currentfeed"] = context["feedlink"]
- context["currentfeedlink"] = kw["currentfeed"]
- context["feedpagenum"] = i
- context["feedpagecount"] = num_pages
- kw['feed_teasers'] = self.config['FEED_TEASERS']
- kw['feed_plain'] = self.config['FEED_PLAIN']
- kw['feed_previewimage'] = self.config['FEED_PREVIEWIMAGE']
- atom_task = {
- "basename": basename,
- "name": atom_output_name,
- "file_dep": sorted([_.base_path for _ in post_list]),
- "task_dep": ['render_posts'],
- "targets": [atom_output_name],
- "actions": [(self.atom_feed_renderer,
- (lang,
- post_list,
- atom_output_name,
- kw['filters'],
- context,))],
- "clean": True,
- "uptodate": [utils.config_changed(kw, 'nikola.nikola.Nikola.atom_feed_renderer')] + additional_dependencies
- }
- yield utils.apply_filters(atom_task, kw['filters'])
-
- if kw["indexes_pages_main"] and kw['indexes_prety_page_url'](lang):
+ if kw["indexes_pages_main"] and kw['indexes_pretty_page_url'](lang):
# create redirection
- output_name = os.path.join(kw['output_folder'], page_path(0, utils.get_displayed_page_number(0, num_pages, self), num_pages, True))
- link = page_link(0, utils.get_displayed_page_number(0, num_pages, self), num_pages, False)
+ output_name = os.path.join(kw['output_folder'], page_path(0, displayed_page_numbers[0], num_pages, True))
+ link = page_links[0]
yield utils.apply_filters({
'basename': basename,
'name': output_name,
@@ -2440,159 +2699,58 @@ class Nikola(object):
'uptodate': [utils.config_changed(kw, 'nikola.nikola.Nikola.generic_index_renderer')],
}, kw["filters"])
- def __repr__(self):
- """Representation of a Nikola site."""
- return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE'](self.config['DEFAULT_LANG']))
-
+ def generic_atom_renderer(self, lang, posts, context_source, kw, basename, classification, kind, additional_dependencies=None):
+ """Create an Atom feed.
-def sanitized_locales(locale_fallback, locale_default, locales, translations):
- """Sanitize all locales availble in Nikola.
-
- There will be one locale for each language in translations.
-
- Locales for languages not in translations are ignored.
+ lang: The language
+ posts: A list of posts
+ context_source: This will be copied and extended and used as every
+ page's context
+ kw: An extended version will be used for uptodate dependencies
+ basename: Basename for task
+ classification: name of current classification (used to generate links)
+ kind: classification kind (used to generate links)
+ additional_dependencies: a list of dependencies which will be added
+ to task['uptodate']
+ """
+ # Update kw
+ kw = kw.copy()
+ kw["feed_length"] = self.config['FEED_LENGTH']
+ kw['generate_atom'] = self.config["GENERATE_ATOM"]
+ kw['feed_links_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"]
+ kw['feed_teasers'] = self.config['FEED_TEASERS']
+ kw['feed_plain'] = self.config['FEED_PLAIN']
- An explicit locale for a language can be specified in locales[language].
+ if additional_dependencies is None:
+ additional_dependencies = []
- Locales at the input must be in the string style (like 'en', 'en.utf8'), and
- the string can be unicode or bytes; at the output will be of type str, as
- required by locale.setlocale.
+ post_list = posts[:kw["feed_length"]]
+ feedlink = self.link(kind + "_atom", classification, lang)
+ feedpath = self.path(kind + "_atom", classification, lang)
- Explicit but invalid locales are replaced with the sanitized locale_fallback
+ context = context_source.copy()
+ if 'has_other_languages' not in context:
+ context['has_other_languages'] = False
- Languages with no explicit locale are set to
- the sanitized locale_default if it was explicitly set
- sanitized guesses compatible with v 6.0.4 if locale_default was None
+ output_name = os.path.join(kw['output_folder'], feedpath)
+ context["feedlink"] = feedlink
+ task = {
+ "basename": basename,
+ "name": output_name,
+ "file_dep": sorted([_.base_path for _ in post_list]),
+ "task_dep": ['render_posts'],
+ "targets": [output_name],
+ "actions": [(self.atom_feed_renderer,
+ (lang,
+ post_list,
+ output_name,
+ kw['filters'],
+ context,))],
+ "clean": True,
+ "uptodate": [utils.config_changed(kw, 'nikola.nikola.Nikola.atom_feed_renderer')] + additional_dependencies
+ }
+ yield utils.apply_filters(task, kw['filters'])
- NOTE: never use locale.getlocale() , it can return values that
- locale.setlocale will not accept in Windows XP, 7 and pythons 2.6, 2.7, 3.3
- Examples: "Spanish", "French" can't do the full circle set / get / set
- """
- if sys.platform != 'win32':
- workaround_empty_LC_ALL_posix()
-
- # locales for languages not in translations are ignored
- extras = set(locales) - set(translations)
- if extras:
- msg = 'Unexpected languages in LOCALES, ignoring them: {0}'
- utils.LOGGER.warn(msg.format(', '.join(extras)))
- for lang in extras:
- del locales[lang]
-
- # py2x: get/setlocale related functions require the locale string as a str
- # so convert
- locale_fallback = str(locale_fallback) if locale_fallback else None
- locale_default = str(locale_default) if locale_default else None
- for lang in locales:
- locales[lang] = str(locales[lang])
-
- locale_fallback = valid_locale_fallback(locale_fallback)
-
- # explicit but invalid locales are replaced with the sanitized locale_fallback
- for lang in locales:
- if not is_valid_locale(locales[lang]):
- msg = 'Locale {0} for language {1} not accepted by python locale.'
- utils.LOGGER.warn(msg.format(locales[lang], lang))
- locales[lang] = locale_fallback
-
- # languages with no explicit locale
- missing = set(translations) - set(locales)
- if locale_default:
- # are set to the sanitized locale_default if it was explicitly set
- if not is_valid_locale(locale_default):
- msg = 'LOCALE_DEFAULT {0} could not be set, using {1}'
- utils.LOGGER.warn(msg.format(locale_default, locale_fallback))
- locale_default = locale_fallback
- for lang in missing:
- locales[lang] = locale_default
- else:
- # are set to sanitized guesses compatible with v 6.0.4 in Linux-Mac (was broken in Windows)
- if sys.platform == 'win32':
- guess_locale_fom_lang = guess_locale_from_lang_windows
- else:
- guess_locale_fom_lang = guess_locale_from_lang_posix
- for lang in missing:
- locale_n = guess_locale_fom_lang(lang)
- if not locale_n:
- locale_n = locale_fallback
- msg = "Could not guess locale for language {0}, using locale {1}"
- utils.LOGGER.warn(msg.format(lang, locale_n))
- utils.LOGGER.warn("Please fix your OS locale configuration or use the LOCALES option in conf.py to specify your preferred locale.")
- if sys.platform != 'win32':
- utils.LOGGER.warn("Make sure to use an UTF-8 locale to ensure Unicode support.")
- locales[lang] = locale_n
-
- return locale_fallback, locale_default, locales
-
-
-def is_valid_locale(locale_n):
- """Check if locale (type str) is valid."""
- try:
- locale.setlocale(locale.LC_ALL, locale_n)
- return True
- except locale.Error:
- return False
-
-
-def valid_locale_fallback(desired_locale=None):
- """Provide a default fallback_locale, a string that locale.setlocale will accept.
-
- If desired_locale is provided must be of type str for py2x compatibility
- """
- # Whenever fallbacks change, adjust test TestHarcodedFallbacksWork
- candidates_windows = [str('English'), str('C')]
- candidates_posix = [str('en_US.UTF-8'), str('C')]
- candidates = candidates_windows if sys.platform == 'win32' else candidates_posix
- if desired_locale:
- candidates = list(candidates)
- candidates.insert(0, desired_locale)
- found_valid = False
- for locale_n in candidates:
- found_valid = is_valid_locale(locale_n)
- if found_valid:
- break
- if not found_valid:
- msg = 'Could not find a valid fallback locale, tried: {0}'
- utils.LOGGER.warn(msg.format(candidates))
- elif desired_locale and (desired_locale != locale_n):
- msg = 'Desired fallback locale {0} could not be set, using: {1}'
- utils.LOGGER.warn(msg.format(desired_locale, locale_n))
- return locale_n
-
-
-def guess_locale_from_lang_windows(lang):
- """Guess a locale, basing on Windows language."""
- locale_n = str(LEGAL_VALUES['_WINDOWS_LOCALE_GUESSES'].get(lang, None))
- if not is_valid_locale(locale_n):
- locale_n = None
- return locale_n
-
-
-def guess_locale_from_lang_posix(lang):
- """Guess a locale, basing on POSIX system language."""
- # compatibility v6.0.4
- if is_valid_locale(str(lang)):
- locale_n = str(lang)
- else:
- # this works in Travis when locale support set by Travis suggestion
- locale_n = str((locale.normalize(lang).split('.')[0]) + '.UTF-8')
- if not is_valid_locale(locale_n):
- # http://thread.gmane.org/gmane.comp.web.nikola/337/focus=343
- locale_n = str((locale.normalize(lang).split('.')[0]))
- if not is_valid_locale(locale_n):
- locale_n = None
- return locale_n
-
-
-def workaround_empty_LC_ALL_posix():
- # clunky hack: we have seen some posix locales with all or most of LC_*
- # defined to the same value, but with LC_ALL empty.
- # Manually doing what we do here seems to work for nikola in that case.
- # It is unknown if it will work when the LC_* aren't homogeneous
- try:
- lc_time = os.environ.get('LC_TIME', None)
- lc_all = os.environ.get('LC_ALL', None)
- if lc_time and not lc_all:
- os.environ['LC_ALL'] = lc_time
- except Exception:
- pass
+ def __repr__(self):
+ """Representation of a Nikola site."""
+ return '<Nikola Site: {0!r}>'.format(self.config['BLOG_TITLE'](self.config['DEFAULT_LANG']))
diff --git a/nikola/packages/README.md b/nikola/packages/README.md
index ad94b00..7265069 100644
--- a/nikola/packages/README.md
+++ b/nikola/packages/README.md
@@ -2,6 +2,9 @@ We ship some third-party things with Nikola. They live here, along with their l
Packages:
- * tzlocal by Lennart Regebro, CC0 license (modified)
+ * tzlocal by Lennart Regebro, CC0 license (modified to remove pytz dependency)
* datecond by Chris Warrick (Nikola contributor), 3-clause BSD license
(modified)
+ * pygments_better_html by Chris Warrick (Nikola contributor), 3-clause BSD license,
+ portions copyright the Pygments team (2-clause BSD license).
+
diff --git a/nikola/packages/datecond/LICENSE b/nikola/packages/datecond/LICENSE
index 5e8b6d6..d9980a8 100644
--- a/nikola/packages/datecond/LICENSE
+++ b/nikola/packages/datecond/LICENSE
@@ -1,4 +1,4 @@
-Copyright © 2016, Chris Warrick.
+Copyright © 2016-2020, Chris Warrick.
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/nikola/packages/datecond/__init__.py b/nikola/packages/datecond/__init__.py
index b409057..92e7908 100644
--- a/nikola/packages/datecond/__init__.py
+++ b/nikola/packages/datecond/__init__.py
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
-# Date Conditionals (datecond)
-# Version 0.1.2
-# Copyright © 2015-2016, Chris Warrick.
+# Date Conditionals v0.1.7
+# Copyright © 2015-2020, Chris Warrick.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@@ -35,7 +34,7 @@
"""Date range parser."""
-from __future__ import print_function, unicode_literals
+import datetime
import dateutil.parser
import re
import operator
@@ -54,7 +53,7 @@ OPERATORS = {
}
-def date_in_range(date_range, date, debug=True):
+def date_in_range(date_range, date, debug=False, now=None):
"""Check if date is in the range specified.
Format:
@@ -63,7 +62,10 @@ def date_in_range(date_range, date, debug=True):
* attribute: year, month, day, hour, month, second, weekday, isoweekday
or empty for full datetime
* comparison_operator: == != <= >= < >
- * value: integer or dateutil-compatible date input
+ * value: integer, 'now', 'today', or dateutil-compatible date input
+
+ The optional `now` parameter can be used to provide a specific `now`/`today` value
+ (if none is provided, datetime.datetime.now()/datetime.date.today() is used).
"""
out = True
@@ -73,6 +75,15 @@ def date_in_range(date_range, date, debug=True):
if attribute in ('weekday', 'isoweekday'):
left = getattr(date, attribute)()
right = int(value)
+ elif value == 'now':
+ left = date
+ right = now or datetime.datetime.now()
+ elif value == 'today':
+ left = date.date() if isinstance(date, datetime.datetime) else date
+ if now:
+ right = now.date() if isinstance(now, datetime.datetime) else now
+ else:
+ right = datetime.date.today()
elif attribute:
left = getattr(date, attribute)
right = int(value)
diff --git a/nikola/packages/pygments_better_html/LICENSE b/nikola/packages/pygments_better_html/LICENSE
new file mode 100644
index 0000000..196413e
--- /dev/null
+++ b/nikola/packages/pygments_better_html/LICENSE
@@ -0,0 +1,30 @@
+Copyright © 2020, Chris Warrick.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the author of this software nor the names of
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ consent.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/nikola/packages/pygments_better_html/LICENSE.pygments b/nikola/packages/pygments_better_html/LICENSE.pygments
new file mode 100644
index 0000000..13d1c74
--- /dev/null
+++ b/nikola/packages/pygments_better_html/LICENSE.pygments
@@ -0,0 +1,25 @@
+Copyright (c) 2006-2019 by the respective authors (see AUTHORS file).
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/nikola/packages/pygments_better_html/__init__.py b/nikola/packages/pygments_better_html/__init__.py
new file mode 100644
index 0000000..ed6e004
--- /dev/null
+++ b/nikola/packages/pygments_better_html/__init__.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+"""Better HTML formatter for Pygments.
+
+Copyright © 2020, Chris Warrick.
+License: 3-clause BSD.
+Portions copyright © 2006-2019, the Pygments authors. (2-clause BSD).
+"""
+
+__all__ = ["BetterHtmlFormatter"]
+__version__ = "0.1.4"
+
+import enum
+import re
+import warnings
+
+from pygments.formatters.html import HtmlFormatter
+
+MANY_SPACES = re.compile("( +)")
+
+
+def _sp_to_nbsp(m):
+ return "&nbsp;" * (m.end() - m.start())
+
+
+class BetterLinenos(enum.Enum):
+ TABLE = "table"
+ OL = "ol"
+
+
+class BetterHtmlFormatter(HtmlFormatter):
+ r"""
+ Format tokens as HTML 4 ``<span>`` tags, with alternate formatting styles.
+
+ * ``linenos = 'table'`` renders each line of code in a separate table row
+ * ``linenos = 'ol'`` renders each line in a <li> element (inside <ol>)
+
+ Both options allow word wrap and don't include line numbers when copying.
+ """
+
+ name = "HTML"
+ aliases = ["html"]
+ filenames = ["*.html", "*.htm"]
+
+ def __init__(self, **options):
+ """Initialize the formatter."""
+ super().__init__(**options)
+ self.linenos_name = self.options.get("linenos", "table")
+ if self.linenos_name is False:
+ self.linenos_val = False
+ self.linenos = 0
+ elif self.linenos_name is True:
+ self.linenos_name = "table"
+ if self.linenos_name is not False:
+ self.linenos_val = BetterLinenos(self.linenos_name)
+ self.linenos = 2 if self.linenos_val == BetterLinenos.OL else 1
+
+ def get_style_defs(self, arg=None, wrapper_classes=None):
+ """Generate CSS style definitions.
+
+ Return CSS style definitions for the classes produced by the current
+ highlighting style. ``arg`` can be a string or list of selectors to
+ insert before the token type classes. ``wrapper_classes`` are a list of
+ classes for the wrappers, defaults to the ``cssclass`` option.
+ """
+ base = super().get_style_defs(arg)
+ new_styles = (
+ ("{0} table, {0} tr, {0} td", "border-spacing: 0; border-collapse: separate; padding: 0"),
+ ("{0} pre", "white-space: pre-wrap; line-height: normal"),
+ (
+ "{0}table td.linenos",
+ "vertical-align: top; padding-left: 10px; padding-right: 10px; user-select: none; -webkit-user-select: none",
+ ),
+ # Hack for Safari (user-select does not affect copy-paste)
+ ("{0}table td.linenos code:before", "content: attr(data-line-number)"),
+ ("{0}table td.code", "overflow-wrap: normal; border-collapse: collapse"),
+ (
+ "{0}table td.code code",
+ "overflow: unset; border: none; padding: 0; margin: 0; white-space: pre-wrap; line-height: unset; background: none",
+ ),
+ ("{0} .lineno.nonumber", "list-style: none"),
+ )
+ new_styles_code = []
+ if wrapper_classes is None:
+ wrapper_classes = ["." + self.cssclass]
+ for cls, rule in new_styles:
+ new_styles_code.append(", ".join(cls.format(c) for c in wrapper_classes) + " { " + rule + " }")
+ return base + "\n" + "\n".join(new_styles_code)
+
+ def _wrap_tablelinenos(self, inner):
+ lncount = 0
+ codelines = []
+ for t, line in inner:
+ if t:
+ lncount += 1
+ codelines.append(line)
+
+ fl = self.linenostart
+ mw = len(str(lncount + fl - 1))
+ sp = self.linenospecial
+ st = self.linenostep
+ la = self.lineanchors
+ aln = self.anchorlinenos
+ nocls = self.noclasses
+ if sp:
+ lines = []
+
+ for i in range(fl, fl + lncount):
+ line_before = ""
+ line_after = ""
+ if i % st == 0:
+ if i % sp == 0:
+ if aln:
+ line_before = '<a href="#%s-%d" class="special">' % (la, i)
+ line_after = "</a>"
+ else:
+ line_before = '<span class="special">'
+ line_after = "</span>"
+ elif aln:
+ line_before = '<a href="#%s-%d">' % (la, i)
+ line_after = "</a>"
+ lines.append((line_before, "%*d" % (mw, i), line_after))
+ else:
+ lines.append(("", "", ""))
+ else:
+ lines = []
+ for i in range(fl, fl + lncount):
+ line_before = ""
+ line_after = ""
+ if i % st == 0:
+ if aln:
+ line_before = '<a href="#%s-%d">' % (la, i)
+ line_after = "</a>"
+ lines.append((line_before, "%*d" % (mw, i), line_after))
+ else:
+ lines.append(("", "", ""))
+
+ yield 0, '<div class="%s"><table class="%stable">' % (
+ self.cssclass,
+ self.cssclass,
+ )
+ for lndata, cl in zip(lines, codelines):
+ ln_b, ln, ln_a = lndata
+ cl = MANY_SPACES.sub(_sp_to_nbsp, cl)
+ if nocls:
+ yield 0, (
+ '<tr><td class="linenos linenodiv" style="background-color: #f0f0f0; padding-right: 10px">' + ln_b +
+ '<code data-line-number="' + ln + '"></code>' + ln_a + '</td><td class="code"><code>' + cl + "</code></td></tr>"
+ )
+ else:
+ yield 0, (
+ '<tr><td class="linenos linenodiv">' + ln_b + '<code data-line-number="' + ln +
+ '"></code>' + ln_a + '</td><td class="code"><code>' + cl + "</code></td></tr>"
+ )
+ yield 0, "</table></div>"
+
+ def _wrap_inlinelinenos(self, inner):
+ # Override with new method
+ return self._wrap_ollineos(self, inner)
+
+ def _wrap_ollinenos(self, inner):
+ lines = inner
+ sp = self.linenospecial
+ st = self.linenostep or 1
+ num = self.linenostart
+
+ if self.anchorlinenos:
+ warnings.warn("anchorlinenos is not supported for linenos='ol'.")
+
+ yield 0, "<ol>"
+ if self.noclasses:
+ if sp:
+ for t, line in lines:
+ if num % sp == 0:
+ style = "background-color: #ffffc0; padding: 0 5px 0 5px"
+ else:
+ style = "background-color: #f0f0f0; padding: 0 5px 0 5px"
+ if num % st != 0:
+ style += "; list-style: none"
+ yield 1, '<li style="%s" value="%s">' % (style, num,) + line + "</li>"
+ num += 1
+ else:
+ for t, line in lines:
+ yield 1, (
+ '<li style="background-color: #f0f0f0; padding: 0 5px 0 5px%s" value="%s">'
+ % (("; list-style: none" if num % st != 0 else ""), num) + line + "</li>"
+ )
+ num += 1
+ elif sp:
+ for t, line in lines:
+ yield 1, '<li class="lineno%s%s" value="%s">' % (
+ " special" if num % sp == 0 else "",
+ " nonumber" if num % st != 0 else "",
+ num,
+ ) + line + "</li>"
+ num += 1
+ else:
+ for t, line in lines:
+ yield 1, '<li class="lineno%s" value="%s">' % (
+ "" if num % st != 0 else " nonumber",
+ num,
+ ) + line + "</li>"
+ num += 1
+
+ yield 0, "</ol>"
+
+ def format_unencoded(self, tokensource, outfile):
+ """Format code and write to outfile.
+
+ The formatting process uses several nested generators; which of
+ them are used is determined by the user's options.
+
+ Each generator should take at least one argument, ``inner``,
+ and wrap the pieces of text generated by this.
+
+ Always yield 2-tuples: (code, text). If "code" is 1, the text
+ is part of the original tokensource being highlighted, if it's
+ 0, the text is some piece of wrapping. This makes it possible to
+ use several different wrappers that process the original source
+ linewise, e.g. line number generators.
+ """
+ if self.linenos_val is False:
+ return super().format_unencoded(tokensource, outfile)
+ source = self._format_lines(tokensource)
+ if self.hl_lines:
+ source = self._highlight_lines(source)
+ if not self.nowrap:
+ if self.linenos_val == BetterLinenos.OL:
+ source = self._wrap_ollinenos(source)
+ if self.lineanchors:
+ source = self._wrap_lineanchors(source)
+ if self.linespans:
+ source = self._wrap_linespans(source)
+ if self.linenos_val == BetterLinenos.TABLE:
+ source = self._wrap_tablelinenos(source)
+ if self.linenos_val == BetterLinenos.OL:
+ source = self.wrap(source, outfile)
+ if self.full:
+ source = self._wrap_full(source, outfile)
+
+ for t, piece in source:
+ outfile.write(piece)
diff --git a/nikola/packages/tzlocal/__init__.py b/nikola/packages/tzlocal/__init__.py
index 4a6b1d6..5b4947c 100644
--- a/nikola/packages/tzlocal/__init__.py
+++ b/nikola/packages/tzlocal/__init__.py
@@ -1,9 +1,8 @@
-"""tzlocal init."""
-
+"""Try to figure out what your local timezone is."""
import sys
-if sys.platform == 'win32':
+__version__ = "2.0.0-nikola"
+
+if sys.platform == "win32":
from .win32 import get_localzone, reload_localzone # NOQA
-elif 'darwin' in sys.platform:
- from .darwin import get_localzone, reload_localzone # NOQA
else:
from .unix import get_localzone, reload_localzone # NOQA
diff --git a/nikola/packages/tzlocal/darwin.py b/nikola/packages/tzlocal/darwin.py
deleted file mode 100644
index 0dbf1c1..0000000
--- a/nikola/packages/tzlocal/darwin.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""tzlocal for OS X."""
-
-import os
-import dateutil.tz
-import subprocess
-
-_cache_tz = None
-
-
-def _get_localzone():
- tzname = subprocess.check_output(["systemsetup", "-gettimezone"]).decode('utf-8')
- tzname = tzname.replace("Time Zone: ", "")
- # OS X 10.9+, this command is root-only
- if 'exiting!' in tzname:
- tzname = ''
-
- if not tzname:
- # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
- link = os.readlink("/etc/localtime")
- tzname = link.split('zoneinfo/')[-1]
- tzname = tzname.strip()
- try:
- # test the name
- assert tzname
- dateutil.tz.gettz(tzname)
- return tzname
- except:
- return None
-
-
-def get_localzone():
- """Get the computers configured local timezone, if any."""
- global _cache_tz
- if _cache_tz is None:
- _cache_tz = _get_localzone()
- return _cache_tz
-
-
-def reload_localzone():
- """Reload the cached localzone. You need to call this if the timezone has changed."""
- global _cache_tz
- _cache_tz = _get_localzone()
- return _cache_tz
diff --git a/nikola/packages/tzlocal/unix.py b/nikola/packages/tzlocal/unix.py
index 8f7fc84..086ab7c 100644
--- a/nikola/packages/tzlocal/unix.py
+++ b/nikola/packages/tzlocal/unix.py
@@ -1,115 +1,128 @@
-"""tzlocal for UNIX."""
-
-from __future__ import with_statement
+"""Unix support for tzlocal."""
import os
import re
+
import dateutil.tz
_cache_tz = None
-def _get_localzone():
- """Try to find the local timezone configuration.
+def _try_tz_from_env():
+ tzenv = os.environ.get("TZ")
+ if tzenv and tzenv[0] == ":":
+ tzenv = tzenv[1:]
+ try:
+ if tzenv:
+ dateutil.tz.gettz(tzenv)
+ return tzenv
+ except Exception:
+ pass
+
- This method prefers finding the timezone name and passing that to pytz,
- over passing in the localtime file, as in the later case the zoneinfo
- name is unknown.
+def _get_localzone(_root="/"):
+ """Try to find the local timezone configuration.
The parameter _root makes the function look for files like /etc/localtime
beneath the _root directory. This is primarily used by the tests.
In normal usage you call the function without parameters.
"""
- tz = os.environ.get('TZ')
- if tz and tz[0] == ':':
- tz = tz[1:]
- try:
- if tz:
- dateutil.tz.gettz(tz)
- return tz
- except:
- pass
+ tzenv = _try_tz_from_env()
+ if tzenv:
+ return tzenv
- try:
- # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
- link = os.readlink('/etc/localtime')
- tz = link.split('zoneinfo/')[-1]
+ # Are we under Termux on Android?
+ if os.path.exists("/system/bin/getprop"):
+ import subprocess
- if tz:
- dateutil.tz.gettz(tz)
- return tz
- except:
- return None
+ androidtz = (
+ subprocess.check_output(["getprop", "persist.sys.timezone"])
+ .strip()
+ .decode()
+ )
+ return androidtz
# Now look for distribution specific configuration files
# that contain the timezone name.
- tzpath = os.path.join('/etc/timezone')
- if os.path.exists(tzpath):
- with open(tzpath, 'rb') as tzfile:
- data = tzfile.read()
-
- # Issue #3 was that /etc/timezone was a zoneinfo file.
- # That's a misconfiguration, but we need to handle it gracefully:
- if data[:5] != 'TZif2':
+ for configfile in ("etc/timezone", "var/db/zoneinfo"):
+ tzpath = os.path.join(_root, configfile)
+ try:
+ with open(tzpath, "rb") as tzfile:
+ data = tzfile.read()
+
+ # Issue #3 was that /etc/timezone was a zoneinfo file.
+ # That's a misconfiguration, but we need to handle it gracefully:
+ if data[:5] == b"TZif2":
+ continue
+
etctz = data.strip().decode()
- # Get rid of host definitions and comments:
- if ' ' in etctz:
- etctz, dummy = etctz.split(' ', 1)
- if '#' in etctz:
- etctz, dummy = etctz.split('#', 1)
- tz = etctz.replace(' ', '_')
- try:
- if tz:
- dateutil.tz.gettz(tz)
- return tz
- except:
- pass
+ if not etctz:
+ # Empty file, skip
+ continue
+ for etctz in data.decode().splitlines():
+ # Get rid of host definitions and comments:
+ if " " in etctz:
+ etctz, dummy = etctz.split(" ", 1)
+ if "#" in etctz:
+ etctz, dummy = etctz.split("#", 1)
+ if not etctz:
+ continue
+ tz = etctz.replace(" ", "_")
+ return tz
+
+ except IOError:
+ # File doesn't exist or is a directory
+ continue
# CentOS has a ZONE setting in /etc/sysconfig/clock,
# OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
# Gentoo has a TIMEZONE setting in /etc/conf.d/clock
# We look through these files for a timezone:
- zone_re = re.compile('\s*ZONE\s*=\s*\"')
- timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"')
- end_re = re.compile('\"')
-
- for tzpath in ('/etc/sysconfig/clock', '/etc/conf.d/clock'):
- if not os.path.exists(tzpath):
+ zone_re = re.compile(r"\s*ZONE\s*=\s*\"")
+ timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"")
+ end_re = re.compile('"')
+
+ for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
+ tzpath = os.path.join(_root, filename)
+ try:
+ with open(tzpath, "rt") as tzfile:
+ data = tzfile.readlines()
+
+ for line in data:
+ # Look for the ZONE= setting.
+ match = zone_re.match(line)
+ if match is None:
+ # No ZONE= setting. Look for the TIMEZONE= setting.
+ match = timezone_re.match(line)
+ if match is not None:
+ # Some setting existed
+ line = line[match.end():]
+ etctz = line[: end_re.search(line).start()]
+
+ # We found a timezone
+ tz = etctz.replace(" ", "_")
+ return tz
+
+ except IOError:
+ # File doesn't exist or is a directory
continue
- with open(tzpath, 'rt') as tzfile:
- data = tzfile.readlines()
-
- for line in data:
- # Look for the ZONE= setting.
- match = zone_re.match(line)
- if match is None:
- # No ZONE= setting. Look for the TIMEZONE= setting.
- match = timezone_re.match(line)
- if match is not None:
- # Some setting existed
- line = line[match.end():]
- etctz = line[:end_re.search(line).start()]
-
- # We found a timezone
- tz = etctz.replace(' ', '_')
- try:
- if tz:
- dateutil.tz.gettz(tz)
- return tz
- except:
- pass
-
- # Nikola cannot use this thing below...
-
- # No explicit setting existed. Use localtime
- # for filename in ('etc/localtime', 'usr/local/etc/localtime'):
- # tzpath = os.path.join(_root, filename)
-
- # if not os.path.exists(tzpath):
- # continue
- # with open(tzpath, 'rb') as tzfile:
- # return pytz.tzfile.build_tzinfo('local', tzfile)
+ # systemd distributions use symlinks that include the zone name,
+ # see manpage of localtime(5) and timedatectl(1)
+ tzpath = os.path.join(_root, "etc/localtime")
+ if os.path.exists(tzpath) and os.path.islink(tzpath):
+ tzpath = os.path.realpath(tzpath)
+ start = tzpath.find("/") + 1
+ while start != 0:
+ tzpath = tzpath[start:]
+ try:
+ dateutil.tz.gettz(tzpath)
+ return tzpath
+ except Exception:
+ pass
+ start = tzpath.find("/") + 1
+
+ # Nothing found, return UTC
return None
@@ -118,6 +131,7 @@ def get_localzone():
global _cache_tz
if _cache_tz is None:
_cache_tz = _get_localzone()
+
return _cache_tz
diff --git a/nikola/packages/tzlocal/win32.py b/nikola/packages/tzlocal/win32.py
index cb19284..b8be8b4 100644
--- a/nikola/packages/tzlocal/win32.py
+++ b/nikola/packages/tzlocal/win32.py
@@ -1,12 +1,8 @@
-"""tzlocal for Windows."""
-
+"""Windows support for tzlocal."""
try:
import _winreg as winreg
except ImportError:
- try:
- import winreg
- except ImportError:
- pass # not windows
+ import winreg
from .windows_tz import win_tz
@@ -24,7 +20,7 @@ def valuestodict(key):
def get_localzone_name():
- """Get local time zone name."""
+ """Get local zone name."""
# Windows is special. It has unique time zone names (in several
# meanings of the word) available, but unfortunately, they can be
# translated to the language of the operating system, so we need to
@@ -36,18 +32,19 @@ def get_localzone_name():
localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
keyvalues = valuestodict(localtz)
localtz.Close()
- if 'TimeZoneKeyName' in keyvalues:
+
+ if "TimeZoneKeyName" in keyvalues:
# Windows 7 (and Vista?)
# For some reason this returns a string with loads of NUL bytes at
# least on some systems. I don't know if this is a bug somewhere, I
# just work around it.
- tzkeyname = keyvalues['TimeZoneKeyName'].split('\x00', 1)[0]
+ tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
else:
# Windows 2000 or XP
# This is the localized name:
- tzwin = keyvalues['StandardName']
+ tzwin = keyvalues["StandardName"]
# Open the list of timezones to look up the real name:
TZKEYNAME = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
@@ -60,15 +57,20 @@ def get_localzone_name():
sub = winreg.OpenKey(tzkey, subkey)
data = valuestodict(sub)
sub.Close()
- if data['Std'] == tzwin:
- tzkeyname = subkey
- break
+ try:
+ if data["Std"] == tzwin:
+ tzkeyname = subkey
+ break
+ except KeyError:
+ # This timezone didn't have proper configuration.
+ # Ignore it.
+ pass
tzkey.Close()
handle.Close()
if tzkeyname is None:
- raise LookupError('Can not find Windows timezone configuration')
+ raise LookupError("Can not find Windows timezone configuration")
timezone = win_tz.get(tzkeyname)
if timezone is None:
@@ -85,6 +87,7 @@ def get_localzone():
global _cache_tz
if _cache_tz is None:
_cache_tz = get_localzone_name()
+
return _cache_tz
diff --git a/nikola/packages/tzlocal/windows_tz.py b/nikola/packages/tzlocal/windows_tz.py
index c171aa5..0e911fd 100644
--- a/nikola/packages/tzlocal/windows_tz.py
+++ b/nikola/packages/tzlocal/windows_tz.py
@@ -1,544 +1,700 @@
-"""Windows timezone names."""
-# This file is autogenerated by the get_windows_info.py script
+"""Windows timezone mapping."""
+# This file is autogenerated by the update_windows_mapping.py script
# Do not edit.
win_tz = {
- 'AUS Central Standard Time': 'Australia/Darwin',
- 'AUS Eastern Standard Time': 'Australia/Sydney',
- 'Afghanistan Standard Time': 'Asia/Kabul',
- 'Alaskan Standard Time': 'America/Anchorage',
- 'Arab Standard Time': 'Asia/Riyadh',
- 'Arabian Standard Time': 'Asia/Dubai',
- 'Arabic Standard Time': 'Asia/Baghdad',
- 'Argentina Standard Time': 'America/Buenos_Aires',
- 'Atlantic Standard Time': 'America/Halifax',
- 'Azerbaijan Standard Time': 'Asia/Baku',
- 'Azores Standard Time': 'Atlantic/Azores',
- 'Bahia Standard Time': 'America/Bahia',
- 'Bangladesh Standard Time': 'Asia/Dhaka',
- 'Canada Central Standard Time': 'America/Regina',
- 'Cape Verde Standard Time': 'Atlantic/Cape_Verde',
- 'Caucasus Standard Time': 'Asia/Yerevan',
- 'Cen. Australia Standard Time': 'Australia/Adelaide',
- 'Central America Standard Time': 'America/Guatemala',
- 'Central Asia Standard Time': 'Asia/Almaty',
- 'Central Brazilian Standard Time': 'America/Cuiaba',
- 'Central Europe Standard Time': 'Europe/Budapest',
- 'Central European Standard Time': 'Europe/Warsaw',
- 'Central Pacific Standard Time': 'Pacific/Guadalcanal',
- 'Central Standard Time': 'America/Chicago',
- 'Central Standard Time (Mexico)': 'America/Mexico_City',
- 'China Standard Time': 'Asia/Shanghai',
- 'Dateline Standard Time': 'Etc/GMT+12',
- 'E. Africa Standard Time': 'Africa/Nairobi',
- 'E. Australia Standard Time': 'Australia/Brisbane',
- 'E. Europe Standard Time': 'Asia/Nicosia',
- 'E. South America Standard Time': 'America/Sao_Paulo',
- 'Eastern Standard Time': 'America/New_York',
- 'Egypt Standard Time': 'Africa/Cairo',
- 'Ekaterinburg Standard Time': 'Asia/Yekaterinburg',
- 'FLE Standard Time': 'Europe/Kiev',
- 'Fiji Standard Time': 'Pacific/Fiji',
- 'GMT Standard Time': 'Europe/London',
- 'GTB Standard Time': 'Europe/Bucharest',
- 'Georgian Standard Time': 'Asia/Tbilisi',
- 'Greenland Standard Time': 'America/Godthab',
- 'Greenwich Standard Time': 'Atlantic/Reykjavik',
- 'Hawaiian Standard Time': 'Pacific/Honolulu',
- 'India Standard Time': 'Asia/Calcutta',
- 'Iran Standard Time': 'Asia/Tehran',
- 'Israel Standard Time': 'Asia/Jerusalem',
- 'Jordan Standard Time': 'Asia/Amman',
- 'Kaliningrad Standard Time': 'Europe/Kaliningrad',
- 'Korea Standard Time': 'Asia/Seoul',
- 'Libya Standard Time': 'Africa/Tripoli',
- 'Magadan Standard Time': 'Asia/Magadan',
- 'Mauritius Standard Time': 'Indian/Mauritius',
- 'Middle East Standard Time': 'Asia/Beirut',
- 'Montevideo Standard Time': 'America/Montevideo',
- 'Morocco Standard Time': 'Africa/Casablanca',
- 'Mountain Standard Time': 'America/Denver',
- 'Mountain Standard Time (Mexico)': 'America/Chihuahua',
- 'Myanmar Standard Time': 'Asia/Rangoon',
- 'N. Central Asia Standard Time': 'Asia/Novosibirsk',
- 'Namibia Standard Time': 'Africa/Windhoek',
- 'Nepal Standard Time': 'Asia/Katmandu',
- 'New Zealand Standard Time': 'Pacific/Auckland',
- 'Newfoundland Standard Time': 'America/St_Johns',
- 'North Asia East Standard Time': 'Asia/Irkutsk',
- 'North Asia Standard Time': 'Asia/Krasnoyarsk',
- 'Pacific SA Standard Time': 'America/Santiago',
- 'Pacific Standard Time': 'America/Los_Angeles',
- 'Pacific Standard Time (Mexico)': 'America/Santa_Isabel',
- 'Pakistan Standard Time': 'Asia/Karachi',
- 'Paraguay Standard Time': 'America/Asuncion',
- 'Romance Standard Time': 'Europe/Paris',
- 'Russian Standard Time': 'Europe/Moscow',
- 'SA Eastern Standard Time': 'America/Cayenne',
- 'SA Pacific Standard Time': 'America/Bogota',
- 'SA Western Standard Time': 'America/La_Paz',
- 'SE Asia Standard Time': 'Asia/Bangkok',
- 'Samoa Standard Time': 'Pacific/Apia',
- 'Singapore Standard Time': 'Asia/Singapore',
- 'South Africa Standard Time': 'Africa/Johannesburg',
- 'Sri Lanka Standard Time': 'Asia/Colombo',
- 'Syria Standard Time': 'Asia/Damascus',
- 'Taipei Standard Time': 'Asia/Taipei',
- 'Tasmania Standard Time': 'Australia/Hobart',
- 'Tokyo Standard Time': 'Asia/Tokyo',
- 'Tonga Standard Time': 'Pacific/Tongatapu',
- 'Turkey Standard Time': 'Europe/Istanbul',
- 'US Eastern Standard Time': 'America/Indianapolis',
- 'US Mountain Standard Time': 'America/Phoenix',
- 'UTC': 'Etc/GMT',
- 'UTC+12': 'Etc/GMT-12',
- 'UTC-02': 'Etc/GMT+2',
- 'UTC-11': 'Etc/GMT+11',
- 'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar',
- 'Venezuela Standard Time': 'America/Caracas',
- 'Vladivostok Standard Time': 'Asia/Vladivostok',
- 'W. Australia Standard Time': 'Australia/Perth',
- 'W. Central Africa Standard Time': 'Africa/Lagos',
- 'W. Europe Standard Time': 'Europe/Berlin',
- 'West Asia Standard Time': 'Asia/Tashkent',
- 'West Pacific Standard Time': 'Pacific/Port_Moresby',
- 'Yakutsk Standard Time': 'Asia/Yakutsk'
+ "AUS Central Standard Time": "Australia/Darwin",
+ "AUS Eastern Standard Time": "Australia/Sydney",
+ "Afghanistan Standard Time": "Asia/Kabul",
+ "Alaskan Standard Time": "America/Anchorage",
+ "Aleutian Standard Time": "America/Adak",
+ "Altai Standard Time": "Asia/Barnaul",
+ "Arab Standard Time": "Asia/Riyadh",
+ "Arabian Standard Time": "Asia/Dubai",
+ "Arabic Standard Time": "Asia/Baghdad",
+ "Argentina Standard Time": "America/Buenos_Aires",
+ "Astrakhan Standard Time": "Europe/Astrakhan",
+ "Atlantic Standard Time": "America/Halifax",
+ "Aus Central W. Standard Time": "Australia/Eucla",
+ "Azerbaijan Standard Time": "Asia/Baku",
+ "Azores Standard Time": "Atlantic/Azores",
+ "Bahia Standard Time": "America/Bahia",
+ "Bangladesh Standard Time": "Asia/Dhaka",
+ "Belarus Standard Time": "Europe/Minsk",
+ "Bougainville Standard Time": "Pacific/Bougainville",
+ "Canada Central Standard Time": "America/Regina",
+ "Cape Verde Standard Time": "Atlantic/Cape_Verde",
+ "Caucasus Standard Time": "Asia/Yerevan",
+ "Cen. Australia Standard Time": "Australia/Adelaide",
+ "Central America Standard Time": "America/Guatemala",
+ "Central Asia Standard Time": "Asia/Almaty",
+ "Central Brazilian Standard Time": "America/Cuiaba",
+ "Central Europe Standard Time": "Europe/Budapest",
+ "Central European Standard Time": "Europe/Warsaw",
+ "Central Pacific Standard Time": "Pacific/Guadalcanal",
+ "Central Standard Time": "America/Chicago",
+ "Central Standard Time (Mexico)": "America/Mexico_City",
+ "Chatham Islands Standard Time": "Pacific/Chatham",
+ "China Standard Time": "Asia/Shanghai",
+ "Cuba Standard Time": "America/Havana",
+ "Dateline Standard Time": "Etc/GMT+12",
+ "E. Africa Standard Time": "Africa/Nairobi",
+ "E. Australia Standard Time": "Australia/Brisbane",
+ "E. Europe Standard Time": "Europe/Chisinau",
+ "E. South America Standard Time": "America/Sao_Paulo",
+ "Easter Island Standard Time": "Pacific/Easter",
+ "Eastern Standard Time": "America/New_York",
+ "Eastern Standard Time (Mexico)": "America/Cancun",
+ "Egypt Standard Time": "Africa/Cairo",
+ "Ekaterinburg Standard Time": "Asia/Yekaterinburg",
+ "FLE Standard Time": "Europe/Kiev",
+ "Fiji Standard Time": "Pacific/Fiji",
+ "GMT Standard Time": "Europe/London",
+ "GTB Standard Time": "Europe/Bucharest",
+ "Georgian Standard Time": "Asia/Tbilisi",
+ "Greenland Standard Time": "America/Godthab",
+ "Greenwich Standard Time": "Atlantic/Reykjavik",
+ "Haiti Standard Time": "America/Port-au-Prince",
+ "Hawaiian Standard Time": "Pacific/Honolulu",
+ "India Standard Time": "Asia/Calcutta",
+ "Iran Standard Time": "Asia/Tehran",
+ "Israel Standard Time": "Asia/Jerusalem",
+ "Jordan Standard Time": "Asia/Amman",
+ "Kaliningrad Standard Time": "Europe/Kaliningrad",
+ "Korea Standard Time": "Asia/Seoul",
+ "Libya Standard Time": "Africa/Tripoli",
+ "Line Islands Standard Time": "Pacific/Kiritimati",
+ "Lord Howe Standard Time": "Australia/Lord_Howe",
+ "Magadan Standard Time": "Asia/Magadan",
+ "Magallanes Standard Time": "America/Punta_Arenas",
+ "Marquesas Standard Time": "Pacific/Marquesas",
+ "Mauritius Standard Time": "Indian/Mauritius",
+ "Middle East Standard Time": "Asia/Beirut",
+ "Montevideo Standard Time": "America/Montevideo",
+ "Morocco Standard Time": "Africa/Casablanca",
+ "Mountain Standard Time": "America/Denver",
+ "Mountain Standard Time (Mexico)": "America/Chihuahua",
+ "Myanmar Standard Time": "Asia/Rangoon",
+ "N. Central Asia Standard Time": "Asia/Novosibirsk",
+ "Namibia Standard Time": "Africa/Windhoek",
+ "Nepal Standard Time": "Asia/Katmandu",
+ "New Zealand Standard Time": "Pacific/Auckland",
+ "Newfoundland Standard Time": "America/St_Johns",
+ "Norfolk Standard Time": "Pacific/Norfolk",
+ "North Asia East Standard Time": "Asia/Irkutsk",
+ "North Asia Standard Time": "Asia/Krasnoyarsk",
+ "North Korea Standard Time": "Asia/Pyongyang",
+ "Omsk Standard Time": "Asia/Omsk",
+ "Pacific SA Standard Time": "America/Santiago",
+ "Pacific Standard Time": "America/Los_Angeles",
+ "Pacific Standard Time (Mexico)": "America/Tijuana",
+ "Pakistan Standard Time": "Asia/Karachi",
+ "Paraguay Standard Time": "America/Asuncion",
+ "Romance Standard Time": "Europe/Paris",
+ "Russia Time Zone 10": "Asia/Srednekolymsk",
+ "Russia Time Zone 11": "Asia/Kamchatka",
+ "Russia Time Zone 3": "Europe/Samara",
+ "Russian Standard Time": "Europe/Moscow",
+ "SA Eastern Standard Time": "America/Cayenne",
+ "SA Pacific Standard Time": "America/Bogota",
+ "SA Western Standard Time": "America/La_Paz",
+ "SE Asia Standard Time": "Asia/Bangkok",
+ "Saint Pierre Standard Time": "America/Miquelon",
+ "Sakhalin Standard Time": "Asia/Sakhalin",
+ "Samoa Standard Time": "Pacific/Apia",
+ "Sao Tome Standard Time": "Africa/Sao_Tome",
+ "Saratov Standard Time": "Europe/Saratov",
+ "Singapore Standard Time": "Asia/Singapore",
+ "South Africa Standard Time": "Africa/Johannesburg",
+ "Sri Lanka Standard Time": "Asia/Colombo",
+ "Sudan Standard Time": "Africa/Khartoum",
+ "Syria Standard Time": "Asia/Damascus",
+ "Taipei Standard Time": "Asia/Taipei",
+ "Tasmania Standard Time": "Australia/Hobart",
+ "Tocantins Standard Time": "America/Araguaina",
+ "Tokyo Standard Time": "Asia/Tokyo",
+ "Tomsk Standard Time": "Asia/Tomsk",
+ "Tonga Standard Time": "Pacific/Tongatapu",
+ "Transbaikal Standard Time": "Asia/Chita",
+ "Turkey Standard Time": "Europe/Istanbul",
+ "Turks And Caicos Standard Time": "America/Grand_Turk",
+ "US Eastern Standard Time": "America/Indianapolis",
+ "US Mountain Standard Time": "America/Phoenix",
+ "UTC": "Etc/GMT",
+ "UTC+12": "Etc/GMT-12",
+ "UTC+13": "Etc/GMT-13",
+ "UTC-02": "Etc/GMT+2",
+ "UTC-08": "Etc/GMT+8",
+ "UTC-09": "Etc/GMT+9",
+ "UTC-11": "Etc/GMT+11",
+ "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
+ "Venezuela Standard Time": "America/Caracas",
+ "Vladivostok Standard Time": "Asia/Vladivostok",
+ "W. Australia Standard Time": "Australia/Perth",
+ "W. Central Africa Standard Time": "Africa/Lagos",
+ "W. Europe Standard Time": "Europe/Berlin",
+ "W. Mongolia Standard Time": "Asia/Hovd",
+ "West Asia Standard Time": "Asia/Tashkent",
+ "West Bank Standard Time": "Asia/Hebron",
+ "West Pacific Standard Time": "Pacific/Port_Moresby",
+ "Yakutsk Standard Time": "Asia/Yakutsk",
}
# Old name for the win_tz variable:
tz_names = win_tz
tz_win = {
- 'Africa/Abidjan': 'Greenwich Standard Time',
- 'Africa/Accra': 'Greenwich Standard Time',
- 'Africa/Addis_Ababa': 'E. Africa Standard Time',
- 'Africa/Algiers': 'W. Central Africa Standard Time',
- 'Africa/Asmera': 'E. Africa Standard Time',
- 'Africa/Bamako': 'Greenwich Standard Time',
- 'Africa/Bangui': 'W. Central Africa Standard Time',
- 'Africa/Banjul': 'Greenwich Standard Time',
- 'Africa/Bissau': 'Greenwich Standard Time',
- 'Africa/Blantyre': 'South Africa Standard Time',
- 'Africa/Brazzaville': 'W. Central Africa Standard Time',
- 'Africa/Bujumbura': 'South Africa Standard Time',
- 'Africa/Cairo': 'Egypt Standard Time',
- 'Africa/Casablanca': 'Morocco Standard Time',
- 'Africa/Ceuta': 'Romance Standard Time',
- 'Africa/Conakry': 'Greenwich Standard Time',
- 'Africa/Dakar': 'Greenwich Standard Time',
- 'Africa/Dar_es_Salaam': 'E. Africa Standard Time',
- 'Africa/Djibouti': 'E. Africa Standard Time',
- 'Africa/Douala': 'W. Central Africa Standard Time',
- 'Africa/El_Aaiun': 'Morocco Standard Time',
- 'Africa/Freetown': 'Greenwich Standard Time',
- 'Africa/Gaborone': 'South Africa Standard Time',
- 'Africa/Harare': 'South Africa Standard Time',
- 'Africa/Johannesburg': 'South Africa Standard Time',
- 'Africa/Juba': 'E. Africa Standard Time',
- 'Africa/Kampala': 'E. Africa Standard Time',
- 'Africa/Khartoum': 'E. Africa Standard Time',
- 'Africa/Kigali': 'South Africa Standard Time',
- 'Africa/Kinshasa': 'W. Central Africa Standard Time',
- 'Africa/Lagos': 'W. Central Africa Standard Time',
- 'Africa/Libreville': 'W. Central Africa Standard Time',
- 'Africa/Lome': 'Greenwich Standard Time',
- 'Africa/Luanda': 'W. Central Africa Standard Time',
- 'Africa/Lubumbashi': 'South Africa Standard Time',
- 'Africa/Lusaka': 'South Africa Standard Time',
- 'Africa/Malabo': 'W. Central Africa Standard Time',
- 'Africa/Maputo': 'South Africa Standard Time',
- 'Africa/Maseru': 'South Africa Standard Time',
- 'Africa/Mbabane': 'South Africa Standard Time',
- 'Africa/Mogadishu': 'E. Africa Standard Time',
- 'Africa/Monrovia': 'Greenwich Standard Time',
- 'Africa/Nairobi': 'E. Africa Standard Time',
- 'Africa/Ndjamena': 'W. Central Africa Standard Time',
- 'Africa/Niamey': 'W. Central Africa Standard Time',
- 'Africa/Nouakchott': 'Greenwich Standard Time',
- 'Africa/Ouagadougou': 'Greenwich Standard Time',
- 'Africa/Porto-Novo': 'W. Central Africa Standard Time',
- 'Africa/Sao_Tome': 'Greenwich Standard Time',
- 'Africa/Tripoli': 'Libya Standard Time',
- 'Africa/Tunis': 'W. Central Africa Standard Time',
- 'Africa/Windhoek': 'Namibia Standard Time',
- 'America/Anchorage': 'Alaskan Standard Time',
- 'America/Anguilla': 'SA Western Standard Time',
- 'America/Antigua': 'SA Western Standard Time',
- 'America/Araguaina': 'SA Eastern Standard Time',
- 'America/Argentina/La_Rioja': 'Argentina Standard Time',
- 'America/Argentina/Rio_Gallegos': 'Argentina Standard Time',
- 'America/Argentina/Salta': 'Argentina Standard Time',
- 'America/Argentina/San_Juan': 'Argentina Standard Time',
- 'America/Argentina/San_Luis': 'Argentina Standard Time',
- 'America/Argentina/Tucuman': 'Argentina Standard Time',
- 'America/Argentina/Ushuaia': 'Argentina Standard Time',
- 'America/Aruba': 'SA Western Standard Time',
- 'America/Asuncion': 'Paraguay Standard Time',
- 'America/Bahia': 'Bahia Standard Time',
- 'America/Bahia_Banderas': 'Central Standard Time (Mexico)',
- 'America/Barbados': 'SA Western Standard Time',
- 'America/Belem': 'SA Eastern Standard Time',
- 'America/Belize': 'Central America Standard Time',
- 'America/Blanc-Sablon': 'SA Western Standard Time',
- 'America/Boa_Vista': 'SA Western Standard Time',
- 'America/Bogota': 'SA Pacific Standard Time',
- 'America/Boise': 'Mountain Standard Time',
- 'America/Buenos_Aires': 'Argentina Standard Time',
- 'America/Cambridge_Bay': 'Mountain Standard Time',
- 'America/Campo_Grande': 'Central Brazilian Standard Time',
- 'America/Cancun': 'Central Standard Time (Mexico)',
- 'America/Caracas': 'Venezuela Standard Time',
- 'America/Catamarca': 'Argentina Standard Time',
- 'America/Cayenne': 'SA Eastern Standard Time',
- 'America/Cayman': 'SA Pacific Standard Time',
- 'America/Chicago': 'Central Standard Time',
- 'America/Chihuahua': 'Mountain Standard Time (Mexico)',
- 'America/Coral_Harbour': 'SA Pacific Standard Time',
- 'America/Cordoba': 'Argentina Standard Time',
- 'America/Costa_Rica': 'Central America Standard Time',
- 'America/Creston': 'US Mountain Standard Time',
- 'America/Cuiaba': 'Central Brazilian Standard Time',
- 'America/Curacao': 'SA Western Standard Time',
- 'America/Danmarkshavn': 'UTC',
- 'America/Dawson': 'Pacific Standard Time',
- 'America/Dawson_Creek': 'US Mountain Standard Time',
- 'America/Denver': 'Mountain Standard Time',
- 'America/Detroit': 'Eastern Standard Time',
- 'America/Dominica': 'SA Western Standard Time',
- 'America/Edmonton': 'Mountain Standard Time',
- 'America/Eirunepe': 'SA Pacific Standard Time',
- 'America/El_Salvador': 'Central America Standard Time',
- 'America/Fortaleza': 'SA Eastern Standard Time',
- 'America/Glace_Bay': 'Atlantic Standard Time',
- 'America/Godthab': 'Greenland Standard Time',
- 'America/Goose_Bay': 'Atlantic Standard Time',
- 'America/Grand_Turk': 'Eastern Standard Time',
- 'America/Grenada': 'SA Western Standard Time',
- 'America/Guadeloupe': 'SA Western Standard Time',
- 'America/Guatemala': 'Central America Standard Time',
- 'America/Guayaquil': 'SA Pacific Standard Time',
- 'America/Guyana': 'SA Western Standard Time',
- 'America/Halifax': 'Atlantic Standard Time',
- 'America/Havana': 'Eastern Standard Time',
- 'America/Hermosillo': 'US Mountain Standard Time',
- 'America/Indiana/Knox': 'Central Standard Time',
- 'America/Indiana/Marengo': 'US Eastern Standard Time',
- 'America/Indiana/Petersburg': 'Eastern Standard Time',
- 'America/Indiana/Tell_City': 'Central Standard Time',
- 'America/Indiana/Vevay': 'US Eastern Standard Time',
- 'America/Indiana/Vincennes': 'Eastern Standard Time',
- 'America/Indiana/Winamac': 'Eastern Standard Time',
- 'America/Indianapolis': 'US Eastern Standard Time',
- 'America/Inuvik': 'Mountain Standard Time',
- 'America/Iqaluit': 'Eastern Standard Time',
- 'America/Jamaica': 'SA Pacific Standard Time',
- 'America/Jujuy': 'Argentina Standard Time',
- 'America/Juneau': 'Alaskan Standard Time',
- 'America/Kentucky/Monticello': 'Eastern Standard Time',
- 'America/Kralendijk': 'SA Western Standard Time',
- 'America/La_Paz': 'SA Western Standard Time',
- 'America/Lima': 'SA Pacific Standard Time',
- 'America/Los_Angeles': 'Pacific Standard Time',
- 'America/Louisville': 'Eastern Standard Time',
- 'America/Lower_Princes': 'SA Western Standard Time',
- 'America/Maceio': 'SA Eastern Standard Time',
- 'America/Managua': 'Central America Standard Time',
- 'America/Manaus': 'SA Western Standard Time',
- 'America/Marigot': 'SA Western Standard Time',
- 'America/Martinique': 'SA Western Standard Time',
- 'America/Matamoros': 'Central Standard Time',
- 'America/Mazatlan': 'Mountain Standard Time (Mexico)',
- 'America/Mendoza': 'Argentina Standard Time',
- 'America/Menominee': 'Central Standard Time',
- 'America/Merida': 'Central Standard Time (Mexico)',
- 'America/Mexico_City': 'Central Standard Time (Mexico)',
- 'America/Moncton': 'Atlantic Standard Time',
- 'America/Monterrey': 'Central Standard Time (Mexico)',
- 'America/Montevideo': 'Montevideo Standard Time',
- 'America/Montreal': 'Eastern Standard Time',
- 'America/Montserrat': 'SA Western Standard Time',
- 'America/Nassau': 'Eastern Standard Time',
- 'America/New_York': 'Eastern Standard Time',
- 'America/Nipigon': 'Eastern Standard Time',
- 'America/Nome': 'Alaskan Standard Time',
- 'America/Noronha': 'UTC-02',
- 'America/North_Dakota/Beulah': 'Central Standard Time',
- 'America/North_Dakota/Center': 'Central Standard Time',
- 'America/North_Dakota/New_Salem': 'Central Standard Time',
- 'America/Ojinaga': 'Mountain Standard Time',
- 'America/Panama': 'SA Pacific Standard Time',
- 'America/Pangnirtung': 'Eastern Standard Time',
- 'America/Paramaribo': 'SA Eastern Standard Time',
- 'America/Phoenix': 'US Mountain Standard Time',
- 'America/Port-au-Prince': 'Eastern Standard Time',
- 'America/Port_of_Spain': 'SA Western Standard Time',
- 'America/Porto_Velho': 'SA Western Standard Time',
- 'America/Puerto_Rico': 'SA Western Standard Time',
- 'America/Rainy_River': 'Central Standard Time',
- 'America/Rankin_Inlet': 'Central Standard Time',
- 'America/Recife': 'SA Eastern Standard Time',
- 'America/Regina': 'Canada Central Standard Time',
- 'America/Resolute': 'Central Standard Time',
- 'America/Rio_Branco': 'SA Pacific Standard Time',
- 'America/Santa_Isabel': 'Pacific Standard Time (Mexico)',
- 'America/Santarem': 'SA Eastern Standard Time',
- 'America/Santiago': 'Pacific SA Standard Time',
- 'America/Santo_Domingo': 'SA Western Standard Time',
- 'America/Sao_Paulo': 'E. South America Standard Time',
- 'America/Scoresbysund': 'Azores Standard Time',
- 'America/Shiprock': 'Mountain Standard Time',
- 'America/Sitka': 'Alaskan Standard Time',
- 'America/St_Barthelemy': 'SA Western Standard Time',
- 'America/St_Johns': 'Newfoundland Standard Time',
- 'America/St_Kitts': 'SA Western Standard Time',
- 'America/St_Lucia': 'SA Western Standard Time',
- 'America/St_Thomas': 'SA Western Standard Time',
- 'America/St_Vincent': 'SA Western Standard Time',
- 'America/Swift_Current': 'Canada Central Standard Time',
- 'America/Tegucigalpa': 'Central America Standard Time',
- 'America/Thule': 'Atlantic Standard Time',
- 'America/Thunder_Bay': 'Eastern Standard Time',
- 'America/Tijuana': 'Pacific Standard Time',
- 'America/Toronto': 'Eastern Standard Time',
- 'America/Tortola': 'SA Western Standard Time',
- 'America/Vancouver': 'Pacific Standard Time',
- 'America/Whitehorse': 'Pacific Standard Time',
- 'America/Winnipeg': 'Central Standard Time',
- 'America/Yakutat': 'Alaskan Standard Time',
- 'America/Yellowknife': 'Mountain Standard Time',
- 'Antarctica/Casey': 'W. Australia Standard Time',
- 'Antarctica/Davis': 'SE Asia Standard Time',
- 'Antarctica/DumontDUrville': 'West Pacific Standard Time',
- 'Antarctica/Macquarie': 'Central Pacific Standard Time',
- 'Antarctica/Mawson': 'West Asia Standard Time',
- 'Antarctica/McMurdo': 'New Zealand Standard Time',
- 'Antarctica/Palmer': 'Pacific SA Standard Time',
- 'Antarctica/Rothera': 'SA Eastern Standard Time',
- 'Antarctica/South_Pole': 'New Zealand Standard Time',
- 'Antarctica/Syowa': 'E. Africa Standard Time',
- 'Antarctica/Vostok': 'Central Asia Standard Time',
- 'Arctic/Longyearbyen': 'W. Europe Standard Time',
- 'Asia/Aden': 'Arab Standard Time',
- 'Asia/Almaty': 'Central Asia Standard Time',
- 'Asia/Amman': 'Jordan Standard Time',
- 'Asia/Anadyr': 'Magadan Standard Time',
- 'Asia/Aqtau': 'West Asia Standard Time',
- 'Asia/Aqtobe': 'West Asia Standard Time',
- 'Asia/Ashgabat': 'West Asia Standard Time',
- 'Asia/Baghdad': 'Arabic Standard Time',
- 'Asia/Bahrain': 'Arab Standard Time',
- 'Asia/Baku': 'Azerbaijan Standard Time',
- 'Asia/Bangkok': 'SE Asia Standard Time',
- 'Asia/Beirut': 'Middle East Standard Time',
- 'Asia/Bishkek': 'Central Asia Standard Time',
- 'Asia/Brunei': 'Singapore Standard Time',
- 'Asia/Calcutta': 'India Standard Time',
- 'Asia/Choibalsan': 'Ulaanbaatar Standard Time',
- 'Asia/Chongqing': 'China Standard Time',
- 'Asia/Colombo': 'Sri Lanka Standard Time',
- 'Asia/Damascus': 'Syria Standard Time',
- 'Asia/Dhaka': 'Bangladesh Standard Time',
- 'Asia/Dili': 'Tokyo Standard Time',
- 'Asia/Dubai': 'Arabian Standard Time',
- 'Asia/Dushanbe': 'West Asia Standard Time',
- 'Asia/Harbin': 'China Standard Time',
- 'Asia/Hong_Kong': 'China Standard Time',
- 'Asia/Hovd': 'SE Asia Standard Time',
- 'Asia/Irkutsk': 'North Asia East Standard Time',
- 'Asia/Jakarta': 'SE Asia Standard Time',
- 'Asia/Jayapura': 'Tokyo Standard Time',
- 'Asia/Jerusalem': 'Israel Standard Time',
- 'Asia/Kabul': 'Afghanistan Standard Time',
- 'Asia/Kamchatka': 'Magadan Standard Time',
- 'Asia/Karachi': 'Pakistan Standard Time',
- 'Asia/Kashgar': 'China Standard Time',
- 'Asia/Katmandu': 'Nepal Standard Time',
- 'Asia/Khandyga': 'Yakutsk Standard Time',
- 'Asia/Krasnoyarsk': 'North Asia Standard Time',
- 'Asia/Kuala_Lumpur': 'Singapore Standard Time',
- 'Asia/Kuching': 'Singapore Standard Time',
- 'Asia/Kuwait': 'Arab Standard Time',
- 'Asia/Macau': 'China Standard Time',
- 'Asia/Magadan': 'Magadan Standard Time',
- 'Asia/Makassar': 'Singapore Standard Time',
- 'Asia/Manila': 'Singapore Standard Time',
- 'Asia/Muscat': 'Arabian Standard Time',
- 'Asia/Nicosia': 'E. Europe Standard Time',
- 'Asia/Novokuznetsk': 'N. Central Asia Standard Time',
- 'Asia/Novosibirsk': 'N. Central Asia Standard Time',
- 'Asia/Omsk': 'N. Central Asia Standard Time',
- 'Asia/Oral': 'West Asia Standard Time',
- 'Asia/Phnom_Penh': 'SE Asia Standard Time',
- 'Asia/Pontianak': 'SE Asia Standard Time',
- 'Asia/Pyongyang': 'Korea Standard Time',
- 'Asia/Qatar': 'Arab Standard Time',
- 'Asia/Qyzylorda': 'Central Asia Standard Time',
- 'Asia/Rangoon': 'Myanmar Standard Time',
- 'Asia/Riyadh': 'Arab Standard Time',
- 'Asia/Saigon': 'SE Asia Standard Time',
- 'Asia/Sakhalin': 'Vladivostok Standard Time',
- 'Asia/Samarkand': 'West Asia Standard Time',
- 'Asia/Seoul': 'Korea Standard Time',
- 'Asia/Shanghai': 'China Standard Time',
- 'Asia/Singapore': 'Singapore Standard Time',
- 'Asia/Taipei': 'Taipei Standard Time',
- 'Asia/Tashkent': 'West Asia Standard Time',
- 'Asia/Tbilisi': 'Georgian Standard Time',
- 'Asia/Tehran': 'Iran Standard Time',
- 'Asia/Thimphu': 'Bangladesh Standard Time',
- 'Asia/Tokyo': 'Tokyo Standard Time',
- 'Asia/Ulaanbaatar': 'Ulaanbaatar Standard Time',
- 'Asia/Urumqi': 'China Standard Time',
- 'Asia/Ust-Nera': 'Vladivostok Standard Time',
- 'Asia/Vientiane': 'SE Asia Standard Time',
- 'Asia/Vladivostok': 'Vladivostok Standard Time',
- 'Asia/Yakutsk': 'Yakutsk Standard Time',
- 'Asia/Yekaterinburg': 'Ekaterinburg Standard Time',
- 'Asia/Yerevan': 'Caucasus Standard Time',
- 'Atlantic/Azores': 'Azores Standard Time',
- 'Atlantic/Bermuda': 'Atlantic Standard Time',
- 'Atlantic/Canary': 'GMT Standard Time',
- 'Atlantic/Cape_Verde': 'Cape Verde Standard Time',
- 'Atlantic/Faeroe': 'GMT Standard Time',
- 'Atlantic/Madeira': 'GMT Standard Time',
- 'Atlantic/Reykjavik': 'Greenwich Standard Time',
- 'Atlantic/South_Georgia': 'UTC-02',
- 'Atlantic/St_Helena': 'Greenwich Standard Time',
- 'Atlantic/Stanley': 'SA Eastern Standard Time',
- 'Australia/Adelaide': 'Cen. Australia Standard Time',
- 'Australia/Brisbane': 'E. Australia Standard Time',
- 'Australia/Broken_Hill': 'Cen. Australia Standard Time',
- 'Australia/Currie': 'Tasmania Standard Time',
- 'Australia/Darwin': 'AUS Central Standard Time',
- 'Australia/Hobart': 'Tasmania Standard Time',
- 'Australia/Lindeman': 'E. Australia Standard Time',
- 'Australia/Melbourne': 'AUS Eastern Standard Time',
- 'Australia/Perth': 'W. Australia Standard Time',
- 'Australia/Sydney': 'AUS Eastern Standard Time',
- 'CST6CDT': 'Central Standard Time',
- 'EST5EDT': 'Eastern Standard Time',
- 'Etc/GMT': 'UTC',
- 'Etc/GMT+1': 'Cape Verde Standard Time',
- 'Etc/GMT+10': 'Hawaiian Standard Time',
- 'Etc/GMT+11': 'UTC-11',
- 'Etc/GMT+12': 'Dateline Standard Time',
- 'Etc/GMT+2': 'UTC-02',
- 'Etc/GMT+3': 'SA Eastern Standard Time',
- 'Etc/GMT+4': 'SA Western Standard Time',
- 'Etc/GMT+5': 'SA Pacific Standard Time',
- 'Etc/GMT+6': 'Central America Standard Time',
- 'Etc/GMT+7': 'US Mountain Standard Time',
- 'Etc/GMT-1': 'W. Central Africa Standard Time',
- 'Etc/GMT-10': 'West Pacific Standard Time',
- 'Etc/GMT-11': 'Central Pacific Standard Time',
- 'Etc/GMT-12': 'UTC+12',
- 'Etc/GMT-13': 'Tonga Standard Time',
- 'Etc/GMT-2': 'South Africa Standard Time',
- 'Etc/GMT-3': 'E. Africa Standard Time',
- 'Etc/GMT-4': 'Arabian Standard Time',
- 'Etc/GMT-5': 'West Asia Standard Time',
- 'Etc/GMT-6': 'Central Asia Standard Time',
- 'Etc/GMT-7': 'SE Asia Standard Time',
- 'Etc/GMT-8': 'Singapore Standard Time',
- 'Etc/GMT-9': 'Tokyo Standard Time',
- 'Etc/UTC': 'UTC',
- 'Europe/Amsterdam': 'W. Europe Standard Time',
- 'Europe/Andorra': 'W. Europe Standard Time',
- 'Europe/Athens': 'GTB Standard Time',
- 'Europe/Belgrade': 'Central Europe Standard Time',
- 'Europe/Berlin': 'W. Europe Standard Time',
- 'Europe/Bratislava': 'Central Europe Standard Time',
- 'Europe/Brussels': 'Romance Standard Time',
- 'Europe/Bucharest': 'GTB Standard Time',
- 'Europe/Budapest': 'Central Europe Standard Time',
- 'Europe/Busingen': 'W. Europe Standard Time',
- 'Europe/Chisinau': 'GTB Standard Time',
- 'Europe/Copenhagen': 'Romance Standard Time',
- 'Europe/Dublin': 'GMT Standard Time',
- 'Europe/Gibraltar': 'W. Europe Standard Time',
- 'Europe/Guernsey': 'GMT Standard Time',
- 'Europe/Helsinki': 'FLE Standard Time',
- 'Europe/Isle_of_Man': 'GMT Standard Time',
- 'Europe/Istanbul': 'Turkey Standard Time',
- 'Europe/Jersey': 'GMT Standard Time',
- 'Europe/Kaliningrad': 'Kaliningrad Standard Time',
- 'Europe/Kiev': 'FLE Standard Time',
- 'Europe/Lisbon': 'GMT Standard Time',
- 'Europe/Ljubljana': 'Central Europe Standard Time',
- 'Europe/London': 'GMT Standard Time',
- 'Europe/Luxembourg': 'W. Europe Standard Time',
- 'Europe/Madrid': 'Romance Standard Time',
- 'Europe/Malta': 'W. Europe Standard Time',
- 'Europe/Mariehamn': 'FLE Standard Time',
- 'Europe/Minsk': 'Kaliningrad Standard Time',
- 'Europe/Monaco': 'W. Europe Standard Time',
- 'Europe/Moscow': 'Russian Standard Time',
- 'Europe/Oslo': 'W. Europe Standard Time',
- 'Europe/Paris': 'Romance Standard Time',
- 'Europe/Podgorica': 'Central Europe Standard Time',
- 'Europe/Prague': 'Central Europe Standard Time',
- 'Europe/Riga': 'FLE Standard Time',
- 'Europe/Rome': 'W. Europe Standard Time',
- 'Europe/Samara': 'Russian Standard Time',
- 'Europe/San_Marino': 'W. Europe Standard Time',
- 'Europe/Sarajevo': 'Central European Standard Time',
- 'Europe/Simferopol': 'FLE Standard Time',
- 'Europe/Skopje': 'Central European Standard Time',
- 'Europe/Sofia': 'FLE Standard Time',
- 'Europe/Stockholm': 'W. Europe Standard Time',
- 'Europe/Tallinn': 'FLE Standard Time',
- 'Europe/Tirane': 'Central Europe Standard Time',
- 'Europe/Uzhgorod': 'FLE Standard Time',
- 'Europe/Vaduz': 'W. Europe Standard Time',
- 'Europe/Vatican': 'W. Europe Standard Time',
- 'Europe/Vienna': 'W. Europe Standard Time',
- 'Europe/Vilnius': 'FLE Standard Time',
- 'Europe/Volgograd': 'Russian Standard Time',
- 'Europe/Warsaw': 'Central European Standard Time',
- 'Europe/Zagreb': 'Central European Standard Time',
- 'Europe/Zaporozhye': 'FLE Standard Time',
- 'Europe/Zurich': 'W. Europe Standard Time',
- 'Indian/Antananarivo': 'E. Africa Standard Time',
- 'Indian/Chagos': 'Central Asia Standard Time',
- 'Indian/Christmas': 'SE Asia Standard Time',
- 'Indian/Cocos': 'Myanmar Standard Time',
- 'Indian/Comoro': 'E. Africa Standard Time',
- 'Indian/Kerguelen': 'West Asia Standard Time',
- 'Indian/Mahe': 'Mauritius Standard Time',
- 'Indian/Maldives': 'West Asia Standard Time',
- 'Indian/Mauritius': 'Mauritius Standard Time',
- 'Indian/Mayotte': 'E. Africa Standard Time',
- 'Indian/Reunion': 'Mauritius Standard Time',
- 'MST7MDT': 'Mountain Standard Time',
- 'PST8PDT': 'Pacific Standard Time',
- 'Pacific/Apia': 'Samoa Standard Time',
- 'Pacific/Auckland': 'New Zealand Standard Time',
- 'Pacific/Efate': 'Central Pacific Standard Time',
- 'Pacific/Enderbury': 'Tonga Standard Time',
- 'Pacific/Fakaofo': 'Tonga Standard Time',
- 'Pacific/Fiji': 'Fiji Standard Time',
- 'Pacific/Funafuti': 'UTC+12',
- 'Pacific/Galapagos': 'Central America Standard Time',
- 'Pacific/Guadalcanal': 'Central Pacific Standard Time',
- 'Pacific/Guam': 'West Pacific Standard Time',
- 'Pacific/Honolulu': 'Hawaiian Standard Time',
- 'Pacific/Johnston': 'Hawaiian Standard Time',
- 'Pacific/Kosrae': 'Central Pacific Standard Time',
- 'Pacific/Kwajalein': 'UTC+12',
- 'Pacific/Majuro': 'UTC+12',
- 'Pacific/Midway': 'UTC-11',
- 'Pacific/Nauru': 'UTC+12',
- 'Pacific/Niue': 'UTC-11',
- 'Pacific/Noumea': 'Central Pacific Standard Time',
- 'Pacific/Pago_Pago': 'UTC-11',
- 'Pacific/Palau': 'Tokyo Standard Time',
- 'Pacific/Ponape': 'Central Pacific Standard Time',
- 'Pacific/Port_Moresby': 'West Pacific Standard Time',
- 'Pacific/Rarotonga': 'Hawaiian Standard Time',
- 'Pacific/Saipan': 'West Pacific Standard Time',
- 'Pacific/Tahiti': 'Hawaiian Standard Time',
- 'Pacific/Tarawa': 'UTC+12',
- 'Pacific/Tongatapu': 'Tonga Standard Time',
- 'Pacific/Truk': 'West Pacific Standard Time',
- 'Pacific/Wake': 'UTC+12',
- 'Pacific/Wallis': 'UTC+12'
+ "Africa/Abidjan": "Greenwich Standard Time",
+ "Africa/Accra": "Greenwich Standard Time",
+ "Africa/Addis_Ababa": "E. Africa Standard Time",
+ "Africa/Algiers": "W. Central Africa Standard Time",
+ "Africa/Asmera": "E. Africa Standard Time",
+ "Africa/Bamako": "Greenwich Standard Time",
+ "Africa/Bangui": "W. Central Africa Standard Time",
+ "Africa/Banjul": "Greenwich Standard Time",
+ "Africa/Bissau": "Greenwich Standard Time",
+ "Africa/Blantyre": "South Africa Standard Time",
+ "Africa/Brazzaville": "W. Central Africa Standard Time",
+ "Africa/Bujumbura": "South Africa Standard Time",
+ "Africa/Cairo": "Egypt Standard Time",
+ "Africa/Casablanca": "Morocco Standard Time",
+ "Africa/Ceuta": "Romance Standard Time",
+ "Africa/Conakry": "Greenwich Standard Time",
+ "Africa/Dakar": "Greenwich Standard Time",
+ "Africa/Dar_es_Salaam": "E. Africa Standard Time",
+ "Africa/Djibouti": "E. Africa Standard Time",
+ "Africa/Douala": "W. Central Africa Standard Time",
+ "Africa/El_Aaiun": "Morocco Standard Time",
+ "Africa/Freetown": "Greenwich Standard Time",
+ "Africa/Gaborone": "South Africa Standard Time",
+ "Africa/Harare": "South Africa Standard Time",
+ "Africa/Johannesburg": "South Africa Standard Time",
+ "Africa/Juba": "E. Africa Standard Time",
+ "Africa/Kampala": "E. Africa Standard Time",
+ "Africa/Khartoum": "Sudan Standard Time",
+ "Africa/Kigali": "South Africa Standard Time",
+ "Africa/Kinshasa": "W. Central Africa Standard Time",
+ "Africa/Lagos": "W. Central Africa Standard Time",
+ "Africa/Libreville": "W. Central Africa Standard Time",
+ "Africa/Lome": "Greenwich Standard Time",
+ "Africa/Luanda": "W. Central Africa Standard Time",
+ "Africa/Lubumbashi": "South Africa Standard Time",
+ "Africa/Lusaka": "South Africa Standard Time",
+ "Africa/Malabo": "W. Central Africa Standard Time",
+ "Africa/Maputo": "South Africa Standard Time",
+ "Africa/Maseru": "South Africa Standard Time",
+ "Africa/Mbabane": "South Africa Standard Time",
+ "Africa/Mogadishu": "E. Africa Standard Time",
+ "Africa/Monrovia": "Greenwich Standard Time",
+ "Africa/Nairobi": "E. Africa Standard Time",
+ "Africa/Ndjamena": "W. Central Africa Standard Time",
+ "Africa/Niamey": "W. Central Africa Standard Time",
+ "Africa/Nouakchott": "Greenwich Standard Time",
+ "Africa/Ouagadougou": "Greenwich Standard Time",
+ "Africa/Porto-Novo": "W. Central Africa Standard Time",
+ "Africa/Sao_Tome": "Sao Tome Standard Time",
+ "Africa/Timbuktu": "Greenwich Standard Time",
+ "Africa/Tripoli": "Libya Standard Time",
+ "Africa/Tunis": "W. Central Africa Standard Time",
+ "Africa/Windhoek": "Namibia Standard Time",
+ "America/Adak": "Aleutian Standard Time",
+ "America/Anchorage": "Alaskan Standard Time",
+ "America/Anguilla": "SA Western Standard Time",
+ "America/Antigua": "SA Western Standard Time",
+ "America/Araguaina": "Tocantins Standard Time",
+ "America/Argentina/La_Rioja": "Argentina Standard Time",
+ "America/Argentina/Rio_Gallegos": "Argentina Standard Time",
+ "America/Argentina/Salta": "Argentina Standard Time",
+ "America/Argentina/San_Juan": "Argentina Standard Time",
+ "America/Argentina/San_Luis": "Argentina Standard Time",
+ "America/Argentina/Tucuman": "Argentina Standard Time",
+ "America/Argentina/Ushuaia": "Argentina Standard Time",
+ "America/Aruba": "SA Western Standard Time",
+ "America/Asuncion": "Paraguay Standard Time",
+ "America/Atka": "Aleutian Standard Time",
+ "America/Bahia": "Bahia Standard Time",
+ "America/Bahia_Banderas": "Central Standard Time (Mexico)",
+ "America/Barbados": "SA Western Standard Time",
+ "America/Belem": "SA Eastern Standard Time",
+ "America/Belize": "Central America Standard Time",
+ "America/Blanc-Sablon": "SA Western Standard Time",
+ "America/Boa_Vista": "SA Western Standard Time",
+ "America/Bogota": "SA Pacific Standard Time",
+ "America/Boise": "Mountain Standard Time",
+ "America/Buenos_Aires": "Argentina Standard Time",
+ "America/Cambridge_Bay": "Mountain Standard Time",
+ "America/Campo_Grande": "Central Brazilian Standard Time",
+ "America/Cancun": "Eastern Standard Time (Mexico)",
+ "America/Caracas": "Venezuela Standard Time",
+ "America/Catamarca": "Argentina Standard Time",
+ "America/Cayenne": "SA Eastern Standard Time",
+ "America/Cayman": "SA Pacific Standard Time",
+ "America/Chicago": "Central Standard Time",
+ "America/Chihuahua": "Mountain Standard Time (Mexico)",
+ "America/Coral_Harbour": "SA Pacific Standard Time",
+ "America/Cordoba": "Argentina Standard Time",
+ "America/Costa_Rica": "Central America Standard Time",
+ "America/Creston": "US Mountain Standard Time",
+ "America/Cuiaba": "Central Brazilian Standard Time",
+ "America/Curacao": "SA Western Standard Time",
+ "America/Danmarkshavn": "UTC",
+ "America/Dawson": "Pacific Standard Time",
+ "America/Dawson_Creek": "US Mountain Standard Time",
+ "America/Denver": "Mountain Standard Time",
+ "America/Detroit": "Eastern Standard Time",
+ "America/Dominica": "SA Western Standard Time",
+ "America/Edmonton": "Mountain Standard Time",
+ "America/Eirunepe": "SA Pacific Standard Time",
+ "America/El_Salvador": "Central America Standard Time",
+ "America/Ensenada": "Pacific Standard Time (Mexico)",
+ "America/Fort_Nelson": "US Mountain Standard Time",
+ "America/Fortaleza": "SA Eastern Standard Time",
+ "America/Glace_Bay": "Atlantic Standard Time",
+ "America/Godthab": "Greenland Standard Time",
+ "America/Goose_Bay": "Atlantic Standard Time",
+ "America/Grand_Turk": "Turks And Caicos Standard Time",
+ "America/Grenada": "SA Western Standard Time",
+ "America/Guadeloupe": "SA Western Standard Time",
+ "America/Guatemala": "Central America Standard Time",
+ "America/Guayaquil": "SA Pacific Standard Time",
+ "America/Guyana": "SA Western Standard Time",
+ "America/Halifax": "Atlantic Standard Time",
+ "America/Havana": "Cuba Standard Time",
+ "America/Hermosillo": "US Mountain Standard Time",
+ "America/Indiana/Knox": "Central Standard Time",
+ "America/Indiana/Marengo": "US Eastern Standard Time",
+ "America/Indiana/Petersburg": "Eastern Standard Time",
+ "America/Indiana/Tell_City": "Central Standard Time",
+ "America/Indiana/Vevay": "US Eastern Standard Time",
+ "America/Indiana/Vincennes": "Eastern Standard Time",
+ "America/Indiana/Winamac": "Eastern Standard Time",
+ "America/Indianapolis": "US Eastern Standard Time",
+ "America/Inuvik": "Mountain Standard Time",
+ "America/Iqaluit": "Eastern Standard Time",
+ "America/Jamaica": "SA Pacific Standard Time",
+ "America/Jujuy": "Argentina Standard Time",
+ "America/Juneau": "Alaskan Standard Time",
+ "America/Kentucky/Monticello": "Eastern Standard Time",
+ "America/Knox_IN": "Central Standard Time",
+ "America/Kralendijk": "SA Western Standard Time",
+ "America/La_Paz": "SA Western Standard Time",
+ "America/Lima": "SA Pacific Standard Time",
+ "America/Los_Angeles": "Pacific Standard Time",
+ "America/Louisville": "Eastern Standard Time",
+ "America/Lower_Princes": "SA Western Standard Time",
+ "America/Maceio": "SA Eastern Standard Time",
+ "America/Managua": "Central America Standard Time",
+ "America/Manaus": "SA Western Standard Time",
+ "America/Marigot": "SA Western Standard Time",
+ "America/Martinique": "SA Western Standard Time",
+ "America/Matamoros": "Central Standard Time",
+ "America/Mazatlan": "Mountain Standard Time (Mexico)",
+ "America/Mendoza": "Argentina Standard Time",
+ "America/Menominee": "Central Standard Time",
+ "America/Merida": "Central Standard Time (Mexico)",
+ "America/Metlakatla": "Pacific Standard Time",
+ "America/Mexico_City": "Central Standard Time (Mexico)",
+ "America/Miquelon": "Saint Pierre Standard Time",
+ "America/Moncton": "Atlantic Standard Time",
+ "America/Monterrey": "Central Standard Time (Mexico)",
+ "America/Montevideo": "Montevideo Standard Time",
+ "America/Montreal": "Eastern Standard Time",
+ "America/Montserrat": "SA Western Standard Time",
+ "America/Nassau": "Eastern Standard Time",
+ "America/New_York": "Eastern Standard Time",
+ "America/Nipigon": "Eastern Standard Time",
+ "America/Nome": "Alaskan Standard Time",
+ "America/Noronha": "UTC-02",
+ "America/North_Dakota/Beulah": "Central Standard Time",
+ "America/North_Dakota/Center": "Central Standard Time",
+ "America/North_Dakota/New_Salem": "Central Standard Time",
+ "America/Ojinaga": "Mountain Standard Time",
+ "America/Panama": "SA Pacific Standard Time",
+ "America/Pangnirtung": "Eastern Standard Time",
+ "America/Paramaribo": "SA Eastern Standard Time",
+ "America/Phoenix": "US Mountain Standard Time",
+ "America/Port-au-Prince": "Haiti Standard Time",
+ "America/Port_of_Spain": "SA Western Standard Time",
+ "America/Porto_Acre": "SA Pacific Standard Time",
+ "America/Porto_Velho": "SA Western Standard Time",
+ "America/Puerto_Rico": "SA Western Standard Time",
+ "America/Punta_Arenas": "Magallanes Standard Time",
+ "America/Rainy_River": "Central Standard Time",
+ "America/Rankin_Inlet": "Central Standard Time",
+ "America/Recife": "SA Eastern Standard Time",
+ "America/Regina": "Canada Central Standard Time",
+ "America/Resolute": "Central Standard Time",
+ "America/Rio_Branco": "SA Pacific Standard Time",
+ "America/Santa_Isabel": "Pacific Standard Time (Mexico)",
+ "America/Santarem": "SA Eastern Standard Time",
+ "America/Santiago": "Pacific SA Standard Time",
+ "America/Santo_Domingo": "SA Western Standard Time",
+ "America/Sao_Paulo": "E. South America Standard Time",
+ "America/Scoresbysund": "Azores Standard Time",
+ "America/Shiprock": "Mountain Standard Time",
+ "America/Sitka": "Alaskan Standard Time",
+ "America/St_Barthelemy": "SA Western Standard Time",
+ "America/St_Johns": "Newfoundland Standard Time",
+ "America/St_Kitts": "SA Western Standard Time",
+ "America/St_Lucia": "SA Western Standard Time",
+ "America/St_Thomas": "SA Western Standard Time",
+ "America/St_Vincent": "SA Western Standard Time",
+ "America/Swift_Current": "Canada Central Standard Time",
+ "America/Tegucigalpa": "Central America Standard Time",
+ "America/Thule": "Atlantic Standard Time",
+ "America/Thunder_Bay": "Eastern Standard Time",
+ "America/Tijuana": "Pacific Standard Time (Mexico)",
+ "America/Toronto": "Eastern Standard Time",
+ "America/Tortola": "SA Western Standard Time",
+ "America/Vancouver": "Pacific Standard Time",
+ "America/Virgin": "SA Western Standard Time",
+ "America/Whitehorse": "Pacific Standard Time",
+ "America/Winnipeg": "Central Standard Time",
+ "America/Yakutat": "Alaskan Standard Time",
+ "America/Yellowknife": "Mountain Standard Time",
+ "Antarctica/Casey": "W. Australia Standard Time",
+ "Antarctica/Davis": "SE Asia Standard Time",
+ "Antarctica/DumontDUrville": "West Pacific Standard Time",
+ "Antarctica/Macquarie": "Central Pacific Standard Time",
+ "Antarctica/Mawson": "West Asia Standard Time",
+ "Antarctica/McMurdo": "New Zealand Standard Time",
+ "Antarctica/Palmer": "Magallanes Standard Time",
+ "Antarctica/Rothera": "SA Eastern Standard Time",
+ "Antarctica/South_Pole": "New Zealand Standard Time",
+ "Antarctica/Syowa": "E. Africa Standard Time",
+ "Antarctica/Vostok": "Central Asia Standard Time",
+ "Arctic/Longyearbyen": "W. Europe Standard Time",
+ "Asia/Aden": "Arab Standard Time",
+ "Asia/Almaty": "Central Asia Standard Time",
+ "Asia/Amman": "Jordan Standard Time",
+ "Asia/Anadyr": "Russia Time Zone 11",
+ "Asia/Aqtau": "West Asia Standard Time",
+ "Asia/Aqtobe": "West Asia Standard Time",
+ "Asia/Ashgabat": "West Asia Standard Time",
+ "Asia/Ashkhabad": "West Asia Standard Time",
+ "Asia/Atyrau": "West Asia Standard Time",
+ "Asia/Baghdad": "Arabic Standard Time",
+ "Asia/Bahrain": "Arab Standard Time",
+ "Asia/Baku": "Azerbaijan Standard Time",
+ "Asia/Bangkok": "SE Asia Standard Time",
+ "Asia/Barnaul": "Altai Standard Time",
+ "Asia/Beirut": "Middle East Standard Time",
+ "Asia/Bishkek": "Central Asia Standard Time",
+ "Asia/Brunei": "Singapore Standard Time",
+ "Asia/Calcutta": "India Standard Time",
+ "Asia/Chita": "Transbaikal Standard Time",
+ "Asia/Choibalsan": "Ulaanbaatar Standard Time",
+ "Asia/Chongqing": "China Standard Time",
+ "Asia/Chungking": "China Standard Time",
+ "Asia/Colombo": "Sri Lanka Standard Time",
+ "Asia/Dacca": "Bangladesh Standard Time",
+ "Asia/Damascus": "Syria Standard Time",
+ "Asia/Dhaka": "Bangladesh Standard Time",
+ "Asia/Dili": "Tokyo Standard Time",
+ "Asia/Dubai": "Arabian Standard Time",
+ "Asia/Dushanbe": "West Asia Standard Time",
+ "Asia/Famagusta": "GTB Standard Time",
+ "Asia/Gaza": "West Bank Standard Time",
+ "Asia/Harbin": "China Standard Time",
+ "Asia/Hebron": "West Bank Standard Time",
+ "Asia/Hong_Kong": "China Standard Time",
+ "Asia/Hovd": "W. Mongolia Standard Time",
+ "Asia/Irkutsk": "North Asia East Standard Time",
+ "Asia/Jakarta": "SE Asia Standard Time",
+ "Asia/Jayapura": "Tokyo Standard Time",
+ "Asia/Jerusalem": "Israel Standard Time",
+ "Asia/Kabul": "Afghanistan Standard Time",
+ "Asia/Kamchatka": "Russia Time Zone 11",
+ "Asia/Karachi": "Pakistan Standard Time",
+ "Asia/Kashgar": "Central Asia Standard Time",
+ "Asia/Katmandu": "Nepal Standard Time",
+ "Asia/Khandyga": "Yakutsk Standard Time",
+ "Asia/Krasnoyarsk": "North Asia Standard Time",
+ "Asia/Kuala_Lumpur": "Singapore Standard Time",
+ "Asia/Kuching": "Singapore Standard Time",
+ "Asia/Kuwait": "Arab Standard Time",
+ "Asia/Macao": "China Standard Time",
+ "Asia/Macau": "China Standard Time",
+ "Asia/Magadan": "Magadan Standard Time",
+ "Asia/Makassar": "Singapore Standard Time",
+ "Asia/Manila": "Singapore Standard Time",
+ "Asia/Muscat": "Arabian Standard Time",
+ "Asia/Nicosia": "GTB Standard Time",
+ "Asia/Novokuznetsk": "North Asia Standard Time",
+ "Asia/Novosibirsk": "N. Central Asia Standard Time",
+ "Asia/Omsk": "Omsk Standard Time",
+ "Asia/Oral": "West Asia Standard Time",
+ "Asia/Phnom_Penh": "SE Asia Standard Time",
+ "Asia/Pontianak": "SE Asia Standard Time",
+ "Asia/Pyongyang": "North Korea Standard Time",
+ "Asia/Qatar": "Arab Standard Time",
+ "Asia/Qostanay": "Central Asia Standard Time",
+ "Asia/Qyzylorda": "West Asia Standard Time",
+ "Asia/Rangoon": "Myanmar Standard Time",
+ "Asia/Riyadh": "Arab Standard Time",
+ "Asia/Saigon": "SE Asia Standard Time",
+ "Asia/Sakhalin": "Sakhalin Standard Time",
+ "Asia/Samarkand": "West Asia Standard Time",
+ "Asia/Seoul": "Korea Standard Time",
+ "Asia/Shanghai": "China Standard Time",
+ "Asia/Singapore": "Singapore Standard Time",
+ "Asia/Srednekolymsk": "Russia Time Zone 10",
+ "Asia/Taipei": "Taipei Standard Time",
+ "Asia/Tashkent": "West Asia Standard Time",
+ "Asia/Tbilisi": "Georgian Standard Time",
+ "Asia/Tehran": "Iran Standard Time",
+ "Asia/Tel_Aviv": "Israel Standard Time",
+ "Asia/Thimbu": "Bangladesh Standard Time",
+ "Asia/Thimphu": "Bangladesh Standard Time",
+ "Asia/Tokyo": "Tokyo Standard Time",
+ "Asia/Tomsk": "Tomsk Standard Time",
+ "Asia/Ujung_Pandang": "Singapore Standard Time",
+ "Asia/Ulaanbaatar": "Ulaanbaatar Standard Time",
+ "Asia/Ulan_Bator": "Ulaanbaatar Standard Time",
+ "Asia/Urumqi": "Central Asia Standard Time",
+ "Asia/Ust-Nera": "Vladivostok Standard Time",
+ "Asia/Vientiane": "SE Asia Standard Time",
+ "Asia/Vladivostok": "Vladivostok Standard Time",
+ "Asia/Yakutsk": "Yakutsk Standard Time",
+ "Asia/Yekaterinburg": "Ekaterinburg Standard Time",
+ "Asia/Yerevan": "Caucasus Standard Time",
+ "Atlantic/Azores": "Azores Standard Time",
+ "Atlantic/Bermuda": "Atlantic Standard Time",
+ "Atlantic/Canary": "GMT Standard Time",
+ "Atlantic/Cape_Verde": "Cape Verde Standard Time",
+ "Atlantic/Faeroe": "GMT Standard Time",
+ "Atlantic/Jan_Mayen": "W. Europe Standard Time",
+ "Atlantic/Madeira": "GMT Standard Time",
+ "Atlantic/Reykjavik": "Greenwich Standard Time",
+ "Atlantic/South_Georgia": "UTC-02",
+ "Atlantic/St_Helena": "Greenwich Standard Time",
+ "Atlantic/Stanley": "SA Eastern Standard Time",
+ "Australia/ACT": "AUS Eastern Standard Time",
+ "Australia/Adelaide": "Cen. Australia Standard Time",
+ "Australia/Brisbane": "E. Australia Standard Time",
+ "Australia/Broken_Hill": "Cen. Australia Standard Time",
+ "Australia/Canberra": "AUS Eastern Standard Time",
+ "Australia/Currie": "Tasmania Standard Time",
+ "Australia/Darwin": "AUS Central Standard Time",
+ "Australia/Eucla": "Aus Central W. Standard Time",
+ "Australia/Hobart": "Tasmania Standard Time",
+ "Australia/LHI": "Lord Howe Standard Time",
+ "Australia/Lindeman": "E. Australia Standard Time",
+ "Australia/Lord_Howe": "Lord Howe Standard Time",
+ "Australia/Melbourne": "AUS Eastern Standard Time",
+ "Australia/NSW": "AUS Eastern Standard Time",
+ "Australia/North": "AUS Central Standard Time",
+ "Australia/Perth": "W. Australia Standard Time",
+ "Australia/Queensland": "E. Australia Standard Time",
+ "Australia/South": "Cen. Australia Standard Time",
+ "Australia/Sydney": "AUS Eastern Standard Time",
+ "Australia/Tasmania": "Tasmania Standard Time",
+ "Australia/Victoria": "AUS Eastern Standard Time",
+ "Australia/West": "W. Australia Standard Time",
+ "Australia/Yancowinna": "Cen. Australia Standard Time",
+ "Brazil/Acre": "SA Pacific Standard Time",
+ "Brazil/DeNoronha": "UTC-02",
+ "Brazil/East": "E. South America Standard Time",
+ "Brazil/West": "SA Western Standard Time",
+ "CST6CDT": "Central Standard Time",
+ "Canada/Atlantic": "Atlantic Standard Time",
+ "Canada/Central": "Central Standard Time",
+ "Canada/Eastern": "Eastern Standard Time",
+ "Canada/Mountain": "Mountain Standard Time",
+ "Canada/Newfoundland": "Newfoundland Standard Time",
+ "Canada/Pacific": "Pacific Standard Time",
+ "Canada/Saskatchewan": "Canada Central Standard Time",
+ "Canada/Yukon": "Pacific Standard Time",
+ "Chile/Continental": "Pacific SA Standard Time",
+ "Chile/EasterIsland": "Easter Island Standard Time",
+ "Cuba": "Cuba Standard Time",
+ "EST5EDT": "Eastern Standard Time",
+ "Egypt": "Egypt Standard Time",
+ "Eire": "GMT Standard Time",
+ "Etc/GMT": "UTC",
+ "Etc/GMT+1": "Cape Verde Standard Time",
+ "Etc/GMT+10": "Hawaiian Standard Time",
+ "Etc/GMT+11": "UTC-11",
+ "Etc/GMT+12": "Dateline Standard Time",
+ "Etc/GMT+2": "UTC-02",
+ "Etc/GMT+3": "SA Eastern Standard Time",
+ "Etc/GMT+4": "SA Western Standard Time",
+ "Etc/GMT+5": "SA Pacific Standard Time",
+ "Etc/GMT+6": "Central America Standard Time",
+ "Etc/GMT+7": "US Mountain Standard Time",
+ "Etc/GMT+8": "UTC-08",
+ "Etc/GMT+9": "UTC-09",
+ "Etc/GMT-1": "W. Central Africa Standard Time",
+ "Etc/GMT-10": "West Pacific Standard Time",
+ "Etc/GMT-11": "Central Pacific Standard Time",
+ "Etc/GMT-12": "UTC+12",
+ "Etc/GMT-13": "UTC+13",
+ "Etc/GMT-14": "Line Islands Standard Time",
+ "Etc/GMT-2": "South Africa Standard Time",
+ "Etc/GMT-3": "E. Africa Standard Time",
+ "Etc/GMT-4": "Arabian Standard Time",
+ "Etc/GMT-5": "West Asia Standard Time",
+ "Etc/GMT-6": "Central Asia Standard Time",
+ "Etc/GMT-7": "SE Asia Standard Time",
+ "Etc/GMT-8": "Singapore Standard Time",
+ "Etc/GMT-9": "Tokyo Standard Time",
+ "Etc/UCT": "UTC",
+ "Etc/UTC": "UTC",
+ "Europe/Amsterdam": "W. Europe Standard Time",
+ "Europe/Andorra": "W. Europe Standard Time",
+ "Europe/Astrakhan": "Astrakhan Standard Time",
+ "Europe/Athens": "GTB Standard Time",
+ "Europe/Belfast": "GMT Standard Time",
+ "Europe/Belgrade": "Central Europe Standard Time",
+ "Europe/Berlin": "W. Europe Standard Time",
+ "Europe/Bratislava": "Central Europe Standard Time",
+ "Europe/Brussels": "Romance Standard Time",
+ "Europe/Bucharest": "GTB Standard Time",
+ "Europe/Budapest": "Central Europe Standard Time",
+ "Europe/Busingen": "W. Europe Standard Time",
+ "Europe/Chisinau": "E. Europe Standard Time",
+ "Europe/Copenhagen": "Romance Standard Time",
+ "Europe/Dublin": "GMT Standard Time",
+ "Europe/Gibraltar": "W. Europe Standard Time",
+ "Europe/Guernsey": "GMT Standard Time",
+ "Europe/Helsinki": "FLE Standard Time",
+ "Europe/Isle_of_Man": "GMT Standard Time",
+ "Europe/Istanbul": "Turkey Standard Time",
+ "Europe/Jersey": "GMT Standard Time",
+ "Europe/Kaliningrad": "Kaliningrad Standard Time",
+ "Europe/Kiev": "FLE Standard Time",
+ "Europe/Kirov": "Russian Standard Time",
+ "Europe/Lisbon": "GMT Standard Time",
+ "Europe/Ljubljana": "Central Europe Standard Time",
+ "Europe/London": "GMT Standard Time",
+ "Europe/Luxembourg": "W. Europe Standard Time",
+ "Europe/Madrid": "Romance Standard Time",
+ "Europe/Malta": "W. Europe Standard Time",
+ "Europe/Mariehamn": "FLE Standard Time",
+ "Europe/Minsk": "Belarus Standard Time",
+ "Europe/Monaco": "W. Europe Standard Time",
+ "Europe/Moscow": "Russian Standard Time",
+ "Europe/Oslo": "W. Europe Standard Time",
+ "Europe/Paris": "Romance Standard Time",
+ "Europe/Podgorica": "Central Europe Standard Time",
+ "Europe/Prague": "Central Europe Standard Time",
+ "Europe/Riga": "FLE Standard Time",
+ "Europe/Rome": "W. Europe Standard Time",
+ "Europe/Samara": "Russia Time Zone 3",
+ "Europe/San_Marino": "W. Europe Standard Time",
+ "Europe/Sarajevo": "Central European Standard Time",
+ "Europe/Saratov": "Saratov Standard Time",
+ "Europe/Simferopol": "Russian Standard Time",
+ "Europe/Skopje": "Central European Standard Time",
+ "Europe/Sofia": "FLE Standard Time",
+ "Europe/Stockholm": "W. Europe Standard Time",
+ "Europe/Tallinn": "FLE Standard Time",
+ "Europe/Tirane": "Central Europe Standard Time",
+ "Europe/Tiraspol": "E. Europe Standard Time",
+ "Europe/Ulyanovsk": "Astrakhan Standard Time",
+ "Europe/Uzhgorod": "FLE Standard Time",
+ "Europe/Vaduz": "W. Europe Standard Time",
+ "Europe/Vatican": "W. Europe Standard Time",
+ "Europe/Vienna": "W. Europe Standard Time",
+ "Europe/Vilnius": "FLE Standard Time",
+ "Europe/Volgograd": "Russian Standard Time",
+ "Europe/Warsaw": "Central European Standard Time",
+ "Europe/Zagreb": "Central European Standard Time",
+ "Europe/Zaporozhye": "FLE Standard Time",
+ "Europe/Zurich": "W. Europe Standard Time",
+ "GB": "GMT Standard Time",
+ "GB-Eire": "GMT Standard Time",
+ "GMT+0": "UTC",
+ "GMT-0": "UTC",
+ "GMT0": "UTC",
+ "Greenwich": "UTC",
+ "Hongkong": "China Standard Time",
+ "Iceland": "Greenwich Standard Time",
+ "Indian/Antananarivo": "E. Africa Standard Time",
+ "Indian/Chagos": "Central Asia Standard Time",
+ "Indian/Christmas": "SE Asia Standard Time",
+ "Indian/Cocos": "Myanmar Standard Time",
+ "Indian/Comoro": "E. Africa Standard Time",
+ "Indian/Kerguelen": "West Asia Standard Time",
+ "Indian/Mahe": "Mauritius Standard Time",
+ "Indian/Maldives": "West Asia Standard Time",
+ "Indian/Mauritius": "Mauritius Standard Time",
+ "Indian/Mayotte": "E. Africa Standard Time",
+ "Indian/Reunion": "Mauritius Standard Time",
+ "Iran": "Iran Standard Time",
+ "Israel": "Israel Standard Time",
+ "Jamaica": "SA Pacific Standard Time",
+ "Japan": "Tokyo Standard Time",
+ "Kwajalein": "UTC+12",
+ "Libya": "Libya Standard Time",
+ "MST7MDT": "Mountain Standard Time",
+ "Mexico/BajaNorte": "Pacific Standard Time (Mexico)",
+ "Mexico/BajaSur": "Mountain Standard Time (Mexico)",
+ "Mexico/General": "Central Standard Time (Mexico)",
+ "NZ": "New Zealand Standard Time",
+ "NZ-CHAT": "Chatham Islands Standard Time",
+ "Navajo": "Mountain Standard Time",
+ "PRC": "China Standard Time",
+ "PST8PDT": "Pacific Standard Time",
+ "Pacific/Apia": "Samoa Standard Time",
+ "Pacific/Auckland": "New Zealand Standard Time",
+ "Pacific/Bougainville": "Bougainville Standard Time",
+ "Pacific/Chatham": "Chatham Islands Standard Time",
+ "Pacific/Easter": "Easter Island Standard Time",
+ "Pacific/Efate": "Central Pacific Standard Time",
+ "Pacific/Enderbury": "UTC+13",
+ "Pacific/Fakaofo": "UTC+13",
+ "Pacific/Fiji": "Fiji Standard Time",
+ "Pacific/Funafuti": "UTC+12",
+ "Pacific/Galapagos": "Central America Standard Time",
+ "Pacific/Gambier": "UTC-09",
+ "Pacific/Guadalcanal": "Central Pacific Standard Time",
+ "Pacific/Guam": "West Pacific Standard Time",
+ "Pacific/Honolulu": "Hawaiian Standard Time",
+ "Pacific/Johnston": "Hawaiian Standard Time",
+ "Pacific/Kiritimati": "Line Islands Standard Time",
+ "Pacific/Kosrae": "Central Pacific Standard Time",
+ "Pacific/Kwajalein": "UTC+12",
+ "Pacific/Majuro": "UTC+12",
+ "Pacific/Marquesas": "Marquesas Standard Time",
+ "Pacific/Midway": "UTC-11",
+ "Pacific/Nauru": "UTC+12",
+ "Pacific/Niue": "UTC-11",
+ "Pacific/Norfolk": "Norfolk Standard Time",
+ "Pacific/Noumea": "Central Pacific Standard Time",
+ "Pacific/Pago_Pago": "UTC-11",
+ "Pacific/Palau": "Tokyo Standard Time",
+ "Pacific/Pitcairn": "UTC-08",
+ "Pacific/Ponape": "Central Pacific Standard Time",
+ "Pacific/Port_Moresby": "West Pacific Standard Time",
+ "Pacific/Rarotonga": "Hawaiian Standard Time",
+ "Pacific/Saipan": "West Pacific Standard Time",
+ "Pacific/Samoa": "UTC-11",
+ "Pacific/Tahiti": "Hawaiian Standard Time",
+ "Pacific/Tarawa": "UTC+12",
+ "Pacific/Tongatapu": "Tonga Standard Time",
+ "Pacific/Truk": "West Pacific Standard Time",
+ "Pacific/Wake": "UTC+12",
+ "Pacific/Wallis": "UTC+12",
+ "Poland": "Central European Standard Time",
+ "Portugal": "GMT Standard Time",
+ "ROC": "Taipei Standard Time",
+ "ROK": "Korea Standard Time",
+ "Singapore": "Singapore Standard Time",
+ "Turkey": "Turkey Standard Time",
+ "UCT": "UTC",
+ "US/Alaska": "Alaskan Standard Time",
+ "US/Aleutian": "Aleutian Standard Time",
+ "US/Arizona": "US Mountain Standard Time",
+ "US/Central": "Central Standard Time",
+ "US/Eastern": "Eastern Standard Time",
+ "US/Hawaii": "Hawaiian Standard Time",
+ "US/Indiana-Starke": "Central Standard Time",
+ "US/Michigan": "Eastern Standard Time",
+ "US/Mountain": "Mountain Standard Time",
+ "US/Pacific": "Pacific Standard Time",
+ "US/Samoa": "UTC-11",
+ "UTC": "UTC",
+ "Universal": "UTC",
+ "W-SU": "Russian Standard Time",
+ "Zulu": "UTC",
}
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
index 4b4f956..f6c1def 100644
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,16 +26,21 @@
"""Nikola plugin categories."""
-from __future__ import absolute_import
-import sys
-import os
-import re
import io
+import logging
+import os
+import sys
+import typing
-from yapsy.IPlugin import IPlugin
+import doit
from doit.cmd_base import Command as DoitCommand
+from yapsy.IPlugin import IPlugin
+
+from .utils import LOGGER, first_line, get_logger, req_missing
-from .utils import LOGGER, first_line
+if typing.TYPE_CHECKING:
+ import nikola
+ import nikola.post
__all__ = (
'Command',
@@ -43,22 +48,29 @@ __all__ = (
'PageCompiler',
'RestExtension',
'MarkdownExtension',
+ 'MetadataExtractor',
'Task',
'TaskMultiplier',
'TemplateSystem',
'SignalHandler',
'ConfigPlugin',
'PostScanner',
+ 'Taxonomy',
)
class BasePlugin(IPlugin):
"""Base plugin class."""
+ logger = None
+
def set_site(self, site):
"""Set site, which is a Nikola instance."""
self.site = site
self.inject_templates()
+ self.logger = get_logger(self.name)
+ if not site.debug:
+ self.logger.level = logging.INFO
def inject_templates(self):
"""Inject 'templates/<engine>' (if exists) very early in the theme chain."""
@@ -90,10 +102,14 @@ class BasePlugin(IPlugin):
class PostScanner(BasePlugin):
"""The scan method of these plugins is called by Nikola.scan_posts."""
- def scan(self):
+ def scan(self) -> 'typing.List[nikola.post.Post]':
"""Create a list of posts from some source. Returns a list of Post objects."""
raise NotImplementedError()
+ def supported_extensions(self) -> 'typing.Optional[typing.List]':
+ """Return a list of supported file extensions, or None if such a list isn't known beforehand."""
+ return None
+
class Command(BasePlugin, DoitCommand):
"""Doit command implementation."""
@@ -118,7 +134,7 @@ class Command(BasePlugin, DoitCommand):
DoitCommand.__init__(self, config, **kwargs)
return self
- def execute(self, options=None, args=None):
+ def execute(self, options=None, args=None) -> int:
"""Check if the command can run in the current environment, fail if needed, or call _execute."""
options = options or {}
args = args or []
@@ -128,7 +144,7 @@ class Command(BasePlugin, DoitCommand):
return False
return self._execute(options, args)
- def _execute(self, options, args):
+ def _execute(self, options, args) -> int:
"""Do whatever this command does.
@param options (dict) with values from cmd_options
@@ -154,7 +170,10 @@ def help(self):
text.append(self.doc_description)
return "\n".join(text)
-DoitCommand.help = help
+
+# we need to patch DoitCommand.help with doit <0.31.0
+if doit.__version__ < (0, 31, 0):
+ DoitCommand.help = help
class BaseTask(BasePlugin):
@@ -166,11 +185,11 @@ class BaseTask(BasePlugin):
# the others have to be specifie in the command line.
is_default = True
- def gen_tasks(self):
+ def gen_tasks(self) -> 'typing.List[dict]':
"""Generate tasks."""
raise NotImplementedError()
- def group_task(self):
+ def group_task(self) -> dict:
"""Return dict for group task."""
return {
'basename': self.name,
@@ -196,23 +215,23 @@ class TemplateSystem(BasePlugin):
name = "dummy_templates"
- def set_directories(self, directories, cache_folder):
+ def set_directories(self, directories: 'typing.List[str]', cache_folder: str):
"""Set the list of folders where templates are located and cache."""
raise NotImplementedError()
- def template_deps(self, template_name):
+ def template_deps(self, template_name: str):
"""Return filenames which are dependencies for a template."""
raise NotImplementedError()
- def get_deps(self, filename):
+ def get_deps(self, filename: str):
"""Return paths to dependencies for the template loaded from filename."""
raise NotImplementedError()
- def get_string_deps(self, text):
+ def get_string_deps(self, text: str):
"""Find dependencies for a template string."""
raise NotImplementedError()
- def render_template(self, template_name, output_name, context):
+ def render_template(self, template_name: str, output_name: str, context: 'typing.Dict[str, str]'):
"""Render template to a file using context.
This must save the data to output_name *and* return it
@@ -220,15 +239,15 @@ class TemplateSystem(BasePlugin):
"""
raise NotImplementedError()
- def render_template_to_string(self, template, context):
+ def render_template_to_string(self, template: str, context: 'typing.Dict[str, str]') -> str:
"""Render template to a string using context."""
raise NotImplementedError()
- def inject_directory(self, directory):
+ def inject_directory(self, directory: str):
"""Inject the directory with the lowest priority in the template search mechanism."""
raise NotImplementedError()
- def get_template_path(self, template_name):
+ def get_template_path(self, template_name: str) -> str:
"""Get the path to a template or return None."""
raise NotImplementedError()
@@ -238,7 +257,7 @@ class TaskMultiplier(BasePlugin):
name = "dummy multiplier"
- def process(self, task):
+ def process(self, task) -> list:
"""Examine task and create more tasks. Returns extra tasks only."""
return []
@@ -250,6 +269,9 @@ class PageCompiler(BasePlugin):
friendly_name = ''
demote_headers = False
supports_onefile = True
+ use_dep_file = True # If set to false, the .dep file is never written and not automatically added as a target
+ supports_metadata = False
+ metadata_conditions = []
default_metadata = {
'title': '',
'slug': '',
@@ -262,48 +284,75 @@ class PageCompiler(BasePlugin):
}
config_dependencies = []
- def _read_extra_deps(self, post):
+ def get_dep_filename(self, post: 'nikola.post.Post', lang: str) -> str:
+ """Return the .dep file's name for the given post and language."""
+ return post.translated_base_path(lang) + '.dep'
+
+ def _read_extra_deps(self, post: 'nikola.post.Post', lang: str) -> 'typing.List[str]':
"""Read contents of .dep file and return them as a list."""
- dep_path = post.base_path + '.dep'
+ dep_path = self.get_dep_filename(post, lang)
if os.path.isfile(dep_path):
- with io.open(dep_path, 'r+', encoding='utf8') as depf:
+ with io.open(dep_path, 'r+', encoding='utf-8-sig') as depf:
deps = [l.strip() for l in depf.readlines()]
return deps
return []
- def register_extra_dependencies(self, post):
+ def register_extra_dependencies(self, post: 'nikola.post.Post'):
"""Add dependency to post object to check .dep file."""
- post.add_dependency(lambda: self._read_extra_deps(post), 'fragment')
+ def create_lambda(lang: str) -> 'typing.Callable':
+ # We create a lambda like this so we can pass `lang` to it, because if we didn’t
+ # add that function, `lang` would always be the last language in TRANSLATIONS.
+ # (See http://docs.python-guide.org/en/latest/writing/gotchas/#late-binding-closures)
+ return lambda: self._read_extra_deps(post, lang)
+
+ for lang in self.site.config['TRANSLATIONS']:
+ post.add_dependency(create_lambda(lang), 'fragment', lang=lang)
+
+ def get_extra_targets(self, post: 'nikola.post.Post', lang: str, dest: str) -> 'typing.List[str]':
+ """Return a list of extra targets for the render_posts task when compiling the post for the specified language."""
+ if self.use_dep_file:
+ return [self.get_dep_filename(post, lang)]
+ else:
+ return []
+
+ def compile(self, source: str, dest: str, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
+ raise NotImplementedError()
- def compile_html(self, source, dest, is_two_file=False):
- """Compile the source, save it on dest."""
+ def compile_string(self, data: str, source_path=None, is_two_file=True, post=None, lang=None) -> str:
+ """Compile the source file into HTML strings (with shortcode support).
+
+ Returns a tuple of at least two elements: HTML string [0] and shortcode dependencies [last].
+ """
+ # This function used to have some different APIs in different places.
raise NotImplementedError()
- def create_post(self, path, content=None, onefile=False, is_page=False, **kw):
+ def create_post(self, path: str, content=None, onefile=False, is_page=False, **kw):
"""Create post file with optional metadata."""
raise NotImplementedError()
- def extension(self):
- """The preferred extension for the output of this compiler."""
+ def extension(self) -> str:
+ """Return the preferred extension for the output of this compiler."""
return ".html"
- def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
+ def read_metadata(self, post: 'nikola.post.Post', lang=None) -> 'typing.Dict[str, str]':
"""Read the metadata from a post, and return a metadata dict."""
return {}
- def split_metadata(self, data):
- """Split data from metadata in the raw post content.
+ def split_metadata(self, data: str, post=None, lang=None) -> (str, str):
+ """Split data from metadata in the raw post content."""
+ if lang and post:
+ extractor = post.used_extractor[lang]
+ else:
+ import nikola.metadata_extractors
+ extractor = nikola.metadata_extractors.DEFAULT_EXTRACTOR
- This splits in the first empty line that is NOT at the beginning
- of the document.
- """
- split_result = re.split('(\n\n|\r\n\r\n)', data.lstrip(), maxsplit=1)
- if len(split_result) == 1:
- return '', split_result[0]
- # ['metadata', '\n\n', 'post content']
- return split_result[0], split_result[-1]
+ if isinstance(extractor, MetadataExtractor):
+ return extractor.split_metadata_from_text(data)
+ # Ouch!
+ return data, data
- def get_compiler_extensions(self):
+ def get_compiler_extensions(self) -> list:
"""Activate all the compiler extension plugins for a given compiler and return them."""
plugins = []
for plugin_info in self.site.compiler_extensions:
@@ -344,6 +393,73 @@ class MarkdownExtension(CompilerExtension):
compiler_name = "markdown"
+class MetadataExtractor(BasePlugin):
+ """Plugins that can extract meta information from post files."""
+
+ # Name of the extractor. (required)
+ name = "unknown"
+ # Where to get metadata from. (MetaSource; required)
+ source = None
+ # Priority of extractor. (MetaPriority; required)
+ priority = None
+ # List of tuples (MetaCondition, arg) with conditions used to select this extractor.
+ conditions = []
+ # Regular expression used for splitting metadata, or None if not applicable.
+ split_metadata_re = None
+ # List of tuples (import name, pip name, friendly name) of Python packages required for this extractor.
+ requirements = []
+ # Name of METADATA_MAPPING to use, if any.
+ map_from = None
+ # Whether or not the extractor supports writing metadata.
+ supports_write = False
+
+ def _extract_metadata_from_text(self, source_text: str) -> 'typing.Dict[str, str]':
+ """Extract metadata from text."""
+ raise NotImplementedError()
+
+ def split_metadata_from_text(self, source_text: str) -> (str, str):
+ """Split text into metadata and content (both strings)."""
+ if self.split_metadata_re is None:
+ return source_text
+ else:
+ split_result = self.split_metadata_re.split(source_text.lstrip(), maxsplit=1)
+ if len(split_result) == 1:
+ return split_result[0], split_result[0]
+ else:
+ # Necessary?
+ return split_result[0], split_result[-1]
+
+ def extract_text(self, source_text: str) -> 'typing.Dict[str, str]':
+ """Split file, return metadata and the content."""
+ split = self.split_metadata_from_text(source_text)
+ if not split:
+ return {}
+ meta = self._extract_metadata_from_text(split[0])
+ return meta
+
+ def extract_filename(self, filename: str, lang: str) -> 'typing.Dict[str, str]':
+ """Extract metadata from filename."""
+ return {}
+
+ def write_metadata(self, metadata: 'typing.Dict[str, str]', comment_wrap=False) -> str:
+ """Write metadata in this extractor’s format.
+
+ ``comment_wrap`` is either True, False, or a 2-tuple of comments to use for wrapping, if necessary.
+ If it’s set to True, defaulting to ``('<!--', '-->')`` is recommended.
+
+ This function should insert comment markers (if applicable) and must insert trailing newlines.
+ """
+ raise NotImplementedError()
+
+ def check_requirements(self):
+ """Check if requirements for an extractor are satisfied."""
+ for import_name, pip_name, friendly_name in self.requirements:
+ try:
+ __import__(import_name)
+ except ImportError:
+ req_missing([pip_name], "use {0} metadata".format(friendly_name), python=True, optional=False)
+
+
class SignalHandler(BasePlugin):
"""Signal handlers."""
@@ -361,6 +477,12 @@ class ShortcodePlugin(BasePlugin):
name = "dummy_shortcode_plugin"
+ def set_site(self, site):
+ """Set Nikola site."""
+ self.site = site
+ site.register_shortcode(self.name, self.handler)
+ return super().set_site(site)
+
class Importer(Command):
"""Basic structure for importing data into Nikola.
@@ -393,7 +515,7 @@ class Importer(Command):
"""Import the data into Nikola."""
raise NotImplementedError()
- def generate_base_site(self, path):
+ def generate_base_site(self, path: str):
"""Create the base site."""
raise NotImplementedError()
@@ -443,3 +565,333 @@ class Importer(Command):
def save_post(self):
"""Save a post to disk."""
raise NotImplementedError()
+
+
+class Taxonomy(BasePlugin):
+ """Taxonomy for posts.
+
+ A taxonomy plugin allows to classify posts (see #2107) by
+ classification strings. Classification plugins must adjust
+ a set of options to determine certain aspects.
+
+ The following options are class attributes with their default
+ values. These variables should be set in the class definition,
+ in the constructor or latest in the `set_site` function.
+
+ classification_name = "taxonomy":
+ The classification name to be used for path handlers.
+ Must be overridden!
+
+ overview_page_items_variable_name = "items":
+ When rendering the overview page, its template will have a list
+ of pairs
+ (friendly_name, link)
+ for the classifications available in a variable by this name.
+
+ The template will also have a list
+ (friendly_name, link, post_count)
+ for the classifications available in a variable by the name
+ `overview_page_items_variable_name + '_with_postcount'`.
+
+ overview_page_variable_name = "taxonomy":
+ When rendering the overview page, its template will have a list
+ of classifications available in a variable by this name.
+
+ overview_page_hierarchy_variable_name = "taxonomy_hierarchy":
+ When rendering the overview page, its template will have a list
+ of tuples
+ (friendly_name, classification, classification_path, link,
+ indent_levels, indent_change_before, indent_change_after)
+ available in a variable by this name. These tuples can be used
+ to render the hierarchy as a tree.
+
+ The template will also have a list
+ (friendly_name, classification, classification_path, link,
+ indent_levels, indent_change_before, indent_change_after,
+ number_of_children, post_count)
+ available in the variable by the name
+ `overview_page_hierarchy_variable_name + '_with_postcount'`.
+
+ more_than_one_classifications_per_post = False:
+ If True, there can be more than one classification per post; in that case,
+ the classification data in the metadata is stored as a list. If False,
+ the classification data in the metadata is stored as a string, or None
+ when no classification is given.
+
+ has_hierarchy = False:
+ Whether the classification has a hierarchy.
+
+ include_posts_from_subhierarchies = False:
+ If True, the post list for a classification includes all posts with a
+ sub-classification (in case has_hierarchy is True).
+
+ include_posts_into_hierarchy_root = False:
+ If True, include_posts_from_subhierarchies == True will also insert
+ posts into the post list for the empty hierarchy [].
+
+ show_list_as_subcategories_list = False:
+ If True, for every classification which has at least one
+ subclassification, create a list of subcategories instead of a list/index
+ of posts. This is only used when has_hierarchy = True. The template
+ specified in subcategories_list_template will be used. If this is set
+ to True, it is recommended to set include_posts_from_subhierarchies to
+ True to get correct post counts.
+
+ show_list_as_index = False:
+ Whether to show the posts for one classification as an index or
+ as a post list.
+
+ subcategories_list_template = "taxonomy_list.tmpl":
+ The template to use for the subcategories list when
+ show_list_as_subcategories_list is True.
+
+ template_for_single_list = "tagindex.tmpl":
+ The template to use for the post list for one classification.
+
+ template_for_classification_overview = "list.tmpl":
+ The template to use for the classification overview page.
+ Set to None to avoid generating overviews.
+
+ always_disable_atom = False:
+ Whether to always disable Atom feed generation.
+
+ always_disable_rss = False:
+ Whether to always disable RSS feed generation.
+
+ apply_to_posts = True:
+ Whether this classification applies to posts.
+
+ apply_to_pages = False:
+ Whether this classification applies to pages.
+
+ minimum_post_count_per_classification_in_overview = 1:
+ The minimum number of posts a classification must have to be listed in
+ the overview.
+
+ omit_empty_classifications = False:
+ Whether post lists resp. indexes should be created for empty
+ classifications.
+
+ add_other_languages_variable = False:
+ In case this is `True`, each classification page will get a list
+ of triples `(other_lang, other_classification, title)` of classifications
+ in other languages which should be linked. The list will be stored in the
+ variable `other_languages`.
+
+ path_handler_docstrings:
+ A dictionary of docstrings for path handlers. See eg. nikola.py for
+ examples. Must be overridden, keys are "taxonomy_index", "taxonomy",
+ "taxonomy_atom", "taxonomy_rss" (but using classification_name instead
+ of "taxonomy"). If one of the values is False, the corresponding path
+ handler will not be created.
+ """
+
+ name = "dummy_taxonomy"
+
+ # Adjust the following values in your plugin!
+ classification_name = "taxonomy"
+ overview_page_variable_name = "taxonomy"
+ overview_page_items_variable_name = "items"
+ overview_page_hierarchy_variable_name = "taxonomy_hierarchy"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = False
+ include_posts_from_subhierarchies = False
+ include_posts_into_hierarchy_root = False
+ show_list_as_subcategories_list = False
+ show_list_as_index = False
+ subcategories_list_template = "taxonomy_list.tmpl"
+ template_for_single_list = "tagindex.tmpl"
+ template_for_classification_overview = "list.tmpl"
+ always_disable_atom = False
+ always_disable_rss = False
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = False
+ add_other_languages_variable = False
+ path_handler_docstrings = {
+ 'taxonomy_index': '',
+ 'taxonomy': '',
+ 'taxonomy_atom': '',
+ 'taxonomy_rss': '',
+ }
+
+ def is_enabled(self, lang=None) -> bool:
+ """Return True if this taxonomy is enabled, or False otherwise.
+
+ If lang is None, this determins whether the classification is
+ made at all. If lang is not None, this determines whether the
+ overview page and the classification lists are created for this
+ language.
+ """
+ return True
+
+ def get_implicit_classifications(self, lang: str) -> 'typing.List[str]':
+ """Return a list of classification strings which should always appear in posts_per_classification."""
+ return []
+
+ def classify(self, post: 'nikola.post.Post', lang: str) -> 'typing.Iterable[str]':
+ """Classify the given post for the given language.
+
+ Must return a list or tuple of strings.
+ """
+ raise NotImplementedError()
+
+ def sort_posts(self, posts: 'typing.List[nikola.post.Post]', classification: str, lang: str):
+ """Sort the given list of posts.
+
+ Allows the plugin to order the posts per classification as it wants.
+ The posts will be ordered by date (latest first) before calling
+ this function. This function must sort in-place.
+ """
+ pass
+
+ def sort_classifications(self, classifications: 'typing.List[str]', lang: str, level=None):
+ """Sort the given list of classification strings.
+
+ Allows the plugin to order the classifications as it wants. The
+ classifications will be ordered by `natsort` before calling this
+ function. This function must sort in-place.
+
+ For hierarchical taxonomies, the elements of the list are a single
+ path element of the path returned by `extract_hierarchy()`. The index
+ of the path element in the path will be provided in `level`.
+ """
+ pass
+
+ def get_classification_friendly_name(self, classification: str, lang: str, only_last_component=False) -> str:
+ """Extract a friendly name from the classification.
+
+ The result of this function is usually displayed to the user, instead
+ of using the classification string.
+
+ The argument `only_last_component` is only relevant to hierarchical
+ taxonomies. If it is set, the printable name should only describe the
+ last component of `classification` if possible.
+ """
+ raise NotImplementedError()
+
+ def get_overview_path(self, lang: str, dest_type='page') -> str:
+ """Return path for classification overview.
+
+ This path handler for the classification overview must return one or
+ two values (in this order):
+ * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY;
+ * a string with values 'auto', 'always' or 'never', indicating whether
+ INDEX_FILE should be added or not.
+
+ Note that this function must always return a list or tuple of strings;
+ the other return value is optional with default value `'auto'`.
+
+ In case INDEX_FILE should potentially be added, the last element in the
+ returned path must have no extension, and the PRETTY_URLS config must
+ be ignored by this handler. The return value will be modified based on
+ the PRETTY_URLS and INDEX_FILE settings.
+
+ `dest_type` can be either 'page', 'feed' (for Atom feed) or 'rss'.
+ """
+ raise NotImplementedError()
+
+ def get_path(self, classification: str, lang: str, dest_type='page') -> str:
+ """Return path to the classification page.
+
+ This path handler for the given classification must return one to
+ three values (in this order):
+ * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY;
+ * a string with values 'auto', 'always' or 'never', indicating whether
+ INDEX_FILE should be added or not;
+ * an integer if a specific page of the index is to be targeted (will be
+ ignored for post lists), or `None` if the most current page is targeted.
+
+ Note that this function must always return a list or tuple of strings;
+ the other two return values are optional with default values `'auto'` and
+ `None`.
+
+ In case INDEX_FILE should potentially be added, the last element in the
+ returned path must have no extension, and the PRETTY_URLS config must
+ be ignored by this handler. The return value will be modified based on
+ the PRETTY_URLS and INDEX_FILE settings.
+
+ `dest_type` can be either 'page', 'feed' (for Atom feed) or 'rss'.
+
+ For hierarchical taxonomies, the result of extract_hierarchy is provided
+ as `classification`. For non-hierarchical taxonomies, the classification
+ string itself is provided as `classification`.
+ """
+ raise NotImplementedError()
+
+ def extract_hierarchy(self, classification: str) -> 'typing.List[str]':
+ """Given a classification, return a list of parts in the hierarchy.
+
+ For non-hierarchical taxonomies, it usually suffices to return
+ `[classification]`.
+ """
+ return [classification]
+
+ def recombine_classification_from_hierarchy(self, hierarchy: 'typing.List[str]') -> str:
+ """Given a list of parts in the hierarchy, return the classification string.
+
+ For non-hierarchical taxonomies, it usually suffices to return hierarchy[0].
+ """
+ return hierarchy[0]
+
+ def provide_overview_context_and_uptodate(self, lang: str) -> str:
+ """Provide data for the context and the uptodate list for the classification overview.
+
+ Must return a tuple of two dicts. The first is merged into the page's context,
+ the second will be put into the uptodate list of all generated tasks.
+
+ Context must contain `title`.
+ """
+ raise NotImplementedError()
+
+ def provide_context_and_uptodate(self, classification: str, lang: str, node=None) -> 'typing.Tuple[typing.Dict]':
+ """Provide data for the context and the uptodate list for the list of the given classification.
+
+ Must return a tuple of two dicts. The first is merged into the page's context,
+ the second will be put into the uptodate list of all generated tasks.
+
+ For hierarchical taxonomies, node is the `hierarchy_utils.TreeNode` element
+ corresponding to the classification.
+
+ Context must contain `title`, which should be something like 'Posts about <classification>'.
+ """
+ raise NotImplementedError()
+
+ def should_generate_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool:
+ """Only generates list of posts for classification if this function returns True."""
+ return True
+
+ def should_generate_atom_for_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool:
+ """Only generates Atom feed for list of posts for classification if this function returns True."""
+ return self.should_generate_classification_page(classification, post_list, lang)
+
+ def should_generate_rss_for_classification_page(self, classification: str, post_list: 'typing.List[nikola.post.Post]', lang: str) -> bool:
+ """Only generates RSS feed for list of posts for classification if this function returns True."""
+ return self.should_generate_classification_page(classification, post_list, lang)
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language: 'typing.List[nikola.post.Post]', flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None) -> 'typing.List[nikola.post.Post]':
+ """Rearrange, modify or otherwise use the list of posts per classification and per language.
+
+ For compatibility reasons, the list could be stored somewhere else as well.
+
+ In case `has_hierarchy` is `True`, `flat_hierarchy_per_lang` is the flat
+ hierarchy consisting of `hierarchy_utils.TreeNode` elements, and
+ `hierarchy_lookup_per_lang` is the corresponding hierarchy lookup mapping
+ classification strings to `hierarchy_utils.TreeNode` objects.
+ """
+ pass
+
+ def get_other_language_variants(self, classification: str, lang: str, classifications_per_language: 'typing.List[str]') -> 'typing.List[str]':
+ """Return a list of variants of the same classification in other languages.
+
+ Given a `classification` in a language `lang`, return a list of pairs
+ `(other_lang, other_classification)` with `lang != other_lang` such that
+ `classification` should be linked to `other_classification`.
+
+ Classifications where links to other language versions makes no sense
+ should simply return an empty list.
+
+ Provided is a set of classifications per language (`classifications_per_language`).
+ """
+ return []
diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py
index b83f43f..70c8c0d 100644
--- a/nikola/plugins/__init__.py
+++ b/nikola/plugins/__init__.py
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
"""Plugins for Nikola."""
-
-from __future__ import absolute_import
diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py
index cf98ebc..3e6e21e 100644
--- a/nikola/plugins/basic_import.py
+++ b/nikola/plugins/basic_import.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,21 +26,15 @@
"""Mixin for importer plugins."""
-from __future__ import unicode_literals, print_function
import io
import csv
import datetime
import os
-import sys
-from pkg_resources import resource_filename
-
-try:
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import urlparse # NOQA
+from urllib.parse import urlparse
from lxml import etree, html
from mako.template import Template
+from pkg_resources import resource_filename
from nikola import utils
@@ -90,7 +84,7 @@ class ImportMixin(object):
src = (urlparse(k).path + 'index.html')[1:]
dst = (urlparse(v).path)
if src == index:
- utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k))
+ utils.LOGGER.warning("Can't do a redirect for: {0!r}".format(k))
else:
redirections.append((src, dst))
return redirections
@@ -101,8 +95,8 @@ class ImportMixin(object):
os.system('nikola init -q ' + self.output_folder)
else:
self.import_into_existing_site = True
- utils.LOGGER.notice('The folder {0} already exists - assuming that this is a '
- 'already existing Nikola site.'.format(self.output_folder))
+ utils.LOGGER.warning('The folder {0} already exists - assuming that this is a '
+ 'already existing Nikola site.'.format(self.output_folder))
filename = resource_filename('nikola', 'conf.py.in')
# The 'strict_undefined=True' will give the missing symbol name if any,
@@ -150,7 +144,7 @@ class ImportMixin(object):
content = html.tostring(doc, encoding='utf8')
except etree.ParserError:
pass
- if isinstance(content, utils.bytes_str):
+ if isinstance(content, bytes):
content = content.decode('utf-8')
compiler.create_post(
filename,
@@ -158,8 +152,7 @@ class ImportMixin(object):
onefile=True,
**headers)
- @staticmethod
- def write_metadata(filename, title, slug, post_date, description, tags, **kwargs):
+ def write_metadata(self, filename, title, slug, post_date, description, tags, **kwargs):
"""Write metadata to meta file."""
if not description:
description = ""
@@ -168,13 +161,13 @@ class ImportMixin(object):
with io.open(filename, "w+", encoding="utf8") as fd:
data = {'title': title, 'slug': slug, 'date': post_date, 'tags': ','.join(tags), 'description': description}
data.update(kwargs)
- fd.write(utils.write_metadata(data))
+ fd.write(utils.write_metadata(data, site=self.site, comment_wrap=False))
@staticmethod
def write_urlmap_csv(output_file, url_map):
"""Write urlmap to csv file."""
utils.makedirs(os.path.dirname(output_file))
- fmode = 'wb+' if sys.version_info[0] == 2 else 'w+'
+ fmode = 'w+'
with io.open(output_file, fmode) as fd:
csv_writer = csv.writer(fd)
for item in url_map.items():
diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py
index 62d7086..cdd1560 100644
--- a/nikola/plugins/command/__init__.py
+++ b/nikola/plugins/command/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin
index 1081c78..a847e14 100644
--- a/nikola/plugins/command/auto.plugin
+++ b/nikola/plugins/command/auto.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Automatically detect site changes, rebuild and optionally refresh a browser.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/auto/__init__.py b/nikola/plugins/command/auto/__init__.py
index a82dc3e..6bedcac 100644
--- a/nikola/plugins/command/auto/__init__.py
+++ b/nikola/plugins/command/auto/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,66 +26,55 @@
"""Automatic rebuilds for Nikola."""
-from __future__ import print_function
-
-import json
+import asyncio
+import datetime
import mimetypes
import os
import re
+import stat
import subprocess
import sys
-import time
-try:
- from urlparse import urlparse
- from urllib2 import unquote
-except ImportError:
- from urllib.parse import urlparse, unquote # NOQA
+import typing
import webbrowser
-from wsgiref.simple_server import make_server
-import wsgiref.util
+
import pkg_resources
-from blinker import signal
+from nikola.plugin_categories import Command
+from nikola.utils import dns_sd, req_missing, get_theme_path, makedirs
+
try:
- from ws4py.websocket import WebSocket
- from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler, WebSocketWSGIHandler
- from ws4py.server.wsgiutils import WebSocketWSGIApplication
- from ws4py.messaging import TextMessage
+ import aiohttp
+ from aiohttp import web
+ from aiohttp.web_urldispatcher import StaticResource
+ from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden, HTTPMovedPermanently
+ from aiohttp.web_response import Response
+ from aiohttp.web_fileresponse import FileResponse
except ImportError:
- WebSocket = object
+ aiohttp = web = None
+ StaticResource = HTTPNotFound = HTTPForbidden = Response = FileResponse = object
+
try:
- import watchdog
from watchdog.observers import Observer
- from watchdog.events import FileSystemEventHandler, PatternMatchingEventHandler
except ImportError:
- watchdog = None
- FileSystemEventHandler = object
- PatternMatchingEventHandler = object
+ Observer = None
-from nikola.plugin_categories import Command
-from nikola.utils import dns_sd, req_missing, get_logger, get_theme_path, STDERR_HANDLER
LRJS_PATH = os.path.join(os.path.dirname(__file__), 'livereload.js')
-error_signal = signal('error')
-refresh_signal = signal('refresh')
+REBUILDING_REFRESH_DELAY = 0.35
+IDLE_REFRESH_DELAY = 0.05
-ERROR_N = '''<html>
-<head>
-</head>
-<boody>
-ERROR {}
-</body>
-</html>
-'''
+if sys.platform == 'win32':
+ asyncio.set_event_loop(asyncio.ProactorEventLoop())
class CommandAuto(Command):
"""Automatic rebuilds for Nikola."""
name = "auto"
- logger = None
has_server = True
doc_purpose = "builds and serves a site; automatically detects site changes, rebuilds, and optionally refreshes a browser"
dns_sd = None
+ delta_last_rebuild = datetime.timedelta(milliseconds=100)
+ web_runner = None # type: web.AppRunner
cmd_options = [
{
@@ -94,7 +83,7 @@ class CommandAuto(Command):
'long': 'port',
'default': 8000,
'type': int,
- 'help': 'Port nummber (default: 8000)',
+ 'help': 'Port number',
},
{
'name': 'address',
@@ -102,7 +91,7 @@ class CommandAuto(Command):
'long': 'address',
'type': str,
'default': '127.0.0.1',
- 'help': 'Address to bind (default: 127.0.0.1 -- localhost)',
+ 'help': 'Address to bind',
},
{
'name': 'browser',
@@ -127,26 +116,50 @@ class CommandAuto(Command):
'type': bool,
'help': 'Disable the server, automate rebuilds only'
},
+ {
+ 'name': 'process',
+ 'short': 'n',
+ 'long': 'process',
+ 'default': 0,
+ 'type': int,
+ 'help': 'Number of subprocesses (nikola build argument)'
+ },
+ {
+ 'name': 'parallel-type',
+ 'short': 'P',
+ 'long': 'parallel-type',
+ 'default': 'process',
+ 'type': str,
+ 'help': "Parallelization mode ('process' or 'thread', nikola build argument)"
+ },
]
def _execute(self, options, args):
"""Start the watcher."""
- self.logger = get_logger('auto', STDERR_HANDLER)
- LRSocket.logger = self.logger
-
- if WebSocket is object and watchdog is None:
- req_missing(['ws4py', 'watchdog'], 'use the "auto" command')
- elif WebSocket is object:
- req_missing(['ws4py'], 'use the "auto" command')
- elif watchdog is None:
+ self.sockets = []
+ self.rebuild_queue = asyncio.Queue()
+ self.reload_queue = asyncio.Queue()
+ self.last_rebuild = datetime.datetime.now()
+ self.is_rebuilding = False
+
+ if aiohttp is None and Observer is None:
+ req_missing(['aiohttp', 'watchdog'], 'use the "auto" command')
+ elif aiohttp is None:
+ req_missing(['aiohttp'], 'use the "auto" command')
+ elif Observer is None:
req_missing(['watchdog'], 'use the "auto" command')
- self.cmd_arguments = ['nikola', 'build']
+ if sys.argv[0].endswith('__main__.py'):
+ self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build']
+ else:
+ self.nikola_cmd = [sys.argv[0], 'build']
+
if self.site.configuration_filename != 'conf.py':
- self.cmd_arguments.append('--conf=' + self.site.configuration_filename)
+ self.nikola_cmd.append('--conf=' + self.site.configuration_filename)
- # Run an initial build so we are up-to-date
- subprocess.call(self.cmd_arguments)
+ if options and options.get('process'):
+ self.nikola_cmd += ['--process={}'.format(options['process']),
+ '--parallel-type={}'.format(options['parallel-type'])]
port = options and options.get('port')
self.snippet = '''<script>document.write('<script src="http://'
@@ -155,7 +168,7 @@ class CommandAuto(Command):
+ 'script>')</script>
</head>'''.format(port)
- # Do not duplicate entries -- otherwise, multiple rebuilds are triggered
+ # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered
watched = set([
'templates/'
] + [get_theme_path(name) for name in self.site.THEMES])
@@ -167,12 +180,17 @@ class CommandAuto(Command):
watched.add(item)
for item in self.site.config['LISTINGS_FOLDERS']:
watched.add(item)
+ for item in self.site.config['IMAGE_FOLDERS']:
+ watched.add(item)
for item in self.site._plugin_places:
watched.add(item)
# Nikola itself (useful for developers)
watched.add(pkg_resources.resource_filename('nikola', ''))
out_folder = self.site.config['OUTPUT_FOLDER']
+ if not os.path.exists(out_folder):
+ makedirs(out_folder)
+
if options and options.get('browser'):
browser = True
else:
@@ -181,289 +199,387 @@ class CommandAuto(Command):
if options['ipv6']:
dhost = '::'
else:
- dhost = None
+ dhost = '0.0.0.0'
host = options['address'].strip('[').strip(']') or dhost
+ # Prepare asyncio event loop
+ # Required for subprocessing to work
+ loop = asyncio.get_event_loop()
+
+ # Set debug setting
+ loop.set_debug(self.site.debug)
+
# Server can be disabled (Issue #1883)
self.has_server = not options['no-server']
- # Instantiate global observer
- observer = Observer()
if self.has_server:
- # Watch output folders and trigger reloads
- observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True)
+ loop.run_until_complete(self.set_up_server(host, port, out_folder))
+
+ # Run an initial build so we are up-to-date. The server is running, but we are not watching yet.
+ loop.run_until_complete(self.run_initial_rebuild())
+
+ self.wd_observer = Observer()
+ # Watch output folders and trigger reloads
+ if self.has_server:
+ self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), out_folder, recursive=True)
# Watch input folders and trigger rebuilds
for p in watched:
if os.path.exists(p):
- observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True)
+ self.wd_observer.schedule(NikolaEventHandler(self.queue_rebuild, loop), p, recursive=True)
# Watch config file (a bit of a hack, but we need a directory)
_conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py')
_conf_dn = os.path.dirname(_conf_fn)
- observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False)
-
- try:
- self.logger.info("Watching files for changes...")
- observer.start()
- except KeyboardInterrupt:
- pass
+ self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.queue_rebuild, loop), _conf_dn, recursive=False)
+ self.wd_observer.start()
- parent = self
+ win_sleeper = None
+ # https://bugs.python.org/issue23057 (fixed in Python 3.8)
+ if sys.platform == 'win32' and sys.version_info < (3, 8):
+ win_sleeper = asyncio.ensure_future(windows_ctrlc_workaround())
- class Mixed(WebSocketWSGIApplication):
- """A class that supports WS and HTTP protocols on the same port."""
+ if not self.has_server:
+ self.logger.info("Watching for changes...")
+ # Run the event loop forever (no server mode).
+ try:
+ # Run rebuild queue
+ loop.run_until_complete(self.run_rebuild_queue())
- def __call__(self, environ, start_response):
- if environ.get('HTTP_UPGRADE') is None:
- return parent.serve_static(environ, start_response)
- return super(Mixed, self).__call__(environ, start_response)
+ loop.run_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ if win_sleeper:
+ win_sleeper.cancel()
+ self.wd_observer.stop()
+ self.wd_observer.join()
+ loop.close()
+ return
- if self.has_server:
- ws = make_server(
- host, port, server_class=WSGIServer,
- handler_class=WebSocketWSGIRequestHandler,
- app=Mixed(handler_cls=LRSocket)
- )
- ws.initialize_websockets_manager()
- self.logger.info("Serving HTTP on {0} port {1}...".format(host, port))
- if browser:
- if options['ipv6'] or '::' in host:
- server_url = "http://[{0}]:{1}/".format(host, port)
- else:
- server_url = "http://{0}:{1}/".format(host, port)
+ if options['ipv6'] or '::' in host:
+ server_url = "http://[{0}]:{1}/".format(host, port)
+ else:
+ server_url = "http://{0}:{1}/".format(host, port)
+ self.logger.info("Serving on {0} ...".format(server_url))
- self.logger.info("Opening {0} in the default web browser...".format(server_url))
- # Yes, this is racy
- webbrowser.open('http://{0}:{1}'.format(host, port))
+ if browser:
+ # Some browsers fail to load 0.0.0.0 (Issue #2755)
+ if host == '0.0.0.0':
+ server_url = "http://127.0.0.1:{0}/".format(port)
+ self.logger.info("Opening {0} in the default web browser...".format(server_url))
+ webbrowser.open(server_url)
- try:
- self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host))
- ws.serve_forever()
- except KeyboardInterrupt:
- self.logger.info("Server is shutting down.")
- if self.dns_sd:
- self.dns_sd.Reset()
- # This is a hack, but something is locking up in a futex
- # and exit() doesn't work.
- os.kill(os.getpid(), 15)
- else:
- # Workaround: can’t have nothing running (instant exit)
- # but also can’t join threads (no way to exit)
- # The joys of threading.
- try:
- while True:
- time.sleep(1)
- except KeyboardInterrupt:
- self.logger.info("Shutting down.")
- # This is a hack, but something is locking up in a futex
- # and exit() doesn't work.
- os.kill(os.getpid(), 15)
+ # Run the event loop forever and handle shutdowns.
+ try:
+ # Run rebuild queue
+ rebuild_queue_fut = asyncio.ensure_future(self.run_rebuild_queue())
+ reload_queue_fut = asyncio.ensure_future(self.run_reload_queue())
- def do_rebuild(self, event):
+ self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host))
+ loop.run_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ self.logger.info("Server is shutting down.")
+ if win_sleeper:
+ win_sleeper.cancel()
+ if self.dns_sd:
+ self.dns_sd.Reset()
+ rebuild_queue_fut.cancel()
+ reload_queue_fut.cancel()
+ loop.run_until_complete(self.web_runner.cleanup())
+ self.wd_observer.stop()
+ self.wd_observer.join()
+ loop.close()
+
+ async def set_up_server(self, host: str, port: int, out_folder: str) -> None:
+ """Set up aiohttp server and start it."""
+ webapp = web.Application()
+ webapp.router.add_get('/livereload.js', self.serve_livereload_js)
+ webapp.router.add_get('/robots.txt', self.serve_robots_txt)
+ webapp.router.add_route('*', '/livereload', self.websocket_handler)
+ resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder)
+ webapp.router.register_resource(resource)
+ webapp.on_shutdown.append(self.remove_websockets)
+
+ self.web_runner = web.AppRunner(webapp)
+ await self.web_runner.setup()
+ website = web.TCPSite(self.web_runner, host, port)
+ await website.start()
+
+ async def run_initial_rebuild(self) -> None:
+ """Run an initial rebuild."""
+ await self._rebuild_site()
+ # If there are any clients, have them reload the root.
+ await self._send_reload_command(self.site.config['INDEX_FILE'])
+
+ async def queue_rebuild(self, event) -> None:
"""Rebuild the site."""
# Move events have a dest_path, some editors like gedit use a
# move on larger save operations for write protection
event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path
- fname = os.path.basename(event_path)
- if (fname.endswith('~') or
- fname.startswith('.') or
+ if sys.platform == 'win32':
+ # Windows hidden files support
+ is_hidden = os.stat(event_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN
+ else:
+ is_hidden = False
+ has_hidden_component = any(p.startswith('.') for p in event_path.split(os.sep))
+ if (is_hidden or has_hidden_component or
'__pycache__' in event_path or
- event_path.endswith(('.pyc', '.pyo', '.pyd')) or
- os.path.isdir(event_path)): # Skip on folders, these are usually duplicates
+ event_path.endswith(('.pyc', '.pyo', '.pyd', '_bak', '~')) or
+ event.is_directory): # Skip on folders, these are usually duplicates
return
- self.logger.info('REBUILDING SITE (from {0})'.format(event_path))
- p = subprocess.Popen(self.cmd_arguments, stderr=subprocess.PIPE)
- error = p.stderr.read()
- errord = error.decode('utf-8')
- if p.wait() != 0:
- self.logger.error(errord)
- error_signal.send(error=errord)
+
+ self.logger.debug('Queuing rebuild from {0}'.format(event_path))
+ await self.rebuild_queue.put((datetime.datetime.now(), event_path))
+
+ async def run_rebuild_queue(self) -> None:
+ """Run rebuilds from a queue (Nikola can only build in a single instance)."""
+ while True:
+ date, event_path = await self.rebuild_queue.get()
+ if date < (self.last_rebuild + self.delta_last_rebuild):
+ self.logger.debug("Skipping rebuild from {0} (within delta)".format(event_path))
+ continue
+ await self._rebuild_site(event_path)
+
+ async def _rebuild_site(self, event_path: typing.Optional[str] = None) -> None:
+ """Rebuild the site."""
+ self.is_rebuilding = True
+ self.last_rebuild = datetime.datetime.now()
+ if event_path:
+ self.logger.info('REBUILDING SITE (from {0})'.format(event_path))
else:
- print(errord)
+ self.logger.info('REBUILDING SITE')
- def do_refresh(self, event):
- """Refresh the page."""
+ p = await asyncio.create_subprocess_exec(*self.nikola_cmd, stderr=subprocess.PIPE)
+ exit_code = await p.wait()
+ out = (await p.stderr.read()).decode('utf-8')
+
+ if exit_code != 0:
+ self.logger.error("Rebuild failed\n" + out)
+ await self.send_to_websockets({'command': 'alert', 'message': out})
+ else:
+ self.logger.info("Rebuild successful\n" + out)
+
+ self.is_rebuilding = False
+
+ async def run_reload_queue(self) -> None:
+ """Send reloads from a queue to limit CPU usage."""
+ while True:
+ p = await self.reload_queue.get()
+ self.logger.info('REFRESHING: {0}'.format(p))
+ await self._send_reload_command(p)
+ if self.is_rebuilding:
+ await asyncio.sleep(REBUILDING_REFRESH_DELAY)
+ else:
+ await asyncio.sleep(IDLE_REFRESH_DELAY)
+
+ async def _send_reload_command(self, path: str) -> None:
+ """Send a reload command."""
+ await self.send_to_websockets({'command': 'reload', 'path': path, 'liveCSS': True})
+
+ async def reload_page(self, event) -> None:
+ """Reload the page."""
# Move events have a dest_path, some editors like gedit use a
# move on larger save operations for write protection
- event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path
- self.logger.info('REFRESHING: {0}'.format(event_path))
- p = os.path.relpath(event_path, os.path.abspath(self.site.config['OUTPUT_FOLDER']))
- refresh_signal.send(path=p)
-
- def serve_static(self, environ, start_response):
- """Trivial static file server."""
- uri = wsgiref.util.request_uri(environ)
- p_uri = urlparse(uri)
- f_path = os.path.join(self.site.config['OUTPUT_FOLDER'], *[unquote(x) for x in p_uri.path.split('/')])
-
- # ‘Pretty’ URIs and root are assumed to be HTML
- mimetype = 'text/html' if uri.endswith('/') else mimetypes.guess_type(uri)[0] or 'application/octet-stream'
-
- if os.path.isdir(f_path):
- if not p_uri.path.endswith('/'): # Redirect to avoid breakage
- start_response('301 Moved Permanently', [('Location', p_uri.path + '/')])
- return []
- f_path = os.path.join(f_path, self.site.config['INDEX_FILE'])
- mimetype = 'text/html'
-
- if p_uri.path == '/robots.txt':
- start_response('200 OK', [('Content-type', 'text/plain; charset=UTF-8')])
- return ['User-Agent: *\nDisallow: /\n'.encode('utf-8')]
- elif os.path.isfile(f_path):
- with open(f_path, 'rb') as fd:
- if mimetype.startswith('text/') or mimetype.endswith('+xml'):
- start_response('200 OK', [('Content-type', "{0}; charset=UTF-8".format(mimetype))])
- else:
- start_response('200 OK', [('Content-type', mimetype)])
- return [self.file_filter(mimetype, fd.read())]
- elif p_uri.path == '/livereload.js':
- with open(LRJS_PATH, 'rb') as fd:
- start_response('200 OK', [('Content-type', mimetype)])
- return [self.file_filter(mimetype, fd.read())]
- start_response('404 ERR', [])
- return [self.file_filter('text/html', ERROR_N.format(404).format(uri).encode('utf-8'))]
-
- def file_filter(self, mimetype, data):
- """Apply necessary changes to document before serving."""
- if mimetype == 'text/html':
- data = data.decode('utf8')
- data = self.remove_base_tag(data)
- data = self.inject_js(data)
- data = data.encode('utf8')
- return data
-
- def inject_js(self, data):
- """Inject livereload.js."""
- data = re.sub('</head>', self.snippet, data, 1, re.IGNORECASE)
- return data
-
- def remove_base_tag(self, data):
- """Comment out any <base> to allow local resolution of relative URLs."""
- data = re.sub(r'<base\s([^>]*)>', '<!--base \g<1>-->', data, re.IGNORECASE)
- return data
-
-
-pending = []
-
-
-class LRSocket(WebSocket):
- """Speak Livereload protocol."""
-
- def __init__(self, *a, **kw):
- """Initialize protocol handler."""
- refresh_signal.connect(self.notify)
- error_signal.connect(self.send_error)
- super(LRSocket, self).__init__(*a, **kw)
-
- def received_message(self, message):
- """Handle received message."""
- message = json.loads(message.data.decode('utf8'))
- self.logger.info('<--- {0}'.format(message))
- response = None
- if message['command'] == 'hello': # Handshake
- response = {
- 'command': 'hello',
- 'protocols': [
- 'http://livereload.com/protocols/official-7',
- ],
- 'serverName': 'nikola-livereload',
- }
- elif message['command'] == 'info': # Someone connected
- self.logger.info('****** Browser connected: {0}'.format(message.get('url')))
- self.logger.info('****** sending {0} pending messages'.format(len(pending)))
- while pending:
- msg = pending.pop()
- self.logger.info('---> {0}'.format(msg.data))
- self.send(msg, msg.is_binary)
- else:
- response = {
- 'command': 'alert',
- 'message': 'HEY',
- }
- if response is not None:
- response = json.dumps(response)
- self.logger.info('---> {0}'.format(response))
- response = TextMessage(response)
- self.send(response, response.is_binary)
-
- def notify(self, sender, path):
- """Send reload requests to the client."""
- p = os.path.join('/', path)
- message = {
- 'command': 'reload',
- 'liveCSS': True,
- 'path': p,
- }
- response = json.dumps(message)
- self.logger.info('---> {0}'.format(p))
- response = TextMessage(response)
- if self.stream is None: # No client connected or whatever
- pending.append(response)
+ if event:
+ event_path = event.dest_path if hasattr(event, 'dest_path') else event.src_path
else:
- self.send(response, response.is_binary)
+ event_path = self.site.config['OUTPUT_FOLDER']
+ p = os.path.relpath(event_path, os.path.abspath(self.site.config['OUTPUT_FOLDER'])).replace(os.sep, '/')
+ await self.reload_queue.put(p)
+
+ async def serve_livereload_js(self, request):
+ """Handle requests to /livereload.js and serve the JS file."""
+ return FileResponse(LRJS_PATH)
+
+ async def serve_robots_txt(self, request):
+ """Handle requests to /robots.txt."""
+ return Response(body=b'User-Agent: *\nDisallow: /\n', content_type='text/plain', charset='utf-8')
+
+ async def websocket_handler(self, request):
+ """Handle requests to /livereload and initiate WebSocket communication."""
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+ self.sockets.append(ws)
+
+ while True:
+ msg = await ws.receive()
+
+ self.logger.debug("Received message: {0}".format(msg))
+ if msg.type == aiohttp.WSMsgType.TEXT:
+ message = msg.json()
+ if message['command'] == 'hello':
+ response = {
+ 'command': 'hello',
+ 'protocols': [
+ 'http://livereload.com/protocols/official-7',
+ ],
+ 'serverName': 'Nikola Auto (livereload)',
+ }
+ await ws.send_json(response)
+ elif message['command'] != 'info':
+ self.logger.warning("Unknown command in message: {0}".format(message))
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSING):
+ break
+ elif msg.type == aiohttp.WSMsgType.CLOSE:
+ self.logger.debug("Closing WebSocket")
+ await ws.close()
+ break
+ elif msg.type == aiohttp.WSMsgType.ERROR:
+ self.logger.error('WebSocket connection closed with exception {0}'.format(ws.exception()))
+ break
+ else:
+ self.logger.warning("Received unknown message: {0}".format(msg))
+
+ self.sockets.remove(ws)
+ self.logger.debug("WebSocket connection closed: {0}".format(ws))
+
+ return ws
+
+ async def remove_websockets(self, app) -> None:
+ """Remove all websockets."""
+ for ws in self.sockets:
+ await ws.close()
+ self.sockets.clear()
+
+ async def send_to_websockets(self, message: dict) -> None:
+ """Send a message to all open WebSockets."""
+ to_delete = []
+ for ws in self.sockets:
+ if ws.closed:
+ to_delete.append(ws)
+ continue
- def send_error(self, sender, error=None):
- """Send reload requests to the client."""
- if self.stream is None: # No client connected or whatever
- return
- message = {
- 'command': 'alert',
- 'message': error,
- }
- response = json.dumps(message)
- response = TextMessage(response)
- if self.stream is None: # No client connected or whatever
- pending.append(response)
+ try:
+ await ws.send_json(message)
+ if ws._close_code:
+ await ws.close()
+ to_delete.append(ws)
+ except RuntimeError as e:
+ if 'closed' in e.args[0]:
+ self.logger.warning("WebSocket {0} closed uncleanly".format(ws))
+ to_delete.append(ws)
+ else:
+ raise
+
+ for ws in to_delete:
+ self.sockets.remove(ws)
+
+
+async def windows_ctrlc_workaround() -> None:
+ """Work around bpo-23057."""
+ # https://bugs.python.org/issue23057
+ while True:
+ await asyncio.sleep(1)
+
+
+class IndexHtmlStaticResource(StaticResource):
+ """A StaticResource implementation that serves /index.html in directory roots."""
+
+ modify_html = True
+ snippet = "</head>"
+
+ def __init__(self, modify_html=True, snippet="</head>", *args, **kwargs):
+ """Initialize a resource."""
+ self.modify_html = modify_html
+ self.snippet = snippet
+ super().__init__(*args, **kwargs)
+
+ async def _handle(self, request: 'web.Request') -> 'web.Response':
+ """Handle incoming requests (pass to handle_file)."""
+ filename = request.match_info['filename']
+ return await self.handle_file(request, filename)
+
+ async def handle_file(self, request: 'web.Request', filename: str, from_index=None) -> 'web.Response':
+ """Handle file requests."""
+ try:
+ filepath = self._directory.joinpath(filename).resolve()
+ if not self._follow_symlinks:
+ filepath.relative_to(self._directory)
+ except (ValueError, FileNotFoundError) as error:
+ # relatively safe
+ raise HTTPNotFound() from error
+ except Exception as error:
+ # perm error or other kind!
+ request.app.logger.exception(error)
+ raise HTTPNotFound() from error
+
+ # on opening a dir, load it's contents if allowed
+ if filepath.is_dir():
+ if filename.endswith('/') or not filename:
+ ret = await self.handle_file(request, filename + 'index.html', from_index=filename)
+ else:
+ # Redirect and add trailing slash so relative links work (Issue #3140)
+ new_url = request.rel_url.path + '/'
+ if request.rel_url.query_string:
+ new_url += '?' + request.rel_url.query_string
+ raise HTTPMovedPermanently(new_url)
+ elif filepath.is_file():
+ ct, encoding = mimetypes.guess_type(str(filepath))
+ encoding = encoding or 'utf-8'
+ if ct == 'text/html' and self.modify_html:
+ if sys.version_info[0] == 3 and sys.version_info[1] <= 5:
+ # Python 3.4 and 3.5 do not accept pathlib.Path objects in calls to open()
+ filepath = str(filepath)
+ with open(filepath, 'r', encoding=encoding) as fh:
+ text = fh.read()
+ text = self.transform_html(text)
+ ret = Response(text=text, content_type=ct, charset=encoding)
+ else:
+ ret = FileResponse(filepath, chunk_size=self._chunk_size)
+ elif from_index:
+ filepath = self._directory.joinpath(from_index).resolve()
+ try:
+ return Response(text=self._directory_as_html(filepath),
+ content_type="text/html")
+ except PermissionError:
+ raise HTTPForbidden
else:
- self.send(response, response.is_binary)
+ raise HTTPNotFound
+
+ return ret
+
+ def transform_html(self, text: str) -> str:
+ """Apply some transforms to HTML content."""
+ # Inject livereload.js
+ text = text.replace('</head>', self.snippet, 1)
+ # Disable <base> tag
+ text = re.sub(r'<base\s([^>]*)>', r'<!--base \g<1>-->', text, flags=re.IGNORECASE)
+ return text
-class OurWatchHandler(FileSystemEventHandler):
- """A Nikola-specific handler for Watchdog."""
+# Based on code from the 'hachiko' library by John Biesnecker — thanks!
+# https://github.com/biesnecker/hachiko
+class NikolaEventHandler:
+ """A Nikola-specific event handler for Watchdog. Based on code from hachiko."""
- def __init__(self, function):
+ def __init__(self, function, loop):
"""Initialize the handler."""
self.function = function
- super(OurWatchHandler, self).__init__()
+ self.loop = loop
- def on_any_event(self, event):
- """Call the provided function on any event."""
- self.function(event)
+ async def on_any_event(self, event):
+ """Handle all file events."""
+ await self.function(event)
+ def dispatch(self, event):
+ """Dispatch events to handler."""
+ self.loop.call_soon_threadsafe(asyncio.ensure_future, self.on_any_event(event))
-class ConfigWatchHandler(FileSystemEventHandler):
+
+class ConfigEventHandler(NikolaEventHandler):
"""A Nikola-specific handler for Watchdog that handles the config file (as a workaround)."""
- def __init__(self, configuration_filename, function):
+ def __init__(self, configuration_filename, function, loop):
"""Initialize the handler."""
self.configuration_filename = configuration_filename
self.function = function
+ self.loop = loop
- def on_any_event(self, event):
- """Call the provided function on any event."""
+ async def on_any_event(self, event):
+ """Handle file events if they concern the configuration file."""
if event._src_path == self.configuration_filename:
- self.function(event)
-
-
-try:
- # Monkeypatch to hide Broken Pipe Errors
- f = WebSocketWSGIHandler.finish_response
-
- if sys.version_info[0] == 3:
- EX = BrokenPipeError # NOQA
- else:
- EX = IOError
-
- def finish_response(self):
- """Monkeypatched finish_response that ignores broken pipes."""
- try:
- f(self)
- except EX: # Client closed the connection, not a real error
- pass
-
- WebSocketWSGIHandler.finish_response = finish_response
-except NameError:
- # In case there is no WebSocketWSGIHandler because of a failed import.
- pass
+ await self.function(event)
diff --git a/nikola/plugins/command/auto/livereload.js b/nikola/plugins/command/auto/livereload.js
index b4cafb3..282dce5 120000
--- a/nikola/plugins/command/auto/livereload.js
+++ b/nikola/plugins/command/auto/livereload.js
@@ -1 +1 @@
-../../../../bower_components/livereload-js/dist/livereload.js \ No newline at end of file
+../../../../npm_assets/node_modules/livereload-js/dist/livereload.js \ No newline at end of file
diff --git a/nikola/plugins/command/bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py
deleted file mode 100644
index 4808fdb..0000000
--- a/nikola/plugins/command/bootswatch_theme.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Given a swatch name from bootswatch.com and a parent theme, creates a custom theme."""
-
-from __future__ import print_function
-import os
-import requests
-
-from nikola.plugin_categories import Command
-from nikola import utils
-
-LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER)
-
-
-def _check_for_theme(theme, themes):
- for t in themes:
- if t.endswith(os.sep + theme):
- return True
- return False
-
-
-class CommandBootswatchTheme(Command):
- """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme."""
-
- name = "bootswatch_theme"
- doc_usage = "[options]"
- doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\
- " theme"
- cmd_options = [
- {
- 'name': 'name',
- 'short': 'n',
- 'long': 'name',
- 'default': 'custom',
- 'type': str,
- 'help': 'New theme name (default: custom)',
- },
- {
- 'name': 'swatch',
- 'short': 's',
- 'default': '',
- 'type': str,
- 'help': 'Name of the swatch from bootswatch.com.'
- },
- {
- 'name': 'parent',
- 'short': 'p',
- 'long': 'parent',
- 'default': 'bootstrap3',
- 'help': 'Parent theme name (default: bootstrap3)',
- },
- ]
-
- def _execute(self, options, args):
- """Given a swatch name and a parent theme, creates a custom theme."""
- name = options['name']
- swatch = options['swatch']
- if not swatch:
- LOGGER.error('The -s option is mandatory')
- return 1
- parent = options['parent']
- version = ''
-
- # See if we need bootswatch for bootstrap v2 or v3
- themes = utils.get_theme_chain(parent, self.site.themes_dirs)
- if not _check_for_theme('bootstrap3', themes) and not _check_for_theme('bootstrap3-jinja', themes):
- version = '2'
- elif not _check_for_theme('bootstrap', themes) and not _check_for_theme('bootstrap-jinja', themes):
- LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap')
- elif _check_for_theme('bootstrap3-gradients', themes) or _check_for_theme('bootstrap3-gradients-jinja', themes):
- LOGGER.warn('"bootswatch_theme" doesn\'t work well with the bootstrap3-gradients family')
-
- LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent))
- utils.makedirs(os.path.join('themes', name, 'assets', 'css'))
- for fname in ('bootstrap.min.css', 'bootstrap.css'):
- url = 'https://bootswatch.com'
- if version:
- url += '/' + version
- url = '/'.join((url, swatch, fname))
- LOGGER.info("Downloading: " + url)
- r = requests.get(url)
- if r.status_code > 299:
- LOGGER.error('Error {} getting {}', r.status_code, url)
- exit(1)
- data = r.text
- with open(os.path.join('themes', name, 'assets', 'css', fname),
- 'wb+') as output:
- output.write(data.encode('utf-8'))
-
- with open(os.path.join('themes', name, 'parent'), 'wb+') as output:
- output.write(parent.encode('utf-8'))
- LOGGER.notice('Theme created. Change the THEME setting to "{0}" to use it.'.format(name))
diff --git a/nikola/plugins/command/check.plugin b/nikola/plugins/command/check.plugin
index 6d2df82..bc6ede3 100644
--- a/nikola/plugins/command/check.plugin
+++ b/nikola/plugins/command/check.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Check the generated site
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py
index 0141a6b..cac6000 100644
--- a/nikola/plugins/command/check.py
+++ b/nikola/plugins/command/check.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,25 +26,19 @@
"""Check the generated site."""
-from __future__ import print_function
-from collections import defaultdict
+import logging
import os
import re
import sys
import time
-import logbook
-try:
- from urllib import unquote
- from urlparse import urlparse, urljoin, urldefrag
-except ImportError:
- from urllib.parse import unquote, urlparse, urljoin, urldefrag # NOQA
+from collections import defaultdict
+from urllib.parse import unquote, urlparse, urljoin, urldefrag
-from doit.loader import generate_tasks
import lxml.html
import requests
+from doit.loader import generate_tasks
from nikola.plugin_categories import Command
-from nikola.utils import get_logger, STDERR_HANDLER
def _call_nikola_list(site, cache=None):
@@ -104,7 +98,6 @@ class CommandCheck(Command):
"""Check the generated site."""
name = "check"
- logger = None
doc_usage = "[-v] (-l [--find-sources] [-r] | -f [--clean-files])"
doc_purpose = "check links and files in the generated site"
@@ -159,15 +152,13 @@ class CommandCheck(Command):
def _execute(self, options, args):
"""Check the generated site."""
- self.logger = get_logger('check', STDERR_HANDLER)
-
if not options['links'] and not options['files'] and not options['clean']:
print(self.help())
- return False
+ return 1
if options['verbose']:
- self.logger.level = logbook.DEBUG
+ self.logger.level = logging.DEBUG
else:
- self.logger.level = logbook.NOTICE
+ self.logger.level = logging.WARNING
failure = False
if options['links']:
failure |= self.scan_links(options['find_sources'], options['remote'])
@@ -191,6 +182,7 @@ class CommandCheck(Command):
self.existing_targets.add(self.site.config['SITE_URL'])
self.existing_targets.add(self.site.config['BASE_URL'])
url_type = self.site.config['URL_TYPE']
+ atom_extension = self.site.config['ATOM_EXTENSION']
deps = {}
if find_sources:
@@ -205,7 +197,7 @@ class CommandCheck(Command):
# Do not look at links in the cache, which are not parsed by
# anyone and may result in false positives. Problems arise
# with galleries, for example. Full rationale: (Issue #1447)
- self.logger.notice("Ignoring {0} (in cache, links may be incorrect)".format(filename))
+ self.logger.warning("Ignoring {0} (in cache, links may be incorrect)".format(filename))
return False
if not os.path.exists(fname):
@@ -213,7 +205,8 @@ class CommandCheck(Command):
return False
if '.html' == fname[-5:]:
- d = lxml.html.fromstring(open(filename, 'rb').read())
+ with open(filename, 'rb') as inf:
+ d = lxml.html.fromstring(inf.read())
extra_objs = lxml.html.fromstring('<html/>')
# Turn elements with a srcset attribute into individual img elements with src attributes
@@ -223,7 +216,7 @@ class CommandCheck(Command):
extra_objs.append(lxml.etree.Element('img', src=srcset_item.strip().split(' ')[0]))
link_elements = list(d.iterlinks()) + list(extra_objs.iterlinks())
# Extract links from XML formats to minimal HTML, allowing those to go through the link checks
- elif '.atom' == filename[-5:]:
+ elif atom_extension == filename[-len(atom_extension):]:
d = lxml.etree.parse(filename)
link_elements = lxml.html.fromstring('<html/>')
for elm in d.findall('*//{http://www.w3.org/2005/Atom}link'):
@@ -257,13 +250,13 @@ class CommandCheck(Command):
# Warn about links from https to http (mixed-security)
if base_url.netloc == parsed.netloc and base_url.scheme == "https" and parsed.scheme == "http":
- self.logger.warn("Mixed-content security for link in {0}: {1}".format(filename, target))
+ self.logger.warning("Mixed-content security for link in {0}: {1}".format(filename, target))
# Link to an internal REDIRECTIONS page
if target in self.internal_redirects:
redir_status_code = 301
redir_target = [_dest for _target, _dest in self.site.config['REDIRECTIONS'] if urljoin('/', _target) == target][0]
- self.logger.warn("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: 301]".format(redir_target, filename, target))
+ self.logger.warning("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: 301]".format(redir_target, filename, target))
# Absolute links to other domains, skip
# Absolute links when using only paths, skip.
@@ -273,7 +266,7 @@ class CommandCheck(Command):
continue
if target in self.checked_remote_targets: # already checked this exact target
if self.checked_remote_targets[target] in [301, 308]:
- self.logger.warn("Remote link PERMANENTLY redirected in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target]))
+ self.logger.warning("Remote link PERMANENTLY redirected in {0}: {1} [Error {2}]".format(filename, target, self.checked_remote_targets[target]))
elif self.checked_remote_targets[target] in [302, 307]:
self.logger.debug("Remote link temporarily redirected in {0}: {1} [HTTP: {2}]".format(filename, target, self.checked_remote_targets[target]))
elif self.checked_remote_targets[target] > 399:
@@ -281,7 +274,7 @@ class CommandCheck(Command):
continue
# Skip whitelisted targets
- if any(re.search(_, target) for _ in self.whitelist):
+ if any(pattern.search(target) for pattern in self.whitelist):
continue
# Check the remote link works
@@ -301,7 +294,7 @@ class CommandCheck(Command):
resp = requests.get(target, headers=req_headers, allow_redirects=True)
# Permanent redirects should be updated
if redir_status_code in [301, 308]:
- self.logger.warn("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code))
+ self.logger.warning("Remote link moved PERMANENTLY to \"{0}\" and should be updated in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code))
if redir_status_code in [302, 307]:
self.logger.debug("Remote link temporarily redirected to \"{0}\" in {1}: {2} [HTTP: {3}]".format(resp.url, filename, target, redir_status_code))
self.checked_remote_targets[resp.url] = resp.status_code
@@ -315,7 +308,7 @@ class CommandCheck(Command):
elif resp.status_code <= 399: # The address leads *somewhere* that is not an error
self.logger.debug("Successfully checked remote link in {0}: {1} [HTTP: {2}]".format(filename, target, resp.status_code))
continue
- self.logger.warn("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target))
+ self.logger.warning("Could not check remote link in {0}: {1} [Unknown problem]".format(filename, target))
continue
if url_type == 'rel_path':
@@ -323,23 +316,44 @@ class CommandCheck(Command):
target_filename = os.path.abspath(
os.path.join(self.site.config['OUTPUT_FOLDER'], unquote(target.lstrip('/'))))
else: # Relative path
- unquoted_target = unquote(target).encode('utf-8') if sys.version_info.major >= 3 else unquote(target).decode('utf-8')
+ unquoted_target = unquote(target).encode('utf-8')
target_filename = os.path.abspath(
os.path.join(os.path.dirname(filename).encode('utf-8'), unquoted_target))
- elif url_type in ('full_path', 'absolute'):
+ else:
+ relative = False
if url_type == 'absolute':
# convert to 'full_path' case, ie url relative to root
- url_rel_path = parsed.path[len(url_netloc_to_root):]
+ if parsed.path.startswith(url_netloc_to_root):
+ url_rel_path = parsed.path[len(url_netloc_to_root):]
+ else:
+ url_rel_path = parsed.path
+ if not url_rel_path.startswith('/'):
+ relative = True
else:
# convert to relative to base path
- url_rel_path = target[len(url_netloc_to_root):]
+ if target.startswith(url_netloc_to_root):
+ url_rel_path = target[len(url_netloc_to_root):]
+ else:
+ url_rel_path = target
+ if not url_rel_path.startswith('/'):
+ relative = True
if url_rel_path == '' or url_rel_path.endswith('/'):
url_rel_path = urljoin(url_rel_path, self.site.config['INDEX_FILE'])
- fs_rel_path = fs_relpath_from_url_path(url_rel_path)
- target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], fs_rel_path)
+ if relative:
+ unquoted_target = unquote(target).encode('utf-8')
+ target_filename = os.path.abspath(
+ os.path.join(os.path.dirname(filename).encode('utf-8'), unquoted_target))
+ else:
+ fs_rel_path = fs_relpath_from_url_path(url_rel_path)
+ target_filename = os.path.join(self.site.config['OUTPUT_FOLDER'], fs_rel_path)
+
+ if isinstance(target_filename, str):
+ target_filename_str = target_filename
+ else:
+ target_filename_str = target_filename.decode("utf-8", errors="surrogateescape")
- if any(re.search(x, target_filename) for x in self.whitelist):
+ if any(pattern.search(target_filename_str) for pattern in self.whitelist):
continue
elif target_filename not in self.existing_targets:
@@ -348,11 +362,11 @@ class CommandCheck(Command):
self.existing_targets.add(target_filename)
else:
rv = True
- self.logger.warn("Broken link in {0}: {1}".format(filename, target))
+ self.logger.warning("Broken link in {0}: {1}".format(filename, target))
if find_sources:
- self.logger.warn("Possible sources:")
- self.logger.warn("\n".join(deps[filename]))
- self.logger.warn("===============================\n")
+ self.logger.warning("Possible sources:")
+ self.logger.warning("\n".join(deps[filename]))
+ self.logger.warning("===============================\n")
except Exception as exc:
self.logger.error(u"Error with: {0} {1}".format(filename, exc))
return rv
@@ -363,6 +377,7 @@ class CommandCheck(Command):
self.logger.debug("===============\n")
self.logger.debug("{0} mode".format(self.site.config['URL_TYPE']))
failure = False
+ atom_extension = self.site.config['ATOM_EXTENSION']
# Maybe we should just examine all HTML files
output_folder = self.site.config['OUTPUT_FOLDER']
@@ -374,7 +389,7 @@ class CommandCheck(Command):
if '.html' == fname[-5:]:
if self.analyze(fname, find_sources, check_remote):
failure = True
- if '.atom' == fname[-5:]:
+ if atom_extension == fname[-len(atom_extension):]:
if self.analyze(fname, find_sources, False):
failure = True
if fname.endswith('sitemap.xml') or fname.endswith('sitemapindex.xml'):
@@ -397,15 +412,15 @@ class CommandCheck(Command):
if only_on_output:
only_on_output.sort()
- self.logger.warn("Files from unknown origins (orphans):")
+ self.logger.warning("Files from unknown origins (orphans):")
for f in only_on_output:
- self.logger.warn(f)
+ self.logger.warning(f)
failure = True
if only_on_input:
only_on_input.sort()
- self.logger.warn("Files not generated:")
+ self.logger.warning("Files not generated:")
for f in only_on_input:
- self.logger.warn(f)
+ self.logger.warning(f)
if not failure:
self.logger.debug("All files checked.")
return failure
@@ -434,6 +449,7 @@ class CommandCheck(Command):
pass
if warn_flag:
- self.logger.warn('Some files or directories have been removed, your site may need rebuilding')
+ self.logger.warning('Some files or directories have been removed, your site may need rebuilding')
+ return True
- return True
+ return False
diff --git a/nikola/plugins/command/console.plugin b/nikola/plugins/command/console.plugin
index 9bcc909..35e3585 100644
--- a/nikola/plugins/command/console.plugin
+++ b/nikola/plugins/command/console.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Start a debugging python console
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/console.py b/nikola/plugins/command/console.py
index c6a8376..b4342b4 100644
--- a/nikola/plugins/command/console.py
+++ b/nikola/plugins/command/console.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Chris Warrick, Roberto Alsina and others.
+# Copyright © 2012-2020 Chris Warrick, Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,14 @@
"""Start debugging console."""
-from __future__ import print_function, unicode_literals
import os
from nikola import __version__
from nikola.plugin_categories import Command
-from nikola.utils import get_logger, STDERR_HANDLER, req_missing, Commands
+from nikola.utils import get_logger, req_missing, Commands
-LOGGER = get_logger('console', STDERR_HANDLER)
+LOGGER = get_logger('console')
class CommandConsole(Command):
@@ -44,9 +43,9 @@ class CommandConsole(Command):
shells = ['ipython', 'bpython', 'plain']
doc_purpose = "start an interactive Python console with access to your site"
doc_description = """\
-The site engine is accessible as `site`, the config file as `conf`, and commands are available as `commands`.
+The site engine is accessible as `site` and `nikola_site`, the config file as `conf`, and commands are available as `commands`.
If there is no console to use specified (as -b, -i, -p) it tries IPython, then falls back to bpython, and finally falls back to the plain Python console."""
- header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site = site engine, commands = nikola commands)"
+ header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration file, site, nikola_site = site engine, commands = nikola commands)"
cmd_options = [
{
'name': 'bpython',
@@ -72,19 +71,35 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f
'default': False,
'help': 'Use the plain Python interpreter',
},
+ {
+ 'name': 'command',
+ 'short': 'c',
+ 'long': 'command',
+ 'type': str,
+ 'default': None,
+ 'help': 'Run a single command',
+ },
+ {
+ 'name': 'script',
+ 'short': 's',
+ 'long': 'script',
+ 'type': str,
+ 'default': None,
+ 'help': 'Execute a python script in the console context',
+ },
]
def ipython(self, willful=True):
"""Run an IPython shell."""
try:
import IPython
- except ImportError as e:
+ except ImportError:
if willful:
req_missing(['IPython'], 'use the IPython console')
- raise e # That’s how _execute knows whether to try something else.
+ raise # That’s how _execute knows whether to try something else.
else:
site = self.context['site'] # NOQA
- nikola_site = self.context['site'] # NOQA
+ nikola_site = self.context['nikola_site'] # NOQA
conf = self.context['conf'] # NOQA
commands = self.context['commands'] # NOQA
IPython.embed(header=self.header.format('IPython'))
@@ -93,10 +108,10 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f
"""Run a bpython shell."""
try:
import bpython
- except ImportError as e:
+ except ImportError:
if willful:
req_missing(['bpython'], 'use the bpython console')
- raise e # That’s how _execute knows whether to try something else.
+ raise # That’s how _execute knows whether to try something else.
else:
bpython.embed(banner=self.header.format('bpython'), locals_=self.context)
@@ -134,7 +149,13 @@ If there is no console to use specified (as -b, -i, -p) it tries IPython, then f
'nikola_site': self.site,
'commands': self.site.commands,
}
- if options['bpython']:
+ if options['command']:
+ exec(options['command'], None, self.context)
+ elif options['script']:
+ with open(options['script']) as inf:
+ code = compile(inf.read(), options['script'], 'exec')
+ exec(code, None, self.context)
+ elif options['bpython']:
self.bpython(True)
elif options['ipython']:
self.ipython(True)
diff --git a/nikola/plugins/command/default_config.plugin b/nikola/plugins/command/default_config.plugin
new file mode 100644
index 0000000..af279f6
--- /dev/null
+++ b/nikola/plugins/command/default_config.plugin
@@ -0,0 +1,13 @@
+[Core]
+name = default_config
+module = default_config
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Show the default configuration.
+
+[Nikola]
+PluginCategory = Command
+
diff --git a/nikola/plugins/command/default_config.py b/nikola/plugins/command/default_config.py
new file mode 100644
index 0000000..036f4d1
--- /dev/null
+++ b/nikola/plugins/command/default_config.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Show the default configuration."""
+
+import sys
+
+import nikola.plugins.command.init
+from nikola.plugin_categories import Command
+from nikola.utils import get_logger
+
+
+LOGGER = get_logger('default_config')
+
+
+class CommandShowConfig(Command):
+ """Show the default configuration."""
+
+ name = "default_config"
+
+ doc_usage = ""
+ needs_config = False
+ doc_purpose = "Print the default Nikola configuration."
+ cmd_options = []
+
+ def _execute(self, options=None, args=None):
+ """Show the default configuration."""
+ try:
+ print(nikola.plugins.command.init.CommandInit.create_configuration_to_string())
+ except Exception:
+ sys.stdout.buffer.write(nikola.plugins.command.init.CommandInit.create_configuration_to_string().encode('utf-8'))
diff --git a/nikola/plugins/command/deploy.plugin b/nikola/plugins/command/deploy.plugin
index 8bdc0e2..7cff28d 100644
--- a/nikola/plugins/command/deploy.plugin
+++ b/nikola/plugins/command/deploy.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Deploy the site
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py
index c2289e8..5273b58 100644
--- a/nikola/plugins/command/deploy.py
+++ b/nikola/plugins/command/deploy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,19 +26,16 @@
"""Deploy site."""
-from __future__ import print_function
-import io
-from datetime import datetime
-from dateutil.tz import gettz
-import dateutil
-import os
import subprocess
import time
+from datetime import datetime
+import dateutil
from blinker import signal
+from dateutil.tz import gettz
from nikola.plugin_categories import Command
-from nikola.utils import get_logger, clean_before_deployment, STDERR_HANDLER
+from nikola.utils import clean_before_deployment
class CommandDeploy(Command):
@@ -49,49 +46,28 @@ class CommandDeploy(Command):
doc_usage = "[preset [preset...]]"
doc_purpose = "deploy the site"
doc_description = "Deploy the site by executing deploy commands from the presets listed on the command line. If no presets are specified, `default` is executed."
- logger = None
def _execute(self, command, args):
"""Execute the deploy command."""
- self.logger = get_logger('deploy', STDERR_HANDLER)
- # Get last successful deploy date
- timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
-
# Get last-deploy from persistent state
last_deploy = self.site.state.get('last_deploy')
- if last_deploy is None:
- # If there is a last-deploy saved, move it to the new state persistence thing
- # FIXME: remove in Nikola 8
- if os.path.isfile(timestamp_path):
- try:
- with io.open(timestamp_path, 'r', encoding='utf8') as inf:
- last_deploy = dateutil.parser.parse(inf.read())
- clean = False
- except (IOError, Exception) as e:
- self.logger.debug("Problem when reading `{0}`: {1}".format(timestamp_path, e))
- last_deploy = datetime(1970, 1, 1)
- clean = True
- os.unlink(timestamp_path) # Remove because from now on it's in state
- else: # Just a default
- last_deploy = datetime(1970, 1, 1)
- clean = True
- else:
+ if last_deploy is not None:
last_deploy = dateutil.parser.parse(last_deploy)
clean = False
- if self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo':
- self.logger.warn("\nWARNING WARNING WARNING WARNING\n"
- "You are deploying using the nikolademo Disqus account.\n"
- "That means you will not be able to moderate the comments in your own site.\n"
- "And is probably not what you want to do.\n"
- "Think about it for 5 seconds, I'll wait :-)\n"
- "(press Ctrl+C to abort)\n")
+ if self.site.config['COMMENT_SYSTEM'] and self.site.config['COMMENT_SYSTEM_ID'] == 'nikolademo':
+ self.logger.warning("\nWARNING WARNING WARNING WARNING\n"
+ "You are deploying using the nikolademo Disqus account.\n"
+ "That means you will not be able to moderate the comments in your own site.\n"
+ "And is probably not what you want to do.\n"
+ "Think about it for 5 seconds, I'll wait :-)\n"
+ "(press Ctrl+C to abort)\n")
time.sleep(5)
# Remove drafts and future posts if requested
undeployed_posts = clean_before_deployment(self.site)
if undeployed_posts:
- self.logger.notice("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts)))
+ self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts)))
if args:
presets = args
@@ -102,7 +78,7 @@ class CommandDeploy(Command):
for preset in presets:
try:
self.site.config['DEPLOY_COMMANDS'][preset]
- except:
+ except KeyError:
self.logger.error('No such preset: {0}'.format(preset))
return 255
diff --git a/nikola/plugins/command/github_deploy.plugin b/nikola/plugins/command/github_deploy.plugin
index 21e246c..fbdd3bf 100644
--- a/nikola/plugins/command/github_deploy.plugin
+++ b/nikola/plugins/command/github_deploy.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Deploy the site to GitHub pages.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/github_deploy.py b/nikola/plugins/command/github_deploy.py
index b5ad322..d2c1f3f 100644
--- a/nikola/plugins/command/github_deploy.py
+++ b/nikola/plugins/command/github_deploy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2014-2016 Puneeth Chaganti and others.
+# Copyright © 2014-2020 Puneeth Chaganti and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,14 +26,13 @@
"""Deploy site to GitHub Pages."""
-from __future__ import print_function
import os
import subprocess
from textwrap import dedent
from nikola.plugin_categories import Command
from nikola.plugins.command.check import real_scan_files
-from nikola.utils import get_logger, req_missing, clean_before_deployment, STDERR_HANDLER
+from nikola.utils import req_missing, clean_before_deployment
from nikola.__main__ import main
from nikola import __version__
@@ -54,6 +53,12 @@ def check_ghp_import_installed():
req_missing(['ghp-import2'], 'deploy the site to GitHub Pages')
+class DeployFailedException(Exception):
+ """An internal exception for deployment errors."""
+
+ pass
+
+
class CommandGitHubDeploy(Command):
"""Deploy site to GitHub Pages."""
@@ -63,11 +68,9 @@ class CommandGitHubDeploy(Command):
doc_purpose = 'deploy the site to GitHub Pages'
doc_description = dedent(
"""\
- This command can be used to deploy your site to GitHub Pages.
-
- It uses ghp-import to do this task.
+ This command can be used to deploy your site to GitHub Pages. It uses ghp-import to do this task. It also optionally commits to the source branch.
- """
+ Configuration help: https://getnikola.com/handbook.html#deploying-to-github"""
)
cmd_options = [
{
@@ -76,15 +79,12 @@ class CommandGitHubDeploy(Command):
'long': 'message',
'default': 'Nikola auto commit.',
'type': str,
- 'help': 'Commit message (default: Nikola auto commit.)',
+ 'help': 'Commit message',
},
]
- logger = None
def _execute(self, options, args):
"""Run the deployment."""
- self.logger = get_logger(CommandGitHubDeploy.name, STDERR_HANDLER)
-
# Check if ghp-import is installed
check_ghp_import_installed()
@@ -102,12 +102,10 @@ class CommandGitHubDeploy(Command):
# Remove drafts and future posts if requested (Issue #2406)
undeployed_posts = clean_before_deployment(self.site)
if undeployed_posts:
- self.logger.notice("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts)))
+ self.logger.warning("Deleted {0} posts due to DEPLOY_* settings".format(len(undeployed_posts)))
# Commit and push
- self._commit_and_push(options['commit_message'])
-
- return
+ return self._commit_and_push(options['commit_message'])
def _run_command(self, command, xfail=False):
"""Run a command that may or may not fail."""
@@ -122,7 +120,7 @@ class CommandGitHubDeploy(Command):
'Failed GitHub deployment -- command {0} '
'returned {1}'.format(e.cmd, e.returncode)
)
- raise SystemError(e.returncode)
+ raise DeployFailedException(e.returncode)
def _commit_and_push(self, commit_first_line):
"""Commit all the files and push."""
@@ -145,9 +143,16 @@ class CommandGitHubDeploy(Command):
if e != 0:
self._run_command(['git', 'commit', '-am', commit_message])
else:
- self.logger.notice('Nothing to commit to source branch.')
+ self.logger.info('Nothing to commit to source branch.')
+
+ try:
+ source_commit = uni_check_output(['git', 'rev-parse', source])
+ except subprocess.CalledProcessError:
+ try:
+ source_commit = uni_check_output(['git', 'rev-parse', 'HEAD'])
+ except subprocess.CalledProcessError:
+ source_commit = '?'
- source_commit = uni_check_output(['git', 'rev-parse', source])
commit_message = (
'{0}\n\n'
'Source commit: {1}'
@@ -161,7 +166,7 @@ class CommandGitHubDeploy(Command):
if autocommit:
self._run_command(['git', 'push', '-u', remote, source])
- except SystemError as e:
+ except DeployFailedException as e:
return e.args[0]
self.logger.info("Successful deployment")
diff --git a/nikola/plugins/command/import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin
index eab9d17..46df1ef 100644
--- a/nikola/plugins/command/import_wordpress.plugin
+++ b/nikola/plugins/command/import_wordpress.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Import a wordpress site from a XML dump (requires markdown).
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/import_wordpress.py b/nikola/plugins/command/import_wordpress.py
index 0b48583..5e2aee6 100644
--- a/nikola/plugins/command/import_wordpress.py
+++ b/nikola/plugins/command/import_wordpress.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,46 +26,45 @@
"""Import a WordPress dump."""
-from __future__ import unicode_literals, print_function
-import os
-import re
-import sys
import datetime
import io
import json
+import os
+import re
+import sys
+from collections import defaultdict
+from urllib.parse import urlparse, unquote
+
import requests
from lxml import etree
-from collections import defaultdict
-try:
- import html2text
-except:
- html2text = None
+from nikola.plugin_categories import Command
+from nikola import utils, hierarchy_utils
+from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin, links
+from nikola.plugins.command.init import (
+ SAMPLE_CONF, prepare_config,
+ format_default_translations_config,
+ get_default_translations_dict
+)
try:
- from urlparse import urlparse
- from urllib import unquote
+ import html2text
except ImportError:
- from urllib.parse import urlparse, unquote # NOQA
+ html2text = None
try:
import phpserialize
except ImportError:
- phpserialize = None # NOQA
+ phpserialize = None
-from nikola.plugin_categories import Command
-from nikola import utils
-from nikola.utils import req_missing, unicode_str
-from nikola.plugins.basic_import import ImportMixin, links
-from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN
-from nikola.plugins.command.init import SAMPLE_CONF, prepare_config, format_default_translations_config
-
-LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER)
+LOGGER = utils.get_logger('import_wordpress')
def install_plugin(site, plugin_name, output_dir=None, show_install_notes=False):
"""Install a Nikola plugin."""
- LOGGER.notice("Installing plugin '{0}'".format(plugin_name))
+ LOGGER.info("Installing plugin '{0}'".format(plugin_name))
# Get hold of the 'plugin' plugin
plugin_installer_info = site.plugin_manager.getPluginByName('plugin', 'Command')
if plugin_installer_info is None:
@@ -148,15 +147,22 @@ class CommandImportWordpress(Command, ImportMixin):
'long': 'qtranslate',
'default': False,
'type': bool,
- 'help': "Look for translations generated by qtranslate plugin",
- # WARNING: won't recover translated titles that actually
- # don't seem to be part of the wordpress XML export at the
- # time of writing :(
+ 'help': """Look for translations generated by qtranslate plugin.
+WARNING: a default wordpress export won't allow to recover title translations.
+For this to be possible consider applying the hack suggested at
+https://github.com/qtranslate/qtranslate-xt/issues/199 :
+
+In wp-admin/includes/export.php change
+`echo apply_filters( 'the_title_rss', $post->post_title );
+
+to
+`echo apply_filters( 'the_title_export', $post->post_title );
+"""
},
{
'name': 'translations_pattern',
'long': 'translations_pattern',
- 'default': None,
+ 'default': DEFAULT_TRANSLATIONS_PATTERN,
'type': str,
'help': "The pattern for translation files names",
},
@@ -259,9 +265,9 @@ class CommandImportWordpress(Command, ImportMixin):
options['output_folder'] = args.pop(0)
if args:
- LOGGER.warn('You specified additional arguments ({0}). Please consider '
- 'putting these arguments before the filename if you '
- 'are running into problems.'.format(args))
+ LOGGER.warning('You specified additional arguments ({0}). Please consider '
+ 'putting these arguments before the filename if you '
+ 'are running into problems.'.format(args))
self.onefile = options.get('one_file', False)
@@ -307,7 +313,7 @@ class CommandImportWordpress(Command, ImportMixin):
LOGGER.error("You can use at most one of the options --html2text, --transform-to-html and --transform-to-markdown.")
return False
if (self.html2text or self.transform_to_html or self.transform_to_markdown) and self.use_wordpress_compiler:
- LOGGER.warn("It does not make sense to combine --use-wordpress-compiler with any of --html2text, --transform-to-html and --transform-to-markdown, as the latter convert all posts to HTML and the first option then affects zero posts.")
+ LOGGER.warning("It does not make sense to combine --use-wordpress-compiler with any of --html2text, --transform-to-html and --transform-to-markdown, as the latter convert all posts to HTML and the first option then affects zero posts.")
if (self.html2text or self.transform_to_markdown) and not html2text:
LOGGER.error("You need to install html2text via 'pip install html2text' before you can use the --html2text and --transform-to-markdown options.")
@@ -339,14 +345,14 @@ class CommandImportWordpress(Command, ImportMixin):
# cat_id = get_text_tag(cat, '{{{0}}}term_id'.format(wordpress_namespace), None)
cat_slug = get_text_tag(cat, '{{{0}}}category_nicename'.format(wordpress_namespace), None)
cat_parent_slug = get_text_tag(cat, '{{{0}}}category_parent'.format(wordpress_namespace), None)
- cat_name = get_text_tag(cat, '{{{0}}}cat_name'.format(wordpress_namespace), None)
+ cat_name = utils.html_unescape(get_text_tag(cat, '{{{0}}}cat_name'.format(wordpress_namespace), None))
cat_path = [cat_name]
if cat_parent_slug in cat_map:
cat_path = cat_map[cat_parent_slug] + cat_path
cat_map[cat_slug] = cat_path
self._category_paths = dict()
for cat, path in cat_map.items():
- self._category_paths[cat] = utils.join_hierarchical_category_path(path)
+ self._category_paths[cat] = hierarchy_utils.join_hierarchical_category_path(path)
def _execute(self, options={}, args=[]):
"""Import a WordPress blog from an export file into a Nikola site."""
@@ -373,17 +379,12 @@ class CommandImportWordpress(Command, ImportMixin):
if phpserialize is None:
req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads')
- channel = self.get_channel_from_file(self.wordpress_export_file)
+ export_file_preprocessor = modernize_qtranslate_tags if self.separate_qtranslate_content else None
+ channel = self.get_channel_from_file(self.wordpress_export_file, export_file_preprocessor)
self._prepare(channel)
conf_template = self.generate_base_site()
- # If user has specified a custom pattern for translation files we
- # need to fix the config
- if self.translations_pattern:
- self.context['TRANSLATIONS_PATTERN'] = self.translations_pattern
-
self.import_posts(channel)
-
self.context['TRANSLATIONS'] = format_default_translations_config(
self.extra_languages)
self.context['REDIRECTIONS'] = self.configure_redirections(
@@ -397,7 +398,7 @@ class CommandImportWordpress(Command, ImportMixin):
# Add tag redirects
for tag in self.all_tags:
try:
- if isinstance(tag, utils.bytes_str):
+ if isinstance(tag, bytes):
tag_str = tag.decode('utf8', 'replace')
else:
tag_str = tag
@@ -420,9 +421,9 @@ class CommandImportWordpress(Command, ImportMixin):
if not install_plugin(self.site, 'wordpress_compiler', output_dir=os.path.join(self.output_folder, 'plugins')):
return False
else:
- LOGGER.warn("Make sure to install the WordPress page compiler via")
- LOGGER.warn(" nikola plugin -i wordpress_compiler")
- LOGGER.warn("in your imported blog's folder ({0}), if you haven't installed it system-wide or user-wide. Otherwise, your newly imported blog won't compile.".format(self.output_folder))
+ LOGGER.warning("Make sure to install the WordPress page compiler via")
+ LOGGER.warning(" nikola plugin -i wordpress_compiler")
+ LOGGER.warning("in your imported blog's folder ({0}), if you haven't installed it system-wide or user-wide. Otherwise, your newly imported blog won't compile.".format(self.output_folder))
@classmethod
def read_xml_file(cls, filename):
@@ -438,9 +439,16 @@ class CommandImportWordpress(Command, ImportMixin):
return b''.join(xml)
@classmethod
- def get_channel_from_file(cls, filename):
- """Get channel from XML file."""
- tree = etree.fromstring(cls.read_xml_file(filename))
+ def get_channel_from_file(cls, filename, xml_preprocessor=None):
+ """Get channel from XML file.
+
+ An optional 'xml_preprocessor' allows to modify the xml
+ (typically to deal with variations in tags injected by some WP plugin)
+ """
+ xml_string = cls.read_xml_file(filename)
+ if xml_preprocessor:
+ xml_string = xml_preprocessor(xml_string)
+ tree = etree.fromstring(xml_string)
channel = tree.find('channel')
return channel
@@ -451,7 +459,10 @@ class CommandImportWordpress(Command, ImportMixin):
context = SAMPLE_CONF.copy()
self.lang = get_text_tag(channel, 'language', 'en')[:2]
context['DEFAULT_LANG'] = self.lang
- context['TRANSLATIONS_PATTERN'] = DEFAULT_TRANSLATIONS_PATTERN
+ # If user has specified a custom pattern for translation files we
+ # need to fix the config
+ context['TRANSLATIONS_PATTERN'] = self.translations_pattern
+
context['BLOG_TITLE'] = get_text_tag(channel, 'title',
'PUT TITLE HERE')
context['BLOG_DESCRIPTION'] = get_text_tag(
@@ -482,17 +493,17 @@ class CommandImportWordpress(Command, ImportMixin):
PAGES = '(\n'
for extension in extensions:
POSTS += ' ("posts/*.{0}", "posts", "post.tmpl"),\n'.format(extension)
- PAGES += ' ("pages/*.{0}", "pages", "story.tmpl"),\n'.format(extension)
+ PAGES += ' ("pages/*.{0}", "pages", "page.tmpl"),\n'.format(extension)
POSTS += ')\n'
PAGES += ')\n'
context['POSTS'] = POSTS
context['PAGES'] = PAGES
COMPILERS = '{\n'
- COMPILERS += ''' "rest": ('.txt', '.rst'),''' + '\n'
- COMPILERS += ''' "markdown": ('.md', '.mdown', '.markdown'),''' + '\n'
- COMPILERS += ''' "html": ('.html', '.htm'),''' + '\n'
+ COMPILERS += ''' "rest": ['.txt', '.rst'],''' + '\n'
+ COMPILERS += ''' "markdown": ['.md', '.mdown', '.markdown'],''' + '\n'
+ COMPILERS += ''' "html": ['.html', '.htm'],''' + '\n'
if self.use_wordpress_compiler:
- COMPILERS += ''' "wordpress": ('.wp'),''' + '\n'
+ COMPILERS += ''' "wordpress": ['.wp'],''' + '\n'
COMPILERS += '}'
context['COMPILERS'] = COMPILERS
@@ -503,12 +514,12 @@ class CommandImportWordpress(Command, ImportMixin):
try:
request = requests.get(url, auth=self.auth)
if request.status_code >= 400:
- LOGGER.warn("Downloading {0} to {1} failed with HTTP status code {2}".format(url, dst_path, request.status_code))
+ LOGGER.warning("Downloading {0} to {1} failed with HTTP status code {2}".format(url, dst_path, request.status_code))
return
with open(dst_path, 'wb+') as fd:
fd.write(request.content)
except requests.exceptions.ConnectionError as err:
- LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err))
+ LOGGER.warning("Downloading {0} to {1} failed: {2}".format(url, dst_path, err))
def import_attachment(self, item, wordpress_namespace):
"""Import an attachment to the site."""
@@ -549,14 +560,7 @@ class CommandImportWordpress(Command, ImportMixin):
# that the export should give you the power to insert
# your blogging into another site or system its not.
# Why don't they just use JSON?
- if sys.version_info[0] == 2:
- try:
- metadata = phpserialize.loads(utils.sys_encode(meta_value.text))
- except ValueError:
- # local encoding might be wrong sometimes
- metadata = phpserialize.loads(meta_value.text.encode('utf-8'))
- else:
- metadata = phpserialize.loads(meta_value.text.encode('utf-8'))
+ metadata = phpserialize.loads(meta_value.text.encode('utf-8'))
meta_key = b'image_meta'
size_key = b'sizes'
@@ -583,6 +587,9 @@ class CommandImportWordpress(Command, ImportMixin):
if ignore_zero and value == 0:
return
elif is_float:
+ # in some locales (like fr) and for old posts there may be a comma here.
+ if isinstance(value, bytes):
+ value = value.replace(b",", b".")
value = float(value)
if ignore_zero and value == 0:
return
@@ -775,7 +782,7 @@ class CommandImportWordpress(Command, ImportMixin):
elif approved == 'spam' or approved == 'trash':
pass
else:
- LOGGER.warn("Unknown comment approved status: {0}".format(approved))
+ LOGGER.warning("Unknown comment approved status: {0}".format(approved))
parent = int(get_text_tag(comment, "{{{0}}}comment_parent".format(wordpress_namespace), 0))
if parent == 0:
parent = None
@@ -796,7 +803,7 @@ class CommandImportWordpress(Command, ImportMixin):
"""Write comment header line."""
if header_content is None:
return
- header_content = unicode_str(header_content).replace('\n', ' ')
+ header_content = str(header_content).replace('\n', ' ')
line = '.. ' + header_field + ': ' + header_content + '\n'
fd.write(line.encode('utf8'))
@@ -813,6 +820,16 @@ class CommandImportWordpress(Command, ImportMixin):
write_header_line(fd, "wordpress_user_id", comment["user_id"])
fd.write(('\n' + comment['content']).encode('utf8'))
+ def _create_meta_and_content_filenames(self, slug, extension, lang, default_language, translations_config):
+ out_meta_filename = slug + '.meta'
+ out_content_filename = slug + '.' + extension
+ if lang and lang != default_language:
+ out_meta_filename = utils.get_translation_candidate(translations_config,
+ out_meta_filename, lang)
+ out_content_filename = utils.get_translation_candidate(translations_config,
+ out_content_filename, lang)
+ return out_meta_filename, out_content_filename
+
def _create_metadata(self, status, excerpt, tags, categories, post_name=None):
"""Create post metadata."""
other_meta = {'wp-status': status}
@@ -824,16 +841,16 @@ class CommandImportWordpress(Command, ImportMixin):
if text in self._category_paths:
cats.append(self._category_paths[text])
else:
- cats.append(utils.join_hierarchical_category_path([text]))
+ cats.append(hierarchy_utils.join_hierarchical_category_path([utils.html_unescape(text)]))
other_meta['categories'] = ','.join(cats)
if len(cats) > 0:
other_meta['category'] = cats[0]
if len(cats) > 1:
- LOGGER.warn(('Post "{0}" has more than one category! ' +
- 'Will only use the first one.').format(post_name))
- tags_cats = tags
+ LOGGER.warning(('Post "{0}" has more than one category! ' +
+ 'Will only use the first one.').format(post_name))
+ tags_cats = [utils.html_unescape(tag) for tag in tags]
else:
- tags_cats = tags + categories
+ tags_cats = [utils.html_unescape(tag) for tag in tags + categories]
return tags_cats, other_meta
_tag_sanitize_map = {True: {}, False: {}}
@@ -847,7 +864,7 @@ class CommandImportWordpress(Command, ImportMixin):
previous = self._tag_sanitize_map[is_category][tag.lower()]
if self.tag_saniziting_strategy == 'first':
if tag != previous[0]:
- LOGGER.warn("Changing spelling of {0} name '{1}' to {2}.".format('category' if is_category else 'tag', tag, previous[0]))
+ LOGGER.warning("Changing spelling of {0} name '{1}' to {2}.".format('category' if is_category else 'tag', tag, previous[0]))
return previous[0]
else:
LOGGER.error("Unknown tag sanitizing strategy '{0}'!".format(self.tag_saniziting_strategy))
@@ -873,7 +890,7 @@ class CommandImportWordpress(Command, ImportMixin):
path = unquote(parsed.path.strip('/'))
try:
- if isinstance(path, utils.bytes_str):
+ if isinstance(path, bytes):
path = path.decode('utf8', 'replace')
else:
path = path
@@ -925,17 +942,19 @@ class CommandImportWordpress(Command, ImportMixin):
tags = []
categories = []
+ post_status = 'published'
+ has_math = "no"
if status == 'trash':
- LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title))
+ LOGGER.warning('Trashed post "{0}" will not be imported.'.format(title))
return False
elif status == 'private':
- tags.append('private')
is_draft = False
is_private = True
+ post_status = 'private'
elif status != 'publish':
- tags.append('draft')
is_draft = True
is_private = False
+ post_status = 'draft'
else:
is_draft = False
is_private = False
@@ -953,7 +972,7 @@ class CommandImportWordpress(Command, ImportMixin):
tags.append(text)
if '$latex' in content:
- tags.append('mathjax')
+ has_math = "yes"
for i, cat in enumerate(categories[:]):
cat = self._sanitize(cat, True)
@@ -974,52 +993,56 @@ class CommandImportWordpress(Command, ImportMixin):
post_format = 'wp'
if is_draft and self.exclude_drafts:
- LOGGER.notice('Draft "{0}" will not be imported.'.format(title))
+ LOGGER.warning('Draft "{0}" will not be imported.'.format(title))
return False
elif is_private and self.exclude_privates:
- LOGGER.notice('Private post "{0}" will not be imported.'.format(title))
+ LOGGER.warning('Private post "{0}" will not be imported.'.format(title))
return False
elif content.strip() or self.import_empty_items:
# If no content is found, no files are written.
self.url_map[link] = (self.context['SITE_URL'] +
out_folder.rstrip('/') + '/' + slug +
'.html').replace(os.sep, '/')
- if hasattr(self, "separate_qtranslate_content") \
- and self.separate_qtranslate_content:
- content_translations = separate_qtranslate_content(content)
+ default_language = self.context["DEFAULT_LANG"]
+ if self.separate_qtranslate_content:
+ content_translations = separate_qtranslate_tagged_langs(content)
+ title_translations = separate_qtranslate_tagged_langs(title)
else:
content_translations = {"": content}
- default_language = self.context["DEFAULT_LANG"]
+ title_translations = {"": title}
+ # in case of mistmatch between the languages found in the title and in the content
+ default_title = title_translations.get(default_language, title)
+ extra_languages = [lang for lang in content_translations.keys() if lang not in ("", default_language)]
+ for extra_lang in extra_languages:
+ self.extra_languages.add(extra_lang)
+ translations_dict = get_default_translations_dict(default_language, extra_languages)
+ current_translations_config = {
+ "DEFAULT_LANG": default_language,
+ "TRANSLATIONS": translations_dict,
+ "TRANSLATIONS_PATTERN": self.context["TRANSLATIONS_PATTERN"]
+ }
for lang, content in content_translations.items():
try:
content, extension, rewrite_html = self.transform_content(content, post_format, attachments)
- except:
+ except Exception:
LOGGER.error(('Cannot interpret post "{0}" (language {1}) with post ' +
'format {2}!').format(os.path.join(out_folder, slug), lang, post_format))
return False
- if lang:
- out_meta_filename = slug + '.meta'
- if lang == default_language:
- out_content_filename = slug + '.' + extension
- else:
- out_content_filename \
- = utils.get_translation_candidate(self.context,
- slug + "." + extension, lang)
- self.extra_languages.add(lang)
- meta_slug = slug
- else:
- out_meta_filename = slug + '.meta'
- out_content_filename = slug + '.' + extension
- meta_slug = slug
+
+ out_meta_filename, out_content_filename = self._create_meta_and_content_filenames(
+ slug, extension, lang, default_language, current_translations_config)
+
tags, other_meta = self._create_metadata(status, excerpt, tags, categories,
post_name=os.path.join(out_folder, slug))
-
+ current_title = title_translations.get(lang, default_title)
meta = {
- "title": title,
- "slug": meta_slug,
+ "title": current_title,
+ "slug": slug,
"date": post_date,
"description": description,
"tags": ','.join(tags),
+ "status": post_status,
+ "has_math": has_math,
}
meta.update(other_meta)
if self.onefile:
@@ -1033,7 +1056,7 @@ class CommandImportWordpress(Command, ImportMixin):
else:
self.write_metadata(os.path.join(self.output_folder, out_folder,
out_meta_filename),
- title, meta_slug, post_date, description, tags, **other_meta)
+ current_title, slug, post_date, description, tags, **other_meta)
self.write_content(
os.path.join(self.output_folder,
out_folder, out_content_filename),
@@ -1053,8 +1076,8 @@ class CommandImportWordpress(Command, ImportMixin):
return (out_folder, slug)
else:
- LOGGER.warn(('Not going to import "{0}" because it seems to contain'
- ' no content.').format(title))
+ LOGGER.warning(('Not going to import "{0}" because it seems to contain'
+ ' no content.').format(title))
return False
def _extract_item_info(self, item):
@@ -1080,7 +1103,7 @@ class CommandImportWordpress(Command, ImportMixin):
if parent_id is not None and int(parent_id) != 0:
self.attachments[int(parent_id)][post_id] = data
else:
- LOGGER.warn("Attachment #{0} ({1}) has no parent!".format(post_id, data['files']))
+ LOGGER.warning("Attachment #{0} ({1}) has no parent!".format(post_id, data['files']))
def write_attachments_info(self, path, attachments):
"""Write attachments info file."""
@@ -1118,8 +1141,8 @@ class CommandImportWordpress(Command, ImportMixin):
self.process_item_if_post_or_page(item)
# Assign attachments to posts
for post_id in self.attachments:
- LOGGER.warn(("Found attachments for post or page #{0}, but didn't find post or page. " +
- "(Attachments: {1})").format(post_id, [e['files'][0] for e in self.attachments[post_id].values()]))
+ LOGGER.warning(("Found attachments for post or page #{0}, but didn't find post or page. " +
+ "(Attachments: {1})").format(post_id, [e['files'][0] for e in self.attachments[post_id].values()]))
def get_text_tag(tag, name, default):
@@ -1133,15 +1156,20 @@ def get_text_tag(tag, name, default):
return default
-def separate_qtranslate_content(text):
- """Parse the content of a wordpress post or page and separate qtranslate languages.
+def separate_qtranslate_tagged_langs(text):
+ """Parse the content of a wordpress post or page and separate languages.
+
+ For qtranslateX tags: [:LL]blabla[:]
- qtranslate tags: <!--:LL-->blabla<!--:-->
+ Note: qtranslate* plugins had a troubled history and used various
+ tags over time, application of the 'modernize_qtranslate_tags'
+ function is required for this function to handle most of the legacy
+ cases.
"""
- # TODO: uniformize qtranslate tags <!--/en--> => <!--:-->
- qt_start = "<!--:"
- qt_end = "-->"
- qt_end_with_lang_len = 5
+ qt_start = "[:"
+ qt_end = "]"
+ qt_end_len = len(qt_end)
+ qt_end_with_lang_len = qt_end_len + 2
qt_chunks = text.split(qt_start)
content_by_lang = {}
common_txt_list = []
@@ -1153,9 +1181,9 @@ def separate_qtranslate_content(text):
# be some piece of common text or tags, or just nothing
lang = "" # default language
c = c.lstrip(qt_end)
- if not c:
+ if not c.strip():
continue
- elif c[2:].startswith(qt_end):
+ elif c[2:qt_end_with_lang_len].startswith(qt_end):
# a language specific section (with language code at the begining)
lang = c[:2]
c = c[qt_end_with_lang_len:]
@@ -1176,3 +1204,26 @@ def separate_qtranslate_content(text):
for l in content_by_lang.keys():
content_by_lang[l] = " ".join(content_by_lang[l])
return content_by_lang
+
+
+def modernize_qtranslate_tags(xml_bytes):
+ """
+ Uniformize the "tag" used by various version of qtranslate.
+
+ The resulting byte string will only contain one set of qtranslate tags
+ (namely [:LG] and [:]), older ones being converted to new ones.
+ """
+ old_start_lang = re.compile(b"<!--:?(\\w{2})-->")
+ new_start_lang = b"[:\\1]"
+ old_end_lang = re.compile(b"<!--(/\\w{2}|:)-->")
+ new_end_lang = b"[:]"
+ title_match = re.compile(b"<title>(.*?)</title>")
+ modern_starts = old_start_lang.sub(new_start_lang, xml_bytes)
+ modernized_bytes = old_end_lang.sub(new_end_lang, modern_starts)
+
+ def title_escape(match):
+ title = match.group(1)
+ title = title.replace(b"&", b"&amp;").replace(b"<", b"&lt;").replace(b">", b"&gt;")
+ return b"<title>" + title + b"</title>"
+ fixed_bytes = title_match.sub(title_escape, modernized_bytes)
+ return fixed_bytes
diff --git a/nikola/plugins/command/init.plugin b/nikola/plugins/command/init.plugin
index a8b1523..6ee27d3 100644
--- a/nikola/plugins/command/init.plugin
+++ b/nikola/plugins/command/init.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create a new site.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/init.py b/nikola/plugins/command/init.py
index 3d6669c..0026edc 100644
--- a/nikola/plugins/command/init.py
+++ b/nikola/plugins/command/init.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,28 +26,28 @@
"""Create a new site."""
-from __future__ import print_function, unicode_literals
-import os
-import shutil
+import datetime
import io
import json
+import os
+import shutil
import textwrap
-import datetime
import unidecode
+from urllib.parse import urlsplit, urlunsplit
+
import dateutil.tz
import dateutil.zoneinfo
from mako.template import Template
from pkg_resources import resource_filename
-import tarfile
import nikola
-from nikola.nikola import DEFAULT_TRANSLATIONS_PATTERN, DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_FEED_READ_MORE_LINK, LEGAL_VALUES, urlsplit, urlunsplit
+from nikola.nikola import DEFAULT_INDEX_READ_MORE_LINK, DEFAULT_FEED_READ_MORE_LINK, LEGAL_VALUES
from nikola.plugin_categories import Command
-from nikola.utils import ask, ask_yesno, get_logger, makedirs, STDERR_HANDLER, load_messages
+from nikola.utils import ask, ask_yesno, get_logger, makedirs, load_messages
from nikola.packages.tzlocal import get_localzone
-LOGGER = get_logger('init', STDERR_HANDLER)
+LOGGER = get_logger('init')
SAMPLE_CONF = {
'BLOG_AUTHOR': "Your Name",
@@ -55,50 +55,51 @@ SAMPLE_CONF = {
'SITE_URL': "https://example.com/",
'BLOG_EMAIL': "joe@demo.site",
'BLOG_DESCRIPTION': "This is a demo site for Nikola.",
- 'PRETTY_URLS': False,
- 'STRIP_INDEXES': False,
+ 'PRETTY_URLS': True,
+ 'STRIP_INDEXES': True,
'DEFAULT_LANG': "en",
'TRANSLATIONS': """{
DEFAULT_LANG: "",
# Example for another language:
# "es": "./es",
}""",
- 'THEME': 'bootstrap3',
+ 'THEME': LEGAL_VALUES['DEFAULT_THEME'],
'TIMEZONE': 'UTC',
'COMMENT_SYSTEM': 'disqus',
'COMMENT_SYSTEM_ID': 'nikolademo',
'CATEGORY_ALLOW_HIERARCHIES': False,
'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
- 'TRANSLATIONS_PATTERN': DEFAULT_TRANSLATIONS_PATTERN,
'INDEX_READ_MORE_LINK': DEFAULT_INDEX_READ_MORE_LINK,
'FEED_READ_MORE_LINK': DEFAULT_FEED_READ_MORE_LINK,
'POSTS': """(
("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.md", "posts", "post.tmpl"),
("posts/*.txt", "posts", "post.tmpl"),
("posts/*.html", "posts", "post.tmpl"),
)""",
'PAGES': """(
- ("pages/*.rst", "pages", "story.tmpl"),
- ("pages/*.txt", "pages", "story.tmpl"),
- ("pages/*.html", "pages", "story.tmpl"),
+ ("pages/*.rst", "pages", "page.tmpl"),
+ ("pages/*.md", "pages", "page.tmpl"),
+ ("pages/*.txt", "pages", "page.tmpl"),
+ ("pages/*.html", "pages", "page.tmpl"),
)""",
'COMPILERS': """{
- "rest": ('.rst', '.txt'),
- "markdown": ('.md', '.mdown', '.markdown'),
- "textile": ('.textile',),
- "txt2tags": ('.t2t',),
- "bbcode": ('.bb',),
- "wiki": ('.wiki',),
- "ipynb": ('.ipynb',),
- "html": ('.html', '.htm'),
+ "rest": ['.rst', '.txt'],
+ "markdown": ['.md', '.mdown', '.markdown'],
+ "textile": ['.textile'],
+ "txt2tags": ['.t2t'],
+ "bbcode": ['.bb'],
+ "wiki": ['.wiki'],
+ "ipynb": ['.ipynb'],
+ "html": ['.html', '.htm'],
# PHP files are rendered the usual way (i.e. with the full templates).
# The resulting files have .php extensions, making it possible to run
# them without reconfiguring your server to recognize them.
- "php": ('.php',),
+ "php": ['.php'],
# Pandoc detects the input from the source filename
# but is disabled by default as it would conflict
# with many of the others.
- # "pandoc": ('.rst', '.md', '.txt'),
+ # "pandoc": ['.rst', '.md', '.txt'],
}""",
'NAVIGATION_LINKS': """{
DEFAULT_LANG: (
@@ -108,6 +109,7 @@ SAMPLE_CONF = {
),
}""",
'REDIRECTIONS': [],
+ '_METADATA_MAPPING_FORMATS': ', '.join(LEGAL_VALUES['METADATA_MAPPING'])
}
@@ -171,6 +173,14 @@ def format_default_translations_config(additional_languages):
return "{{\n{0}\n}}".format("\n".join(lang_paths))
+def get_default_translations_dict(default_lang, additional_languages):
+ """Generate a TRANSLATIONS dict matching the config from 'format_default_translations_config'."""
+ tr = {default_lang: ''}
+ for l in additional_languages:
+ tr[l] = './' + l
+ return tr
+
+
def format_navigation_links(additional_languages, default_lang, messages, strip_indexes=False):
"""Return the string to configure NAVIGATION_LINKS."""
f = u"""\
@@ -212,7 +222,7 @@ def prepare_config(config):
"""Parse sample config with JSON."""
p = config.copy()
p.update({k: json.dumps(v, ensure_ascii=False) for k, v in p.items()
- if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'FEED_READ_MORE_LINK')})
+ if k not in ('POSTS', 'PAGES', 'COMPILERS', 'TRANSLATIONS', 'NAVIGATION_LINKS', '_SUPPORTED_LANGUAGES', '_SUPPORTED_COMMENT_SYSTEMS', 'INDEX_READ_MORE_LINK', 'FEED_READ_MORE_LINK', '_METADATA_MAPPING_FORMATS')})
# READ_MORE_LINKs require some special treatment.
p['INDEX_READ_MORE_LINK'] = "'" + p['INDEX_READ_MORE_LINK'].replace("'", "\\'") + "'"
p['FEED_READ_MORE_LINK'] = "'" + p['FEED_READ_MORE_LINK'].replace("'", "\\'") + "'"
@@ -285,7 +295,7 @@ class CommandInit(Command):
@classmethod
def create_empty_site(cls, target):
"""Create an empty site with directories only."""
- for folder in ('files', 'galleries', 'listings', 'posts', 'pages'):
+ for folder in ('files', 'galleries', 'images', 'listings', 'posts', 'pages'):
makedirs(os.path.join(target, folder))
@staticmethod
@@ -323,7 +333,6 @@ class CommandInit(Command):
def prettyhandler(default, toconf):
SAMPLE_CONF['PRETTY_URLS'] = ask_yesno('Enable pretty URLs (/page/ instead of /page.html) that don\'t need web server configuration?', default=True)
- SAMPLE_CONF['STRIP_INDEXES'] = SAMPLE_CONF['PRETTY_URLS']
def lhandler(default, toconf, show_header=True):
if show_header:
@@ -354,9 +363,8 @@ class CommandInit(Command):
# Get messages for navigation_links. In order to do this, we need
# to generate a throwaway TRANSLATIONS dict.
- tr = {default: ''}
- for l in langs:
- tr[l] = './' + l
+ tr = get_default_translations_dict(default, langs)
+
# Assuming that base contains all the locales, and that base does
# not inherit from anywhere.
try:
@@ -377,22 +385,22 @@ class CommandInit(Command):
while not answered:
try:
lz = get_localzone()
- except:
+ except Exception:
lz = None
answer = ask('Time zone', lz if lz else "UTC")
tz = dateutil.tz.gettz(answer)
if tz is None:
print(" WARNING: Time zone not found. Searching list of time zones for a match.")
- zonesfile = tarfile.open(fileobj=dateutil.zoneinfo.getzoneinfofile_stream())
- zonenames = [zone for zone in zonesfile.getnames() if answer.lower() in zone.lower()]
- if len(zonenames) == 1:
- tz = dateutil.tz.gettz(zonenames[0])
- answer = zonenames[0]
+ all_zones = dateutil.zoneinfo.get_zonefile_instance().zones
+ matching_zones = [zone for zone in all_zones if answer.lower() in zone.lower()]
+ if len(matching_zones) == 1:
+ tz = dateutil.tz.gettz(matching_zones[0])
+ answer = matching_zones[0]
print(" Picking '{0}'.".format(answer))
- elif len(zonenames) > 1:
+ elif len(matching_zones) > 1:
print(" The following time zones match your query:")
- print(' ' + '\n '.join(zonenames))
+ print(' ' + '\n '.join(matching_zones))
continue
if tz is not None:
diff --git a/nikola/plugins/command/install_theme.plugin b/nikola/plugins/command/install_theme.plugin
deleted file mode 100644
index aa68773..0000000
--- a/nikola/plugins/command/install_theme.plugin
+++ /dev/null
@@ -1,13 +0,0 @@
-[Core]
-name = install_theme
-module = install_theme
-
-[Documentation]
-author = Roberto Alsina
-version = 1.0
-website = https://getnikola.com/
-description = Install a theme into the current site.
-
-[Nikola]
-plugincategory = Command
-
diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py
deleted file mode 100644
index 28f7aa3..0000000
--- a/nikola/plugins/command/install_theme.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Install a theme."""
-
-from __future__ import print_function
-
-from nikola import utils
-from nikola.plugin_categories import Command
-LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER)
-
-
-class CommandInstallTheme(Command):
- """Install a theme."""
-
- name = "install_theme"
- doc_usage = "[[-u] theme_name] | [[-u] -l]"
- doc_purpose = "install theme into current site"
- output_dir = 'themes'
- cmd_options = [
- {
- 'name': 'list',
- 'short': 'l',
- 'long': 'list',
- 'type': bool,
- 'default': False,
- 'help': 'Show list of available themes.'
- },
- {
- 'name': 'url',
- 'short': 'u',
- 'long': 'url',
- 'type': str,
- 'help': "URL for the theme repository (default: "
- "https://themes.getnikola.com/v7/themes.json)",
- 'default': 'https://themes.getnikola.com/v7/themes.json'
- },
- {
- 'name': 'getpath',
- 'short': 'g',
- 'long': 'get-path',
- 'type': bool,
- 'default': False,
- 'help': "Print the path for installed theme",
- },
- ]
-
- def _execute(self, options, args):
- """Install theme into current site."""
- p = self.site.plugin_manager.getPluginByName('theme', 'Command').plugin_object
- listing = options['list']
- url = options['url']
- if args:
- name = args[0]
- else:
- name = None
-
- if options['getpath'] and name:
- return p.get_path(name)
-
- if name is None and not listing:
- LOGGER.error("This command needs either a theme name or the -l option.")
- return False
-
- if listing:
- p.list_available(url)
- else:
- p.do_install_deps(url, name)
diff --git a/nikola/plugins/command/new_page.plugin b/nikola/plugins/command/new_page.plugin
index 3eaecb4..8734805 100644
--- a/nikola/plugins/command/new_page.plugin
+++ b/nikola/plugins/command/new_page.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create a new page.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/new_page.py b/nikola/plugins/command/new_page.py
index c09b4be..0f7996a 100644
--- a/nikola/plugins/command/new_page.py
+++ b/nikola/plugins/command/new_page.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others.
+# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Create a new page."""
-from __future__ import unicode_literals, print_function
from nikola.plugin_categories import Command
@@ -107,6 +106,7 @@ class CommandNewPage(Command):
options['tags'] = ''
options['schedule'] = False
options['is_page'] = True
+ options['date-path'] = False
# Even though stuff was split into `new_page`, it’s easier to do it
# there not to duplicate the code.
p = self.site.plugin_manager.getPluginByName('new_post', 'Command').plugin_object
diff --git a/nikola/plugins/command/new_post.plugin b/nikola/plugins/command/new_post.plugin
index e9c3af5..efdeb58 100644
--- a/nikola/plugins/command/new_post.plugin
+++ b/nikola/plugins/command/new_post.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create a new post.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/new_post.py b/nikola/plugins/command/new_post.py
index 36cc04f..e6eabbd 100644
--- a/nikola/plugins/command/new_post.py
+++ b/nikola/plugins/command/new_post.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Create a new post."""
-from __future__ import unicode_literals, print_function
import io
import datetime
import operator
@@ -35,15 +34,15 @@ import shutil
import subprocess
import sys
-from blinker import signal
import dateutil.tz
+from blinker import signal
from nikola.plugin_categories import Command
from nikola import utils
COMPILERS_DOC_LINK = 'https://getnikola.com/handbook.html#configuring-other-input-formats'
-POSTLOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER)
-PAGELOGGER = utils.get_logger('new_page', utils.STDERR_HANDLER)
+POSTLOGGER = utils.get_logger('new_post')
+PAGELOGGER = utils.get_logger('new_page')
LOGGER = POSTLOGGER
@@ -90,7 +89,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False):
except ImportError:
LOGGER.error('To use the --schedule switch of new_post, '
'you have to install the "dateutil" package.')
- rrule = None # NOQA
+ rrule = None
if schedule and rrule and rule:
try:
rule_ = rrule.rrulestr(rule, dtstart=last_date or date)
@@ -111,7 +110,7 @@ def get_date(schedule=False, rule=None, last_date=None, tz=None, iso8601=False):
else:
tz_str = ' UTC'
- return date.strftime('%Y-%m-%d %H:%M:%S') + tz_str
+ return (date.strftime('%Y-%m-%d %H:%M:%S') + tz_str, date)
class CommandNewPost(Command):
@@ -204,7 +203,14 @@ class CommandNewPost(Command):
'default': '',
'help': 'Import an existing file instead of creating a placeholder'
},
-
+ {
+ 'name': 'date-path',
+ 'short': 'd',
+ 'long': 'date-path',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Create post with date path (eg. year/month/day, see NEW_POST_DATE_PATH_FORMAT in config)'
+ },
]
def _execute(self, options, args):
@@ -234,6 +240,10 @@ class CommandNewPost(Command):
twofile = options['twofile']
import_file = options['import']
wants_available = options['available-formats']
+ date_path_opt = options['date-path']
+ date_path_auto = self.site.config['NEW_POST_DATE_PATH'] and content_type == 'post'
+ date_path_format = self.site.config['NEW_POST_DATE_PATH_FORMAT'].strip('/')
+ post_type = options.get('type', 'text')
if wants_available:
self.print_compilers()
@@ -255,16 +265,39 @@ class CommandNewPost(Command):
if "@" in content_format:
content_format, content_subformat = content_format.split("@")
- if not content_format: # Issue #400
+ if not content_format and path and not os.path.isdir(path):
+ # content_format not specified. If path was given, use
+ # it to guess (Issue #2798)
+ extension = os.path.splitext(path)[-1]
+ for compiler, extensions in self.site.config['COMPILERS'].items():
+ if extension in extensions:
+ content_format = compiler
+ if not content_format:
+ LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension))
+ return
+
+ elif not content_format and import_file:
+ # content_format not specified. If import_file was given, use
+ # it to guess (Issue #2798)
+ extension = os.path.splitext(import_file)[-1]
+ for compiler, extensions in self.site.config['COMPILERS'].items():
+ if extension in extensions:
+ content_format = compiler
+ if not content_format:
+ LOGGER.error("Unknown {0} extension {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, extension))
+ return
+
+ elif not content_format: # Issue #400
content_format = get_default_compiler(
is_post,
self.site.config['COMPILERS'],
self.site.config['post_pages'])
- if content_format not in compiler_names:
- LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin?".format(content_type, content_format))
+ elif content_format not in compiler_names:
+ LOGGER.error("Unknown {0} format {1}, maybe you need to install a plugin or enable an existing one?".format(content_type, content_format))
self.print_compilers()
return
+
compiler_plugin = self.site.plugin_manager.getPluginByName(
content_format, "PageCompiler").plugin_object
@@ -286,7 +319,7 @@ class CommandNewPost(Command):
while not title:
title = utils.ask('Title')
- if isinstance(title, utils.bytes_str):
+ if isinstance(title, bytes):
try:
title = title.decode(sys.stdin.encoding)
except (AttributeError, TypeError): # for tests
@@ -296,26 +329,34 @@ class CommandNewPost(Command):
if not path:
slug = utils.slugify(title, lang=self.site.default_lang)
else:
- if isinstance(path, utils.bytes_str):
+ if isinstance(path, bytes):
try:
path = path.decode(sys.stdin.encoding)
except (AttributeError, TypeError): # for tests
path = path.decode('utf-8')
- slug = utils.slugify(os.path.splitext(os.path.basename(path))[0], lang=self.site.default_lang)
+ if os.path.isdir(path):
+ # If the user provides a directory, add the file name generated from title (Issue #2651)
+ slug = utils.slugify(title, lang=self.site.default_lang)
+ pattern = os.path.basename(entry[0])
+ suffix = pattern[1:]
+ path = os.path.join(path, slug + suffix)
+ else:
+ slug = utils.slugify(os.path.splitext(os.path.basename(path))[0], lang=self.site.default_lang)
- if isinstance(author, utils.bytes_str):
- try:
- author = author.decode(sys.stdin.encoding)
- except (AttributeError, TypeError): # for tests
- author = author.decode('utf-8')
+ if isinstance(author, bytes):
+ try:
+ author = author.decode(sys.stdin.encoding)
+ except (AttributeError, TypeError): # for tests
+ author = author.decode('utf-8')
# Calculate the date to use for the content
- schedule = options['schedule'] or self.site.config['SCHEDULE_ALL']
+ # SCHEDULE_ALL is post-only (Issue #2921)
+ schedule = options['schedule'] or (self.site.config['SCHEDULE_ALL'] and is_post)
rule = self.site.config['SCHEDULE_RULE']
self.site.scan_posts()
timeline = self.site.timeline
last_date = None if not timeline else timeline[0].date
- date = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601'])
+ date, dateobj = get_date(schedule, rule, last_date, self.site.tzinfo, self.site.config['FORCE_ISO8601'])
data = {
'title': title,
'slug': slug,
@@ -323,17 +364,21 @@ class CommandNewPost(Command):
'tags': tags,
'link': '',
'description': '',
- 'type': 'text',
+ 'type': post_type,
}
if not path:
pattern = os.path.basename(entry[0])
suffix = pattern[1:]
output_path = os.path.dirname(entry[0])
+ if date_path_auto or date_path_opt:
+ output_path += os.sep + dateobj.strftime(date_path_format)
txt_path = os.path.join(output_path, slug + suffix)
meta_path = os.path.join(output_path, slug + ".meta")
else:
+ if date_path_opt:
+ LOGGER.warning("A path has been specified, ignoring -d")
txt_path = os.path.join(self.site.original_cwd, path)
meta_path = os.path.splitext(txt_path)[0] + ".meta"
@@ -360,18 +405,18 @@ class CommandNewPost(Command):
metadata.update(self.site.config['ADDITIONAL_METADATA'])
data.update(metadata)
- # ipynb plugin needs the ipython kernel info. We get the kernel name
+ # ipynb plugin needs the Jupyter kernel info. We get the kernel name
# from the content_subformat and pass it to the compiler in the metadata
if content_format == "ipynb" and content_subformat is not None:
- metadata["ipython_kernel"] = content_subformat
+ metadata["jupyter_kernel"] = content_subformat
# Override onefile if not really supported.
if not compiler_plugin.supports_onefile and onefile:
onefile = False
- LOGGER.warn('This compiler does not support one-file posts.')
+ LOGGER.warning('This compiler does not support one-file posts.')
if onefile and import_file:
- with io.open(import_file, 'r', encoding='utf-8') as fh:
+ with io.open(import_file, 'r', encoding='utf-8-sig') as fh:
content = fh.read()
elif not import_file:
if is_page:
@@ -385,13 +430,13 @@ class CommandNewPost(Command):
else:
compiler_plugin.create_post(
txt_path, content=content, onefile=onefile, title=title,
- slug=slug, date=date, tags=tags, is_page=is_page, **metadata)
+ slug=slug, date=date, tags=tags, is_page=is_page, type=post_type, **metadata)
event = dict(path=txt_path)
if not onefile: # write metadata file
with io.open(meta_path, "w+", encoding="utf8") as fd:
- fd.write(utils.write_metadata(data))
+ fd.write(utils.write_metadata(data, comment_wrap=False, site=self.site))
LOGGER.info("Your {0}'s metadata is at: {1}".format(content_type, meta_path))
event['meta_path'] = meta_path
LOGGER.info("Your {0}'s text is at: {1}".format(content_type, txt_path))
@@ -406,7 +451,7 @@ class CommandNewPost(Command):
if editor:
subprocess.call(to_run)
else:
- LOGGER.error('$EDITOR not set, cannot edit the post. Please do it manually.')
+ LOGGER.error('The $EDITOR environment variable is not set, cannot edit the post with \'-e\'. Please edit the post manually.')
def filter_post_pages(self, compiler, is_post):
"""Return the correct entry from post_pages.
@@ -523,6 +568,6 @@ class CommandNewPost(Command):
More compilers are available in the Plugins Index.
Compilers marked with ! and ~ require additional configuration:
- ! not in the PAGES/POSTS tuples (unused)
+ ! not in the POSTS/PAGES tuples and any post scanners (unused)
~ not in the COMPILERS dict (disabled)
Read more: {0}""".format(COMPILERS_DOC_LINK))
diff --git a/nikola/plugins/command/orphans.plugin b/nikola/plugins/command/orphans.plugin
index d20c539..5107032 100644
--- a/nikola/plugins/command/orphans.plugin
+++ b/nikola/plugins/command/orphans.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = List all orphans
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/orphans.py b/nikola/plugins/command/orphans.py
index 5e2574d..0cf2e63 100644
--- a/nikola/plugins/command/orphans.py
+++ b/nikola/plugins/command/orphans.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others.
+# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""List all orphans."""
-from __future__ import print_function
import os
from nikola.plugin_categories import Command
diff --git a/nikola/plugins/command/plugin.plugin b/nikola/plugins/command/plugin.plugin
index 016bcaa..db99ceb 100644
--- a/nikola/plugins/command/plugin.plugin
+++ b/nikola/plugins/command/plugin.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Manage Nikola plugins
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/plugin.py b/nikola/plugins/command/plugin.py
index 364f343..33dee23 100644
--- a/nikola/plugins/command/plugin.py
+++ b/nikola/plugins/command/plugin.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,8 +26,8 @@
"""Manage plugins."""
-from __future__ import print_function
import io
+import json.decoder
import os
import sys
import shutil
@@ -42,7 +42,7 @@ from pygments.formatters import TerminalFormatter
from nikola.plugin_categories import Command
from nikola import utils
-LOGGER = utils.get_logger('plugin', utils.STDERR_HANDLER)
+LOGGER = utils.get_logger('plugin')
class CommandPlugin(Command):
@@ -84,9 +84,8 @@ class CommandPlugin(Command):
'short': 'u',
'long': 'url',
'type': str,
- 'help': "URL for the plugin repository (default: "
- "https://plugins.getnikola.com/v7/plugins.json)",
- 'default': 'https://plugins.getnikola.com/v7/plugins.json'
+ 'help': "URL for the plugin repository",
+ 'default': 'https://plugins.getnikola.com/v8/plugins.json'
},
{
'name': 'user',
@@ -137,11 +136,11 @@ class CommandPlugin(Command):
self.output_dir = options.get('output_dir')
else:
if not self.site.configured and not user_mode and install:
- LOGGER.notice('No site found, assuming --user')
+ LOGGER.warning('No site found, assuming --user')
user_mode = True
if user_mode:
- self.output_dir = os.path.expanduser('~/.nikola/plugins')
+ self.output_dir = os.path.expanduser(os.path.join('~', '.nikola', 'plugins'))
else:
self.output_dir = 'plugins'
@@ -179,9 +178,18 @@ class CommandPlugin(Command):
plugins.sort()
print('Installed Plugins:')
print('------------------')
+ maxlength = max(len(i[0]) for i in plugins)
+ if self.site.colorful:
+ formatstring = '\x1b[1m{0:<{2}}\x1b[0m at {1}'
+ else:
+ formatstring = '{0:<{2}} at {1}'
for name, path in plugins:
- print('{0} at {1}'.format(name, path))
- print('\n\nAlso, you have disabled these plugins: {}'.format(self.site.config['DISABLED_PLUGINS']))
+ print(formatstring.format(name, path, maxlength))
+ dp = self.site.config['DISABLED_PLUGINS']
+ if dp:
+ print('\n\nAlso, you have disabled these plugins: {}'.format(', '.join(dp)))
+ else:
+ print('\n\nNo plugins are disabled.')
return 0
def do_upgrade(self, url):
@@ -235,43 +243,32 @@ class CommandPlugin(Command):
utils.extract_all(zip_file, self.output_dir)
dest_path = os.path.join(self.output_dir, name)
else:
- try:
- plugin_path = utils.get_plugin_path(name)
- except:
- LOGGER.error("Can't find plugin " + name)
- return 1
-
- utils.makedirs(self.output_dir)
- dest_path = os.path.join(self.output_dir, name)
- if os.path.exists(dest_path):
- LOGGER.error("{0} is already installed".format(name))
- return 1
-
- LOGGER.info('Copying {0} into plugins'.format(plugin_path))
- shutil.copytree(plugin_path, dest_path)
+ LOGGER.error("Can't find plugin " + name)
+ return 1
reqpath = os.path.join(dest_path, 'requirements.txt')
if os.path.exists(reqpath):
- LOGGER.notice('This plugin has Python dependencies.')
+ LOGGER.warning('This plugin has Python dependencies.')
LOGGER.info('Installing dependencies with pip...')
try:
subprocess.check_call((sys.executable, '-m', 'pip', 'install', '-r', reqpath))
except subprocess.CalledProcessError:
LOGGER.error('Could not install the dependencies.')
print('Contents of the requirements.txt file:\n')
- with io.open(reqpath, 'r', encoding='utf-8') as fh:
+ with io.open(reqpath, 'r', encoding='utf-8-sig') as fh:
print(utils.indent(fh.read(), 4 * ' '))
print('You have to install those yourself or through a '
'package manager.')
else:
LOGGER.info('Dependency installation succeeded.')
+
reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt')
if os.path.exists(reqnpypath):
- LOGGER.notice('This plugin has third-party '
- 'dependencies you need to install '
- 'manually.')
+ LOGGER.warning('This plugin has third-party '
+ 'dependencies you need to install '
+ 'manually.')
print('Contents of the requirements-nonpy.txt file:\n')
- with io.open(reqnpypath, 'r', encoding='utf-8') as fh:
+ with io.open(reqnpypath, 'r', encoding='utf-8-sig') as fh:
for l in fh.readlines():
i, j = l.split('::')
print(utils.indent(i.strip(), 4 * ' '))
@@ -280,17 +277,36 @@ class CommandPlugin(Command):
print('You have to install those yourself or through a package '
'manager.')
+
+ req_plug_path = os.path.join(dest_path, 'requirements-plugins.txt')
+ if os.path.exists(req_plug_path):
+ LOGGER.info('This plugin requires other Nikola plugins.')
+ LOGGER.info('Installing plugins...')
+ plugin_failure = False
+ try:
+ with io.open(req_plug_path, 'r', encoding='utf-8-sig') as inf:
+ for plugname in inf.readlines():
+ plugin_failure = self.do_install(url, plugname.strip(), show_install_notes) != 0
+ except Exception:
+ plugin_failure = True
+ if plugin_failure:
+ LOGGER.error('Could not install a plugin.')
+ print('Contents of the requirements-plugins.txt file:\n')
+ with io.open(req_plug_path, 'r', encoding='utf-8-sig') as fh:
+ print(utils.indent(fh.read(), 4 * ' '))
+ print('You have to install those yourself manually.')
+ else:
+ LOGGER.info('Dependency installation succeeded.')
+
confpypath = os.path.join(dest_path, 'conf.py.sample')
if os.path.exists(confpypath) and show_install_notes:
- LOGGER.notice('This plugin has a sample config file. Integrate it with yours in order to make this plugin work!')
+ LOGGER.warning('This plugin has a sample config file. Integrate it with yours in order to make this plugin work!')
print('Contents of the conf.py.sample file:\n')
- with io.open(confpypath, 'r', encoding='utf-8') as fh:
+ with io.open(confpypath, 'r', encoding='utf-8-sig') as fh:
if self.site.colorful:
- print(utils.indent(pygments.highlight(
- fh.read(), PythonLexer(), TerminalFormatter()),
- 4 * ' '))
+ print(pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter()))
else:
- print(utils.indent(fh.read(), 4 * ' '))
+ print(fh.read())
return 0
def do_uninstall(self, name):
@@ -320,10 +336,19 @@ class CommandPlugin(Command):
"""Download the JSON file with all plugins."""
if self.json is None:
try:
- self.json = requests.get(url).json()
- except requests.exceptions.SSLError:
- LOGGER.warning("SSL error, using http instead of https (press ^C to abort)")
- time.sleep(1)
- url = url.replace('https', 'http', 1)
- self.json = requests.get(url).json()
+ try:
+ self.json = requests.get(url).json()
+ except requests.exceptions.SSLError:
+ LOGGER.warning("SSL error, using http instead of https (press ^C to abort)")
+ time.sleep(1)
+ url = url.replace('https', 'http', 1)
+ self.json = requests.get(url).json()
+ except json.decoder.JSONDecodeError as e:
+ LOGGER.error("Failed to decode JSON data in response from server.")
+ LOGGER.error("JSON error encountered: " + str(e))
+ LOGGER.error("This issue might be caused by server-side issues, or by to unusual activity in your "
+ "network (as determined by CloudFlare). Please visit https://plugins.getnikola.com/ in "
+ "a browser.")
+ sys.exit(2)
+
return self.json
diff --git a/nikola/plugins/command/rst2html.plugin b/nikola/plugins/command/rst2html.plugin
index a095705..6f2fb25 100644
--- a/nikola/plugins/command/rst2html.plugin
+++ b/nikola/plugins/command/rst2html.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Compile reStructuredText to HTML using the Nikola architecture
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/rst2html/__init__.py b/nikola/plugins/command/rst2html/__init__.py
index c877f63..5576b35 100644
--- a/nikola/plugins/command/rst2html/__init__.py
+++ b/nikola/plugins/command/rst2html/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2015-2016 Chris Warrick and others.
+# Copyright © 2015-2020 Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Compile reStructuredText to HTML, using Nikola architecture."""
-from __future__ import unicode_literals, print_function
import io
import lxml.html
@@ -50,12 +49,12 @@ class CommandRst2Html(Command):
print("This command takes only one argument (input file name).")
return 2
source = args[0]
- with io.open(source, "r", encoding="utf8") as in_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
data = in_file.read()
- output, error_level, deps = compiler.compile_html_string(data, source, True)
+ output, error_level, deps, shortcode_deps = compiler.compile_string(data, source, True)
- rstcss_path = resource_filename('nikola', 'data/themes/base/assets/css/rst.css')
- with io.open(rstcss_path, "r", encoding="utf8") as fh:
+ rstcss_path = resource_filename('nikola', 'data/themes/base/assets/css/rst_base.css')
+ with io.open(rstcss_path, "r", encoding="utf-8-sig") as fh:
rstcss = fh.read()
template_path = resource_filename('nikola', 'plugins/command/rst2html/rst2html.tmpl')
diff --git a/nikola/plugins/command/serve.plugin b/nikola/plugins/command/serve.plugin
index a4a726f..aa40073 100644
--- a/nikola/plugins/command/serve.plugin
+++ b/nikola/plugins/command/serve.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Start test server.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py
index c9702d5..ede5179 100644
--- a/nikola/plugins/command/serve.py
+++ b/nikola/plugins/command/serve.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,26 +26,18 @@
"""Start test server."""
-from __future__ import print_function
import os
+import sys
import re
+import signal
import socket
import webbrowser
-try:
- from BaseHTTPServer import HTTPServer
- from SimpleHTTPServer import SimpleHTTPRequestHandler
-except ImportError:
- from http.server import HTTPServer # NOQA
- from http.server import SimpleHTTPRequestHandler # NOQA
-
-try:
- from StringIO import StringIO
-except ImportError:
- from io import BytesIO as StringIO # NOQA
-
+from http.server import HTTPServer
+from http.server import SimpleHTTPRequestHandler
+from io import BytesIO as StringIO
from nikola.plugin_categories import Command
-from nikola.utils import dns_sd, get_logger, STDERR_HANDLER
+from nikola.utils import dns_sd
class IPv6Server(HTTPServer):
@@ -60,7 +52,6 @@ class CommandServe(Command):
name = "serve"
doc_usage = "[options]"
doc_purpose = "start the test webserver"
- logger = None
dns_sd = None
cmd_options = (
@@ -70,7 +61,7 @@ class CommandServe(Command):
'long': 'port',
'default': 8000,
'type': int,
- 'help': 'Port number (default: 8000)',
+ 'help': 'Port number',
},
{
'name': 'address',
@@ -78,7 +69,7 @@ class CommandServe(Command):
'long': 'address',
'type': str,
'default': '',
- 'help': 'Address to bind (default: 0.0.0.0 -- all local IPv4 interfaces)',
+ 'help': 'Address to bind, defaults to all local IPv4 interfaces',
},
{
'name': 'detach',
@@ -106,13 +97,24 @@ class CommandServe(Command):
},
)
+ def shutdown(self, signum=None, _frame=None):
+ """Shut down the server that is running detached."""
+ if self.dns_sd:
+ self.dns_sd.Reset()
+ if os.path.exists(self.serve_pidfile):
+ os.remove(self.serve_pidfile)
+ if not self.detached:
+ self.logger.info("Server is shutting down.")
+ if signum:
+ sys.exit(0)
+
def _execute(self, options, args):
"""Start test server."""
- self.logger = get_logger('serve', STDERR_HANDLER)
out_dir = self.site.config['OUTPUT_FOLDER']
if not os.path.isdir(out_dir):
self.logger.error("Missing '{0}' folder?".format(out_dir))
else:
+ self.serve_pidfile = os.path.abspath('nikolaserve.pid')
os.chdir(out_dir)
if '[' in options['address']:
options['address'] = options['address'].strip('[').strip(']')
@@ -128,35 +130,43 @@ class CommandServe(Command):
httpd = OurHTTP((options['address'], options['port']),
OurHTTPRequestHandler)
sa = httpd.socket.getsockname()
- self.logger.info("Serving HTTP on {0} port {1}...".format(*sa))
+ if ipv6:
+ server_url = "http://[{0}]:{1}/".format(*sa)
+ else:
+ server_url = "http://{0}:{1}/".format(*sa)
+ self.logger.info("Serving on {0} ...".format(server_url))
+
if options['browser']:
- if ipv6:
- server_url = "http://[{0}]:{1}/".format(*sa)
- else:
- server_url = "http://{0}:{1}/".format(*sa)
+ # Some browsers fail to load 0.0.0.0 (Issue #2755)
+ if sa[0] == '0.0.0.0':
+ server_url = "http://127.0.0.1:{1}/".format(*sa)
self.logger.info("Opening {0} in the default web browser...".format(server_url))
webbrowser.open(server_url)
if options['detach']:
+ self.detached = True
OurHTTPRequestHandler.quiet = True
try:
pid = os.fork()
if pid == 0:
+ signal.signal(signal.SIGTERM, self.shutdown)
httpd.serve_forever()
else:
- self.logger.info("Detached with PID {0}. Run `kill {0}` to stop the server.".format(pid))
- except AttributeError as e:
+ with open(self.serve_pidfile, 'w') as fh:
+ fh.write('{0}\n'.format(pid))
+ self.logger.info("Detached with PID {0}. Run `kill {0}` or `kill $(cat nikolaserve.pid)` to stop the server.".format(pid))
+ except AttributeError:
if os.name == 'nt':
self.logger.warning("Detaching is not available on Windows, server is running in the foreground.")
else:
- raise e
+ raise
else:
+ self.detached = False
try:
self.dns_sd = dns_sd(options['port'], (options['ipv6'] or '::' in options['address']))
+ signal.signal(signal.SIGTERM, self.shutdown)
httpd.serve_forever()
except KeyboardInterrupt:
- self.logger.info("Server is shutting down.")
- if self.dns_sd:
- self.dns_sd.Reset()
+ self.shutdown()
return 130
@@ -172,8 +182,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
if self.quiet:
return
else:
- # Old-style class in Python 2.7, cannot use super()
- return SimpleHTTPRequestHandler.log_message(self, *args)
+ return super().log_message(*args)
# NOTICE: this is a patched version of send_head() to disable all sorts of
# caching. `nikola serve` is a development server, hence caching should
@@ -185,9 +194,9 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
# Note that it might break in future versions of Python, in which case we
# would need to do even more magic.
def send_head(self):
- """Common code for GET and HEAD commands.
+ """Send response code and MIME header.
- This sends the response code and MIME headers.
+ This is common code for GET and HEAD commands.
Return value is either a file object (which has to be copied
to the outputfile by the caller unless the command was HEAD,
@@ -198,10 +207,12 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
path = self.translate_path(self.path)
f = None
if os.path.isdir(path):
- if not self.path.endswith('/'):
+ path_parts = list(self.path.partition('?'))
+ if not path_parts[0].endswith('/'):
# redirect browser - doing basically what apache does
+ path_parts[0] += '/'
self.send_response(301)
- self.send_header("Location", self.path + "/")
+ self.send_header("Location", ''.join(path_parts))
# begin no-cache patch
# For redirects. With redirects, caching is even worse and can
# break more. Especially with 301 Moved Permanently redirects,
@@ -227,7 +238,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
# transmitted *less* than the content-length!
f = open(path, 'rb')
except IOError:
- self.send_error(404, "File not found")
+ self.send_error(404, "File not found: {}".format(path))
return None
filtered_bytes = None
@@ -235,7 +246,7 @@ class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
# Comment out any <base> to allow local resolution of relative URLs.
data = f.read().decode('utf8')
f.close()
- data = re.sub(r'<base\s([^>]*)>', '<!--base \g<1>-->', data, re.IGNORECASE)
+ data = re.sub(r'<base\s([^>]*)>', r'<!--base \g<1>-->', data, flags=re.IGNORECASE)
data = data.encode('utf8')
f = StringIO()
f.write(data)
diff --git a/nikola/plugins/command/status.plugin b/nikola/plugins/command/status.plugin
index 91390d2..7e2bd96 100644
--- a/nikola/plugins/command/status.plugin
+++ b/nikola/plugins/command/status.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com
description = Site status
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/status.py b/nikola/plugins/command/status.py
index b3ffbb4..c96d13f 100644
--- a/nikola/plugins/command/status.py
+++ b/nikola/plugins/command/status.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Display site status."""
-from __future__ import print_function
import os
from datetime import datetime
from dateutil.tz import gettz, tzlocal
diff --git a/nikola/plugins/command/bootswatch_theme.plugin b/nikola/plugins/command/subtheme.plugin
index 51e6718..d377e22 100644
--- a/nikola/plugins/command/bootswatch_theme.plugin
+++ b/nikola/plugins/command/subtheme.plugin
@@ -1,13 +1,13 @@
[Core]
-name = bootswatch_theme
-module = bootswatch_theme
+name = subtheme
+module = subtheme
[Documentation]
author = Roberto Alsina
-version = 1.0
+version = 1.1
website = https://getnikola.com/
-description = Given a swatch name and a parent theme, creates a custom theme.
+description = Given a swatch name and a parent theme, creates a custom subtheme.
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/subtheme.py b/nikola/plugins/command/subtheme.py
new file mode 100644
index 0000000..554a241
--- /dev/null
+++ b/nikola/plugins/command/subtheme.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Given a swatch name from bootswatch.com or hackerthemes.com and a parent theme, creates a custom theme."""
+
+import configparser
+import os
+
+import requests
+
+from nikola import utils
+from nikola.plugin_categories import Command
+
+LOGGER = utils.get_logger('subtheme')
+
+
+def _check_for_theme(theme, themes):
+ for t in themes:
+ if t.endswith(os.sep + theme):
+ return True
+ return False
+
+
+class CommandSubTheme(Command):
+ """Given a swatch name from bootswatch.com and a parent theme, creates a custom theme."""
+
+ name = "subtheme"
+ doc_usage = "[options]"
+ doc_purpose = "given a swatch name from bootswatch.com or hackerthemes.com and a parent theme, creates a custom"\
+ " theme"
+ cmd_options = [
+ {
+ 'name': 'name',
+ 'short': 'n',
+ 'long': 'name',
+ 'default': 'custom',
+ 'type': str,
+ 'help': 'New theme name',
+ },
+ {
+ 'name': 'swatch',
+ 'short': 's',
+ 'default': '',
+ 'type': str,
+ 'help': 'Name of the swatch from bootswatch.com.'
+ },
+ {
+ 'name': 'parent',
+ 'short': 'p',
+ 'long': 'parent',
+ 'default': 'bootstrap4',
+ 'help': 'Parent theme name',
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Given a swatch name and a parent theme, creates a custom theme."""
+ name = options['name']
+ swatch = options['swatch']
+ if not swatch:
+ LOGGER.error('The -s option is mandatory')
+ return 1
+ parent = options['parent']
+ version = '4'
+
+ # Check which Bootstrap version to use
+ themes = utils.get_theme_chain(parent, self.site.themes_dirs)
+ if _check_for_theme('bootstrap', themes) or _check_for_theme('bootstrap-jinja', themes):
+ version = '2'
+ elif _check_for_theme('bootstrap3', themes) or _check_for_theme('bootstrap3-jinja', themes):
+ version = '3'
+ elif _check_for_theme('bootstrap4', themes) or _check_for_theme('bootstrap4-jinja', themes):
+ version = '4'
+ elif not _check_for_theme('bootstrap4', themes) and not _check_for_theme('bootstrap4-jinja', themes):
+ LOGGER.warning(
+ '"subtheme" only makes sense for themes that use bootstrap')
+ elif _check_for_theme('bootstrap3-gradients', themes) or _check_for_theme('bootstrap3-gradients-jinja', themes):
+ LOGGER.warning(
+ '"subtheme" doesn\'t work well with the bootstrap3-gradients family')
+
+ LOGGER.info("Creating '{0}' theme from '{1}' and '{2}'".format(
+ name, swatch, parent))
+ utils.makedirs(os.path.join('themes', name, 'assets', 'css'))
+ for fname in ('bootstrap.min.css', 'bootstrap.css'):
+ if swatch in [
+ 'bubblegum', 'business-tycoon', 'charming', 'daydream',
+ 'executive-suite', 'good-news', 'growth', 'harbor', 'hello-world',
+ 'neon-glow', 'pleasant', 'retro', 'vibrant-sea', 'wizardry']: # Hackerthemes
+ LOGGER.info(
+ 'Hackertheme-based subthemes often require you use a custom font for full effect.')
+ if version != '4':
+ LOGGER.error(
+ 'The hackertheme subthemes are only available for Bootstrap 4.')
+ return 1
+ if fname == 'bootstrap.css':
+ url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.css'.format(
+ swatch=swatch)
+ else:
+ url = 'https://raw.githubusercontent.com/HackerThemes/theme-machine/master/dist/{swatch}/css/bootstrap4-{swatch}.min.css'.format(
+ swatch=swatch)
+ else: # Bootswatch
+ url = 'https://bootswatch.com'
+ if version:
+ url += '/' + version
+ url = '/'.join((url, swatch, fname))
+ LOGGER.info("Downloading: " + url)
+ r = requests.get(url)
+ if r.status_code > 299:
+ LOGGER.error('Error {} getting {}', r.status_code, url)
+ return 1
+ data = r.text
+
+ with open(os.path.join('themes', name, 'assets', 'css', fname),
+ 'w+') as output:
+ output.write(data)
+
+ with open(os.path.join('themes', name, '%s.theme' % name), 'w+') as output:
+ parent_theme_data_path = utils.get_asset_path(
+ '%s.theme' % parent, themes)
+ cp = configparser.ConfigParser()
+ cp.read(parent_theme_data_path)
+ cp['Theme']['parent'] = parent
+ cp['Family'] = {'family': cp['Family']['family']}
+ cp.write(output)
+
+ LOGGER.info(
+ 'Theme created. Change the THEME setting to "{0}" to use it.'.format(name))
diff --git a/nikola/plugins/command/theme.plugin b/nikola/plugins/command/theme.plugin
index b0c1886..421d027 100644
--- a/nikola/plugins/command/theme.plugin
+++ b/nikola/plugins/command/theme.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Manage Nikola themes
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/theme.py b/nikola/plugins/command/theme.py
index 7513491..6f4339a 100644
--- a/nikola/plugins/command/theme.py
+++ b/nikola/plugins/command/theme.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina, Chris Warrick and others.
+# Copyright © 2012-2020 Roberto Alsina, Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,13 +26,15 @@
"""Manage themes."""
-from __future__ import print_function
-import os
+import configparser
import io
+import json.decoder
+import os
import shutil
+import sys
import time
-import requests
+import requests
import pygments
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
@@ -41,7 +43,7 @@ from pkg_resources import resource_filename
from nikola.plugin_categories import Command
from nikola import utils
-LOGGER = utils.get_logger('theme', utils.STDERR_HANDLER)
+LOGGER = utils.get_logger('theme')
class CommandTheme(Command):
@@ -89,9 +91,8 @@ class CommandTheme(Command):
'short': 'u',
'long': 'url',
'type': str,
- 'help': "URL for the theme repository (default: "
- "https://themes.getnikola.com/v7/themes.json)",
- 'default': 'https://themes.getnikola.com/v7/themes.json'
+ 'help': "URL for the theme repository",
+ 'default': 'https://themes.getnikola.com/v8/themes.json'
},
{
'name': 'getpath',
@@ -122,14 +123,21 @@ class CommandTheme(Command):
'long': 'engine',
'type': str,
'default': 'mako',
- 'help': 'Engine to use for new theme (mako or jinja -- default: mako)',
+ 'help': 'Engine to use for new theme (mako or jinja)',
},
{
'name': 'new_parent',
'long': 'parent',
'type': str,
'default': 'base',
- 'help': 'Parent to use for new theme (default: base)',
+ 'help': 'Parent to use for new theme',
+ },
+ {
+ 'name': 'new_legacy_meta',
+ 'long': 'legacy-meta',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Create legacy meta files for new theme',
},
]
@@ -147,6 +155,7 @@ class CommandTheme(Command):
new = options.get('new')
new_engine = options.get('new_engine')
new_parent = options.get('new_parent')
+ new_legacy_meta = options.get('new_legacy_meta')
command_count = [bool(x) for x in (
install,
uninstall,
@@ -172,7 +181,7 @@ class CommandTheme(Command):
elif copy_template:
return self.copy_template(copy_template)
elif new:
- return self.new_theme(new, new_engine, new_parent)
+ return self.new_theme(new, new_engine, new_parent, new_legacy_meta)
def do_install_deps(self, url, name):
"""Install themes and their dependencies."""
@@ -188,11 +197,11 @@ class CommandTheme(Command):
try:
utils.get_theme_path_real(parent_name, self.site.themes_dirs)
break
- except: # Not available
+ except Exception: # Not available
self.do_install(parent_name, data)
name = parent_name
if installstatus:
- LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname))
+ LOGGER.info('Remember to set THEME="{0}" in conf.py to use this theme.'.format(origname))
def do_install(self, name, data):
"""Download and install a theme."""
@@ -225,15 +234,13 @@ class CommandTheme(Command):
confpypath = os.path.join(dest_path, 'conf.py.sample')
if os.path.exists(confpypath):
- LOGGER.notice('This theme has a sample config file. Integrate it with yours in order to make this theme work!')
+ LOGGER.warning('This theme has a sample config file. Integrate it with yours in order to make this theme work!')
print('Contents of the conf.py.sample file:\n')
- with io.open(confpypath, 'r', encoding='utf-8') as fh:
+ with io.open(confpypath, 'r', encoding='utf-8-sig') as fh:
if self.site.colorful:
- print(utils.indent(pygments.highlight(
- fh.read(), PythonLexer(), TerminalFormatter()),
- 4 * ' '))
+ print(pygments.highlight(fh.read(), PythonLexer(), TerminalFormatter()))
else:
- print(utils.indent(fh.read(), 4 * ' '))
+ print(fh.read())
return True
def do_uninstall(self, name):
@@ -282,7 +289,9 @@ class CommandTheme(Command):
themes = []
themes_dirs = self.site.themes_dirs + [resource_filename('nikola', os.path.join('data', 'themes'))]
for tdir in themes_dirs:
- themes += [(i, os.path.join(tdir, i)) for i in os.listdir(tdir)]
+ if os.path.isdir(tdir):
+ themes += [(i, os.path.join(tdir, i)) for i in os.listdir(tdir)]
+
for tname, tpath in sorted(set(themes)):
if os.path.isdir(tpath):
print("{0} at {1}".format(tname, tpath))
@@ -316,7 +325,7 @@ class CommandTheme(Command):
LOGGER.error("This file already exists in your templates directory ({0}).".format(base))
return 3
- def new_theme(self, name, engine, parent):
+ def new_theme(self, name, engine, parent, create_legacy_meta=False):
"""Create a new theme."""
base = 'themes'
themedir = os.path.join(base, name)
@@ -326,9 +335,7 @@ class CommandTheme(Command):
LOGGER.info("Created directory {0}".format(base))
# Check if engine and parent match
- engine_file = utils.get_asset_path('engine', utils.get_theme_chain(parent, self.site.themes_dirs))
- with io.open(engine_file, 'r', encoding='utf-8') as fh:
- parent_engine = fh.read().strip()
+ parent_engine = utils.get_template_engine(utils.get_theme_chain(parent, self.site.themes_dirs))
if parent_engine != engine:
LOGGER.error("Cannot use engine {0} because parent theme '{1}' uses {2}".format(engine, parent, parent_engine))
@@ -342,24 +349,45 @@ class CommandTheme(Command):
LOGGER.error("Theme already exists")
return 2
- with io.open(os.path.join(themedir, 'parent'), 'w', encoding='utf-8') as fh:
- fh.write(parent + '\n')
- LOGGER.info("Created file {0}".format(os.path.join(themedir, 'parent')))
- with io.open(os.path.join(themedir, 'engine'), 'w', encoding='utf-8') as fh:
- fh.write(engine + '\n')
- LOGGER.info("Created file {0}".format(os.path.join(themedir, 'engine')))
+ cp = configparser.ConfigParser()
+ cp['Theme'] = {
+ 'engine': engine,
+ 'parent': parent
+ }
+
+ theme_meta_path = os.path.join(themedir, name + '.theme')
+ with io.open(theme_meta_path, 'w', encoding='utf-8') as fh:
+ cp.write(fh)
+ LOGGER.info("Created file {0}".format(theme_meta_path))
+
+ if create_legacy_meta:
+ with io.open(os.path.join(themedir, 'parent'), 'w', encoding='utf-8') as fh:
+ fh.write(parent + '\n')
+ LOGGER.info("Created file {0}".format(os.path.join(themedir, 'parent')))
+ with io.open(os.path.join(themedir, 'engine'), 'w', encoding='utf-8') as fh:
+ fh.write(engine + '\n')
+ LOGGER.info("Created file {0}".format(os.path.join(themedir, 'engine')))
LOGGER.info("Theme {0} created successfully.".format(themedir))
- LOGGER.notice('Remember to set THEME="{0}" in conf.py to use this theme.'.format(name))
+ LOGGER.info('Remember to set THEME="{0}" in conf.py to use this theme.'.format(name))
def get_json(self, url):
"""Download the JSON file with all plugins."""
if self.json is None:
try:
- self.json = requests.get(url).json()
- except requests.exceptions.SSLError:
- LOGGER.warning("SSL error, using http instead of https (press ^C to abort)")
- time.sleep(1)
- url = url.replace('https', 'http', 1)
- self.json = requests.get(url).json()
+ try:
+ self.json = requests.get(url).json()
+ except requests.exceptions.SSLError:
+ LOGGER.warning("SSL error, using http instead of https (press ^C to abort)")
+ time.sleep(1)
+ url = url.replace('https', 'http', 1)
+ self.json = requests.get(url).json()
+ except json.decoder.JSONDecodeError as e:
+ LOGGER.error("Failed to decode JSON data in response from server.")
+ LOGGER.error("JSON error encountered:" + str(e))
+ LOGGER.error("This issue might be caused by server-side issues, or by to unusual activity in your "
+ "network (as determined by CloudFlare). Please visit https://themes.getnikola.com/ in "
+ "a browser.")
+ sys.exit(2)
+
return self.json
diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin
index d78b79b..a172e28 100644
--- a/nikola/plugins/command/version.plugin
+++ b/nikola/plugins/command/version.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Show nikola version
[Nikola]
-plugincategory = Command
+PluginCategory = Command
diff --git a/nikola/plugins/command/version.py b/nikola/plugins/command/version.py
index 267837e..9b81343 100644
--- a/nikola/plugins/command/version.py
+++ b/nikola/plugins/command/version.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,13 @@
"""Print Nikola version."""
-from __future__ import print_function
-import lxml
import requests
from nikola.plugin_categories import Command
from nikola import __version__
-URL = 'https://pypi.python.org/pypi?:action=doap&name=Nikola'
+URL = 'https://pypi.org/pypi/Nikola/json'
class CommandVersion(Command):
@@ -60,10 +58,11 @@ class CommandVersion(Command):
"""Print the version number."""
print("Nikola v" + __version__)
if options.get('check'):
- data = requests.get(URL).text
- doc = lxml.etree.fromstring(data.encode('utf8'))
- revision = doc.findall('*//{http://usefulinc.com/ns/doap#}revision')[0].text
- if revision == __version__:
+ data = requests.get(URL).json()
+ pypi_version = data['info']['version']
+ if pypi_version == __version__:
print("Nikola is up-to-date")
else:
- print("The latest version of Nikola is v{0} -- please upgrade using `pip install --upgrade Nikola=={0}` or your system package manager".format(revision))
+ print("The latest version of Nikola is v{0}. Please upgrade "
+ "using `pip install --upgrade Nikola=={0}` or your "
+ "system package manager.".format(pypi_version))
diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py
index ff7e9a2..db78fce 100644
--- a/nikola/plugins/compile/__init__.py
+++ b/nikola/plugins/compile/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/compile/html.plugin b/nikola/plugins/compile/html.plugin
index f95bdd5..be1f876 100644
--- a/nikola/plugins/compile/html.plugin
+++ b/nikola/plugins/compile/html.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Compile HTML into HTML (just copy)
[Nikola]
-plugincategory = Compiler
+PluginCategory = Compiler
friendlyname = HTML
diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py
index 942d6da..80b6713 100644
--- a/nikola/plugins/compile/html.py
+++ b/nikola/plugins/compile/html.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,15 +24,17 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html for HTML source files."""
+"""Page compiler plugin for HTML source files."""
-from __future__ import unicode_literals
-import os
import io
+import os
+
+import lxml.html
+from nikola import shortcodes as sc
from nikola.plugin_categories import PageCompiler
-from nikola.utils import makedirs, write_metadata
+from nikola.utils import LocaleBorg, makedirs, map_metadata, write_metadata
class CompileHtml(PageCompiler):
@@ -40,25 +42,27 @@ class CompileHtml(PageCompiler):
name = "html"
friendly_name = "HTML"
+ supports_metadata = True
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
+ """Compile HTML into HTML strings, with shortcode support."""
+ if not is_two_file:
+ _, data = self.split_metadata(data, post, lang)
+ new_data, shortcodes = sc.extract_shortcodes(data)
+ return self.site.apply_shortcodes_uuid(new_data, shortcodes, filename=source_path, extra_context={'post': post})
+
+ def compile(self, source, dest, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
makedirs(os.path.dirname(dest))
- try:
- post = self.site.post_per_input_file[source]
- except KeyError:
- post = None
- with io.open(dest, "w+", encoding="utf8") as out_file:
- with io.open(source, "r", encoding="utf8") as in_file:
+ with io.open(dest, "w+", encoding="utf-8") as out_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
data = in_file.read()
- if not is_two_file:
- _, data = self.split_metadata(data)
- data, shortcode_deps = self.site.apply_shortcodes(data, with_dependencies=True, extra_context=dict(post=post))
+ data, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang)
out_file.write(data)
if post is None:
if shortcode_deps:
self.logger.error(
- "Cannot save dependencies for post {0} due to unregistered source file name",
+ "Cannot save dependencies for post {0} (post unknown)",
source)
else:
post._depfile[dest] += shortcode_deps
@@ -76,9 +80,41 @@ class CompileHtml(PageCompiler):
makedirs(os.path.dirname(path))
if not content.endswith('\n'):
content += '\n'
- with io.open(path, "w+", encoding="utf8") as fd:
+ with io.open(path, "w+", encoding="utf-8") as fd:
if onefile:
- fd.write('<!--\n')
- fd.write(write_metadata(metadata))
- fd.write('-->\n\n')
+ fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self))
fd.write(content)
+
+ def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
+ """Read the metadata from a post's meta tags, and return a metadata dict."""
+ if lang is None:
+ lang = LocaleBorg().current_lang
+ source_path = post.translated_source_path(lang)
+
+ with io.open(source_path, 'r', encoding='utf-8-sig') as inf:
+ data = inf.read()
+
+ metadata = {}
+ try:
+ doc = lxml.html.document_fromstring(data)
+ except lxml.etree.ParserError as e:
+ # Issue #374 -> #2851
+ if str(e) == "Document is empty":
+ return {}
+ # let other errors raise
+ raise
+ title_tag = doc.find('*//title')
+ if title_tag is not None and title_tag.text:
+ metadata['title'] = title_tag.text
+ meta_tags = doc.findall('*//meta')
+ for tag in meta_tags:
+ k = tag.get('name', '').lower()
+ if not k:
+ continue
+ elif k == 'keywords':
+ k = 'tags'
+ content = tag.get('content')
+ if content:
+ metadata[k] = content
+ map_metadata(metadata, 'html_metadata', self.site.config)
+ return metadata
diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin
index c369ab2..c146172 100644
--- a/nikola/plugins/compile/ipynb.plugin
+++ b/nikola/plugins/compile/ipynb.plugin
@@ -6,8 +6,8 @@ module = ipynb
author = Damian Avila, Chris Warrick and others
version = 2.0.0
website = http://www.damian.oquanta.info/
-description = Compile IPython notebooks into Nikola posts
+description = Compile Jupyter notebooks into Nikola posts
[Nikola]
-plugincategory = Compiler
-friendlyname = Jupyter/IPython Notebook
+PluginCategory = Compiler
+friendlyname = Jupyter Notebook
diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py
index f3fdeea..039604b 100644
--- a/nikola/plugins/compile/ipynb.py
+++ b/nikola/plugins/compile/ipynb.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2013-2016 Damián Avila, Chris Warrick and others.
+# Copyright © 2013-2020 Damián Avila, Chris Warrick and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,99 +24,95 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on nbconvert."""
+"""Page compiler plugin for nbconvert."""
-from __future__ import unicode_literals, print_function
import io
+import json
import os
-import sys
try:
+ import nbconvert
from nbconvert.exporters import HTMLExporter
import nbformat
current_nbformat = nbformat.current_nbformat
from jupyter_client import kernelspec
from traitlets.config import Config
+ NBCONVERT_VERSION_MAJOR = int(nbconvert.__version__.partition(".")[0])
flag = True
- ipy_modern = True
except ImportError:
- try:
- import IPython
- from IPython.nbconvert.exporters import HTMLExporter
- if IPython.version_info[0] >= 3: # API changed with 3.0.0
- from IPython import nbformat
- current_nbformat = nbformat.current_nbformat
- from IPython.kernel import kernelspec
- ipy_modern = True
- else:
- import IPython.nbformat.current as nbformat
- current_nbformat = 'json'
- kernelspec = None
- ipy_modern = False
-
- from IPython.config import Config
- flag = True
- except ImportError:
- flag = None
- ipy_modern = None
+ flag = None
+from nikola import shortcodes as sc
from nikola.plugin_categories import PageCompiler
-from nikola.utils import makedirs, req_missing, get_logger, STDERR_HANDLER
+from nikola.utils import makedirs, req_missing, LocaleBorg
class CompileIPynb(PageCompiler):
"""Compile IPynb into HTML."""
name = "ipynb"
- friendly_name = "Jupyter/IPython Notebook"
+ friendly_name = "Jupyter Notebook"
demote_headers = True
- default_kernel = 'python2' if sys.version_info[0] == 2 else 'python3'
+ default_kernel = 'python3'
+ supports_metadata = True
- def set_site(self, site):
- """Set Nikola site."""
- self.logger = get_logger('compile_ipynb', STDERR_HANDLER)
- super(CompileIPynb, self).set_site(site)
-
- def compile_html_string(self, source, is_two_file=True):
+ def _compile_string(self, nb_json):
"""Export notebooks as HTML strings."""
- if flag is None:
- req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)')
- c = Config(self.site.config['IPYNB_CONFIG'])
+ self._req_missing_ipynb()
+ c = Config(get_default_jupyter_config())
+ c.merge(Config(self.site.config['IPYNB_CONFIG']))
+ if 'template_file' not in self.site.config['IPYNB_CONFIG'].get('Exporter', {}):
+ if NBCONVERT_VERSION_MAJOR >= 6:
+ c['Exporter']['template_file'] = 'classic/base.html.j2'
+ else:
+ c['Exporter']['template_file'] = 'basic.tpl' # not a typo
exportHtml = HTMLExporter(config=c)
- with io.open(source, "r", encoding="utf8") as in_file:
- nb_json = nbformat.read(in_file, current_nbformat)
- (body, resources) = exportHtml.from_notebook_node(nb_json)
+ body, _ = exportHtml.from_notebook_node(nb_json)
return body
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
+ @staticmethod
+ def _nbformat_read(in_file):
+ return nbformat.read(in_file, current_nbformat)
+
+ def _req_missing_ipynb(self):
+ if flag is None:
+ req_missing(['notebook>=4.0.0'], 'build this site (compile ipynb)')
+
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
+ """Compile notebooks into HTML strings."""
+ new_data, shortcodes = sc.extract_shortcodes(data)
+ output = self._compile_string(nbformat.reads(new_data, current_nbformat))
+ return self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post})
+
+ def compile(self, source, dest, is_two_file=False, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
makedirs(os.path.dirname(dest))
- try:
- post = self.site.post_per_input_file[source]
- except KeyError:
- post = None
- with io.open(dest, "w+", encoding="utf8") as out_file:
- output = self.compile_html_string(source, is_two_file)
- output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post))
+ with io.open(dest, "w+", encoding="utf-8") as out_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
+ nb_str = in_file.read()
+ output, shortcode_deps = self.compile_string(nb_str, source,
+ is_two_file, post,
+ lang)
out_file.write(output)
if post is None:
if shortcode_deps:
self.logger.error(
- "Cannot save dependencies for post {0} due to unregistered source file name",
+ "Cannot save dependencies for post {0} (post unknown)",
source)
else:
post._depfile[dest] += shortcode_deps
- def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
+ def read_metadata(self, post, lang=None):
"""Read metadata directly from ipynb file.
- As ipynb file support arbitrary metadata as json, the metadata used by Nikola
+ As ipynb files support arbitrary metadata as json, the metadata used by Nikola
will be assume to be in the 'nikola' subfield.
"""
- if flag is None:
- req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)')
- source = post.source_path
- with io.open(source, "r", encoding="utf8") as in_file:
+ self._req_missing_ipynb()
+ if lang is None:
+ lang = LocaleBorg().current_lang
+ source = post.translated_source_path(lang)
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
nb_json = nbformat.read(in_file, current_nbformat)
# Metadata might not exist in two-file posts or in hand-crafted
# .ipynb files.
@@ -124,11 +120,10 @@ class CompileIPynb(PageCompiler):
def create_post(self, path, **kw):
"""Create a new post."""
- if flag is None:
- req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)')
+ self._req_missing_ipynb()
content = kw.pop('content', None)
onefile = kw.pop('onefile', False)
- kernel = kw.pop('ipython_kernel', None)
+ kernel = kw.pop('jupyter_kernel', None)
# is_page is not needed to create the file
kw.pop('is_page', False)
@@ -142,40 +137,52 @@ class CompileIPynb(PageCompiler):
# imported .ipynb file, guaranteed to start with "{" because it’s JSON.
nb = nbformat.reads(content, current_nbformat)
else:
- if ipy_modern:
- nb = nbformat.v4.new_notebook()
- nb["cells"] = [nbformat.v4.new_markdown_cell(content)]
- else:
- nb = nbformat.new_notebook()
- nb["worksheets"] = [nbformat.new_worksheet(cells=[nbformat.new_text_cell('markdown', [content])])]
-
- if kernelspec is not None:
- if kernel is None:
- kernel = self.default_kernel
- self.logger.notice('No kernel specified, assuming "{0}".'.format(kernel))
-
- IPYNB_KERNELS = {}
- ksm = kernelspec.KernelSpecManager()
- for k in ksm.find_kernel_specs():
- IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict()
- IPYNB_KERNELS[k]['name'] = k
- del IPYNB_KERNELS[k]['argv']
-
- if kernel not in IPYNB_KERNELS:
- self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel))
- self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS))))
- raise Exception('Unknown kernel "{0}"'.format(kernel))
-
- nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel]
- else:
- # Older IPython versions don’t need kernelspecs.
- pass
+ nb = nbformat.v4.new_notebook()
+ nb["cells"] = [nbformat.v4.new_markdown_cell(content)]
+
+ if kernel is None:
+ kernel = self.default_kernel
+ self.logger.warning('No kernel specified, assuming "{0}".'.format(kernel))
+
+ IPYNB_KERNELS = {}
+ ksm = kernelspec.KernelSpecManager()
+ for k in ksm.find_kernel_specs():
+ IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict()
+ IPYNB_KERNELS[k]['name'] = k
+ del IPYNB_KERNELS[k]['argv']
+
+ if kernel not in IPYNB_KERNELS:
+ self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel))
+ self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS))))
+ raise Exception('Unknown kernel "{0}"'.format(kernel))
+
+ nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel]
if onefile:
nb["metadata"]["nikola"] = metadata
- with io.open(path, "w+", encoding="utf8") as fd:
- if ipy_modern:
- nbformat.write(nb, fd, 4)
- else:
- nbformat.write(nb, fd, 'ipynb')
+ with io.open(path, "w+", encoding="utf-8") as fd:
+ nbformat.write(nb, fd, 4)
+
+
+def get_default_jupyter_config():
+ """Search default jupyter configuration location paths.
+
+ Return dictionary from configuration json files.
+ """
+ config = {}
+ from jupyter_core.paths import jupyter_config_path
+
+ for parent in jupyter_config_path():
+ try:
+ for file in os.listdir(parent):
+ if 'nbconvert' in file and file.endswith('.json'):
+ abs_path = os.path.join(parent, file)
+ with open(abs_path) as config_file:
+ config.update(json.load(config_file))
+ except OSError:
+ # some paths jupyter uses to find configurations
+ # may not exist
+ pass
+
+ return config
diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin
index 2607413..85c67c3 100644
--- a/nikola/plugins/compile/markdown.plugin
+++ b/nikola/plugins/compile/markdown.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Compile Markdown into HTML
[Nikola]
-plugincategory = Compiler
+PluginCategory = Compiler
friendlyname = Markdown
diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py
index 2e4234c..74e8c75 100644
--- a/nikola/plugins/compile/markdown/__init__.py
+++ b/nikola/plugins/compile/markdown/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,23 +24,44 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on markdown."""
-
-from __future__ import unicode_literals
+"""Page compiler plugin for Markdown."""
import io
+import json
import os
+import threading
+
+from nikola import shortcodes as sc
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing, write_metadata, LocaleBorg, map_metadata
try:
- from markdown import markdown
+ from markdown import Markdown
except ImportError:
- markdown = None # NOQA
- nikola_extension = None
- gist_extension = None
- podcast_extension = None
+ Markdown = None
-from nikola.plugin_categories import PageCompiler
-from nikola.utils import makedirs, req_missing, write_metadata
+
+class ThreadLocalMarkdown(threading.local):
+ """Convert Markdown to HTML using per-thread Markdown objects.
+
+ See discussion in #2661.
+ """
+
+ def __init__(self, extensions, extension_configs):
+ """Create a Markdown instance."""
+ self.markdown = Markdown(extensions=extensions, extension_configs=extension_configs, output_format="html5")
+
+ def convert(self, data):
+ """Convert data to HTML and reset internal state."""
+ result = self.markdown.convert(data)
+ try:
+ meta = {}
+ for k in self.markdown.Meta: # This reads everything as lists
+ meta[k.lower()] = ','.join(self.markdown.Meta[k])
+ except Exception:
+ meta = {}
+ self.markdown.reset()
+ return result, meta
class CompileMarkdown(PageCompiler):
@@ -49,42 +70,61 @@ class CompileMarkdown(PageCompiler):
name = "markdown"
friendly_name = "Markdown"
demote_headers = True
- extensions = []
site = None
+ supports_metadata = False
def set_site(self, site):
"""Set Nikola site."""
- super(CompileMarkdown, self).set_site(site)
+ super().set_site(site)
self.config_dependencies = []
+ extensions = []
for plugin_info in self.get_compiler_extensions():
self.config_dependencies.append(plugin_info.name)
- self.extensions.append(plugin_info.plugin_object)
+ extensions.append(plugin_info.plugin_object)
plugin_info.plugin_object.short_help = plugin_info.description
- self.config_dependencies.append(str(sorted(site.config.get("MARKDOWN_EXTENSIONS"))))
+ site_extensions = self.site.config.get("MARKDOWN_EXTENSIONS")
+ self.config_dependencies.append(str(sorted(site_extensions)))
+ extensions.extend(site_extensions)
+
+ site_extension_configs = self.site.config.get("MARKDOWN_EXTENSION_CONFIGS")
+ if site_extension_configs:
+ self.config_dependencies.append(json.dumps(site_extension_configs.values, sort_keys=True))
+
+ if Markdown is not None:
+ self.converters = {}
+ for lang in self.site.config['TRANSLATIONS']:
+ lang_extension_configs = site_extension_configs(lang) if site_extension_configs else {}
+ self.converters[lang] = ThreadLocalMarkdown(extensions, lang_extension_configs)
+ self.supports_metadata = 'markdown.extensions.meta' in extensions
+
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
+ """Compile Markdown into HTML strings."""
+ if lang is None:
+ lang = LocaleBorg().current_lang
+ if Markdown is None:
+ req_missing(['markdown'], 'build this site (compile Markdown)')
+ if not is_two_file:
+ _, data = self.split_metadata(data, post, lang)
+ new_data, shortcodes = sc.extract_shortcodes(data)
+ output, _ = self.converters[lang].convert(new_data)
+ output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post})
+ return output, shortcode_deps
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
- if markdown is None:
+ def compile(self, source, dest, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
+ if Markdown is None:
req_missing(['markdown'], 'build this site (compile Markdown)')
makedirs(os.path.dirname(dest))
- self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS")
- try:
- post = self.site.post_per_input_file[source]
- except KeyError:
- post = None
- with io.open(dest, "w+", encoding="utf8") as out_file:
- with io.open(source, "r", encoding="utf8") as in_file:
+ with io.open(dest, "w+", encoding="utf-8") as out_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
data = in_file.read()
- if not is_two_file:
- _, data = self.split_metadata(data)
- output = markdown(data, self.extensions, output_format="html5")
- output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post))
+ output, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang)
out_file.write(output)
if post is None:
if shortcode_deps:
self.logger.error(
- "Cannot save dependencies for post {0} due to unregistered source file name",
+ "Cannot save dependencies for post {0} (post unknown)",
source)
else:
post._depfile[dest] += shortcode_deps
@@ -102,9 +142,30 @@ class CompileMarkdown(PageCompiler):
makedirs(os.path.dirname(path))
if not content.endswith('\n'):
content += '\n'
- with io.open(path, "w+", encoding="utf8") as fd:
+ with io.open(path, "w+", encoding="utf-8") as fd:
if onefile:
- fd.write('<!-- \n')
- fd.write(write_metadata(metadata))
- fd.write('-->\n\n')
+ fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self))
fd.write(content)
+
+ def read_metadata(self, post, lang=None):
+ """Read the metadata from a post, and return a metadata dict."""
+ lang = lang or self.site.config['DEFAULT_LANG']
+ if not self.supports_metadata:
+ return {}
+ if Markdown is None:
+ req_missing(['markdown'], 'build this site (compile Markdown)')
+ if lang is None:
+ lang = LocaleBorg().current_lang
+ source = post.translated_source_path(lang)
+ with io.open(source, 'r', encoding='utf-8-sig') as inf:
+ # Note: markdown meta returns lowercase keys
+ data = inf.read()
+ # If the metadata starts with "---" it's actually YAML and
+ # we should not let markdown parse it, because it will do
+ # bad things like setting empty tags to "''"
+ if data.startswith('---\n'):
+ return {}
+ _, meta = self.converters[lang].convert(data)
+ # Map metadata from other platforms to names Nikola expects (Issue #2817)
+ map_metadata(meta, 'markdown_metadata', self.site.config)
+ return meta
diff --git a/nikola/plugins/compile/markdown/mdx_gist.plugin b/nikola/plugins/compile/markdown/mdx_gist.plugin
index 85b5450..f962cb7 100644
--- a/nikola/plugins/compile/markdown/mdx_gist.plugin
+++ b/nikola/plugins/compile/markdown/mdx_gist.plugin
@@ -4,7 +4,7 @@ module = mdx_gist
[Nikola]
compiler = markdown
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py
index 25c071f..f6ce20a 100644
--- a/nikola/plugins/compile/markdown/mdx_gist.py
+++ b/nikola/plugins/compile/markdown/mdx_gist.py
@@ -75,7 +75,10 @@ Error Case: non-existent file:
[:gist: 4747847 doesntexist.py]
"""
-from __future__ import unicode_literals, print_function
+import requests
+
+from nikola.plugin_categories import MarkdownExtension
+from nikola.utils import get_logger
try:
from markdown.extensions import Extension
@@ -87,12 +90,8 @@ except ImportError:
# the markdown compiler will fail first
Extension = Pattern = object
-from nikola.plugin_categories import MarkdownExtension
-from nikola.utils import get_logger, STDERR_HANDLER
-import requests
-
-LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
+LOGGER = get_logger('compile_markdown.mdx_gist')
GIST_JS_URL = "https://gist.github.com/{0}.js"
GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
@@ -167,7 +166,7 @@ class GistPattern(Pattern):
pre_elem.text = AtomicString(raw_gist)
except GistFetchException as e:
- LOGGER.warn(e.message)
+ LOGGER.warning(e.message)
warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
noscript_elem.append(warning_comment)
@@ -186,15 +185,15 @@ class GistExtension(MarkdownExtension, Extension):
for key, value in configs:
self.setConfig(key, value)
- def extendMarkdown(self, md, md_globals):
+ def extendMarkdown(self, md, md_globals=None):
"""Extend Markdown."""
gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs())
gist_md_pattern.md = md
- md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong")
+ md.inlinePatterns.register(gist_md_pattern, 'gist', 175)
gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs())
gist_rst_pattern.md = md
- md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist")
+ md.inlinePatterns.register(gist_rst_pattern, 'gist-rst', 176)
md.registerExtension(self)
@@ -203,6 +202,7 @@ def makeExtension(configs=None): # pragma: no cover
"""Make Markdown extension."""
return GistExtension(configs)
+
if __name__ == '__main__':
import doctest
diff --git a/nikola/plugins/compile/markdown/mdx_nikola.plugin b/nikola/plugins/compile/markdown/mdx_nikola.plugin
index 3c5c638..9751598 100644
--- a/nikola/plugins/compile/markdown/mdx_nikola.plugin
+++ b/nikola/plugins/compile/markdown/mdx_nikola.plugin
@@ -4,7 +4,7 @@ module = mdx_nikola
[Nikola]
compiler = markdown
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py
index 59a5d5b..06a6d9a 100644
--- a/nikola/plugins/compile/markdown/mdx_nikola.py
+++ b/nikola/plugins/compile/markdown/mdx_nikola.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,8 +30,10 @@
- Strikethrough inline patterns.
"""
-from __future__ import unicode_literals
import re
+
+from nikola.plugin_categories import MarkdownExtension
+
try:
from markdown.postprocessors import Postprocessor
from markdown.inlinepatterns import SimpleTagPattern
@@ -41,8 +43,6 @@ except ImportError:
# the markdown compiler will fail first
Postprocessor = SimpleTagPattern = Extension = object
-from nikola.plugin_categories import MarkdownExtension
-
CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL)
STRIKE_RE = r"(~{2})(.+?)(~{2})" # ~~strike~~
@@ -68,14 +68,14 @@ class NikolaExtension(MarkdownExtension, Extension):
def _add_nikola_post_processor(self, md):
"""Extend Markdown with the postprocessor."""
pp = NikolaPostProcessor()
- md.postprocessors.add('nikola_post_processor', pp, '_end')
+ md.postprocessors.register(pp, 'nikola_post_processor', 1)
def _add_strikethrough_inline_pattern(self, md):
"""Support PHP-Markdown style strikethrough, for example: ``~~strike~~``."""
pattern = SimpleTagPattern(STRIKE_RE, 'del')
- md.inlinePatterns.add('strikethrough', pattern, '_end')
+ md.inlinePatterns.register(pattern, 'strikethrough', 175)
- def extendMarkdown(self, md, md_globals):
+ def extendMarkdown(self, md, md_globals=None):
"""Extend markdown to Nikola flavours."""
self._add_nikola_post_processor(md)
self._add_strikethrough_inline_pattern(md)
diff --git a/nikola/plugins/compile/markdown/mdx_podcast.plugin b/nikola/plugins/compile/markdown/mdx_podcast.plugin
index c4ee7e9..df5260d 100644
--- a/nikola/plugins/compile/markdown/mdx_podcast.plugin
+++ b/nikola/plugins/compile/markdown/mdx_podcast.plugin
@@ -4,7 +4,7 @@ module = mdx_podcast
[Nikola]
compiler = markdown
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py
index 96a70ed..5090407 100644
--- a/nikola/plugins/compile/markdown/mdx_podcast.py
+++ b/nikola/plugins/compile/markdown/mdx_podcast.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright © 2013-2016 Michael Rabbitt, Roberto Alsina and others.
+# Copyright © 2013-2020 Michael Rabbitt, Roberto Alsina and others.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
@@ -36,7 +36,6 @@ Basic Example:
<p><audio controls=""><source src="https://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3" type="audio/mpeg"></source></audio></p>
"""
-from __future__ import print_function, unicode_literals
from nikola.plugin_categories import MarkdownExtension
try:
from markdown.extensions import Extension
@@ -69,7 +68,7 @@ class PodcastPattern(Pattern):
class PodcastExtension(MarkdownExtension, Extension):
- """"Podcast extension for Markdown."""
+ """Podcast extension for Markdown."""
def __init__(self, configs={}):
"""Initialize extension."""
@@ -80,11 +79,11 @@ class PodcastExtension(MarkdownExtension, Extension):
for key, value in configs:
self.setConfig(key, value)
- def extendMarkdown(self, md, md_globals):
+ def extendMarkdown(self, md, md_globals=None):
"""Extend Markdown."""
podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs())
podcast_md_pattern.md = md
- md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong")
+ md.inlinePatterns.register(podcast_md_pattern, 'podcast', 175)
md.registerExtension(self)
@@ -92,6 +91,7 @@ def makeExtension(configs=None): # pragma: no cover
"""Make Markdown extension."""
return PodcastExtension(configs)
+
if __name__ == '__main__':
import doctest
doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin
index 2a69095..8f339e4 100644
--- a/nikola/plugins/compile/pandoc.plugin
+++ b/nikola/plugins/compile/pandoc.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Compile markups into HTML using pandoc
[Nikola]
-plugincategory = Compiler
+PluginCategory = Compiler
friendlyname = Pandoc
diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py
index 2368ae9..af14344 100644
--- a/nikola/plugins/compile/pandoc.py
+++ b/nikola/plugins/compile/pandoc.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,12 +24,11 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on pandoc.
+"""Page compiler plugin for pandoc.
You will need, of course, to install pandoc
"""
-from __future__ import unicode_literals
import io
import os
@@ -48,25 +47,21 @@ class CompilePandoc(PageCompiler):
def set_site(self, site):
"""Set Nikola site."""
self.config_dependencies = [str(site.config['PANDOC_OPTIONS'])]
- super(CompilePandoc, self).set_site(site)
+ super().set_site(site)
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
+ def compile(self, source, dest, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
makedirs(os.path.dirname(dest))
try:
- try:
- post = self.site.post_per_input_file[source]
- except KeyError:
- post = None
subprocess.check_call(['pandoc', '-o', dest, source] + self.site.config['PANDOC_OPTIONS'])
- with open(dest, 'r', encoding='utf-8') as inf:
- output, shortcode_deps = self.site.apply_shortcodes(inf.read(), with_dependencies=True)
+ with open(dest, 'r', encoding='utf-8-sig') as inf:
+ output, shortcode_deps = self.site.apply_shortcodes(inf.read())
with open(dest, 'w', encoding='utf-8') as outf:
outf.write(output)
if post is None:
if shortcode_deps:
self.logger.error(
- "Cannot save dependencies for post {0} due to unregistered source file name",
+ "Cannot save dependencies for post {0} (post unknown)",
source)
else:
post._depfile[dest] += shortcode_deps
@@ -74,6 +69,10 @@ class CompilePandoc(PageCompiler):
if e.strreror == 'No such file or directory':
req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False)
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
+ """Compile into HTML strings."""
+ raise ValueError("Pandoc compiler does not support compile_string due to multiple output formats")
+
def create_post(self, path, **kw):
"""Create a new post."""
content = kw.pop('content', None)
@@ -88,7 +87,5 @@ class CompilePandoc(PageCompiler):
content += '\n'
with io.open(path, "w+", encoding="utf8") as fd:
if onefile:
- fd.write('<!--\n')
- fd.write(write_metadata(metadata))
- fd.write('-->\n\n')
+ fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self))
fd.write(content)
diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin
index f4fb0c1..13384bd 100644
--- a/nikola/plugins/compile/php.plugin
+++ b/nikola/plugins/compile/php.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Compile PHP into HTML (just copy and name the file .php)
[Nikola]
-plugincategory = Compiler
+PluginCategory = Compiler
friendlyname = PHP
diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py
index d2559fd..818e10d 100644
--- a/nikola/plugins/compile/php.py
+++ b/nikola/plugins/compile/php.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,16 +24,14 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html for HTML+php."""
+"""Page compiler plugin for PHP."""
-from __future__ import unicode_literals
-
-import os
import io
+import os
+from hashlib import md5
from nikola.plugin_categories import PageCompiler
from nikola.utils import makedirs, write_metadata
-from hashlib import md5
class CompilePhp(PageCompiler):
@@ -42,8 +40,8 @@ class CompilePhp(PageCompiler):
name = "php"
friendly_name = "PHP"
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
+ def compile(self, source, dest, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
makedirs(os.path.dirname(dest))
with io.open(dest, "w+", encoding="utf8") as out_file:
with open(source, "rb") as in_file:
@@ -51,6 +49,10 @@ class CompilePhp(PageCompiler):
out_file.write('<!-- __NIKOLA_PHP_TEMPLATE_INJECTION source:{0} checksum:{1}__ -->'.format(source, hash))
return True
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
+ """Compile PHP into HTML strings."""
+ return data, []
+
def create_post(self, path, **kw):
"""Create a new post."""
content = kw.pop('content', None)
@@ -76,9 +78,7 @@ class CompilePhp(PageCompiler):
content += '\n'
with io.open(path, "w+", encoding="utf8") as fd:
if onefile:
- fd.write('<!--\n')
- fd.write(write_metadata(metadata))
- fd.write('-->\n\n')
+ fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self))
fd.write(content)
def extension(self):
diff --git a/nikola/plugins/compile/rest.plugin b/nikola/plugins/compile/rest.plugin
index 4d9041a..43bdf2d 100644
--- a/nikola/plugins/compile/rest.plugin
+++ b/nikola/plugins/compile/rest.plugin
@@ -6,8 +6,8 @@ module = rest
author = Roberto Alsina
version = 1.0
website = https://getnikola.com/
-description = Compile reSt into HTML
+description = Compile reST into HTML
[Nikola]
-plugincategory = Compiler
+PluginCategory = Compiler
friendlyname = reStructuredText
diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py
index b75849f..44da076 100644
--- a/nikola/plugins/compile/rest/__init__.py
+++ b/nikola/plugins/compile/rest/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,28 +26,28 @@
"""reStructuredText compiler for Nikola."""
-from __future__ import unicode_literals
import io
+import logging
import os
import docutils.core
import docutils.nodes
+import docutils.transforms
import docutils.utils
import docutils.io
import docutils.readers.standalone
-import docutils.writers.html4css1
+import docutils.writers.html5_polyglot
import docutils.parsers.rst.directives
from docutils.parsers.rst import roles
from nikola.nikola import LEGAL_VALUES
+from nikola.metadata_extractors import MetaCondition
from nikola.plugin_categories import PageCompiler
from nikola.utils import (
- unicode_str,
- get_logger,
makedirs,
write_metadata,
- STDERR_HANDLER,
- LocaleBorg
+ LocaleBorg,
+ map_metadata
)
@@ -58,15 +58,57 @@ class CompileRest(PageCompiler):
friendly_name = "reStructuredText"
demote_headers = True
logger = None
-
- def compile_html_string(self, data, source_path=None, is_two_file=True):
+ supports_metadata = True
+ metadata_conditions = [(MetaCondition.config_bool, "USE_REST_DOCINFO_METADATA")]
+
+ def read_metadata(self, post, lang=None):
+ """Read the metadata from a post, and return a metadata dict."""
+ if lang is None:
+ lang = LocaleBorg().current_lang
+ source_path = post.translated_source_path(lang)
+
+ # Silence reST errors, some of which are due to a different
+ # environment. Real issues will be reported while compiling.
+ null_logger = logging.getLogger('NULL')
+ null_logger.setLevel(1000)
+ with io.open(source_path, 'r', encoding='utf-8-sig') as inf:
+ data = inf.read()
+ _, _, _, document = rst2html(data, logger=null_logger, source_path=source_path, transforms=self.site.rst_transforms)
+ meta = {}
+ if 'title' in document:
+ meta['title'] = document['title']
+ for docinfo in document.traverse(docutils.nodes.docinfo):
+ for element in docinfo.children:
+ if element.tagname == 'field': # custom fields (e.g. summary)
+ name_elem, body_elem = element.children
+ name = name_elem.astext()
+ value = body_elem.astext()
+ elif element.tagname == 'authors': # author list
+ name = element.tagname
+ value = [element.astext() for element in element.children]
+ else: # standard fields (e.g. address)
+ name = element.tagname
+ value = element.astext()
+ name = name.lower()
+
+ meta[name] = value
+
+ # Put 'authors' meta field contents in 'author', too
+ if 'authors' in meta and 'author' not in meta:
+ meta['author'] = '; '.join(meta['authors'])
+
+ # Map metadata from other platforms to names Nikola expects (Issue #2817)
+ map_metadata(meta, 'rest_docinfo', self.site.config)
+ return meta
+
+ def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None):
"""Compile reST into HTML strings."""
# If errors occur, this will be added to the line number reported by
# docutils so the line number matches the actual line number (off by
# 7 with default metadata, could be more or less depending on the post).
add_ln = 0
if not is_two_file:
- m_data, data = self.split_metadata(data)
+ m_data, data = self.split_metadata(data, post, lang)
add_ln = len(m_data.splitlines()) + 1
default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt')
@@ -76,38 +118,42 @@ class CompileRest(PageCompiler):
'stylesheet_path': None,
'link_stylesheet': True,
'syntax_highlight': 'short',
- 'math_output': 'mathjax',
+ # This path is not used by Nikola, but we need something to silence
+ # warnings about it from reST.
+ 'math_output': 'mathjax /assets/js/mathjax.js',
'template': default_template_path,
- 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en')
+ 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en'),
+ 'doctitle_xform': self.site.config.get('USE_REST_DOCINFO_METADATA'),
+ 'file_insertion_enabled': self.site.config.get('REST_FILE_INSERTION_ENABLED'),
}
- output, error_level, deps = rst2html(
- data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms,
- no_title_transform=self.site.config.get('NO_DOCUTILS_TITLE_TRANSFORM', False))
- if not isinstance(output, unicode_str):
+ from nikola import shortcodes as sc
+ new_data, shortcodes = sc.extract_shortcodes(data)
+ if self.site.config.get('HIDE_REST_DOCINFO', False):
+ self.site.rst_transforms.append(RemoveDocinfo)
+ output, error_level, deps, _ = rst2html(
+ new_data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms)
+ if not isinstance(output, str):
# To prevent some weird bugs here or there.
# Original issue: empty files. `output` became a bytestring.
output = output.decode('utf-8')
- return output, error_level, deps
- def compile_html(self, source, dest, is_two_file=True):
- """Compile source file into HTML and save as dest."""
+ output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post})
+ return output, error_level, deps, shortcode_deps
+
+ def compile(self, source, dest, is_two_file=True, post=None, lang=None):
+ """Compile the source file into HTML and save as dest."""
makedirs(os.path.dirname(dest))
error_level = 100
- with io.open(dest, "w+", encoding="utf8") as out_file:
- try:
- post = self.site.post_per_input_file[source]
- except KeyError:
- post = None
- with io.open(source, "r", encoding="utf8") as in_file:
+ with io.open(dest, "w+", encoding="utf-8") as out_file:
+ with io.open(source, "r", encoding="utf-8-sig") as in_file:
data = in_file.read()
- output, error_level, deps = self.compile_html_string(data, source, is_two_file)
- output, shortcode_deps = self.site.apply_shortcodes(output, filename=source, with_dependencies=True, extra_context=dict(post=post))
+ output, error_level, deps, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang)
out_file.write(output)
if post is None:
if deps.list:
self.logger.error(
- "Cannot save dependencies for post {0} due to unregistered source file name",
+ "Cannot save dependencies for post {0} (post unknown)",
source)
else:
post._depfile[dest] += deps.list
@@ -129,23 +175,21 @@ class CompileRest(PageCompiler):
makedirs(os.path.dirname(path))
if not content.endswith('\n'):
content += '\n'
- with io.open(path, "w+", encoding="utf8") as fd:
+ with io.open(path, "w+", encoding="utf-8") as fd:
if onefile:
- fd.write(write_metadata(metadata))
- fd.write('\n')
+ fd.write(write_metadata(metadata, comment_wrap=False, site=self.site, compiler=self))
fd.write(content)
def set_site(self, site):
"""Set Nikola site."""
- super(CompileRest, self).set_site(site)
+ super().set_site(site)
self.config_dependencies = []
for plugin_info in self.get_compiler_extensions():
self.config_dependencies.append(plugin_info.name)
plugin_info.plugin_object.short_help = plugin_info.description
- self.logger = get_logger('compile_rest', STDERR_HANDLER)
if not site.debug:
- self.logger.level = 4
+ self.logger.level = logging.WARNING
def get_observer(settings):
@@ -155,19 +199,25 @@ def get_observer(settings):
Error code mapping:
- +------+---------+------+----------+
- | dNUM | dNAME | lNUM | lNAME | d = docutils, l = logbook
- +------+---------+------+----------+
- | 0 | DEBUG | 1 | DEBUG |
- | 1 | INFO | 2 | INFO |
- | 2 | WARNING | 4 | WARNING |
- | 3 | ERROR | 5 | ERROR |
- | 4 | SEVERE | 6 | CRITICAL |
- +------+---------+------+----------+
+ +----------+----------+
+ | docutils | logging |
+ +----------+----------+
+ | DEBUG | DEBUG |
+ | INFO | INFO |
+ | WARNING | WARNING |
+ | ERROR | ERROR |
+ | SEVERE | CRITICAL |
+ +----------+----------+
"""
- errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6}
+ errormap = {
+ docutils.utils.Reporter.DEBUG_LEVEL: logging.DEBUG,
+ docutils.utils.Reporter.INFO_LEVEL: logging.INFO,
+ docutils.utils.Reporter.WARNING_LEVEL: logging.WARNING,
+ docutils.utils.Reporter.ERROR_LEVEL: logging.ERROR,
+ docutils.utils.Reporter.SEVERE_LEVEL: logging.CRITICAL
+ }
text = docutils.nodes.Element.astext(msg)
- line = msg['line'] + settings['add_ln'] if 'line' in msg else 0
+ line = msg['line'] + settings['add_ln'] if 'line' in msg else ''
out = '[{source}{colon}{line}] {text}'.format(
source=settings['source'], colon=(':' if line else ''),
line=line, text=text)
@@ -179,32 +229,32 @@ def get_observer(settings):
class NikolaReader(docutils.readers.standalone.Reader):
"""Nikola-specific docutils reader."""
+ config_section = 'nikola'
+
def __init__(self, *args, **kwargs):
"""Initialize the reader."""
self.transforms = kwargs.pop('transforms', [])
- self.no_title_transform = kwargs.pop('no_title_transform', False)
+ self.logging_settings = kwargs.pop('nikola_logging_settings', {})
docutils.readers.standalone.Reader.__init__(self, *args, **kwargs)
def get_transforms(self):
"""Get docutils transforms."""
- transforms = docutils.readers.standalone.Reader(self).get_transforms() + self.transforms
- if self.no_title_transform:
- transforms = [t for t in transforms if str(t) != "<class 'docutils.transforms.frontmatter.DocTitle'>"]
- return transforms
+ return docutils.readers.standalone.Reader(self).get_transforms() + self.transforms
def new_document(self):
"""Create and return a new empty document tree (root node)."""
document = docutils.utils.new_document(self.source.source_path, self.settings)
document.reporter.stream = False
- document.reporter.attach_observer(get_observer(self.l_settings))
+ document.reporter.attach_observer(get_observer(self.logging_settings))
return document
def shortcode_role(name, rawtext, text, lineno, inliner,
options={}, content=[]):
- """A shortcode role that passes through raw inline HTML."""
+ """Return a shortcode role that passes through raw inline HTML."""
return [docutils.nodes.raw('', text, format='html')], []
+
roles.register_canonical_role('raw-html', shortcode_role)
roles.register_canonical_role('html', shortcode_role)
roles.register_canonical_role('sc', shortcode_role)
@@ -226,7 +276,7 @@ def add_node(node, visit_function=None, depart_function=None):
self.site = site
directives.register_directive('math', MathDirective)
add_node(MathBlock, visit_Math, depart_Math)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
class MathDirective(Directive):
def run(self):
@@ -245,18 +295,53 @@ def add_node(node, visit_function=None, depart_function=None):
"""
docutils.nodes._add_node_class_names([node.__name__])
if visit_function:
- setattr(docutils.writers.html4css1.HTMLTranslator, 'visit_' + node.__name__, visit_function)
+ setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_' + node.__name__, visit_function)
if depart_function:
- setattr(docutils.writers.html4css1.HTMLTranslator, 'depart_' + node.__name__, depart_function)
+ setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'depart_' + node.__name__, depart_function)
+
+
+# Output <code> for ``double backticks``. (Code and extra logic based on html4css1 translator)
+def visit_literal(self, node):
+ """Output <code> for double backticks."""
+ # special case: "code" role
+ classes = node.get('classes', [])
+ if 'code' in classes:
+ # filter 'code' from class arguments
+ node['classes'] = [cls for cls in classes if cls != 'code']
+ self.body.append(self.starttag(node, 'code', ''))
+ return
+ self.body.append(
+ self.starttag(node, 'code', '', CLASS='docutils literal'))
+ text = node.astext()
+ for token in self.words_and_spaces.findall(text):
+ if token.strip():
+ # Protect text like "--an-option" and the regular expression
+ # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
+ if self.in_word_wrap_point.search(token):
+ self.body.append('<span class="pre">%s</span>'
+ % self.encode(token))
+ else:
+ self.body.append(self.encode(token))
+ elif token in ('\n', ' '):
+ # Allow breaks at whitespace:
+ self.body.append(token)
+ else:
+ # Protect runs of multiple spaces; the last space can wrap:
+ self.body.append('&nbsp;' * (len(token) - 1) + ' ')
+ self.body.append('</code>')
+ # Content already processed:
+ raise docutils.nodes.SkipNode
+
+
+setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_literal', visit_literal)
def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
destination_path=None, reader=None,
parser=None, parser_name='restructuredtext', writer=None,
- writer_name='html', settings=None, settings_spec=None,
- settings_overrides=None, config_section=None,
- enable_exit_status=None, logger=None, l_add_ln=0, transforms=None,
- no_title_transform=False):
+ writer_name='html5_polyglot', settings=None, settings_spec=None,
+ settings_overrides=None, config_section='nikola',
+ enable_exit_status=None, logger=None, l_add_ln=0, transforms=None):
"""Set up & run a ``Publisher``, and return a dictionary of document parts.
Dictionary keys are the names of parts, and values are Unicode strings;
@@ -268,20 +353,22 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
- Parameters: see `publish_programmatically`.
+ For a description of the parameters, see `publish_programmatically`.
WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report
reStructuredText syntax errors.
"""
if reader is None:
- reader = NikolaReader(transforms=transforms, no_title_transform=no_title_transform)
# For our custom logging, we have special needs and special settings we
# specify here.
# logger a logger from Nikola
# source source filename (docutils gets a string)
- # add_ln amount of metadata lines (see comment in compile_html above)
- reader.l_settings = {'logger': logger, 'source': source_path,
- 'add_ln': l_add_ln}
+ # add_ln amount of metadata lines (see comment in CompileRest.compile above)
+ reader = NikolaReader(transforms=transforms,
+ nikola_logging_settings={
+ 'logger': logger, 'source': source_path,
+ 'add_ln': l_add_ln
+ })
pub = docutils.core.Publisher(reader, parser, writer, settings=settings,
source_class=source_class,
@@ -294,7 +381,8 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
pub.set_destination(None, destination_path)
pub.publish(enable_exit_status=enable_exit_status)
- return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
+ return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies, pub.document
+
# Alignment helpers for extensions
_align_options_base = ('left', 'center', 'right')
@@ -302,3 +390,14 @@ _align_options_base = ('left', 'center', 'right')
def _align_choice(argument):
return docutils.parsers.rst.directives.choice(argument, _align_options_base + ("none", ""))
+
+
+class RemoveDocinfo(docutils.transforms.Transform):
+ """Remove docinfo nodes."""
+
+ default_priority = 870
+
+ def apply(self):
+ """Remove docinfo nodes."""
+ for node in self.document.traverse(docutils.nodes.docinfo):
+ node.parent.remove(node)
diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin
index 0a7896f..4434477 100644
--- a/nikola/plugins/compile/rest/chart.plugin
+++ b/nikola/plugins/compile/rest/chart.plugin
@@ -4,7 +4,7 @@ module = chart
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py
index 24f459b..17363cb 100644
--- a/nikola/plugins/compile/rest/chart.py
+++ b/nikola/plugins/compile/rest/chart.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,21 +23,17 @@
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
"""Chart directive for reSTructuredText."""
-from ast import literal_eval
-
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
try:
import pygal
except ImportError:
- pygal = None # NOQA
-
-from nikola.plugin_categories import RestExtension
-from nikola.utils import req_missing
+ pygal = None
_site = None
@@ -52,8 +48,7 @@ class Plugin(RestExtension):
global _site
_site = self.site = site
directives.register_directive('chart', Chart)
- self.site.register_shortcode('chart', _gen_chart)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
class Chart(Directive):
@@ -77,6 +72,7 @@ class Chart(Directive):
"classes": directives.unchanged,
"css": directives.unchanged,
"defs": directives.unchanged,
+ "data_file": directives.unchanged,
"disable_xml_declaration": directives.unchanged,
"dots_size": directives.unchanged,
"dynamic_print_values": directives.unchanged,
@@ -157,41 +153,9 @@ class Chart(Directive):
def run(self):
"""Run the directive."""
self.options['site'] = None
- html = _gen_chart(self.arguments[0], data='\n'.join(self.content), **self.options)
+ html = _site.plugin_manager.getPluginByName(
+ 'chart', 'ShortcodePlugin').plugin_object.handler(
+ self.arguments[0],
+ data='\n'.join(self.content),
+ **self.options)
return [nodes.raw('', html, format='html')]
-
-
-def _gen_chart(chart_type, **_options):
- if pygal is None:
- msg = req_missing(['pygal'], 'use the Chart directive', optional=True)
- return '<div class="text-error">{0}</div>'.format(msg)
- options = {}
- data = _options.pop('data')
- _options.pop('post', None)
- _options.pop('site')
- if 'style' in _options:
- style_name = _options.pop('style')
- else:
- style_name = 'BlueStyle'
- if '(' in style_name: # Parametric style
- style = eval('pygal.style.' + style_name)
- else:
- style = getattr(pygal.style, style_name)
- for k, v in _options.items():
- try:
- options[k] = literal_eval(v)
- except:
- options[k] = v
- chart = pygal
- for o in chart_type.split('.'):
- chart = getattr(chart, o)
- chart = chart(style=style)
- if _site and _site.invariant:
- chart.no_prefix = True
- chart.config(**options)
- for line in data.splitlines():
- line = line.strip()
- if line:
- label, series = literal_eval('({0})'.format(line))
- chart.add(label, series)
- return chart.render().decode('utf8')
diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin
index e447eb2..3b5c9c7 100644
--- a/nikola/plugins/compile/rest/doc.plugin
+++ b/nikola/plugins/compile/rest/doc.plugin
@@ -4,7 +4,7 @@ module = doc
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Manuel Kaufmann
diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py
index 55f576d..705c0bc 100644
--- a/nikola/plugins/compile/rest/doc.py
+++ b/nikola/plugins/compile/rest/doc.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,7 +29,7 @@
from docutils import nodes
from docutils.parsers.rst import roles
-from nikola.utils import split_explicit_title, LOGGER
+from nikola.utils import split_explicit_title, LOGGER, slugify
from nikola.plugin_categories import RestExtension
@@ -44,14 +44,11 @@ class Plugin(RestExtension):
roles.register_canonical_role('doc', doc_role)
self.site.register_shortcode('doc', doc_shortcode)
doc_role.site = site
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
-def _doc_link(rawtext, text, options={}, content=[]):
- """Handle the doc role."""
- # split link's text and post's slug in role content
- has_explicit_title, title, slug = split_explicit_title(text)
- # check if the slug given is part of our blog posts/pages
+def _find_post(slug):
+ """Find a post with the given slug in posts or pages."""
twin_slugs = False
post = None
for p in doc_role.site.timeline:
@@ -61,10 +58,27 @@ def _doc_link(rawtext, text, options={}, content=[]):
else:
twin_slugs = True
break
+ return post, twin_slugs
+
+
+def _doc_link(rawtext, text, options={}, content=[]):
+ """Handle the doc role."""
+ # split link's text and post's slug in role content
+ has_explicit_title, title, slug = split_explicit_title(text)
+ if '#' in slug:
+ slug, fragment = slug.split('#', 1)
+ else:
+ fragment = None
+
+ # Look for the unslugified input first, then try to slugify (Issue #3450)
+ post, twin_slugs = _find_post(slug)
+ if post is None:
+ slug = slugify(slug)
+ post, twin_slugs = _find_post(slug)
try:
if post is None:
- raise ValueError
+ raise ValueError("No post with matching slug found.")
except ValueError:
return False, False, None, None, slug
@@ -72,6 +86,8 @@ def _doc_link(rawtext, text, options={}, content=[]):
# use post's title as link's text
title = post.title()
permalink = post.permalink()
+ if fragment:
+ permalink += '#' + fragment
return True, twin_slugs, title, permalink, slug
@@ -83,7 +99,7 @@ def doc_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
if twin_slugs:
inliner.reporter.warning(
'More than one post with the same slug. Using "{0}"'.format(permalink))
- LOGGER.warn(
+ LOGGER.warning(
'More than one post with the same slug. Using "{0}" for doc role'.format(permalink))
node = make_link_node(rawtext, title, permalink, options)
return [node], []
@@ -101,7 +117,7 @@ def doc_shortcode(*args, **kwargs):
success, twin_slugs, title, permalink, slug = _doc_link(text, text, LOGGER)
if success:
if twin_slugs:
- LOGGER.warn(
+ LOGGER.warning(
'More than one post with the same slug. Using "{0}" for doc shortcode'.format(permalink))
return '<a href="{0}">{1}</a>'.format(permalink, title)
else:
diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin
index 763c1d2..4a8a3a7 100644
--- a/nikola/plugins/compile/rest/gist.plugin
+++ b/nikola/plugins/compile/rest/gist.plugin
@@ -4,7 +4,7 @@ module = gist
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/rest/gist.py b/nikola/plugins/compile/rest/gist.py
index e40c3b2..08aa46d 100644
--- a/nikola/plugins/compile/rest/gist.py
+++ b/nikola/plugins/compile/rest/gist.py
@@ -19,7 +19,7 @@ class Plugin(RestExtension):
"""Set Nikola site."""
self.site = site
directives.register_directive('gist', GitHubGist)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
class GitHubGist(Directive):
diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin
index 3ebb296..5239f92 100644
--- a/nikola/plugins/compile/rest/listing.plugin
+++ b/nikola/plugins/compile/rest/listing.plugin
@@ -4,7 +4,7 @@ module = listing
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py
index 4dfbedc..e5a73fa 100644
--- a/nikola/plugins/compile/rest/listing.py
+++ b/nikola/plugins/compile/rest/listing.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,26 +28,21 @@
"""Define and register a listing directive using the existing CodeBlock."""
-from __future__ import unicode_literals
import io
import os
import uuid
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
+from urllib.parse import urlunsplit
import docutils.parsers.rst.directives.body
import docutils.parsers.rst.directives.misc
+import pygments
+import pygments.util
from docutils import core
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.parsers.rst.roles import set_classes
from docutils.parsers.rst.directives.misc import Include
-
from pygments.lexers import get_lexer_by_name
-import pygments
-import pygments.util
from nikola import utils
from nikola.plugin_categories import RestExtension
@@ -119,6 +114,7 @@ class CodeBlock(Directive):
return [node]
+
# Monkey-patch: replace insane docutils CodeBlock with our implementation.
docutils.parsers.rst.directives.body.CodeBlock = CodeBlock
docutils.parsers.rst.directives.misc.CodeBlock = CodeBlock
@@ -142,7 +138,7 @@ class Plugin(RestExtension):
directives.register_directive('sourcecode', CodeBlock)
directives.register_directive('listing', Listing)
Listing.folders = site.config['LISTINGS_FOLDERS']
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
# Add sphinx compatibility option
@@ -186,7 +182,7 @@ class Listing(Include):
self.arguments.insert(0, fpath)
if 'linenos' in self.options:
self.options['number-lines'] = self.options['linenos']
- with io.open(fpath, 'r+', encoding='utf8') as fileobject:
+ with io.open(fpath, 'r+', encoding='utf-8-sig') as fileobject:
self.content = fileobject.read().splitlines()
self.state.document.settings.record_dependencies.add(fpath)
target = urlunsplit(("link", 'listing', fpath.replace('\\', '/'), '', ''))
@@ -200,8 +196,11 @@ class Listing(Include):
def get_code_from_file(self, data):
"""Create CodeBlock nodes from file object content."""
- return super(Listing, self).run()
+ return super().run()
def assert_has_content(self):
- """Listing has no content, override check from superclass."""
+ """Override check from superclass with nothing.
+
+ Listing has no content, override check from superclass.
+ """
pass
diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin
index 8dfb19c..396c2f9 100644
--- a/nikola/plugins/compile/rest/media.plugin
+++ b/nikola/plugins/compile/rest/media.plugin
@@ -4,7 +4,7 @@ module = media
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py
index 8a69586..d29d0a2 100644
--- a/nikola/plugins/compile/rest/media.py
+++ b/nikola/plugins/compile/rest/media.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,14 +29,13 @@
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
try:
import micawber
except ImportError:
- micawber = None # NOQA
-
-
-from nikola.plugin_categories import RestExtension
-from nikola.utils import req_missing
+ micawber = None
class Plugin(RestExtension):
@@ -49,7 +48,7 @@ class Plugin(RestExtension):
self.site = site
directives.register_directive('media', Media)
self.site.register_shortcode('media', _gen_media_embed)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
class Media(Directive):
diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin
index 1802f2b..68abaef 100644
--- a/nikola/plugins/compile/rest/post_list.plugin
+++ b/nikola/plugins/compile/rest/post_list.plugin
@@ -4,11 +4,11 @@ module = post_list
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Udo Spallek
-version = 0.1
+version = 0.2
website = https://getnikola.com/
-description = Includes a list of posts with tag and slide based filters.
+description = Includes a list of posts with tag and slice based filters.
diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py
index 8cfd5bf..f7e95ed 100644
--- a/nikola/plugins/compile/rest/post_list.py
+++ b/nikola/plugins/compile/rest/post_list.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2013-2016 Udo Spallek, Roberto Alsina and others.
+# Copyright © 2013-2020 Udo Spallek, Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,21 +23,13 @@
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
"""Post list directive for reStructuredText."""
-from __future__ import unicode_literals
-
-import os
-import uuid
-import natsort
-
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from nikola import utils
from nikola.plugin_categories import RestExtension
-from nikola.packages.datecond import date_in_range
# WARNING: the directive name is post-list
# (with a DASH instead of an UNDERSCORE)
@@ -51,91 +43,14 @@ class Plugin(RestExtension):
def set_site(self, site):
"""Set Nikola site."""
self.site = site
- self.site.register_shortcode('post-list', _do_post_list)
- directives.register_directive('post-list', PostList)
- PostList.site = site
- return super(Plugin, self).set_site(site)
-
-
-class PostList(Directive):
- """Provide a reStructuredText directive to create a list of posts.
-
- Post List
- =========
- :Directive Arguments: None.
- :Directive Options: lang, start, stop, reverse, sort, date, tags, categories, sections, slugs, post_type, all, template, id
- :Directive Content: None.
-
- The posts appearing in the list can be filtered by options.
- *List slicing* is provided with the *start*, *stop* and *reverse* options.
-
- The following not required options are recognized:
-
- ``start`` : integer
- The index of the first post to show.
- A negative value like ``-3`` will show the *last* three posts in the
- post-list.
- Defaults to None.
-
- ``stop`` : integer
- The index of the last post to show.
- A value negative value like ``-1`` will show every post, but not the
- *last* in the post-list.
- Defaults to None.
-
- ``reverse`` : flag
- Reverse the order of the post-list.
- Defaults is to not reverse the order of posts.
-
- ``sort`` : string
- Sort post list by one of each post's attributes, usually ``title`` or a
- custom ``priority``. Defaults to None (chronological sorting).
+ directives.register_directive('post-list', PostListDirective)
+ directives.register_directive('post_list', PostListDirective)
+ PostListDirective.site = site
+ return super().set_site(site)
- ``date`` : string
- Show posts that match date range specified by this option. Format:
- * comma-separated clauses (AND)
- * clause: attribute comparison_operator value (spaces optional)
- * attribute: year, month, day, hour, month, second, weekday, isoweekday; or empty for full datetime
- * comparison_operator: == != <= >= < >
- * value: integer or dateutil-compatible date input
-
- ``tags`` : string [, string...]
- Filter posts to show only posts having at least one of the ``tags``.
- Defaults to None.
-
- ``categories`` : string [, string...]
- Filter posts to show only posts having one of the ``categories``.
- Defaults to None.
-
- ``sections`` : string [, string...]
- Filter posts to show only posts having one of the ``sections``.
- Defaults to None.
-
- ``slugs`` : string [, string...]
- Filter posts to show only posts having at least one of the ``slugs``.
- Defaults to None.
-
- ``post_type`` (or ``type``) : string
- Show only ``posts``, ``pages`` or ``all``.
- Replaces ``all``. Defaults to ``posts``.
-
- ``all`` : flag
- (deprecated, use ``post_type`` instead)
- Shows all posts and pages in the post list. Defaults to show only posts.
-
- ``lang`` : string
- The language of post *titles* and *links*.
- Defaults to default language.
-
- ``template`` : string
- The name of an alternative template to render the post-list.
- Defaults to ``post_list_directive.tmpl``
-
- ``id`` : string
- A manual id for the post list.
- Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex.
- """
+class PostListDirective(Directive):
+ """Provide a reStructuredText directive to create a list of posts."""
option_spec = {
'start': int,
@@ -143,12 +58,12 @@ class PostList(Directive):
'reverse': directives.flag,
'sort': directives.unchanged,
'tags': directives.unchanged,
+ 'require_all_tags': directives.flag,
'categories': directives.unchanged,
'sections': directives.unchanged,
'slugs': directives.unchanged,
'post_type': directives.unchanged,
'type': directives.unchanged,
- 'all': directives.flag,
'lang': directives.unchanged,
'template': directives.path,
'id': directives.unchanged,
@@ -161,151 +76,42 @@ class PostList(Directive):
stop = self.options.get('stop')
reverse = self.options.get('reverse', False)
tags = self.options.get('tags')
+ require_all_tags = 'require_all_tags' in self.options
categories = self.options.get('categories')
sections = self.options.get('sections')
slugs = self.options.get('slugs')
post_type = self.options.get('post_type')
type = self.options.get('type', False)
- all = self.options.get('all', False)
lang = self.options.get('lang', utils.LocaleBorg().current_lang)
template = self.options.get('template', 'post_list_directive.tmpl')
sort = self.options.get('sort')
date = self.options.get('date')
-
- output, deps = _do_post_list(start, stop, reverse, tags, categories, sections, slugs, post_type, type,
- all, lang, template, sort, state=self.state, site=self.site, date=date)
- self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE")
+ filename = self.state.document.settings._nikola_source_path
+
+ output, deps = self.site.plugin_manager.getPluginByName(
+ 'post_list', 'ShortcodePlugin').plugin_object.handler(
+ start,
+ stop,
+ reverse,
+ tags,
+ require_all_tags,
+ categories,
+ sections,
+ slugs,
+ post_type,
+ type,
+ lang,
+ template,
+ sort,
+ state=self.state,
+ site=self.site,
+ date=date,
+ filename=filename)
+ self.state.document.settings.record_dependencies.add(
+ "####MAGIC####TIMELINE")
for d in deps:
self.state.document.settings.record_dependencies.add(d)
if output:
return [nodes.raw('', output, format='html')]
else:
return []
-
-
-def _do_post_list(start=None, stop=None, reverse=False, tags=None, categories=None,
- sections=None, slugs=None, post_type='post', type=False, all=False,
- lang=None, template='post_list_directive.tmpl', sort=None,
- id=None, data=None, state=None, site=None, date=None, filename=None, post=None):
- if lang is None:
- lang = utils.LocaleBorg().current_lang
- if site.invariant: # for testing purposes
- post_list_id = id or 'post_list_' + 'fixedvaluethatisnotauuid'
- else:
- post_list_id = id or 'post_list_' + uuid.uuid4().hex
-
- # Get post from filename if available
- if filename:
- self_post = site.post_per_input_file.get(filename)
- else:
- self_post = None
-
- if self_post:
- self_post.register_depfile("####MAGIC####TIMELINE", lang=lang)
-
- # If we get strings for start/stop, make them integers
- if start is not None:
- start = int(start)
- if stop is not None:
- stop = int(stop)
-
- # Parse tags/categories/sections/slugs (input is strings)
- tags = [t.strip().lower() for t in tags.split(',')] if tags else []
- categories = [c.strip().lower() for c in categories.split(',')] if categories else []
- sections = [s.strip().lower() for s in sections.split(',')] if sections else []
- slugs = [s.strip() for s in slugs.split(',')] if slugs else []
-
- filtered_timeline = []
- posts = []
- step = -1 if reverse is None else None
-
- if type is not False:
- post_type = type
-
- # TODO: remove in v8
- if all is not False:
- timeline = [p for p in site.timeline]
- elif post_type == 'page' or post_type == 'pages':
- timeline = [p for p in site.timeline if not p.use_in_feeds]
- elif post_type == 'all':
- timeline = [p for p in site.timeline]
- else: # post
- timeline = [p for p in site.timeline if p.use_in_feeds]
-
- # TODO: replaces all, uncomment in v8
- # if post_type == 'page' or post_type == 'pages':
- # timeline = [p for p in site.timeline if not p.use_in_feeds]
- # elif post_type == 'all':
- # timeline = [p for p in site.timeline]
- # else: # post
- # timeline = [p for p in site.timeline if p.use_in_feeds]
-
- if categories:
- timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories]
-
- if sections:
- timeline = [p for p in timeline if p.section_name(lang).lower() in sections]
-
- for post in timeline:
- if tags:
- cont = True
- tags_lower = [t.lower() for t in post.tags]
- for tag in tags:
- if tag in tags_lower:
- cont = False
-
- if cont:
- continue
-
- filtered_timeline.append(post)
-
- if sort:
- filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC)
-
- if date:
- filtered_timeline = [p for p in filtered_timeline if date_in_range(date, p.date)]
-
- for post in filtered_timeline[start:stop:step]:
- if slugs:
- cont = True
- for slug in slugs:
- if slug == post.meta('slug'):
- cont = False
-
- if cont:
- continue
-
- bp = post.translated_base_path(lang)
- if os.path.exists(bp) and state:
- state.document.settings.record_dependencies.add(bp)
- elif os.path.exists(bp) and self_post:
- self_post.register_depfile(bp, lang=lang)
-
- posts += [post]
-
- if not posts:
- return '', []
-
- template_deps = site.template_system.template_deps(template)
- if state:
- # Register template as a dependency (Issue #2391)
- for d in template_deps:
- state.document.settings.record_dependencies.add(d)
- elif self_post:
- for d in template_deps:
- self_post.register_depfile(d, lang=lang)
-
- template_data = {
- 'lang': lang,
- 'posts': posts,
- # Need to provide str, not TranslatableSetting (Issue #2104)
- 'date_format': site.GLOBAL_CONTEXT.get('date_format')[lang],
- 'post_list_id': post_list_id,
- 'messages': site.MESSAGES,
- }
- output = site.template_system.render_template(
- template, None, template_data)
- return output, template_deps
-
-# Request file name from shortcode (Issue #2412)
-_do_post_list.nikola_shortcode_pass_filename = True
diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin
deleted file mode 100644
index 389da39..0000000
--- a/nikola/plugins/compile/rest/slides.plugin
+++ /dev/null
@@ -1,14 +0,0 @@
-[Core]
-name = rest_slides
-module = slides
-
-[Nikola]
-compiler = rest
-plugincategory = CompilerExtension
-
-[Documentation]
-author = Roberto Alsina
-version = 0.1
-website = https://getnikola.com/
-description = Slides directive
-
diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py
deleted file mode 100644
index 7c5b34b..0000000
--- a/nikola/plugins/compile/rest/slides.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Slides directive for reStructuredText."""
-
-from __future__ import unicode_literals
-
-import uuid
-
-from docutils import nodes
-from docutils.parsers.rst import Directive, directives
-
-from nikola.plugin_categories import RestExtension
-
-
-class Plugin(RestExtension):
- """Plugin for reST slides directive."""
-
- name = "rest_slides"
-
- def set_site(self, site):
- """Set Nikola site."""
- self.site = site
- directives.register_directive('slides', Slides)
- Slides.site = site
- return super(Plugin, self).set_site(site)
-
-
-class Slides(Directive):
- """reST extension for inserting slideshows."""
-
- has_content = True
-
- def run(self):
- """Run the slides directive."""
- if len(self.content) == 0: # pragma: no cover
- return
-
- if self.site.invariant: # for testing purposes
- carousel_id = 'slides_' + 'fixedvaluethatisnotauuid'
- else:
- carousel_id = 'slides_' + uuid.uuid4().hex
-
- output = self.site.template_system.render_template(
- 'slides.tmpl',
- None,
- {
- 'slides_content': self.content,
- 'carousel_id': carousel_id,
- }
- )
- return [nodes.raw('', output, format='html')]
-
-
-directives.register_directive('slides', Slides)
diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin
index 4e36ea4..f85a964 100644
--- a/nikola/plugins/compile/rest/soundcloud.plugin
+++ b/nikola/plugins/compile/rest/soundcloud.plugin
@@ -4,7 +4,7 @@ module = soundcloud
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/compile/rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py
index 9fabe70..5dbcfc3 100644
--- a/nikola/plugins/compile/rest/soundcloud.py
+++ b/nikola/plugins/compile/rest/soundcloud.py
@@ -1,5 +1,29 @@
# -*- coding: utf-8 -*-
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
"""SoundCloud directive for reStructuredText."""
from docutils import nodes
@@ -19,7 +43,7 @@ class Plugin(RestExtension):
self.site = site
directives.register_directive('soundcloud', SoundCloud)
directives.register_directive('soundcloud_playlist', SoundCloudPlaylist)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
CODE = """\
diff --git a/nikola/plugins/compile/rest/thumbnail.plugin b/nikola/plugins/compile/rest/thumbnail.plugin
index 3324c31..e7b649d 100644
--- a/nikola/plugins/compile/rest/thumbnail.plugin
+++ b/nikola/plugins/compile/rest/thumbnail.plugin
@@ -4,7 +4,7 @@ module = thumbnail
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
author = Pelle Nilsson
diff --git a/nikola/plugins/compile/rest/thumbnail.py b/nikola/plugins/compile/rest/thumbnail.py
index 37e0973..06ca9e4 100644
--- a/nikola/plugins/compile/rest/thumbnail.py
+++ b/nikola/plugins/compile/rest/thumbnail.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2014-2016 Pelle Nilsson and others.
+# Copyright © 2014-2020 Pelle Nilsson and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -43,7 +43,7 @@ class Plugin(RestExtension):
"""Set Nikola site."""
self.site = site
directives.register_directive('thumbnail', Thumbnail)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
class Thumbnail(Figure):
@@ -69,7 +69,7 @@ class Thumbnail(Figure):
"""Run the thumbnail directive."""
uri = directives.uri(self.arguments[0])
if uri.endswith('.svg'):
- # the ? at the end makes docutil output an <img> instead of an object for the svg, which colorbox requires
+ # the ? at the end makes docutil output an <img> instead of an object for the svg, which lightboxes may require
self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) + '?'
else:
self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri))
diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin
index 688f981..89b171b 100644
--- a/nikola/plugins/compile/rest/vimeo.plugin
+++ b/nikola/plugins/compile/rest/vimeo.plugin
@@ -4,7 +4,7 @@ module = vimeo
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
description = Vimeo directive
diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py
index f1ac6c3..7047b03 100644
--- a/nikola/plugins/compile/rest/vimeo.py
+++ b/nikola/plugins/compile/rest/vimeo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,14 @@
"""Vimeo directive for reStructuredText."""
-from docutils import nodes
-from docutils.parsers.rst import Directive, directives
-from nikola.plugins.compile.rest import _align_choice, _align_options_base
-
-import requests
import json
+import requests
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
from nikola.plugin_categories import RestExtension
+from nikola.plugins.compile.rest import _align_choice, _align_options_base
class Plugin(RestExtension):
@@ -46,7 +45,7 @@ class Plugin(RestExtension):
"""Set Nikola site."""
self.site = site
directives.register_directive('vimeo', Vimeo)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
CODE = """<div class="vimeo-video{align}">
diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin
index 5fbd67b..d83d0f8 100644
--- a/nikola/plugins/compile/rest/youtube.plugin
+++ b/nikola/plugins/compile/rest/youtube.plugin
@@ -4,7 +4,7 @@ module = youtube
[Nikola]
compiler = rest
-plugincategory = CompilerExtension
+PluginCategory = CompilerExtension
[Documentation]
version = 0.1
diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py
index b3dde62..d52ec64 100644
--- a/nikola/plugins/compile/rest/youtube.py
+++ b/nikola/plugins/compile/rest/youtube.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,8 +28,8 @@
from docutils import nodes
from docutils.parsers.rst import Directive, directives
-from nikola.plugins.compile.rest import _align_choice, _align_options_base
+from nikola.plugins.compile.rest import _align_choice, _align_options_base
from nikola.plugin_categories import RestExtension
@@ -42,13 +42,14 @@ class Plugin(RestExtension):
"""Set Nikola site."""
self.site = site
directives.register_directive('youtube', Youtube)
- return super(Plugin, self).set_site(site)
+ return super().set_site(site)
CODE = """\
<div class="youtube-video{align}">
<iframe width="{width}" height="{height}"
-src="https://www.youtube.com/embed/{yid}?rel=0&amp;hd=1&amp;wmode=transparent"
+src="https://www.youtube-nocookie.com/embed/{yid}?rel=0&wmode=transparent"
+frameborder="0" allow="encrypted-media" allowfullscreen
></iframe>
</div>"""
@@ -66,8 +67,8 @@ class Youtube(Directive):
has_content = True
required_arguments = 1
option_spec = {
- "width": directives.positive_int,
- "height": directives.positive_int,
+ "width": directives.unchanged,
+ "height": directives.unchanged,
"align": _align_choice
}
@@ -76,10 +77,10 @@ class Youtube(Directive):
self.check_content()
options = {
'yid': self.arguments[0],
- 'width': 425,
- 'height': 344,
+ 'width': 560,
+ 'height': 315,
}
- options.update(self.options)
+ options.update({k: v for k, v in self.options.items() if v})
if self.options.get('align') in _align_options_base:
options['align'] = ' align-' + self.options['align']
else:
diff --git a/nikola/plugins/misc/__init__.py b/nikola/plugins/misc/__init__.py
index 518fac1..1e7e6e1 100644
--- a/nikola/plugins/misc/__init__.py
+++ b/nikola/plugins/misc/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/misc/scan_posts.py b/nikola/plugins/misc/scan_posts.py
index f584a05..8812779 100644
--- a/nikola/plugins/misc/scan_posts.py
+++ b/nikola/plugins/misc/scan_posts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""The default post scanner."""
-from __future__ import unicode_literals, print_function
import glob
import os
import sys
@@ -35,7 +34,7 @@ from nikola.plugin_categories import PostScanner
from nikola import utils
from nikola.post import Post
-LOGGER = utils.get_logger('scan_posts', utils.STDERR_HANDLER)
+LOGGER = utils.get_logger('scan_posts')
class ScanPosts(PostScanner):
@@ -55,10 +54,10 @@ class ScanPosts(PostScanner):
self.site.config['post_pages']:
if not self.site.quiet:
print(".", end='', file=sys.stderr)
+ destination_translatable = utils.TranslatableSetting('destination', destination, self.site.config['TRANSLATIONS'])
dirname = os.path.dirname(wildcard)
for dirpath, _, _ in os.walk(dirname, followlinks=True):
- dest_dir = os.path.normpath(os.path.join(destination,
- os.path.relpath(dirpath, dirname))) # output/destination/foo/
+ rel_dest_dir = os.path.relpath(dirpath, dirname)
# Get all the untranslated paths
dir_glob = os.path.join(dirpath, os.path.basename(wildcard)) # posts/foo/*.rst
untranslated = glob.glob(dir_glob)
@@ -84,24 +83,30 @@ class ScanPosts(PostScanner):
if not any([x.startswith('.')
for x in p.split(os.sep)])]
- for base_path in full_list:
+ for base_path in sorted(full_list):
if base_path in seen:
continue
- else:
- seen.add(base_path)
try:
post = Post(
base_path,
self.site.config,
- dest_dir,
+ rel_dest_dir,
use_in_feeds,
self.site.MESSAGES,
template_name,
- self.site.get_compiler(base_path)
+ self.site.get_compiler(base_path),
+ destination_base=destination_translatable,
+ metadata_extractors_by=self.site.metadata_extractors_by
)
+ for lang in post.translated_to:
+ seen.add(post.translated_source_path(lang))
timeline.append(post)
- except Exception as err:
+ except Exception:
LOGGER.error('Error reading post {}'.format(base_path))
- raise err
+ raise
return timeline
+
+ def supported_extensions(self):
+ """Return a list of supported file extensions, or None if such a list isn't known beforehand."""
+ return list({os.path.splitext(x[0])[1] for x in self.site.config['post_pages']})
diff --git a/nikola/plugins/misc/taxonomies_classifier.plugin b/nikola/plugins/misc/taxonomies_classifier.plugin
new file mode 100644
index 0000000..55c59af
--- /dev/null
+++ b/nikola/plugins/misc/taxonomies_classifier.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = classify_taxonomies
+module = taxonomies_classifier
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Classifies the timeline into taxonomies.
+
+[Nikola]
+PluginCategory = SignalHandler
diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py
new file mode 100644
index 0000000..da8045b
--- /dev/null
+++ b/nikola/plugins/misc/taxonomies_classifier.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the taxonomy overviews, classification pages and feeds."""
+
+import functools
+import os
+import sys
+from collections import defaultdict
+
+import blinker
+import natsort
+
+from nikola.plugin_categories import SignalHandler
+from nikola import utils, hierarchy_utils
+
+
+class TaxonomiesClassifier(SignalHandler):
+ """Classify posts and pages by taxonomies."""
+
+ name = "classify_taxonomies"
+
+ def _do_classification(self, site):
+ # Needed to avoid strange errors during tests
+ if site is not self.site:
+ return
+
+ # Get list of enabled taxonomy plugins and initialize data structures
+ taxonomies = site.taxonomy_plugins.values()
+ site.posts_per_classification = {}
+ for taxonomy in taxonomies:
+ site.posts_per_classification[taxonomy.classification_name] = {
+ lang: defaultdict(set) for lang in site.config['TRANSLATIONS'].keys()
+ }
+
+ # Classify posts
+ for post in site.timeline:
+ # Do classify pages, but don’t classify posts that are hidden
+ # (draft/private/future)
+ if post.is_post and not post.use_in_feeds:
+ continue
+ for taxonomy in taxonomies:
+ if taxonomy.apply_to_posts if post.is_post else taxonomy.apply_to_pages:
+ classifications = {}
+ for lang in site.config['TRANSLATIONS'].keys():
+ # Extract classifications for this language
+ classifications[lang] = taxonomy.classify(post, lang)
+ if not taxonomy.more_than_one_classifications_per_post and len(classifications[lang]) > 1:
+ raise ValueError("Too many {0} classifications for post {1}".format(taxonomy.classification_name, post.source_path))
+ # Add post to sets
+ for classification in classifications[lang]:
+ while True:
+ site.posts_per_classification[taxonomy.classification_name][lang][classification].add(post)
+ if not taxonomy.include_posts_from_subhierarchies or not taxonomy.has_hierarchy:
+ break
+ classification_path = taxonomy.extract_hierarchy(classification)
+ if len(classification_path) <= 1:
+ if len(classification_path) == 0 or not taxonomy.include_posts_into_hierarchy_root:
+ break
+ classification = taxonomy.recombine_classification_from_hierarchy(classification_path[:-1])
+
+ # Sort everything.
+ site.page_count_per_classification = {}
+ site.hierarchy_per_classification = {}
+ site.flat_hierarchy_per_classification = {}
+ site.hierarchy_lookup_per_classification = {}
+ for taxonomy in taxonomies:
+ site.page_count_per_classification[taxonomy.classification_name] = {}
+ # Sort post lists
+ for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items():
+ # Ensure implicit classifications are inserted
+ for classification in taxonomy.get_implicit_classifications(lang):
+ if classification not in posts_per_classification:
+ posts_per_classification[classification] = []
+ site.page_count_per_classification[taxonomy.classification_name][lang] = {}
+ # Convert sets to lists and sort them
+ for classification in list(posts_per_classification.keys()):
+ posts = list(posts_per_classification[classification])
+ posts = self.site.sort_posts_chronologically(posts, lang)
+ taxonomy.sort_posts(posts, classification, lang)
+ posts_per_classification[classification] = posts
+ # Create hierarchy information
+ if taxonomy.has_hierarchy:
+ site.hierarchy_per_classification[taxonomy.classification_name] = {}
+ site.flat_hierarchy_per_classification[taxonomy.classification_name] = {}
+ site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {}
+ for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items():
+ # Compose hierarchy
+ hierarchy = {}
+ for classification in posts_per_classification.keys():
+ hier = taxonomy.extract_hierarchy(classification)
+ node = hierarchy
+ for he in hier:
+ if he not in node:
+ node[he] = {}
+ node = node[he]
+ hierarchy_lookup = {}
+
+ def create_hierarchy(hierarchy, parent=None, level=0):
+ """Create hierarchy."""
+ result = {}
+ for name, children in hierarchy.items():
+ node = hierarchy_utils.TreeNode(name, parent)
+ node.children = create_hierarchy(children, node, level + 1)
+ node.classification_path = [pn.name for pn in node.get_path()]
+ node.classification_name = taxonomy.recombine_classification_from_hierarchy(node.classification_path)
+ hierarchy_lookup[node.classification_name] = node
+ result[node.name] = node
+ classifications = natsort.natsorted(result.keys(), alg=natsort.ns.F | natsort.ns.IC)
+ taxonomy.sort_classifications(classifications, lang, level=level)
+ return [result[classification] for classification in classifications]
+
+ root_list = create_hierarchy(hierarchy)
+ if '' in posts_per_classification:
+ node = hierarchy_utils.TreeNode('', parent=None)
+ node.children = root_list
+ node.classification_path = []
+ node.classification_name = ''
+ hierarchy_lookup[node.name] = node
+ root_list = [node]
+ flat_hierarchy = hierarchy_utils.flatten_tree_structure(root_list)
+ # Store result
+ site.hierarchy_per_classification[taxonomy.classification_name][lang] = root_list
+ site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy
+ site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup
+ taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name],
+ site.flat_hierarchy_per_classification[taxonomy.classification_name],
+ site.hierarchy_lookup_per_classification[taxonomy.classification_name])
+ else:
+ taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name])
+
+ # Check for valid paths and for collisions
+ taxonomy_outputs = {lang: dict() for lang in site.config['TRANSLATIONS'].keys()}
+ quit = False
+ for taxonomy in taxonomies:
+ # Check for collisions (per language)
+ for lang in site.config['TRANSLATIONS'].keys():
+ if not taxonomy.is_enabled(lang):
+ continue
+ for classification, posts in site.posts_per_classification[taxonomy.classification_name][lang].items():
+ # Do we actually generate this classification page?
+ filtered_posts = [x for x in posts if self.site.config["SHOW_UNTRANSLATED_POSTS"] or x.is_translation_available(lang)]
+ generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang)
+ if not generate_list:
+ continue
+ # Obtain path as tuple
+ path = site.path_handlers[taxonomy.classification_name](classification, lang)
+ # Check that path is OK
+ for path_element in path:
+ if len(path_element) == 0:
+ utils.LOGGER.error("{0} {1} yields invalid path '{2}'!".format(taxonomy.classification_name.title(), classification, '/'.join(path)))
+ quit = True
+ # Combine path
+ path = os.path.join(*[os.path.normpath(p) for p in path if p != '.'])
+ # Determine collisions
+ if path in taxonomy_outputs[lang]:
+ other_classification_name, other_classification, other_posts = taxonomy_outputs[lang][path]
+ if other_classification_name == taxonomy.classification_name and other_classification == classification:
+ taxonomy_outputs[lang][path][2].extend(filtered_posts)
+ else:
+ utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {2} "{3}" both result in output path {4} for language {5}.'.format(
+ taxonomy.classification_name, classification, other_classification_name, other_classification, path, lang))
+ utils.LOGGER.error('{0} "{1}" is used in: {2}'.format(
+ taxonomy.classification_name.title(), classification, ', '.join(sorted([p.source_path for p in filtered_posts]))))
+ utils.LOGGER.error('{0} "{1}" is used in: {2}'.format(
+ other_classification_name.title(), other_classification, ', '.join(sorted([p.source_path for p in other_posts]))))
+ quit = True
+ else:
+ taxonomy_outputs[lang][path] = (taxonomy.classification_name, classification, list(posts))
+ if quit:
+ sys.exit(1)
+ blinker.signal('taxonomies_classified').send(site)
+
+ def _get_filtered_list(self, taxonomy, classification, lang):
+ """Return the filtered list of posts for this classification and language."""
+ post_list = self.site.posts_per_classification[taxonomy.classification_name][lang].get(classification, [])
+ if self.site.config["SHOW_UNTRANSLATED_POSTS"]:
+ return post_list
+ else:
+ return [x for x in post_list if x.is_translation_available(lang)]
+
+ @staticmethod
+ def _compute_number_of_pages(filtered_posts, posts_count):
+ """Given a list of posts and the maximal number of posts per page, computes the number of pages needed."""
+ return min(1, (len(filtered_posts) + posts_count - 1) // posts_count)
+
+ def _postprocess_path(self, path, lang, append_index='auto', dest_type='page', page_info=None, alternative_path=False):
+ """Postprocess a generated path.
+
+ Takes the path `path` for language `lang`, and postprocesses it.
+
+ It appends `site.config['INDEX_FILE']` depending on `append_index`
+ (which can have the values `'always'`, `'never'` and `'auto'`) and
+ `site.config['PRETTY_URLS']`.
+
+ It also modifies/adds the extension of the last path element resp.
+ `site.config['INDEX_FILE']` depending on `dest_type`, which can be
+ `'feed'`, `'rss'` or `'page'`.
+
+ If `dest_type` is `'page'`, `page_info` can be `None` or a tuple
+ of two integers: the page number and the number of pages. This will
+ be used to append the correct page number by calling
+ `utils.adjust_name_for_index_path_list` and
+ `utils.get_displayed_page_number`.
+
+ If `alternative_path` is set to `True`, `utils.adjust_name_for_index_path_list`
+ is called with `force_addition=True`, resulting in an alternative path for the
+ first page of an index or Atom feed by including the page number into the path.
+ """
+ # Forcing extension for Atom feeds and RSS feeds
+ force_extension = None
+ if dest_type == 'feed':
+ force_extension = self.site.config['ATOM_EXTENSION']
+ elif dest_type == 'rss':
+ force_extension = self.site.config['RSS_EXTENSION']
+ # Determine how to extend path
+ path = [_f for _f in path if _f]
+ if force_extension is not None:
+ if len(path) == 0 and dest_type == 'rss':
+ path = [self.site.config['RSS_FILENAME_BASE'](lang)]
+ elif len(path) == 0 and dest_type == 'feed':
+ path = [self.site.config['ATOM_FILENAME_BASE'](lang)]
+ elif len(path) == 0 or append_index == 'always':
+ path = path + [os.path.splitext(self.site.config['INDEX_FILE'])[0]]
+ elif len(path) > 0 and append_index == 'never':
+ path[-1] = os.path.splitext(path[-1])[0]
+ path[-1] += force_extension
+ elif (self.site.config['PRETTY_URLS'] and append_index != 'never') or len(path) == 0 or append_index == 'always':
+ path = path + [self.site.config['INDEX_FILE']]
+ elif append_index != 'never':
+ path[-1] += '.html'
+ # Create path
+ result = [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + path if _f]
+ if page_info is not None and dest_type in ('page', 'feed'):
+ result = utils.adjust_name_for_index_path_list(result,
+ page_info[0],
+ utils.get_displayed_page_number(page_info[0], page_info[1], self.site),
+ lang,
+ self.site, force_addition=alternative_path, extension=force_extension)
+ return result
+
+ @staticmethod
+ def _parse_path_result(result):
+ """Interpret the return values of taxonomy.get_path() and taxonomy.get_overview_path() as if all three return values were given."""
+ if not isinstance(result[0], (list, tuple)):
+ # The result must be a list or tuple of strings. Wrap into a tuple
+ result = (result, )
+ path = result[0]
+ append_index = result[1] if len(result) > 1 else 'auto'
+ page_info = result[2] if len(result) > 2 else None
+ return path, append_index, page_info
+
+ def _taxonomy_index_path(self, name, lang, taxonomy):
+ """Return path to the classification overview."""
+ result = taxonomy.get_overview_path(lang)
+ path, append_index, _ = self._parse_path_result(result)
+ return self._postprocess_path(path, lang, append_index=append_index, dest_type='list')
+
+ def _taxonomy_path(self, name, lang, taxonomy, dest_type='page', page=None, alternative_path=False):
+ """Return path to a classification."""
+ if taxonomy.has_hierarchy:
+ result = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang, dest_type=dest_type)
+ else:
+ result = taxonomy.get_path(name, lang, dest_type=dest_type)
+ path, append_index, page_ = self._parse_path_result(result)
+
+ if page is not None:
+ page = int(page)
+ else:
+ page = page_
+
+ page_info = None
+ if taxonomy.show_list_as_index and page is not None:
+ number_of_pages = self.site.page_count_per_classification[taxonomy.classification_name][lang].get(name)
+ if number_of_pages is None:
+ number_of_pages = self._compute_number_of_pages(self._get_filtered_list(taxonomy, name, lang), self.site.config['INDEX_DISPLAY_POST_COUNT'])
+ self.site.page_count_per_classification[taxonomy.classification_name][lang][name] = number_of_pages
+ page_info = (page, number_of_pages)
+ return self._postprocess_path(path, lang, append_index=append_index, dest_type=dest_type, page_info=page_info)
+
+ def _taxonomy_atom_path(self, name, lang, taxonomy, page=None, alternative_path=False):
+ """Return path to a classification Atom feed."""
+ return self._taxonomy_path(name, lang, taxonomy, dest_type='feed', page=page, alternative_path=alternative_path)
+
+ def _taxonomy_rss_path(self, name, lang, taxonomy):
+ """Return path to a classification RSS feed."""
+ return self._taxonomy_path(name, lang, taxonomy, dest_type='rss')
+
+ def _register_path_handlers(self, taxonomy):
+ functions = (
+ ('{0}_index', self._taxonomy_index_path),
+ ('{0}', self._taxonomy_path),
+ ('{0}_atom', self._taxonomy_atom_path),
+ ('{0}_rss', self._taxonomy_rss_path),
+ )
+
+ for name, function in functions:
+ name = name.format(taxonomy.classification_name)
+ p = functools.partial(function, taxonomy=taxonomy)
+ doc = taxonomy.path_handler_docstrings[name]
+ if doc is not False:
+ p.__doc__ = doc
+ self.site.register_path_handler(name, p)
+
+ def set_site(self, site):
+ """Set site, which is a Nikola instance."""
+ super().set_site(site)
+ # Add hook for after post scanning
+ blinker.signal("scanned").connect(self._do_classification)
+ # Register path handlers
+ for taxonomy in site.taxonomy_plugins.values():
+ self._register_path_handlers(taxonomy)
diff --git a/nikola/plugins/shortcode/chart.plugin b/nikola/plugins/shortcode/chart.plugin
new file mode 100644
index 0000000..edcbc13
--- /dev/null
+++ b/nikola/plugins/shortcode/chart.plugin
@@ -0,0 +1,13 @@
+[Core]
+name = chart
+module = chart
+
+[Nikola]
+PluginCategory = Shortcode
+
+[Documentation]
+author = Roberto Alsina
+version = 0.1
+website = https://getnikola.com/
+description = Chart directive based in PyGal
+
diff --git a/nikola/plugins/shortcode/chart.py b/nikola/plugins/shortcode/chart.py
new file mode 100644
index 0000000..64341e8
--- /dev/null
+++ b/nikola/plugins/shortcode/chart.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""Chart shortcode."""
+
+from ast import literal_eval
+
+from nikola.plugin_categories import ShortcodePlugin
+from nikola.utils import req_missing, load_data
+
+try:
+ import pygal
+except ImportError:
+ pygal = None
+
+_site = None
+
+
+class ChartShortcode(ShortcodePlugin):
+ """Plugin for chart shortcode."""
+
+ name = "chart"
+
+ def handler(self, chart_type, **_options):
+ """Generate chart using Pygal."""
+ if pygal is None:
+ msg = req_missing(
+ ['pygal'], 'use the Chart directive', optional=True)
+ return '<div class="text-error">{0}</div>'.format(msg)
+ options = {}
+ chart_data = []
+ _options.pop('post', None)
+ _options.pop('site')
+ data = _options.pop('data')
+
+ for line in data.splitlines():
+ line = line.strip()
+ if line:
+ chart_data.append(literal_eval('({0})'.format(line)))
+ if 'data_file' in _options:
+ options = load_data(_options['data_file'])
+ _options.pop('data_file')
+ if not chart_data: # If there is data in the document, it wins
+ for k, v in options.pop('data', {}).items():
+ chart_data.append((k, v))
+
+ options.update(_options)
+
+ style_name = options.pop('style', 'BlueStyle')
+ if '(' in style_name: # Parametric style
+ style = eval('pygal.style.' + style_name)
+ else:
+ style = getattr(pygal.style, style_name)
+ for k, v in options.items():
+ try:
+ options[k] = literal_eval(v)
+ except Exception:
+ options[k] = v
+ chart = pygal
+ for o in chart_type.split('.'):
+ chart = getattr(chart, o)
+ chart = chart(style=style)
+ if _site and _site.invariant:
+ chart.no_prefix = True
+ chart.config(**options)
+ for label, series in chart_data:
+ chart.add(label, series)
+ return chart.render().decode('utf8')
diff --git a/nikola/plugins/shortcode/emoji.plugin b/nikola/plugins/shortcode/emoji.plugin
new file mode 100644
index 0000000..c9a272c
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji.plugin
@@ -0,0 +1,13 @@
+[Core]
+name = emoji
+module = emoji
+
+[Nikola]
+PluginCategory = Shortcode
+
+[Documentation]
+author = Roberto Alsina
+version = 0.1
+website = https://getnikola.com/
+description = emoji shortcode
+
diff --git a/nikola/plugins/shortcode/emoji/__init__.py b/nikola/plugins/shortcode/emoji/__init__.py
new file mode 100644
index 0000000..9ae2228
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/__init__.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# This file is public domain according to its author, Roberto Alsina
+
+"""Emoji directive for reStructuredText."""
+
+import glob
+import json
+import os
+
+from nikola.plugin_categories import ShortcodePlugin
+from nikola import utils
+
+TABLE = {}
+
+LOGGER = utils.get_logger('scan_posts')
+
+
+def _populate():
+ for fname in glob.glob(os.path.join(os.path.dirname(__file__), 'data', '*.json')):
+ with open(fname, encoding="utf-8-sig") as inf:
+ data = json.load(inf)
+ data = data[list(data.keys())[0]]
+ data = data[list(data.keys())[0]]
+ for item in data:
+ if item['key'] in TABLE:
+ LOGGER.warning('Repeated emoji {}'.format(item['key']))
+ else:
+ TABLE[item['key']] = item['value']
+
+
+class Plugin(ShortcodePlugin):
+ """Plugin for gist directive."""
+
+ name = "emoji"
+
+ def handler(self, name, filename=None, site=None, data=None, lang=None, post=None):
+ """Create HTML for emoji."""
+ if not TABLE:
+ _populate()
+ try:
+ output = u'''<span class="emoji">{}</span>'''.format(TABLE[name])
+ except KeyError:
+ LOGGER.warning('Unknown emoji {}'.format(name))
+ output = u'''<span class="emoji error">{}</span>'''.format(name)
+
+ return output, []
diff --git a/nikola/plugins/shortcode/emoji/data/Activity.json b/nikola/plugins/shortcode/emoji/data/Activity.json
new file mode 100644
index 0000000..1461f19
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Activity.json
@@ -0,0 +1,418 @@
+{
+ "activities": {
+ "activity": [
+ {
+ "key": "soccer_ball",
+ "value": "⚽"
+ },
+ {
+ "key": "basket_ball",
+ "value": "🏀"
+ },
+ {
+ "key": "american_football",
+ "value": "🏈"
+ },
+ {
+ "key": "baseball",
+ "value": "⚾"
+ },
+ {
+ "key": "tennis_racquet_ball",
+ "value": "🎾"
+ },
+ {
+ "key": "volley_ball",
+ "value": "🏐"
+ },
+ {
+ "key": "rugby_football",
+ "value": "🏉"
+ },
+ {
+ "key": "billiards",
+ "value": "🎱"
+ },
+ {
+ "key": "activity_in_hole",
+ "value": "⛳"
+ },
+ {
+ "key": "golfer",
+ "value": "🏌"
+ },
+ {
+ "key": "table_tennis_paddle_ball",
+ "value": "🏓"
+ },
+ {
+ "key": "badminton_racquet_shuttle_cock",
+ "value": "🏸"
+ },
+ {
+ "key": "ice_hockey_stick_puck",
+ "value": "🏒"
+ },
+ {
+ "key": "field_hockey_stick_ball",
+ "value": "🏑"
+ },
+ {
+ "key": "cricket_bat_ball",
+ "value": "🏏"
+ },
+ {
+ "key": "ski_and_ski_boot",
+ "value": "🎿"
+ },
+ {
+ "key": "skier",
+ "value": "⛷"
+ },
+ {
+ "key": "snow_boarder",
+ "value": "🏂"
+ },
+ {
+ "key": "ice_skate",
+ "value": "⛸"
+ },
+ {
+ "key": "bow_and_arrow",
+ "value": "🏹"
+ },
+ {
+ "key": "fishing_pole_and_fish",
+ "value": "🎣"
+ },
+ {
+ "key": "row_boat",
+ "value": "🚣"
+ },
+ {
+ "key": "row_boat_type_1_2",
+ "value": "🚣🏻"
+ },
+ {
+ "key": "row_boat_type_3",
+ "value": "🚣🏼"
+ },
+ {
+ "key": "row_boat_type_4",
+ "value": "🚣🏽"
+ },
+ {
+ "key": "row_boat_type_5",
+ "value": "🚣🏾"
+ },
+ {
+ "key": "row_boat_type_6",
+ "value": "🚣🏿"
+ },
+ {
+ "key": "swimmer",
+ "value": "🏊"
+ },
+ {
+ "key": "swimmer_type_1_2",
+ "value": "🏊🏻"
+ },
+ {
+ "key": "swimmer_type_3",
+ "value": "🏊🏼"
+ },
+ {
+ "key": "swimmer_type_4",
+ "value": "🏊🏽"
+ },
+ {
+ "key": "swimmer_type_5",
+ "value": "🏊🏾"
+ },
+ {
+ "key": "swimmer_type_6",
+ "value": "🏊🏿"
+ },
+ {
+ "key": "surfer",
+ "value": "🏄"
+ },
+ {
+ "key": "surfer_type_1_2",
+ "value": "🏄🏻"
+ },
+ {
+ "key": "surfer_type_3",
+ "value": "🏄🏼"
+ },
+ {
+ "key": "surfer_type_4",
+ "value": "🏄🏽"
+ },
+ {
+ "key": "surfer_type_5",
+ "value": "🏄🏾"
+ },
+ {
+ "key": "surfer_type_6",
+ "value": "🏄🏿"
+ },
+ {
+ "key": "bath",
+ "value": "🛀"
+ },
+ {
+ "key": "bath_type_1_2",
+ "value": "🛀🏻"
+ },
+ {
+ "key": "bath_type_3",
+ "value": "🛀🏼"
+ },
+ {
+ "key": "bath_type_4",
+ "value": "🛀🏽"
+ },
+ {
+ "key": "bath_type_5",
+ "value": "🛀🏾"
+ },
+ {
+ "key": "bath_type_6",
+ "value": "🛀🏿"
+ },
+ {
+ "key": "person_with_ball",
+ "value": "⛹"
+ },
+ {
+ "key": "person_with_ball_type_1_2",
+ "value": "⛹🏻"
+ },
+ {
+ "key": "person_with_ball_type_3",
+ "value": "⛹🏼"
+ },
+ {
+ "key": "person_with_ball_type_4",
+ "value": "⛹🏽"
+ },
+ {
+ "key": "person_with_ball_type_5",
+ "value": "⛹🏾"
+ },
+ {
+ "key": "person_with_ball_type_6",
+ "value": "⛹🏿"
+ },
+ {
+ "key": "weight_lifter",
+ "value": "🏋"
+ },
+ {
+ "key": "weight_lifter_type_1_2",
+ "value": "🏋🏻"
+ },
+ {
+ "key": "weight_lifter_type_3",
+ "value": "🏋🏼"
+ },
+ {
+ "key": "weight_lifter_type_4",
+ "value": "🏋🏽"
+ },
+ {
+ "key": "weight_lifter_type_5",
+ "value": "🏋🏾"
+ },
+ {
+ "key": "weight_lifter_type_6",
+ "value": "🏋🏿"
+ },
+ {
+ "key": "bicyclist",
+ "value": "🚴"
+ },
+ {
+ "key": "bicyclist_type_1_2",
+ "value": "🚴🏻"
+ },
+ {
+ "key": "bicyclist_type_3",
+ "value": "🚴🏼"
+ },
+ {
+ "key": "bicyclist_type_4",
+ "value": "🚴🏽"
+ },
+ {
+ "key": "bicyclist_type_5",
+ "value": "🚴🏾"
+ },
+ {
+ "key": "bicyclist_type_6",
+ "value": "🚴🏿"
+ },
+ {
+ "key": "mountain_bicyclist",
+ "value": "🚵"
+ },
+ {
+ "key": "mountain_bicyclist_type_1_2",
+ "value": "🚵🏻"
+ },
+ {
+ "key": "mountain_bicyclist_type_3",
+ "value": "🚵🏼"
+ },
+ {
+ "key": "mountain_bicyclist_type_4",
+ "value": "🚵🏽"
+ },
+ {
+ "key": "mountain_bicyclist_type_5",
+ "value": "🚵🏾"
+ },
+ {
+ "key": "mountain_bicyclist_type_6",
+ "value": "🚵🏿"
+ },
+ {
+ "key": "horse_racing",
+ "value": "🏇"
+ },
+ {
+ "key": "horse_racing_type_1_2",
+ "value": "🏇🏻"
+ },
+ {
+ "key": "horse_racing_type_3",
+ "value": "🏇🏻"
+ },
+ {
+ "key": "horse_racing_type_4",
+ "value": "🏇🏽"
+ },
+ {
+ "key": "horse_racing_type_5",
+ "value": "🏇🏾"
+ },
+ {
+ "key": "horse_racing_type_6",
+ "value": "🏇🏿"
+ },
+ {
+ "key": "main_business_suit_levitating",
+ "value": "🕴"
+ },
+ {
+ "key": "trophy",
+ "value": "🏆"
+ },
+ {
+ "key": "running_shirt_with_sash",
+ "value": "🎽"
+ },
+ {
+ "key": "sports_medal",
+ "value": "🏅"
+ },
+ {
+ "key": "military_medal",
+ "value": "🎖"
+ },
+ {
+ "key": "reminder_ribbon",
+ "value": "🎗"
+ },
+ {
+ "key": "rosette",
+ "value": "🏵"
+ },
+ {
+ "key": "ticket",
+ "value": "🎫"
+ },
+ {
+ "key": "admission_tickets",
+ "value": "🎟"
+ },
+ {
+ "key": "performing_arts",
+ "value": "🎭"
+ },
+ {
+ "key": "artist_palette",
+ "value": "🎨"
+ },
+ {
+ "key": "circus_tent",
+ "value": "🎪"
+ },
+ {
+ "key": "microphone",
+ "value": "🎤"
+ },
+ {
+ "key": "headphone",
+ "value": "🎧"
+ },
+ {
+ "key": "musical_score",
+ "value": "🎼"
+ },
+ {
+ "key": "musical_keyboard",
+ "value": "🎹"
+ },
+ {
+ "key": "saxophone",
+ "value": "🎷"
+ },
+ {
+ "key": "trumpet",
+ "value": "🎺"
+ },
+ {
+ "key": "guitar",
+ "value": "🎸"
+ },
+ {
+ "key": "violin",
+ "value": "🎻"
+ },
+ {
+ "key": "clapper_board",
+ "value": "🎬"
+ },
+ {
+ "key": "video_game",
+ "value": "🎮"
+ },
+ {
+ "key": "alien_monster",
+ "value": "👾"
+ },
+ {
+ "key": "direct_hit",
+ "value": "🎯"
+ },
+ {
+ "key": "game_die",
+ "value": "🎲"
+ },
+ {
+ "key": "slot_machine",
+ "value": "🎰"
+ },
+ {
+ "key": "bowling",
+ "value": "🎳"
+ },
+ {
+ "key": "olympic_rings",
+ "value": "◯‍◯‍◯‍◯‍◯"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/Flags.json b/nikola/plugins/shortcode/emoji/data/Flags.json
new file mode 100644
index 0000000..d1d4bdc
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Flags.json
@@ -0,0 +1,998 @@
+{
+ "flags": {
+ "flag": [
+ {
+ "key": "afghanistan",
+ "value": "🇦🇫"
+ },
+ {
+ "key": "land_island",
+ "value": "🇦🇽"
+ },
+ {
+ "key": "albania",
+ "value": "🇦🇱"
+ },
+ {
+ "key": "algeria",
+ "value": "🇩🇿"
+ },
+ {
+ "key": "american_samoa",
+ "value": "🇦🇸"
+ },
+ {
+ "key": "andorra",
+ "value": "🇦🇩"
+ },
+ {
+ "key": "angola",
+ "value": "🇦🇴"
+ },
+ {
+ "key": "anguilla",
+ "value": "🇦🇮"
+ },
+ {
+ "key": "antarctica",
+ "value": "🇦🇶"
+ },
+ {
+ "key": "antigua_and_barbuda",
+ "value": "🇦🇬"
+ },
+ {
+ "key": "argentina",
+ "value": "🇦🇷"
+ },
+ {
+ "key": "armenia",
+ "value": "🇦🇲"
+ },
+ {
+ "key": "aruba",
+ "value": "🇦🇼"
+ },
+ {
+ "key": "australia",
+ "value": "🇦🇺"
+ },
+ {
+ "key": "austria",
+ "value": "🇦🇹"
+ },
+ {
+ "key": "azerbaijan",
+ "value": "🇦🇿"
+ },
+ {
+ "key": "bahamas",
+ "value": "🇧🇸"
+ },
+ {
+ "key": "bahrain",
+ "value": "🇧🇭"
+ },
+ {
+ "key": "bangladesh",
+ "value": "🇧🇩"
+ },
+ {
+ "key": "barbados",
+ "value": "🇧🇧"
+ },
+ {
+ "key": "belarus",
+ "value": "🇧🇾"
+ },
+ {
+ "key": "belgium",
+ "value": "🇧🇪"
+ },
+ {
+ "key": "belize",
+ "value": "🇧🇿"
+ },
+ {
+ "key": "benin",
+ "value": "🇧🇯"
+ },
+ {
+ "key": "bermuda",
+ "value": "🇧🇲"
+ },
+ {
+ "key": "bhutan",
+ "value": "🇧🇹"
+ },
+ {
+ "key": "bolivia",
+ "value": "🇧🇴"
+ },
+ {
+ "key": "caribbean_netherlands",
+ "value": "🇧🇶"
+ },
+ {
+ "key": "bosnia_and_herzegovina",
+ "value": "🇧🇦"
+ },
+ {
+ "key": "botswana",
+ "value": "🇧🇼"
+ },
+ {
+ "key": "brazil",
+ "value": "🇧🇷"
+ },
+ {
+ "key": "british_indian_ocean_territory",
+ "value": "🇮🇴"
+ },
+ {
+ "key": "british_virgin_islands",
+ "value": "🇻🇬"
+ },
+ {
+ "key": "brunei",
+ "value": "🇧🇳"
+ },
+ {
+ "key": "bulgaria",
+ "value": "🇧🇬"
+ },
+ {
+ "key": "burkina_faso",
+ "value": "🇧🇫"
+ },
+ {
+ "key": "burundi",
+ "value": "🇧🇮"
+ },
+ {
+ "key": "cape_verde",
+ "value": "🇨🇻"
+ },
+ {
+ "key": "cambodia",
+ "value": "🇰🇭"
+ },
+ {
+ "key": "cameroon",
+ "value": "🇨🇲"
+ },
+ {
+ "key": "canada",
+ "value": "🇨🇦"
+ },
+ {
+ "key": "canary_islands",
+ "value": "🇮🇨"
+ },
+ {
+ "key": "cayman_islands",
+ "value": "🇰🇾"
+ },
+ {
+ "key": "central_african_republic",
+ "value": "🇨🇫"
+ },
+ {
+ "key": "chad",
+ "value": "🇹🇩"
+ },
+ {
+ "key": "chile",
+ "value": "🇨🇱"
+ },
+ {
+ "key": "china",
+ "value": "🇨🇳"
+ },
+ {
+ "key": "christmas_island",
+ "value": "🇨🇽"
+ },
+ {
+ "key": "cocos_keeling_island",
+ "value": "🇨🇨"
+ },
+ {
+ "key": "colombia",
+ "value": "🇨🇴"
+ },
+ {
+ "key": "comoros",
+ "value": "🇰🇲"
+ },
+ {
+ "key": "congo_brazzaville",
+ "value": "🇨🇬"
+ },
+ {
+ "key": "congo_kingshasa",
+ "value": "🇨🇩"
+ },
+ {
+ "key": "cook_islands",
+ "value": "🇨🇰"
+ },
+ {
+ "key": "costa_rica",
+ "value": "🇨🇷"
+ },
+ {
+ "key": "croatia",
+ "value": "🇭🇷"
+ },
+ {
+ "key": "cuba",
+ "value": "🇨🇺"
+ },
+ {
+ "key": "curaao",
+ "value": "🇨🇼"
+ },
+ {
+ "key": "cyprus",
+ "value": "🇨🇾"
+ },
+ {
+ "key": "czech_republic",
+ "value": "🇨🇿"
+ },
+ {
+ "key": "denmark",
+ "value": "🇩🇰"
+ },
+ {
+ "key": "djibouti",
+ "value": "🇩🇯"
+ },
+ {
+ "key": "dominica",
+ "value": "🇩🇲"
+ },
+ {
+ "key": "dominican_republic",
+ "value": "🇩🇴"
+ },
+ {
+ "key": "ecuador",
+ "value": "🇪🇨"
+ },
+ {
+ "key": "egypt",
+ "value": "🇪🇬"
+ },
+ {
+ "key": "el_salvador",
+ "value": "🇸🇻"
+ },
+ {
+ "key": "equatorial_guinea",
+ "value": "🇬🇶"
+ },
+ {
+ "key": "eritrea",
+ "value": "🇪🇷"
+ },
+ {
+ "key": "estonia",
+ "value": "🇪🇪"
+ },
+ {
+ "key": "ethiopia",
+ "value": "🇪🇹"
+ },
+ {
+ "key": "european_union",
+ "value": "🇪🇺"
+ },
+ {
+ "key": "falkland_islands",
+ "value": "🇫🇰"
+ },
+ {
+ "key": "faroe_islands",
+ "value": "🇫🇴"
+ },
+ {
+ "key": "fiji",
+ "value": "🇫🇯"
+ },
+ {
+ "key": "finland",
+ "value": "🇫🇮"
+ },
+ {
+ "key": "france",
+ "value": "🇫🇷"
+ },
+ {
+ "key": "french_guiana",
+ "value": "🇬🇫"
+ },
+ {
+ "key": "french_polynesia",
+ "value": "🇵🇫"
+ },
+ {
+ "key": "french_southern_territories",
+ "value": "🇹🇫"
+ },
+ {
+ "key": "gabon",
+ "value": "🇬🇦"
+ },
+ {
+ "key": "gambia",
+ "value": "🇬🇲"
+ },
+ {
+ "key": "georgia",
+ "value": "🇬🇪"
+ },
+ {
+ "key": "germany",
+ "value": "🇩🇪"
+ },
+ {
+ "key": "ghana",
+ "value": "🇬🇭"
+ },
+ {
+ "key": "gibraltar",
+ "value": "🇬🇮"
+ },
+ {
+ "key": "greece",
+ "value": "🇬🇷"
+ },
+ {
+ "key": "greenland",
+ "value": "🇬🇱"
+ },
+ {
+ "key": "grenada",
+ "value": "🇬🇩"
+ },
+ {
+ "key": "guadeloupe",
+ "value": "🇬🇵"
+ },
+ {
+ "key": "guam",
+ "value": "🇬🇺"
+ },
+ {
+ "key": "guatemala",
+ "value": "🇬🇹"
+ },
+ {
+ "key": "guernsey",
+ "value": "🇬🇬"
+ },
+ {
+ "key": "guinea",
+ "value": "🇬🇳"
+ },
+ {
+ "key": "guinea_bissau",
+ "value": "🇬🇼"
+ },
+ {
+ "key": "guyana",
+ "value": "🇬🇾"
+ },
+ {
+ "key": "haiti",
+ "value": "🇭🇹"
+ },
+ {
+ "key": "honduras",
+ "value": "🇭🇳"
+ },
+ {
+ "key": "hong_kong",
+ "value": "🇭🇰"
+ },
+ {
+ "key": "hungary",
+ "value": "🇭🇺"
+ },
+ {
+ "key": "iceland",
+ "value": "🇮🇸"
+ },
+ {
+ "key": "india",
+ "value": "🇮🇳"
+ },
+ {
+ "key": "indonesia",
+ "value": "🇮🇩"
+ },
+ {
+ "key": "iran",
+ "value": "🇮🇷"
+ },
+ {
+ "key": "iraq",
+ "value": "🇮🇶"
+ },
+ {
+ "key": "ireland",
+ "value": "🇮🇪"
+ },
+ {
+ "key": "isle_of_man",
+ "value": "🇮🇲"
+ },
+ {
+ "key": "israel",
+ "value": "🇮🇱"
+ },
+ {
+ "key": "italy",
+ "value": "🇮🇹"
+ },
+ {
+ "key": "ctedivoire",
+ "value": "🇨🇮"
+ },
+ {
+ "key": "jamaica",
+ "value": "🇯🇲"
+ },
+ {
+ "key": "japan",
+ "value": "🇯🇵"
+ },
+ {
+ "key": "jersey",
+ "value": "🇯🇪"
+ },
+ {
+ "key": "jordan",
+ "value": "🇯🇴"
+ },
+ {
+ "key": "kazakhstan",
+ "value": "🇰🇿"
+ },
+ {
+ "key": "kenya",
+ "value": "🇰🇪"
+ },
+ {
+ "key": "kiribati",
+ "value": "🇰🇮"
+ },
+ {
+ "key": "kosovo",
+ "value": "🇽🇰"
+ },
+ {
+ "key": "kuwait",
+ "value": "🇰🇼"
+ },
+ {
+ "key": "kyrgyzstan",
+ "value": "🇰🇬"
+ },
+ {
+ "key": "laos",
+ "value": "🇱🇦"
+ },
+ {
+ "key": "latvia",
+ "value": "🇱🇻"
+ },
+ {
+ "key": "lebanon",
+ "value": "🇱🇧"
+ },
+ {
+ "key": "lesotho",
+ "value": "🇱🇸"
+ },
+ {
+ "key": "liberia",
+ "value": "🇱🇷"
+ },
+ {
+ "key": "libya",
+ "value": "🇱🇾"
+ },
+ {
+ "key": "liechtenstein",
+ "value": "🇱🇮"
+ },
+ {
+ "key": "lithuania",
+ "value": "🇱🇹"
+ },
+ {
+ "key": "luxembourg",
+ "value": "🇱🇺"
+ },
+ {
+ "key": "macau",
+ "value": "🇲🇴"
+ },
+ {
+ "key": "macedonia",
+ "value": "🇲🇰"
+ },
+ {
+ "key": "madagascar",
+ "value": "🇲🇬"
+ },
+ {
+ "key": "malawi",
+ "value": "🇲🇼"
+ },
+ {
+ "key": "malaysia",
+ "value": "🇲🇾"
+ },
+ {
+ "key": "maldives",
+ "value": "🇲🇻"
+ },
+ {
+ "key": "mali",
+ "value": "🇲🇱"
+ },
+ {
+ "key": "malta",
+ "value": "🇲🇹"
+ },
+ {
+ "key": "marshall_islands",
+ "value": "🇲🇭"
+ },
+ {
+ "key": "martinique",
+ "value": "🇲🇶"
+ },
+ {
+ "key": "mauritania",
+ "value": "🇲🇷"
+ },
+ {
+ "key": "mauritius",
+ "value": "🇲🇺"
+ },
+ {
+ "key": "mayotte",
+ "value": "🇾🇹"
+ },
+ {
+ "key": "mexico",
+ "value": "🇲🇽"
+ },
+ {
+ "key": "micronesia",
+ "value": "🇫🇲"
+ },
+ {
+ "key": "moldova",
+ "value": "🇲🇩"
+ },
+ {
+ "key": "monaco",
+ "value": "🇲🇨"
+ },
+ {
+ "key": "mongolia",
+ "value": "🇲🇳"
+ },
+ {
+ "key": "montenegro",
+ "value": "🇲🇪"
+ },
+ {
+ "key": "montserrat",
+ "value": "🇲🇸"
+ },
+ {
+ "key": "morocco",
+ "value": "🇲🇦"
+ },
+ {
+ "key": "mozambique",
+ "value": "🇲🇿"
+ },
+ {
+ "key": "myanmar_burma",
+ "value": "🇲🇲"
+ },
+ {
+ "key": "namibia",
+ "value": "🇳🇦"
+ },
+ {
+ "key": "nauru",
+ "value": "🇳🇷"
+ },
+ {
+ "key": "nepal",
+ "value": "🇳🇵"
+ },
+ {
+ "key": "netherlands",
+ "value": "🇳🇱"
+ },
+ {
+ "key": "new_caledonia",
+ "value": "🇳🇨"
+ },
+ {
+ "key": "new_zealand",
+ "value": "🇳🇿"
+ },
+ {
+ "key": "nicaragua",
+ "value": "🇳🇮"
+ },
+ {
+ "key": "niger",
+ "value": "🇳🇪"
+ },
+ {
+ "key": "nigeria",
+ "value": "🇳🇬"
+ },
+ {
+ "key": "niue",
+ "value": "🇳🇺"
+ },
+ {
+ "key": "norfolk_island",
+ "value": "🇳🇫"
+ },
+ {
+ "key": "northern_mariana_islands",
+ "value": "🇲🇵"
+ },
+ {
+ "key": "north_korea",
+ "value": "🇰🇵"
+ },
+ {
+ "key": "norway",
+ "value": "🇳🇴"
+ },
+ {
+ "key": "oman",
+ "value": "🇴🇲"
+ },
+ {
+ "key": "pakistan",
+ "value": "🇵🇰"
+ },
+ {
+ "key": "palau",
+ "value": "🇵🇼"
+ },
+ {
+ "key": "palestinian_territories",
+ "value": "🇵🇸"
+ },
+ {
+ "key": "panama",
+ "value": "🇵🇦"
+ },
+ {
+ "key": "papua_new_guinea",
+ "value": "🇵🇬"
+ },
+ {
+ "key": "paraguay",
+ "value": "🇵🇾"
+ },
+ {
+ "key": "peru",
+ "value": "🇵🇪"
+ },
+ {
+ "key": "philippines",
+ "value": "🇵🇭"
+ },
+ {
+ "key": "pitcairn_islands",
+ "value": "🇵🇳"
+ },
+ {
+ "key": "poland",
+ "value": "🇵🇱"
+ },
+ {
+ "key": "portugal",
+ "value": "🇵🇹"
+ },
+ {
+ "key": "puerto_rico",
+ "value": "🇵🇷"
+ },
+ {
+ "key": "qatar",
+ "value": "🇶🇦"
+ },
+ {
+ "key": "reunion",
+ "value": "🇷🇪"
+ },
+ {
+ "key": "romania",
+ "value": "🇷🇴"
+ },
+ {
+ "key": "russia",
+ "value": "🇷🇺"
+ },
+ {
+ "key": "rwanda",
+ "value": "🇷🇼"
+ },
+ {
+ "key": "saint_barthlemy",
+ "value": "🇧🇱"
+ },
+ {
+ "key": "saint_helena",
+ "value": "🇸🇭"
+ },
+ {
+ "key": "saint_kitts_and_nevis",
+ "value": "🇰🇳"
+ },
+ {
+ "key": "saint_lucia",
+ "value": "🇱🇨"
+ },
+ {
+ "key": "saint_pierre_and_miquelon",
+ "value": "🇵🇲"
+ },
+ {
+ "key": "st_vincent_grenadines",
+ "value": "🇻🇨"
+ },
+ {
+ "key": "samoa",
+ "value": "🇼🇸"
+ },
+ {
+ "key": "san_marino",
+ "value": "🇸🇲"
+ },
+ {
+ "key": "sotom_and_prncipe",
+ "value": "🇸🇹"
+ },
+ {
+ "key": "saudi_arabia",
+ "value": "🇸🇦"
+ },
+ {
+ "key": "senegal",
+ "value": "🇸🇳"
+ },
+ {
+ "key": "serbia",
+ "value": "🇷🇸"
+ },
+ {
+ "key": "seychelles",
+ "value": "🇸🇨"
+ },
+ {
+ "key": "sierra_leone",
+ "value": "🇸🇱"
+ },
+ {
+ "key": "singapore",
+ "value": "🇸🇬"
+ },
+ {
+ "key": "sint_maarten",
+ "value": "🇸🇽"
+ },
+ {
+ "key": "slovakia",
+ "value": "🇸🇰"
+ },
+ {
+ "key": "slovenia",
+ "value": "🇸🇮"
+ },
+ {
+ "key": "solomon_islands",
+ "value": "🇸🇧"
+ },
+ {
+ "key": "somalia",
+ "value": "🇸🇴"
+ },
+ {
+ "key": "south_africa",
+ "value": "🇿🇦"
+ },
+ {
+ "key": "south_georgia_south_sandwich_islands",
+ "value": "🇬🇸"
+ },
+ {
+ "key": "south_korea",
+ "value": "🇰🇷"
+ },
+ {
+ "key": "south_sudan",
+ "value": "🇸🇸"
+ },
+ {
+ "key": "spain",
+ "value": "🇪🇸"
+ },
+ {
+ "key": "sri_lanka",
+ "value": "🇱🇰"
+ },
+ {
+ "key": "sudan",
+ "value": "🇸🇩"
+ },
+ {
+ "key": "suriname",
+ "value": "🇸🇷"
+ },
+ {
+ "key": "swaziland",
+ "value": "🇸🇿"
+ },
+ {
+ "key": "sweden",
+ "value": "🇸🇪"
+ },
+ {
+ "key": "switzerland",
+ "value": "🇨🇭"
+ },
+ {
+ "key": "syria",
+ "value": "🇸🇾"
+ },
+ {
+ "key": "taiwan",
+ "value": "🇹🇼"
+ },
+ {
+ "key": "tajikistan",
+ "value": "🇹🇯"
+ },
+ {
+ "key": "tanzania",
+ "value": "🇹🇿"
+ },
+ {
+ "key": "thailand",
+ "value": "🇹🇭"
+ },
+ {
+ "key": "timorleste",
+ "value": "🇹🇱"
+ },
+ {
+ "key": "togo",
+ "value": "🇹🇬"
+ },
+ {
+ "key": "tokelau",
+ "value": "🇹🇰"
+ },
+ {
+ "key": "tonga",
+ "value": "🇹🇴"
+ },
+ {
+ "key": "trinidad_and_tobago",
+ "value": "🇹🇹"
+ },
+ {
+ "key": "tunisia",
+ "value": "🇹🇳"
+ },
+ {
+ "key": "turkey",
+ "value": "🇹🇷"
+ },
+ {
+ "key": "turkmenistan",
+ "value": "🇹🇲"
+ },
+ {
+ "key": "turks_and_caicos_islands",
+ "value": "🇹🇨"
+ },
+ {
+ "key": "tuvalu",
+ "value": "🇹🇻"
+ },
+ {
+ "key": "uganda",
+ "value": "🇺🇬"
+ },
+ {
+ "key": "ukraine",
+ "value": "🇺🇦"
+ },
+ {
+ "key": "united_arab_emirates",
+ "value": "🇦🇪"
+ },
+ {
+ "key": "united_kingdom",
+ "value": "🇬🇧"
+ },
+ {
+ "key": "united_states",
+ "value": "🇺🇸"
+ },
+ {
+ "key": "us_virgin_islands",
+ "value": "🇻🇮"
+ },
+ {
+ "key": "uruguay",
+ "value": "🇺🇾"
+ },
+ {
+ "key": "uzbekistan",
+ "value": "🇺🇿"
+ },
+ {
+ "key": "vanuatu",
+ "value": "🇻🇺"
+ },
+ {
+ "key": "vatican_city",
+ "value": "🇻🇦"
+ },
+ {
+ "key": "venezuela",
+ "value": "🇻🇪"
+ },
+ {
+ "key": "vietnam",
+ "value": "🇻🇳"
+ },
+ {
+ "key": "wallis_and_futuna",
+ "value": "🇼🇫"
+ },
+ {
+ "key": "western_sahara",
+ "value": "🇪🇭"
+ },
+ {
+ "key": "yemen",
+ "value": "🇾🇪"
+ },
+ {
+ "key": "zambia",
+ "value": "🇿🇲"
+ },
+ {
+ "key": "zimbabwe",
+ "value": "🇿🇼"
+ },
+ {
+ "key": "england",
+ "value": "🇽🇪"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/Food.json b/nikola/plugins/shortcode/emoji/data/Food.json
new file mode 100644
index 0000000..c755a20
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Food.json
@@ -0,0 +1,274 @@
+{
+ "foods": {
+ "food": [
+ {
+ "key": "green_apple",
+ "value": "🍏"
+ },
+ {
+ "key": "red_apple",
+ "value": "🍎"
+ },
+ {
+ "key": "pear",
+ "value": "🍐"
+ },
+ {
+ "key": "tangerine",
+ "value": "🍊"
+ },
+ {
+ "key": "lemon",
+ "value": "🍋"
+ },
+ {
+ "key": "banana",
+ "value": "🍌"
+ },
+ {
+ "key": "watermelon",
+ "value": "🍉"
+ },
+ {
+ "key": "grapes",
+ "value": "🍇"
+ },
+ {
+ "key": "strawberry",
+ "value": "🍓"
+ },
+ {
+ "key": "melon",
+ "value": "🍈"
+ },
+ {
+ "key": "cherry",
+ "value": "🍒"
+ },
+ {
+ "key": "peach",
+ "value": "🍑"
+ },
+ {
+ "key": "pineapple",
+ "value": "🍍"
+ },
+ {
+ "key": "tomato",
+ "value": "🍅"
+ },
+ {
+ "key": "egg_plant",
+ "value": "🍆"
+ },
+ {
+ "key": "hot_pepper",
+ "value": "🌶"
+ },
+ {
+ "key": "ear_of_maize",
+ "value": "🌽"
+ },
+ {
+ "key": "roasted_sweet_potato",
+ "value": "🍠"
+ },
+ {
+ "key": "honey_pot",
+ "value": "🍯"
+ },
+ {
+ "key": "bread",
+ "value": "🍞"
+ },
+ {
+ "key": "cheese",
+ "value": "🧀"
+ },
+ {
+ "key": "poultry_leg",
+ "value": "🍗"
+ },
+ {
+ "key": "meat_on_bone",
+ "value": "🍖"
+ },
+ {
+ "key": "fried_shrimp",
+ "value": "🍤"
+ },
+ {
+ "key": "cooking",
+ "value": "🍳"
+ },
+ {
+ "key": "hamburger",
+ "value": "🍔"
+ },
+ {
+ "key": "french_fries",
+ "value": "🍟"
+ },
+ {
+ "key": "hot_dog",
+ "value": "🌭"
+ },
+ {
+ "key": "slice_of_pizza",
+ "value": "🍕"
+ },
+ {
+ "key": "spaghetti",
+ "value": "🍝"
+ },
+ {
+ "key": "taco",
+ "value": "🌮"
+ },
+ {
+ "key": "burrito",
+ "value": "🌯"
+ },
+ {
+ "key": "steaming_bowl",
+ "value": "🍜"
+ },
+ {
+ "key": "pot_of_food",
+ "value": "🍲"
+ },
+ {
+ "key": "fish_cake",
+ "value": "🍥"
+ },
+ {
+ "key": "sushi",
+ "value": "🍣"
+ },
+ {
+ "key": "bento_box",
+ "value": "🍱"
+ },
+ {
+ "key": "curry_and_rice",
+ "value": "🍛"
+ },
+ {
+ "key": "rice_ball",
+ "value": "🍙"
+ },
+ {
+ "key": "cooked_rice",
+ "value": "🍚"
+ },
+ {
+ "key": "rice_cracker",
+ "value": "🍘"
+ },
+ {
+ "key": "oden",
+ "value": "🍢"
+ },
+ {
+ "key": "dango",
+ "value": "🍡"
+ },
+ {
+ "key": "shaved_ice",
+ "value": "🍧"
+ },
+ {
+ "key": "ice_cream",
+ "value": "🍨"
+ },
+ {
+ "key": "soft_ice_cream",
+ "value": "🍦"
+ },
+ {
+ "key": "short_cake",
+ "value": "🍰"
+ },
+ {
+ "key": "birthday_cake",
+ "value": "🎂"
+ },
+ {
+ "key": "custard",
+ "value": "🍮"
+ },
+ {
+ "key": "candy",
+ "value": "🍬"
+ },
+ {
+ "key": "lollipop",
+ "value": "🍭"
+ },
+ {
+ "key": "chocolate_bar",
+ "value": "🍫"
+ },
+ {
+ "key": "popcorn",
+ "value": "🍿"
+ },
+ {
+ "key": "doughnut",
+ "value": "🍩"
+ },
+ {
+ "key": "cookie",
+ "value": "🍪"
+ },
+ {
+ "key": "bear_mug",
+ "value": "🍺"
+ },
+ {
+ "key": "clinking_beer_mugs",
+ "value": "🍻"
+ },
+ {
+ "key": "wine_glass",
+ "value": "🍷"
+ },
+ {
+ "key": "cocktail_glass",
+ "value": "🍸"
+ },
+ {
+ "key": "tropical_drink",
+ "value": "🍹"
+ },
+ {
+ "key": "bottle_with_popping_cork",
+ "value": "🍾"
+ },
+ {
+ "key": "sake_bottle_and_cup",
+ "value": "🍶"
+ },
+ {
+ "key": "tea_cup_without_handle",
+ "value": "🍵"
+ },
+ {
+ "key": "hot_beverage",
+ "value": "☕"
+ },
+ {
+ "key": "baby_bottle",
+ "value": "🍼"
+ },
+ {
+ "key": "fork_and_knife",
+ "value": "🍴"
+ },
+ {
+ "key": "fork_and_knife_with_plate",
+ "value": "🍽"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/LICENSE b/nikola/plugins/shortcode/emoji/data/LICENSE
new file mode 100644
index 0000000..c7bf1f4
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/LICENSE
@@ -0,0 +1,25 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 -2017 Shayan Rais
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------------
+
+Copied from https://github.com/shanraisshan/EmojiCodeSheet
diff --git a/nikola/plugins/shortcode/emoji/data/Nature.json b/nikola/plugins/shortcode/emoji/data/Nature.json
new file mode 100644
index 0000000..f845a64
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Nature.json
@@ -0,0 +1,594 @@
+{
+ "natures": {
+ "nature": [
+ {
+ "key": "dog_face",
+ "value": "🐶"
+ },
+ {
+ "key": "cat_face",
+ "value": "🐱"
+ },
+ {
+ "key": "mouse_face",
+ "value": "🐭"
+ },
+ {
+ "key": "hamster_face",
+ "value": "🐹"
+ },
+ {
+ "key": "rabbit_face",
+ "value": "🐰"
+ },
+ {
+ "key": "bear_face",
+ "value": "🐻"
+ },
+ {
+ "key": "panda_face",
+ "value": "🐼"
+ },
+ {
+ "key": "koala_face",
+ "value": "🐨"
+ },
+ {
+ "key": "lion_face",
+ "value": "🦁"
+ },
+ {
+ "key": "cow_face",
+ "value": "🐮"
+ },
+ {
+ "key": "pig_face",
+ "value": "🐷"
+ },
+ {
+ "key": "pig_nose",
+ "value": "🐽"
+ },
+ {
+ "key": "frog_face",
+ "value": "🐸"
+ },
+ {
+ "key": "octopus",
+ "value": "🐙"
+ },
+ {
+ "key": "monkey_face",
+ "value": "🐵"
+ },
+ {
+ "key": "tiger_face",
+ "value": "🐯"
+ },
+ {
+ "key": "see_no_evil_monkey",
+ "value": "🙈"
+ },
+ {
+ "key": "hear_no_evil_monkey",
+ "value": "🙉"
+ },
+ {
+ "key": "speak_no_evil_monkey",
+ "value": "🙊"
+ },
+ {
+ "key": "monkey",
+ "value": "🐒"
+ },
+ {
+ "key": "chicken",
+ "value": "🐔"
+ },
+ {
+ "key": "penguin",
+ "value": "🐧"
+ },
+ {
+ "key": "bird",
+ "value": "🐦"
+ },
+ {
+ "key": "baby_chick",
+ "value": "🐤"
+ },
+ {
+ "key": "hatching_chick",
+ "value": "🐣"
+ },
+ {
+ "key": "front_face_chick",
+ "value": "🐥"
+ },
+ {
+ "key": "wolf_face",
+ "value": "🐺"
+ },
+ {
+ "key": "boar",
+ "value": "🐗"
+ },
+ {
+ "key": "horse_face",
+ "value": "🐴"
+ },
+ {
+ "key": "unicorn_face",
+ "value": "🦄"
+ },
+ {
+ "key": "honey_bee",
+ "value": "🐝"
+ },
+ {
+ "key": "bug",
+ "value": "🐛"
+ },
+ {
+ "key": "snail",
+ "value": "🐌"
+ },
+ {
+ "key": "lady_beetle",
+ "value": "🐞"
+ },
+ {
+ "key": "ant",
+ "value": "🐜"
+ },
+ {
+ "key": "spider",
+ "value": "🕷"
+ },
+ {
+ "key": "scorpion",
+ "value": "🦂"
+ },
+ {
+ "key": "crab",
+ "value": "🦀"
+ },
+ {
+ "key": "snake",
+ "value": "🐍"
+ },
+ {
+ "key": "turtle",
+ "value": "🐢"
+ },
+ {
+ "key": "tropical_fish",
+ "value": "🐠"
+ },
+ {
+ "key": "fish",
+ "value": "🐟"
+ },
+ {
+ "key": "blow_fish",
+ "value": "🐡"
+ },
+ {
+ "key": "dolphin",
+ "value": "🐬"
+ },
+ {
+ "key": "spouting_whale",
+ "value": "🐳"
+ },
+ {
+ "key": "whale",
+ "value": "🐋"
+ },
+ {
+ "key": "crocodile",
+ "value": "🐊"
+ },
+ {
+ "key": "leopard",
+ "value": "🐆"
+ },
+ {
+ "key": "tiger",
+ "value": "🐅"
+ },
+ {
+ "key": "water_buffalo",
+ "value": "🐃"
+ },
+ {
+ "key": "ox",
+ "value": "🐂"
+ },
+ {
+ "key": "cow",
+ "value": "🐄"
+ },
+ {
+ "key": "dromedary_camel",
+ "value": "🐪"
+ },
+ {
+ "key": "bactrian_camel",
+ "value": "🐫"
+ },
+ {
+ "key": "elephant",
+ "value": "🐘"
+ },
+ {
+ "key": "goat",
+ "value": "🐐"
+ },
+ {
+ "key": "ram",
+ "value": "🐏"
+ },
+ {
+ "key": "sheep",
+ "value": "🐑"
+ },
+ {
+ "key": "horse",
+ "value": "🐎"
+ },
+ {
+ "key": "pig",
+ "value": "🐖"
+ },
+ {
+ "key": "rat",
+ "value": "🐀"
+ },
+ {
+ "key": "mouse",
+ "value": "🐁"
+ },
+ {
+ "key": "rooster",
+ "value": "🐓"
+ },
+ {
+ "key": "turkey",
+ "value": "🦃"
+ },
+ {
+ "key": "dove",
+ "value": "🕊"
+ },
+ {
+ "key": "dog",
+ "value": "🐕"
+ },
+ {
+ "key": "poodle",
+ "value": "🐩"
+ },
+ {
+ "key": "cat",
+ "value": "🐈"
+ },
+ {
+ "key": "rabbit",
+ "value": "🐇"
+ },
+ {
+ "key": "chipmunk",
+ "value": "🐿"
+ },
+ {
+ "key": "paw_prints",
+ "value": "🐾"
+ },
+ {
+ "key": "dragon",
+ "value": "🐉"
+ },
+ {
+ "key": "dragon_face",
+ "value": "🐲"
+ },
+ {
+ "key": "cactus",
+ "value": "🌵"
+ },
+ {
+ "key": "christmas_tree",
+ "value": "🎄"
+ },
+ {
+ "key": "ever_green_tree",
+ "value": "🌲"
+ },
+ {
+ "key": "deciduous_tree",
+ "value": "🌳"
+ },
+ {
+ "key": "palm_tree",
+ "value": "🌴"
+ },
+ {
+ "key": "seedling",
+ "value": "🌱"
+ },
+ {
+ "key": "herb",
+ "value": "🌿"
+ },
+ {
+ "key": "shamrock",
+ "value": "☘"
+ },
+ {
+ "key": "four_leaf",
+ "value": "🍀"
+ },
+ {
+ "key": "pine_decoration",
+ "value": "🎍"
+ },
+ {
+ "key": "tanabata_tree",
+ "value": "🎋"
+ },
+ {
+ "key": "leaf_wind",
+ "value": "🍃"
+ },
+ {
+ "key": "fallen_leaf",
+ "value": "🍂"
+ },
+ {
+ "key": "maple_leaf",
+ "value": "🍁"
+ },
+ {
+ "key": "ear_of_rice",
+ "value": "🌾"
+ },
+ {
+ "key": "hibiscus",
+ "value": "🌺"
+ },
+ {
+ "key": "sunflower",
+ "value": "🌻"
+ },
+ {
+ "key": "rose",
+ "value": "🌹"
+ },
+ {
+ "key": "tulip",
+ "value": "🌷"
+ },
+ {
+ "key": "blossom",
+ "value": "🌼"
+ },
+ {
+ "key": "cherry_blossom",
+ "value": "🌸"
+ },
+ {
+ "key": "bouquet",
+ "value": "💐"
+ },
+ {
+ "key": "mushroom",
+ "value": "🍄"
+ },
+ {
+ "key": "chestnut",
+ "value": "🌰"
+ },
+ {
+ "key": "jack_o_lantern",
+ "value": "🎃"
+ },
+ {
+ "key": "spiral_shell",
+ "value": "🐚"
+ },
+ {
+ "key": "spider_web",
+ "value": "🕸"
+ },
+ {
+ "key": "earth_america",
+ "value": "🌎"
+ },
+ {
+ "key": "earth_europe",
+ "value": "🌍"
+ },
+ {
+ "key": "earth_australia",
+ "value": "🌏"
+ },
+ {
+ "key": "full_moon",
+ "value": "🌕"
+ },
+ {
+ "key": "waning_gibbous_moon",
+ "value": "🌖"
+ },
+ {
+ "key": "last_quarter_moon",
+ "value": "🌗"
+ },
+ {
+ "key": "waning_crescent_moon",
+ "value": "🌘"
+ },
+ {
+ "key": "new_moon_symbol",
+ "value": "🌑"
+ },
+ {
+ "key": "waxing_crescent_moon",
+ "value": "🌒"
+ },
+ {
+ "key": "first_quarter_moon",
+ "value": "🌓"
+ },
+ {
+ "key": "waxing_gibbous_moon",
+ "value": "🌔"
+ },
+ {
+ "key": "new_moon_with_face",
+ "value": "🌚"
+ },
+ {
+ "key": "full_moon_face",
+ "value": "🌝"
+ },
+ {
+ "key": "first_quarter_moon_face",
+ "value": "🌛"
+ },
+ {
+ "key": "last_quarter_moon_face",
+ "value": "🌜"
+ },
+ {
+ "key": "sun_face",
+ "value": "🌞"
+ },
+ {
+ "key": "crescent_moon",
+ "value": "🌙"
+ },
+ {
+ "key": "white_star",
+ "value": "⭐"
+ },
+ {
+ "key": "glowing_star",
+ "value": "🌟"
+ },
+ {
+ "key": "dizzy_symbol",
+ "value": "💫"
+ },
+ {
+ "key": "sparkles",
+ "value": "✨"
+ },
+ {
+ "key": "comet",
+ "value": "☄"
+ },
+ {
+ "key": "black_sun_with_rays",
+ "value": "☀"
+ },
+ {
+ "key": "white_sun_small_cloud",
+ "value": "🌤"
+ },
+ {
+ "key": "sun_behind_cloud",
+ "value": "⛅"
+ },
+ {
+ "key": "white_sun_behind_cloud",
+ "value": "🌥"
+ },
+ {
+ "key": "white_sun_behind_cloud_rain",
+ "value": "🌦"
+ },
+ {
+ "key": "cloud",
+ "value": "☁"
+ },
+ {
+ "key": "cloud_with_rain",
+ "value": "🌧"
+ },
+ {
+ "key": "thunder_cloud_rain",
+ "value": "⛈"
+ },
+ {
+ "key": "cloud_lightening",
+ "value": "🌩"
+ },
+ {
+ "key": "high_voltage",
+ "value": "⚡"
+ },
+ {
+ "key": "fire",
+ "value": "🔥"
+ },
+ {
+ "key": "collision",
+ "value": "💥"
+ },
+ {
+ "key": "snow_flake",
+ "value": "❄"
+ },
+ {
+ "key": "cloud_with_snow",
+ "value": "🌨"
+ },
+ {
+ "key": "snowman",
+ "value": "☃"
+ },
+ {
+ "key": "snowman_without_snow",
+ "value": "⛄"
+ },
+ {
+ "key": "wind_blowing_face",
+ "value": "🌬"
+ },
+ {
+ "key": "dash_symbol",
+ "value": "💨"
+ },
+ {
+ "key": "cloud_with_tornado",
+ "value": "🌪"
+ },
+ {
+ "key": "fog",
+ "value": "🌫"
+ },
+ {
+ "key": "umbrella",
+ "value": "☂"
+ },
+ {
+ "key": "umbrella_with_rain_drops",
+ "value": "☔"
+ },
+ {
+ "key": "droplet",
+ "value": "💧"
+ },
+ {
+ "key": "splashing_sweat",
+ "value": "💦"
+ },
+ {
+ "key": "water_wave",
+ "value": "🌊"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/Objects.json b/nikola/plugins/shortcode/emoji/data/Objects.json
new file mode 100644
index 0000000..5f13056
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Objects.json
@@ -0,0 +1,718 @@
+{
+ "objects": {
+ "object": [
+ {
+ "key": "watch",
+ "value": "⌚"
+ },
+ {
+ "key": "mobile_phone",
+ "value": "📱"
+ },
+ {
+ "key": "mobile_phone_with_right_arrow",
+ "value": "📲"
+ },
+ {
+ "key": "personal_computer",
+ "value": "💻"
+ },
+ {
+ "key": "keyboard",
+ "value": "⌨"
+ },
+ {
+ "key": "desktop_computer",
+ "value": "🖥"
+ },
+ {
+ "key": "printer",
+ "value": "🖨"
+ },
+ {
+ "key": "three_button_mouse",
+ "value": "🖱"
+ },
+ {
+ "key": "track_ball",
+ "value": "🖲"
+ },
+ {
+ "key": "joystick",
+ "value": "🕹"
+ },
+ {
+ "key": "compression",
+ "value": "🗜"
+ },
+ {
+ "key": "mini_disc",
+ "value": "💽"
+ },
+ {
+ "key": "floppy_disk",
+ "value": "💾"
+ },
+ {
+ "key": "optical_disc",
+ "value": "💿"
+ },
+ {
+ "key": "dvd",
+ "value": "📀"
+ },
+ {
+ "key": "video_cassette",
+ "value": "📼"
+ },
+ {
+ "key": "camera",
+ "value": "📷"
+ },
+ {
+ "key": "camera_with_flash",
+ "value": "📸"
+ },
+ {
+ "key": "video_camera",
+ "value": "📹"
+ },
+ {
+ "key": "movie_camera",
+ "value": "🎥"
+ },
+ {
+ "key": "film_projector",
+ "value": "📽"
+ },
+ {
+ "key": "film_frames",
+ "value": "🎞"
+ },
+ {
+ "key": "telephone_receiver",
+ "value": "📞"
+ },
+ {
+ "key": "black_telephone",
+ "value": "☎"
+ },
+ {
+ "key": "pager",
+ "value": "📟"
+ },
+ {
+ "key": "fax_machine",
+ "value": "📠"
+ },
+ {
+ "key": "television",
+ "value": "📺"
+ },
+ {
+ "key": "radio",
+ "value": "📻"
+ },
+ {
+ "key": "studio_microphone",
+ "value": "🎙"
+ },
+ {
+ "key": "level_slider",
+ "value": "🎚"
+ },
+ {
+ "key": "control_knobs",
+ "value": "🎛"
+ },
+ {
+ "key": "stop_watch",
+ "value": "⏱"
+ },
+ {
+ "key": "timer_clock",
+ "value": "⏲"
+ },
+ {
+ "key": "alarm_clock",
+ "value": "⏰"
+ },
+ {
+ "key": "mantel_piece_clock",
+ "value": "🕰"
+ },
+ {
+ "key": "hour_glass_with_flowing_stand",
+ "value": "⏳"
+ },
+ {
+ "key": "hour_glass",
+ "value": "⌛"
+ },
+ {
+ "key": "satellite_antenna",
+ "value": "📡"
+ },
+ {
+ "key": "battery",
+ "value": "🔋"
+ },
+ {
+ "key": "electric_plug",
+ "value": "🔌"
+ },
+ {
+ "key": "electric_light_bulb",
+ "value": "💡"
+ },
+ {
+ "key": "electric_torch",
+ "value": "🔦"
+ },
+ {
+ "key": "candle",
+ "value": "🕯"
+ },
+ {
+ "key": "waste_basket",
+ "value": "🗑"
+ },
+ {
+ "key": "oil_drum",
+ "value": "🛢"
+ },
+ {
+ "key": "money_with_wings",
+ "value": "💸"
+ },
+ {
+ "key": "bank_note_with_dollar_sign",
+ "value": "💵"
+ },
+ {
+ "key": "bank_note_with_yen_sign",
+ "value": "💴"
+ },
+ {
+ "key": "bank_note_with_euro_sign",
+ "value": "💶"
+ },
+ {
+ "key": "bank_note_with_pounds_sign",
+ "value": "💷"
+ },
+ {
+ "key": "money_bag",
+ "value": "💰"
+ },
+ {
+ "key": "credit_card",
+ "value": "💳"
+ },
+ {
+ "key": "gem_stone",
+ "value": "💎"
+ },
+ {
+ "key": "scales",
+ "value": "⚖"
+ },
+ {
+ "key": "wrench",
+ "value": "🔧"
+ },
+ {
+ "key": "hammer",
+ "value": "🔨"
+ },
+ {
+ "key": "hammer_and_pick",
+ "value": "⚒"
+ },
+ {
+ "key": "hammer_and_wrench",
+ "value": "🛠"
+ },
+ {
+ "key": "pick",
+ "value": "⛏"
+ },
+ {
+ "key": "nut_and_bolt",
+ "value": "🔩"
+ },
+ {
+ "key": "gear",
+ "value": "⚙"
+ },
+ {
+ "key": "chains",
+ "value": "⛓"
+ },
+ {
+ "key": "pistol",
+ "value": "🔫"
+ },
+ {
+ "key": "bomb",
+ "value": "💣"
+ },
+ {
+ "key": "hocho",
+ "value": "🔪"
+ },
+ {
+ "key": "dagger_knife",
+ "value": "🗡"
+ },
+ {
+ "key": "crossed_words",
+ "value": "⚔"
+ },
+ {
+ "key": "shield",
+ "value": "🛡"
+ },
+ {
+ "key": "smoking_symbol",
+ "value": "🚬"
+ },
+ {
+ "key": "skull_and_cross_bones",
+ "value": "☠"
+ },
+ {
+ "key": "coffin",
+ "value": "⚰"
+ },
+ {
+ "key": "funeral_urn",
+ "value": "⚱"
+ },
+ {
+ "key": "amphora",
+ "value": "🏺"
+ },
+ {
+ "key": "crystal_ball",
+ "value": "🔮"
+ },
+ {
+ "key": "prayer_beads",
+ "value": "📿"
+ },
+ {
+ "key": "barber_pole",
+ "value": "💈"
+ },
+ {
+ "key": "alembic",
+ "value": "⚗"
+ },
+ {
+ "key": "telescope",
+ "value": "🔭"
+ },
+ {
+ "key": "microscope",
+ "value": "🔬"
+ },
+ {
+ "key": "hole",
+ "value": "🕳"
+ },
+ {
+ "key": "pill",
+ "value": "💊"
+ },
+ {
+ "key": "syringe",
+ "value": "💉"
+ },
+ {
+ "key": "thermometer",
+ "value": "🌡"
+ },
+ {
+ "key": "label",
+ "value": "🏷"
+ },
+ {
+ "key": "bookmark",
+ "value": "🔖"
+ },
+ {
+ "key": "toilet",
+ "value": "🚽"
+ },
+ {
+ "key": "shower",
+ "value": "🚿"
+ },
+ {
+ "key": "bath_tub",
+ "value": "🛁"
+ },
+ {
+ "key": "key",
+ "value": "🔑"
+ },
+ {
+ "key": "old_key",
+ "value": "🗝"
+ },
+ {
+ "key": "couch_and_lamp",
+ "value": "🛋"
+ },
+ {
+ "key": "sleeping_accommodation",
+ "value": "🛌"
+ },
+ {
+ "key": "bed",
+ "value": "🛏"
+ },
+ {
+ "key": "door",
+ "value": "🚪"
+ },
+ {
+ "key": "bell_hop_bell",
+ "value": "🛎"
+ },
+ {
+ "key": "frame_with_picture",
+ "value": "🖼"
+ },
+ {
+ "key": "world_map",
+ "value": "🗺"
+ },
+ {
+ "key": "umbrella_on_ground",
+ "value": "⛱"
+ },
+ {
+ "key": "moyai",
+ "value": "🗿"
+ },
+ {
+ "key": "shopping_bags",
+ "value": "🛍"
+ },
+ {
+ "key": "balloon",
+ "value": "🎈"
+ },
+ {
+ "key": "carp_streamer",
+ "value": "🎏"
+ },
+ {
+ "key": "ribbon",
+ "value": "🎀"
+ },
+ {
+ "key": "wrapped_present",
+ "value": "🎁"
+ },
+ {
+ "key": "confetti_ball",
+ "value": "🎊"
+ },
+ {
+ "key": "party_popper",
+ "value": "🎉"
+ },
+ {
+ "key": "japanese_dolls",
+ "value": "🎎"
+ },
+ {
+ "key": "wind_chime",
+ "value": "🎐"
+ },
+ {
+ "key": "crossed_flags",
+ "value": "🎌"
+ },
+ {
+ "key": "izakaya_lantern",
+ "value": "🏮"
+ },
+ {
+ "key": "envelope",
+ "value": "✉"
+ },
+ {
+ "key": "envelope_with_down_arrow",
+ "value": "📩"
+ },
+ {
+ "key": "incoming_envelope",
+ "value": "📨"
+ },
+ {
+ "key": "email_symbol",
+ "value": "📧"
+ },
+ {
+ "key": "love_letter",
+ "value": "💌"
+ },
+ {
+ "key": "post_box",
+ "value": "📮"
+ },
+ {
+ "key": "closed_mail_box_with_lowered_flag",
+ "value": "📪"
+ },
+ {
+ "key": "closed_mail_box_with_raised_flag",
+ "value": "📫"
+ },
+ {
+ "key": "open_mail_box_with_raised_flag",
+ "value": "📬"
+ },
+ {
+ "key": "open_mail_box_with_lowered_flag",
+ "value": "📭"
+ },
+ {
+ "key": "package",
+ "value": "📦"
+ },
+ {
+ "key": "postal_horn",
+ "value": "📯"
+ },
+ {
+ "key": "inbox_tray",
+ "value": "📥"
+ },
+ {
+ "key": "outbox_tray",
+ "value": "📤"
+ },
+ {
+ "key": "scroll",
+ "value": "📜"
+ },
+ {
+ "key": "page_with_curl",
+ "value": "📃"
+ },
+ {
+ "key": "bookmark_tabs",
+ "value": "📑"
+ },
+ {
+ "key": "bar_chart",
+ "value": "📊"
+ },
+ {
+ "key": "chart_with_upwards_trend",
+ "value": "📈"
+ },
+ {
+ "key": "chart_with_downwards_trend",
+ "value": "📉"
+ },
+ {
+ "key": "page_facing_up",
+ "value": "📄"
+ },
+ {
+ "key": "calender",
+ "value": "📅"
+ },
+ {
+ "key": "tear_off_calendar",
+ "value": "📆"
+ },
+ {
+ "key": "spiral_calendar_pad",
+ "value": "🗓"
+ },
+ {
+ "key": "card_index",
+ "value": "📇"
+ },
+ {
+ "key": "card_file_box",
+ "value": "🗃"
+ },
+ {
+ "key": "ballot_box_with_ballot",
+ "value": "🗳"
+ },
+ {
+ "key": "file_cabinet",
+ "value": "🗄"
+ },
+ {
+ "key": "clip_board",
+ "value": "📋"
+ },
+ {
+ "key": "spiral_notepad",
+ "value": "🗒"
+ },
+ {
+ "key": "file_folder",
+ "value": "📁"
+ },
+ {
+ "key": "open_file_folder",
+ "value": "📂"
+ },
+ {
+ "key": "card_index_dividers",
+ "value": "🗂"
+ },
+ {
+ "key": "rolled_up_newspaper",
+ "value": "🗞"
+ },
+ {
+ "key": "newspaper",
+ "value": "📰"
+ },
+ {
+ "key": "notebook",
+ "value": "📓"
+ },
+ {
+ "key": "closed_book",
+ "value": "📕"
+ },
+ {
+ "key": "green_book",
+ "value": "📗"
+ },
+ {
+ "key": "blue_book",
+ "value": "📘"
+ },
+ {
+ "key": "orange_book",
+ "value": "📙"
+ },
+ {
+ "key": "notebook_with_decorative_cover",
+ "value": "📔"
+ },
+ {
+ "key": "ledger",
+ "value": "📒"
+ },
+ {
+ "key": "books",
+ "value": "📚"
+ },
+ {
+ "key": "open_book",
+ "value": "📖"
+ },
+ {
+ "key": "link_symbol",
+ "value": "🔗"
+ },
+ {
+ "key": "paper_clip",
+ "value": "📎"
+ },
+ {
+ "key": "linked_paper_clips",
+ "value": "🖇"
+ },
+ {
+ "key": "black_scissors",
+ "value": "✂"
+ },
+ {
+ "key": "triangular_ruler",
+ "value": "📐"
+ },
+ {
+ "key": "straight_ruler",
+ "value": "📏"
+ },
+ {
+ "key": "pushpin",
+ "value": "📌"
+ },
+ {
+ "key": "round_pushpin",
+ "value": "📍"
+ },
+ {
+ "key": "triangular_flag_post",
+ "value": "🚩"
+ },
+ {
+ "key": "waving_white_flag",
+ "value": "🏳"
+ },
+ {
+ "key": "waving_black_flag",
+ "value": "🏴"
+ },
+ {
+ "key": "closed_lock_with_key",
+ "value": "🔐"
+ },
+ {
+ "key": "lock",
+ "value": "🔒"
+ },
+ {
+ "key": "open_lock",
+ "value": "🔓"
+ },
+ {
+ "key": "lock_with_ink_pen",
+ "value": "🔏"
+ },
+ {
+ "key": "lower_left_ball_point_pen",
+ "value": "🖊"
+ },
+ {
+ "key": "lower_left_fountain_pen",
+ "value": "🖋"
+ },
+ {
+ "key": "black_nib",
+ "value": "✒"
+ },
+ {
+ "key": "memo",
+ "value": "📝"
+ },
+ {
+ "key": "pencil",
+ "value": "✏"
+ },
+ {
+ "key": "lower_left_crayon",
+ "value": "🖍"
+ },
+ {
+ "key": "lower_left_paint_brush",
+ "value": "🖌"
+ },
+ {
+ "key": "left_pointing_magnifying_glass",
+ "value": "🔍"
+ },
+ {
+ "key": "right_pointing_magnifying_glass",
+ "value": "🔎"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/People.json b/nikola/plugins/shortcode/emoji/data/People.json
new file mode 100644
index 0000000..a5fb88f
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/People.json
@@ -0,0 +1,1922 @@
+{
+ "peoples": {
+ "people": [
+ {
+ "key": "grinning_face",
+ "value": "😀"
+ },
+ {
+ "key": "grimacing_face",
+ "value": "😬"
+ },
+ {
+ "key": "grimacing_face_with_smile_eyes",
+ "value": "😁"
+ },
+ {
+ "key": "face_with_tear_of_joy",
+ "value": "😂"
+ },
+ {
+ "key": "smiling_face_with_open_mouth",
+ "value": "😃"
+ },
+ {
+ "key": "smiling_face_with_open_mouth_eyes",
+ "value": "😄"
+ },
+ {
+ "key": "smiling_face_with_open_mouth_cold_sweat",
+ "value": "😅"
+ },
+ {
+ "key": "smiling_face_with_open_mouth_hand_tight",
+ "value": "😆"
+ },
+ {
+ "key": "smiling_face_with_halo",
+ "value": "😇"
+ },
+ {
+ "key": "winking_face",
+ "value": "😉"
+ },
+ {
+ "key": "black_smiling_face",
+ "value": "😊"
+ },
+ {
+ "key": "slightly_smiling_face",
+ "value": "🙂"
+ },
+ {
+ "key": "upside_down_face",
+ "value": "🙃"
+ },
+ {
+ "key": "white_smiling_face",
+ "value": "☺"
+ },
+ {
+ "key": "face_savouring_delicious_food",
+ "value": "😋"
+ },
+ {
+ "key": "relieved_face",
+ "value": "😌"
+ },
+ {
+ "key": "smiling_face_heart_eyes",
+ "value": "😍"
+ },
+ {
+ "key": "face_throwing_kiss",
+ "value": "😘"
+ },
+ {
+ "key": "kissing_face",
+ "value": "😗"
+ },
+ {
+ "key": "kissing_face_with_smile_eyes",
+ "value": "😙"
+ },
+ {
+ "key": "kissing_face_with_closed_eyes",
+ "value": "😚"
+ },
+ {
+ "key": "face_with_tongue_wink_eye",
+ "value": "😜"
+ },
+ {
+ "key": "face_with_tongue_closed_eye",
+ "value": "😝"
+ },
+ {
+ "key": "face_with_stuck_out_tongue",
+ "value": "😛"
+ },
+ {
+ "key": "money_mouth_face",
+ "value": "🤑"
+ },
+ {
+ "key": "nerd_face",
+ "value": "🤓"
+ },
+ {
+ "key": "smiling_face_with_sun_glass",
+ "value": "😎"
+ },
+ {
+ "key": "hugging_face",
+ "value": "🤗"
+ },
+ {
+ "key": "smirking_face",
+ "value": "😏"
+ },
+ {
+ "key": "face_without_mouth",
+ "value": "😶"
+ },
+ {
+ "key": "neutral_face",
+ "value": "😐"
+ },
+ {
+ "key": "expressionless_face",
+ "value": "😑"
+ },
+ {
+ "key": "unamused_face",
+ "value": "😒"
+ },
+ {
+ "key": "face_with_rolling_eyes",
+ "value": "🙄"
+ },
+ {
+ "key": "thinking_face",
+ "value": "🤔"
+ },
+ {
+ "key": "flushed_face",
+ "value": "😳"
+ },
+ {
+ "key": "disappointed_face",
+ "value": "😞"
+ },
+ {
+ "key": "worried_face",
+ "value": "😟"
+ },
+ {
+ "key": "angry_face",
+ "value": "😠"
+ },
+ {
+ "key": "pouting_face",
+ "value": "😡"
+ },
+ {
+ "key": "pensive_face",
+ "value": "😔"
+ },
+ {
+ "key": "confused_face",
+ "value": "😕"
+ },
+ {
+ "key": "slightly_frowning_face",
+ "value": "🙁"
+ },
+ {
+ "key": "white_frowning_face",
+ "value": "☹"
+ },
+ {
+ "key": "persevering_face",
+ "value": "😣"
+ },
+ {
+ "key": "confounded_face",
+ "value": "😖"
+ },
+ {
+ "key": "tired_face",
+ "value": "😫"
+ },
+ {
+ "key": "weary_face",
+ "value": "😩"
+ },
+ {
+ "key": "face_with_look_of_triumph",
+ "value": "😤"
+ },
+ {
+ "key": "face_with_open_mouth",
+ "value": "😮"
+ },
+ {
+ "key": "face_screaming_in_fear",
+ "value": "😱"
+ },
+ {
+ "key": "fearful_face",
+ "value": "😨"
+ },
+ {
+ "key": "face_with_open_mouth_cold_sweat",
+ "value": "😰"
+ },
+ {
+ "key": "hushed_face",
+ "value": "😯"
+ },
+ {
+ "key": "frowning_face_with_open_mouth",
+ "value": "😦"
+ },
+ {
+ "key": "anguished_face",
+ "value": "😧"
+ },
+ {
+ "key": "crying_face",
+ "value": "😢"
+ },
+ {
+ "key": "disappointed_but_relieved_face",
+ "value": "😥"
+ },
+ {
+ "key": "sleepy_face",
+ "value": "😪"
+ },
+ {
+ "key": "face_with_cold_sweat",
+ "value": "😓"
+ },
+ {
+ "key": "loudly_crying_face",
+ "value": "😭"
+ },
+ {
+ "key": "dizzy_face",
+ "value": "😵"
+ },
+ {
+ "key": "astonished_face",
+ "value": "😲"
+ },
+ {
+ "key": "zipper_mouth_face",
+ "value": "🤐"
+ },
+ {
+ "key": "face_with_medical_mask",
+ "value": "😷"
+ },
+ {
+ "key": "face_with_thermometer",
+ "value": "🤒"
+ },
+ {
+ "key": "face_with_head_bandage",
+ "value": "🤕"
+ },
+ {
+ "key": "sleeping_face",
+ "value": "😴"
+ },
+ {
+ "key": "sleeping_symbol",
+ "value": "💤"
+ },
+ {
+ "key": "pile_of_poo",
+ "value": "💩"
+ },
+ {
+ "key": "smiling_face_with_horns",
+ "value": "😈"
+ },
+ {
+ "key": "imp",
+ "value": "👿"
+ },
+ {
+ "key": "japanese_ogre",
+ "value": "👹"
+ },
+ {
+ "key": "japanese_goblin",
+ "value": "👺"
+ },
+ {
+ "key": "skull",
+ "value": "💀"
+ },
+ {
+ "key": "ghost",
+ "value": "👻"
+ },
+ {
+ "key": "extra_terrestrial_alien",
+ "value": "👽"
+ },
+ {
+ "key": "robot_face",
+ "value": "🤖"
+ },
+ {
+ "key": "smiling_cat_face_open_mouth",
+ "value": "😺"
+ },
+ {
+ "key": "grinning_cat_face_smile_eyes",
+ "value": "😸"
+ },
+ {
+ "key": "cat_face_tears_of_joy",
+ "value": "😹"
+ },
+ {
+ "key": "smiling_cat_face_heart_shaped_eyes",
+ "value": "😻"
+ },
+ {
+ "key": "cat_face_wry_smile",
+ "value": "😼"
+ },
+ {
+ "key": "kissing_cat_face_closed_eyes",
+ "value": "😽"
+ },
+ {
+ "key": "weary_cat_face",
+ "value": "🙀"
+ },
+ {
+ "key": "crying_cat_face",
+ "value": "😿"
+ },
+ {
+ "key": "pouting_cat_face",
+ "value": "😾"
+ },
+ {
+ "key": "person_both_hand_celebration",
+ "value": "🙌"
+ },
+ {
+ "key": "person_both_hand_celebration_type_1_2",
+ "value": "🙌🏻"
+ },
+ {
+ "key": "person_both_hand_celebration_type_3",
+ "value": "🙌🏼"
+ },
+ {
+ "key": "person_both_hand_celebration_type_4",
+ "value": "🙌🏽"
+ },
+ {
+ "key": "person_both_hand_celebration_type_5",
+ "value": "🙌🏾"
+ },
+ {
+ "key": "person_both_hand_celebration_type_6",
+ "value": "🙌🏿"
+ },
+ {
+ "key": "clapping_hand",
+ "value": "👏"
+ },
+ {
+ "key": "clapping_hand_type_1_2",
+ "value": "👏🏼"
+ },
+ {
+ "key": "clapping_hand_type_3",
+ "value": "👏🏼"
+ },
+ {
+ "key": "clapping_hand_type_4",
+ "value": "👏🏽"
+ },
+ {
+ "key": "clapping_hand_type_5",
+ "value": "👏🏾"
+ },
+ {
+ "key": "clapping_hand_type_6",
+ "value": "👏🏿"
+ },
+ {
+ "key": "waving_hands",
+ "value": "👋"
+ },
+ {
+ "key": "waving_hands_type_1_2",
+ "value": "👋🏻"
+ },
+ {
+ "key": "waving_hands_type_3",
+ "value": "👋🏼"
+ },
+ {
+ "key": "waving_hands_type_4",
+ "value": "👋🏽"
+ },
+ {
+ "key": "waving_hands_type_5",
+ "value": "👋🏾"
+ },
+ {
+ "key": "waving_hands_type_6",
+ "value": "👋🏿"
+ },
+ {
+ "key": "thumbs_up",
+ "value": "👍"
+ },
+ {
+ "key": "thumbs_up_type_1_2",
+ "value": "👍🏻"
+ },
+ {
+ "key": "thumbs_up_type_3",
+ "value": "👍🏼"
+ },
+ {
+ "key": "thumbs_up_type_4",
+ "value": "👍🏽"
+ },
+ {
+ "key": "thumbs_up_type_5",
+ "value": "👍🏾"
+ },
+ {
+ "key": "thumbs_up_type_6",
+ "value": "👍🏿"
+ },
+ {
+ "key": "thumbs_down",
+ "value": "👎"
+ },
+ {
+ "key": "thumbs_down_type_1_2",
+ "value": "👎🏻"
+ },
+ {
+ "key": "thumbs_down_type_3",
+ "value": "👎🏼"
+ },
+ {
+ "key": "thumbs_down_type_4",
+ "value": "👎🏽"
+ },
+ {
+ "key": "thumbs_down_type_5",
+ "value": "👎🏾"
+ },
+ {
+ "key": "thumbs_down_type_6",
+ "value": "👎🏿"
+ },
+ {
+ "key": "fist_hand",
+ "value": "👊"
+ },
+ {
+ "key": "fist_hand_type_1_2",
+ "value": "👊🏻"
+ },
+ {
+ "key": "fist_hand_type_3",
+ "value": "👊🏼"
+ },
+ {
+ "key": "fist_hand_type_4",
+ "value": "👊🏽"
+ },
+ {
+ "key": "fist_hand_type_5",
+ "value": "👊🏾"
+ },
+ {
+ "key": "fist_hand_type_6",
+ "value": "👊🏿"
+ },
+ {
+ "key": "raised_fist",
+ "value": "✊"
+ },
+ {
+ "key": "raised_fist_type_1_2",
+ "value": "✊🏻"
+ },
+ {
+ "key": "raised_fist_type_3",
+ "value": "✊🏼"
+ },
+ {
+ "key": "raised_fist_type_4",
+ "value": "✊🏽"
+ },
+ {
+ "key": "raised_fist_type_5",
+ "value": "✊🏾"
+ },
+ {
+ "key": "raised_fist_type_6",
+ "value": "✊🏿"
+ },
+ {
+ "key": "victory_hand",
+ "value": "✌"
+ },
+ {
+ "key": "victory_hand_type_1_2",
+ "value": "✌🏻"
+ },
+ {
+ "key": "victory_hand_type_3",
+ "value": "✌🏼"
+ },
+ {
+ "key": "victory_hand_type_4",
+ "value": "✌🏽"
+ },
+ {
+ "key": "victory_hand_type_5",
+ "value": "✌🏾"
+ },
+ {
+ "key": "victory_hand_type_6",
+ "value": "✌🏿"
+ },
+ {
+ "key": "ok_hand",
+ "value": "👌"
+ },
+ {
+ "key": "ok_hand_type_1_2",
+ "value": "👌🏻"
+ },
+ {
+ "key": "ok_hand_type_3",
+ "value": "👌🏼"
+ },
+ {
+ "key": "ok_hand_type_4",
+ "value": "👌🏽"
+ },
+ {
+ "key": "ok_hand_type_5",
+ "value": "👌🏾"
+ },
+ {
+ "key": "ok_hand_type_6",
+ "value": "👌🏿"
+ },
+ {
+ "key": "raised_hand",
+ "value": "✋"
+ },
+ {
+ "key": "raised_hand_type_1_2",
+ "value": "✋🏻"
+ },
+ {
+ "key": "raised_hand_type_3",
+ "value": "✋🏼"
+ },
+ {
+ "key": "raised_hand_type_4",
+ "value": "✋🏽"
+ },
+ {
+ "key": "raised_hand_type_5",
+ "value": "✋🏾"
+ },
+ {
+ "key": "raised_hand_type_6",
+ "value": "✋🏿"
+ },
+ {
+ "key": "open_hand",
+ "value": "👐"
+ },
+ {
+ "key": "open_hand_type_1_2",
+ "value": "👐🏻"
+ },
+ {
+ "key": "open_hand_type_3",
+ "value": "👐🏼"
+ },
+ {
+ "key": "open_hand_type_4",
+ "value": "👐🏽"
+ },
+ {
+ "key": "open_hand_type_5",
+ "value": "👐🏾"
+ },
+ {
+ "key": "open_hand_type_6",
+ "value": "👐🏿"
+ },
+ {
+ "key": "flexed_biceps",
+ "value": "💪"
+ },
+ {
+ "key": "flexed_biceps_type_1_2",
+ "value": "💪🏻"
+ },
+ {
+ "key": "flexed_biceps_type_3",
+ "value": "💪🏼"
+ },
+ {
+ "key": "flexed_biceps_type_4",
+ "value": "💪🏽"
+ },
+ {
+ "key": "flexed_biceps_type_5",
+ "value": "💪🏾"
+ },
+ {
+ "key": "flexed_biceps_type_6",
+ "value": "💪🏿"
+ },
+ {
+ "key": "folded_hands",
+ "value": "🙏"
+ },
+ {
+ "key": "folded_hands_type_1_2",
+ "value": "🙏🏻"
+ },
+ {
+ "key": "folded_hands_type_3",
+ "value": "🙏🏼"
+ },
+ {
+ "key": "folded_hands_type_4",
+ "value": "🙏🏽"
+ },
+ {
+ "key": "folded_hands_type_5",
+ "value": "🙏🏾"
+ },
+ {
+ "key": "folded_hands_type_6",
+ "value": "🙏🏿"
+ },
+ {
+ "key": "up_pointing_index",
+ "value": "☝"
+ },
+ {
+ "key": "up_pointing_index_type_1_2",
+ "value": "☝🏻"
+ },
+ {
+ "key": "up_pointing_index_type_3",
+ "value": "☝🏼"
+ },
+ {
+ "key": "up_pointing_index_type_4",
+ "value": "☝🏽"
+ },
+ {
+ "key": "up_pointing_index_type_5",
+ "value": "☝🏾"
+ },
+ {
+ "key": "up_pointing_index_type_6",
+ "value": "☝🏿"
+ },
+ {
+ "key": "up_pointing_backhand_index",
+ "value": "👆"
+ },
+ {
+ "key": "up_pointing_backhand_index_type_1_2",
+ "value": "👆🏻"
+ },
+ {
+ "key": "up_pointing_backhand_index_type_3",
+ "value": "👆🏼"
+ },
+ {
+ "key": "up_pointing_backhand_index_type_4",
+ "value": "👆🏽"
+ },
+ {
+ "key": "up_pointing_backhand_index_type_5",
+ "value": "👆🏾"
+ },
+ {
+ "key": "up_pointing_backhand_index_type_6",
+ "value": "👆🏿"
+ },
+ {
+ "key": "down_pointing_backhand_index",
+ "value": "👇"
+ },
+ {
+ "key": "down_pointing_backhand_index_type_1_2",
+ "value": "👇🏻"
+ },
+ {
+ "key": "down_pointing_backhand_index_type_3",
+ "value": "👇🏼"
+ },
+ {
+ "key": "down_pointing_backhand_index_type_4",
+ "value": "👇🏽"
+ },
+ {
+ "key": "down_pointing_backhand_index_type_5",
+ "value": "👇🏾"
+ },
+ {
+ "key": "down_pointing_backhand_index_type_6",
+ "value": "👇🏿"
+ },
+ {
+ "key": "left_pointing_backhand_index",
+ "value": "👈"
+ },
+ {
+ "key": "left_pointing_backhand_index_type_1_2",
+ "value": "👈🏻"
+ },
+ {
+ "key": "left_pointing_backhand_index_type_3",
+ "value": "👈🏼"
+ },
+ {
+ "key": "left_pointing_backhand_index_type_4",
+ "value": "👈🏽"
+ },
+ {
+ "key": "left_pointing_backhand_index_type_5",
+ "value": "👈🏾"
+ },
+ {
+ "key": "left_pointing_backhand_index_type_6",
+ "value": "👈🏿"
+ },
+ {
+ "key": "right_pointing_backhand_index",
+ "value": "👉"
+ },
+ {
+ "key": "right_pointing_backhand_index_type_1_2",
+ "value": "👉🏻"
+ },
+ {
+ "key": "right_pointing_backhand_index_type_3",
+ "value": "👉🏼"
+ },
+ {
+ "key": "right_pointing_backhand_index_type_4",
+ "value": "👉🏽"
+ },
+ {
+ "key": "right_pointing_backhand_index_type_5",
+ "value": "👉🏾"
+ },
+ {
+ "key": "right_pointing_backhand_index_type_6",
+ "value": "👉🏿"
+ },
+ {
+ "key": "reverse_middle_finger",
+ "value": "🖕"
+ },
+ {
+ "key": "reverse_middle_finger_type_1_2",
+ "value": "🖕🏻"
+ },
+ {
+ "key": "reverse_middle_finger_type_3",
+ "value": "🖕🏼"
+ },
+ {
+ "key": "reverse_middle_finger_type_4",
+ "value": "🖕🏽"
+ },
+ {
+ "key": "reverse_middle_finger_type_5",
+ "value": "🖕🏾"
+ },
+ {
+ "key": "reverse_middle_finger_type_6",
+ "value": "🖕🏿"
+ },
+ {
+ "key": "raised_hand_fingers_splayed",
+ "value": "🖐"
+ },
+ {
+ "key": "raised_hand_fingers_splayed_type_1_2",
+ "value": "🖐🏻"
+ },
+ {
+ "key": "raised_hand_fingers_splayed_type_3",
+ "value": "🖐🏼"
+ },
+ {
+ "key": "raised_hand_fingers_splayed_type_4",
+ "value": "🖐🏽"
+ },
+ {
+ "key": "raised_hand_fingers_splayed_type_5",
+ "value": "🖐🏾"
+ },
+ {
+ "key": "raised_hand_fingers_splayed_type_6",
+ "value": "🖐🏿"
+ },
+ {
+ "key": "sign_of_horn",
+ "value": "🤘"
+ },
+ {
+ "key": "sign_of_horn_type_1_2",
+ "value": "🤘🏻"
+ },
+ {
+ "key": "sign_of_horn_type_3",
+ "value": "🤘🏼"
+ },
+ {
+ "key": "sign_of_horn_type_4",
+ "value": "🤘🏽"
+ },
+ {
+ "key": "sign_of_horn_type_5",
+ "value": "🤘🏾"
+ },
+ {
+ "key": "sign_of_horn_type_6",
+ "value": "🤘🏿"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring",
+ "value": "🖖"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring_type_1_2",
+ "value": "🖖🏻"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring_type_3",
+ "value": "🖖🏼"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring_type_4",
+ "value": "🖖🏽"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring_type_5",
+ "value": "🖖🏾"
+ },
+ {
+ "key": "raised_hand_part_between_middle_ring_type_6",
+ "value": "🖖🏿"
+ },
+ {
+ "key": "writing_hand",
+ "value": "✍"
+ },
+ {
+ "key": "writing_hand_type_1_2",
+ "value": "✍🏻"
+ },
+ {
+ "key": "writing_hand_type_3",
+ "value": "✍🏼"
+ },
+ {
+ "key": "writing_hand_type_4",
+ "value": "✍🏽"
+ },
+ {
+ "key": "writing_hand_type_5",
+ "value": "✍🏾"
+ },
+ {
+ "key": "writing_hand_type_6",
+ "value": "✍🏿"
+ },
+ {
+ "key": "nail_polish",
+ "value": "💅"
+ },
+ {
+ "key": "nail_polish_type_1_2",
+ "value": "💅🏻"
+ },
+ {
+ "key": "nail_polish_type_3",
+ "value": "💅🏼"
+ },
+ {
+ "key": "nail_polish_type_4",
+ "value": "💅🏽"
+ },
+ {
+ "key": "nail_polish_type_5",
+ "value": "💅🏾"
+ },
+ {
+ "key": "nail_polish_type_6",
+ "value": "💅🏿"
+ },
+ {
+ "key": "mouth",
+ "value": "👄"
+ },
+ {
+ "key": "tongue",
+ "value": "👅"
+ },
+ {
+ "key": "ear",
+ "value": "👂"
+ },
+ {
+ "key": "ear_type_1_2",
+ "value": "👂🏻"
+ },
+ {
+ "key": "ear_type_3",
+ "value": "👂🏼"
+ },
+ {
+ "key": "ear_type_4",
+ "value": "👂🏽"
+ },
+ {
+ "key": "ear_type_5",
+ "value": "👂🏾"
+ },
+ {
+ "key": "ear_type_6",
+ "value": "👂🏿"
+ },
+ {
+ "key": "nose",
+ "value": "👃"
+ },
+ {
+ "key": "nose_type_1_2",
+ "value": "👃🏻"
+ },
+ {
+ "key": "nose_type_3",
+ "value": "👃🏼"
+ },
+ {
+ "key": "nose_type_4",
+ "value": "👃🏽"
+ },
+ {
+ "key": "nose_type_5",
+ "value": "👃🏾"
+ },
+ {
+ "key": "nose_type_6",
+ "value": "👃🏿"
+ },
+ {
+ "key": "eye",
+ "value": "👁"
+ },
+ {
+ "key": "eyes",
+ "value": "👀"
+ },
+ {
+ "key": "bust_in_silhouette",
+ "value": "👤"
+ },
+ {
+ "key": "busts_in_silhouette",
+ "value": "👥"
+ },
+ {
+ "key": "speaking_head_in_silhouette",
+ "value": "🗣"
+ },
+ {
+ "key": "baby",
+ "value": "👶"
+ },
+ {
+ "key": "baby_type_1_2",
+ "value": "👶🏻"
+ },
+ {
+ "key": "baby_type_3",
+ "value": "👶🏼"
+ },
+ {
+ "key": "baby_type_4",
+ "value": "👶🏽"
+ },
+ {
+ "key": "baby_type_5",
+ "value": "👶🏾"
+ },
+ {
+ "key": "baby_type_6",
+ "value": "👶🏿"
+ },
+ {
+ "key": "boy",
+ "value": "👦"
+ },
+ {
+ "key": "boy_type_1_2",
+ "value": "👦🏻"
+ },
+ {
+ "key": "boy_type_3",
+ "value": "👦🏼"
+ },
+ {
+ "key": "boy_type_4",
+ "value": "👦🏽"
+ },
+ {
+ "key": "boy_type_5",
+ "value": "👦🏾"
+ },
+ {
+ "key": "boy_type_6",
+ "value": "👦🏿"
+ },
+ {
+ "key": "girl",
+ "value": "👧"
+ },
+ {
+ "key": "girl_type_1_2",
+ "value": "👧🏻"
+ },
+ {
+ "key": "girl_type_3",
+ "value": "👧🏼"
+ },
+ {
+ "key": "girl_type_4",
+ "value": "👧🏽"
+ },
+ {
+ "key": "girl_type_5",
+ "value": "👧🏾"
+ },
+ {
+ "key": "girl_type_6",
+ "value": "👧🏿"
+ },
+ {
+ "key": "man",
+ "value": "👨"
+ },
+ {
+ "key": "man_type_1_2",
+ "value": "👨🏻"
+ },
+ {
+ "key": "man_type_3",
+ "value": "👨🏼"
+ },
+ {
+ "key": "man_type_4",
+ "value": "👨🏽"
+ },
+ {
+ "key": "man_type_5",
+ "value": "👨🏾"
+ },
+ {
+ "key": "man_type_6",
+ "value": "👨🏿"
+ },
+ {
+ "key": "women",
+ "value": "👩"
+ },
+ {
+ "key": "women_type_1_2",
+ "value": "👩🏻"
+ },
+ {
+ "key": "women_type_3",
+ "value": "👩🏼"
+ },
+ {
+ "key": "women_type_4",
+ "value": "👩🏽"
+ },
+ {
+ "key": "women_type_5",
+ "value": "👩🏾"
+ },
+ {
+ "key": "women_type_6",
+ "value": "👩🏿"
+ },
+ {
+ "key": "person_with_blond_hair",
+ "value": "👱"
+ },
+ {
+ "key": "person_with_blond_hair_type_1_2",
+ "value": "👱🏻"
+ },
+ {
+ "key": "person_with_blond_hair_type_3",
+ "value": "👱🏼"
+ },
+ {
+ "key": "person_with_blond_hair_type_4",
+ "value": "👱🏽"
+ },
+ {
+ "key": "person_with_blond_hair_type_5",
+ "value": "👱🏾"
+ },
+ {
+ "key": "person_with_blond_hair_type_6",
+ "value": "👱🏿"
+ },
+ {
+ "key": "older_man",
+ "value": "👴"
+ },
+ {
+ "key": "older_man_type_1_2",
+ "value": "👴🏻"
+ },
+ {
+ "key": "older_man_type_3",
+ "value": "👴🏼"
+ },
+ {
+ "key": "older_man_type_4",
+ "value": "👴🏽"
+ },
+ {
+ "key": "older_man_type_5",
+ "value": "👴🏾"
+ },
+ {
+ "key": "older_man_type_6",
+ "value": "👴🏿"
+ },
+ {
+ "key": "older_women",
+ "value": "👵"
+ },
+ {
+ "key": "older_women_type_1_2",
+ "value": "👵🏻"
+ },
+ {
+ "key": "older_women_type_3",
+ "value": "👵🏼"
+ },
+ {
+ "key": "older_women_type_4",
+ "value": "👵🏽"
+ },
+ {
+ "key": "older_women_type_5",
+ "value": "👵🏾"
+ },
+ {
+ "key": "older_women_type_6",
+ "value": "👵🏿"
+ },
+ {
+ "key": "man_with_gua_pi_mao",
+ "value": "👲"
+ },
+ {
+ "key": "man_with_gua_pi_mao_type_1_2",
+ "value": "👲🏼"
+ },
+ {
+ "key": "man_with_gua_pi_mao_type_3",
+ "value": "👲🏼"
+ },
+ {
+ "key": "man_with_gua_pi_mao_type_4",
+ "value": "👲🏽"
+ },
+ {
+ "key": "man_with_gua_pi_mao_type_5",
+ "value": "👲🏾"
+ },
+ {
+ "key": "man_with_gua_pi_mao_type_6",
+ "value": "👲🏿"
+ },
+ {
+ "key": "man_with_turban",
+ "value": "👳"
+ },
+ {
+ "key": "man_with_turban_type_1_2",
+ "value": "👳🏻"
+ },
+ {
+ "key": "man_with_turban_type_3",
+ "value": "👳🏼"
+ },
+ {
+ "key": "man_with_turban_type_4",
+ "value": "👳🏽"
+ },
+ {
+ "key": "man_with_turban_type_5",
+ "value": "👳🏾"
+ },
+ {
+ "key": "man_with_turban_type_6",
+ "value": "👳🏿"
+ },
+ {
+ "key": "police_officer",
+ "value": "👮"
+ },
+ {
+ "key": "police_officer_type_1_2",
+ "value": "👮🏻"
+ },
+ {
+ "key": "police_officer_type_3",
+ "value": "👮🏼"
+ },
+ {
+ "key": "police_officer_type_4",
+ "value": "👮🏽"
+ },
+ {
+ "key": "police_officer_type_5",
+ "value": "👮🏾"
+ },
+ {
+ "key": "police_officer_type_6",
+ "value": "👮🏿"
+ },
+ {
+ "key": "construction_worker",
+ "value": "👷"
+ },
+ {
+ "key": "construction_worker_type_1_2",
+ "value": "👷🏻"
+ },
+ {
+ "key": "construction_worker_type_3",
+ "value": "👷🏼"
+ },
+ {
+ "key": "construction_worker_type_4",
+ "value": "👷🏽"
+ },
+ {
+ "key": "construction_worker_type_5",
+ "value": "👷🏾"
+ },
+ {
+ "key": "construction_worker_type_6",
+ "value": "👷🏿"
+ },
+ {
+ "key": "guards_man",
+ "value": "💂"
+ },
+ {
+ "key": "guards_man_type_1_2",
+ "value": "💂🏻"
+ },
+ {
+ "key": "guards_man_type_3",
+ "value": "💂🏼"
+ },
+ {
+ "key": "guards_man_type_4",
+ "value": "💂🏽"
+ },
+ {
+ "key": "guards_man_type_5",
+ "value": "💂🏾"
+ },
+ {
+ "key": "guards_man_type_6",
+ "value": "💂🏿"
+ },
+ {
+ "key": "spy",
+ "value": "🕵"
+ },
+ {
+ "key": "father_christmas",
+ "value": "🎅"
+ },
+ {
+ "key": "father_christmas_type_1_2",
+ "value": "🎅🏻"
+ },
+ {
+ "key": "father_christmas_type_3",
+ "value": "🎅🏼"
+ },
+ {
+ "key": "father_christmas_type_4",
+ "value": "🎅🏽"
+ },
+ {
+ "key": "father_christmas_type_5",
+ "value": "🎅🏾"
+ },
+ {
+ "key": "father_christmas_type_6",
+ "value": "🎅🏿"
+ },
+ {
+ "key": "baby_angel",
+ "value": "👼"
+ },
+ {
+ "key": "baby_angel_type_1_2",
+ "value": "👼🏻"
+ },
+ {
+ "key": "baby_angel_type_3",
+ "value": "👼🏼"
+ },
+ {
+ "key": "baby_angel_type_4",
+ "value": "👼🏽"
+ },
+ {
+ "key": "baby_angel_type_5",
+ "value": "👼🏾"
+ },
+ {
+ "key": "baby_angel_type_6",
+ "value": "👼🏿"
+ },
+ {
+ "key": "princess",
+ "value": "👸"
+ },
+ {
+ "key": "princess_type_1_2",
+ "value": "👸🏻"
+ },
+ {
+ "key": "princess_type_3",
+ "value": "👸🏼"
+ },
+ {
+ "key": "princess_type_4",
+ "value": "👸🏽"
+ },
+ {
+ "key": "princess_type_5",
+ "value": "👸🏾"
+ },
+ {
+ "key": "princess_type_6",
+ "value": "👸🏿"
+ },
+ {
+ "key": "bride_with_veil",
+ "value": "👰"
+ },
+ {
+ "key": "bride_with_veil_type_1_2",
+ "value": "👰🏻"
+ },
+ {
+ "key": "bride_with_veil_type_3",
+ "value": "👰🏼"
+ },
+ {
+ "key": "bride_with_veil_type_4",
+ "value": "👰🏽"
+ },
+ {
+ "key": "bride_with_veil_type_5",
+ "value": "👰🏾"
+ },
+ {
+ "key": "bride_with_veil_type_6",
+ "value": "👰🏿"
+ },
+ {
+ "key": "pedestrian",
+ "value": "🚶"
+ },
+ {
+ "key": "pedestrian_type_1_2",
+ "value": "🚶🏻"
+ },
+ {
+ "key": "pedestrian_type_3",
+ "value": "🚶🏼"
+ },
+ {
+ "key": "pedestrian_type_4",
+ "value": "🚶🏽"
+ },
+ {
+ "key": "pedestrian_type_5",
+ "value": "🚶🏾"
+ },
+ {
+ "key": "pedestrian_type_6",
+ "value": "🚶🏿"
+ },
+ {
+ "key": "runner",
+ "value": "🏃"
+ },
+ {
+ "key": "runner_type_1_2",
+ "value": "🏃🏻"
+ },
+ {
+ "key": "runner_type_3",
+ "value": "🏃🏼"
+ },
+ {
+ "key": "runner_type_4",
+ "value": "🏃🏽"
+ },
+ {
+ "key": "runner_type_5",
+ "value": "🏃🏾"
+ },
+ {
+ "key": "runner_type_6",
+ "value": "🏃🏿"
+ },
+ {
+ "key": "dancer",
+ "value": "💃"
+ },
+ {
+ "key": "dancer_type_1_2",
+ "value": "💃🏻"
+ },
+ {
+ "key": "dancer_type_3",
+ "value": "💃🏼"
+ },
+ {
+ "key": "dancer_type_4",
+ "value": "💃🏽"
+ },
+ {
+ "key": "dancer_type_5",
+ "value": "💃🏾"
+ },
+ {
+ "key": "dancer_type_6",
+ "value": "💃🏿"
+ },
+ {
+ "key": "women_with_bunny_years",
+ "value": "👯"
+ },
+ {
+ "key": "man_women_holding_hands",
+ "value": "👫"
+ },
+ {
+ "key": "two_man_holding_hands",
+ "value": "👬"
+ },
+ {
+ "key": "two_women_holding_hands",
+ "value": "👭"
+ },
+ {
+ "key": "person_bowing_deeply",
+ "value": "🙇"
+ },
+ {
+ "key": "person_bowing_deeply_type_1_2",
+ "value": "🙇🏻"
+ },
+ {
+ "key": "person_bowing_deeply_type_3",
+ "value": "🙇🏼"
+ },
+ {
+ "key": "person_bowing_deeply_type_4",
+ "value": "🙇🏽"
+ },
+ {
+ "key": "person_bowing_deeply_type_5",
+ "value": "🙇🏾"
+ },
+ {
+ "key": "person_bowing_deeply_type_6",
+ "value": "🙇🏿"
+ },
+ {
+ "key": "information_desk_person",
+ "value": "💁"
+ },
+ {
+ "key": "information_desk_person_type_1_2",
+ "value": "💁🏻"
+ },
+ {
+ "key": "information_desk_person_type_3",
+ "value": "💁🏼"
+ },
+ {
+ "key": "information_desk_person_type_4",
+ "value": "💁🏽"
+ },
+ {
+ "key": "information_desk_person_type_5",
+ "value": "💁🏾"
+ },
+ {
+ "key": "information_desk_person_type_6",
+ "value": "💁🏿"
+ },
+ {
+ "key": "face_with_no_good_gesture",
+ "value": "🙅"
+ },
+ {
+ "key": "face_with_no_good_gesture_type_1_2",
+ "value": "🙅🏻"
+ },
+ {
+ "key": "face_with_no_good_gesture_type_3",
+ "value": "🙅🏼"
+ },
+ {
+ "key": "face_with_no_good_gesture_type_4",
+ "value": "🙅🏽"
+ },
+ {
+ "key": "face_with_no_good_gesture_type_5",
+ "value": "🙅🏾"
+ },
+ {
+ "key": "face_with_no_good_gesture_type_6",
+ "value": "🙅🏿"
+ },
+ {
+ "key": "face_with_ok_gesture",
+ "value": "🙆"
+ },
+ {
+ "key": "face_with_ok_gesture_type_1_2",
+ "value": "🙆🏻"
+ },
+ {
+ "key": "face_with_ok_gesture_type_3",
+ "value": "🙆🏼"
+ },
+ {
+ "key": "face_with_ok_gesture_type_4",
+ "value": "🙆🏽"
+ },
+ {
+ "key": "face_with_ok_gesture_type_5",
+ "value": "🙆🏾"
+ },
+ {
+ "key": "face_with_ok_gesture_type_6",
+ "value": "🙆🏿"
+ },
+ {
+ "key": "happy_person_raise_one_hand",
+ "value": "🙋"
+ },
+ {
+ "key": "happy_person_raise_one_hand_type_1_2",
+ "value": "🙋🏻"
+ },
+ {
+ "key": "happy_person_raise_one_hand_type_3",
+ "value": "🙋🏼"
+ },
+ {
+ "key": "happy_person_raise_one_hand_type_4",
+ "value": "🙋🏽"
+ },
+ {
+ "key": "happy_person_raise_one_hand_type_5",
+ "value": "🙋🏾"
+ },
+ {
+ "key": "happy_person_raise_one_hand_type_6",
+ "value": "🙋🏿"
+ },
+ {
+ "key": "person_with_pouting_face",
+ "value": "🙎"
+ },
+ {
+ "key": "person_with_pouting_face_type_1_2",
+ "value": "🙎🏻"
+ },
+ {
+ "key": "person_with_pouting_face_type_3",
+ "value": "🙎🏼"
+ },
+ {
+ "key": "person_with_pouting_face_type_4",
+ "value": "🙎🏽"
+ },
+ {
+ "key": "person_with_pouting_face_type_5",
+ "value": "🙎🏾"
+ },
+ {
+ "key": "person_with_pouting_face_type_6",
+ "value": "🙎🏿"
+ },
+ {
+ "key": "person_frowning",
+ "value": "🙍"
+ },
+ {
+ "key": "person_frowning_type_1_2",
+ "value": "🙍🏻"
+ },
+ {
+ "key": "person_frowning_type_3",
+ "value": "🙍🏼"
+ },
+ {
+ "key": "person_frowning_type_4",
+ "value": "🙍🏽"
+ },
+ {
+ "key": "person_frowning_type_5",
+ "value": "🙍🏾"
+ },
+ {
+ "key": "person_frowning_type_6",
+ "value": "🙍🏿"
+ },
+ {
+ "key": "haircut",
+ "value": "💇"
+ },
+ {
+ "key": "haircut_type_1_2",
+ "value": "💇🏻"
+ },
+ {
+ "key": "haircut_type_3",
+ "value": "💇🏼"
+ },
+ {
+ "key": "haircut_type_4",
+ "value": "💇🏽"
+ },
+ {
+ "key": "haircut_type_5",
+ "value": "💇🏾"
+ },
+ {
+ "key": "haircut_type_6",
+ "value": "💇🏿"
+ },
+ {
+ "key": "face_massage",
+ "value": "💆"
+ },
+ {
+ "key": "face_massage_type_1_2",
+ "value": "💆🏻"
+ },
+ {
+ "key": "face_massage_type_3",
+ "value": "💆🏻"
+ },
+ {
+ "key": "face_massage_type_4",
+ "value": "💆🏽"
+ },
+ {
+ "key": "face_massage_type_5",
+ "value": "💆🏾"
+ },
+ {
+ "key": "face_massage_type_6",
+ "value": "💆🏿"
+ },
+ {
+ "key": "couple_with_heart",
+ "value": "💑"
+ },
+ {
+ "key": "couple_with_heart_woman",
+ "value": "👩‍❤️‍👩"
+ },
+ {
+ "key": "couple_with_heart_man",
+ "value": "👨‍❤️‍👨"
+ },
+ {
+ "key": "kiss",
+ "value": "💏"
+ },
+ {
+ "key": "kiss_woman",
+ "value": "👩‍❤️‍💋‍👩"
+ },
+ {
+ "key": "kiss_man",
+ "value": "👨‍❤️‍💋‍👨"
+ },
+ {
+ "key": "family",
+ "value": "👪"
+ },
+ {
+ "key": "family_man_women_girl",
+ "value": "👨‍👩‍👧"
+ },
+ {
+ "key": "family_man_women_girl_boy",
+ "value": "👨‍👩‍👧‍👦"
+ },
+ {
+ "key": "family_man_women_boy_boy",
+ "value": "👨‍👩‍👦‍👦"
+ },
+ {
+ "key": "family_man_women_girl_girl",
+ "value": "👨‍👩‍👧‍👧"
+ },
+ {
+ "key": "family_woman_women_boy",
+ "value": "👩‍👩‍👦"
+ },
+ {
+ "key": "family_woman_women_girl",
+ "value": "👩‍👩‍👧"
+ },
+ {
+ "key": "family_woman_women_girl_boy",
+ "value": "👩‍👩‍👧‍👦"
+ },
+ {
+ "key": "family_woman_women_boy_boy",
+ "value": "👩‍👩‍👦‍👦"
+ },
+ {
+ "key": "family_woman_women_girl_girl",
+ "value": "👩‍👩‍👧‍👧"
+ },
+ {
+ "key": "family_man_man_boy",
+ "value": "👨‍👨‍👦"
+ },
+ {
+ "key": "family_man_man_girl",
+ "value": "👨‍👨‍👧"
+ },
+ {
+ "key": "family_man_man_girl_boy",
+ "value": "👨‍👨‍👧‍👦"
+ },
+ {
+ "key": "family_man_man_boy_boy",
+ "value": "👨‍👨‍👦‍👦"
+ },
+ {
+ "key": "family_man_man_girl_girl",
+ "value": "👨‍👨‍👧‍👧"
+ },
+ {
+ "key": "woman_clothes",
+ "value": "👚"
+ },
+ {
+ "key": "t_shirt",
+ "value": "👕"
+ },
+ {
+ "key": "jeans",
+ "value": "👖"
+ },
+ {
+ "key": "necktie",
+ "value": "👔"
+ },
+ {
+ "key": "dress",
+ "value": "👗"
+ },
+ {
+ "key": "bikini",
+ "value": "👙"
+ },
+ {
+ "key": "kimono",
+ "value": "👘"
+ },
+ {
+ "key": "lipstick",
+ "value": "💄"
+ },
+ {
+ "key": "kiss_mark",
+ "value": "💋"
+ },
+ {
+ "key": "footprints",
+ "value": "👣"
+ },
+ {
+ "key": "high_heeled_shoe",
+ "value": "👠"
+ },
+ {
+ "key": "woman_sandal",
+ "value": "👡"
+ },
+ {
+ "key": "woman_boots",
+ "value": "👢"
+ },
+ {
+ "key": "man_shoe",
+ "value": "👞"
+ },
+ {
+ "key": "athletic_shoe",
+ "value": "👟"
+ },
+ {
+ "key": "woman_hat",
+ "value": "👒"
+ },
+ {
+ "key": "top_hat",
+ "value": "🎩"
+ },
+ {
+ "key": "graduation_cap",
+ "value": "🎓"
+ },
+ {
+ "key": "crown",
+ "value": "👑"
+ },
+ {
+ "key": "helmet_with_white_cross",
+ "value": "⛑"
+ },
+ {
+ "key": "school_satchel",
+ "value": "🎒"
+ },
+ {
+ "key": "pouch",
+ "value": "👝"
+ },
+ {
+ "key": "purse",
+ "value": "👛"
+ },
+ {
+ "key": "handbag",
+ "value": "👜"
+ },
+ {
+ "key": "briefcase",
+ "value": "💼"
+ },
+ {
+ "key": "eye_glasses",
+ "value": "👓"
+ },
+ {
+ "key": "dark_sun_glasses",
+ "value": "🕶"
+ },
+ {
+ "key": "ring",
+ "value": "💍"
+ },
+ {
+ "key": "closed_umbrella",
+ "value": "🌂"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/Symbols.json b/nikola/plugins/shortcode/emoji/data/Symbols.json
new file mode 100644
index 0000000..2dd5454
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Symbols.json
@@ -0,0 +1,1082 @@
+{
+ "symbols": {
+ "symbol": [
+ {
+ "key": "heavy_black_heart",
+ "value": "❤"
+ },
+ {
+ "key": "yellow_heart",
+ "value": "💛"
+ },
+ {
+ "key": "green_heart",
+ "value": "💚"
+ },
+ {
+ "key": "blue_heart",
+ "value": "💙"
+ },
+ {
+ "key": "purple_heart",
+ "value": "💜"
+ },
+ {
+ "key": "broken_heart",
+ "value": "💔"
+ },
+ {
+ "key": "heavy_heart_exclamation_mark_ornament",
+ "value": "❣"
+ },
+ {
+ "key": "two_hearts",
+ "value": "💕"
+ },
+ {
+ "key": "revolving_hearts",
+ "value": "💞"
+ },
+ {
+ "key": "beating_heart",
+ "value": "💓"
+ },
+ {
+ "key": "growing_heart",
+ "value": "💗"
+ },
+ {
+ "key": "sparkling_heart",
+ "value": "💖"
+ },
+ {
+ "key": "heart_with_arrow",
+ "value": "💘"
+ },
+ {
+ "key": "heart_with_ribbon",
+ "value": "💝"
+ },
+ {
+ "key": "heart_decoration",
+ "value": "💟"
+ },
+ {
+ "key": "peace_symbol",
+ "value": "☮"
+ },
+ {
+ "key": "latin_cross",
+ "value": "✝"
+ },
+ {
+ "key": "star_and_crescent",
+ "value": "☪"
+ },
+ {
+ "key": "om_symbol",
+ "value": "🕉"
+ },
+ {
+ "key": "wheel_of_dharma",
+ "value": "☸"
+ },
+ {
+ "key": "star_of_david",
+ "value": "✡"
+ },
+ {
+ "key": "six_pointed_star_with_middle_dot",
+ "value": "🔯"
+ },
+ {
+ "key": "menorah_with_nine_branches",
+ "value": "🕎"
+ },
+ {
+ "key": "yin_yang",
+ "value": "☯"
+ },
+ {
+ "key": "orthodox_cross",
+ "value": "☦"
+ },
+ {
+ "key": "place_of_worship",
+ "value": "🛐"
+ },
+ {
+ "key": "ophiuchus",
+ "value": "⛎"
+ },
+ {
+ "key": "aries",
+ "value": "♈"
+ },
+ {
+ "key": "taurus",
+ "value": "♉"
+ },
+ {
+ "key": "gemini",
+ "value": "♊"
+ },
+ {
+ "key": "cancer",
+ "value": "♋"
+ },
+ {
+ "key": "leo",
+ "value": "♌"
+ },
+ {
+ "key": "virgo",
+ "value": "♍"
+ },
+ {
+ "key": "libra",
+ "value": "♎"
+ },
+ {
+ "key": "scorpius",
+ "value": "♏"
+ },
+ {
+ "key": "sagittarius",
+ "value": "♐"
+ },
+ {
+ "key": "capricorn",
+ "value": "♑"
+ },
+ {
+ "key": "aquarius",
+ "value": "♒"
+ },
+ {
+ "key": "pisces",
+ "value": "♓"
+ },
+ {
+ "key": "squared_id",
+ "value": "🆔"
+ },
+ {
+ "key": "atom_symbol",
+ "value": "⚛"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_7a7a",
+ "value": "🈳"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_5272",
+ "value": "🈹"
+ },
+ {
+ "key": "radioactive_sign",
+ "value": "☢"
+ },
+ {
+ "key": "biohazard_sign",
+ "value": "☣"
+ },
+ {
+ "key": "mobile_phone_off",
+ "value": "📴"
+ },
+ {
+ "key": "vibration_mode",
+ "value": "📳"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_6709",
+ "value": "🈶"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_7121",
+ "value": "🈚"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_7533",
+ "value": "🈸"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_55b6",
+ "value": "🈺"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_6708",
+ "value": "🈷"
+ },
+ {
+ "key": "eight_pointed_black_star",
+ "value": "✴"
+ },
+ {
+ "key": "squared_vs",
+ "value": "🆚"
+ },
+ {
+ "key": "circled_ideograph_accept",
+ "value": "🉑"
+ },
+ {
+ "key": "white_flower",
+ "value": "💮"
+ },
+ {
+ "key": "circled_ideograph_advantage",
+ "value": "🉐"
+ },
+ {
+ "key": "circled_ideograph_secret",
+ "value": "㊙"
+ },
+ {
+ "key": "circled_ideograph_congratulation",
+ "value": "㊗"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_5408",
+ "value": "🈴"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_6e80",
+ "value": "🈵"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_7981",
+ "value": "🈲"
+ },
+ {
+ "key": "negative_squared_latin_capital_letter_a",
+ "value": "🅰"
+ },
+ {
+ "key": "negative_squared_latin_capital_letter_b",
+ "value": "🅱"
+ },
+ {
+ "key": "negative_squared_ab",
+ "value": "🆎"
+ },
+ {
+ "key": "squared_cl",
+ "value": "🆑"
+ },
+ {
+ "key": "negative_squared_latin_capital_letter_o",
+ "value": "🅾"
+ },
+ {
+ "key": "squared_sos",
+ "value": "🆘"
+ },
+ {
+ "key": "no_entry",
+ "value": "⛔"
+ },
+ {
+ "key": "name_badge",
+ "value": "📛"
+ },
+ {
+ "key": "no_entry_sign",
+ "value": "🚫"
+ },
+ {
+ "key": "cross_mark",
+ "value": "❌"
+ },
+ {
+ "key": "heavy_large_circle",
+ "value": "⭕"
+ },
+ {
+ "key": "anger_symbol",
+ "value": "💢"
+ },
+ {
+ "key": "hot_springs",
+ "value": "♨"
+ },
+ {
+ "key": "no_pedestrians",
+ "value": "🚷"
+ },
+ {
+ "key": "do_not_litter_symbol",
+ "value": "🚯"
+ },
+ {
+ "key": "no_bi_cycles",
+ "value": "🚳"
+ },
+ {
+ "key": "non_potable_water_symbol",
+ "value": "🚱"
+ },
+ {
+ "key": "no_one_under_eighteen_symbol",
+ "value": "🔞"
+ },
+ {
+ "key": "no_mobile_phones",
+ "value": "📵"
+ },
+ {
+ "key": "heavy_exclamation_mark_symbol",
+ "value": "❗"
+ },
+ {
+ "key": "white_exclamation_mark_ornament",
+ "value": "❕"
+ },
+ {
+ "key": "black_question_mark_ornament",
+ "value": "❓"
+ },
+ {
+ "key": "white_question_mark_ornament",
+ "value": "❔"
+ },
+ {
+ "key": "double_exclamation_mark",
+ "value": "‼"
+ },
+ {
+ "key": "exclamation_question_mark",
+ "value": "⁉"
+ },
+ {
+ "key": "hundred_points_symbol",
+ "value": "💯"
+ },
+ {
+ "key": "low_brightness_symbol",
+ "value": "🔅"
+ },
+ {
+ "key": "high_brightness_symbol",
+ "value": "🔆"
+ },
+ {
+ "key": "trident_emblem",
+ "value": "🔱"
+ },
+ {
+ "key": "fleur_de_lis",
+ "value": "⚜"
+ },
+ {
+ "key": "part_alternation_mark",
+ "value": "〽"
+ },
+ {
+ "key": "warning_sign",
+ "value": "⚠"
+ },
+ {
+ "key": "children_crossing",
+ "value": "🚸"
+ },
+ {
+ "key": "japanese_symbol_for_beginner",
+ "value": "🔰"
+ },
+ {
+ "key": "black_universal_recycling_symbol",
+ "value": "♻"
+ },
+ {
+ "key": "squared_cjk_unified_ideograph_6307",
+ "value": "🈯"
+ },
+ {
+ "key": "chart_with_upwards_trend_and_yen_sign",
+ "value": "💹"
+ },
+ {
+ "key": "sparkle",
+ "value": "❇"
+ },
+ {
+ "key": "eight_spoked_asterisk",
+ "value": "✳"
+ },
+ {
+ "key": "negative_squared_crossmark",
+ "value": "❎"
+ },
+ {
+ "key": "white_heavy_checkmark",
+ "value": "✅"
+ },
+ {
+ "key": "diamond_shape_with_a_dot_inside",
+ "value": "💠"
+ },
+ {
+ "key": "cyclone",
+ "value": "🌀"
+ },
+ {
+ "key": "double_curly_loop",
+ "value": "➿"
+ },
+ {
+ "key": "globe_with_meridians",
+ "value": "🌐"
+ },
+ {
+ "key": "circled_latin_capital_letter_m",
+ "value": "ⓜ"
+ },
+ {
+ "key": "automated_teller_machine",
+ "value": "🏧"
+ },
+ {
+ "key": "squared_katakanasa",
+ "value": "🈂"
+ },
+ {
+ "key": "passport_control",
+ "value": "🛂"
+ },
+ {
+ "key": "customs",
+ "value": "🛃"
+ },
+ {
+ "key": "baggage_claim",
+ "value": "🛄"
+ },
+ {
+ "key": "left_luggage",
+ "value": "🛅"
+ },
+ {
+ "key": "wheel_chair_symbol",
+ "value": "♿"
+ },
+ {
+ "key": "no_smoking_symbol",
+ "value": "🚭"
+ },
+ {
+ "key": "water_closet",
+ "value": "🚾"
+ },
+ {
+ "key": "negative_squared_letter_p",
+ "value": "🅿"
+ },
+ {
+ "key": "potable_water_symbol",
+ "value": "🚰"
+ },
+ {
+ "key": "mens_symbol",
+ "value": "🚹"
+ },
+ {
+ "key": "womens_symbol",
+ "value": "🚺"
+ },
+ {
+ "key": "baby_symbol",
+ "value": "🚼"
+ },
+ {
+ "key": "restroom",
+ "value": "🚻"
+ },
+ {
+ "key": "put_litter_in_its_place",
+ "value": "🚮"
+ },
+ {
+ "key": "cinema",
+ "value": "🎦"
+ },
+ {
+ "key": "antenna_with_bars",
+ "value": "📶"
+ },
+ {
+ "key": "squared_katakana_koko",
+ "value": "🈁"
+ },
+ {
+ "key": "squared_ng",
+ "value": "🆖"
+ },
+ {
+ "key": "squared_ok",
+ "value": "🆗"
+ },
+ {
+ "key": "squared_exclamation_mark",
+ "value": "🆙"
+ },
+ {
+ "key": "squared_cool",
+ "value": "🆒"
+ },
+ {
+ "key": "squared_new",
+ "value": "🆕"
+ },
+ {
+ "key": "squared_free",
+ "value": "🆓"
+ },
+ {
+ "key": "keycap_digit_zero",
+ "value": "0⃣"
+ },
+ {
+ "key": "keycap_digit_one",
+ "value": "1⃣"
+ },
+ {
+ "key": "keycap_digit_two",
+ "value": "2⃣"
+ },
+ {
+ "key": "keycap_digit_three",
+ "value": "3⃣"
+ },
+ {
+ "key": "keycap_digit_four",
+ "value": "4⃣"
+ },
+ {
+ "key": "keycap_digit_five",
+ "value": "5⃣"
+ },
+ {
+ "key": "keycap_digit_six",
+ "value": "6⃣"
+ },
+ {
+ "key": "keycap_digit_seven",
+ "value": "7⃣"
+ },
+ {
+ "key": "keycap_digit_eight",
+ "value": "8⃣"
+ },
+ {
+ "key": "keycap_digit_nine",
+ "value": "9⃣"
+ },
+ {
+ "key": "keycap_ten",
+ "value": "🔟"
+ },
+ {
+ "key": "input_symbol_for_numbers",
+ "value": "🔢"
+ },
+ {
+ "key": "black_right_pointing_triangle",
+ "value": "▶"
+ },
+ {
+ "key": "double_vertical_bar",
+ "value": "⏸"
+ },
+ {
+ "key": "blk_rgt_point_triangle_dbl_vertical_bar",
+ "value": "⏯"
+ },
+ {
+ "key": "black_square_for_stop",
+ "value": "⏹"
+ },
+ {
+ "key": "black_circle_for_record",
+ "value": "⏺"
+ },
+ {
+ "key": "blk_rgt_point_dbl_triangle_vertical_bar",
+ "value": "⏭"
+ },
+ {
+ "key": "blk_lft_point_dbl_triangle_vertical_bar",
+ "value": "⏮"
+ },
+ {
+ "key": "blk_rgt_point_dbl_triangle",
+ "value": "⏩"
+ },
+ {
+ "key": "blk_lft_point_dbl_triangle",
+ "value": "⏪"
+ },
+ {
+ "key": "twisted_rightwards_arrows",
+ "value": "🔀"
+ },
+ {
+ "key": "cwise_rgt_lft_open_circle_arrow",
+ "value": "🔁"
+ },
+ {
+ "key": "cwise_rgt_lft_open_circle_arrow_overlay",
+ "value": "🔂"
+ },
+ {
+ "key": "blk_lft_point_triangle",
+ "value": "◀"
+ },
+ {
+ "key": "up_point_small_red_triangle",
+ "value": "🔼"
+ },
+ {
+ "key": "down_point_small_red_triangle",
+ "value": "🔽"
+ },
+ {
+ "key": "blk_up_point_double_triangle",
+ "value": "⏫"
+ },
+ {
+ "key": "blk_down_point_double_triangle",
+ "value": "⏬"
+ },
+ {
+ "key": "black_rightwards_arrow",
+ "value": "➡"
+ },
+ {
+ "key": "leftwards_black_arrow",
+ "value": "⬅"
+ },
+ {
+ "key": "upwards_black_arrow",
+ "value": "⬆"
+ },
+ {
+ "key": "downwards_black_arrow",
+ "value": "⬇"
+ },
+ {
+ "key": "northeast_arrow",
+ "value": "↗"
+ },
+ {
+ "key": "southeast_arrow",
+ "value": "↘"
+ },
+ {
+ "key": "south_west_arrow",
+ "value": "↙"
+ },
+ {
+ "key": "north_west_arrow",
+ "value": "↖"
+ },
+ {
+ "key": "up_down_arrow",
+ "value": "↕"
+ },
+ {
+ "key": "left_right_arrow",
+ "value": "↔"
+ },
+ {
+ "key": "acwise_down_up_open_circle_arrow",
+ "value": "🔄"
+ },
+ {
+ "key": "rightwards_arrow_with_hook",
+ "value": "↪"
+ },
+ {
+ "key": "leftwards_arrow_with_hook",
+ "value": "↩"
+ },
+ {
+ "key": "arrow_point_rgt_then_curving_up",
+ "value": "⤴"
+ },
+ {
+ "key": "arrow_point_rgt_then_curving_down",
+ "value": "⤵"
+ },
+ {
+ "key": "keycap_number_sign",
+ "value": "#⃣"
+ },
+ {
+ "key": "keycap_asterisk",
+ "value": "*⃣"
+ },
+ {
+ "key": "information_source",
+ "value": "ℹ"
+ },
+ {
+ "key": "input_symbol_for_latin_letters",
+ "value": "🔤"
+ },
+ {
+ "key": "input_symbol_latin_small_letters",
+ "value": "🔡"
+ },
+ {
+ "key": "input_symbol_latin_capital_letters",
+ "value": "🔠"
+ },
+ {
+ "key": "input_symbol_symbols",
+ "value": "🔣"
+ },
+ {
+ "key": "musical_note",
+ "value": "🎵"
+ },
+ {
+ "key": "multiple_musical_notes",
+ "value": "🎶"
+ },
+ {
+ "key": "wavy_dash",
+ "value": "〰"
+ },
+ {
+ "key": "curly_loop",
+ "value": "➰"
+ },
+ {
+ "key": "heavy_check_mark",
+ "value": "✔"
+ },
+ {
+ "key": "cwise_down_up_open_circle_arrows",
+ "value": "🔃"
+ },
+ {
+ "key": "heavy_plus_sign",
+ "value": "➕"
+ },
+ {
+ "key": "heavy_minus_sign",
+ "value": "➖"
+ },
+ {
+ "key": "heavy_division_sign",
+ "value": "➗"
+ },
+ {
+ "key": "heavy_multiplication_x",
+ "value": "✖"
+ },
+ {
+ "key": "heavy_dollar_sign",
+ "value": "💲"
+ },
+ {
+ "key": "currency_exchange",
+ "value": "💱"
+ },
+ {
+ "key": "copyright_sign",
+ "value": "©"
+ },
+ {
+ "key": "registered_sign",
+ "value": "®"
+ },
+ {
+ "key": "trademark_sign",
+ "value": "™"
+ },
+ {
+ "key": "end_with_lft_arrow_above",
+ "value": "🔚"
+ },
+ {
+ "key": "back_with_lft_arrow_above",
+ "value": "🔙"
+ },
+ {
+ "key": "on_exclamation_lft_rgt_arrow",
+ "value": "🔛"
+ },
+ {
+ "key": "top_with_up_arrow_above",
+ "value": "🔝"
+ },
+ {
+ "key": "soon_right_arrow_above",
+ "value": "🔜"
+ },
+ {
+ "key": "ballot_box_with_check",
+ "value": "☑"
+ },
+ {
+ "key": "radio_button",
+ "value": "🔘"
+ },
+ {
+ "key": "medium_white_circle",
+ "value": "⚪"
+ },
+ {
+ "key": "medium_black_circle",
+ "value": "⚫"
+ },
+ {
+ "key": "large_red_circle",
+ "value": "🔴"
+ },
+ {
+ "key": "large_blue_circle",
+ "value": "🔵"
+ },
+ {
+ "key": "small_orange_diamond",
+ "value": "🔸"
+ },
+ {
+ "key": "small_blue_diamond",
+ "value": "🔹"
+ },
+ {
+ "key": "large_orange_diamond",
+ "value": "🔶"
+ },
+ {
+ "key": "large_blue_diamond",
+ "value": "🔷"
+ },
+ {
+ "key": "up_point_red_triangle",
+ "value": "🔺"
+ },
+ {
+ "key": "black_small_square",
+ "value": "▪"
+ },
+ {
+ "key": "white_small_square",
+ "value": "▫"
+ },
+ {
+ "key": "black_large_square",
+ "value": "⬛"
+ },
+ {
+ "key": "white_large_square",
+ "value": "⬜"
+ },
+ {
+ "key": "down_point_red_triangle",
+ "value": "🔻"
+ },
+ {
+ "key": "black_medium_square",
+ "value": "◼"
+ },
+ {
+ "key": "white_medium_square",
+ "value": "◻"
+ },
+ {
+ "key": "black_medium_small_square",
+ "value": "◾"
+ },
+ {
+ "key": "white_medium_small_square",
+ "value": "◽"
+ },
+ {
+ "key": "black_square_button",
+ "value": "🔲"
+ },
+ {
+ "key": "white_square_button",
+ "value": "🔳"
+ },
+ {
+ "key": "speaker",
+ "value": "🔈"
+ },
+ {
+ "key": "speaker_one_sound_wave",
+ "value": "🔉"
+ },
+ {
+ "key": "speaker_three_sound_waves",
+ "value": "🔊"
+ },
+ {
+ "key": "speaker_cancellation_stroke",
+ "value": "🔇"
+ },
+ {
+ "key": "cheering_megaphone",
+ "value": "📣"
+ },
+ {
+ "key": "public_address_loudspeaker",
+ "value": "📢"
+ },
+ {
+ "key": "bell",
+ "value": "🔔"
+ },
+ {
+ "key": "bell_with_cancellation_stroke",
+ "value": "🔕"
+ },
+ {
+ "key": "playing_card_black_joker",
+ "value": "🃏"
+ },
+ {
+ "key": "mahjong_tile_red_dragon",
+ "value": "🀄"
+ },
+ {
+ "key": "black_spade_suit",
+ "value": "♠"
+ },
+ {
+ "key": "black_club_suit",
+ "value": "♣"
+ },
+ {
+ "key": "black_heart_suit",
+ "value": "♥"
+ },
+ {
+ "key": "black_diamond_suit",
+ "value": "♦"
+ },
+ {
+ "key": "flower_playing_cards",
+ "value": "🎴"
+ },
+ {
+ "key": "eye_in_speech_bubble",
+ "value": "👁‍🗨"
+ },
+ {
+ "key": "thought_balloon",
+ "value": "💭"
+ },
+ {
+ "key": "right_anger_bubble",
+ "value": "🗯"
+ },
+ {
+ "key": "speech_balloon",
+ "value": "💬"
+ },
+ {
+ "key": "clock_face_one_o_clock",
+ "value": "🕐"
+ },
+ {
+ "key": "clock_face_two_o_clock",
+ "value": "🕑"
+ },
+ {
+ "key": "clock_face_three_o_clock",
+ "value": "🕒"
+ },
+ {
+ "key": "clock_face_four_o_clock",
+ "value": "🕓"
+ },
+ {
+ "key": "clock_face_five_o_clock",
+ "value": "🕔"
+ },
+ {
+ "key": "clock_face_six_o_clock",
+ "value": "🕕"
+ },
+ {
+ "key": "clock_face_seven_o_clock",
+ "value": "🕖"
+ },
+ {
+ "key": "clock_face_eight_o_clock",
+ "value": "🕗"
+ },
+ {
+ "key": "clock_face_nine_o_clock",
+ "value": "🕘"
+ },
+ {
+ "key": "clock_face_ten_o_clock",
+ "value": "🕙"
+ },
+ {
+ "key": "clock_face_eleven_o_clock",
+ "value": "🕚"
+ },
+ {
+ "key": "clock_face_twelve_o_clock",
+ "value": "🕛"
+ },
+ {
+ "key": "clock_face_one_thirty",
+ "value": "🕜"
+ },
+ {
+ "key": "clock_face_two_thirty",
+ "value": "🕝"
+ },
+ {
+ "key": "clock_face_three_thirty",
+ "value": "🕞"
+ },
+ {
+ "key": "clock_face_four_thirty",
+ "value": "🕟"
+ },
+ {
+ "key": "clock_face_five_thirty",
+ "value": "🕠"
+ },
+ {
+ "key": "clock_face_six_thirty",
+ "value": "🕡"
+ },
+ {
+ "key": "clock_face_seven_thirty",
+ "value": "🕢"
+ },
+ {
+ "key": "clock_face_eight_thirty",
+ "value": "🕣"
+ },
+ {
+ "key": "clock_face_nine_thirty",
+ "value": "🕤"
+ },
+ {
+ "key": "clock_face_ten_thirty",
+ "value": "🕥"
+ },
+ {
+ "key": "clock_face_eleven_thirty",
+ "value": "🕦"
+ },
+ {
+ "key": "clock_face_twelve_thirty",
+ "value": "🕧"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/nikola/plugins/shortcode/emoji/data/Travel.json b/nikola/plugins/shortcode/emoji/data/Travel.json
new file mode 100644
index 0000000..e38b84f
--- /dev/null
+++ b/nikola/plugins/shortcode/emoji/data/Travel.json
@@ -0,0 +1,466 @@
+{
+ "travels": {
+ "travel": [
+ {
+ "key": "automobile",
+ "value": "🚗"
+ },
+ {
+ "key": "taxi",
+ "value": "🚕"
+ },
+ {
+ "key": "recreational_vehicle",
+ "value": "🚙"
+ },
+ {
+ "key": "bus",
+ "value": "🚌"
+ },
+ {
+ "key": "trolley_bus",
+ "value": "🚎"
+ },
+ {
+ "key": "racing_car",
+ "value": "🏎"
+ },
+ {
+ "key": "police_car",
+ "value": "🚓"
+ },
+ {
+ "key": "ambulance",
+ "value": "🚑"
+ },
+ {
+ "key": "fire_engine",
+ "value": "🚒"
+ },
+ {
+ "key": "minibus",
+ "value": "🚐"
+ },
+ {
+ "key": "delivery_truck",
+ "value": "🚚"
+ },
+ {
+ "key": "articulated_lorry",
+ "value": "🚛"
+ },
+ {
+ "key": "tractor",
+ "value": "🚜"
+ },
+ {
+ "key": "racing_motorcycle",
+ "value": "🏍"
+ },
+ {
+ "key": "bicycle",
+ "value": "🚲"
+ },
+ {
+ "key": "police_light",
+ "value": "🚨"
+ },
+ {
+ "key": "on_coming_police_car",
+ "value": "🚔"
+ },
+ {
+ "key": "on_coming_bus",
+ "value": "🚍"
+ },
+ {
+ "key": "on_coming_automobile",
+ "value": "🚘"
+ },
+ {
+ "key": "on_coming_taxi",
+ "value": "🚖"
+ },
+ {
+ "key": "aerial_tramway",
+ "value": "🚡"
+ },
+ {
+ "key": "mountain_cableway",
+ "value": "🚠"
+ },
+ {
+ "key": "suspension_railway",
+ "value": "🚟"
+ },
+ {
+ "key": "railway_car",
+ "value": "🚃"
+ },
+ {
+ "key": "tramcar",
+ "value": "🚋"
+ },
+ {
+ "key": "monorail",
+ "value": "🚝"
+ },
+ {
+ "key": "high_speed_train",
+ "value": "🚄"
+ },
+ {
+ "key": "high_speed_train_bullet_nose",
+ "value": "🚅"
+ },
+ {
+ "key": "light_rail",
+ "value": "🚈"
+ },
+ {
+ "key": "mountain_railway",
+ "value": "🚞"
+ },
+ {
+ "key": "steam_locomotive",
+ "value": "🚂"
+ },
+ {
+ "key": "train",
+ "value": "🚆"
+ },
+ {
+ "key": "metro",
+ "value": "🚇"
+ },
+ {
+ "key": "tram",
+ "value": "🚊"
+ },
+ {
+ "key": "station",
+ "value": "🚉"
+ },
+ {
+ "key": "helicopter",
+ "value": "🚁"
+ },
+ {
+ "key": "small_airplane",
+ "value": "🛩"
+ },
+ {
+ "key": "airplane",
+ "value": "✈"
+ },
+ {
+ "key": "airplane_departure",
+ "value": "🛫"
+ },
+ {
+ "key": "airplane_arriving",
+ "value": "🛬"
+ },
+ {
+ "key": "sailboat",
+ "value": "⛵"
+ },
+ {
+ "key": "motorboat",
+ "value": "🛥"
+ },
+ {
+ "key": "speedboat",
+ "value": "🚤"
+ },
+ {
+ "key": "ferry",
+ "value": "⛴"
+ },
+ {
+ "key": "passenger_ship",
+ "value": "🛳"
+ },
+ {
+ "key": "rocket",
+ "value": "🚀"
+ },
+ {
+ "key": "satellite",
+ "value": "🛰"
+ },
+ {
+ "key": "seat",
+ "value": "💺"
+ },
+ {
+ "key": "anchor",
+ "value": "⚓"
+ },
+ {
+ "key": "construction_sign",
+ "value": "🚧"
+ },
+ {
+ "key": "fuel_pump",
+ "value": "⛽"
+ },
+ {
+ "key": "bus_stop",
+ "value": "🚏"
+ },
+ {
+ "key": "vertical_traffic_light",
+ "value": "🚦"
+ },
+ {
+ "key": "horizontal_traffic_light",
+ "value": "🚥"
+ },
+ {
+ "key": "chequered_flag",
+ "value": "🏁"
+ },
+ {
+ "key": "ship",
+ "value": "🚢"
+ },
+ {
+ "key": "ferris_wheel",
+ "value": "🎡"
+ },
+ {
+ "key": "roller_coaster",
+ "value": "🎢"
+ },
+ {
+ "key": "carousel_horse",
+ "value": "🎠"
+ },
+ {
+ "key": "building_construction",
+ "value": "🏗"
+ },
+ {
+ "key": "foggy",
+ "value": "🌁"
+ },
+ {
+ "key": "tokyo_tower",
+ "value": "🗼"
+ },
+ {
+ "key": "factory",
+ "value": "🏭"
+ },
+ {
+ "key": "fountain",
+ "value": "⛲"
+ },
+ {
+ "key": "moon_viewing_ceremony",
+ "value": "🎑"
+ },
+ {
+ "key": "mountain",
+ "value": "⛰"
+ },
+ {
+ "key": "snow_capped_mountain",
+ "value": "🏔"
+ },
+ {
+ "key": "mount_fuji",
+ "value": "🗻"
+ },
+ {
+ "key": "volcano",
+ "value": "🌋"
+ },
+ {
+ "key": "silhouette_of_japan",
+ "value": "🗾"
+ },
+ {
+ "key": "camping",
+ "value": "🏕"
+ },
+ {
+ "key": "tent",
+ "value": "⛺"
+ },
+ {
+ "key": "national_park",
+ "value": "🏞"
+ },
+ {
+ "key": "motorway",
+ "value": "🛣"
+ },
+ {
+ "key": "railway_track",
+ "value": "🛤"
+ },
+ {
+ "key": "sunrise",
+ "value": "🌅"
+ },
+ {
+ "key": "sunrise_over_mountain",
+ "value": "🌄"
+ },
+ {
+ "key": "desert",
+ "value": "🏜"
+ },
+ {
+ "key": "beach_with_umbrella",
+ "value": "🏖"
+ },
+ {
+ "key": "desert_island",
+ "value": "🏝"
+ },
+ {
+ "key": "sunset_over_buildings",
+ "value": "🌇"
+ },
+ {
+ "key": "city_scape_at_dusk",
+ "value": "🌆"
+ },
+ {
+ "key": "city_scape",
+ "value": "🏙"
+ },
+ {
+ "key": "night_with_stars",
+ "value": "🌃"
+ },
+ {
+ "key": "bridge_at_night",
+ "value": "🌉"
+ },
+ {
+ "key": "milky_way",
+ "value": "🌌"
+ },
+ {
+ "key": "shooting_star",
+ "value": "🌠"
+ },
+ {
+ "key": "fire_work_sparkler",
+ "value": "🎇"
+ },
+ {
+ "key": "fireworks",
+ "value": "🎆"
+ },
+ {
+ "key": "rainbow",
+ "value": "🌈"
+ },
+ {
+ "key": "house_buildings",
+ "value": "🏘"
+ },
+ {
+ "key": "european_castle",
+ "value": "🏰"
+ },
+ {
+ "key": "japanese_castle",
+ "value": "🏯"
+ },
+ {
+ "key": "stadium",
+ "value": "🏟"
+ },
+ {
+ "key": "statue_of_liberty",
+ "value": "🗽"
+ },
+ {
+ "key": "house_building",
+ "value": "🏠"
+ },
+ {
+ "key": "house_with_garden",
+ "value": "🏡"
+ },
+ {
+ "key": "derelict_house_building",
+ "value": "🏚"
+ },
+ {
+ "key": "office_building",
+ "value": "🏢"
+ },
+ {
+ "key": "department_store",
+ "value": "🏬"
+ },
+ {
+ "key": "japanese_post_office",
+ "value": "🏣"
+ },
+ {
+ "key": "european_post_office",
+ "value": "🏤"
+ },
+ {
+ "key": "hospital",
+ "value": "🏥"
+ },
+ {
+ "key": "bank",
+ "value": "🏦"
+ },
+ {
+ "key": "hotel",
+ "value": "🏨"
+ },
+ {
+ "key": "convenience_store",
+ "value": "🏪"
+ },
+ {
+ "key": "school",
+ "value": "🏫"
+ },
+ {
+ "key": "love_hotel",
+ "value": "🏩"
+ },
+ {
+ "key": "wedding",
+ "value": "💒"
+ },
+ {
+ "key": "classical_building",
+ "value": "🏛"
+ },
+ {
+ "key": "church",
+ "value": "⛪"
+ },
+ {
+ "key": "mosque",
+ "value": "🕌"
+ },
+ {
+ "key": "synagogue",
+ "value": "🕍"
+ },
+ {
+ "key": "kaaba",
+ "value": "🕋"
+ },
+ {
+ "key": "shinto_shrine",
+ "value": "⛩"
+ }
+ ]
+ }
+}
diff --git a/nikola/plugins/shortcode/gist.plugin b/nikola/plugins/shortcode/gist.plugin
index cd19a72..b610763 100644
--- a/nikola/plugins/shortcode/gist.plugin
+++ b/nikola/plugins/shortcode/gist.plugin
@@ -3,7 +3,7 @@ name = gist
module = gist
[Nikola]
-plugincategory = Shortcode
+PluginCategory = Shortcode
[Documentation]
author = Roberto Alsina
diff --git a/nikola/plugins/shortcode/gist.py b/nikola/plugins/shortcode/gist.py
index 64fd0d9..eb9e976 100644
--- a/nikola/plugins/shortcode/gist.py
+++ b/nikola/plugins/shortcode/gist.py
@@ -13,12 +13,6 @@ class Plugin(ShortcodePlugin):
name = "gist"
- def set_site(self, site):
- """Set Nikola site."""
- self.site = site
- site.register_shortcode('gist', self.handler)
- return super(Plugin, self).set_site(site)
-
def get_raw_gist_with_filename(self, gistID, filename):
"""Get raw gist text for a filename."""
url = '/'.join(("https://gist.github.com/raw", gistID, filename))
diff --git a/nikola/plugins/shortcode/listing.plugin b/nikola/plugins/shortcode/listing.plugin
new file mode 100644
index 0000000..90fb6eb
--- /dev/null
+++ b/nikola/plugins/shortcode/listing.plugin
@@ -0,0 +1,13 @@
+[Core]
+name = listing_shortcode
+module = listing
+
+[Nikola]
+PluginCategory = Shortcode
+
+[Documentation]
+author = Roberto Alsina
+version = 0.1
+website = https://getnikola.com/
+description = Listing shortcode
+
diff --git a/nikola/plugins/shortcode/listing.py b/nikola/plugins/shortcode/listing.py
new file mode 100644
index 0000000..b51365a
--- /dev/null
+++ b/nikola/plugins/shortcode/listing.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2017-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Listing shortcode (equivalent to reST’s listing directive)."""
+
+import os
+from urllib.parse import urlunsplit
+
+import pygments
+
+from nikola.plugin_categories import ShortcodePlugin
+
+
+class Plugin(ShortcodePlugin):
+ """Plugin for listing shortcode."""
+
+ name = "listing"
+
+ def set_site(self, site):
+ """Set Nikola site."""
+ self.site = site
+ Plugin.folders = site.config['LISTINGS_FOLDERS']
+ return super().set_site(site)
+
+ def handler(self, fname, language='text', linenumbers=False, filename=None, site=None, data=None, lang=None, post=None):
+ """Create HTML for a listing."""
+ fname = fname.replace('/', os.sep)
+ if len(self.folders) == 1:
+ listings_folder = next(iter(self.folders.keys()))
+ if fname.startswith(listings_folder):
+ fpath = os.path.join(fname) # new syntax: specify folder name
+ else:
+ # old syntax: don't specify folder name
+ fpath = os.path.join(listings_folder, fname)
+ else:
+ # must be new syntax: specify folder name
+ fpath = os.path.join(fname)
+ linenumbers = 'table' if linenumbers else False
+ deps = [fpath]
+ with open(fpath, 'r') as inf:
+ target = urlunsplit(
+ ("link", 'listing', fpath.replace('\\', '/'), '', ''))
+ src_target = urlunsplit(
+ ("link", 'listing_source', fpath.replace('\\', '/'), '', ''))
+ src_label = self.site.MESSAGES('Source')
+
+ data = inf.read()
+ lexer = pygments.lexers.get_lexer_by_name(language)
+ formatter = pygments.formatters.get_formatter_by_name(
+ 'html', linenos=linenumbers)
+ output = '<a href="{1}">{0}</a> <a href="{3}">({2})</a>' .format(
+ fname, target, src_label, src_target) + pygments.highlight(data, lexer, formatter)
+
+ return output, deps
diff --git a/nikola/plugins/shortcode/post_list.plugin b/nikola/plugins/shortcode/post_list.plugin
new file mode 100644
index 0000000..494a1d8
--- /dev/null
+++ b/nikola/plugins/shortcode/post_list.plugin
@@ -0,0 +1,13 @@
+[Core]
+name = post_list
+module = post_list
+
+[Nikola]
+PluginCategory = Shortcode
+
+[Documentation]
+author = Udo Spallek
+version = 0.2
+website = https://getnikola.com/
+description = Includes a list of posts with tag and slice based filters.
+
diff --git a/nikola/plugins/shortcode/post_list.py b/nikola/plugins/shortcode/post_list.py
new file mode 100644
index 0000000..462984a
--- /dev/null
+++ b/nikola/plugins/shortcode/post_list.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013-2020 Udo Spallek, Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Post list shortcode."""
+
+
+import operator
+import os
+import uuid
+
+import natsort
+
+from nikola import utils
+from nikola.packages.datecond import date_in_range
+from nikola.plugin_categories import ShortcodePlugin
+
+
+class PostListShortcode(ShortcodePlugin):
+ """Provide a shortcode to create a list of posts.
+
+ Post List
+ =========
+ :Directive Arguments: None.
+ :Directive Options: lang, start, stop, reverse, sort, date, tags, categories, sections, slugs, post_type, template, id
+ :Directive Content: None.
+
+ The posts appearing in the list can be filtered by options.
+ *List slicing* is provided with the *start*, *stop* and *reverse* options.
+
+ The following not required options are recognized:
+
+ ``start`` : integer
+ The index of the first post to show.
+ A negative value like ``-3`` will show the *last* three posts in the
+ post-list.
+ Defaults to None.
+
+ ``stop`` : integer
+ The index of the last post to show.
+ A value negative value like ``-1`` will show every post, but not the
+ *last* in the post-list.
+ Defaults to None.
+
+ ``reverse`` : flag
+ Reverse the order of the post-list.
+ Defaults is to not reverse the order of posts.
+
+ ``sort`` : string
+ Sort post list by one of each post's attributes, usually ``title`` or a
+ custom ``priority``. Defaults to None (chronological sorting).
+
+ ``date`` : string
+ Show posts that match date range specified by this option. Format:
+
+ * comma-separated clauses (AND)
+ * clause: attribute comparison_operator value (spaces optional)
+ * attribute: year, month, day, hour, month, second, weekday, isoweekday; or empty for full datetime
+ * comparison_operator: == != <= >= < >
+ * value: integer, 'now', 'today', or dateutil-compatible date input
+
+ ``tags`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``tags``.
+ Defaults to None.
+
+ ``require_all_tags`` : flag
+ Change tag filter behaviour to show only posts that have all specified ``tags``.
+ Defaults to False.
+
+ ``categories`` : string [, string...]
+ Filter posts to show only posts having one of the ``categories``.
+ Defaults to None.
+
+ ``sections`` : string [, string...]
+ Filter posts to show only posts having one of the ``sections``.
+ Defaults to None.
+
+ ``slugs`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``slugs``.
+ Defaults to None.
+
+ ``post_type`` (or ``type``) : string
+ Show only ``posts``, ``pages`` or ``all``.
+ Replaces ``all``. Defaults to ``posts``.
+
+ ``lang`` : string
+ The language of post *titles* and *links*.
+ Defaults to default language.
+
+ ``template`` : string
+ The name of an alternative template to render the post-list.
+ Defaults to ``post_list_directive.tmpl``
+
+ ``id`` : string
+ A manual id for the post list.
+ Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex.
+ """
+
+ name = "post_list"
+
+ def set_site(self, site):
+ """Set the site."""
+ super().set_site(site)
+ site.register_shortcode('post-list', self.handler)
+
+ def handler(self, start=None, stop=None, reverse=False, tags=None, require_all_tags=False, categories=None,
+ sections=None, slugs=None, post_type='post', type=False,
+ lang=None, template='post_list_directive.tmpl', sort=None,
+ id=None, data=None, state=None, site=None, date=None, filename=None, post=None):
+ """Generate HTML for post-list."""
+ if lang is None:
+ lang = utils.LocaleBorg().current_lang
+ if site.invariant: # for testing purposes
+ post_list_id = id or 'post_list_' + 'fixedvaluethatisnotauuid'
+ else:
+ post_list_id = id or 'post_list_' + uuid.uuid4().hex
+
+ # Get post from filename if available
+ if filename:
+ self_post = site.post_per_input_file.get(filename)
+ else:
+ self_post = None
+
+ if self_post:
+ self_post.register_depfile("####MAGIC####TIMELINE", lang=lang)
+
+ # If we get strings for start/stop, make them integers
+ if start is not None:
+ start = int(start)
+ if stop is not None:
+ stop = int(stop)
+
+ # Parse tags/categories/sections/slugs (input is strings)
+ categories = [c.strip().lower() for c in categories.split(',')] if categories else []
+ sections = [s.strip().lower() for s in sections.split(',')] if sections else []
+ slugs = [s.strip() for s in slugs.split(',')] if slugs else []
+
+ filtered_timeline = []
+ posts = []
+ step = None if reverse is False else -1
+
+ if type is not False:
+ post_type = type
+
+ if post_type == 'page' or post_type == 'pages':
+ timeline = [p for p in site.timeline if not p.use_in_feeds]
+ elif post_type == 'all':
+ timeline = [p for p in site.timeline]
+ else: # post
+ timeline = [p for p in site.timeline if p.use_in_feeds]
+
+ # self_post should be removed from timeline because this is redundant
+ timeline = [p for p in timeline if p.source_path != filename]
+
+ if categories:
+ timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories]
+
+ if sections:
+ timeline = [p for p in timeline if p.section_name(lang).lower() in sections]
+
+ if tags:
+ tags = {t.strip().lower() for t in tags.split(',')}
+ if require_all_tags:
+ compare = set.issubset
+ else:
+ compare = operator.and_
+ for post in timeline:
+ post_tags = {t.lower() for t in post.tags}
+ if compare(tags, post_tags):
+ filtered_timeline.append(post)
+ else:
+ filtered_timeline = timeline
+
+ if sort:
+ filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC)
+
+ if date:
+ _now = utils.current_time()
+ filtered_timeline = [p for p in filtered_timeline if date_in_range(utils.html_unescape(date), p.date, now=_now)]
+
+ for post in filtered_timeline[start:stop:step]:
+ if slugs:
+ cont = True
+ for slug in slugs:
+ if slug == post.meta('slug'):
+ cont = False
+
+ if cont:
+ continue
+
+ bp = post.translated_base_path(lang)
+ if os.path.exists(bp) and state:
+ state.document.settings.record_dependencies.add(bp)
+ elif os.path.exists(bp) and self_post:
+ self_post.register_depfile(bp, lang=lang)
+
+ posts += [post]
+
+ template_deps = site.template_system.template_deps(template)
+ if state:
+ # Register template as a dependency (Issue #2391)
+ for d in template_deps:
+ state.document.settings.record_dependencies.add(d)
+ elif self_post:
+ for d in template_deps:
+ self_post.register_depfile(d, lang=lang)
+
+ template_data = {
+ 'lang': lang,
+ 'posts': posts,
+ # Need to provide str, not TranslatableSetting (Issue #2104)
+ 'date_format': site.GLOBAL_CONTEXT.get('date_format')[lang],
+ 'post_list_id': post_list_id,
+ 'messages': site.MESSAGES,
+ '_link': site.link,
+ }
+ output = site.template_system.render_template(
+ template, None, template_data)
+ return output, template_deps
+
+
+# Request file name from shortcode (Issue #2412)
+PostListShortcode.handler.nikola_shortcode_pass_filename = True
diff --git a/nikola/plugins/shortcode/thumbnail.plugin b/nikola/plugins/shortcode/thumbnail.plugin
new file mode 100644
index 0000000..e55d34f
--- /dev/null
+++ b/nikola/plugins/shortcode/thumbnail.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = thumbnail
+module = thumbnail
+
+[Nikola]
+PluginCategory = Shortcode
+
+[Documentation]
+author = Chris Warrick
+version = 0.1
+website = https://getnikola.com/
+description = Thumbnail shortcode
diff --git a/nikola/plugins/shortcode/thumbnail.py b/nikola/plugins/shortcode/thumbnail.py
new file mode 100644
index 0000000..feb731b
--- /dev/null
+++ b/nikola/plugins/shortcode/thumbnail.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2017-2020 Roberto Alsina, Chris Warrick and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Thumbnail shortcode (equivalent to reST’s thumbnail directive)."""
+
+import os.path
+
+from nikola.plugin_categories import ShortcodePlugin
+
+
+class ThumbnailShortcode(ShortcodePlugin):
+ """Plugin for thumbnail directive."""
+
+ name = "thumbnail"
+
+ def handler(self, uri, alt=None, align=None, linktitle=None, title=None, imgclass=None, figclass=None, site=None, data=None, lang=None, post=None):
+ """Create HTML for thumbnail."""
+ if uri.endswith('.svg'):
+ # the ? at the end makes docutil output an <img> instead of an object for the svg, which lightboxes may require
+ src = '.thumbnail'.join(os.path.splitext(uri)) + '?'
+ else:
+ src = '.thumbnail'.join(os.path.splitext(uri))
+
+ if imgclass is None:
+ imgclass = ''
+ if figclass is None:
+ figclass = ''
+
+ if align and data:
+ figclass += ' align-{0}'.format(align)
+ elif align:
+ imgclass += ' align-{0}'.format(align)
+
+ output = '<a href="{0}" class="image-reference"'.format(uri)
+ if linktitle:
+ output += ' title="{0}"'.format(linktitle)
+ output += '><img src="{0}"'.format(src)
+ for item, name in ((alt, 'alt'), (title, 'title'), (imgclass, 'class')):
+ if item:
+ output += ' {0}="{1}"'.format(name, item)
+ output += '></a>'
+
+ if data:
+ output = '<div class="figure {0}">{1}{2}</div>'.format(figclass, output, data)
+
+ return output, []
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
index 4eeae62..3e18cd5 100644
--- a/nikola/plugins/task/__init__.py
+++ b/nikola/plugins/task/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin
index eb079da..62e5fd9 100644
--- a/nikola/plugins/task/archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_archive
+name = classify_archive
module = archive
[Documentation]
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generates the blog's archive pages.
[Nikola]
-plugincategory = Task
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py
index 303d349..4cbf215 100644
--- a/nikola/plugins/task/archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,231 +24,216 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the post archives."""
+"""Classify the posts in archives."""
-import copy
-import os
-
-# for tearDown with _reload we cannot use 'import from' to access LocaleBorg
-import nikola.utils
import datetime
-from nikola.plugin_categories import Task
-from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name_for_index_link
+from collections import defaultdict
+
+import natsort
+import nikola.utils
+from nikola.plugin_categories import Taxonomy
+
+
+class Archive(Taxonomy):
+ """Classify the post archives."""
+
+ name = "classify_archive"
+
+ classification_name = "archive"
+ overview_page_variable_name = "archive"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = True
+ include_posts_into_hierarchy_root = True
+ subcategories_list_template = "list.tmpl"
+ template_for_classification_overview = None
+ always_disable_rss = True
+ always_disable_atom = True
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = False
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'archive_index': False,
+ 'archive': """Link to archive path, name is the year.
-class Archive(Task):
- """Render the post archives."""
+ Example:
- name = "render_archive"
+ link://archive/2013 => /archives/2013/index.html""",
+ 'archive_atom': False,
+ 'archive_rss': False,
+ }
def set_site(self, site):
"""Set Nikola site."""
- site.register_path_handler('archive', self.archive_path)
- site.register_path_handler('archive_atom', self.archive_atom_path)
- return super(Archive, self).set_site(site)
-
- def _prepare_task(self, kw, name, lang, posts, items, template_name,
- title, deps_translatable=None):
- """Prepare an archive task."""
- # name: used to build permalink and destination
- # posts, items: posts or items; only one of them should be used,
- # the other should be None
- # template_name: name of the template to use
- # title: the (translated) title for the generated page
- # deps_translatable: dependencies (None if not added)
- assert posts is not None or items is not None
- task_cfg = [copy.copy(kw)]
- context = {}
- context["lang"] = lang
- context["title"] = title
- context["permalink"] = self.site.link("archive", name, lang)
- context["pagekind"] = ["list", "archive_page"]
- if posts is not None:
- context["posts"] = posts
- # Depend on all post metadata because it can be used in templates (Issue #1931)
- task_cfg.append([repr(p) for p in posts])
+ # Sanity checks
+ if (site.config['CREATE_MONTHLY_ARCHIVE'] and site.config['CREATE_SINGLE_ARCHIVE']) and not site.config['CREATE_FULL_ARCHIVES']:
+ raise Exception('Cannot create monthly and single archives at the same time.')
+ # Finish setup
+ self.show_list_as_subcategories_list = not site.config['CREATE_FULL_ARCHIVES']
+ self.show_list_as_index = site.config['ARCHIVES_ARE_INDEXES']
+ self.template_for_single_list = "archiveindex.tmpl" if site.config['ARCHIVES_ARE_INDEXES'] else "archive.tmpl"
+ # Determine maximum hierarchy height
+ if site.config['CREATE_DAILY_ARCHIVE'] or site.config['CREATE_FULL_ARCHIVES']:
+ self.max_levels = 3
+ elif site.config['CREATE_MONTHLY_ARCHIVE']:
+ self.max_levels = 2
+ elif site.config['CREATE_SINGLE_ARCHIVE']:
+ self.max_levels = 0
else:
- # Depend on the content of items, to rebuild if links change (Issue #1931)
- context["items"] = items
- task_cfg.append(items)
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- os.path.join(kw['output_folder'], self.site.path("archive", name, lang)),
- template_name,
- kw['filters'],
- context,
- )
-
- task_cfg = {i: x for i, x in enumerate(task_cfg)}
- if deps_translatable is not None:
- task_cfg[3] = deps_translatable
- task['uptodate'] = task['uptodate'] + [config_changed(task_cfg, 'nikola.plugins.task.archive')]
- task['basename'] = self.name
- return task
-
- def _generate_posts_task(self, kw, name, lang, posts, title, deps_translatable=None):
- """Genereate a task for an archive with posts."""
- posts = sorted(posts, key=lambda a: a.date)
- posts.reverse()
- if kw['archives_are_indexes']:
- def page_link(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return adjust_name_for_index_link(self.site.link("archive" + feed, name, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- def page_path(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return adjust_name_for_index_path(self.site.path("archive" + feed, name, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- uptodate = []
- if deps_translatable is not None:
- uptodate += [config_changed(deps_translatable, 'nikola.plugins.task.archive')]
- context = {"archive_name": name,
- "is_feed_stale": kw["is_feed_stale"],
- "pagekind": ["index", "archive_page"]}
- yield self.site.generic_index_renderer(
- lang,
- posts,
- title,
- "archiveindex.tmpl",
- context,
- kw,
- str(self.name),
- page_link,
- page_path,
- uptodate)
+ self.max_levels = 1
+ return super().set_site(site)
+
+ def get_implicit_classifications(self, lang):
+ """Return a list of classification strings which should always appear in posts_per_classification."""
+ return ['']
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ levels = [str(post.date.year).zfill(4), str(post.date.month).zfill(2), str(post.date.day).zfill(2)]
+ return ['/'.join(levels[:self.max_levels])]
+
+ def sort_classifications(self, classifications, lang, level=None):
+ """Sort the given list of classification strings."""
+ if level in (0, 1):
+ # Years or months: sort descending
+ classifications.sort()
+ classifications.reverse()
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ classification = self.extract_hierarchy(classification)
+ if len(classification) == 0:
+ return self.site.MESSAGES[lang]['Archive']
+ elif len(classification) == 1:
+ return classification[0]
+ elif len(classification) == 2:
+ if only_last_component:
+ date_str = "{month}"
+ else:
+ date_str = "{month_year}"
+ return nikola.utils.LocaleBorg().format_date_in_string(
+ date_str,
+ datetime.date(int(classification[0]), int(classification[1]), 1),
+ lang)
+ else:
+ if only_last_component:
+ return str(classification[2])
+ return nikola.utils.LocaleBorg().format_date_in_string(
+ "{month_day_year}",
+ datetime.date(int(classification[0]), int(classification[1]), int(classification[2])),
+ lang)
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ components = [self.site.config['ARCHIVE_PATH'](lang)]
+ if classification:
+ components.extend(classification)
+ add_index = 'always'
else:
- yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable)
+ components.append(self.site.config['ARCHIVE_FILENAME'](lang))
+ add_index = 'never'
+ return [_f for _f in components if _f], add_index
+
+ def extract_hierarchy(self, classification):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return classification.split('/') if classification else []
- def gen_tasks(self):
- """Generate archive tasks."""
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return '/'.join(hierarchy)
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ hierarchy = self.extract_hierarchy(classification)
kw = {
"messages": self.site.MESSAGES,
- "translations": self.site.config['TRANSLATIONS'],
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "filters": self.site.config['FILTERS'],
- "archives_are_indexes": self.site.config['ARCHIVES_ARE_INDEXES'],
- "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
- "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "create_full_archives": self.site.config['CREATE_FULL_ARCHIVES'],
- "create_daily_archive": self.site.config['CREATE_DAILY_ARCHIVE'],
- "pretty_urls": self.site.config['PRETTY_URLS'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
- "index_file": self.site.config['INDEX_FILE'],
- "generate_atom": self.site.config["GENERATE_ATOM"],
}
- self.site.scan_posts()
- yield self.group_task()
- # TODO add next/prev links for years
- if (kw['create_monthly_archive'] and kw['create_single_archive']) and not kw['create_full_archives']:
- raise Exception('Cannot create monthly and single archives at the same time.')
- for lang in kw["translations"]:
- if kw['create_single_archive'] and not kw['create_full_archives']:
- # if we are creating one single archive
- archdata = {}
- else:
- # if we are not creating one single archive, start with all years
- archdata = self.site.posts_per_year.copy()
- if kw['create_single_archive'] or kw['create_full_archives']:
- # if we are creating one single archive, or full archives
- archdata[None] = self.site.posts # for create_single_archive
-
- for year, posts in archdata.items():
- # Filter untranslated posts (Issue #1360)
- if not kw["show_untranslated_posts"]:
- posts = [p for p in posts if lang in p.translated_to]
-
- # Add archive per year or total archive
- if year:
- title = kw["messages"][lang]["Posts for year %s"] % year
- kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != year)
- else:
- title = kw["messages"][lang]["Archive"]
- kw["is_feed_stale"] = False
- deps_translatable = {}
- for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
- deps_translatable[k] = self.site.GLOBAL_CONTEXT[k](lang)
- if not kw["create_monthly_archive"] or kw["create_full_archives"]:
- yield self._generate_posts_task(kw, year, lang, posts, title, deps_translatable)
- else:
- months = set([(m.split('/')[1], self.site.link("archive", m, lang), len(self.site.posts_per_month[m])) for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
- months = sorted(list(months))
- months.reverse()
- items = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link, count] for month, link, count in months]
- yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable)
-
- if not kw["create_monthly_archive"] and not kw["create_full_archives"] and not kw["create_daily_archive"]:
- continue # Just to avoid nesting the other loop in this if
- for yearmonth, posts in self.site.posts_per_month.items():
- # Add archive per month
- year, month = yearmonth.split('/')
-
- kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m") != yearmonth)
-
- # Filter untranslated posts (via Issue #1360)
- if not kw["show_untranslated_posts"]:
- posts = [p for p in posts if lang in p.translated_to]
-
- if kw["create_monthly_archive"] or kw["create_full_archives"]:
- title = kw["messages"][lang]["Posts for {month} {year}"].format(
- year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
- yield self._generate_posts_task(kw, yearmonth, lang, posts, title)
-
- if not kw["create_full_archives"] and not kw["create_daily_archive"]:
- continue # Just to avoid nesting the other loop in this if
- # Add archive per day
- days = dict()
- for p in posts:
- if p.date.day not in days:
- days[p.date.day] = list()
- days[p.date.day].append(p)
- for day, posts in days.items():
- title = kw["messages"][lang]["Posts for {month} {day}, {year}"].format(
- year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang), day=day)
- yield self._generate_posts_task(kw, yearmonth + '/{0:02d}'.format(day), lang, posts, title)
-
- if not kw['create_single_archive'] and not kw['create_full_archives']:
- # And an "all your years" page for yearly and monthly archives
- if "is_feed_stale" in kw:
- del kw["is_feed_stale"]
- years = list(self.site.posts_per_year.keys())
- years.sort(reverse=True)
- kw['years'] = years
- for lang in kw["translations"]:
- items = [(y, self.site.link("archive", y, lang), len(self.site.posts_per_year[y])) for y in years]
- yield self._prepare_task(kw, None, lang, None, items, "list.tmpl", kw["messages"][lang]["Archive"])
-
- def archive_path(self, name, lang, is_feed=False):
- """Link to archive path, name is the year.
-
- Example:
-
- link://archive/2013 => /archives/2013/index.html
- """
- if is_feed:
- extension = ".atom"
- archive_file = os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0] + extension
- index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
- else:
- archive_file = self.site.config['ARCHIVE_FILENAME']
- index_file = self.site.config['INDEX_FILE']
- if name:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['ARCHIVE_PATH'], name,
- index_file] if _f]
+ page_kind = "list"
+ if self.show_list_as_index:
+ if not self.show_list_as_subcategories_list or len(hierarchy) == self.max_levels:
+ page_kind = "index"
+ if len(hierarchy) == 0:
+ title = kw["messages"][lang]["Archive"]
+ elif len(hierarchy) == 1:
+ title = kw["messages"][lang]["Posts for year %s"] % hierarchy[0]
+ elif len(hierarchy) == 2:
+ title = nikola.utils.LocaleBorg().format_date_in_string(
+ kw["messages"][lang]["Posts for {month_year}"],
+ datetime.date(int(hierarchy[0]), int(hierarchy[1]), 1),
+ lang)
+ elif len(hierarchy) == 3:
+ title = nikola.utils.LocaleBorg().format_date_in_string(
+ kw["messages"][lang]["Posts for {month_day_year}"],
+ datetime.date(int(hierarchy[0]), int(hierarchy[1]), int(hierarchy[2])),
+ lang)
else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['ARCHIVE_PATH'],
- archive_file] if _f]
+ raise Exception("Cannot interpret classification {}!".format(repr(classification)))
- def archive_atom_path(self, name, lang):
- """Link to atom archive path, name is the year.
-
- Example:
+ context = {
+ "title": title,
+ "pagekind": [page_kind, "archive_page"],
+ "create_archive_navigation": self.site.config["CREATE_ARCHIVE_NAVIGATION"],
+ "archive_name": classification
+ }
- link://archive_atom/2013 => /archives/2013/index.atom
- """
- return self.archive_path(name, lang, is_feed=True)
+ # Generate links for hierarchies
+ if context["create_archive_navigation"]:
+ if hierarchy:
+ # Up level link makes sense only if this is not the top-level
+ # page (hierarchy is empty)
+ parent = '/'.join(hierarchy[:-1])
+ context["up_archive"] = self.site.link('archive', parent, lang)
+ context["up_archive_name"] = self.get_classification_friendly_name(parent, lang)
+ else:
+ context["up_archive"] = None
+ context["up_archive_name"] = None
+
+ nodelevel = len(hierarchy)
+ flat_samelevel = self.archive_navigation[lang][nodelevel]
+ idx = flat_samelevel.index(classification)
+ if idx == -1:
+ raise Exception("Cannot find classification {0} in flat hierarchy!".format(classification))
+ previdx, nextidx = idx - 1, idx + 1
+ # If the previous index is -1, or the next index is 1, the previous/next archive does not exist.
+ context["previous_archive"] = self.site.link('archive', flat_samelevel[previdx], lang) if previdx != -1 else None
+ context["previous_archive_name"] = self.get_classification_friendly_name(flat_samelevel[previdx], lang) if previdx != -1 else None
+ context["next_archive"] = self.site.link('archive', flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None
+ context["next_archive_name"] = self.get_classification_friendly_name(flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None
+ context["archive_nodelevel"] = nodelevel
+ context["has_archive_navigation"] = bool(context["previous_archive"] or context["up_archive"] or context["next_archive"])
+ else:
+ context["has_archive_navigation"] = False
+ kw.update(context)
+ return context, kw
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ # Build a lookup table for archive navigation, if we’ll need one.
+ if self.site.config['CREATE_ARCHIVE_NAVIGATION']:
+ if flat_hierarchy_per_lang is None:
+ raise ValueError('Archives need flat_hierarchy_per_lang')
+ self.archive_navigation = {}
+ for lang, flat_hierarchy in flat_hierarchy_per_lang.items():
+ self.archive_navigation[lang] = defaultdict(list)
+ for node in flat_hierarchy:
+ if not self.site.config["SHOW_UNTRANSLATED_POSTS"]:
+ if not [x for x in posts_per_classification_per_language[lang][node.classification_name] if x.is_translation_available(lang)]:
+ continue
+ self.archive_navigation[lang][len(node.classification_path)].append(node.classification_name)
+
+ # We need to sort it. Natsort means it’s year 10000 compatible!
+ for k, v in self.archive_navigation[lang].items():
+ self.archive_navigation[lang][k] = natsort.natsorted(v, alg=natsort.ns.F | natsort.ns.IC)
+
+ return super().postprocess_posts_per_classification(posts_per_classification_per_language, flat_hierarchy_per_lang, hierarchy_lookup_per_lang)
+
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ return classification == '' or len(post_list) > 0
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same classification in other languages."""
+ return [(other_lang, classification) for other_lang, lookup in classifications_per_language.items() if classification in lookup and other_lang != lang]
diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin
index 3fc4ef2..19e687c 100644
--- a/nikola/plugins/task/authors.plugin
+++ b/nikola/plugins/task/authors.plugin
@@ -1,5 +1,5 @@
[Core]
-Name = render_authors
+Name = classify_authors
Module = authors
[Documentation]
@@ -8,3 +8,5 @@ Version = 0.1
Website = http://getnikola.com
Description = Render the author pages and feeds.
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py
index ec61800..24fe650 100644
--- a/nikola/plugins/task/authors.py
+++ b/nikola/plugins/task/authors.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2015-2016 Juanjo Conti and others.
+# Copyright © 2015-2020 Juanjo Conti and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,301 +26,134 @@
"""Render the author pages and feeds."""
-from __future__ import unicode_literals
-import os
-import natsort
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from collections import defaultdict
-from blinker import signal
-
-from nikola.plugin_categories import Task
+from nikola.plugin_categories import Taxonomy
from nikola import utils
-class RenderAuthors(Task):
- """Render the author pages and feeds."""
-
- name = "render_authors"
- posts_per_author = None
-
- def set_site(self, site):
- """Set Nikola site."""
- self.generate_author_pages = False
- if site.config["ENABLE_AUTHOR_PAGES"]:
- site.register_path_handler('author_index', self.author_index_path)
- site.register_path_handler('author', self.author_path)
- site.register_path_handler('author_atom', self.author_atom_path)
- site.register_path_handler('author_rss', self.author_rss_path)
- signal('scanned').connect(self.posts_scanned)
- return super(RenderAuthors, self).set_site(site)
-
- def posts_scanned(self, event):
- """Called after posts are scanned via signal."""
- self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and len(self._posts_per_author()) > 1
- self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages
-
- def gen_tasks(self):
- """Render the author pages and feeds."""
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "blog_title": self.site.config["BLOG_TITLE"],
- "site_url": self.site.config["SITE_URL"],
- "base_url": self.site.config["BASE_URL"],
- "messages": self.site.MESSAGES,
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "filters": self.site.config['FILTERS'],
- 'author_path': self.site.config['AUTHOR_PATH'],
- "author_pages_are_indexes": self.site.config['AUTHOR_PAGES_ARE_INDEXES'],
- "generate_rss": self.site.config['GENERATE_RSS'],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "tzinfo": self.site.tzinfo,
- "pretty_urls": self.site.config['PRETTY_URLS'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
- "index_file": self.site.config['INDEX_FILE'],
- }
-
- self.site.scan_posts()
- yield self.group_task()
-
- if self.generate_author_pages:
- yield self.list_authors_page(kw)
-
- if not self._posts_per_author(): # this may be self.site.posts_per_author
- return
-
- author_list = list(self._posts_per_author().items())
+class ClassifyAuthors(Taxonomy):
+ """Classify the posts by authors."""
- def render_lists(author, posts):
- """Render author pages as RSS files and lists/indexes."""
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for lang in kw["translations"]:
- if kw["show_untranslated_posts"]:
- filtered_posts = post_list
- else:
- filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
- if kw["generate_rss"]:
- yield self.author_rss(author, lang, filtered_posts, kw)
- # Render HTML
- if kw['author_pages_are_indexes']:
- yield self.author_page_as_index(author, lang, filtered_posts, kw)
- else:
- yield self.author_page_as_list(author, lang, filtered_posts, kw)
+ name = "classify_authors"
- for author, posts in author_list:
- for task in render_lists(author, posts):
- yield task
+ classification_name = "author"
+ overview_page_variable_name = "authors"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = False
+ template_for_classification_overview = "authors.tmpl"
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = False
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'author_index': """ Link to the authors index.
- def _create_authors_page(self, kw):
- """Create a global "all authors" page for each language."""
- template_name = "authors.tmpl"
- kw = kw.copy()
- for lang in kw["translations"]:
- authors = natsort.natsorted([author for author in self._posts_per_author().keys()],
- alg=natsort.ns.F | natsort.ns.IC)
- has_authors = (authors != [])
- kw['authors'] = authors
- output_name = os.path.join(
- kw['output_folder'], self.site.path('author_index', None, lang))
- context = {}
- if has_authors:
- context["title"] = kw["messages"][lang]["Authors"]
- context["items"] = [(author, self.site.link("author", author, lang)) for author
- in authors]
- context["description"] = context["title"]
- else:
- context["items"] = None
- context["permalink"] = self.site.link("author_index", None, lang)
- context["pagekind"] = ["list", "authors_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:page')]
- task['basename'] = str(self.name)
- yield task
+ Example:
- def list_authors_page(self, kw):
- """Create a global "all authors" page for each language."""
- yield self._create_authors_page(kw)
+ link://authors/ => /authors/index.html""",
+ 'author': """Link to an author's page.
- def _get_title(self, author):
- return author
+ Example:
- def _get_description(self, author, lang):
- descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS']
- return descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None
+ link://author/joe => /authors/joe.html""",
+ 'author_atom': """Link to an author's Atom feed.
- def author_page_as_index(self, author, lang, post_list, kw):
- """Render a sort of index page collection using only this author's posts."""
- kind = "author"
+Example:
- def page_link(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_link(self.site.link(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension)
+link://author_atom/joe => /authors/joe.atom""",
+ 'author_rss': """Link to an author's RSS feed.
- def page_path(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_path(self.site.path(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension)
+Example:
- context_source = {}
- title = self._get_title(author)
- if kw["generate_rss"]:
- # On a author page, the feeds include the author's feeds
- rss_link = ("""<link rel="alternate" type="application/rss+xml" """
- """title="RSS for author """
- """{0} ({1})" href="{2}">""".format(
- title, lang, self.site.link(kind + "_rss", author, lang)))
- context_source['rss_link'] = rss_link
- context_source["author"] = title
- indexes_title = kw["messages"][lang]["Posts by %s"] % title
- context_source["description"] = self._get_description(author, lang)
- context_source["pagekind"] = ["index", "author_page"]
- template_name = "authorindex.tmpl"
+link://author_rss/joe => /authors/joe.xml""",
+ }
- yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path)
+ def set_site(self, site):
+ """Set Nikola site."""
+ super().set_site(site)
+ self.show_list_as_index = site.config['AUTHOR_PAGES_ARE_INDEXES']
+ self.more_than_one_classifications_per_post = site.config.get('MULTIPLE_AUTHORS_PER_POST', False)
+ self.template_for_single_list = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl"
+ self.translation_manager = utils.ClassificationTranslationManager()
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ if not self.site.config["ENABLE_AUTHOR_PAGES"]:
+ return False
+ if lang is not None:
+ return self.generate_author_pages
+ return True
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ if self.more_than_one_classifications_per_post:
+ return post.authors(lang=lang)
+ else:
+ return [post.author(lang=lang)]
- def author_page_as_list(self, author, lang, post_list, kw):
- """Render a single flat link list with this author's posts."""
- kind = "author"
- template_name = "author.tmpl"
- output_name = os.path.join(kw['output_folder'], self.site.path(
- kind, author, lang))
- context = {}
- context["lang"] = lang
- title = self._get_title(author)
- context["author"] = title
- context["title"] = kw["messages"][lang]["Posts by %s"] % title
- context["posts"] = post_list
- context["permalink"] = self.site.link(kind, author, lang)
- context["kind"] = kind
- context["description"] = self._get_description(author, lang)
- context["pagekind"] = ["list", "author_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:list')]
- task['basename'] = str(self.name)
- yield task
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return classification
- def author_rss(self, author, lang, posts, kw):
- """Create a RSS feed for a single author in a given language."""
- kind = "author"
- # Render RSS
- output_name = os.path.normpath(
- os.path.join(kw['output_folder'],
- self.site.path(kind + "_rss", author, lang)))
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", author, lang).lstrip('/'))
- deps = []
- deps_uptodate = []
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for post in post_list:
- deps += post.deps(lang)
- deps_uptodate += post.deps_uptodate(lang)
- task = {
- 'basename': str(self.name),
- 'name': output_name,
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(author)),
- kw["site_url"], None, post_list,
- output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
- feed_url, None, kw["feed_link_append_query"]))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.authors:rss')] + deps_uptodate,
- 'task_dep': ['render_posts'],
- }
- return utils.apply_filters(task, kw['filters'])
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ path = self.site.config['AUTHOR_PATH'](lang)
+ return [component for component in path.split('/') if component], 'always'
- def slugify_author_name(self, name, lang=None):
- """Slugify an author name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderAuthors.slugify_author_name() called without language!")
- lang = ''
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
if self.site.config['SLUG_AUTHOR_PATH']:
- name = utils.slugify(name, lang)
- return name
-
- def author_index_path(self, name, lang):
- """Link to the author's index.
-
- Example:
-
- link://authors/ => /authors/index.html
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.site.config['INDEX_FILE']] if _f]
-
- def author_path(self, name, lang):
- """Link to an author's page.
-
- Example:
-
- link://author/joe => /authors/joe.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.slugify_author_name(name, lang),
- self.site.config['INDEX_FILE']] if _f]
+ slug = utils.slugify(classification, lang)
else:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'],
- self.slugify_author_name(name, lang) + ".html"] if _f]
-
- def author_atom_path(self, name, lang):
- """Link to an author's Atom feed.
-
- Example:
-
- link://author_atom/joe => /authors/joe.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".atom"] if
- _f]
-
- def author_rss_path(self, name, lang):
- """Link to an author's RSS feed.
-
- Example:
-
- link://author_rss/joe => /authors/joe.rss
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".xml"] if
- _f]
+ slug = classification
+ return [self.site.config['AUTHOR_PATH'](lang), slug], 'auto'
- def _add_extension(self, path, extension):
- path[-1] += extension
- return path
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ "messages": self.site.MESSAGES,
+ }
+ context = {
+ "title": kw["messages"][lang]["Authors"],
+ "description": kw["messages"][lang]["Authors"],
+ "permalink": self.site.link("author_index", None, lang),
+ "pagekind": ["list", "authors_page"],
+ }
+ kw.update(context)
+ return context, kw
- def _posts_per_author(self):
- """Return a dict of posts per author."""
- if self.posts_per_author is None:
- self.posts_per_author = defaultdict(list)
- for post in self.site.timeline:
- if post.is_post:
- self.posts_per_author[post.author()].append(post)
- return self.posts_per_author
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS']
+ kw = {
+ "messages": self.site.MESSAGES,
+ }
+ context = {
+ "author": classification,
+ "title": kw["messages"][lang]["Posts by %s"] % classification,
+ "description": descriptions[lang][classification] if lang in descriptions and classification in descriptions[lang] else None,
+ "pagekind": ["index" if self.show_list_as_index else "list", "author_page"],
+ }
+ kw.update(context)
+ return context, kw
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same author in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ more_than_one = False
+ for lang, posts_per_author in posts_per_classification_per_language.items():
+ authors = set()
+ for author, posts in posts_per_author.items():
+ for post in posts:
+ if not self.site.config["SHOW_UNTRANSLATED_POSTS"] and not post.is_translation_available(lang):
+ continue
+ authors.add(author)
+ if len(authors) > 1:
+ more_than_one = True
+ self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one
+ self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages
+ self.translation_manager.add_defaults(posts_per_classification_per_language)
diff --git a/nikola/plugins/task/bundles.plugin b/nikola/plugins/task/bundles.plugin
index b5bf6e4..939065b 100644
--- a/nikola/plugins/task/bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -6,8 +6,8 @@ module = bundles
author = Roberto Alsina
version = 1.0
website = https://getnikola.com/
-description = Theme bundles using WebAssets
+description = Bundle assets
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/bundles.py b/nikola/plugins/task/bundles.py
index b33d8e0..aa4ce78 100644
--- a/nikola/plugins/task/bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,38 +24,26 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Bundle assets using WebAssets."""
+"""Bundle assets."""
-from __future__ import unicode_literals
+import configparser
+import io
+import itertools
import os
-
-try:
- import webassets
-except ImportError:
- webassets = None # NOQA
+import shutil
from nikola.plugin_categories import LateTask
from nikola import utils
class BuildBundles(LateTask):
- """Bundle assets using WebAssets."""
+ """Bundle assets."""
name = "create_bundles"
- def set_site(self, site):
- """Set Nikola site."""
- self.logger = utils.get_logger('bundles', utils.STDERR_HANDLER)
- if webassets is None and site.config['USE_BUNDLES']:
- utils.req_missing(['webassets'], 'USE_BUNDLES', optional=True)
- self.logger.warn('Setting USE_BUNDLES to False.')
- site.config['USE_BUNDLES'] = False
- site._GLOBAL_CONTEXT['use_bundles'] = False
- super(BuildBundles, self).set_site(site)
-
def gen_tasks(self):
- """Bundle assets using WebAssets."""
+ """Bundle assets."""
kw = {
'filters': self.site.config['FILTERS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
@@ -69,28 +57,21 @@ class BuildBundles(LateTask):
def build_bundle(output, inputs):
out_dir = os.path.join(kw['output_folder'],
os.path.dirname(output))
- inputs = [os.path.relpath(i, out_dir) for i in inputs if os.path.isfile(i)]
- cache_dir = os.path.join(kw['cache_folder'], 'webassets')
- utils.makedirs(cache_dir)
- env = webassets.Environment(out_dir, os.path.dirname(output),
- cache=cache_dir)
- if inputs:
- bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
- env.register(output, bundle)
- # This generates the file
- try:
- env[output].urls()
- except Exception as e:
- self.logger.error("Failed to build bundles.")
- self.logger.exception(e)
- self.logger.notice("Try running ``nikola clean`` and building again.")
- else:
- with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'):
- pass # Create empty file
+ inputs = [
+ os.path.join(
+ out_dir,
+ os.path.relpath(i, out_dir))
+ for i in inputs if os.path.isfile(i)
+ ]
+ with open(os.path.join(out_dir, os.path.basename(output)), 'wb+') as out_fh:
+ for i in inputs:
+ with open(i, 'rb') as in_fh:
+ shutil.copyfileobj(in_fh, out_fh)
+ out_fh.write(b'\n')
yield self.group_task()
- if (webassets is not None and self.site.config['USE_BUNDLES'] is not
- False):
+
+ if self.site.config['USE_BUNDLES']:
for name, _files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
@@ -127,19 +108,17 @@ class BuildBundles(LateTask):
def get_theme_bundles(themes):
"""Given a theme chain, return the bundle definitions."""
- bundles = {}
for theme_name in themes:
bundles_path = os.path.join(
utils.get_theme_path(theme_name), 'bundles')
if os.path.isfile(bundles_path):
- with open(bundles_path) as fd:
- for line in fd:
- try:
- name, files = line.split('=')
- files = [f.strip() for f in files.split(',')]
- bundles[name.strip().replace('/', os.sep)] = files
- except ValueError:
- # for empty lines
- pass
- break
- return bundles
+ config = configparser.ConfigParser()
+ header = io.StringIO('[bundles]\n')
+ with open(bundles_path, 'rt') as fd:
+ config.read_file(itertools.chain(header, fd))
+ bundles = {}
+ for name, files in config['bundles'].items():
+ name = name.strip().replace('/', os.sep)
+ files = [f.strip() for f in files.split(',') if f.strip()]
+ bundles[name] = files
+ return bundles
diff --git a/nikola/plugins/task/categories.plugin b/nikola/plugins/task/categories.plugin
new file mode 100644
index 0000000..be2bb79
--- /dev/null
+++ b/nikola/plugins/task/categories.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = classify_categories
+module = categories
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Render the category pages and feeds.
+
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/categories.py b/nikola/plugins/task/categories.py
new file mode 100644
index 0000000..68f9caa
--- /dev/null
+++ b/nikola/plugins/task/categories.py
@@ -0,0 +1,248 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the category pages and feeds."""
+
+import os
+
+from nikola.plugin_categories import Taxonomy
+from nikola import utils, hierarchy_utils
+
+
+class ClassifyCategories(Taxonomy):
+ """Classify the posts by categories."""
+
+ name = "classify_categories"
+
+ classification_name = "category"
+ overview_page_variable_name = "categories"
+ overview_page_items_variable_name = "cat_items"
+ overview_page_hierarchy_variable_name = "cat_hierarchy"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = True
+ include_posts_into_hierarchy_root = False
+ show_list_as_subcategories_list = False
+ template_for_classification_overview = "tags.tmpl"
+ always_disable_rss = False
+ always_disable_atom = False
+ apply_to_posts = True
+ apply_to_pages = False
+ minimum_post_count_per_classification_in_overview = 1
+ omit_empty_classifications = True
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'category_index': """A link to the category index.
+
+Example:
+
+link://category_index => /categories/index.html""",
+ 'category': """A link to a category. Takes page number as optional keyword argument.
+
+Example:
+
+link://category/dogs => /categories/dogs.html""",
+ 'category_atom': """A link to a category's Atom feed.
+
+Example:
+
+link://category_atom/dogs => /categories/dogs.atom""",
+ 'category_rss': """A link to a category's RSS feed.
+
+Example:
+
+link://category_rss/dogs => /categories/dogs.xml""",
+ }
+
+ def set_site(self, site):
+ """Set site, which is a Nikola instance."""
+ super().set_site(site)
+ self.show_list_as_index = self.site.config['CATEGORY_PAGES_ARE_INDEXES']
+ self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl"
+ self.translation_manager = utils.ClassificationTranslationManager()
+
+ # Needed to undo names for CATEGORY_PAGES_FOLLOW_DESTPATH
+ self.destpath_names_reverse = {}
+ for lang in self.site.config['TRANSLATIONS']:
+ self.destpath_names_reverse[lang] = {}
+ for k, v in self.site.config['CATEGORY_DESTPATH_NAMES'](lang).items():
+ self.destpath_names_reverse[lang][v] = k
+ self.destpath_names_reverse = utils.TranslatableSetting(
+ '_CATEGORY_DESTPATH_NAMES_REVERSE', self.destpath_names_reverse,
+ self.site.config['TRANSLATIONS'])
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return True
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ cat = post.meta('category', lang=lang).strip()
+ return [cat] if cat else []
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ classification = self.extract_hierarchy(classification)
+ return classification[-1] if classification else ''
+
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ if self.site.config['CATEGORIES_INDEX_PATH'](lang):
+ path = self.site.config['CATEGORIES_INDEX_PATH'](lang)
+ append_index = 'never'
+ else:
+ path = self.site.config['CATEGORY_PATH'](lang)
+ append_index = 'always'
+ return [component for component in path.split('/') if component], append_index
+
+ def slugify_tag_name(self, name, lang):
+ """Slugify a tag name."""
+ if self.site.config['SLUG_TAG_PATH']:
+ name = utils.slugify(name, lang)
+ return name
+
+ def slugify_category_name(self, path, lang):
+ """Slugify a category name."""
+ if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']:
+ path = path[-1:] # only the leaf
+ result = [self.slugify_tag_name(part, lang) for part in path]
+ result[0] = self.site.config['CATEGORY_PREFIX'] + result[0]
+ if not self.site.config['PRETTY_URLS']:
+ result = ['-'.join(result)]
+ return result
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ cat_string = '/'.join(classification)
+ classification_raw = classification # needed to undo CATEGORY_DESTPATH_NAMES
+ destpath_names_reverse = self.destpath_names_reverse(lang)
+ if self.site.config['CATEGORY_PAGES_FOLLOW_DESTPATH']:
+ base_dir = None
+ for post in self.site.posts_per_category[cat_string]:
+ if post.category_from_destpath:
+ base_dir = post.folder_base(lang)
+ # Handle CATEGORY_DESTPATH_NAMES
+ if cat_string in destpath_names_reverse:
+ cat_string = destpath_names_reverse[cat_string]
+ classification_raw = cat_string.split('/')
+ break
+
+ if not self.site.config['CATEGORY_DESTPATH_TRIM_PREFIX']:
+ # If prefixes are not trimmed, we'll already have the base_dir in classification_raw
+ base_dir = ''
+
+ if base_dir is None:
+ # fallback: first POSTS entry + classification
+ base_dir = self.site.config['POSTS'][0][1]
+ base_dir_list = base_dir.split(os.sep)
+ sub_dir = [self.slugify_tag_name(part, lang) for part in classification_raw]
+ return [_f for _f in (base_dir_list + sub_dir) if _f], 'auto'
+ else:
+ return [_f for _f in [self.site.config['CATEGORY_PATH'](lang)] if _f] + self.slugify_category_name(
+ classification, lang), 'auto'
+
+ def extract_hierarchy(self, classification):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return hierarchy_utils.parse_escaped_hierarchical_category_name(classification)
+
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return hierarchy_utils.join_hierarchical_category_path(hierarchy)
+
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ 'category_path': self.site.config['CATEGORY_PATH'],
+ 'category_prefix': self.site.config['CATEGORY_PREFIX'],
+ "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
+ "tzinfo": self.site.tzinfo,
+ "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
+ "category_titles": self.site.config['CATEGORY_TITLES'],
+ }
+ context = {
+ "title": self.site.MESSAGES[lang]["Categories"],
+ "description": self.site.MESSAGES[lang]["Categories"],
+ "pagekind": ["list", "tags_page"],
+ }
+ kw.update(context)
+ return context, kw
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ cat_path = self.extract_hierarchy(classification)
+ kw = {
+ 'category_path': self.site.config['CATEGORY_PATH'],
+ 'category_prefix': self.site.config['CATEGORY_PREFIX'],
+ "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
+ "tzinfo": self.site.tzinfo,
+ "category_descriptions": self.site.config['CATEGORY_DESCRIPTIONS'],
+ "category_titles": self.site.config['CATEGORY_TITLES'],
+ }
+ posts = self.site.posts_per_classification[self.classification_name][lang]
+ if node is None:
+ children = []
+ else:
+ children = [child for child in node.children if len([post for post in posts.get(child.classification_name, []) if self.site.config['SHOW_UNTRANSLATED_POSTS'] or post.is_translation_available(lang)]) > 0]
+ subcats = [(child.name, self.site.link(self.classification_name, child.classification_name, lang)) for child in children]
+ friendly_name = self.get_classification_friendly_name(classification, lang)
+ context = {
+ "title": self.site.config['CATEGORY_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % friendly_name),
+ "description": self.site.config['CATEGORY_DESCRIPTIONS'].get(lang, {}).get(classification),
+ "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"],
+ "tag": friendly_name,
+ "category": classification,
+ "category_path": cat_path,
+ "subcategories": subcats,
+ }
+ kw.update(context)
+ return context, kw
+
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same category in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
+
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ self.translation_manager.read_from_config(self.site, 'CATEGORY', posts_per_classification_per_language, False)
+
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ if self.site.config["CATEGORY_PAGES_FOLLOW_DESTPATH"]:
+ # In destpath mode, allow users to replace the default category index with a custom page.
+ classification_hierarchy = self.extract_hierarchy(classification)
+ dest_list, _ = self.get_path(classification_hierarchy, lang)
+ short_destination = os.sep.join(dest_list + [self.site.config["INDEX_FILE"]])
+ if short_destination in self.site.post_per_file:
+ return False
+ return True
+
+ def should_generate_atom_for_classification_page(self, classification, post_list, lang):
+ """Only generates Atom feed for list of posts for classification if this function returns True."""
+ return True
+
+ def should_generate_rss_for_classification_page(self, classification, post_list, lang):
+ """Only generates RSS feed for list of posts for classification if this function returns True."""
+ return True
diff --git a/nikola/plugins/task/copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index ddd38df..b63581d 100644
--- a/nikola/plugins/task/copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy theme assets into output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/copy_assets.py b/nikola/plugins/task/copy_assets.py
index 4ed7414..c6d32c7 100644
--- a/nikola/plugins/task/copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,11 +26,11 @@
"""Copy theme assets into output."""
-from __future__ import unicode_literals
import io
import os
+from nikola.packages.pygments_better_html import BetterHtmlFormatter
from nikola.plugin_categories import Task
from nikola import utils
@@ -48,13 +48,19 @@ class CopyAssets(Task):
"""
kw = {
"themes": self.site.THEMES,
+ "translations": self.site.translations,
"files_folders": self.site.config['FILES_FOLDERS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
- "code.css_selectors": 'pre.code',
+ "code.css_selectors": ['pre.code', '.code .codetable', '.highlight pre'],
+ "code.css_wrappers": ['.highlight', '.code'],
"code.css_head": '/* code.css file generated by Nikola */\n',
- "code.css_close": "\ntable.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}\n",
+ "code.css_close": (
+ "\ntable.codetable, table.highlighttable { width: 100%;}\n"
+ ".codetable td.linenos, td.linenos { text-align: right; width: 3.5em; "
+ "padding-right: 0.5em; background: rgba(127, 127, 127, 0.2) }\n"
+ ".codetable td.code, td.code { padding-left: 0.5em; }\n"),
}
tasks = {}
code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
@@ -63,11 +69,20 @@ class CopyAssets(Task):
files_folders=kw['files_folders'], output_dir=None)
yield self.group_task()
+ main_theme = utils.get_theme_path(kw['themes'][0])
+ theme_ini = utils.parse_theme_meta(main_theme)
+ if theme_ini:
+ ignored_assets = theme_ini.get("Nikola", "ignored_assets", fallback='').split(',')
+ ignored_assets = [os.path.normpath(asset_name.strip()) for asset_name in ignored_assets]
+ else:
+ ignored_assets = []
+
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
- if task['name'] in tasks:
+ asset_name = os.path.relpath(task['name'], dst)
+ if task['name'] in tasks or asset_name in ignored_assets:
continue
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.copy_assets')]
@@ -79,18 +94,18 @@ class CopyAssets(Task):
yield utils.apply_filters(task, kw['filters'])
# Check whether or not there is a code.css file around.
- if not code_css_input:
+ if not code_css_input and kw['code_color_scheme']:
def create_code_css():
- from pygments.formatters import get_formatter_by_name
- formatter = get_formatter_by_name('html', style=kw["code_color_scheme"])
+ formatter = BetterHtmlFormatter(style=kw["code_color_scheme"])
utils.makedirs(os.path.dirname(code_css_path))
- with io.open(code_css_path, 'w+', encoding='utf8') as outf:
+ with io.open(code_css_path, 'w+', encoding='utf-8') as outf:
outf.write(kw["code.css_head"])
- outf.write(formatter.get_style_defs(kw["code.css_selectors"]))
+ outf.write(formatter.get_style_defs(
+ kw["code.css_selectors"], kw["code.css_wrappers"]))
outf.write(kw["code.css_close"])
if os.path.exists(code_css_path):
- with io.open(code_css_path, 'r', encoding='utf-8') as fh:
+ with io.open(code_css_path, 'r', encoding='utf-8-sig') as fh:
testcontents = fh.read(len(kw["code.css_head"])) == kw["code.css_head"]
else:
testcontents = False
diff --git a/nikola/plugins/task/copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index e4bb1cf..45c2253 100644
--- a/nikola/plugins/task/copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy static files into the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/copy_files.py b/nikola/plugins/task/copy_files.py
index 6f6cfb8..26364d4 100644
--- a/nikola/plugins/task/copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/task/galleries.plugin b/nikola/plugins/task/galleries.plugin
index 2064e68..d06e117 100644
--- a/nikola/plugins/task/galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create image galleries automatically.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
index edfd33d..b8ac9ee 100644
--- a/nikola/plugins/task/galleries.py
+++ b/nikola/plugins/task/galleries.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,32 +26,29 @@
"""Render image galleries."""
-from __future__ import unicode_literals
import datetime
import glob
import io
import json
import mimetypes
import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
+from collections import OrderedDict
+from urllib.parse import urljoin
import natsort
-try:
- from PIL import Image # NOQA
-except ImportError:
- import Image as _Image
- Image = _Image
-
import PyRSS2Gen as rss
+from PIL import Image
from nikola.plugin_categories import Task
from nikola import utils
from nikola.image_processing import ImageProcessor
from nikola.post import Post
+try:
+ from ruamel.yaml import YAML
+except ImportError:
+ YAML = None
+
_image_size_cache = {}
@@ -63,12 +60,11 @@ class Galleries(Task, ImageProcessor):
def set_site(self, site):
"""Set Nikola site."""
+ super().set_site(site)
site.register_path_handler('gallery', self.gallery_path)
site.register_path_handler('gallery_global', self.gallery_global_path)
site.register_path_handler('gallery_rss', self.gallery_rss_path)
- self.logger = utils.get_logger('render_galleries', utils.STDERR_HANDLER)
-
self.kw = {
'thumbnail_size': site.config['THUMBNAIL_SIZE'],
'max_image_size': site.config['MAX_IMAGE_SIZE'],
@@ -87,6 +83,11 @@ class Galleries(Task, ImageProcessor):
'generate_rss': site.config['GENERATE_RSS'],
'preserve_exif_data': site.config['PRESERVE_EXIF_DATA'],
'exif_whitelist': site.config['EXIF_WHITELIST'],
+ 'preserve_icc_profiles': site.config['PRESERVE_ICC_PROFILES'],
+ 'index_path': site.config['INDEX_PATH'],
+ 'disable_indexes': site.config['DISABLE_INDEXES'],
+ 'galleries_use_thumbnail': site.config['GALLERIES_USE_THUMBNAIL'],
+ 'galleries_default_thumbnail': site.config['GALLERIES_DEFAULT_THUMBNAIL'],
}
# Verify that no folder in GALLERY_FOLDERS appears twice
@@ -104,8 +105,6 @@ class Galleries(Task, ImageProcessor):
# Create self.gallery_links
self.create_galleries_paths()
- return super(Galleries, self).set_site(site)
-
def _find_gallery_path(self, name):
# The system using self.proper_gallery_links and self.improper_gallery_links
# is similar as in listings.py.
@@ -165,7 +164,7 @@ class Galleries(Task, ImageProcessor):
gallery_path = self._find_gallery_path(name)
return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] +
gallery_path.split(os.sep) +
- ['rss.xml'] if _f]
+ [self.site.config['RSS_FILENAME_BASE'](lang) + self.site.config['RSS_EXTENSION']] if _f]
def gen_tasks(self):
"""Render image galleries."""
@@ -173,7 +172,7 @@ class Galleries(Task, ImageProcessor):
self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- self.kw['||template_hooks|{0}||'.format(k)] = v._items
+ self.kw['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
self.site.scan_posts()
yield self.group_task()
@@ -223,6 +222,12 @@ class Galleries(Task, ImageProcessor):
self.kw[k] = self.site.GLOBAL_CONTEXT[k](lang)
context = {}
+
+ # Do we have a metadata file?
+ meta_path, order, captions, img_metadata = self.find_metadata(gallery, lang)
+ context['meta_path'] = meta_path
+ context['order'] = order
+ context['captions'] = captions
context["lang"] = lang
if post:
context["title"] = post.title(lang)
@@ -232,7 +237,20 @@ class Galleries(Task, ImageProcessor):
image_name_list = [os.path.basename(p) for p in image_list]
- if self.kw['use_filename_as_title']:
+ if captions:
+ img_titles = []
+ for fn in image_name_list:
+ if fn in captions:
+ img_titles.append(captions[fn])
+ else:
+ if self.kw['use_filename_as_title']:
+ img_titles.append(fn)
+ else:
+ img_titles.append('')
+ self.logger.debug(
+ "Image {0} found in gallery but not listed in {1}".
+ format(fn, context['meta_path']))
+ elif self.kw['use_filename_as_title']:
img_titles = []
for fn in image_name_list:
name_without_ext = os.path.splitext(os.path.basename(fn))[0]
@@ -248,6 +266,7 @@ class Galleries(Task, ImageProcessor):
folders = []
# Generate friendly gallery names
+ fpost_list = []
for path, folder in folder_list:
fpost = self.parse_index(path, input_folder, output_folder)
if fpost:
@@ -256,8 +275,17 @@ class Galleries(Task, ImageProcessor):
ft = folder
if not folder.endswith('/'):
folder += '/'
- folders.append((folder, ft))
+ # TODO: This is to keep compatibility with user's custom gallery.tmpl
+ # To be removed in v9 someday
+ if self.kw['galleries_use_thumbnail']:
+ folders.append((folder, ft, fpost))
+ if fpost:
+ fpost_list.append(fpost.source_path)
+ else:
+ folders.append((folder, ft))
+
+ context["gallery_path"] = gallery
context["folders"] = natsort.natsorted(
folders, alg=natsort.ns.F | natsort.ns.IC)
context["crumbs"] = utils.get_crumbs(gallery, index_folder=self, lang=lang)
@@ -265,6 +293,7 @@ class Galleries(Task, ImageProcessor):
context["enable_comments"] = self.kw['comments_in_galleries']
context["thumbnail_size"] = self.kw["thumbnail_size"]
context["pagekind"] = ["gallery_front"]
+ context["galleries_use_thumbnail"] = self.kw['galleries_use_thumbnail']
if post:
yield {
@@ -291,7 +320,7 @@ class Galleries(Task, ImageProcessor):
yield utils.apply_filters({
'basename': self.name,
'name': dst,
- 'file_dep': file_dep,
+ 'file_dep': file_dep + dest_img_list + fpost_list,
'targets': [dst],
'actions': [
(self.render_gallery_index, (
@@ -301,7 +330,7 @@ class Galleries(Task, ImageProcessor):
dest_img_list,
img_titles,
thumbs,
- file_dep))],
+ img_metadata))],
'clean': True,
'uptodate': [utils.config_changed({
1: self.kw.copy(),
@@ -343,7 +372,14 @@ class Galleries(Task, ImageProcessor):
self.gallery_list = []
for input_folder, output_folder in self.kw['gallery_folders'].items():
for root, dirs, files in os.walk(input_folder, followlinks=True):
- self.gallery_list.append((root, input_folder, output_folder))
+ # If output folder is empty, the top-level gallery
+ # index will collide with the main page for the site.
+ # Don't generate the top-level gallery index in that
+ # case.
+ # FIXME: also ignore pages named index
+ if (output_folder or root != input_folder and
+ (not self.kw['disable_indexes'] and self.kw['index_path'] == '')):
+ self.gallery_list.append((root, input_folder, output_folder))
def create_galleries_paths(self):
"""Given a list of galleries, put their paths into self.gallery_links."""
@@ -395,12 +431,73 @@ class Galleries(Task, ImageProcessor):
'uptodate': [utils.config_changed(self.kw.copy(), 'nikola.plugins.task.galleries:mkdir')],
}
+ def find_metadata(self, gallery, lang):
+ """Search for a gallery metadata file.
+
+ If there is an metadata file for the gallery, use that to determine
+ captions and the order in which images shall be displayed in the
+ gallery. You only need to list the images if a specific ordering or
+ caption is required. The metadata file is YAML-formatted, with field
+ names of
+ #
+ name:
+ caption:
+ order:
+ #
+ If a numeric order value is specified, we use that directly, otherwise
+ we depend on how the library returns the information - which may or may not
+ be in the same order as in the file itself. Non-numeric ordering is not
+ supported. If no caption is specified, then we return an empty string.
+ Returns a string (l18n'd filename), list (ordering), dict (captions),
+ dict (image metadata).
+ """
+ base_meta_path = os.path.join(gallery, "metadata.yml")
+ localized_meta_path = utils.get_translation_candidate(self.site.config,
+ base_meta_path, lang)
+ order = []
+ captions = {}
+ custom_metadata = {}
+ used_path = ""
+
+ if os.path.isfile(localized_meta_path):
+ used_path = localized_meta_path
+ elif os.path.isfile(base_meta_path):
+ used_path = base_meta_path
+ else:
+ return "", [], {}, {}
+
+ self.logger.debug("Using {0} for gallery {1}".format(
+ used_path, gallery))
+ with open(used_path, "r", encoding='utf-8-sig') as meta_file:
+ if YAML is None:
+ utils.req_missing(['ruamel.yaml'], 'use metadata.yml files for galleries')
+ yaml = YAML(typ='safe')
+ meta = yaml.load_all(meta_file)
+ for img in meta:
+ # load_all and safe_load_all both return None as their
+ # final element, so skip it
+ if not img:
+ continue
+ if 'name' in img:
+ img_name = img.pop('name')
+ if 'caption' in img and img['caption']:
+ captions[img_name] = img.pop('caption')
+
+ if 'order' in img and img['order'] is not None:
+ order.insert(img.pop('order'), img_name)
+ else:
+ order.append(img_name)
+ custom_metadata[img_name] = img
+ else:
+ self.logger.error("no 'name:' for ({0}) in {1}".format(
+ img, used_path))
+ return used_path, order, captions, custom_metadata
+
def parse_index(self, gallery, input_folder, output_folder):
"""Return a Post object if there is an index.txt."""
index_path = os.path.join(gallery, "index.txt")
- destination = os.path.join(
- self.kw["output_folder"], output_folder,
- os.path.relpath(gallery, input_folder))
+ destination = os.path.join(output_folder,
+ os.path.relpath(gallery, input_folder))
if os.path.isfile(index_path):
post = Post(
index_path,
@@ -408,15 +505,18 @@ class Galleries(Task, ImageProcessor):
destination,
False,
self.site.MESSAGES,
- 'story.tmpl',
- self.site.get_compiler(index_path)
+ 'page.tmpl',
+ self.site.get_compiler(index_path),
+ None,
+ self.site.metadata_extractors_by
)
# If this did not exist, galleries without a title in the
# index.txt file would be errorneously named `index`
# (warning: galleries titled index and filenamed differently
# may break)
- if post.title == 'index':
- post.title = os.path.split(gallery)[1]
+ if post.title() == 'index':
+ for lang in post.meta.keys():
+ post.meta[lang]['title'] = os.path.split(gallery)[1]
# Register the post (via #2417)
self.site.post_per_input_file[index_path] = post
else:
@@ -428,8 +528,8 @@ class Galleries(Task, ImageProcessor):
exclude_path = os.path.join(gallery_path, "exclude.meta")
try:
- f = open(exclude_path, 'r')
- excluded_image_name_list = f.read().split()
+ with open(exclude_path, 'r') as f:
+ excluded_image_name_list = f.read().split()
except IOError:
excluded_image_name_list = []
@@ -473,34 +573,26 @@ class Galleries(Task, ImageProcessor):
orig_dest_path = os.path.join(output_gallery, img_name)
yield utils.apply_filters({
'basename': self.name,
- 'name': thumb_path,
- 'file_dep': [img],
- 'targets': [thumb_path],
- 'actions': [
- (self.resize_image,
- (img, thumb_path, self.kw['thumbnail_size'], False, self.kw['preserve_exif_data'],
- self.kw['exif_whitelist']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed({
- 1: self.kw['thumbnail_size']
- }, 'nikola.plugins.task.galleries:resize_thumb')],
- }, self.kw['filters'])
-
- yield utils.apply_filters({
- 'basename': self.name,
'name': orig_dest_path,
'file_dep': [img],
- 'targets': [orig_dest_path],
+ 'targets': [thumb_path, orig_dest_path],
'actions': [
(self.resize_image,
- (img, orig_dest_path, self.kw['max_image_size'], False, self.kw['preserve_exif_data'],
- self.kw['exif_whitelist']))
- ],
+ [img], {
+ 'dst_paths': [thumb_path, orig_dest_path],
+ 'max_sizes': [self.kw['thumbnail_size'], self.kw['max_image_size']],
+ 'bigger_panoramas': True,
+ 'preserve_exif_data': self.kw['preserve_exif_data'],
+ 'exif_whitelist': self.kw['exif_whitelist'],
+ 'preserve_icc_profiles': self.kw['preserve_icc_profiles']})],
'clean': True,
'uptodate': [utils.config_changed({
- 1: self.kw['max_image_size']
- }, 'nikola.plugins.task.galleries:resize_max')],
+ 1: self.kw['thumbnail_size'],
+ 2: self.kw['max_image_size'],
+ 3: self.kw['preserve_exif_data'],
+ 4: self.kw['exif_whitelist'],
+ 5: self.kw['preserve_icc_profiles'],
+ }, 'nikola.plugins.task.galleries:resize_thumb')],
}, self.kw['filters'])
def remove_excluded_image(self, img, input_folder):
@@ -546,7 +638,7 @@ class Galleries(Task, ImageProcessor):
img_list,
img_titles,
thumbs,
- file_dep):
+ img_metadata):
"""Build the gallery index."""
# The photo array needs to be created here, because
# it relies on thumbnails already being created on
@@ -568,7 +660,7 @@ class Galleries(Task, ImageProcessor):
else:
img_list, thumbs, img_titles = [], [], []
- photo_array = []
+ photo_info = OrderedDict()
for img, thumb, title in zip(img_list, thumbs, img_titles):
w, h = _image_size_cache.get(thumb, (None, None))
if w is None:
@@ -578,8 +670,11 @@ class Galleries(Task, ImageProcessor):
im = Image.open(thumb)
w, h = im.size
_image_size_cache[thumb] = w, h
- # Thumbs are files in output, we need URLs
- photo_array.append({
+ im.close()
+ # Use basename to avoid issues with multilingual sites (Issue #3078)
+ img_basename = os.path.basename(img)
+ photo_info[img_basename] = {
+ # Thumbs are files in output, we need URLs
'url': url_from_path(img),
'url_thumb': url_from_path(thumb),
'title': title,
@@ -587,9 +682,27 @@ class Galleries(Task, ImageProcessor):
'w': w,
'h': h
},
- })
+ 'width': w,
+ 'height': h
+ }
+ if img_basename in img_metadata:
+ photo_info[img_basename].update(img_metadata[img_basename])
+ photo_array = []
+ if context['order']:
+ for entry in context['order']:
+ photo_array.append(photo_info.pop(entry))
+ # Do we have any orphan entries from metadata.yml, or
+ # are the files from the gallery not listed in metadata.yml?
+ if photo_info:
+ for entry in photo_info:
+ photo_array.append(photo_info[entry])
+ else:
+ for entry in photo_info:
+ photo_array.append(photo_info[entry])
+
context['photo_array'] = photo_array
context['photo_array_json'] = json.dumps(photo_array, sort_keys=True)
+
self.site.render_template(template_name, output_name, context)
def gallery_rss(self, img_list, dest_img_list, img_titles, lang, permalink, output_path, title):
@@ -647,6 +760,6 @@ class Galleries(Task, ImageProcessor):
utils.makedirs(dst_dir)
with io.open(output_path, "w+", encoding="utf-8") as rss_file:
data = rss_obj.to_xml(encoding='utf-8')
- if isinstance(data, utils.bytes_str):
+ if isinstance(data, bytes):
data = data.decode('utf-8')
rss_file.write(data)
diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin
index d3a34ee..cc078b7 100644
--- a/nikola/plugins/task/gzip.plugin
+++ b/nikola/plugins/task/gzip.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create gzipped copies of files
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
index 79a11dc..ebd427f 100644
--- a/nikola/plugins/task/gzip.py
+++ b/nikola/plugins/task/gzip.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/task/indexes.plugin b/nikola/plugins/task/indexes.plugin
index 553b5ad..f4a8f05 100644
--- a/nikola/plugins/task/indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_indexes
+name = classify_indexes
module = indexes
[Documentation]
@@ -9,5 +9,4 @@ website = https://getnikola.com/
description = Generates the blog's index pages.
[Nikola]
-plugincategory = Task
-
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py
index 8ecd1de..20491fb 100644
--- a/nikola/plugins/task/indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,323 +24,114 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the blog indexes."""
+"""Render the blog's main index."""
-from __future__ import unicode_literals
-from collections import defaultdict
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from nikola.plugin_categories import Task
-from nikola import utils
-from nikola.nikola import _enclosure
+from nikola.plugin_categories import Taxonomy
-class Indexes(Task):
- """Render the blog indexes."""
+class Indexes(Taxonomy):
+ """Classify for the blog's main index."""
- name = "render_indexes"
+ name = "classify_indexes"
- def set_site(self, site):
- """Set Nikola site."""
- self.number_of_pages = dict()
- self.number_of_pages_section = {lang: dict() for lang in site.config['TRANSLATIONS']}
- site.register_path_handler('index', self.index_path)
- site.register_path_handler('index_atom', self.index_atom_path)
- site.register_path_handler('section_index', self.index_section_path)
- site.register_path_handler('section_index_atom', self.index_section_atom_path)
- site.register_path_handler('section_index_rss', self.index_section_rss_path)
- return super(Indexes, self).set_site(site)
-
- def _get_filtered_posts(self, lang, show_untranslated_posts):
- """Return a filtered list of all posts for the given language.
-
- If show_untranslated_posts is True, will only include posts which
- are translated to the given language. Otherwise, returns all posts.
- """
- if show_untranslated_posts:
- return self.site.posts
- else:
- return [x for x in self.site.posts if x.is_translation_available(lang)]
-
- def _compute_number_of_pages(self, filtered_posts, posts_count):
- """Given a list of posts and the maximal number of posts per page, computes the number of pages needed."""
- return min(1, (len(filtered_posts) + posts_count - 1) // posts_count)
-
- def gen_tasks(self):
- """Render the blog indexes."""
- self.site.scan_posts()
- yield self.group_task()
-
- kw = {
- "translations": self.site.config['TRANSLATIONS'],
- "messages": self.site.MESSAGES,
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "filters": self.site.config['FILTERS'],
- "index_file": self.site.config['INDEX_FILE'],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'],
- "indexes_title": self.site.config['INDEXES_TITLE'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
- "blog_title": self.site.config["BLOG_TITLE"],
- "generate_atom": self.site.config["GENERATE_ATOM"],
- "site_url": self.site.config["SITE_URL"],
- }
-
- template_name = "index.tmpl"
- for lang in kw["translations"]:
- def page_link(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_link(self.site.link("index" + feed, None, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- def page_path(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_path(self.site.path("index" + feed, None, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- filtered_posts = self._get_filtered_posts(lang, kw["show_untranslated_posts"])
-
- indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang)
- self.number_of_pages[lang] = self._compute_number_of_pages(filtered_posts, kw['index_display_post_count'])
-
- context = {}
- context["pagekind"] = ["main_index", "index"]
-
- yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path)
-
- if self.site.config['POSTS_SECTIONS']:
- index_len = len(kw['index_file'])
-
- groups = defaultdict(list)
- for p in filtered_posts:
- groups[p.section_slug(lang)].append(p)
-
- # don't build sections when there is only one, aka. default setups
- if not len(groups.items()) > 1:
- continue
-
- for section_slug, post_list in groups.items():
- self.number_of_pages_section[lang][section_slug] = self._compute_number_of_pages(post_list, kw['index_display_post_count'])
-
- def cat_link(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_link(self.site.link("section_index" + feed, section_slug, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
-
- def cat_path(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_path(self.site.path("section_index" + feed, section_slug, lang), i, displayed_i,
- lang, self.site, force_addition, extension)
+ classification_name = "index"
+ overview_page_variable_name = None
+ more_than_one_classifications_per_post = False
+ has_hierarchy = False
+ show_list_as_index = True
+ template_for_single_list = "index.tmpl"
+ template_for_classification_overview = None
+ apply_to_posts = True
+ apply_to_pages = False
+ omit_empty_classifications = False
+ path_handler_docstrings = {
+ 'index_index': False,
+ 'index': """Link to a numbered index.
- context = {}
+Example:
- short_destination = os.path.join(section_slug, kw['index_file'])
- link = short_destination.replace('\\', '/')
- if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
- link = link[:-index_len]
- context["permalink"] = link
- context["pagekind"] = ["section_page"]
- context["description"] = self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang)[section_slug] if section_slug in self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang) else ""
+link://index/3 => /index-3.html""",
+ 'index_atom': """Link to a numbered Atom index.
- if self.site.config["POSTS_SECTION_ARE_INDEXES"]:
- context["pagekind"].append("index")
- posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang)
+Example:
- section_title = None
- if type(posts_section_title) is dict:
- if section_slug in posts_section_title:
- section_title = posts_section_title[section_slug]
- elif type(posts_section_title) is str:
- section_title = posts_section_title
- if not section_title:
- section_title = post_list[0].section_name(lang)
- section_title = section_title.format(name=post_list[0].section_name(lang))
+link://index_atom/3 => /index-3.atom""",
+ 'index_rss': """A link to the RSS feed path.
- task = self.site.generic_index_renderer(lang, post_list, section_title, "sectionindex.tmpl", context, kw, self.name, cat_link, cat_path)
- else:
- context["pagekind"].append("list")
- output_name = os.path.join(kw['output_folder'], section_slug, kw['index_file'])
- task = self.site.generic_post_list_renderer(lang, post_list, output_name, "list.tmpl", kw['filters'], context)
- task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
- task['basename'] = self.name
- yield task
+Example:
- # RSS feed for section
- deps = []
- deps_uptodate = []
- if kw["show_untranslated_posts"]:
- posts = post_list[:kw['feed_length']]
- else:
- posts = [x for x in post_list if x.is_translation_available(lang)][:kw['feed_length']]
- for post in posts:
- deps += post.deps(lang)
- deps_uptodate += post.deps_uptodate(lang)
+link://rss => /blog/rss.xml""",
+ }
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link('section_index_rss', section_slug, lang).lstrip('/'))
- output_name = os.path.join(kw['output_folder'], self.site.path('section_index_rss', section_slug, lang).lstrip(os.sep))
- task = {
- 'basename': self.name,
- 'name': os.path.normpath(output_name),
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"](lang), kw["site_url"],
- context["description"], posts, output_name,
- kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url,
- _enclosure, kw["feed_links_append_query"]))],
-
- 'task_dep': ['render_posts'],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.indexes')] + deps_uptodate,
- }
- yield task
-
- if not self.site.config["PAGE_INDEX"]:
- return
+ def set_site(self, site):
+ """Set Nikola site."""
+ # Redirect automatically generated 'index_rss' path handler to 'rss' for compatibility with old rss plugin
+ site.register_path_handler('rss', lambda name, lang: site.path_handlers['index_rss'](name, lang))
+ site.path_handlers['rss'].__doc__ = """A link to the RSS feed path.
+
+Example:
+
+ link://rss => /blog/rss.xml
+ """.strip()
+ return super().set_site(site)
+
+ def get_implicit_classifications(self, lang):
+ """Return a list of classification strings which should always appear in posts_per_classification."""
+ return [""]
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ return [""]
+
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return self.site.config["BLOG_TITLE"](lang)
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ if dest_type == 'rss':
+ return [
+ self.site.config['RSS_PATH'](lang),
+ self.site.config['RSS_FILENAME_BASE'](lang)
+ ], 'auto'
+ if dest_type == 'feed':
+ return [
+ self.site.config['ATOM_PATH'](lang),
+ self.site.config['ATOM_FILENAME_BASE'](lang)
+ ], 'auto'
+ page_number = None
+ if dest_type == 'page':
+ # Interpret argument as page number
+ try:
+ page_number = int(classification)
+ except (ValueError, TypeError):
+ pass
+ return [self.site.config['INDEX_PATH'](lang)], 'always', page_number
+
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
kw = {
- "translations": self.site.config['TRANSLATIONS'],
- "post_pages": self.site.config["post_pages"],
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "filters": self.site.config['FILTERS'],
- "index_file": self.site.config['INDEX_FILE'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
+ "show_untranslated_posts": self.site.config["SHOW_UNTRANSLATED_POSTS"],
}
- template_name = "list.tmpl"
- index_len = len(kw['index_file'])
- for lang in kw["translations"]:
- # Need to group by folder to avoid duplicated tasks (Issue #758)
- # Group all pages by path prefix
- groups = defaultdict(list)
- for p in self.site.timeline:
- if not p.is_post:
- destpath = p.destination_path(lang)
- if destpath[-(1 + index_len):] == '/' + kw['index_file']:
- destpath = destpath[:-(1 + index_len)]
- dirname = os.path.dirname(destpath)
- groups[dirname].append(p)
- for dirname, post_list in groups.items():
- context = {}
- context["items"] = []
- should_render = True
- output_name = os.path.join(kw['output_folder'], dirname, kw['index_file'])
- short_destination = os.path.join(dirname, kw['index_file'])
- link = short_destination.replace('\\', '/')
- if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']:
- link = link[:-index_len]
- context["permalink"] = link
- context["pagekind"] = ["list"]
- if dirname == "/":
- context["pagekind"].append("front_page")
-
- for post in post_list:
- # If there is an index.html pending to be created from
- # a page, do not generate the PAGE_INDEX
- if post.destination_path(lang) == short_destination:
- should_render = False
- else:
- context["items"].append((post.title(lang),
- post.permalink(lang),
- None))
-
- if should_render:
- task = self.site.generic_post_list_renderer(lang, post_list,
- output_name,
- template_name,
- kw['filters'],
- context)
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')]
- task['basename'] = self.name
- yield task
-
- def index_path(self, name, lang, is_feed=False):
- """Link to a numbered index.
-
- Example:
-
- link://index/3 => /index-3.html
- """
- extension = None
- if is_feed:
- extension = ".atom"
- index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
- else:
- index_file = self.site.config['INDEX_FILE']
- if lang in self.number_of_pages:
- number_of_pages = self.number_of_pages[lang]
- else:
- number_of_pages = self._compute_number_of_pages(self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']), self.site.config['INDEX_DISPLAY_POST_COUNT'])
- self.number_of_pages[lang] = number_of_pages
- return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['INDEX_PATH'],
- index_file] if _f],
- name,
- utils.get_displayed_page_number(name, number_of_pages, self.site),
- lang,
- self.site,
- extension=extension)
-
- def index_section_path(self, name, lang, is_feed=False, is_rss=False):
- """Link to the index for a section.
-
- Example:
-
- link://section_index/cars => /cars/index.html
- """
- extension = None
-
- if is_feed:
- extension = ".atom"
- index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension
- elif is_rss:
- index_file = 'rss.xml'
- else:
- index_file = self.site.config['INDEX_FILE']
- if name in self.number_of_pages_section[lang]:
- number_of_pages = self.number_of_pages_section[lang][name]
- else:
- posts = [post for post in self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']) if post.section_slug(lang) == name]
- number_of_pages = self._compute_number_of_pages(posts, self.site.config['INDEX_DISPLAY_POST_COUNT'])
- self.number_of_pages_section[lang][name] = number_of_pages
- return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang],
- name,
- index_file] if _f],
- None,
- utils.get_displayed_page_number(None, number_of_pages, self.site),
- lang,
- self.site,
- extension=extension)
-
- def index_atom_path(self, name, lang):
- """Link to a numbered Atom index.
-
- Example:
-
- link://index_atom/3 => /index-3.atom
- """
- return self.index_path(name, lang, is_feed=True)
-
- def index_section_atom_path(self, name, lang):
- """Link to the Atom index for a section.
-
- Example:
-
- link://section_index_atom/cars => /cars/index.atom
- """
- return self.index_section_path(name, lang, is_feed=True)
+ context = {
+ "title": self.site.config["INDEXES_TITLE"](lang) or self.site.config["BLOG_TITLE"](lang),
+ "description": self.site.config["BLOG_DESCRIPTION"](lang),
+ "pagekind": ["main_index", "index"],
+ "featured": [p for p in self.site.posts if p.post_status == 'featured' and
+ (lang in p.translated_to or kw["show_untranslated_posts"])],
+ }
+ kw.update(context)
+ return context, kw
- def index_section_rss_path(self, name, lang):
- """Link to the RSS feed for a section.
+ def should_generate_classification_page(self, classification, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_INDEXES"]
- Example:
+ def should_generate_atom_for_classification_page(self, classification, post_list, lang):
+ """Only generates Atom feed for list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_MAIN_ATOM_FEED"]
- link://section_index_rss/cars => /cars/rss.xml
- """
- return self.index_section_path(name, lang, is_rss=True)
+ def should_generate_rss_for_classification_page(self, classification, post_list, lang):
+ """Only generates RSS feed for list of posts for classification if this function returns True."""
+ return not self.site.config["DISABLE_MAIN_RSS_FEED"]
diff --git a/nikola/plugins/task/listings.plugin b/nikola/plugins/task/listings.plugin
index 8fc2e2d..03b67d2 100644
--- a/nikola/plugins/task/listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Render code listings into output
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
index e694aa5..c946313 100644
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,12 @@
"""Render code listings."""
-from __future__ import unicode_literals, print_function
-
-from collections import defaultdict
import os
-import lxml.html
+from collections import defaultdict
+import natsort
from pygments import highlight
from pygments.lexers import get_lexer_for_filename, guess_lexer, TextLexer
-import natsort
from nikola.plugin_categories import Task
from nikola import utils
@@ -92,7 +89,7 @@ class Listings(Task):
self.proper_input_file_mapping = {}
for input_folder, output_folder in self.kw['listings_folders'].items():
- for root, dirs, files in os.walk(input_folder, followlinks=True):
+ for root, _, files in os.walk(input_folder, followlinks=True):
# Compute relative path; can't use os.path.relpath() here as it returns "." instead of ""
rel_path = root[len(input_folder):]
if rel_path[:1] == os.sep:
@@ -104,7 +101,7 @@ class Listings(Task):
# Register file names in the mapping.
self.register_output_name(input_folder, rel_name, rel_output_name)
- return super(Listings, self).set_site(site)
+ return super().set_site(site)
def gen_tasks(self):
"""Render pretty code listings."""
@@ -115,24 +112,31 @@ class Listings(Task):
needs_ipython_css = False
if in_name and in_name.endswith('.ipynb'):
# Special handling: render ipynbs in listings (Issue #1900)
- ipynb_compiler = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler").plugin_object
- ipynb_raw = ipynb_compiler.compile_html_string(in_name, True)
- ipynb_html = lxml.html.fromstring(ipynb_raw)
- # The raw HTML contains garbage (scripts and styles), we can’t leave it in
- code = lxml.html.tostring(ipynb_html.xpath('//*[@id="notebook"]')[0], encoding='unicode')
+ ipynb_plugin = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler")
+ if ipynb_plugin is None:
+ msg = "To use .ipynb files as listings, you must set up the Jupyter compiler in COMPILERS and POSTS/PAGES."
+ utils.LOGGER.error(msg)
+ raise ValueError(msg)
+
+ ipynb_compiler = ipynb_plugin.plugin_object
+ with open(in_name, "r", encoding="utf-8-sig") as in_file:
+ nb_json = ipynb_compiler._nbformat_read(in_file)
+ code = ipynb_compiler._compile_string(nb_json)
title = os.path.basename(in_name)
needs_ipython_css = True
elif in_name:
- with open(in_name, 'r') as fd:
+ with open(in_name, 'r', encoding='utf-8-sig') as fd:
try:
lexer = get_lexer_for_filename(in_name)
- except:
+ except Exception:
try:
lexer = guess_lexer(fd.read())
- except:
+ except Exception:
lexer = TextLexer()
fd.seek(0)
- code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name))
+ code = highlight(
+ fd.read(), lexer,
+ utils.NikolaPygmentsHTML(in_name, linenos='table'))
title = os.path.basename(in_name)
else:
code = ''
@@ -184,7 +188,7 @@ class Listings(Task):
uptodate = {'c': self.site.GLOBAL_CONTEXT}
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- uptodate['||template_hooks|{0}||'.format(k)] = v._items
+ uptodate['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang'])
@@ -220,6 +224,8 @@ class Listings(Task):
'clean': True,
}, self.kw["filters"])
for f in files:
+ if f == '.DS_Store':
+ continue
ext = os.path.splitext(f)[-1]
if ext in ignored_extensions:
continue
@@ -257,7 +263,7 @@ class Listings(Task):
}, self.kw["filters"])
def listing_source_path(self, name, lang):
- """A link to the source code for a listing.
+ """Return a link to the source code for a listing.
It will try to use the file name if it's not ambiguous, or the file path.
@@ -273,7 +279,7 @@ class Listings(Task):
return result
def listing_path(self, namep, lang):
- """A link to a listing.
+ """Return a link to a listing.
It will try to use the file name if it's not ambiguous, or the file path.
@@ -297,7 +303,7 @@ class Listings(Task):
utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name])))
return ["ERROR"]
if len(self.site.config['LISTINGS_FOLDERS']) > 1:
- utils.LOGGER.notice("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
+ utils.LOGGER.warning("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
name = list(self.improper_input_file_mapping[name])[0]
break
else:
diff --git a/nikola/plugins/task/page_index.plugin b/nikola/plugins/task/page_index.plugin
new file mode 100644
index 0000000..42c9288
--- /dev/null
+++ b/nikola/plugins/task/page_index.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = classify_page_index
+module = page_index
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Generates the blog's index pages.
+
+[Nikola]
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/page_index.py b/nikola/plugins/task/page_index.py
new file mode 100644
index 0000000..e7b33cf
--- /dev/null
+++ b/nikola/plugins/task/page_index.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the page index."""
+
+
+from nikola.plugin_categories import Taxonomy
+
+
+class PageIndex(Taxonomy):
+ """Classify for the page index."""
+
+ name = "classify_page_index"
+
+ classification_name = "page_index_folder"
+ overview_page_variable_name = "page_folder"
+ more_than_one_classifications_per_post = False
+ has_hierarchy = True
+ include_posts_from_subhierarchies = False
+ show_list_as_index = False
+ template_for_single_list = "list.tmpl"
+ template_for_classification_overview = None
+ always_disable_rss = True
+ always_disable_atom = True
+ apply_to_posts = False
+ apply_to_pages = True
+ omit_empty_classifications = True
+ path_handler_docstrings = {
+ 'page_index_folder_index': None,
+ 'page_index_folder': None,
+ 'page_index_folder_atom': None,
+ 'page_index_folder_rss': None,
+ }
+
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return self.site.config["PAGE_INDEX"]
+
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ destpath = post.destination_path(lang, sep='/')
+ if post.has_pretty_url(lang):
+ idx = '/index.html'
+ if destpath.endswith(idx):
+ destpath = destpath[:-len(idx)]
+ i = destpath.rfind('/')
+ return [destpath[:i] if i >= 0 else '']
+
+ def get_classification_friendly_name(self, dirname, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return dirname
+
+ def get_path(self, hierarchy, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ return hierarchy, 'always'
+
+ def extract_hierarchy(self, dirname):
+ """Given a classification, return a list of parts in the hierarchy."""
+ return dirname.split('/') if dirname else []
+
+ def recombine_classification_from_hierarchy(self, hierarchy):
+ """Given a list of parts in the hierarchy, return the classification string."""
+ return '/'.join(hierarchy)
+
+ def provide_context_and_uptodate(self, dirname, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "filters": self.site.config['FILTERS'],
+ }
+ context = {
+ "title": self.site.config['BLOG_TITLE'](lang),
+ "pagekind": ["list", "front_page", "page_index"] if dirname == '' else ["list", "page_index"],
+ "kind": "page_index_folder",
+ "classification": dirname,
+ "has_no_feeds": True,
+ }
+ kw.update(context)
+ return context, kw
+
+ def should_generate_classification_page(self, dirname, post_list, lang):
+ """Only generates list of posts for classification if this function returns True."""
+ short_destination = dirname + '/' + self.site.config['INDEX_FILE']
+ for post in post_list:
+ # If there is an index.html pending to be created from a page, do not generate the page index.
+ if post.destination_path(lang, sep='/') == short_destination:
+ return False
+ return True
diff --git a/nikola/plugins/task/pages.plugin b/nikola/plugins/task/pages.plugin
index 1bdc7f4..a04cd05 100644
--- a/nikola/plugins/task/pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create pages in the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/pages.py b/nikola/plugins/task/pages.py
index 7d8287b..0c0bdd2 100644
--- a/nikola/plugins/task/pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,9 +26,10 @@
"""Render pages into output."""
-from __future__ import unicode_literals
+import os
+
from nikola.plugin_categories import Task
-from nikola.utils import config_changed
+from nikola.utils import config_changed, LOGGER
class RenderPages(Task):
@@ -47,6 +48,13 @@ class RenderPages(Task):
}
self.site.scan_posts()
yield self.group_task()
+ index_paths = {}
+ for lang in kw["translations"]:
+ index_paths[lang] = False
+ if not self.site.config["DISABLE_INDEXES"]:
+ index_paths[lang] = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'],
+ self.site.path('index', '', lang=lang)))
+
for lang in kw["translations"]:
for post in self.site.timeline:
if not kw["show_untranslated_posts"] and not post.is_translation_available(lang):
@@ -56,6 +64,12 @@ class RenderPages(Task):
else:
context = {'pagekind': ['story_page', 'page_page']}
for task in self.site.generic_page_renderer(lang, post, kw["filters"], context):
+ if task['name'] == index_paths[lang]:
+ # Issue 3022
+ LOGGER.error(
+ "Post {0!r}: output path ({1}) conflicts with the blog index ({2}). "
+ "Please change INDEX_PATH or disable index generation.".format(
+ post.source_path, task['name'], index_paths[lang]))
task['uptodate'] = task['uptodate'] + [config_changed(kw, 'nikola.plugins.task.pages')]
task['basename'] = self.name
task['task_dep'] = ['render_posts']
diff --git a/nikola/plugins/task/posts.plugin b/nikola/plugins/task/posts.plugin
index c9578bc..6893472 100644
--- a/nikola/plugins/task/posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create HTML fragments out of posts.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py
index fe10c5f..5f48165 100644
--- a/nikola/plugins/task/posts.py
+++ b/nikola/plugins/task/posts.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,11 +26,11 @@
"""Build HTML fragments from metadata and text."""
-from copy import copy
import os
+from copy import copy
from nikola.plugin_categories import Task
-from nikola import filters, utils
+from nikola import utils
def update_deps(post, lang, task):
@@ -85,11 +85,12 @@ class RenderPosts(Task):
deps_dict[k] = self.site.config.get(k)
dest = post.translated_base_path(lang)
file_dep = [p for p in post.fragment_deps(lang) if not p.startswith("####MAGIC####")]
+ extra_targets = post.compiler.get_extra_targets(post, lang, dest)
task = {
'basename': self.name,
'name': dest,
'file_dep': file_dep,
- 'targets': [dest],
+ 'targets': [dest] + extra_targets,
'actions': [(post.compile, (lang, )),
(update_deps, (post, lang, )),
],
@@ -107,12 +108,9 @@ class RenderPosts(Task):
for i, f in enumerate(ff):
if not f:
continue
- if f.startswith('filters.'): # A function from the filters module
- f = f[8:]
- try:
- flist.append(getattr(filters, f))
- except AttributeError:
- pass
+ _f = self.site.filters.get(f)
+ if _f is not None: # A registered filter
+ flist.append(_f)
else:
flist.append(f)
yield utils.apply_filters(task, {os.path.splitext(dest)[-1]: flist})
diff --git a/nikola/plugins/task/py3_switch.plugin b/nikola/plugins/task/py3_switch.plugin
deleted file mode 100644
index b0014e1..0000000
--- a/nikola/plugins/task/py3_switch.plugin
+++ /dev/null
@@ -1,13 +0,0 @@
-[Core]
-name = py3_switch
-module = py3_switch
-
-[Documentation]
-author = Roberto Alsina
-version = 1.0
-website = https://getnikola.com/
-description = Beg the user to switch to Python 3
-
-[Nikola]
-plugincategory = Task
-
diff --git a/nikola/plugins/task/py3_switch.py b/nikola/plugins/task/py3_switch.py
deleted file mode 100644
index 2ff4e2d..0000000
--- a/nikola/plugins/task/py3_switch.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Beg the user to switch to python 3."""
-
-import datetime
-import os
-import random
-import sys
-
-import doit.tools
-
-from nikola.utils import get_logger, STDERR_HANDLER
-from nikola.plugin_categories import LateTask
-
-PY2_AND_NO_PY3_WARNING = """Nikola is going to deprecate Python 2 support in 2016. Your current
-version will continue to work, but please consider upgrading to Python 3.
-
-Please check http://bit.ly/1FKEsiX for details.
-"""
-PY2_WARNING = """Nikola is going to deprecate Python 2 support in 2016. You already have Python 3
-available in your system. Why not switch?
-
-Please check http://bit.ly/1FKEsiX for details.
-"""
-PY2_BARBS = [
- "Python 2 has been deprecated for years. Stop clinging to your long gone youth and switch to Python3.",
- "Python 2 is the safety blanket of languages. Be a big kid and switch to Python 3",
- "Python 2 is old and busted. Python 3 is the new hotness.",
- "Nice unicode you have there, would be a shame something happened to it.. switch to python 3!.",
- "Don't get in the way of progress! Upgrade to Python 3 and save a developer's mind today!",
- "Winners don't use Python 2 -- Signed: The FBI",
- "Python 2? What year is it?",
- "I just wanna tell you how I'm feeling\n"
- "Gotta make you understand\n"
- "Never gonna give you up [But Python 2 has to go]",
- "The year 2009 called, and they want their Python 2.7 back.",
-]
-
-
-LOGGER = get_logger('Nikola', STDERR_HANDLER)
-
-
-def has_python_3():
- """Check if python 3 is available."""
- if 'win' in sys.platform:
- py_bin = 'py.exe'
- else:
- py_bin = 'python3'
- for path in os.environ["PATH"].split(os.pathsep):
- if os.access(os.path.join(path, py_bin), os.X_OK):
- return True
- return False
-
-
-class Py3Switch(LateTask):
- """Beg the user to switch to python 3."""
-
- name = "_switch to py3"
-
- def gen_tasks(self):
- """Beg the user to switch to python 3."""
- def give_warning():
- if sys.version_info[0] == 3:
- return
- if has_python_3():
- LOGGER.warn(random.choice(PY2_BARBS))
- LOGGER.warn(PY2_WARNING)
- else:
- LOGGER.warn(PY2_AND_NO_PY3_WARNING)
-
- task = {
- 'basename': self.name,
- 'name': 'please!',
- 'actions': [give_warning],
- 'clean': True,
- 'uptodate': [doit.tools.timeout(datetime.timedelta(days=3))]
- }
-
- return task
diff --git a/nikola/plugins/task/redirect.plugin b/nikola/plugins/task/redirect.plugin
index c5a3042..57bd0c0 100644
--- a/nikola/plugins/task/redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create redirect pages.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/redirect.py b/nikola/plugins/task/redirect.py
index b170b81..a89fbd0 100644
--- a/nikola/plugins/task/redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""Generate redirections."""
-from __future__ import unicode_literals
import os
@@ -45,12 +44,15 @@ class Redirect(Task):
'redirections': self.site.config['REDIRECTIONS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
'filters': self.site.config['FILTERS'],
+ 'index_file': self.site.config['INDEX_FILE'],
}
yield self.group_task()
if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src.lstrip('/'))
+ if src_path.endswith("/"):
+ src_path += kw['index_file']
yield utils.apply_filters({
'basename': self.name,
'name': src_path,
diff --git a/nikola/plugins/task/robots.plugin b/nikola/plugins/task/robots.plugin
index 7ae56c6..51f7781 100644
--- a/nikola/plugins/task/robots.plugin
+++ b/nikola/plugins/task/robots.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generate /robots.txt exclusion file and promote sitemap.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/robots.py b/nikola/plugins/task/robots.py
index 8537fc8..627d436 100644
--- a/nikola/plugins/task/robots.py
+++ b/nikola/plugins/task/robots.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,13 +26,9 @@
"""Generate a robots.txt file."""
-from __future__ import print_function, absolute_import, unicode_literals
import io
import os
-try:
- from urlparse import urljoin, urlparse
-except ImportError:
- from urllib.parse import urljoin, urlparse # NOQA
+from urllib.parse import urljoin, urlparse
from nikola.plugin_categories import LateTask
from nikola import utils
@@ -59,7 +55,8 @@ class RobotsFile(LateTask):
def write_robots():
if kw["site_url"] != urljoin(kw["site_url"], "/"):
- utils.LOGGER.warn('robots.txt not ending up in server root, will be useless')
+ utils.LOGGER.warning('robots.txt not ending up in server root, will be useless')
+ utils.LOGGER.info('Add "robots" to DISABLED_PLUGINS to disable this warning and robots.txt generation.')
with io.open(robots_path, 'w+', encoding='utf8') as outf:
outf.write("Sitemap: {0}\n\n".format(sitemapindex_url))
@@ -82,6 +79,6 @@ class RobotsFile(LateTask):
"task_dep": ["sitemap"]
}, kw["filters"])
elif kw["robots_exclusions"]:
- utils.LOGGER.warn('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied file.')
+ utils.LOGGER.warning('Did not generate robots.txt as one already exists in FILES_FOLDERS. ROBOTS_EXCLUSIONS will not have any affect on the copied file.')
else:
utils.LOGGER.debug('Did not generate robots.txt as one already exists in FILES_FOLDERS.')
diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/rss.plugin
deleted file mode 100644
index 4dd8aba..0000000
--- a/nikola/plugins/task/rss.plugin
+++ /dev/null
@@ -1,13 +0,0 @@
-[Core]
-name = generate_rss
-module = rss
-
-[Documentation]
-author = Roberto Alsina
-version = 1.0
-website = https://getnikola.com/
-description = Generate RSS feeds.
-
-[Nikola]
-plugincategory = Task
-
diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py
deleted file mode 100644
index 780559b..0000000
--- a/nikola/plugins/task/rss.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright © 2012-2016 Roberto Alsina and others.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-"""Generate RSS feeds."""
-
-from __future__ import unicode_literals, print_function
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-
-from nikola import utils
-from nikola.nikola import _enclosure
-from nikola.plugin_categories import Task
-
-
-class GenerateRSS(Task):
- """Generate RSS feeds."""
-
- name = "generate_rss"
-
- def set_site(self, site):
- """Set Nikola site."""
- site.register_path_handler('rss', self.rss_path)
- return super(GenerateRSS, self).set_site(site)
-
- def gen_tasks(self):
- """Generate RSS feeds."""
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "filters": self.site.config["FILTERS"],
- "blog_title": self.site.config["BLOG_TITLE"],
- "site_url": self.site.config["SITE_URL"],
- "base_url": self.site.config["BASE_URL"],
- "blog_description": self.site.config["BLOG_DESCRIPTION"],
- "output_folder": self.site.config["OUTPUT_FOLDER"],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "feed_previewimage": self.site.config["FEED_PREVIEWIMAGE"],
- "tzinfo": self.site.tzinfo,
- "feed_read_more_link": self.site.config["FEED_READ_MORE_LINK"],
- "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- }
- self.site.scan_posts()
- # Check for any changes in the state of use_in_feeds for any post.
- # Issue #934
- kw['use_in_feeds_status'] = ''.join(
- ['T' if x.use_in_feeds else 'F' for x in self.site.timeline]
- )
- yield self.group_task()
- for lang in kw["translations"]:
- output_name = os.path.join(kw['output_folder'],
- self.site.path("rss", None, lang))
- deps = []
- deps_uptodate = []
- if kw["show_untranslated_posts"]:
- posts = self.site.posts[:kw['feed_length']]
- else:
- posts = [x for x in self.site.posts if x.is_translation_available(lang)][:kw['feed_length']]
- for post in posts:
- deps += post.deps(lang)
- deps_uptodate += post.deps_uptodate(lang)
-
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/'))
-
- task = {
- 'basename': 'generate_rss',
- 'name': os.path.normpath(output_name),
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, kw["blog_title"](lang), kw["site_url"],
- kw["blog_description"](lang), posts, output_name,
- kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url,
- _enclosure, kw["feed_links_append_query"]))],
-
- 'task_dep': ['render_posts'],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.rss')] + deps_uptodate,
- }
- yield utils.apply_filters(task, kw['filters'])
-
- def rss_path(self, name, lang):
- """A link to the RSS feed path.
-
- Example:
-
- link://rss => /blog/rss.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['RSS_PATH'], 'rss.xml'] if _f]
diff --git a/nikola/plugins/task/scale_images.plugin b/nikola/plugins/task/scale_images.plugin
index 3edd0c6..332f583 100644
--- a/nikola/plugins/task/scale_images.plugin
+++ b/nikola/plugins/task/scale_images.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Create down-scaled images and thumbnails.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/scale_images.py b/nikola/plugins/task/scale_images.py
index 2b483ae..fa3a67b 100644
--- a/nikola/plugins/task/scale_images.py
+++ b/nikola/plugins/task/scale_images.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2014-2016 Pelle Nilsson and others.
+# Copyright © 2014-2020 Pelle Nilsson and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -38,29 +38,24 @@ class ScaleImage(Task, ImageProcessor):
name = "scale_images"
- def set_site(self, site):
- """Set Nikola site."""
- self.logger = utils.get_logger('scale_images', utils.STDERR_HANDLER)
- return super(ScaleImage, self).set_site(site)
-
def process_tree(self, src, dst):
"""Process all images in a src tree and put the (possibly) rescaled images in the dst folder."""
- ignore = set(['.svn'])
+ thumb_fmt = self.kw['image_thumbnail_format']
base_len = len(src.split(os.sep))
for root, dirs, files in os.walk(src, followlinks=True):
root_parts = root.split(os.sep)
- if set(root_parts) & ignore:
- continue
dst_dir = os.path.join(dst, *root_parts[base_len:])
utils.makedirs(dst_dir)
for src_name in files:
- if src_name in ('.DS_Store', 'Thumbs.db'):
- continue
if (not src_name.lower().endswith(tuple(self.image_ext_list)) and not src_name.upper().endswith(tuple(self.image_ext_list))):
continue
dst_file = os.path.join(dst_dir, src_name)
src_file = os.path.join(root, src_name)
- thumb_file = '.thumbnail'.join(os.path.splitext(dst_file))
+ thumb_name, thumb_ext = os.path.splitext(src_name)
+ thumb_file = os.path.join(dst_dir, thumb_fmt.format(
+ name=thumb_name,
+ ext=thumb_ext,
+ ))
yield {
'name': dst_file,
'file_dep': [src_file],
@@ -71,19 +66,28 @@ class ScaleImage(Task, ImageProcessor):
def process_image(self, src, dst, thumb):
"""Resize an image."""
- self.resize_image(src, dst, self.kw['max_image_size'], False, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'])
- self.resize_image(src, thumb, self.kw['image_thumbnail_size'], False, preserve_exif_data=self.kw['preserve_exif_data'], exif_whitelist=self.kw['exif_whitelist'])
+ self.resize_image(
+ src,
+ dst_paths=[dst, thumb],
+ max_sizes=[self.kw['max_image_size'], self.kw['image_thumbnail_size']],
+ bigger_panoramas=True,
+ preserve_exif_data=self.kw['preserve_exif_data'],
+ exif_whitelist=self.kw['exif_whitelist'],
+ preserve_icc_profiles=self.kw['preserve_icc_profiles']
+ )
def gen_tasks(self):
"""Copy static files into the output folder."""
self.kw = {
'image_thumbnail_size': self.site.config['IMAGE_THUMBNAIL_SIZE'],
+ 'image_thumbnail_format': self.site.config['IMAGE_THUMBNAIL_FORMAT'],
'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
'image_folders': self.site.config['IMAGE_FOLDERS'],
'output_folder': self.site.config['OUTPUT_FOLDER'],
'filters': self.site.config['FILTERS'],
'preserve_exif_data': self.site.config['PRESERVE_EXIF_DATA'],
'exif_whitelist': self.site.config['EXIF_WHITELIST'],
+ 'preserve_icc_profiles': self.site.config['PRESERVE_ICC_PROFILES'],
}
self.image_ext_list = self.image_ext_list_builtin
diff --git a/nikola/plugins/task/sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index 83e72c4..c8aa832 100644
--- a/nikola/plugins/task/sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Generate google sitemap.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap.py
index 64fcb45..8bbaa63 100644
--- a/nikola/plugins/task/sitemap/__init__.py
+++ b/nikola/plugins/task/sitemap.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,18 +26,13 @@
"""Generate a sitemap."""
-from __future__ import print_function, absolute_import, unicode_literals
-import io
import datetime
-import dateutil.tz
+import io
import os
-import sys
-try:
- from urlparse import urljoin, urlparse
- import robotparser as robotparser
-except ImportError:
- from urllib.parse import urljoin, urlparse # NOQA
- import urllib.robotparser as robotparser # NOQA
+import urllib.robotparser as robotparser
+from urllib.parse import urljoin, urlparse
+
+import dateutil.tz
from nikola.plugin_categories import LateTask
from nikola.utils import apply_filters, config_changed, encodelink
@@ -119,7 +114,6 @@ class Sitemap(LateTask):
"output_folder": self.site.config["OUTPUT_FOLDER"],
"strip_indexes": self.site.config["STRIP_INDEXES"],
"index_file": self.site.config["INDEX_FILE"],
- "sitemap_include_fileless_dirs": self.site.config["SITEMAP_INCLUDE_FILELESS_DIRS"],
"mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.atom', '.html', '.htm', '.php', '.xml', '.rss']),
"robots_exclusions": self.site.config["ROBOTS_EXCLUSIONS"],
"filters": self.site.config["FILTERS"],
@@ -142,18 +136,19 @@ class Sitemap(LateTask):
def scan_locs():
"""Scan site locations."""
for root, dirs, files in os.walk(output, followlinks=True):
- if not dirs and not files and not kw['sitemap_include_fileless_dirs']:
+ if not dirs and not files:
continue # Totally empty, not on sitemap
path = os.path.relpath(root, output)
# ignore the current directory.
if path == '.':
- path = ''
+ path = syspath = ''
else:
+ syspath = path + os.sep
path = path.replace(os.sep, '/') + '/'
lastmod = self.get_lastmod(root)
loc = urljoin(base_url, base_path + path)
if kw['index_file'] in files and kw['strip_indexes']: # ignore folders when not stripping urls
- post = self.site.post_per_file.get(path + kw['index_file'])
+ post = self.site.post_per_file.get(syspath + kw['index_file'])
if post and (post.is_draft or post.is_private or post.publish_later):
continue
alternates = []
@@ -169,7 +164,7 @@ class Sitemap(LateTask):
continue # We already mapped the folder
if os.path.splitext(fname)[-1] in mapped_exts:
real_path = os.path.join(root, fname)
- path = os.path.relpath(real_path, output)
+ path = syspath = os.path.relpath(real_path, output)
if path.endswith(kw['index_file']) and kw['strip_indexes']:
# ignore index files when stripping urls
continue
@@ -177,16 +172,15 @@ class Sitemap(LateTask):
continue
# read in binary mode to make ancient files work
- fh = open(real_path, 'rb')
- filehead = fh.read(1024)
- fh.close()
+ with open(real_path, 'rb') as fh:
+ filehead = fh.read(1024)
if path.endswith('.html') or path.endswith('.htm') or path.endswith('.php'):
- """ ignores "html" files without doctype """
+ # Ignores "html" files without doctype
if b'<!doctype html' not in filehead.lower():
continue
- """ ignores "html" files with noindex robot directives """
+ # Ignores "html" files with noindex robot directives
robots_directives = [b'<meta content=noindex name=robots',
b'<meta content=none name=robots',
b'<meta name=robots content=noindex',
@@ -207,7 +201,7 @@ class Sitemap(LateTask):
continue
else:
continue # ignores all XML files except those presumed to be RSS
- post = self.site.post_per_file.get(path)
+ post = self.site.post_per_file.get(syspath)
if post and (post.is_draft or post.is_private or post.publish_later):
continue
path = path.replace(os.sep, '/')
@@ -227,12 +221,8 @@ class Sitemap(LateTask):
for rule in kw["robots_exclusions"]:
robot = robotparser.RobotFileParser()
robot.parse(["User-Agent: *", "Disallow: {0}".format(rule)])
- if sys.version_info[0] == 3:
- if not robot.can_fetch("*", '/' + path):
- return False # not robot food
- else:
- if not robot.can_fetch("*", ('/' + path).encode('utf-8')):
- return False # not robot food
+ if not robot.can_fetch("*", '/' + path):
+ return False # not robot food
return True
def write_sitemap():
@@ -322,6 +312,7 @@ class Sitemap(LateTask):
lastmod = datetime.datetime.utcfromtimestamp(os.stat(p).st_mtime).replace(tzinfo=dateutil.tz.gettz('UTC'), second=0, microsecond=0).isoformat().replace('+00:00', 'Z')
return lastmod
+
if __name__ == '__main__':
import doctest
doctest.testmod()
diff --git a/nikola/plugins/task/sources.plugin b/nikola/plugins/task/sources.plugin
index 66856f1..1ab1a3c 100644
--- a/nikola/plugins/task/sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Copy page sources into the output.
[Nikola]
-plugincategory = Task
+PluginCategory = Task
diff --git a/nikola/plugins/task/sources.py b/nikola/plugins/task/sources.py
index 0d77aba..1d36429 100644
--- a/nikola/plugins/task/sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -61,12 +61,8 @@ class Sources(Task):
# do not publish PHP sources
if post.source_ext(True) == post.compiler.extension():
continue
- source = post.source_path
- if lang != kw["default_lang"]:
- source_lang = utils.get_translation_candidate(self.site.config, source, lang)
- if os.path.exists(source_lang):
- source = source_lang
- if os.path.isfile(source):
+ source = post.translated_source_path(lang)
+ if source is not None and os.path.isfile(source):
yield {
'basename': 'render_sources',
'name': os.path.normpath(output_name),
diff --git a/nikola/plugins/task/tags.plugin b/nikola/plugins/task/tags.plugin
index c3a5be3..c17b7b3 100644
--- a/nikola/plugins/task/tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -1,5 +1,5 @@
[Core]
-name = render_tags
+name = classify_tags
module = tags
[Documentation]
@@ -9,5 +9,4 @@ website = https://getnikola.com/
description = Render the tag pages and feeds.
[Nikola]
-plugincategory = Task
-
+PluginCategory = Taxonomy
diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py
index 8b4683e..aecf8f5 100644
--- a/nikola/plugins/task/tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,497 +24,137 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Render the tag/category pages and feeds."""
+"""Render the tag pages and feeds."""
-from __future__ import unicode_literals
-import json
-import os
-import natsort
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-from nikola.plugin_categories import Task
+from nikola.plugin_categories import Taxonomy
from nikola import utils
-from nikola.nikola import _enclosure
-class RenderTags(Task):
- """Render the tag/category pages and feeds."""
+class ClassifyTags(Taxonomy):
+ """Classify the posts by tags."""
- name = "render_tags"
+ name = "classify_tags"
- def set_site(self, site):
- """Set Nikola site."""
- site.register_path_handler('tag_index', self.tag_index_path)
- site.register_path_handler('category_index', self.category_index_path)
- site.register_path_handler('tag', self.tag_path)
- site.register_path_handler('tag_atom', self.tag_atom_path)
- site.register_path_handler('tag_rss', self.tag_rss_path)
- site.register_path_handler('category', self.category_path)
- site.register_path_handler('category_atom', self.category_atom_path)
- site.register_path_handler('category_rss', self.category_rss_path)
- return super(RenderTags, self).set_site(site)
-
- def gen_tasks(self):
- """Render the tag pages and feeds."""
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "blog_title": self.site.config["BLOG_TITLE"],
- "site_url": self.site.config["SITE_URL"],
- "base_url": self.site.config["BASE_URL"],
- "messages": self.site.MESSAGES,
- "output_folder": self.site.config['OUTPUT_FOLDER'],
- "filters": self.site.config['FILTERS'],
- 'tag_path': self.site.config['TAG_PATH'],
- "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
- 'category_path': self.site.config['CATEGORY_PATH'],
- 'category_prefix': self.site.config['CATEGORY_PREFIX'],
- "category_pages_are_indexes": self.site.config['CATEGORY_PAGES_ARE_INDEXES'],
- "generate_rss": self.site.config['GENERATE_RSS'],
- "feed_teasers": self.site.config["FEED_TEASERS"],
- "feed_plain": self.site.config["FEED_PLAIN"],
- "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"],
- "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'],
- "feed_length": self.site.config['FEED_LENGTH'],
- "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
- "tzinfo": self.site.tzinfo,
- "pretty_urls": self.site.config['PRETTY_URLS'],
- "strip_indexes": self.site.config['STRIP_INDEXES'],
- "index_file": self.site.config['INDEX_FILE'],
- "category_pages_descriptions": self.site.config['CATEGORY_PAGES_DESCRIPTIONS'],
- "category_pages_titles": self.site.config['CATEGORY_PAGES_TITLES'],
- "tag_pages_descriptions": self.site.config['TAG_PAGES_DESCRIPTIONS'],
- "tag_pages_titles": self.site.config['TAG_PAGES_TITLES'],
- }
-
- self.site.scan_posts()
- yield self.group_task()
-
- yield self.list_tags_page(kw)
-
- if not self.site.posts_per_tag and not self.site.posts_per_category:
- return
-
- for lang in kw["translations"]:
- if kw['category_path'][lang] == kw['tag_path'][lang]:
- tags = {self.slugify_tag_name(tag, lang): tag for tag in self.site.tags_per_language[lang]}
- cats = {tuple(self.slugify_category_name(category, lang)): category for category in self.site.posts_per_category.keys()}
- categories = {k[0]: v for k, v in cats.items() if len(k) == 1}
- intersect = set(tags.keys()) & set(categories.keys())
- if len(intersect) > 0:
- for slug in intersect:
- utils.LOGGER.error("Category '{0}' and tag '{1}' both have the same slug '{2}' for language {3}!".format('/'.join(categories[slug]), tags[slug], slug, lang))
-
- # Test for category slug clashes
- categories = {}
- for category in self.site.posts_per_category.keys():
- slug = tuple(self.slugify_category_name(category, lang))
- for part in slug:
- if len(part) == 0:
- utils.LOGGER.error("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- if slug in categories:
- other_category = categories[slug]
- utils.LOGGER.error('You have categories that are too similar: {0} and {1} (language {2})'.format(category, other_category, lang))
- utils.LOGGER.error('Category {0} is used in: {1}'.format(category, ', '.join([p.source_path for p in self.site.posts_per_category[category]])))
- utils.LOGGER.error('Category {0} is used in: {1}'.format(other_category, ', '.join([p.source_path for p in self.site.posts_per_category[other_category]])))
- raise RuntimeError("Category '{0}' yields invalid slug '{1}'!".format(category, '/'.join(slug)))
- categories[slug] = category
-
- tag_list = list(self.site.posts_per_tag.items())
- cat_list = list(self.site.posts_per_category.items())
-
- def render_lists(tag, posts, is_category=True):
- """Render tag pages as RSS files and lists/indexes."""
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for lang in kw["translations"]:
- if kw["show_untranslated_posts"]:
- filtered_posts = post_list
- else:
- filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
- if kw["generate_rss"]:
- yield self.tag_rss(tag, lang, filtered_posts, kw, is_category)
- # Render HTML
- if kw['category_pages_are_indexes'] if is_category else kw['tag_pages_are_indexes']:
- yield self.tag_page_as_index(tag, lang, filtered_posts, kw, is_category)
- else:
- yield self.tag_page_as_list(tag, lang, filtered_posts, kw, is_category)
-
- for tag, posts in tag_list:
- for task in render_lists(tag, posts, False):
- yield task
-
- for path, posts in cat_list:
- for task in render_lists(path, posts, True):
- yield task
-
- # Tag cloud json file
- tag_cloud_data = {}
- for tag, posts in self.site.posts_per_tag.items():
- if tag in self.site.config['HIDDEN_TAGS']:
- continue
- tag_posts = dict(posts=[{'title': post.meta[post.default_lang]['title'],
- 'date': post.date.strftime('%m/%d/%Y'),
- 'isodate': post.date.isoformat(),
- 'url': post.permalink(post.default_lang)}
- for post in reversed(sorted(self.site.timeline, key=lambda post: post.date))
- if tag in post.alltags])
- tag_cloud_data[tag] = [len(posts), self.site.link(
- 'tag', tag, self.site.config['DEFAULT_LANG']), tag_posts]
- output_name = os.path.join(kw['output_folder'],
- 'assets', 'js', 'tag_cloud_data.json')
+ classification_name = "tag"
+ overview_page_variable_name = "tags"
+ overview_page_items_variable_name = "items"
+ more_than_one_classifications_per_post = True
+ has_hierarchy = False
+ show_list_as_subcategories_list = False
+ template_for_classification_overview = "tags.tmpl"
+ always_disable_rss = False
+ always_disable_atom = False
+ apply_to_posts = True
+ apply_to_pages = False
+ omit_empty_classifications = True
+ add_other_languages_variable = True
+ path_handler_docstrings = {
+ 'tag_index': """A link to the tag index.
- def write_tag_data(data):
- """Write tag data into JSON file, for use in tag clouds."""
- utils.makedirs(os.path.dirname(output_name))
- with open(output_name, 'w+') as fd:
- json.dump(data, fd, sort_keys=True)
+Example:
- if self.site.config['WRITE_TAG_CLOUD']:
- task = {
- 'basename': str(self.name),
- 'name': str(output_name)
- }
+link://tag_index => /tags/index.html""",
+ 'tag': """A link to a tag's page. Takes page number as optional keyword argument.
- task['uptodate'] = [utils.config_changed(tag_cloud_data, 'nikola.plugins.task.tags:tagdata')]
- task['targets'] = [output_name]
- task['actions'] = [(write_tag_data, [tag_cloud_data])]
- task['clean'] = True
- yield utils.apply_filters(task, kw['filters'])
+Example:
- def _create_tags_page(self, kw, lang, include_tags=True, include_categories=True):
- """Create a global "all your tags/categories" page for each language."""
- categories = [cat.category_name for cat in self.site.category_hierarchy]
- has_categories = (categories != []) and include_categories
- template_name = "tags.tmpl"
- kw = kw.copy()
- if include_categories:
- kw['categories'] = categories
- tags = natsort.natsorted([tag for tag in self.site.tags_per_language[lang]
- if len(self.site.posts_per_tag[tag]) >= kw["taglist_minimum_post_count"]],
- alg=natsort.ns.F | natsort.ns.IC)
- has_tags = (tags != []) and include_tags
- if include_tags:
- kw['tags'] = tags
- output_name = os.path.join(
- kw['output_folder'], self.site.path('tag_index' if has_tags else 'category_index', None, lang))
- context = {}
- if has_categories and has_tags:
- context["title"] = kw["messages"][lang]["Tags and Categories"]
- elif has_categories:
- context["title"] = kw["messages"][lang]["Categories"]
- else:
- context["title"] = kw["messages"][lang]["Tags"]
- if has_tags:
- context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
- in tags]
- else:
- context["items"] = None
- if has_categories:
- context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag
- in categories]
- context['cat_hierarchy'] = [(node.name, node.category_name, node.category_path,
- self.site.link("category", node.category_name),
- node.indent_levels, node.indent_change_before,
- node.indent_change_after)
- for node in self.site.category_hierarchy]
- else:
- context["cat_items"] = None
- context["permalink"] = self.site.link("tag_index" if has_tags else "category_index", None, lang)
- context["description"] = context["title"]
- context["pagekind"] = ["list", "tags_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:page')]
- task['basename'] = str(self.name)
- yield task
-
- def list_tags_page(self, kw):
- """Create a global "all your tags/categories" page for each language."""
- for lang in kw["translations"]:
- if self.site.config['TAG_PATH'][lang] == self.site.config['CATEGORY_PATH'][lang]:
- yield self._create_tags_page(kw, lang, True, True)
- else:
- yield self._create_tags_page(kw, lang, False, True)
- yield self._create_tags_page(kw, lang, True, False)
-
- def _get_title(self, tag, is_category):
- if is_category:
- return self.site.parse_category_name(tag)[-1]
- else:
- return tag
-
- def _get_indexes_title(self, tag, nice_tag, is_category, lang, messages):
- titles = self.site.config['CATEGORY_PAGES_TITLES'] if is_category else self.site.config['TAG_PAGES_TITLES']
- return titles[lang][tag] if lang in titles and tag in titles[lang] else messages[lang]["Posts about %s"] % nice_tag
-
- def _get_description(self, tag, is_category, lang):
- descriptions = self.site.config['CATEGORY_PAGES_DESCRIPTIONS'] if is_category else self.site.config['TAG_PAGES_DESCRIPTIONS']
- return descriptions[lang][tag] if lang in descriptions and tag in descriptions[lang] else None
-
- def _get_subcategories(self, category):
- node = self.site.category_hierarchy_lookup[category]
- return [(child.name, self.site.link("category", child.category_name)) for child in node.children]
+link://tag/cats => /tags/cats.html""",
+ 'tag_atom': """A link to a tag's Atom feed.
- def tag_page_as_index(self, tag, lang, post_list, kw, is_category):
- """Render a sort of index page collection using only this tag's posts."""
- kind = "category" if is_category else "tag"
+Example:
- def page_link(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_link(self.site.link(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension)
+link://tag_atom/cats => /tags/cats.atom""",
+ 'tag_rss': """A link to a tag's RSS feed.
- def page_path(i, displayed_i, num_pages, force_addition, extension=None):
- feed = "_atom" if extension == ".atom" else ""
- return utils.adjust_name_for_index_path(self.site.path(kind + feed, tag, lang), i, displayed_i, lang, self.site, force_addition, extension)
+Example:
- context_source = {}
- title = self._get_title(tag, is_category)
- if kw["generate_rss"]:
- # On a tag page, the feeds include the tag's feeds
- rss_link = ("""<link rel="alternate" type="application/rss+xml" """
- """title="RSS for tag """
- """{0} ({1})" href="{2}">""".format(
- title, lang, self.site.link(kind + "_rss", tag, lang)))
- context_source['rss_link'] = rss_link
- if is_category:
- context_source["category"] = tag
- context_source["category_path"] = self.site.parse_category_name(tag)
- context_source["tag"] = title
- indexes_title = self._get_indexes_title(tag, title, is_category, lang, kw["messages"])
- context_source["description"] = self._get_description(tag, is_category, lang)
- if is_category:
- context_source["subcategories"] = self._get_subcategories(tag)
- context_source["pagekind"] = ["index", "tag_page"]
- template_name = "tagindex.tmpl"
+link://tag_rss/cats => /tags/cats.xml""",
+ }
- yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path)
-
- def tag_page_as_list(self, tag, lang, post_list, kw, is_category):
- """Render a single flat link list with this tag's posts."""
- kind = "category" if is_category else "tag"
- template_name = "tag.tmpl"
- output_name = os.path.join(kw['output_folder'], self.site.path(
- kind, tag, lang))
- context = {}
- context["lang"] = lang
- title = self._get_title(tag, is_category)
- if is_category:
- context["category"] = tag
- context["category_path"] = self.site.parse_category_name(tag)
- context["tag"] = title
- context["title"] = self._get_indexes_title(tag, title, is_category, lang, kw["messages"])
- context["posts"] = post_list
- context["permalink"] = self.site.link(kind, tag, lang)
- context["kind"] = kind
- context["description"] = self._get_description(tag, is_category, lang)
- if is_category:
- context["subcategories"] = self._get_subcategories(tag)
- context["pagekind"] = ["list", "tag_page"]
- task = self.site.generic_post_list_renderer(
- lang,
- post_list,
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.tags:list')]
- task['basename'] = str(self.name)
- yield task
-
- if self.site.config['GENERATE_ATOM']:
- yield self.atom_feed_list(kind, tag, lang, post_list, context, kw)
+ def set_site(self, site):
+ """Set site, which is a Nikola instance."""
+ super().set_site(site)
+ self.show_list_as_index = self.site.config['TAG_PAGES_ARE_INDEXES']
+ self.template_for_single_list = "tagindex.tmpl" if self.show_list_as_index else "tag.tmpl"
+ self.minimum_post_count_per_classification_in_overview = self.site.config['TAGLIST_MINIMUM_POSTS']
+ self.translation_manager = utils.ClassificationTranslationManager()
- def atom_feed_list(self, kind, tag, lang, post_list, context, kw):
- """Generate atom feeds for tag lists."""
- if kind == 'tag':
- context['feedlink'] = self.site.abs_link(self.site.path('tag_atom', tag, lang))
- feed_path = os.path.join(kw['output_folder'], self.site.path('tag_atom', tag, lang))
- elif kind == 'category':
- context['feedlink'] = self.site.abs_link(self.site.path('category_atom', tag, lang))
- feed_path = os.path.join(kw['output_folder'], self.site.path('category_atom', tag, lang))
+ def is_enabled(self, lang=None):
+ """Return True if this taxonomy is enabled, or False otherwise."""
+ return True
- task = {
- 'basename': str(self.name),
- 'name': feed_path,
- 'targets': [feed_path],
- 'actions': [(self.site.atom_feed_renderer, (lang, post_list, feed_path, kw['filters'], context))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:atom')],
- 'task_dep': ['render_posts'],
- }
- return task
+ def classify(self, post, lang):
+ """Classify the given post for the given language."""
+ return post.tags_for_language(lang)
- def tag_rss(self, tag, lang, posts, kw, is_category):
- """Create a RSS feed for a single tag in a given language."""
- kind = "category" if is_category else "tag"
- # Render RSS
- output_name = os.path.normpath(
- os.path.join(kw['output_folder'],
- self.site.path(kind + "_rss", tag, lang)))
- feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/'))
- deps = []
- deps_uptodate = []
- post_list = sorted(posts, key=lambda a: a.date)
- post_list.reverse()
- for post in post_list:
- deps += post.deps(lang)
- deps_uptodate += post.deps_uptodate(lang)
- task = {
- 'basename': str(self.name),
- 'name': output_name,
- 'file_dep': deps,
- 'targets': [output_name],
- 'actions': [(utils.generic_rss_renderer,
- (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, is_category)),
- kw["site_url"], None, post_list,
- output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
- feed_url, _enclosure, kw["feed_link_append_query"]))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate,
- 'task_dep': ['render_posts'],
- }
- return utils.apply_filters(task, kw['filters'])
+ def get_classification_friendly_name(self, classification, lang, only_last_component=False):
+ """Extract a friendly name from the classification."""
+ return classification
def slugify_tag_name(self, name, lang):
"""Slugify a tag name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderTags.slugify_tag_name() called without language!")
- lang = ''
if self.site.config['SLUG_TAG_PATH']:
name = utils.slugify(name, lang)
return name
- def tag_index_path(self, name, lang):
- """A link to the tag index.
-
- Example:
-
- link://tag_index => /tags/index.html
- """
- if self.site.config['TAGS_INDEX_PATH'][lang]:
- paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAGS_INDEX_PATH'][lang]] if _f]
+ def get_overview_path(self, lang, dest_type='page'):
+ """Return a path for the list of all classifications."""
+ if self.site.config['TAGS_INDEX_PATH'](lang):
+ path = self.site.config['TAGS_INDEX_PATH'](lang)
+ append_index = 'never'
else:
- paths = [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.site.config['INDEX_FILE']] if _f]
- return paths
-
- def category_index_path(self, name, lang):
- """A link to the category index.
-
- Example:
-
- link://category_index => /categories/index.html
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang],
- self.site.config['INDEX_FILE']] if _f]
-
- def tag_path(self, name, lang):
- """A link to a tag's page.
-
- Example:
-
- link://tag/cats => /tags/cats.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.slugify_tag_name(name, lang),
- self.site.config['INDEX_FILE']] if _f]
- else:
- return [_f for _f in [
- self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang],
- self.slugify_tag_name(name, lang) + ".html"] if _f]
-
- def tag_atom_path(self, name, lang):
- """A link to a tag's Atom feed.
-
- Example:
-
- link://tag_atom/cats => /tags/cats.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".atom"] if
- _f]
-
- def tag_rss_path(self, name, lang):
- """A link to a tag's RSS feed.
-
- Example:
-
- link://tag_rss/cats => /tags/cats.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['TAG_PATH'][lang], self.slugify_tag_name(name, lang) + ".xml"] if
- _f]
-
- def slugify_category_name(self, name, lang):
- """Slugify a category name."""
- if lang is None: # TODO: remove in v8
- utils.LOGGER.warn("RenderTags.slugify_category_name() called without language!")
- lang = ''
- path = self.site.parse_category_name(name)
- if self.site.config['CATEGORY_OUTPUT_FLAT_HIERARCHY']:
- path = path[-1:] # only the leaf
- result = [self.slugify_tag_name(part, lang) for part in path]
- result[0] = self.site.config['CATEGORY_PREFIX'] + result[0]
- if not self.site.config['PRETTY_URLS']:
- result = ['-'.join(result)]
- return result
-
- def _add_extension(self, path, extension):
- path[-1] += extension
- return path
-
- def category_path(self, name, lang):
- """A link to a category.
-
- Example:
-
- link://category/dogs => /categories/dogs.html
- """
- if self.site.config['PRETTY_URLS']:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self.slugify_category_name(name, lang) + [self.site.config['INDEX_FILE']]
- else:
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".html")
-
- def category_atom_path(self, name, lang):
- """A link to a category's Atom feed.
-
- Example:
-
- link://category_atom/dogs => /categories/dogs.atom
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".atom")
+ path = self.site.config['TAG_PATH'](lang)
+ append_index = 'always'
+ return [component for component in path.split('/') if component], append_index
+
+ def get_path(self, classification, lang, dest_type='page'):
+ """Return a path for the given classification."""
+ return [_f for _f in [
+ self.site.config['TAG_PATH'](lang),
+ self.slugify_tag_name(classification, lang)] if _f], 'auto'
+
+ def provide_overview_context_and_uptodate(self, lang):
+ """Provide data for the context and the uptodate list for the list of all classifiations."""
+ kw = {
+ "tag_path": self.site.config['TAG_PATH'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
+ "tzinfo": self.site.tzinfo,
+ "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'],
+ "tag_titles": self.site.config['TAG_TITLES'],
+ }
+ context = {
+ "title": self.site.MESSAGES[lang]["Tags"],
+ "description": self.site.MESSAGES[lang]["Tags"],
+ "pagekind": ["list", "tags_page"],
+ }
+ kw.update(context)
+ return context, kw
- def category_rss_path(self, name, lang):
- """A link to a category's RSS feed.
+ def provide_context_and_uptodate(self, classification, lang, node=None):
+ """Provide data for the context and the uptodate list for the list of the given classifiation."""
+ kw = {
+ "tag_path": self.site.config['TAG_PATH'],
+ "tag_pages_are_indexes": self.site.config['TAG_PAGES_ARE_INDEXES'],
+ "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'],
+ "tzinfo": self.site.tzinfo,
+ "tag_descriptions": self.site.config['TAG_DESCRIPTIONS'],
+ "tag_titles": self.site.config['TAG_TITLES'],
+ }
+ context = {
+ "title": self.site.config['TAG_TITLES'].get(lang, {}).get(classification, self.site.MESSAGES[lang]["Posts about %s"] % classification),
+ "description": self.site.config['TAG_DESCRIPTIONS'].get(lang, {}).get(classification),
+ "pagekind": ["tag_page", "index" if self.show_list_as_index else "list"],
+ "tag": classification,
+ }
+ kw.update(context)
+ return context, kw
- Example:
+ def get_other_language_variants(self, classification, lang, classifications_per_language):
+ """Return a list of variants of the same tag in other languages."""
+ return self.translation_manager.get_translations_as_list(classification, lang, classifications_per_language)
- link://category_rss/dogs => /categories/dogs.xml
- """
- return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
- self.site.config['CATEGORY_PATH'][lang]] if
- _f] + self._add_extension(self.slugify_category_name(name, lang), ".xml")
+ def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None):
+ """Rearrange, modify or otherwise use the list of posts per classification and per language."""
+ self.translation_manager.read_from_config(self.site, 'TAG', posts_per_classification_per_language, False)
diff --git a/nikola/plugins/task/taxonomies.plugin b/nikola/plugins/task/taxonomies.plugin
new file mode 100644
index 0000000..5bda812
--- /dev/null
+++ b/nikola/plugins/task/taxonomies.plugin
@@ -0,0 +1,12 @@
+[Core]
+name = render_taxonomies
+module = taxonomies
+
+[Documentation]
+author = Roberto Alsina
+version = 1.0
+website = https://getnikola.com/
+description = Render the taxonomy overviews, classification pages and feeds.
+
+[Nikola]
+PluginCategory = Task
diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py
new file mode 100644
index 0000000..7dcf6ed
--- /dev/null
+++ b/nikola/plugins/task/taxonomies.py
@@ -0,0 +1,459 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2020 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Render the taxonomy overviews, classification pages and feeds."""
+
+import os
+from collections import defaultdict
+from copy import copy
+from urllib.parse import urljoin
+
+import blinker
+import natsort
+
+from nikola import utils, hierarchy_utils
+from nikola.nikola import _enclosure
+from nikola.plugin_categories import Task
+
+
+class RenderTaxonomies(Task):
+ """Render taxonomy pages and feeds."""
+
+ name = "render_taxonomies"
+
+ def _generate_classification_overview_kw_context(self, taxonomy, lang):
+ """Create context and kw for a classification overview page."""
+ context, kw = taxonomy.provide_overview_context_and_uptodate(lang)
+
+ context = copy(context)
+ context["kind"] = "{}_index".format(taxonomy.classification_name)
+ sorted_links = []
+ for other_lang in sorted(self.site.config['TRANSLATIONS'].keys()):
+ if other_lang != lang:
+ sorted_links.append((other_lang, None, None))
+ # Put the current language in front, so that it appears first in links
+ # (Issue #3248)
+ sorted_links_all = [(lang, None, None)] + sorted_links
+ context['has_other_languages'] = True
+ context['other_languages'] = sorted_links
+ context['all_languages'] = sorted_links_all
+
+ kw = copy(kw)
+ kw["messages"] = self.site.MESSAGES
+ kw["translations"] = self.site.config['TRANSLATIONS']
+ kw["filters"] = self.site.config['FILTERS']
+ kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview
+ kw["output_folder"] = self.site.config['OUTPUT_FOLDER']
+ kw["pretty_urls"] = self.site.config['PRETTY_URLS']
+ kw["strip_indexes"] = self.site.config['STRIP_INDEXES']
+ kw["index_file"] = self.site.config['INDEX_FILE']
+
+ # Collect all relevant classifications
+ if taxonomy.has_hierarchy:
+ def acceptor(node):
+ return len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang)) >= kw["minimum_post_count"]
+
+ clipped_root_list = [hierarchy_utils.clone_treenode(node, parent=None, acceptor=acceptor) for node in self.site.hierarchy_per_classification[taxonomy.classification_name][lang]]
+ clipped_root_list = [node for node in clipped_root_list if node]
+ clipped_flat_hierarchy = hierarchy_utils.flatten_tree_structure(clipped_root_list)
+
+ classifications = [cat.classification_name for cat in clipped_flat_hierarchy]
+ else:
+ classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items()
+ if len(self._filter_list(posts, lang)) >= kw["minimum_post_count"]],
+ alg=natsort.ns.F | natsort.ns.IC)
+ taxonomy.sort_classifications(classifications, lang)
+
+ # Set up classifications in context
+ context[taxonomy.overview_page_variable_name] = classifications
+ context["has_hierarchy"] = taxonomy.has_hierarchy
+ if taxonomy.overview_page_items_variable_name:
+ items = [(classification,
+ self.site.link(taxonomy.classification_name, classification, lang))
+ for classification in classifications]
+ items_with_postcount = [
+ (classification,
+ self.site.link(taxonomy.classification_name, classification, lang),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][classification], lang)))
+ for classification in classifications
+ ]
+ context[taxonomy.overview_page_items_variable_name] = items
+ context[taxonomy.overview_page_items_variable_name + "_with_postcount"] = items_with_postcount
+ if taxonomy.has_hierarchy and taxonomy.overview_page_hierarchy_variable_name:
+ hier_items = [
+ (node.name, node.classification_name, node.classification_path,
+ self.site.link(taxonomy.classification_name, node.classification_name, lang),
+ node.indent_levels, node.indent_change_before,
+ node.indent_change_after)
+ for node in clipped_flat_hierarchy
+ ]
+ hier_items_with_postcount = [
+ (node.name, node.classification_name, node.classification_path,
+ self.site.link(taxonomy.classification_name, node.classification_name, lang),
+ node.indent_levels, node.indent_change_before,
+ node.indent_change_after,
+ len(node.children),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name], lang)))
+ for node in clipped_flat_hierarchy
+ ]
+ context[taxonomy.overview_page_hierarchy_variable_name] = hier_items
+ context[taxonomy.overview_page_hierarchy_variable_name + '_with_postcount'] = hier_items_with_postcount
+ return context, kw
+
+ def _render_classification_overview(self, classification_name, template, lang, context, kw):
+ # Prepare rendering
+ context["permalink"] = self.site.link("{}_index".format(classification_name), None, lang)
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "tags_page"]
+ output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(classification_name), None, lang))
+ blinker.signal('generate_classification_overview').send({
+ 'site': self.site,
+ 'classification_name': classification_name,
+ 'lang': lang,
+ 'context': context,
+ 'kw': kw,
+ 'output_name': output_name,
+ })
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template,
+ kw['filters'],
+ context,
+ )
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:page')]
+ task['basename'] = str(self.name)
+ yield task
+
+ def _generate_classification_overview(self, taxonomy, lang):
+ """Create a global "all your tags/categories" page for a given language."""
+ context, kw = self._generate_classification_overview_kw_context(taxonomy, lang)
+ for task in self._render_classification_overview(taxonomy.classification_name, taxonomy.template_for_classification_overview, lang, context, kw):
+ yield task
+
+ def _generate_tag_and_category_overview(self, tag_taxonomy, category_taxonomy, lang):
+ """Create a global "all your tags/categories" page for a given language."""
+ # Create individual contexts and kw dicts
+ tag_context, tag_kw = self._generate_classification_overview_kw_context(tag_taxonomy, lang)
+ cat_context, cat_kw = self._generate_classification_overview_kw_context(category_taxonomy, lang)
+
+ # Combine resp. select dicts
+ if tag_context['items'] and cat_context['cat_items']:
+ # Combine contexts. We must merge the tag context into the category context
+ # so that tag_context['items'] makes it into the result.
+ context = cat_context
+ context.update(tag_context)
+ kw = cat_kw
+ kw.update(tag_kw)
+
+ # Update title
+ title = self.site.MESSAGES[lang]["Tags and Categories"]
+ context['title'] = title
+ context['description'] = title
+ kw['title'] = title
+ kw['description'] = title
+ elif cat_context['cat_items']:
+ # Use category overview page
+ context = cat_context
+ kw = cat_kw
+ else:
+ # Use tag overview page
+ context = tag_context
+ kw = tag_kw
+
+ # Render result
+ for task in self._render_classification_overview('tag', tag_taxonomy.template_for_classification_overview, lang, context, kw):
+ yield task
+
+ def _generate_classification_page_as_rss(self, taxonomy, classification, filtered_posts, title, description, kw, lang):
+ """Create a RSS feed for a single classification in a given language."""
+ kind = taxonomy.classification_name
+ # Render RSS
+ output_name = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind + "_rss", classification, lang)))
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", classification, lang).lstrip('/'))
+ deps = []
+ deps_uptodate = []
+ for post in filtered_posts:
+ deps += post.deps(lang)
+ deps_uptodate += post.deps_uptodate(lang)
+ blog_title = kw["blog_title"](lang)
+ task = {
+ 'basename': str(self.name),
+ 'name': output_name,
+ 'file_dep': deps,
+ 'targets': [output_name],
+ 'actions': [(utils.generic_rss_renderer,
+ (lang, "{0} ({1})".format(blog_title, title) if blog_title != title else blog_title,
+ kw["site_url"], description, filtered_posts,
+ output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'],
+ feed_url, _enclosure, kw["feed_links_append_query"]))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:rss')] + deps_uptodate,
+ 'task_dep': ['render_posts'],
+ }
+ return utils.apply_filters(task, kw['filters'])
+
+ def _generate_classification_page_as_index(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Render an index page collection using only this classification's posts."""
+ kind = taxonomy.classification_name
+
+ def page_link(i, displayed_i, num_pages, force_addition, extension=None):
+ return self.site.link(kind, classification, lang, alternative_path=force_addition, page=i)
+
+ def page_path(i, displayed_i, num_pages, force_addition, extension=None):
+ return self.site.path(kind, classification, lang, alternative_path=force_addition, page=i)
+
+ context = copy(context)
+ context["kind"] = kind
+ if "pagekind" not in context:
+ context["pagekind"] = ["index", "tag_page"]
+ template_name = taxonomy.template_for_single_list
+
+ yield self.site.generic_index_renderer(lang, filtered_posts, context['title'], template_name, context, kw, str(self.name), page_link, page_path)
+
+ def _generate_classification_page_as_atom(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Generate atom feeds for classification lists."""
+ kind = taxonomy.classification_name
+
+ context = copy(context)
+ context["kind"] = kind
+
+ yield self.site.generic_atom_renderer(lang, filtered_posts, context, kw, str(self.name), classification, kind)
+
+ def _generate_classification_page_as_list(self, taxonomy, classification, filtered_posts, context, kw, lang):
+ """Render a single flat link list with this classification's posts."""
+ kind = taxonomy.classification_name
+ template_name = taxonomy.template_for_single_list
+ output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang))
+ context["lang"] = lang
+ # list.tmpl expects a different format than list_post.tmpl (Issue #2701)
+ if template_name == 'list.tmpl':
+ context["items"] = [(post.title(lang), post.permalink(lang), None) for post in filtered_posts]
+ else:
+ context["posts"] = filtered_posts
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "tag_page"]
+ task = self.site.generic_post_list_renderer(lang, filtered_posts, output_name, template_name, kw['filters'], context)
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:list')]
+ task['basename'] = str(self.name)
+ yield task
+
+ def _filter_list(self, post_list, lang):
+ """Return only the posts which should be shown for this language."""
+ if self.site.config["SHOW_UNTRANSLATED_POSTS"]:
+ return post_list
+ else:
+ return [x for x in post_list if x.is_translation_available(lang)]
+
+ def _generate_subclassification_page(self, taxonomy, node, context, kw, lang):
+ """Render a list of subclassifications."""
+ def get_subnode_data(subnode):
+ return [
+ taxonomy.get_classification_friendly_name(subnode.classification_name, lang, only_last_component=True),
+ self.site.link(taxonomy.classification_name, subnode.classification_name, lang),
+ len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][subnode.classification_name], lang))
+ ]
+
+ items = [get_subnode_data(subnode) for subnode in node.children]
+ context = copy(context)
+ context["lang"] = lang
+ context["permalink"] = self.site.link(taxonomy.classification_name, node.classification_name, lang)
+ if "pagekind" not in context:
+ context["pagekind"] = ["list", "archive_page"]
+ context["items"] = items
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ os.path.join(kw['output_folder'], self.site.path(taxonomy.classification_name, node.classification_name, lang)),
+ taxonomy.subcategories_list_template,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: kw, 2: items}
+ task['uptodate'] = task['uptodate'] + [utils.config_changed(task_cfg, 'nikola.plugins.task.taxonomy')]
+ task['basename'] = self.name
+ return task
+
+ def _generate_classification_page(self, taxonomy, classification, filtered_posts, generate_list, generate_rss, generate_atom, lang, post_lists_per_lang, classification_set_per_lang=None):
+ """Render index or post list and associated feeds per classification."""
+ # Should we create this list?
+ if not any((generate_list, generate_rss, generate_atom)):
+ return
+ # Get data
+ node = None
+ if taxonomy.has_hierarchy:
+ node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang].get(classification)
+ context, kw = taxonomy.provide_context_and_uptodate(classification, lang, node)
+ kw = copy(kw)
+ kw["messages"] = self.site.MESSAGES
+ kw["translations"] = self.site.config['TRANSLATIONS']
+ kw["filters"] = self.site.config['FILTERS']
+ kw["site_url"] = self.site.config['SITE_URL']
+ kw["blog_title"] = self.site.config['BLOG_TITLE']
+ kw["generate_rss"] = self.site.config['GENERATE_RSS']
+ kw["generate_atom"] = self.site.config['GENERATE_ATOM']
+ kw["feed_teasers"] = self.site.config["FEED_TEASERS"]
+ kw["feed_plain"] = self.site.config["FEED_PLAIN"]
+ kw["feed_links_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"]
+ kw["feed_length"] = self.site.config['FEED_LENGTH']
+ kw["output_folder"] = self.site.config['OUTPUT_FOLDER']
+ kw["pretty_urls"] = self.site.config['PRETTY_URLS']
+ kw["strip_indexes"] = self.site.config['STRIP_INDEXES']
+ kw["index_file"] = self.site.config['INDEX_FILE']
+ context = copy(context)
+ context["permalink"] = self.site.link(taxonomy.classification_name, classification, lang)
+ context["kind"] = taxonomy.classification_name
+ # Get links to other language versions of this classification
+ if classification_set_per_lang is not None:
+ other_lang_links = taxonomy.get_other_language_variants(classification, lang, classification_set_per_lang)
+ # Collect by language
+ links_per_lang = defaultdict(list)
+ for other_lang, link in other_lang_links:
+ # Make sure we ignore the current language (in case the
+ # plugin accidentally returns links for it as well)
+ if other_lang != lang:
+ links_per_lang[other_lang].append(link)
+ # Sort first by language, then by classification
+ sorted_links = []
+ sorted_links_all = []
+ for other_lang in sorted(list(links_per_lang.keys()) + [lang]):
+ if other_lang == lang:
+ sorted_links_all.append((lang, classification, taxonomy.get_classification_friendly_name(classification, lang)))
+ else:
+ links = hierarchy_utils.sort_classifications(taxonomy, links_per_lang[other_lang], other_lang)
+ links = [(other_lang, other_classification,
+ taxonomy.get_classification_friendly_name(other_classification, other_lang))
+ for other_classification in links if post_lists_per_lang[other_lang].get(other_classification, ('', False, False))[1]]
+ sorted_links.extend(links)
+ sorted_links_all.extend(links)
+ # Store result in context and kw
+ context['has_other_languages'] = True
+ context['other_languages'] = sorted_links
+ context['all_languages'] = sorted_links_all
+ kw['other_languages'] = sorted_links
+ kw['all_languages'] = sorted_links_all
+ else:
+ context['has_other_languages'] = False
+ # Allow other plugins to modify the result
+ blinker.signal('generate_classification_page').send({
+ 'site': self.site,
+ 'taxonomy': taxonomy,
+ 'classification': classification,
+ 'lang': lang,
+ 'posts': filtered_posts,
+ 'context': context,
+ 'kw': kw,
+ })
+ # Decide what to do
+ if taxonomy.has_hierarchy and taxonomy.show_list_as_subcategories_list:
+ # Determine whether there are subcategories
+ node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang][classification]
+ # Are there subclassifications?
+ if len(node.children) > 0:
+ # Yes: create list with subclassifications instead of list of posts
+ if generate_list:
+ yield self._generate_subclassification_page(taxonomy, node, context, kw, lang)
+ return
+ # Generate RSS feed
+ if generate_rss and kw["generate_rss"] and not taxonomy.always_disable_rss:
+ yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], context.get("description"), kw, lang)
+
+ # Generate Atom feed
+ if generate_atom and kw["generate_atom"] and not taxonomy.always_disable_atom:
+ yield self._generate_classification_page_as_atom(taxonomy, classification, filtered_posts, context, kw, lang)
+
+ # Render HTML
+ if generate_list and taxonomy.show_list_as_index:
+ yield self._generate_classification_page_as_index(taxonomy, classification, filtered_posts, context, kw, lang)
+ elif generate_list:
+ yield self._generate_classification_page_as_list(taxonomy, classification, filtered_posts, context, kw, lang)
+
+ def gen_tasks(self):
+ """Render the tag pages and feeds."""
+ self.site.scan_posts()
+ yield self.group_task()
+
+ # Cache classification sets per language for taxonomies where
+ # add_other_languages_variable is True.
+ classification_set_per_lang = {}
+ for taxonomy in self.site.taxonomy_plugins.values():
+ if taxonomy.add_other_languages_variable:
+ lookup = self.site.posts_per_classification[taxonomy.classification_name]
+ cspl = {lang: set(lookup[lang].keys()) for lang in lookup}
+ classification_set_per_lang[taxonomy.classification_name] = cspl
+
+ # Collect post lists for classification pages and determine whether
+ # they should be generated.
+ post_lists_per_lang = {}
+ for taxonomy in self.site.taxonomy_plugins.values():
+ plpl = {}
+ for lang in self.site.config["TRANSLATIONS"]:
+ result = {}
+ for classification, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items():
+ # Filter list
+ filtered_posts = self._filter_list(posts, lang)
+ if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications:
+ generate_list = generate_rss = generate_atom = False
+ else:
+ # Should we create this list?
+ generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang)
+ generate_rss = taxonomy.should_generate_rss_for_classification_page(classification, filtered_posts, lang)
+ generate_atom = taxonomy.should_generate_atom_for_classification_page(classification, filtered_posts, lang)
+ result[classification] = (filtered_posts, generate_list, generate_rss, generate_atom)
+ plpl[lang] = result
+ post_lists_per_lang[taxonomy.classification_name] = plpl
+
+ # Now generate pages
+ for lang in self.site.config["TRANSLATIONS"]:
+ # To support that tag and category classifications share the same overview,
+ # we explicitly detect this case:
+ ignore_plugins_for_overview = set()
+ if 'tag' in self.site.taxonomy_plugins and 'category' in self.site.taxonomy_plugins and self.site.link("tag_index", None, lang) == self.site.link("category_index", None, lang):
+ # Block both plugins from creating overviews
+ ignore_plugins_for_overview.add(self.site.taxonomy_plugins['tag'])
+ ignore_plugins_for_overview.add(self.site.taxonomy_plugins['category'])
+ for taxonomy in self.site.taxonomy_plugins.values():
+ if not taxonomy.is_enabled(lang):
+ continue
+ # Generate list of classifications (i.e. classification overview)
+ if taxonomy not in ignore_plugins_for_overview:
+ if taxonomy.template_for_classification_overview is not None:
+ for task in self._generate_classification_overview(taxonomy, lang):
+ yield task
+
+ # Process classifications
+ for classification, (filtered_posts, generate_list, generate_rss, generate_atom) in post_lists_per_lang[taxonomy.classification_name][lang].items():
+ for task in self._generate_classification_page(taxonomy, classification, filtered_posts,
+ generate_list, generate_rss, generate_atom, lang,
+ post_lists_per_lang[taxonomy.classification_name],
+ classification_set_per_lang.get(taxonomy.classification_name)):
+ yield task
+ # In case we are ignoring plugins for overview, we must have a collision for
+ # tags and categories. Handle this special case with extra code.
+ if ignore_plugins_for_overview:
+ for task in self._generate_tag_and_category_overview(self.site.taxonomy_plugins['tag'], self.site.taxonomy_plugins['category'], lang):
+ yield task
diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py
index d5efd61..a530db4 100644
--- a/nikola/plugins/template/__init__.py
+++ b/nikola/plugins/template/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
diff --git a/nikola/plugins/template/jinja.plugin b/nikola/plugins/template/jinja.plugin
index 78fd41b..629b20e 100644
--- a/nikola/plugins/template/jinja.plugin
+++ b/nikola/plugins/template/jinja.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Support for Jinja2 templates.
[Nikola]
-plugincategory = Template
+PluginCategory = Template
diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py
index 5a2135f..7795739 100644
--- a/nikola/plugins/template/jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,21 +24,20 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
"""Jinja template handler."""
-from __future__ import unicode_literals
-import os
import io
import json
+import os
+
+from nikola.plugin_categories import TemplateSystem
+from nikola.utils import makedirs, req_missing, sort_posts, _smartjoin_filter
+
try:
import jinja2
from jinja2 import meta
except ImportError:
- jinja2 = None # NOQA
-
-from nikola.plugin_categories import TemplateSystem
-from nikola.utils import makedirs, req_missing
+ jinja2 = None
class JinjaTemplates(TemplateSystem):
@@ -65,6 +64,8 @@ class JinjaTemplates(TemplateSystem):
self.lookup.trim_blocks = True
self.lookup.lstrip_blocks = True
self.lookup.filters['tojson'] = json.dumps
+ self.lookup.filters['sort_posts'] = sort_posts
+ self.lookup.filters['smartjoin'] = _smartjoin_filter
self.lookup.globals['enumerate'] = enumerate
self.lookup.globals['isinstance'] = isinstance
self.lookup.globals['tuple'] = tuple
@@ -107,7 +108,7 @@ class JinjaTemplates(TemplateSystem):
"""Find dependencies for a template string."""
deps = set([])
ast = self.lookup.parse(text)
- dep_names = meta.find_referenced_templates(ast)
+ dep_names = [d for d in meta.find_referenced_templates(ast) if d]
for dep_name in dep_names:
filename = self.lookup.loader.get_source(self.lookup, dep_name)[1]
sub_deps = [filename] + self.get_deps(filename)
@@ -117,7 +118,7 @@ class JinjaTemplates(TemplateSystem):
def get_deps(self, filename):
"""Return paths to dependencies for the template loaded from filename."""
- with io.open(filename, 'r', encoding='utf-8') as fd:
+ with io.open(filename, 'r', encoding='utf-8-sig') as fd:
text = fd.read()
return self.get_string_deps(text)
diff --git a/nikola/plugins/template/mako.plugin b/nikola/plugins/template/mako.plugin
index 308d291..2d353bf 100644
--- a/nikola/plugins/template/mako.plugin
+++ b/nikola/plugins/template/mako.plugin
@@ -9,5 +9,5 @@ website = https://getnikola.com/
description = Support for Mako templates.
[Nikola]
-plugincategory = Template
+PluginCategory = Template
diff --git a/nikola/plugins/template/mako.py b/nikola/plugins/template/mako.py
index 0c9bb64..30e2041 100644
--- a/nikola/plugins/template/mako.py
+++ b/nikola/plugins/template/mako.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,12 +26,9 @@
"""Mako template handler."""
-from __future__ import unicode_literals, print_function, absolute_import
import io
import os
import shutil
-import sys
-import tempfile
from mako import exceptions, util, lexer, parsetree
from mako.lookup import TemplateLookup
@@ -39,9 +36,9 @@ from mako.template import Template
from markupsafe import Markup # It's ok, Mako requires it
from nikola.plugin_categories import TemplateSystem
-from nikola.utils import makedirs, get_logger, STDERR_HANDLER
+from nikola.utils import makedirs, get_logger
-LOGGER = get_logger('mako', STDERR_HANDLER)
+LOGGER = get_logger('mako')
class MakoTemplates(TemplateSystem):
@@ -57,7 +54,7 @@ class MakoTemplates(TemplateSystem):
def get_string_deps(self, text, filename=None):
"""Find dependencies for a template string."""
- lex = lexer.Lexer(text=text, filename=filename)
+ lex = lexer.Lexer(text=text, filename=filename, input_encoding='utf-8')
lex.parse()
deps = []
@@ -68,7 +65,12 @@ class MakoTemplates(TemplateSystem):
# Some templates will include "foo.tmpl" and we need paths, so normalize them
# using the template lookup
for i, d in enumerate(deps):
- deps[i] = self.get_template_path(d)
+ dep = self.get_template_path(d)
+ if dep:
+ deps[i] = dep
+ else:
+ LOGGER.error("Cannot find template {0} referenced in {1}",
+ d, filename)
return deps
def get_deps(self, filename):
@@ -79,13 +81,6 @@ class MakoTemplates(TemplateSystem):
def set_directories(self, directories, cache_folder):
"""Create a new template lookup with set directories."""
cache_dir = os.path.join(cache_folder, '.mako.tmp')
- # Workaround for a Mako bug, Issue #825
- if sys.version_info[0] == 2:
- try:
- os.path.abspath(cache_dir).decode('ascii')
- except UnicodeEncodeError:
- cache_dir = tempfile.mkdtemp()
- LOGGER.warning('Because of a Mako bug, setting cache_dir to {0}'.format(cache_dir))
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
self.directories = directories
@@ -103,6 +98,7 @@ class MakoTemplates(TemplateSystem):
self.lookup = TemplateLookup(
directories=self.directories,
module_directory=self.cache_dir,
+ input_encoding='utf-8',
output_encoding='utf-8')
def set_site(self, site):
@@ -135,9 +131,10 @@ class MakoTemplates(TemplateSystem):
dep_filenames = self.get_deps(template.filename)
deps = [template.filename]
for fname in dep_filenames:
- deps += [fname] + self.get_deps(fname)
- self.cache[template_name] = deps
- return list(self.cache[template_name])
+ # yes, it uses forward slashes on Windows
+ deps += self.template_deps(fname.split('/')[-1])
+ self.cache[template_name] = list(set(deps))
+ return self.cache[template_name]
def get_template_path(self, template_name):
"""Get the path to a template or return None."""
diff --git a/nikola/post.py b/nikola/post.py
index 37e4241..82d957d 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,36 +26,25 @@
"""The Post class."""
-from __future__ import unicode_literals, print_function, absolute_import
-
import io
-from collections import defaultdict
import datetime
import hashlib
import json
import os
import re
-import string
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-
-from . import utils
+from collections import defaultdict
+from math import ceil # for reading time feature
+from urllib.parse import urljoin
-from blinker import signal
import dateutil.tz
import lxml.html
import natsort
-try:
- import pyphen
-except ImportError:
- pyphen = None
-
-from math import ceil # for reading time feature
+from blinker import signal
# for tearDown with _reload we cannot use 'from import' to get forLocaleBorg
import nikola.utils
+from . import metadata_extractors
+from . import utils
from .utils import (
current_time,
Functionary,
@@ -63,22 +52,37 @@ from .utils import (
LocaleBorg,
slugify,
to_datetime,
- unicode_str,
demote_headers,
get_translation_candidate,
- unslugify,
+ map_metadata
)
-from .rc4 import rc4
+
+try:
+ import pyphen
+except ImportError:
+ pyphen = None
+
__all__ = ('Post',)
-TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE)
-_UPGRADE_METADATA_ADVERTISED = False
+TEASER_REGEXP = re.compile(r'<!--\s*(TEASER_END|END_TEASER)(:(.+))?\s*-->', re.IGNORECASE)
class Post(object):
"""Represent a blog post or site page."""
+ _prev_post = None
+ _next_post = None
+ is_draft = False
+ is_private = False
+ _is_two_file = None
+ _reading_time = None
+ _remaining_reading_time = None
+ _paragraph_count = None
+ _remaining_paragraph_count = None
+ post_status = 'published'
+ has_oldstyle_metadata_tags = False
+
def __init__(
self,
source_path,
@@ -87,73 +91,249 @@ class Post(object):
use_in_feeds,
messages,
template_name,
- compiler
+ compiler,
+ destination_base=None,
+ metadata_extractors_by=None
):
"""Initialize post.
The source path is the user created post file. From it we calculate
the meta file, as well as any translations available, and
the .html fragment file path.
+
+ destination_base must be None or a TranslatableSetting instance. If
+ specified, it will be prepended to the destination path.
"""
- self.config = config
+ self._load_config(config)
+ self._set_paths(source_path)
+
self.compiler = compiler
- self.compile_html = self.compiler.compile_html
+ self.is_post = use_in_feeds
+ self.messages = messages
+ self._template_name = template_name
+ self.compile_html = self.compiler.compile
self.demote_headers = self.compiler.demote_headers and self.config['DEMOTE_HEADERS']
- tzinfo = self.config['__tzinfo__']
+ self._dependency_file_fragment = defaultdict(list)
+ self._dependency_file_page = defaultdict(list)
+ self._dependency_uptodate_fragment = defaultdict(list)
+ self._dependency_uptodate_page = defaultdict(list)
+ self._depfile = defaultdict(list)
+ if metadata_extractors_by is None:
+ self.metadata_extractors_by = {'priority': {}, 'source': {}}
+ else:
+ self.metadata_extractors_by = metadata_extractors_by
+
+ self._set_translated_to()
+ self._set_folders(destination, destination_base)
+
+ # Load default metadata
+ default_metadata, default_used_extractor = get_meta(self, lang=None)
+ self.meta = Functionary(lambda: None, self.default_lang)
+ self.used_extractor = Functionary(lambda: None, self.default_lang)
+ self.meta[self.default_lang] = default_metadata
+ self.used_extractor[self.default_lang] = default_used_extractor
+
+ self._set_date(default_metadata)
+
+ # These are the required metadata fields
+ if 'title' not in default_metadata or 'slug' not in default_metadata:
+ raise ValueError("You must set a title (found '{0}') and a slug (found '{1}')! "
+ "[in file {2}]".format(default_metadata.get('title', None),
+ default_metadata.get('slug', None),
+ source_path))
+
+ if 'type' not in default_metadata:
+ default_metadata['type'] = 'text'
+
+ self._load_translated_metadata(default_metadata)
+ self._load_data()
+ self.__migrate_section_to_category()
+ self._set_tags()
+
+ self.publish_later = False if self.current_time is None else self.date >= self.current_time
+
+ # While draft comes from the tags, it's not really a tag
+ self.use_in_feeds = self.is_post and not self.is_draft and not self.is_private and not self.publish_later
+
+ # Allow overriding URL_TYPE via meta
+ # The check is done here so meta dicts won’t change inside of
+ # generic_post_renderer
+ self.url_type = self.meta('url_type') or None
+ # Register potential extra dependencies
+ self.compiler.register_extra_dependencies(self)
+
+ def _load_config(self, config):
+ """Set members to configured values."""
+ self.config = config
if self.config['FUTURE_IS_NOW']:
self.current_time = None
else:
- self.current_time = current_time(tzinfo)
- self.translated_to = set([])
- self._prev_post = None
- self._next_post = None
+ self.current_time = current_time(self.config['__tzinfo__'])
self.base_url = self.config['BASE_URL']
- self.is_draft = False
- self.is_private = False
self.strip_indexes = self.config['STRIP_INDEXES']
self.index_file = self.config['INDEX_FILE']
self.pretty_urls = self.config['PRETTY_URLS']
+ self.default_lang = self.config['DEFAULT_LANG']
+ self.translations = self.config['TRANSLATIONS']
+ self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS']
+ self._default_preview_image = self.config['DEFAULT_PREVIEW_IMAGE']
+ self.types_to_hide_title = self.config['TYPES_TO_HIDE_TITLE']
+
+ def _set_tags(self):
+ """Set post tags."""
+ self._tags = {}
+ for lang in self.translated_to:
+ if isinstance(self.meta[lang]['tags'], (list, tuple, set)):
+ _tag_list = self.meta[lang]['tags']
+ else:
+ _tag_list = self.meta[lang]['tags'].split(',')
+ self._tags[lang] = natsort.natsorted(
+ list(set([x.strip() for x in _tag_list])),
+ alg=natsort.ns.F | natsort.ns.IC)
+ self._tags[lang] = [t for t in self._tags[lang] if t]
+
+ status = self.meta[lang].get('status')
+ if status:
+ if status == 'published':
+ pass # already set before, mixing published + something else should result in the other thing
+ elif status == 'featured':
+ self.post_status = status
+ elif status == 'private':
+ self.post_status = status
+ self.is_private = True
+ elif status == 'draft':
+ self.post_status = status
+ self.is_draft = True
+ else:
+ LOGGER.warning(('The post "{0}" has the unknown status "{1}". '
+ 'Valid values are "published", "featured", "private" and "draft".').format(self.source_path, status))
+
+ if self.config['WARN_ABOUT_TAG_METADATA']:
+ show_warning = False
+ if 'draft' in [_.lower() for _ in self._tags[lang]]:
+ LOGGER.warning('The post "{0}" uses the "draft" tag.'.format(self.source_path))
+ show_warning = True
+ if 'private' in self._tags[lang]:
+ LOGGER.warning('The post "{0}" uses the "private" tag.'.format(self.source_path))
+ show_warning = True
+ if 'mathjax' in self._tags[lang]:
+ LOGGER.warning('The post "{0}" uses the "mathjax" tag.'.format(self.source_path))
+ show_warning = True
+ if show_warning:
+ LOGGER.warning('It is suggested that you convert special tags to metadata and set '
+ 'USE_TAG_METADATA to False. You can use the upgrade_metadata_v8 '
+ 'command plugin for conversion (install with: nikola plugin -i '
+ 'upgrade_metadata_v8). Change the WARN_ABOUT_TAG_METADATA '
+ 'configuration to disable this warning.')
+ if self.config['USE_TAG_METADATA']:
+ if 'draft' in [_.lower() for _ in self._tags[lang]]:
+ self.is_draft = True
+ LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path))
+ self._tags[lang].remove('draft')
+ self.post_status = 'draft'
+ self.has_oldstyle_metadata_tags = True
+
+ if 'private' in self._tags[lang]:
+ self.is_private = True
+ LOGGER.debug('The post "{0}" is private.'.format(self.source_path))
+ self._tags[lang].remove('private')
+ self.post_status = 'private'
+ self.has_oldstyle_metadata_tags = True
+
+ if 'mathjax' in self._tags[lang]:
+ self.has_oldstyle_metadata_tags = True
+
+ def _set_paths(self, source_path):
+ """Set the various paths and the post_name.
+
+ TODO: WTF is all this.
+ """
self.source_path = source_path # posts/blah.txt
self.post_name = os.path.splitext(source_path)[0] # posts/blah
+ _relpath = os.path.relpath(self.post_name)
+ if _relpath != self.post_name:
+ self.post_name = _relpath.replace('..' + os.sep, '_..' + os.sep)
# cache[\/]posts[\/]blah.html
self.base_path = os.path.join(self.config['CACHE_FOLDER'], self.post_name + ".html")
# cache/posts/blah.html
self._base_path = self.base_path.replace('\\', '/')
self.metadata_path = self.post_name + ".meta" # posts/blah.meta
- self.folder = destination
- self.translations = self.config['TRANSLATIONS']
- self.default_lang = self.config['DEFAULT_LANG']
- self.messages = messages
- self.skip_untranslated = not self.config['SHOW_UNTRANSLATED_POSTS']
- self._template_name = template_name
- self.is_two_file = True
- self.newstylemeta = True
- self._reading_time = None
- self._remaining_reading_time = None
- self._paragraph_count = None
- self._remaining_paragraph_count = None
- self._dependency_file_fragment = defaultdict(list)
- self._dependency_file_page = defaultdict(list)
- self._dependency_uptodate_fragment = defaultdict(list)
- self._dependency_uptodate_page = defaultdict(list)
- self._depfile = defaultdict(list)
-
- default_metadata, self.newstylemeta = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'])
-
- self.meta = Functionary(lambda: None, self.default_lang)
- self.meta[self.default_lang] = default_metadata
- # Load internationalized metadata
+ def _set_translated_to(self):
+ """Find post's translations."""
+ self.translated_to = set([])
for lang in self.translations:
if os.path.isfile(get_translation_candidate(self.config, self.source_path, lang)):
self.translated_to.add(lang)
+
+ # If we don't have anything in translated_to, the file does not exist
+ if not self.translated_to and os.path.isfile(self.source_path):
+ raise Exception(("Could not find translations for {}, check your "
+ "TRANSLATIONS_PATTERN").format(self.source_path))
+ elif not self.translated_to:
+ raise Exception(("Cannot use {} (not a file, perhaps a broken "
+ "symbolic link?)").format(self.source_path))
+
+ def _set_folders(self, destination, destination_base):
+ """Compose destination paths."""
+ self.folder_relative = destination
+ self.folder_base = destination_base
+
+ if self.folder_base is not None:
+ # Use translatable destination folders
+ self.folders = {}
+ for lang in self.config['TRANSLATIONS']:
+ if os.path.isabs(self.folder_base(lang)): # Issue 2982
+ self.folder_base[lang] = os.path.relpath(self.folder_base(lang), '/')
+ self.folders[lang] = os.path.normpath(os.path.join(self.folder_base(lang), self.folder_relative))
+ else:
+ # Old behavior (non-translatable destination path, normalized by scanner)
+ self.folders = {lang: self.folder_relative for lang in self.config['TRANSLATIONS'].keys()}
+ self.folder = self.folders[self.default_lang]
+
+ def __migrate_section_to_category(self):
+ """TODO: remove in v9."""
+ for lang, meta in self.meta.items():
+ # Migrate section to category
+ # TODO: remove in v9
+ if 'section' in meta:
+ if 'category' in meta:
+ LOGGER.warning("Post {0} has both 'category' and 'section' metadata. Section will be ignored.".format(self.source_path))
+ else:
+ meta['category'] = meta['section']
+ LOGGER.info("Post {0} uses 'section' metadata, setting its value to 'category'".format(self.source_path))
+
+ # Handle CATEGORY_DESTPATH_AS_DEFAULT
+ if 'category' not in meta and self.config['CATEGORY_DESTPATH_AS_DEFAULT']:
+ self.category_from_destpath = True
+ if self.config['CATEGORY_DESTPATH_TRIM_PREFIX'] and self.folder_relative != '.':
+ category = self.folder_relative
+ else:
+ category = self.folders[lang]
+ category = category.replace(os.sep, '/')
+ if self.config['CATEGORY_DESTPATH_FIRST_DIRECTORY_ONLY']:
+ category = category.split('/')[0]
+ meta['category'] = self.config['CATEGORY_DESTPATH_NAMES'](lang).get(category, category)
+ else:
+ self.category_from_destpath = False
+
+ def _load_data(self):
+ """Load data field from metadata."""
+ self.data = Functionary(lambda: None, self.default_lang)
+ for lang in self.translations:
+ if self.meta[lang].get('data') is not None:
+ self.data[lang] = utils.load_data(self.meta[lang]['data'])
+
+ def _load_translated_metadata(self, default_metadata):
+ """Load metadata from all translation sources."""
+ for lang in self.translations:
if lang != self.default_lang:
meta = defaultdict(lambda: '')
meta.update(default_metadata)
- _meta, _nsm = get_meta(self, self.config['FILE_METADATA_REGEXP'], self.config['UNSLUGIFY_TITLES'], lang)
- self.newstylemeta = self.newstylemeta and _nsm
+ _meta, _extractors = get_meta(self, lang)
meta.update(_meta)
self.meta[lang] = meta
+ self.used_extractor[lang] = _extractors
if not self.is_translation_available(self.default_lang):
# Special case! (Issue #373)
@@ -161,91 +341,80 @@ class Post(object):
for lang in sorted(self.translated_to):
default_metadata.update(self.meta[lang])
- # Load data field from metadata
- self.data = Functionary(lambda: None, self.default_lang)
- for lang in self.translations:
- if self.meta[lang].get('data') is not None:
- self.data[lang] = utils.load_data(self.meta[lang]['data'])
-
- if 'date' not in default_metadata and not use_in_feeds:
+ def _set_date(self, default_metadata):
+ """Set post date/updated based on metadata and configuration."""
+ if 'date' not in default_metadata and not self.is_post:
# For pages we don't *really* need a date
if self.config['__invariant__']:
- default_metadata['date'] = datetime.datetime(2013, 12, 31, 23, 59, 59, tzinfo=tzinfo)
+ default_metadata['date'] = datetime.datetime(2013, 12, 31, 23, 59, 59, tzinfo=self.config['__tzinfo__'])
else:
default_metadata['date'] = datetime.datetime.utcfromtimestamp(
- os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(tzinfo)
+ os.stat(self.source_path).st_ctime).replace(tzinfo=dateutil.tz.tzutc()).astimezone(self.config['__tzinfo__'])
# If time zone is set, build localized datetime.
try:
- self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo)
+ self.date = to_datetime(self.meta[self.default_lang]['date'], self.config['__tzinfo__'])
except ValueError:
- raise ValueError("Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], source_path))
+ if not self.meta[self.default_lang]['date']:
+ msg = 'Missing date in file {}'.format(self.source_path)
+ else:
+ msg = "Invalid date '{0}' in file {1}".format(self.meta[self.default_lang]['date'], self.source_path)
+ LOGGER.error(msg)
+ raise ValueError(msg)
if 'updated' not in default_metadata:
default_metadata['updated'] = default_metadata.get('date', None)
- self.updated = to_datetime(default_metadata['updated'])
+ self.updated = to_datetime(default_metadata['updated'], self.config['__tzinfo__'])
- if 'title' not in default_metadata or 'slug' not in default_metadata \
- or 'date' not in default_metadata:
- raise ValueError("You must set a title (found '{0}'), a slug (found '{1}') and a date (found '{2}')! "
- "[in file {3}]".format(default_metadata.get('title', None),
- default_metadata.get('slug', None),
- default_metadata.get('date', None),
- source_path))
-
- if 'type' not in default_metadata:
- # default value is 'text'
- default_metadata['type'] = 'text'
-
- self.publish_later = False if self.current_time is None else self.date >= self.current_time
-
- is_draft = False
- is_private = False
- self._tags = {}
- for lang in self.translated_to:
- self._tags[lang] = natsort.natsorted(
- list(set([x.strip() for x in self.meta[lang]['tags'].split(',')])),
- alg=natsort.ns.F | natsort.ns.IC)
- self._tags[lang] = [t for t in self._tags[lang] if t]
- if 'draft' in [_.lower() for _ in self._tags[lang]]:
- is_draft = True
- LOGGER.debug('The post "{0}" is a draft.'.format(self.source_path))
- self._tags[lang].remove('draft')
-
- # TODO: remove in v8
- if 'retired' in self._tags[lang]:
- is_private = True
- LOGGER.warning('The "retired" tag in post "{0}" is now deprecated and will be removed in v8. Use "private" instead.'.format(self.source_path))
- self._tags[lang].remove('retired')
- # end remove in v8
-
- if 'private' in self._tags[lang]:
- is_private = True
- LOGGER.debug('The post "{0}" is private.'.format(self.source_path))
- self._tags[lang].remove('private')
+ @property
+ def hyphenate(self):
+ """Post is hyphenated."""
+ return bool(self.config['HYPHENATE'] or self.meta('hyphenate'))
- # While draft comes from the tags, it's not really a tag
- self.is_draft = is_draft
- self.is_private = is_private
- self.is_post = use_in_feeds
- self.use_in_feeds = use_in_feeds and not is_draft and not is_private \
- and not self.publish_later
+ @property
+ def is_two_file(self):
+ """Post has a separate .meta file."""
+ if self._is_two_file is None:
+ return True
+ return self._is_two_file
- # Register potential extra dependencies
- self.compiler.register_extra_dependencies(self)
+ @is_two_file.setter
+ def is_two_file(self, value):
+ """Set the is_two_file property, use with care.
- def _get_hyphenate(self):
- return bool(self.config['HYPHENATE'] or self.meta('hyphenate'))
+ Caution: this MAY REWRITE THE POST FILE.
+ Only should happen if you effectively *change* the value.
- hyphenate = property(_get_hyphenate)
+ Arguments:
+ value {bool} -- Whether the post has a separate .meta file
+ """
+ # for lang in self.translated_to:
+
+ if self._is_two_file is None:
+ # Initial setting, this happens on post creation
+ self._is_two_file = value
+ elif value != self._is_two_file:
+ # Changing the value, this means you are transforming a 2-file
+ # into a 1-file or viceversa.
+ if value and not self.compiler.supports_metadata:
+ raise ValueError("Can't save metadata as 1-file using this compiler {}".format(self.compiler))
+ for lang in self.translated_to:
+ source = self.source(lang)
+ meta = self.meta(lang)
+ self._is_two_file = value
+ self.save(lang=lang, source=source, meta=meta)
+ if not value: # Need to delete old meta file
+ meta_path = get_translation_candidate(self.config, self.metadata_path, lang)
+ if os.path.isfile(meta_path):
+ os.unlink(meta_path)
def __repr__(self):
"""Provide a representation of the post object."""
# Calculate a hash that represents most data about the post
m = hashlib.md5()
# source_path modification date (to avoid reading it)
- m.update(utils.unicode_str(os.stat(self.source_path).st_mtime).encode('utf-8'))
+ m.update(str(os.stat(self.source_path).st_mtime).encode('utf-8'))
clean_meta = {}
for k, v in self.meta.items():
sub_meta = {}
@@ -253,27 +422,45 @@ class Post(object):
for kk, vv in v.items():
if vv:
sub_meta[kk] = vv
- m.update(utils.unicode_str(json.dumps(clean_meta, cls=utils.CustomEncoder, sort_keys=True)).encode('utf-8'))
+ m.update(str(json.dumps(clean_meta, cls=utils.CustomEncoder, sort_keys=True)).encode('utf-8'))
return '<Post: {0!r} {1}>'.format(self.source_path, m.hexdigest())
- def _has_pretty_url(self, lang):
- if self.pretty_urls and \
- self.meta[lang].get('pretty_url', '') != 'False' and \
- self.meta[lang]['slug'] != 'index':
- return True
+ def has_pretty_url(self, lang):
+ """Check if this page has a pretty URL."""
+ m = self.meta[lang].get('pretty_url', '')
+ if m:
+ # match is a non-empty string, overides anything
+ return m.lower() == 'true' or m.lower() == 'yes'
else:
- return False
+ # use PRETTY_URLS, unless the slug is 'index'
+ return self.pretty_urls and self.meta[lang]['slug'] != 'index'
+
+ def _has_pretty_url(self, lang):
+ """Check if this page has a pretty URL."""
+ return self.has_pretty_url(lang)
@property
- def is_mathjax(self):
- """True if this post has the mathjax tag in the current language or is a python notebook."""
+ def has_math(self):
+ """Return True if this post has has_math set to True or is a python notebook.
+
+ Alternatively, it will return True if it has set the mathjax tag in the
+ current language and the USE_TAG_METADATA config setting is True.
+ """
if self.compiler.name == 'ipynb':
return True
lang = nikola.utils.LocaleBorg().current_lang
if self.is_translation_available(lang):
- return 'mathjax' in self.tags_for_language(lang)
+ if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True):
+ return True
+ if self.config['USE_TAG_METADATA']:
+ return 'mathjax' in self.tags_for_language(lang)
# If it has math in ANY other language, enable it. Better inefficient than broken.
- return 'mathjax' in self.alltags
+ for lang in self.translated_to:
+ if self.meta[lang].get('has_math') in ('true', 'True', 'yes', '1', 1, True):
+ return True
+ if self.config['USE_TAG_METADATA']:
+ return 'mathjax' in self.alltags
+ return False
@property
def alltags(self):
@@ -313,7 +500,7 @@ class Post(object):
rv = rv._prev_post
return rv
- @prev_post.setter # NOQA
+ @prev_post.setter
def prev_post(self, v):
"""Set previous post."""
self._prev_post = v
@@ -331,7 +518,7 @@ class Post(object):
rv = rv._next_post
return rv
- @next_post.setter # NOQA
+ @next_post.setter
def next_post(self, v):
"""Set next post."""
self._next_post = v
@@ -343,11 +530,11 @@ class Post(object):
return self.meta[lang]['template'] or self._template_name
def formatted_date(self, date_format, date=None):
- """Return the formatted date as unicode."""
+ """Return the formatted date as string."""
return utils.LocaleBorg().formatted_date(date_format, date if date else self.date)
def formatted_updated(self, date_format):
- """Return the updated date as unicode."""
+ """Return the updated date as string."""
return self.formatted_date(date_format, self.updated)
def title(self, lang=None):
@@ -360,7 +547,7 @@ class Post(object):
lang = nikola.utils.LocaleBorg().current_lang
return self.meta[lang]['title']
- def author(self, lang=None):
+ def author(self, lang=None) -> str:
"""Return localized author or BLOG_AUTHOR if unspecified.
If lang is not specified, it defaults to the current language from
@@ -375,12 +562,38 @@ class Post(object):
return author
+ def authors(self, lang=None) -> list:
+ """Return localized authors or BLOG_AUTHOR if unspecified.
+
+ If lang is not specified, it defaults to the current language from
+ templates, as set in LocaleBorg.
+ """
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+ if self.meta[lang]['author']:
+ author = [i.strip() for i in self.meta[lang]['author'].split(",")]
+ else:
+ author = [self.config['BLOG_AUTHOR'](lang)]
+
+ return author
+
def description(self, lang=None):
"""Return localized description."""
if lang is None:
lang = nikola.utils.LocaleBorg().current_lang
return self.meta[lang]['description']
+ def guid(self, lang=None):
+ """Return localized GUID."""
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+ if self.meta[lang]['guid']:
+ guid = self.meta[lang]['guid']
+ else:
+ guid = self.permalink(lang, absolute=True)
+
+ return guid
+
def add_dependency(self, dependency, add='both', lang=None):
"""Add a file dependency for tasks using that post.
@@ -440,12 +653,15 @@ class Post(object):
self._depfile[dest].append(dep)
@staticmethod
- def write_depfile(dest, deps_list):
+ def write_depfile(dest, deps_list, post=None, lang=None):
"""Write a depfile for a given language."""
- deps_path = dest + '.dep'
- if deps_list:
+ if post is None or lang is None:
+ deps_path = dest + '.dep'
+ else:
+ deps_path = post.compiler.get_dep_filename(post, lang)
+ if deps_list or (post.compiler.use_dep_file if post else False):
deps_list = [p for p in deps_list if p != dest] # Don't depend on yourself (#1671)
- with io.open(deps_path, "w+", encoding="utf8") as deps_file:
+ with io.open(deps_path, "w+", encoding="utf-8") as deps_file:
deps_file.write('\n'.join(deps_list))
else:
if os.path.isfile(deps_path):
@@ -470,11 +686,10 @@ class Post(object):
def deps(self, lang):
"""Return a list of file dependencies to build this post's page."""
deps = []
- if self.default_lang in self.translated_to:
- deps.append(self.base_path)
- deps.append(self.source_path)
- if os.path.exists(self.metadata_path):
- deps.append(self.metadata_path)
+ deps.append(self.base_path)
+ deps.append(self.source_path)
+ if os.path.exists(self.metadata_path):
+ deps.append(self.metadata_path)
if lang != self.default_lang:
cand_1 = get_translation_candidate(self.config, self.source_path, lang)
cand_2 = get_translation_candidate(self.config, self.base_path, lang)
@@ -487,7 +702,7 @@ class Post(object):
deps.append(self.meta('data', lang))
deps += self._get_dependencies(self._dependency_file_page[lang])
deps += self._get_dependencies(self._dependency_file_page[None])
- return sorted(deps)
+ return sorted(set(deps))
def deps_uptodate(self, lang):
"""Return a list of uptodate dependencies to build this post's page.
@@ -503,14 +718,6 @@ class Post(object):
def compile(self, lang):
"""Generate the cache/ file with the compiled post."""
- def wrap_encrypt(path, password):
- """Wrap a post with encryption."""
- with io.open(path, 'r+', encoding='utf8') as inf:
- data = inf.read() + "<!--tail-->"
- data = CRYPT.substitute(data=rc4(password, data))
- with io.open(path, 'w+', encoding='utf8') as outf:
- outf.write(data)
-
dest = self.translated_base_path(lang)
if not self.is_translation_available(lang) and not self.config['SHOW_UNTRANSLATED_POSTS']:
return
@@ -519,34 +726,25 @@ class Post(object):
self.compile_html(
self.translated_source_path(lang),
dest,
- self.is_two_file)
- Post.write_depfile(dest, self._depfile[dest])
+ self.is_two_file,
+ self,
+ lang)
+ Post.write_depfile(dest, self._depfile[dest], post=self, lang=lang)
signal('compiled').send({
'source': self.translated_source_path(lang),
'dest': dest,
'post': self,
+ 'lang': lang,
})
- if self.meta('password'):
- # TODO: get rid of this feature one day (v8?; warning added in v7.3.0.)
- LOGGER.warn("The post {0} is using the `password` attribute, which may stop working in the future.")
- LOGGER.warn("Please consider switching to a more secure method of encryption.")
- LOGGER.warn("More details: https://github.com/getnikola/nikola/issues/1547")
- wrap_encrypt(dest, self.meta('password'))
if self.publish_later:
- LOGGER.notice('{0} is scheduled to be published in the future ({1})'.format(
+ LOGGER.info('{0} is scheduled to be published in the future ({1})'.format(
self.source_path, self.date))
def fragment_deps(self, lang):
- """Return a list of uptodate dependencies to build this post's fragment.
-
- These dependencies should be included in ``uptodate`` for the task
- which generates the fragment.
- """
- deps = []
- if self.default_lang in self.translated_to:
- deps.append(self.source_path)
+ """Return a list of dependencies to build this post's fragment."""
+ deps = [self.source_path]
if os.path.isfile(self.metadata_path):
deps.append(self.metadata_path)
lang_deps = []
@@ -587,20 +785,90 @@ class Post(object):
return get_translation_candidate(self.config, self.base_path, lang)
def _translated_file_path(self, lang):
- """Return path to the translation's file, or to the original."""
+ """Get path to a post's translation.
+
+ Returns path to the translation's file, or to as good a file as it can
+ plus "real" language of the text.
+ """
if lang in self.translated_to:
if lang == self.default_lang:
- return self.base_path
+ return self.base_path, lang
else:
- return get_translation_candidate(self.config, self.base_path, lang)
+ return get_translation_candidate(self.config, self.base_path, lang), lang
elif lang != self.default_lang:
- return self.base_path
+ return self.base_path, self.default_lang
else:
- return get_translation_candidate(self.config, self.base_path, sorted(self.translated_to)[0])
+ real_lang = sorted(self.translated_to)[0]
+ return get_translation_candidate(self.config, self.base_path, real_lang), real_lang
+
+ def write_metadata(self, lang=None):
+ """Save the post's metadata.
+
+ Keep in mind that this will save either in the
+ post file or in a .meta file, depending on self.is_two_file.
+
+ metadata obtained from filenames or document contents will
+ be superseded by this, and becomes inaccessible.
+
+ Post contents will **not** be modified.
+
+ If you write to a language not in self.translated_to
+ an exception will be raised.
+
+ Remember to scan_posts(really=True) after you update metadata if
+ you want the rest of the system to know about the change.
+ """
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+ if lang not in self.translated_to:
+ raise ValueError("Can't save post metadata to language [{}] it's not translated to.".format(lang))
+
+ source = self.source(lang)
+ source_path = self.translated_source_path(lang)
+ metadata = self.meta[lang]
+ self.compiler.create_post(source_path, content=source, onefile=not self.is_two_file, is_page=not self.is_post, **metadata)
+
+ def save(self, lang=None, source=None, meta=None):
+ """Write post source to disk.
+
+ Use this with utmost care, it may wipe out a post.
+
+ Keyword Arguments:
+ lang str -- Language for this source. If set to None,
+ use current language.
+ source str -- The source text for the post in the
+ language. If set to None, use current source for
+ this language.
+ meta dict -- Metadata for this language, if not set,
+ use current metadata for this language.
+ """
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+ if source is None:
+ source = self.source(lang)
+ if meta is None:
+ metadata = self.meta[lang]
+ source_path = self.translated_source_path(lang)
+ metadata = self.meta[lang]
+ self.compiler.create_post(source_path, content=source, onefile=not self.is_two_file, is_page=not self.is_post, **metadata)
+
+ def source(self, lang=None):
+ """Read the post and return its source."""
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+
+ source = self.translated_source_path(lang)
+ with open(source, 'r', encoding='utf-8-sig') as inf:
+ data = inf.read()
+ if self.is_two_file: # Metadata is not here
+ source_data = data
+ else:
+ source_data = self.compiler.split_metadata(data, self, lang)[1]
+ return source_data
def text(self, lang=None, teaser_only=False, strip_html=False, show_read_more_link=True,
feed_read_more_link=False, feed_links_append_query=None):
- """Read the post file for that language and return its contents.
+ """Read the post file for that language and return its compiled contents.
teaser_only=True breaks at the teaser marker and returns only the teaser.
strip_html=True removes HTML tags
@@ -613,7 +881,7 @@ class Post(object):
"""
if lang is None:
lang = nikola.utils.LocaleBorg().current_lang
- file_name = self._translated_file_path(lang)
+ file_name, real_lang = self._translated_file_path(lang)
# Yes, we compile it and screw it.
# This may be controversial, but the user (or someone) is asking for the post text
@@ -621,7 +889,7 @@ class Post(object):
if not os.path.isfile(file_name):
self.compile(lang)
- with io.open(file_name, "r", encoding="utf8") as post_file:
+ with io.open(file_name, "r", encoding="utf-8-sig") as post_file:
data = post_file.read().strip()
if self.compiler.extension() == '.php':
@@ -633,16 +901,16 @@ class Post(object):
if str(e) == "Document is empty":
return ""
# let other errors raise
- raise(e)
+ raise
base_url = self.permalink(lang=lang)
document.make_links_absolute(base_url)
if self.hyphenate:
- hyphenate(document, lang)
+ hyphenate(document, real_lang)
try:
data = lxml.html.tostring(document.body, encoding='unicode')
- except:
+ except Exception:
data = lxml.html.tostring(document, encoding='unicode')
if teaser_only:
@@ -662,7 +930,8 @@ class Post(object):
reading_time=self.reading_time,
remaining_reading_time=self.remaining_reading_time,
paragraph_count=self.paragraph_count,
- remaining_paragraph_count=self.remaining_paragraph_count)
+ remaining_paragraph_count=self.remaining_paragraph_count,
+ post_title=self.title(lang))
# This closes all open tags and sanitizes the broken HTML
document = lxml.html.fromstring(teaser)
try:
@@ -675,7 +944,7 @@ class Post(object):
# Not all posts have a body. For example, you may have a page statically defined in the template that does not take content as input.
content = lxml.html.fromstring(data)
data = content.text_content().strip() # No whitespace wanted.
- except lxml.etree.ParserError:
+ except (lxml.etree.ParserError, ValueError):
data = ""
elif data:
if self.demote_headers:
@@ -691,7 +960,7 @@ class Post(object):
@property
def reading_time(self):
- """Reading time based on length of text."""
+ """Return reading time based on length of text."""
if self._reading_time is None:
text = self.text(strip_html=True)
words_per_minute = 220
@@ -720,8 +989,8 @@ class Post(object):
if self._paragraph_count is None:
# duplicated with Post.text()
lang = nikola.utils.LocaleBorg().current_lang
- file_name = self._translated_file_path(lang)
- with io.open(file_name, "r", encoding="utf8") as post_file:
+ file_name, _ = self._translated_file_path(lang)
+ with io.open(file_name, "r", encoding="utf-8-sig") as post_file:
data = post_file.read().strip()
try:
document = lxml.html.fragment_fromstring(data, "body")
@@ -730,7 +999,7 @@ class Post(object):
if str(e) == "Document is empty":
return ""
# let other errors raise
- raise(e)
+ raise
# output is a float, for no real reason at all
self._paragraph_count = int(document.xpath('count(//p)'))
@@ -748,7 +1017,7 @@ class Post(object):
if str(e) == "Document is empty":
return ""
# let other errors raise
- raise(e)
+ raise
self._remaining_paragraph_count = self.paragraph_count - int(document.xpath('count(//p)'))
return self._remaining_paragraph_count
@@ -768,73 +1037,19 @@ class Post(object):
"""
if lang is None:
lang = nikola.utils.LocaleBorg().current_lang
- if self._has_pretty_url(lang):
+ folder = self.folders[lang]
+ if self.has_pretty_url(lang):
path = os.path.join(self.translations[lang],
- self.folder, self.meta[lang]['slug'], 'index' + extension)
+ folder, self.meta[lang]['slug'], 'index' + extension)
else:
path = os.path.join(self.translations[lang],
- self.folder, self.meta[lang]['slug'] + extension)
+ folder, self.meta[lang]['slug'] + extension)
if sep != os.sep:
path = path.replace(os.sep, sep)
if path.startswith('./'):
path = path[2:]
return path
- def section_color(self, lang=None):
- """Return the color of the post's section."""
- slug = self.section_slug(lang)
- if slug in self.config['POSTS_SECTION_COLORS'](lang):
- return self.config['POSTS_SECTION_COLORS'](lang)[slug]
- base = self.config['THEME_COLOR']
- return utils.colorize_str_from_base_color(slug, base)
-
- def section_link(self, lang=None):
- """Return the link to the post's section (deprecated)."""
- utils.LOGGER.warning("Post.section_link is deprecated. Please use " +
- "site.link('section_index', post.section_slug()) instead.")
- if lang is None:
- lang = nikola.utils.LocaleBorg().current_lang
-
- slug = self.section_slug(lang)
- t = os.path.normpath(self.translations[lang])
- if t == '.':
- t = ''
- link = '/' + '/'.join(i for i in (t, slug) if i) + '/'
- if not self.pretty_urls:
- link = urljoin(link, self.index_file)
- link = utils.encodelink(link)
- return link
-
- def section_name(self, lang=None):
- """Return the name of the post's section."""
- slug = self.section_slug(lang)
- if slug in self.config['POSTS_SECTION_NAME'](lang):
- name = self.config['POSTS_SECTION_NAME'](lang)[slug]
- else:
- name = slug.replace('-', ' ').title()
- return name
-
- def section_slug(self, lang=None):
- """Return the slug for the post's section."""
- if lang is None:
- lang = nikola.utils.LocaleBorg().current_lang
-
- if not self.config['POSTS_SECTION_FROM_META']:
- dest = self.destination_path(lang)
- if dest[-(1 + len(self.index_file)):] == os.sep + self.index_file:
- dest = dest[:-(1 + len(self.index_file))]
- dirname = os.path.dirname(dest)
- slug = dest.split(os.sep)
- if not slug or dirname == '.':
- slug = self.messages[lang]["Uncategorized"]
- elif lang == slug[0]:
- slug = slug[1]
- else:
- slug = slug[0]
- else:
- slug = self.meta[lang]['section'].split(',')[0] if 'section' in self.meta[lang] else self.messages[lang]["Uncategorized"]
- return utils.slugify(slug, lang)
-
def permalink(self, lang=None, absolute=False, extension='.html', query=None):
"""Return permalink for a post."""
if lang is None:
@@ -845,8 +1060,8 @@ class Post(object):
extension = self.compiler.extension()
pieces = self.translations[lang].split(os.sep)
- pieces += self.folder.split(os.sep)
- if self._has_pretty_url(lang):
+ pieces += self.folders[lang].split(os.sep)
+ if self.has_pretty_url(lang):
pieces += [self.meta[lang]['slug'], 'index' + extension]
else:
pieces += [self.meta[lang]['slug'] + extension]
@@ -869,13 +1084,14 @@ class Post(object):
lang = nikola.utils.LocaleBorg().current_lang
image_path = self.meta[lang]['previewimage']
-
if not image_path:
- return None
+ image_path = self._default_preview_image
- # This is further parsed by the template, because we don’t have access
- # to the URL replacer here. (Issue #1473)
- return image_path
+ if not image_path or image_path.startswith("/"):
+ # Paths starting with slashes are expected to be root-relative, pass them directly.
+ return image_path
+ # Other paths are relative to the permalink. The path will be made prettier by the URL replacer later.
+ return urljoin(self.permalink(lang), image_path)
def source_ext(self, prefix=False):
"""Return the source file extension.
@@ -891,47 +1107,17 @@ class Post(object):
else:
return ext
-# Code that fetches metadata from different places
+ def should_hide_title(self):
+ """Return True if this post's title should be hidden. Use in templates to manage posts without titles."""
+ return self.title().strip() in ('NO TITLE', '') or self.meta('hidetitle') or \
+ self.meta('type').strip() in self.types_to_hide_title
+ def should_show_title(self):
+ """Return True if this post's title should be displayed. Use in templates to manage posts without titles."""
+ return not self.should_hide_title()
-def re_meta(line, match=None):
- """Find metadata using regular expressions."""
- if match:
- reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match)))
- else:
- reStr = re.compile('^\.\. (.*?): (.*)')
- result = reStr.findall(line.strip())
- if match and result:
- return (match, result[0])
- elif not match and result:
- return (result[0][0], result[0][1].strip())
- else:
- return (None,)
-
-
-def _get_metadata_from_filename_by_regex(filename, metadata_regexp, unslugify_titles, lang):
- """Try to reed the metadata from the filename based on the given re.
- This requires to use symbolic group names in the pattern.
- The part to read the metadata from the filename based on a regular
- expression is taken from Pelican - pelican/readers.py
- """
- match = re.match(metadata_regexp, filename)
- meta = {}
-
- if match:
- # .items() for py3k compat.
- for key, value in match.groupdict().items():
- k = key.lower().strip() # metadata must be lowercase
- if k == 'title' and unslugify_titles:
- meta[k] = unslugify(value, lang, discard_numbers=False)
- else:
- meta[k] = value
-
- return meta
-
-
-def get_metadata_from_file(source_path, config=None, lang=None):
+def get_metadata_from_file(source_path, post, config, lang, metadata_extractors_by):
"""Extract metadata from the file itself, by parsing contents."""
try:
if lang and config:
@@ -939,183 +1125,113 @@ def get_metadata_from_file(source_path, config=None, lang=None):
elif lang:
source_path += '.' + lang
with io.open(source_path, "r", encoding="utf-8-sig") as meta_file:
- meta_data = [x.strip() for x in meta_file.readlines()]
- return _get_metadata_from_file(meta_data)
+ source_text = meta_file.read()
except (UnicodeDecodeError, UnicodeEncodeError):
- raise ValueError('Error reading {0}: Nikola only supports UTF-8 files'.format(source_path))
+ msg = 'Error reading {0}: Nikola only supports UTF-8 files'.format(source_path)
+ LOGGER.error(msg)
+ raise ValueError(msg)
except Exception: # The file may not exist, for multilingual sites
- return {}
-
-
-re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#')))
-# Assuming rst titles are going to be at least 4 chars long
-# otherwise this detects things like ''' wich breaks other markups.
-re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape(
- string.punctuation)))
-
-
-def _get_title_from_contents(meta_data):
- """Extract title from file contents, LAST RESOURCE."""
- piece = meta_data[:]
- title = None
- for i, line in enumerate(piece):
- if re_rst_title.findall(line) and i > 0:
- title = meta_data[i - 1].strip()
- break
- if (re_rst_title.findall(line) and i >= 0 and
- re_rst_title.findall(meta_data[i + 2])):
- title = meta_data[i + 1].strip()
- break
- if re_md_title.findall(line):
- title = re_md_title.findall(line)[0]
- break
- return title
-
+ return {}, None
-def _get_metadata_from_file(meta_data):
- """Extract metadata from a post's source file."""
meta = {}
- if not meta_data:
- return meta
-
- # Skip up to one empty line at the beginning (for txt2tags)
- if not meta_data[0]:
- meta_data = meta_data[1:]
-
- # First, get metadata from the beginning of the file,
- # up to first empty line
+ used_extractor = None
+ for priority in metadata_extractors.MetaPriority:
+ found_in_priority = False
+ for extractor in metadata_extractors_by['priority'].get(priority, []):
+ if not metadata_extractors.check_conditions(post, source_path, extractor.conditions, config, source_text):
+ continue
+ extractor.check_requirements()
+ new_meta = extractor.extract_text(source_text)
+ if new_meta:
+ found_in_priority = True
+ used_extractor = extractor
+ # Map metadata from other platforms to names Nikola expects (Issue #2817)
+ # Map metadata values (Issue #3025)
+ map_metadata(new_meta, extractor.map_from, config)
+
+ meta.update(new_meta)
+ break
- for i, line in enumerate(meta_data):
- if not line:
+ if found_in_priority:
break
- match = re_meta(line)
- if match[0]:
- meta[match[0]] = match[1]
+ return meta, used_extractor
- # If we have no title, try to get it from document
- if 'title' not in meta:
- t = _get_title_from_contents(meta_data)
- if t is not None:
- meta['title'] = t
- return meta
-
-
-def get_metadata_from_meta_file(path, config=None, lang=None):
+def get_metadata_from_meta_file(path, post, config, lang, metadata_extractors_by=None):
"""Take a post path, and gets data from a matching .meta file."""
- global _UPGRADE_METADATA_ADVERTISED
meta_path = os.path.splitext(path)[0] + '.meta'
if lang and config:
meta_path = get_translation_candidate(config, meta_path, lang)
elif lang:
meta_path += '.' + lang
if os.path.isfile(meta_path):
- with io.open(meta_path, "r", encoding="utf8") as meta_file:
- meta_data = meta_file.readlines()
-
- # Detect new-style metadata.
- newstyleregexp = re.compile(r'\.\. .*?: .*')
- newstylemeta = False
- for l in meta_data:
- if l.strip():
- if re.match(newstyleregexp, l):
- newstylemeta = True
-
- if newstylemeta:
- # New-style metadata is basically the same as reading metadata from
- # a 1-file post.
- return get_metadata_from_file(path, config, lang), newstylemeta
- else:
- if not _UPGRADE_METADATA_ADVERTISED:
- LOGGER.warn("Some posts on your site have old-style metadata. You should upgrade them to the new format, with support for extra fields.")
- LOGGER.warn("Install the 'upgrade_metadata' plugin (with 'nikola plugin -i upgrade_metadata') and run 'nikola upgrade_metadata'.")
- _UPGRADE_METADATA_ADVERTISED = True
- while len(meta_data) < 7:
- meta_data.append("")
- (title, slug, date, tags, link, description, _type) = [
- x.strip() for x in meta_data][:7]
-
- meta = {}
-
- if title:
- meta['title'] = title
- if slug:
- meta['slug'] = slug
- if date:
- meta['date'] = date
- if tags:
- meta['tags'] = tags
- if link:
- meta['link'] = link
- if description:
- meta['description'] = description
- if _type:
- meta['type'] = _type
-
- return meta, newstylemeta
-
+ return get_metadata_from_file(meta_path, post, config, lang, metadata_extractors_by)
elif lang:
# Metadata file doesn't exist, but not default language,
# So, if default language metadata exists, return that.
# This makes the 2-file format detection more reliable (Issue #525)
- return get_metadata_from_meta_file(path, config, lang=None)
- else:
- return {}, True
-
+ return get_metadata_from_meta_file(meta_path, post, config, None, metadata_extractors_by)
+ else: # No 2-file metadata
+ return {}, None
-def get_meta(post, file_metadata_regexp=None, unslugify_titles=False, lang=None):
- """Get post's meta from source.
- If ``file_metadata_regexp`` is given it will be tried to read
- metadata from the filename.
- If ``unslugify_titles`` is True, the extracted title (if any) will be unslugified, as is done in galleries.
- If any metadata is then found inside the file the metadata from the
- file will override previous findings.
- """
+def get_meta(post, lang):
+ """Get post meta from compiler or source file."""
meta = defaultdict(lambda: '')
+ used_extractor = None
- try:
- config = post.config
- except AttributeError:
- config = None
+ config = getattr(post, 'config', None)
+ metadata_extractors_by = getattr(post, 'metadata_extractors_by')
+ if metadata_extractors_by is None:
+ metadata_extractors_by = metadata_extractors.default_metadata_extractors_by()
- _, newstylemeta = get_metadata_from_meta_file(post.metadata_path, config, lang)
- meta.update(_)
+ # If meta file exists, use it
+ metafile_meta, used_extractor = get_metadata_from_meta_file(post.metadata_path, post, config, lang, metadata_extractors_by)
- if not meta:
- post.is_two_file = False
+ is_two_file = bool(metafile_meta)
- if file_metadata_regexp is not None:
- meta.update(_get_metadata_from_filename_by_regex(post.source_path,
- file_metadata_regexp,
- unslugify_titles,
- post.default_lang))
+ # Filename-based metadata extractors (priority 1).
+ if config.get('FILE_METADATA_REGEXP'):
+ extractors = metadata_extractors_by['source'].get(metadata_extractors.MetaSource.filename, [])
+ for extractor in extractors:
+ if not metadata_extractors.check_conditions(post, post.source_path, extractor.conditions, config, None):
+ continue
+ meta.update(extractor.extract_filename(post.source_path, lang))
+ # Fetch compiler metadata (priority 2, overrides filename-based metadata).
compiler_meta = {}
- if getattr(post, 'compiler', None):
- compiler_meta = post.compiler.read_metadata(post, file_metadata_regexp, unslugify_titles, lang)
+ if (getattr(post, 'compiler', None) and post.compiler.supports_metadata and
+ metadata_extractors.check_conditions(post, post.source_path, post.compiler.metadata_conditions, config, None)):
+ compiler_meta = post.compiler.read_metadata(post, lang=lang)
+ used_extractor = post.compiler
meta.update(compiler_meta)
- if not post.is_two_file and not compiler_meta:
- # Meta file has precedence over file, which can contain garbage.
- # Moreover, we should not to talk to the file if we have compiler meta.
- meta.update(get_metadata_from_file(post.source_path, config, lang))
+ # Meta files and inter-file metadata (priority 3, overrides compiler and filename-based metadata).
+ if not metafile_meta:
+ new_meta, used_extractor = get_metadata_from_file(post.source_path, post, config, lang, metadata_extractors_by)
+ meta.update(new_meta)
+ else:
+ meta.update(metafile_meta)
if lang is None:
# Only perform these checks for the default language
-
if 'slug' not in meta:
# If no slug is found in the metadata use the filename
- meta['slug'] = slugify(unicode_str(os.path.splitext(
- os.path.basename(post.source_path))[0]), post.default_lang)
+ meta['slug'] = slugify(os.path.splitext(
+ os.path.basename(post.source_path))[0], post.default_lang)
if 'title' not in meta:
# If no title is found, use the filename without extension
meta['title'] = os.path.splitext(
os.path.basename(post.source_path))[0]
- return meta, newstylemeta
+ # Set one-file status basing on default language only (Issue #3191)
+ if is_two_file or lang is None:
+ # Direct access because setter is complicated
+ post._is_two_file = is_two_file
+
+ return meta, used_extractor
def hyphenate(dom, _lang):
@@ -1140,10 +1256,11 @@ def hyphenate(dom, _lang):
for tag in ('p', 'li', 'span'):
for node in dom.xpath("//%s[not(parent::pre)]" % tag):
skip_node = False
- skippable_nodes = ['kbd', 'code', 'samp', 'mark', 'math', 'data', 'ruby', 'svg']
+ skippable_nodes = ['kbd', 'pre', 'code', 'samp', 'mark', 'math', 'data', 'ruby', 'svg']
if node.getchildren():
for child in node.getchildren():
- if child.tag in skippable_nodes or (child.tag == 'span' and 'math' in child.get('class', [])):
+ if child.tag in skippable_nodes or (child.tag == 'span' and 'math'
+ in child.get('class', [])):
skip_node = True
elif 'math' in node.get('class', []):
skip_node = True
@@ -1162,8 +1279,14 @@ def insert_hyphens(node, hyphenator):
text = getattr(node, attr)
if not text:
continue
- new_data = ' '.join([hyphenator.inserted(w, hyphen='\u00AD')
- for w in text.split(' ')])
+
+ lines = text.splitlines()
+ new_data = "\n".join(
+ [
+ " ".join([hyphenator.inserted(w, hyphen="\u00AD") for w in line.split(" ")])
+ for line in lines
+ ]
+ )
# Spaces are trimmed, we have to add them manually back
if text[0].isspace():
new_data = ' ' + new_data
@@ -1173,53 +1296,3 @@ def insert_hyphens(node, hyphenator):
for child in node.iterchildren():
insert_hyphens(child, hyphenator)
-
-
-CRYPT = string.Template("""\
-<script>
-function rc4(key, str) {
- var s = [], j = 0, x, res = '';
- for (var i = 0; i < 256; i++) {
- s[i] = i;
- }
- for (i = 0; i < 256; i++) {
- j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
- x = s[i];
- s[i] = s[j];
- s[j] = x;
- }
- i = 0;
- j = 0;
- for (var y = 0; y < str.length; y++) {
- i = (i + 1) % 256;
- j = (j + s[i]) % 256;
- x = s[i];
- s[i] = s[j];
- s[j] = x;
- res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
- }
- return res;
-}
-function decrypt() {
- key = $$("#key").val();
- crypt_div = $$("#encr")
- crypted = crypt_div.html();
- decrypted = rc4(key, window.atob(crypted));
- if (decrypted.substr(decrypted.length - 11) == "<!--tail-->"){
- crypt_div.html(decrypted);
- $$("#pwform").hide();
- crypt_div.show();
- } else { alert("Wrong password"); };
-}
-</script>
-
-<div id="encr" style="display: none;">${data}</div>
-<div id="pwform">
-<form onsubmit="javascript:decrypt(); return false;" class="form-inline">
-<fieldset>
-<legend>This post is password-protected.</legend>
-<input type="password" id="key" placeholder="Type password here">
-<button type="submit" class="btn">Show Content</button>
-</fieldset>
-</form>
-</div>""")
diff --git a/nikola/rc4.py b/nikola/rc4.py
deleted file mode 100644
index 93b660f..0000000
--- a/nikola/rc4.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-A RC4 encryption library (used for password-protected posts).
-
-Original RC4 code license:
-
- Copyright (C) 2012 Bo Zhu http://about.bozhu.me
-
- Permission is hereby granted, free of charge, to any person obtaining a
- copy of this software and associated documentation files (the "Software"),
- to deal in the Software without restriction, including without limitation
- the rights to use, copy, modify, merge, publish, distribute, sublicense,
- and/or sell copies of the Software, and to permit persons to whom the
- Software is furnished to do so, subject to the following conditions:
-
- The above copyright notice and this permission notice shall be included in
- all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- DEALINGS IN THE SOFTWARE.
-"""
-
-import base64
-import sys
-
-
-def KSA(key):
- """Run Key Scheduling Algorithm."""
- keylength = len(key)
-
- S = list(range(256))
-
- j = 0
- for i in range(256):
- j = (j + S[i] + key[i % keylength]) % 256
- S[i], S[j] = S[j], S[i] # swap
-
- return S
-
-
-def PRGA(S):
- """Run Pseudo-Random Generation Algorithm."""
- i = 0
- j = 0
- while True:
- i = (i + 1) % 256
- j = (j + S[i]) % 256
- S[i], S[j] = S[j], S[i] # swap
-
- K = S[(S[i] + S[j]) % 256]
- yield K
-
-
-def RC4(key):
- """Generate RC4 keystream."""
- S = KSA(key)
- return PRGA(S)
-
-
-def rc4(key, string):
- """Encrypt things.
-
- >>> print(rc4("Key", "Plaintext"))
- u/MW6NlArwrT
- """
- string.encode('utf8')
- key.encode('utf8')
-
- def convert_key(s):
- return [ord(c) for c in s]
- key = convert_key(key)
- keystream = RC4(key)
- r = b''
- for c in string:
- if sys.version_info[0] == 3:
- r += bytes([ord(c) ^ next(keystream)])
- else:
- r += chr(ord(c) ^ next(keystream))
- return base64.b64encode(r).replace(b'\n', b'').decode('ascii')
diff --git a/nikola/shortcodes.py b/nikola/shortcodes.py
index b11ddac..6116b98 100644
--- a/nikola/shortcodes.py
+++ b/nikola/shortcodes.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,10 @@
"""Support for Hugo-style shortcodes."""
-from __future__ import unicode_literals
-from .utils import LOGGER
import sys
+import uuid
-
-# Constants
-_TEXT = 1
-_SHORTCODE_START = 2
-_SHORTCODE_END = 3
+from .utils import LOGGER
class ParsingError(Exception):
@@ -83,11 +78,10 @@ def _skip_whitespace(data, pos, must_be_nontrivial=False):
def _skip_nonwhitespace(data, pos):
"""Return first position not before pos which contains a non-whitespace character."""
- while pos < len(data):
- if data[pos].isspace():
- break
- pos += 1
- return pos
+ for i, x in enumerate(data[pos:]):
+ if x.isspace():
+ return pos + i
+ return len(data)
def _parse_quoted_string(data, start):
@@ -209,14 +203,69 @@ def _parse_shortcode_args(data, start, shortcode_name, start_pos):
raise ParsingError("Shortcode '{0}' starting at {1} is not terminated correctly with '%}}}}'!".format(shortcode_name, _format_position(data, start_pos)))
+def _new_sc_id():
+ return str('SHORTCODE{0}REPLACEMENT'.format(str(uuid.uuid4()).replace('-', '')))
+
+
+def extract_shortcodes(data):
+ """
+ Return data with replaced shortcodes, shortcodes.
+
+ data is the original data, with the shortcodes replaced by UUIDs.
+
+ a dictionary of shortcodes, where the keys are UUIDs and the values
+ are the shortcodes themselves ready to process.
+ """
+ shortcodes = {}
+ splitted = _split_shortcodes(data)
+
+ if not data: # Empty
+ return '', {}
+
+ def extract_data_chunk(data):
+ """Take a list of splitted shortcodes and return a string and a tail.
+
+ The string is data, the tail is ready for a new run of this same function.
+ """
+ text = []
+ for i, token in enumerate(data):
+ if token[0] == 'SHORTCODE_START':
+ name = token[3]
+ sc_id = _new_sc_id()
+ text.append(sc_id)
+ # See if this shortcode closes
+ for j in range(i, len(data)):
+ if data[j][0] == 'SHORTCODE_END' and data[j][3] == name:
+ # Extract this chunk
+ shortcodes[sc_id] = ''.join(t[1] for t in data[i:j + 1])
+ return ''.join(text), data[j + 1:]
+ # Doesn't close
+ shortcodes[sc_id] = token[1]
+ return ''.join(text), data[i + 1:]
+ elif token[0] == 'TEXT':
+ text.append(token[1])
+ return ''.join(text), data[1:]
+ elif token[0] == 'SHORTCODE_END': # This is malformed
+ raise Exception('Closing unopened shortcode {}'.format(token[3]))
+
+ text = []
+ tail = splitted
+ while True:
+ new_text, tail = extract_data_chunk(tail)
+ text.append(new_text)
+ if not tail:
+ break
+ return ''.join(text), shortcodes
+
+
def _split_shortcodes(data):
"""Given input data, splits it into a sequence of texts, shortcode starts and shortcode ends.
Returns a list of tuples of the following forms:
- 1. (_TEXT, text)
- 2. (_SHORTCODE_START, text, start, name, args)
- 3. (_SHORTCODE_END, text, start, name)
+ 1. ("TEXT", text)
+ 2. ("SHORTCODE_START", text, start, name, args)
+ 3. ("SHORTCODE_END", text, start, name)
Here, text is the raw text represented by the token; start is the starting position in data
of the token; name is the name of the shortcode; and args is a tuple (args, kw) as returned
@@ -228,9 +277,9 @@ def _split_shortcodes(data):
# Search for shortcode start
start = data.find('{{%', pos)
if start < 0:
- result.append((_TEXT, data[pos:]))
+ result.append(("TEXT", data[pos:]))
break
- result.append((_TEXT, data[pos:start]))
+ result.append(("TEXT", data[pos:start]))
# Extract name
name_start = _skip_whitespace(data, start + 3)
name_end = _skip_nonwhitespace(data, name_start)
@@ -246,18 +295,17 @@ def _split_shortcodes(data):
# Must be followed by '%}}'
if pos > len(data) or data[end_start:pos] != '%}}':
raise ParsingError("Syntax error: '{{{{% /{0}' must be followed by ' %}}}}' ({1})!".format(name, _format_position(data, end_start)))
- result.append((_SHORTCODE_END, data[start:pos], start, name))
+ result.append(("SHORTCODE_END", data[start:pos], start, name))
elif name == '%}}':
raise ParsingError("Syntax error: '{{{{%' must be followed by shortcode name ({0})!".format(_format_position(data, start)))
else:
# This is an opening shortcode
pos, args = _parse_shortcode_args(data, name_end, shortcode_name=name, start_pos=start)
- result.append((_SHORTCODE_START, data[start:pos], start, name, args))
+ result.append(("SHORTCODE_START", data[start:pos], start, name, args))
return result
-# FIXME: in v8, get rid of with_dependencies
-def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=False, lang=None, with_dependencies=False, extra_context={}):
+def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=False, lang=None, extra_context=None):
"""Apply Hugo-style shortcodes on data.
{{% name parameters %}} will end up calling the registered "name" function with the given parameters.
@@ -274,7 +322,9 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=
>>> print(apply_shortcodes('==> {{% foo bar=baz %}}some data{{% /foo %}} <==', {'foo': lambda *a, **k: k['bar']+k['data']}))
==> bazsome data <==
"""
- empty_string = data[:0] # same string type as data; to make Python 2 happy
+ if extra_context is None:
+ extra_context = {}
+ empty_string = ''
try:
# Split input data into text, shortcodes and shortcode endings
sc_data = _split_shortcodes(data)
@@ -284,17 +334,17 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=
pos = 0
while pos < len(sc_data):
current = sc_data[pos]
- if current[0] == _TEXT:
+ if current[0] == "TEXT":
result.append(current[1])
pos += 1
- elif current[0] == _SHORTCODE_END:
+ elif current[0] == "SHORTCODE_END":
raise ParsingError("Found shortcode ending '{{{{% /{0} %}}}}' which isn't closing a started shortcode ({1})!".format(current[3], _format_position(data, current[2])))
- elif current[0] == _SHORTCODE_START:
+ elif current[0] == "SHORTCODE_START":
name = current[3]
# Check if we can find corresponding ending
found = None
for p in range(pos + 1, len(sc_data)):
- if sc_data[p][0] == _SHORTCODE_END and sc_data[p][3] == name:
+ if sc_data[p][0] == "SHORTCODE_END" and sc_data[p][3] == name:
found = p
break
if found:
@@ -321,17 +371,15 @@ def apply_shortcodes(data, registry, site=None, filename=None, raise_exceptions=
if not isinstance(res, tuple): # For backards compatibility
res = (res, [])
else:
- LOGGER.error('Unknown shortcode {0} (started at {1})', name, _format_position(data, current[2]))
+ LOGGER.error('Unknown shortcode %s (started at %s)', name, _format_position(data, current[2]))
res = ('', [])
result.append(res[0])
dependencies += res[1]
- if with_dependencies:
- return empty_string.join(result), dependencies
- return empty_string.join(result)
+ return empty_string.join(result), dependencies
except ParsingError as e:
if raise_exceptions:
# Throw up
- raise e
+ raise
if filename:
LOGGER.error("Shortcode error in file {0}: {1}".format(filename, e))
else:
diff --git a/nikola/state.py b/nikola/state.py
index 6632e4f..4669d13 100644
--- a/nikola/state.py
+++ b/nikola/state.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -81,12 +81,7 @@ class Persistor():
def _save(self):
dname = os.path.dirname(self._path)
- with tempfile.NamedTemporaryFile(dir=dname, delete=False) as outf:
- # TODO replace with encoding='utf-8' and mode 'w+' in v8
+ with tempfile.NamedTemporaryFile(dir=dname, delete=False, mode='w+', encoding='utf-8') as outf:
tname = outf.name
- data = json.dumps(self._local.data, sort_keys=True, indent=2)
- try:
- outf.write(data)
- except TypeError:
- outf.write(data.encode('utf-8'))
+ json.dump(self._local.data, outf, sort_keys=True, indent=2)
shutil.move(tname, self._path)
diff --git a/nikola/utils.py b/nikola/utils.py
index 068cb3a..d029b7f 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,15 +26,11 @@
"""Utility functions."""
-from __future__ import print_function, unicode_literals, absolute_import
-import calendar
+import configparser
import datetime
-import dateutil.tz
import hashlib
import io
-import locale
-import logging
-import natsort
+import operator
import os
import re
import json
@@ -42,131 +38,87 @@ import shutil
import socket
import subprocess
import sys
+import threading
+import typing
+from collections import defaultdict, OrderedDict
+from collections.abc import Callable, Iterable
+from html import unescape as html_unescape
+from importlib import reload as _reload
+from unicodedata import normalize as unicodenormalize
+from urllib.parse import quote as urlquote
+from urllib.parse import unquote as urlunquote
+from urllib.parse import urlparse, urlunparse
+from zipfile import ZipFile as zipf
+
+import babel.dates
import dateutil.parser
import dateutil.tz
-import logbook
-try:
- from urllib import quote as urlquote
- from urllib import unquote as urlunquote
- from urlparse import urlparse, urlunparse
-except ImportError:
- from urllib.parse import quote as urlquote # NOQA
- from urllib.parse import unquote as urlunquote # NOQA
- from urllib.parse import urlparse, urlunparse # NOQA
-import warnings
+import pygments.formatters
+import pygments.formatters._mapping
import PyRSS2Gen as rss
+from blinker import signal
+from doit import tools
+from doit.cmdparse import CmdParse
+from pkg_resources import resource_filename
+from nikola.packages.pygments_better_html import BetterHtmlFormatter
+from unidecode import unidecode
+
+# Renames
+from nikola import DEBUG # NOQA
+from .log import LOGGER, get_logger # NOQA
+from .hierarchy_utils import TreeNode, clone_treenode, flatten_tree_structure, sort_classifications
+from .hierarchy_utils import join_hierarchical_category_path, parse_escaped_hierarchical_category_name
+
try:
- import pytoml as toml
+ import toml
except ImportError:
toml = None
+
try:
- import yaml
+ from ruamel.yaml import YAML
except ImportError:
- yaml = None
+ YAML = None
+
try:
import husl
except ImportError:
husl = None
-from collections import defaultdict, Callable, OrderedDict
-from logbook.compat import redirect_logging
-from logbook.more import ExceptionHandler, ColorizedStderrHandler
-from pygments.formatters import HtmlFormatter
-from zipfile import ZipFile as zipf
-from doit import tools
-from unidecode import unidecode
-from unicodedata import normalize as unicodenormalize
-from pkg_resources import resource_filename
-from doit.cmdparse import CmdParse
-
-from nikola import DEBUG
-
-__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real', 'get_theme_chain', 'load_messages', 'copy_tree',
- 'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters',
+__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real',
+ 'get_theme_chain', 'load_messages', 'copy_tree', 'copy_file',
+ 'slugify', 'unslugify', 'to_datetime', 'apply_filters',
'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path',
- '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary',
- 'TranslatableSetting', 'TemplateHookRegistry', 'LocaleBorg',
+ '_reload', 'Functionary', 'TranslatableSetting',
+ 'TemplateHookRegistry', 'LocaleBorg',
'sys_encode', 'sys_decode', 'makedirs', 'get_parent_theme_name',
'demote_headers', 'get_translation_candidate', 'write_metadata',
'ask', 'ask_yesno', 'options2docstring', 'os_path_split',
'get_displayed_page_number', 'adjust_name_for_index_path_list',
'adjust_name_for_index_path', 'adjust_name_for_index_link',
- 'NikolaPygmentsHTML', 'create_redirect', 'TreeNode',
- 'flatten_tree_structure', 'parse_escaped_hierarchical_category_name',
- 'join_hierarchical_category_path', 'clean_before_deployment', 'indent',
- 'load_data')
+ 'NikolaPygmentsHTML', 'create_redirect', 'clean_before_deployment',
+ 'sort_posts', 'smartjoin', 'indent', 'load_data', 'html_unescape',
+ 'rss_writer', 'map_metadata', 'req_missing',
+ # Deprecated, moved to hierarchy_utils:
+ 'TreeNode', 'clone_treenode', 'flatten_tree_structure',
+ 'sort_classifications', 'join_hierarchical_category_path',
+ 'parse_escaped_hierarchical_category_name',)
# Are you looking for 'generic_rss_renderer'?
# It's defined in nikola.nikola.Nikola (the site object).
-if sys.version_info[0] == 3:
- # Python 3
- bytes_str = bytes
- unicode_str = str
- unichr = chr
- raw_input = input
- from imp import reload as _reload
-else:
- bytes_str = str
- unicode_str = unicode # NOQA
- _reload = reload # NOQA
- unichr = unichr
-
-
-class ApplicationWarning(Exception):
- pass
-
-
-class ColorfulStderrHandler(ColorizedStderrHandler):
- """Stream handler with colors."""
+# Aliases, previously for Python 2/3 compatibility.
+# TODO remove in v9
+bytes_str = bytes
+unicode_str = str
+unichr = chr
- _colorful = False
+# For compatibility with old logging setups.
+# TODO remove in v9?
+STDERR_HANDLER = None
- def should_colorize(self, record):
- """Inform about colorization using the value obtained from Nikola."""
- return self._colorful
-
-
-def get_logger(name, handlers):
- """Get a logger with handlers attached."""
- l = logbook.Logger(name)
- for h in handlers:
- if isinstance(h, list):
- l.handlers += h
- else:
- l.handlers.append(h)
- return l
-
-
-STDERR_HANDLER = [ColorfulStderrHandler(
- level=logbook.INFO if not DEBUG else logbook.DEBUG,
- format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}'
-)]
-
-
-LOGGER = get_logger('Nikola', STDERR_HANDLER)
-STRICT_HANDLER = ExceptionHandler(ApplicationWarning, level='WARNING')
USE_SLUGIFY = True
-redirect_logging()
-
-if DEBUG:
- logging.basicConfig(level=logging.DEBUG)
-else:
- logging.basicConfig(level=logging.INFO)
-
-
-def showwarning(message, category, filename, lineno, file=None, line=None):
- """Show a warning (from the warnings module) to the user."""
- try:
- n = category.__name__
- except AttributeError:
- n = str(category)
- get_logger(n, STDERR_HANDLER).warn('{0}:{1}: {2}'.format(filename, lineno, message))
-
-warnings.showwarning = showwarning
-
def req_missing(names, purpose, python=True, optional=False):
"""Log that we are missing some requirements.
@@ -205,7 +157,7 @@ def req_missing(names, purpose, python=True, optional=False):
purpose, pnames, whatarethey_p)
if optional:
- LOGGER.warn(msg)
+ LOGGER.warning(msg)
else:
LOGGER.error(msg)
LOGGER.error('Exiting due to missing dependencies.')
@@ -214,20 +166,19 @@ def req_missing(names, purpose, python=True, optional=False):
return msg
-from nikola import filters as task_filters # NOQA
ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding
def sys_encode(thing):
"""Return bytes encoded in the system's encoding."""
- if isinstance(thing, unicode_str):
+ if isinstance(thing, str):
return thing.encode(ENCODING)
return thing
def sys_decode(thing):
"""Return Unicode."""
- if isinstance(thing, bytes_str):
+ if isinstance(thing, bytes):
return thing.decode(ENCODING)
return thing
@@ -255,7 +206,7 @@ class Functionary(defaultdict):
def __init__(self, default, default_lang):
"""Initialize a functionary."""
- super(Functionary, self).__init__(default)
+ super().__init__(default)
self.default_lang = default_lang
def __call__(self, key, lang=None):
@@ -292,7 +243,7 @@ class TranslatableSetting(object):
def __getattribute__(self, attr):
"""Return attributes, falling back to string attributes."""
try:
- return super(TranslatableSetting, self).__getattribute__(attr)
+ return super().__getattribute__(attr)
except AttributeError:
return self().__getattribute__(attr)
@@ -356,15 +307,11 @@ class TranslatableSetting(object):
def __str__(self):
"""Return the value in the currently set language (deprecated)."""
- return self.values[self.get_lang()]
-
- def __unicode__(self):
- """Return the value in the currently set language (deprecated)."""
- return self.values[self.get_lang()]
+ return str(self.values[self.get_lang()])
def __repr__(self):
"""Provide a representation for programmers."""
- return '<TranslatableSetting: {0!r}>'.format(self.name)
+ return '<TranslatableSetting: {0!r} = {1!r}>'.format(self.name, self._inp)
def format(self, *args, **kwargs):
"""Format ALL the values in the setting the same way."""
@@ -465,9 +412,8 @@ class TemplateHookRegistry(object):
>>> r = TemplateHookRegistry('foo', None)
>>> r.append('Hello!')
>>> r.append(lambda x: 'Hello ' + x + '!', False, 'world')
- >>> str(r()) # str() call is not recommended in real use
+ >>> repr(r())
'Hello!\nHello world!'
- >>>
"""
def __init__(self, name, site):
@@ -509,9 +455,23 @@ class TemplateHookRegistry(object):
c = callable(inp)
self._items.append((c, inp, wants_site_and_context, args, kwargs))
+ def calculate_deps(self):
+ """Calculate dependencies for a registry."""
+ deps = []
+ for is_callable, inp, wants_site_and_context, args, kwargs in self._items:
+ if not is_callable:
+ name = inp
+ elif hasattr(inp, 'template_registry_identifier'):
+ name = inp.template_registry_identifier
+ elif hasattr(inp, '__doc__'):
+ name = inp.__doc__
+ else:
+ name = '_undefined_callable_'
+ deps.append((is_callable, name, wants_site_and_context, args, kwargs))
+
def __hash__(self):
"""Return hash of a registry."""
- return hash(config_changed({self.name: self._items})._calc_digest())
+ return hash(config_changed({self.name: self.calculate_deps()})._calc_digest())
def __str__(self):
"""Stringify a registry."""
@@ -526,12 +486,14 @@ class CustomEncoder(json.JSONEncoder):
"""Custom JSON encoder."""
def default(self, obj):
- """Default encoding handler."""
+ """Create default encoding handler."""
try:
- return super(CustomEncoder, self).default(obj)
+ return super().default(obj)
except TypeError:
if isinstance(obj, (set, frozenset)):
return self.encode(sorted(list(obj)))
+ elif isinstance(obj, TranslatableSetting):
+ s = json.dumps(obj._inp, cls=CustomEncoder, sort_keys=True)
else:
s = repr(obj).split('0x', 1)[0]
return s
@@ -542,11 +504,34 @@ class config_changed(tools.config_changed):
def __init__(self, config, identifier=None):
"""Initialize config_changed."""
- super(config_changed, self).__init__(config)
+ super().__init__(config)
self.identifier = '_config_changed'
if identifier is not None:
self.identifier += ':' + identifier
+ # DEBUG (for unexpected rebuilds)
+ @classmethod
+ def _write_into_debug_db(cls, digest: str, data: str) -> None: # pragma: no cover
+ """Write full values of config_changed into a sqlite3 database."""
+ import sqlite3
+ try:
+ cls.debug_db_cursor
+ except AttributeError:
+ cls.debug_db_conn = sqlite3.connect("cc_debug.sqlite3")
+ cls.debug_db_id = datetime.datetime.now().isoformat()
+ cls.debug_db_cursor = cls.debug_db_conn.cursor()
+ cls.debug_db_cursor.execute("""
+ CREATE TABLE IF NOT EXISTS hashes (hash CHARACTER(32) PRIMARY KEY, json_data TEXT);
+ """)
+ cls.debug_db_conn.commit()
+
+ try:
+ cls.debug_db_cursor.execute("INSERT INTO hashes (hash, json_data) VALUES (?, ?);", (digest, data))
+ cls.debug_db_conn.commit()
+ except sqlite3.IntegrityError:
+ # ON CONFLICT DO NOTHING, except Ubuntu 16.04’s sqlite3 is too ancient for this
+ cls.debug_db_conn.rollback()
+
def _calc_digest(self):
"""Calculate a config_changed digest."""
if isinstance(self.config, str):
@@ -558,9 +543,14 @@ class config_changed(tools.config_changed):
else:
byte_data = data
digest = hashlib.md5(byte_data).hexdigest()
+
+ # DEBUG (for unexpected rebuilds)
+ # self._write_into_debug_db(digest, data)
+ # Alternative (without database):
# LOGGER.debug('{{"{0}": {1}}}'.format(digest, byte_data))
# Humanized format:
# LOGGER.debug('[Digest {0} for {2}]\n{1}\n[Digest {0} for {2}]'.format(digest, byte_data, self.identifier))
+
return digest
else:
raise Exception('Invalid type of config_changed parameter -- got '
@@ -605,27 +595,52 @@ def get_theme_path(theme):
return theme
+def parse_theme_meta(theme_dir):
+ """Parse a .theme meta file."""
+ cp = configparser.ConfigParser()
+ # The `or` case is in case theme_dir ends with a trailing slash
+ theme_name = os.path.basename(theme_dir) or os.path.basename(os.path.dirname(theme_dir))
+ theme_meta_path = os.path.join(theme_dir, theme_name + '.theme')
+ cp.read(theme_meta_path)
+ return cp if cp.has_section('Theme') else None
+
+
def get_template_engine(themes):
"""Get template engine used by a given theme."""
for theme_name in themes:
- engine_path = os.path.join(theme_name, 'engine')
- if os.path.isfile(engine_path):
- with open(engine_path) as fd:
- return fd.readlines()[0].strip()
+ meta = parse_theme_meta(theme_name)
+ if meta:
+ e = meta.get('Theme', 'engine', fallback=None)
+ if e:
+ return e
+ else:
+ # Theme still uses old-style parent/engine files
+ engine_path = os.path.join(theme_name, 'engine')
+ if os.path.isfile(engine_path):
+ with open(engine_path) as fd:
+ return fd.readlines()[0].strip()
# default
return 'mako'
def get_parent_theme_name(theme_name, themes_dirs=None):
"""Get name of parent theme."""
- parent_path = os.path.join(theme_name, 'parent')
- if os.path.isfile(parent_path):
- with open(parent_path) as fd:
- parent = fd.readlines()[0].strip()
- if themes_dirs:
+ meta = parse_theme_meta(theme_name)
+ if meta:
+ parent = meta.get('Theme', 'parent', fallback=None)
+ if themes_dirs and parent:
return get_theme_path_real(parent, themes_dirs)
return parent
- return None
+ else:
+ # Theme still uses old-style parent/engine files
+ parent_path = os.path.join(theme_name, 'parent')
+ if os.path.isfile(parent_path):
+ with open(parent_path) as fd:
+ parent = fd.readlines()[0].strip()
+ if themes_dirs:
+ return get_theme_path_real(parent, themes_dirs)
+ return parent
+ return None
def get_theme_chain(theme, themes_dirs):
@@ -641,7 +656,7 @@ def get_theme_chain(theme, themes_dirs):
return themes
-language_incomplete_warned = []
+INCOMPLETE_LANGUAGES_WARNED = set()
class LanguageNotFoundError(Exception):
@@ -665,38 +680,50 @@ def load_messages(themes, translations, default_lang, themes_dirs):
"""
messages = Functionary(dict, default_lang)
oldpath = list(sys.path)
+ found = {lang: False for lang in translations.keys()}
+ last_exception = None
+ completion_status = {lang: False for lang in translations.keys()}
for theme_name in themes[::-1]:
msg_folder = os.path.join(get_theme_path(theme_name), 'messages')
default_folder = os.path.join(get_theme_path_real('base', themes_dirs), 'messages')
sys.path.insert(0, default_folder)
sys.path.insert(0, msg_folder)
+
english = __import__('messages_en')
# If we don't do the reload, the module is cached
_reload(english)
- for lang in list(translations.keys()):
+ for lang in translations.keys():
try:
translation = __import__('messages_' + lang)
# If we don't do the reload, the module is cached
_reload(translation)
- if sorted(translation.MESSAGES.keys()) !=\
- sorted(english.MESSAGES.keys()) and \
- lang not in language_incomplete_warned:
- language_incomplete_warned.append(lang)
- LOGGER.warn("Incomplete translation for language "
- "'{0}'.".format(lang))
+ found[lang] = True
+ if sorted(translation.MESSAGES.keys()) != sorted(english.MESSAGES.keys()):
+ completion_status[lang] = completion_status[lang] or False
+ else:
+ completion_status[lang] = True
+
messages[lang].update(english.MESSAGES)
for k, v in translation.MESSAGES.items():
if v:
messages[lang][k] = v
del(translation)
except ImportError as orig:
- raise LanguageNotFoundError(lang, orig)
+ last_exception = orig
del(english)
- sys.path = oldpath
+ sys.path = oldpath
+
+ if not all(found.values()):
+ raise LanguageNotFoundError(lang, last_exception)
+ for lang, status in completion_status.items():
+ if not status and lang not in INCOMPLETE_LANGUAGES_WARNED:
+ LOGGER.warning("Incomplete translation for language '{0}'.".format(lang))
+ INCOMPLETE_LANGUAGES_WARNED.add(lang)
+
return messages
-def copy_tree(src, dst, link_cutoff=None):
+def copy_tree(src, dst, link_cutoff=None, ignored_filenames=None):
"""Copy a src tree to the dst folder.
Example:
@@ -707,11 +734,13 @@ def copy_tree(src, dst, link_cutoff=None):
should copy "themes/defauts/assets/foo/bar" to
"output/assets/foo/bar"
- if link_cutoff is set, then the links pointing at things
+ If link_cutoff is set, then the links pointing at things
*inside* that folder will stay as links, and links
pointing *outside* that folder will be copied.
+
+ ignored_filenames is a set of file names that will be ignored.
"""
- ignore = set(['.svn'])
+ ignore = set(['.svn', '.git']) | (ignored_filenames or set())
base_len = len(src.split(os.sep))
for root, dirs, files in os.walk(src, followlinks=True):
root_parts = root.split(os.sep)
@@ -761,11 +790,12 @@ def remove_file(source):
elif os.path.isfile(source) or os.path.islink(source):
os.remove(source)
+
# slugify is adopted from
# http://code.activestate.com/recipes/
# 577257-slugify-make-a-string-usable-in-a-url-or-filename/
-_slugify_strip_re = re.compile(r'[^+\w\s-]')
-_slugify_hyphenate_re = re.compile(r'[-\s]+')
+_slugify_strip_re = re.compile(r'[^+\w\s-]', re.UNICODE)
+_slugify_hyphenate_re = re.compile(r'[-\s]+', re.UNICODE)
def slugify(value, lang=None, force=False):
@@ -782,16 +812,14 @@ def slugify(value, lang=None, force=False):
>>> print(slugify('foo bar', lang='en'))
foo-bar
"""
- if lang is None: # TODO: remove in v8
- LOGGER.warn("slugify() called without language!")
- if not isinstance(value, unicode_str):
+ if not isinstance(value, str):
raise ValueError("Not a unicode object: {0}".format(value))
if USE_SLUGIFY or force:
# This is the standard state of slugify, which actually does some work.
# It is the preferred style, especially for Western languages.
- value = unicode_str(unidecode(value))
- value = _slugify_strip_re.sub('', value, re.UNICODE).strip().lower()
- return _slugify_hyphenate_re.sub('-', value, re.UNICODE)
+ value = str(unidecode(value))
+ value = _slugify_strip_re.sub('', value).strip().lower()
+ return _slugify_hyphenate_re.sub('-', value)
else:
# This is the “disarmed” state of slugify, which lets the user
# have any character they please (be it regular ASCII with spaces,
@@ -814,11 +842,9 @@ def unslugify(value, lang=None, discard_numbers=True):
If discard_numbers is True, numbers right at the beginning of input
will be removed.
"""
- if lang is None: # TODO: remove in v8
- LOGGER.warn("unslugify() called without language!")
if discard_numbers:
value = re.sub('^[0-9]+', '', value)
- value = re.sub('([_\-\.])', ' ', value)
+ value = re.sub(r'([_\-\.])', ' ', value)
value = value.strip().capitalize()
return value
@@ -835,6 +861,16 @@ def encodelink(iri):
encoded_link = urlunparse(link.values())
return encoded_link
+
+def full_path_from_urlparse(parsed) -> str:
+ """Given urlparse output, return the full path (with query and fragment)."""
+ dst = parsed.path
+ if parsed.query:
+ dst = "{0}?{1}".format(dst, parsed.query)
+ if parsed.fragment:
+ dst = "{0}#{1}".format(dst, parsed.fragment)
+ return dst
+
# A very slightly safer version of zip.extractall that works on
# python < 2.6
@@ -868,6 +904,8 @@ def extract_all(zipfile, path='themes'):
def to_datetime(value, tzinfo=None):
"""Convert string to datetime."""
try:
+ if type(value) == datetime.date:
+ value = datetime.datetime.combine(value, datetime.time(0, 0))
if not isinstance(value, datetime.datetime):
# dateutil does bad things with TZs like UTC-03:00.
dateregexp = re.compile(r' UTC([+-][0-9][0-9]:[0-9][0-9])')
@@ -898,6 +936,9 @@ def current_time(tzinfo=None):
return dt
+from nikola import filters as task_filters # NOQA
+
+
def apply_filters(task, filters, skip_ext=None):
"""Apply filters to a task.
@@ -916,11 +957,11 @@ def apply_filters(task, filters, skip_ext=None):
if isinstance(key, (tuple, list)):
if ext in key:
return value
- elif isinstance(key, (bytes_str, unicode_str)):
+ elif isinstance(key, (bytes, str)):
if ext == key:
return value
else:
- assert False, key
+ raise ValueError("Cannot find filter match for {0}".format(key))
for target in task.get('targets', []):
ext = os.path.splitext(target)[-1].lower()
@@ -949,26 +990,26 @@ def get_crumbs(path, is_file=False, index_folder=None, lang=None):
>>> crumbs = get_crumbs('galleries')
>>> len(crumbs)
1
- >>> print('|'.join(crumbs[0]))
- #|galleries
+ >>> crumbs[0]
+ ['#', 'galleries']
>>> crumbs = get_crumbs(os.path.join('galleries','demo'))
>>> len(crumbs)
2
- >>> print('|'.join(crumbs[0]))
- ..|galleries
- >>> print('|'.join(crumbs[1]))
- #|demo
+ >>> crumbs[0]
+ ['..', 'galleries']
+ >>> crumbs[1]
+ ['#', 'demo']
>>> crumbs = get_crumbs(os.path.join('listings','foo','bar'), is_file=True)
>>> len(crumbs)
3
- >>> print('|'.join(crumbs[0]))
- ..|listings
- >>> print('|'.join(crumbs[1]))
- .|foo
- >>> print('|'.join(crumbs[2]))
- #|bar
+ >>> crumbs[0]
+ ['..', 'listings']
+ >>> crumbs[1]
+ ['.', 'foo']
+ >>> crumbs[2]
+ ['#', 'bar']
"""
crumbs = path.split(os.sep)
_crumbs = []
@@ -1009,8 +1050,8 @@ def get_asset_path(path, themes, files_folders={'files': ''}, output_dir='output
If it's not provided by either, it will be chacked in output, where
it may have been created by another plugin.
- >>> print(get_asset_path('assets/css/rst.css', get_theme_chain('bootstrap3', ['themes'])))
- /.../nikola/data/themes/base/assets/css/rst.css
+ >>> print(get_asset_path('assets/css/nikola_rst.css', get_theme_chain('bootstrap3', ['themes'])))
+ /.../nikola/data/themes/base/assets/css/nikola_rst.css
>>> print(get_asset_path('assets/css/theme.css', get_theme_chain('bootstrap3', ['themes'])))
/.../nikola/data/themes/bootstrap3/assets/css/theme.css
@@ -1050,24 +1091,49 @@ class LocaleBorgUninitializedException(Exception):
def __init__(self):
"""Initialize exception."""
- super(LocaleBorgUninitializedException, self).__init__("Attempt to use LocaleBorg before initialization")
+ super().__init__("Attempt to use LocaleBorg before initialization")
+
+
+# Customized versions of babel.dates functions that don't do weird stuff with
+# timezones. Without these fixes, DST would follow local settings (because
+# dateutil’s timezones return stuff depending on their input, and datetime.time
+# objects have no year/month/day to base the information on.
+def format_datetime(datetime=None, format='medium',
+ locale=babel.dates.LC_TIME):
+ """Format a datetime object."""
+ locale = babel.dates.Locale.parse(locale)
+ if format in ('full', 'long', 'medium', 'short'):
+ return babel.dates.get_datetime_format(format, locale=locale) \
+ .replace("'", "") \
+ .replace('{0}', format_time(datetime, format, locale=locale)) \
+ .replace('{1}', babel.dates.format_date(datetime, format, locale=locale))
+ else:
+ return babel.dates.parse_pattern(format).apply(datetime, locale)
-class LocaleBorg(object):
- """Provide locale related services and autoritative current_lang.
+def format_time(time=None, format='medium', locale=babel.dates.LC_TIME):
+ """Format time. Input can be datetime.time or datetime.datetime."""
+ locale = babel.dates.Locale.parse(locale)
+ if format in ('full', 'long', 'medium', 'short'):
+ format = babel.dates.get_time_format(format, locale=locale)
+ return babel.dates.parse_pattern(format).apply(time, locale)
- current_lang is the last lang for which the locale was set
- and is meant to be set only by LocaleBorg.set_locale.
- python's locale code should not be directly called from code outside of
- LocaleBorg, they are compatibilty issues with py version and OS support
- better handled at one central point, LocaleBorg.
+def format_skeleton(skeleton, datetime=None, fo=None, fuzzy=True,
+ locale=babel.dates.LC_TIME):
+ """Format a datetime based on a skeleton."""
+ locale = babel.dates.Locale.parse(locale)
+ if fuzzy and skeleton not in locale.datetime_skeletons:
+ skeleton = babel.dates.match_skeleton(skeleton, locale.datetime_skeletons)
+ format = locale.datetime_skeletons[skeleton]
+ return format_datetime(datetime, format, locale)
- In particular, don't call locale.setlocale outside of LocaleBorg.
- Assumptions:
- We need locales only for the languages there is a nikola translation.
- We don't need to support current_lang through nested contexts
+class LocaleBorg(object):
+ """Provide locale related services and autoritative current_lang.
+
+ This class stores information about the locales used and interfaces
+ with the Babel library to provide internationalization services.
Usage:
# early in cmd or test execution
@@ -1077,46 +1143,39 @@ class LocaleBorg(object):
lang = LocaleBorg().<service>
Available services:
- .current_lang : autoritative current_lang , the last seen in set_locale
- .set_locale(lang) : sets current_lang and sets the locale for lang
- .get_month_name(month_no, lang) : returns the localized month name
+ .current_lang: autoritative current_lang, the last seen in set_locale
+ .formatted_date: format a date(time) according to locale rules
+ .format_date_in_string: take a message and format the date in it
- NOTE: never use locale.getlocale() , it can return values that
- locale.setlocale will not accept in Windows XP, 7 and pythons 2.6, 2.7, 3.3
- Examples: "Spanish", "French" can't do the full circle set / get / set
+ The default implementation uses the Babel package and completely ignores
+ the Python `locale` module. If you wish to override this, write functions
+ and assign them to the appropriate names. The functions are:
+
+ * LocaleBorg.datetime_formatter(date, date_format, lang, locale)
+ * LocaleBorg.in_string_formatter(date, mode, custom_format, lang, locale)
"""
initialized = False
+ # Can be used to override Babel
+ datetime_formatter = None
+ in_string_formatter = None
+
@classmethod
- def initialize(cls, locales, initial_lang):
+ def initialize(cls, locales: 'typing.Dict[str, str]', initial_lang: str):
"""Initialize LocaleBorg.
- locales : dict with lang: locale_n
- the same keys as in nikola's TRANSLATIONS
- locale_n a sanitized locale, meaning
- locale.setlocale(locale.LC_ALL, locale_n) will succeed
- locale_n expressed in the string form, like "en.utf8"
+ locales: dict with custom locale name overrides.
"""
- assert initial_lang is not None and initial_lang in locales
+ if not initial_lang:
+ raise ValueError("Unknown initial language {0}".format(initial_lang))
cls.reset()
cls.locales = locales
- cls.month_name_handlers = []
- cls.formatted_date_handlers = []
-
- # needed to decode some localized output in py2x
- encodings = {}
- for lang in locales:
- locale.setlocale(locale.LC_ALL, locales[lang])
- loc, encoding = locale.getlocale()
- encodings[lang] = encoding
-
- cls.encodings = encodings
cls.__initial_lang = initial_lang
cls.initialized = True
def __get_shared_state(self):
- if not self.initialized:
+ if not self.initialized: # pragma: no cover
raise LocaleBorgUninitializedException()
shared_state = getattr(self.__thread_local, 'shared_state', None)
if shared_state is None:
@@ -1130,38 +1189,14 @@ class LocaleBorg(object):
Used in testing to prevent leaking state between tests.
"""
- import threading
cls.__thread_local = threading.local()
cls.__thread_lock = threading.Lock()
cls.locales = {}
- cls.encodings = {}
cls.initialized = False
- cls.month_name_handlers = []
- cls.formatted_date_handlers = []
cls.thread_local = None
- cls.thread_lock = None
-
- @classmethod
- def add_handler(cls, month_name_handler=None, formatted_date_handler=None):
- """Allow to add month name and formatted date handlers.
-
- If month_name_handler is not None, it is expected to be a callable
- which accepts (month_no, lang) and returns either a string or None.
-
- If formatted_date_handler is not None, it is expected to be a callable
- which accepts (date_format, date, lang) and returns either a string or
- None.
-
- A handler is expected to either return the correct result for the given
- language and data, or return None to indicate it is not able to do the
- job. In that case, the next handler is asked, and finally the default
- implementation is used.
- """
- if month_name_handler is not None:
- cls.month_name_handlers.append(month_name_handler)
- if formatted_date_handler is not None:
- cls.formatted_date_handlers.append(formatted_date_handler)
+ cls.datetime_formatter = None
+ cls.in_string_formatter = None
def __init__(self):
"""Initialize."""
@@ -1169,79 +1204,68 @@ class LocaleBorg(object):
raise LocaleBorgUninitializedException()
@property
- def current_lang(self):
+ def current_lang(self) -> str:
"""Return the current language."""
return self.__get_shared_state()['current_lang']
- def __set_locale(self, lang):
- """Set the locale for language lang without updating current_lang."""
- locale_n = self.locales[lang]
- locale.setlocale(locale.LC_ALL, locale_n)
-
- def set_locale(self, lang):
- """Set the locale for language lang, returns an empty string.
-
- in linux the locale encoding is set to utf8,
- in windows that cannot be guaranted.
- In either case, the locale encoding is available in cls.encodings[lang]
- """
+ def set_locale(self, lang: str) -> str:
+ """Set the current language and return an empty string (to make use in templates easier)."""
with self.__thread_lock:
- # intentional non try-except: templates must ask locales with a lang,
- # let the code explode here and not hide the point of failure
- # Also, not guarded with an if lang==current_lang because calendar may
- # put that out of sync
- self.__set_locale(lang)
self.__get_shared_state()['current_lang'] = lang
return ''
- def get_month_name(self, month_no, lang):
- """Return localized month name in an unicode string."""
- # For thread-safety
- with self.__thread_lock:
- for handler in self.month_name_handlers:
- res = handler(month_no, lang)
- if res is not None:
- return res
- old_lang = self.current_lang
- self.__set_locale(lang)
- s = calendar.month_name[month_no]
- self.__set_locale(old_lang)
- if sys.version_info[0] == 2:
- enc = self.encodings[lang]
- if not enc:
- enc = 'UTF-8'
-
- s = s.decode(enc)
- return s
+ def formatted_date(self, date_format: 'str',
+ date: 'typing.Union[datetime.date, datetime.datetime]',
+ lang: 'typing.Optional[str]' = None) -> str:
+ """Return the formatted date/datetime as a string."""
+ if lang is None:
+ lang = self.current_lang
+ locale = self.locales.get(lang, lang)
+ # Get a string out of a TranslatableSetting
+ if isinstance(date_format, TranslatableSetting):
+ date_format = date_format(lang)
+
+ # Always ask Python if the date_format is webiso
+ if date_format == 'webiso':
+ # Formatted after RFC 3339 (web ISO 8501 profile) with Zulu
+ # zone designator for times in UTC and no microsecond precision.
+ return date.replace(microsecond=0).isoformat().replace('+00:00', 'Z')
+ elif LocaleBorg.datetime_formatter is not None:
+ return LocaleBorg.datetime_formatter(date, date_format, lang, locale)
+ else:
+ return format_datetime(date, date_format, locale=locale)
- def formatted_date(self, date_format, date):
- """Return the formatted date as unicode."""
- with self.__thread_lock:
- current_lang = self.current_lang
- # For thread-safety
- self.__set_locale(current_lang)
- fmt_date = None
- # Get a string out of a TranslatableSetting
- if isinstance(date_format, TranslatableSetting):
- date_format = date_format(current_lang)
- # First check handlers
- for handler in self.formatted_date_handlers:
- fmt_date = handler(date_format, date, current_lang)
- if fmt_date is not None:
- break
- # If no handler was able to format the date, ask Python
- if fmt_date is None:
- if date_format == 'webiso':
- # Formatted after RFC 3339 (web ISO 8501 profile) with Zulu
- # zone desgignator for times in UTC and no microsecond precision.
- fmt_date = date.replace(microsecond=0).isoformat().replace('+00:00', 'Z')
+ def format_date_in_string(self, message: str, date: datetime.date, lang: 'typing.Optional[str]' = None) -> str:
+ """Format date inside a string (message).
+
+ Accepted modes: month, month_year, month_day_year.
+ Format: {month} for standard, {month:MMMM} for customization.
+ """
+ modes = {
+ 'month': ('date', 'LLLL'),
+ 'month_year': ('skeleton', 'yMMMM'),
+ 'month_day_year': ('date', 'long')
+ }
+
+ if lang is None:
+ lang = self.current_lang
+ locale = self.locales.get(lang, lang)
+
+ def date_formatter(match: typing.Match) -> str:
+ """Format a date as requested."""
+ mode, custom_format = match.groups()
+ if LocaleBorg.in_string_formatter is not None:
+ return LocaleBorg.in_string_formatter(date, mode, custom_format, lang, locale)
+ elif custom_format:
+ return babel.dates.format_date(date, custom_format, locale)
+ else:
+ function, fmt = modes[mode]
+ if function == 'skeleton':
+ return format_skeleton(fmt, date, locale=locale)
else:
- fmt_date = date.strftime(date_format)
+ return babel.dates.format_date(date, fmt, locale)
- # Issue #383, this changes from py2 to py3
- if isinstance(fmt_date, bytes_str):
- fmt_date = fmt_date.decode('utf8')
- return fmt_date
+ return re.sub(r'{(.*?)(?::(.*?))?}', date_formatter, message)
class ExtendedRSS2(rss.RSS2):
@@ -1253,8 +1277,7 @@ class ExtendedRSS2(rss.RSS2):
"""Publish a feed."""
if self.xsl_stylesheet_href:
handler.processingInstruction("xml-stylesheet", 'type="text/xsl" href="{0}" media="all"'.format(self.xsl_stylesheet_href))
- # old-style class in py2
- rss.RSS2.publish(self, handler)
+ super().publish(handler)
def publish_extensions(self, handler):
"""Publish extensions."""
@@ -1272,9 +1295,10 @@ class ExtendedItem(rss.RSSItem):
def __init__(self, **kw):
"""Initialize RSS item."""
- self.creator = kw.pop('creator')
+ self.creator = kw.pop('creator', None)
+
# It's an old style class
- return rss.RSSItem.__init__(self, **kw)
+ rss.RSSItem.__init__(self, **kw)
def publish_extensions(self, handler):
"""Publish extensions."""
@@ -1314,24 +1338,34 @@ def demote_headers(doc, level=1):
if level == 0:
return doc
elif level > 0:
- r = range(1, 7 - level)
+ levels = range(1, 7 - (level - 1))
+ levels = reversed(levels)
elif level < 0:
- r = range(1 + level, 7)
- for i in reversed(r):
- # html headers go to 6, so we can’t “lower” beneath five
- elements = doc.xpath('//h' + str(i))
- for e in elements:
- e.tag = 'h' + str(i + level)
+ levels = range(2 + level, 7)
+
+ for before in levels:
+ after = before + level
+ if after < 1:
+ # html headers can't go lower than 1
+ after = 1
+ elif after > 6:
+ # html headers go until 6
+ after = 6
+
+ if before == after:
+ continue
+
+ elements = doc.xpath('//h{}'.format(before))
+ new_tag = 'h{}'.format(after)
+ for element in elements:
+ element.tag = new_tag
def get_root_dir():
"""Find root directory of nikola site by looking for conf.py."""
root = os.getcwd()
- if sys.version_info[0] == 2:
- confname = b'conf.py'
- else:
- confname = 'conf.py'
+ confname = 'conf.py'
while True:
if os.path.exists(os.path.join(root, confname)):
@@ -1385,7 +1419,7 @@ def get_translation_candidate(config, path, lang):
# This will still break if the user has ?*[]\ in the pattern. But WHY WOULD HE?
pattern = pattern.replace('.', r'\.')
pattern = pattern.replace('{path}', '(?P<path>.+?)')
- pattern = pattern.replace('{ext}', '(?P<ext>[^\./]+)')
+ pattern = pattern.replace('{ext}', r'(?P<ext>[^\./]+)')
pattern = pattern.replace('{lang}', '(?P<lang>{0})'.format('|'.join(config['TRANSLATIONS'].keys())))
m = re.match(pattern, path)
if m and all(m.groups()): # It's a translated path
@@ -1406,24 +1440,59 @@ def get_translation_candidate(config, path, lang):
return config['TRANSLATIONS_PATTERN'].format(path=p, ext=e, lang=lang)
-def write_metadata(data):
- """Write metadata."""
- order = ('title', 'slug', 'date', 'tags', 'category', 'link', 'description', 'type')
- f = '.. {0}: {1}'
- meta = []
- for k in order:
- try:
- meta.append(f.format(k, data.pop(k)))
- except KeyError:
- pass
+def write_metadata(data, metadata_format=None, comment_wrap=False, site=None, compiler=None):
+ """Write metadata.
- # Leftover metadata (user-specified/non-default).
- for k in natsort.natsorted(list(data.keys()), alg=natsort.ns.F | natsort.ns.IC):
- meta.append(f.format(k, data[k]))
-
- meta.append('')
-
- return '\n'.join(meta)
+ Recommended usage: pass `site`, `comment_wrap` (True, False, or a 2-tuple of start/end markers), and optionally `compiler`. Other options are for backwards compatibility.
+ """
+ # API compatibility
+ if metadata_format is None and site is not None:
+ metadata_format = site.config.get('METADATA_FORMAT', 'nikola').lower()
+ if metadata_format is None:
+ metadata_format = 'nikola'
+
+ if site is None:
+ import nikola.metadata_extractors
+ metadata_extractors_by = nikola.metadata_extractors.default_metadata_extractors_by()
+ nikola.metadata_extractors.load_defaults(site, metadata_extractors_by)
+ else:
+ metadata_extractors_by = site.metadata_extractors_by
+
+ # Pelican is mapped to rest_docinfo, markdown_meta, or nikola.
+ if metadata_format == 'pelican':
+ if compiler and compiler.name == 'rest':
+ metadata_format = 'rest_docinfo'
+ elif compiler and compiler.name == 'markdown':
+ metadata_format = 'markdown_meta'
+ else:
+ # Quiet fallback.
+ metadata_format = 'nikola'
+
+ default_meta = ('nikola', 'rest_docinfo', 'markdown_meta')
+ extractor = metadata_extractors_by['name'].get(metadata_format)
+ if extractor and extractor.supports_write:
+ extractor.check_requirements()
+ return extractor.write_metadata(data, comment_wrap)
+ elif extractor and metadata_format not in default_meta:
+ LOGGER.warning('Writing METADATA_FORMAT {} is not supported, using "nikola" format'.format(metadata_format))
+ elif metadata_format not in default_meta:
+ LOGGER.warning('Unknown METADATA_FORMAT {}, using "nikola" format'.format(metadata_format))
+
+ if metadata_format == 'rest_docinfo':
+ title = data['title']
+ results = [
+ '=' * len(title),
+ title,
+ '=' * len(title),
+ ''
+ ] + [':{0}: {1}'.format(k, v) for k, v in data.items() if v and k != 'title'] + ['']
+ return '\n'.join(results)
+ elif metadata_format == 'markdown_meta':
+ results = ['{0}: {1}'.format(k, v) for k, v in data.items() if v] + ['', '']
+ return '\n'.join(results)
+ else: # Nikola, default
+ from nikola.metadata_extractors import DEFAULT_EXTRACTOR
+ return DEFAULT_EXTRACTOR.write_metadata(data, comment_wrap)
def ask(query, default=None):
@@ -1432,10 +1501,7 @@ def ask(query, default=None):
default_q = ' [{0}]'.format(default)
else:
default_q = ''
- if sys.version_info[0] == 3:
- inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip()
- else:
- inp = raw_input("{query}{default_q}: ".format(query=query, default_q=default_q).encode('utf-8')).strip()
+ inp = input("{query}{default_q}: ".format(query=query, default_q=default_q)).strip()
if inp or default is None:
return inp
else:
@@ -1450,10 +1516,7 @@ def ask_yesno(query, default=None):
default_q = ' [Y/n]'
elif default is False:
default_q = ' [y/N]'
- if sys.version_info[0] == 3:
- inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q)).strip()
- else:
- inp = raw_input("{query}{default_q} ".format(query=query, default_q=default_q).encode('utf-8')).strip()
+ inp = input("{query}{default_q} ".format(query=query, default_q=default_q)).strip()
if inp:
return inp.lower().startswith('y')
elif default is not None:
@@ -1502,10 +1565,6 @@ class Commands(object):
# cleanup: run is doit-only, init is useless in an existing site
if k in ['run', 'init']:
continue
- if sys.version_info[0] == 2:
- k2 = bytes(k)
- else:
- k2 = k
self._cmdnames.append(k)
@@ -1516,7 +1575,7 @@ class Commands(object):
# doit command: needs some help
opt = v(config=self._config, **self._doitargs).get_options()
nc = type(
- k2,
+ k,
(CommandWrapper,),
{
'__doc__': options2docstring(k, opt)
@@ -1568,17 +1627,27 @@ def options2docstring(name, options):
return '\n'.join(result)
-class NikolaPygmentsHTML(HtmlFormatter):
+class NikolaPygmentsHTML(BetterHtmlFormatter):
"""A Nikola-specific modification of Pygments' HtmlFormatter."""
- def __init__(self, anchor_ref, classes=None, linenos='table', linenostart=1):
+ def __init__(self, anchor_ref=None, classes=None, **kwargs):
"""Initialize formatter."""
if classes is None:
classes = ['code', 'literal-block']
+ if anchor_ref:
+ kwargs['lineanchors'] = slugify(
+ anchor_ref, lang=LocaleBorg().current_lang, force=True)
self.nclasses = classes
- super(NikolaPygmentsHTML, self).__init__(
- cssclass='code', linenos=linenos, linenostart=linenostart, nowrap=False,
- lineanchors=slugify(anchor_ref, lang=LocaleBorg().current_lang, force=True), anchorlinenos=True)
+ kwargs['cssclass'] = 'code'
+ if not kwargs.get('linenos'):
+ # Default to no line numbers (Issue #3426)
+ kwargs['linenos'] = False
+ if kwargs.get('linenos') not in {'table', 'inline', 'ol', False}:
+ # Map invalid values to table
+ kwargs['linenos'] = 'table'
+ kwargs['anchorlinenos'] = kwargs['linenos'] == 'table'
+ kwargs['nowrap'] = False
+ super().__init__(**kwargs)
def wrap(self, source, outfile):
"""Wrap the ``source``, which is a generator yielding individual lines, in custom generators."""
@@ -1596,6 +1665,10 @@ class NikolaPygmentsHTML(HtmlFormatter):
yield 0, '</pre>'
+# For consistency, override the default formatter.
+pygments.formatters._formatter_cache['HTML'] = NikolaPygmentsHTML
+
+
def get_displayed_page_number(i, num_pages, site):
"""Get page number to be displayed for entry `i`."""
if not i:
@@ -1621,7 +1694,7 @@ def adjust_name_for_index_path_list(path_list, i, displayed_i, lang, site, force
path_list.append(index_file)
if site.config["PRETTY_URLS"] and site.config["INDEXES_PRETTY_PAGE_URL"](lang) and path_list[-1] == index_file:
path_schema = site.config["INDEXES_PRETTY_PAGE_URL"](lang)
- if isinstance(path_schema, (bytes_str, unicode_str)):
+ if isinstance(path_schema, (bytes, str)):
path_schema = [path_schema]
else:
path_schema = None
@@ -1664,7 +1737,7 @@ def adjust_name_for_index_link(name, i, displayed_i, lang, site, force_addition=
def create_redirect(src, dst):
- """"Create a redirection."""
+ """Create a redirection."""
makedirs(os.path.dirname(src))
with io.open(src, "w+", encoding="utf8") as fd:
fd.write('<!DOCTYPE html>\n<head>\n<meta charset="utf-8">\n'
@@ -1674,139 +1747,6 @@ def create_redirect(src, dst):
'<a href="{0}">here</a>.</p>\n</body>'.format(dst))
-class TreeNode(object):
- """A tree node."""
-
- indent_levels = None # use for formatting comments as tree
- indent_change_before = 0 # use for formatting comments as tree
- indent_change_after = 0 # use for formatting comments as tree
-
- # The indent levels and changes allow to render a tree structure
- # without keeping track of all that information during rendering.
- #
- # The indent_change_before is the different between the current
- # comment's level and the previous comment's level; if the number
- # is positive, the current level is indented further in, and if it
- # is negative, it is indented further out. Positive values can be
- # used to open HTML tags for each opened level.
- #
- # The indent_change_after is the difference between the next
- # comment's level and the current comment's level. Negative values
- # can be used to close HTML tags for each closed level.
- #
- # The indent_levels list contains one entry (index, count) per
- # level, informing about the index of the current comment on that
- # level and the count of comments on that level (before a comment
- # of a higher level comes). This information can be used to render
- # tree indicators, for example to generate a tree such as:
- #
- # +--- [(0,3)]
- # +-+- [(1,3)]
- # | +--- [(1,3), (0,2)]
- # | +-+- [(1,3), (1,2)]
- # | +--- [(1,3), (1,2), (0, 1)]
- # +-+- [(2,3)]
- # +- [(2,3), (0,1)]
- #
- # (The lists used as labels represent the content of the
- # indent_levels property for that node.)
-
- def __init__(self, name, parent=None):
- """Initialize node."""
- self.name = name
- self.parent = parent
- self.children = []
-
- def get_path(self):
- """Get path."""
- path = []
- curr = self
- while curr is not None:
- path.append(curr)
- curr = curr.parent
- return reversed(path)
-
- def get_children(self):
- """Get children of a node."""
- return self.children
-
-
-def flatten_tree_structure(root_list):
- """Flatten a tree."""
- elements = []
-
- def generate(input_list, indent_levels_so_far):
- for index, element in enumerate(input_list):
- # add to destination
- elements.append(element)
- # compute and set indent levels
- indent_levels = indent_levels_so_far + [(index, len(input_list))]
- element.indent_levels = indent_levels
- # add children
- children = element.get_children()
- element.children_count = len(children)
- generate(children, indent_levels)
-
- generate(root_list, [])
- # Add indent change counters
- level = 0
- last_element = None
- for element in elements:
- new_level = len(element.indent_levels)
- # Compute level change before this element
- change = new_level - level
- if last_element is not None:
- last_element.indent_change_after = change
- element.indent_change_before = change
- # Update variables
- level = new_level
- last_element = element
- # Set level change after last element
- if last_element is not None:
- last_element.indent_change_after = -level
- return elements
-
-
-def parse_escaped_hierarchical_category_name(category_name):
- """Parse a category name."""
- result = []
- current = None
- index = 0
- next_backslash = category_name.find('\\', index)
- next_slash = category_name.find('/', index)
- while index < len(category_name):
- if next_backslash == -1 and next_slash == -1:
- current = (current if current else "") + category_name[index:]
- index = len(category_name)
- elif next_slash >= 0 and (next_backslash == -1 or next_backslash > next_slash):
- result.append((current if current else "") + category_name[index:next_slash])
- current = ''
- index = next_slash + 1
- next_slash = category_name.find('/', index)
- else:
- if len(category_name) == next_backslash + 1:
- raise Exception("Unexpected '\\' in '{0}' at last position!".format(category_name))
- esc_ch = category_name[next_backslash + 1]
- if esc_ch not in {'/', '\\'}:
- raise Exception("Unknown escape sequence '\\{0}' in '{1}'!".format(esc_ch, category_name))
- current = (current if current else "") + category_name[index:next_backslash] + esc_ch
- index = next_backslash + 2
- next_backslash = category_name.find('\\', index)
- if esc_ch == '/':
- next_slash = category_name.find('/', index)
- if current is not None:
- result.append(current)
- return result
-
-
-def join_hierarchical_category_path(category_path):
- """Join a category path."""
- def escape(s):
- return s.replace('\\', '\\\\').replace('/', '\\/')
-
- return '/'.join([escape(p) for p in category_path])
-
-
def colorize_str_from_base_color(string, base_color):
"""Find a perceptual similar color from a base color based on the hash of a string.
@@ -1815,14 +1755,7 @@ def colorize_str_from_base_color(string, base_color):
lightness and saturation untouched using HUSL colorspace.
"""
def hash_str(string, pos):
- x = hashlib.md5(string.encode('utf-8')).digest()[pos]
- try:
- # Python 2: a string
- # TODO: remove in v8
- return ord(x)
- except TypeError:
- # Python 3: already an integer
- return x
+ return hashlib.md5(string.encode('utf-8')).digest()[pos]
def degreediff(dega, degb):
return min(abs(dega - degb), abs((degb - dega) + 360))
@@ -1840,6 +1773,13 @@ def colorize_str_from_base_color(string, base_color):
return husl.husl_to_hex(h, s, l)
+def colorize_str(string: str, base_color: str, presets: dict):
+ """Colorize a string by using a presets dict or generate one based on base_color."""
+ if string in presets:
+ return presets[string]
+ return colorize_str_from_base_color(string, base_color)
+
+
def color_hsl_adjust_hex(hexstr, adjust_h=None, adjust_s=None, adjust_l=None):
"""Adjust a hex color using HSL arguments, adjustments in percentages 1.0 to -1.0. Returns a hex color."""
h, s, l = husl.hex_to_husl(hexstr)
@@ -1901,6 +1841,64 @@ def clean_before_deployment(site):
return undeployed_posts
+def sort_posts(posts, *keys):
+ """Sort posts by a given predicate. Helper function for templates.
+
+ If a key starts with '-', it is sorted in descending order.
+
+ Usage examples::
+
+ sort_posts(timeline, 'title', 'date')
+ sort_posts(timeline, 'author', '-section_name')
+ """
+ # We reverse the keys to get the usual ordering method: the first key
+ # provided is the most important sorting predicate (first by 'title', then
+ # by 'date' in the first example)
+ for key in reversed(keys):
+ if key.startswith('-'):
+ key = key[1:]
+ reverse = True
+ else:
+ reverse = False
+ try:
+ # An attribute (or method) of the Post object
+ a = getattr(posts[0], key)
+ if callable(a):
+ keyfunc = operator.methodcaller(key)
+ else:
+ keyfunc = operator.attrgetter(key)
+ except AttributeError:
+ # Post metadata
+ keyfunc = operator.methodcaller('meta', key)
+
+ posts = sorted(posts, reverse=reverse, key=keyfunc)
+ return posts
+
+
+def smartjoin(join_char: str, string_or_iterable) -> str:
+ """Join string_or_iterable with join_char if it is iterable; otherwise converts it to string.
+
+ >>> smartjoin('; ', 'foo, bar')
+ 'foo, bar'
+ >>> smartjoin('; ', ['foo', 'bar'])
+ 'foo; bar'
+ >>> smartjoin(' to ', ['count', 42])
+ 'count to 42'
+ """
+ if isinstance(string_or_iterable, (str, bytes)):
+ return string_or_iterable
+ elif isinstance(string_or_iterable, Iterable):
+ return join_char.join([str(e) for e in string_or_iterable])
+ else:
+ return str(string_or_iterable)
+
+
+def _smartjoin_filter(string_or_iterable, join_char: str) -> str:
+ """Join stuff smartly, with reversed arguments for Jinja2 filters."""
+ # http://jinja.pocoo.org/docs/2.10/api/#custom-filters
+ return smartjoin(join_char, string_or_iterable)
+
+
# Stolen from textwrap in Python 3.4.3.
def indent(text, prefix, predicate=None):
"""Add 'prefix' to the beginning of selected lines in 'text'.
@@ -1924,11 +1922,13 @@ def load_data(path):
"""Given path to a file, load data from it."""
ext = os.path.splitext(path)[-1]
loader = None
+ function = 'load'
if ext in {'.yml', '.yaml'}:
- loader = yaml
- if yaml is None:
- req_missing(['yaml'], 'use YAML data files')
+ if YAML is None:
+ req_missing(['ruamel.yaml'], 'use YAML data files')
return {}
+ loader = YAML(typ='safe')
+ function = 'load'
elif ext in {'.json', '.js'}:
loader = json
elif ext in {'.toml', '.tml'}:
@@ -1938,5 +1938,141 @@ def load_data(path):
loader = toml
if loader is None:
return
- with io.open(path, 'r', encoding='utf8') as inf:
- return loader.load(inf)
+ with io.open(path, 'r', encoding='utf-8-sig') as inf:
+ return getattr(loader, function)(inf)
+
+
+def rss_writer(rss_obj, output_path):
+ """Write an RSS object to an xml file."""
+ dst_dir = os.path.dirname(output_path)
+ makedirs(dst_dir)
+ with io.open(output_path, "w+", encoding="utf-8") as rss_file:
+ data = rss_obj.to_xml(encoding='utf-8')
+ if isinstance(data, bytes):
+ data = data.decode('utf-8')
+ rss_file.write(data)
+
+
+def map_metadata(meta, key, config):
+ """Map metadata from other platforms to Nikola names.
+
+ This uses the METADATA_MAPPING and METADATA_VALUE_MAPPING settings (via ``config``) and modifies the dict in place.
+ """
+ for foreign, ours in config.get('METADATA_MAPPING', {}).get(key, {}).items():
+ if foreign in meta:
+ meta[ours] = meta[foreign]
+
+ for meta_key, hook in config.get('METADATA_VALUE_MAPPING', {}).get(key, {}).items():
+ if meta_key in meta:
+ meta[meta_key] = hook(meta[meta_key])
+
+
+class ClassificationTranslationManager(object):
+ """Keeps track of which classifications could be translated as which others.
+
+ The internal structure is as follows:
+ - per language, you have a map of classifications to maps
+ - the inner map is a map from other languages to sets of classifications
+ which are considered as translations
+ """
+
+ def __init__(self):
+ self._data = defaultdict(dict)
+
+ def add_translation(self, translation_map):
+ """Add translation of one classification.
+
+ ``translation_map`` must be a dictionary mapping languages to their
+ translations of the added classification.
+ """
+ for lang, classification in translation_map.items():
+ clmap = self._data[lang]
+ cldata = clmap.get(classification)
+ if cldata is None:
+ cldata = defaultdict(set)
+ clmap[classification] = cldata
+ for other_lang, other_classification in translation_map.items():
+ if other_lang != lang:
+ cldata[other_lang].add(other_classification)
+
+ def get_translations(self, classification, lang):
+ """Get a dict mapping other languages to (unsorted) lists of translated classifications."""
+ clmap = self._data[lang]
+ cldata = clmap.get(classification)
+ if cldata is None:
+ return {}
+ else:
+ return {other_lang: list(classifications) for other_lang, classifications in cldata.items()}
+
+ def get_translations_as_list(self, classification, lang, classifications_per_language):
+ """Get a list of pairs ``(other_lang, other_classification)`` which are translations of ``classification``.
+
+ Avoid classifications not in ``classifications_per_language``.
+ """
+ clmap = self._data[lang]
+ cldata = clmap.get(classification)
+ if cldata is None:
+ return []
+ else:
+ result = []
+ for other_lang, classifications in cldata.items():
+ for other_classification in classifications:
+ if other_classification in classifications_per_language[other_lang]:
+ result.append((other_lang, other_classification))
+ return result
+
+ def has_translations(self, classification, lang):
+ """Return whether we know about the classification in that language.
+
+ Note that this function returning ``True`` does not mean that
+ ``get_translations`` returns a non-empty dict or that
+ ``get_translations_as_list`` returns a non-empty list, but only
+ that this classification was explicitly added with
+ ``add_translation`` at some point.
+ """
+ return self._data[lang].get(classification) is not None
+
+ def add_defaults(self, posts_per_classification_per_language):
+ """Treat every classification as its own literal translation into every other language.
+
+ ``posts_per_classification_per_language`` should be the first argument
+ to ``Taxonomy.postprocess_posts_per_classification``.
+ """
+ # First collect all classifications from all languages
+ all_classifications = set()
+ for _, classifications in posts_per_classification_per_language.items():
+ all_classifications.update(classifications.keys())
+ # Next, add translation records for all of them
+ for classification in all_classifications:
+ record = {tlang: classification for tlang in posts_per_classification_per_language}
+ self.add_translation(record)
+
+ def read_from_config(self, site, basename, posts_per_classification_per_language, add_defaults_default):
+ """Read translations from config.
+
+ ``site`` should be the Nikola site object. Will consider
+ the variables ``<basename>_TRANSLATIONS`` and
+ ``<basename>_TRANSLATIONS_ADD_DEFAULTS``.
+
+ ``posts_per_classification_per_language`` should be the first argument
+ to ``Taxonomy.postprocess_posts_per_classification``, i.e. this function
+ should be called from that function. ``add_defaults_default`` specifies
+ what the default value for ``<basename>_TRANSLATIONS_ADD_DEFAULTS`` is.
+
+ Also sends signal via blinker to allow interested plugins to add
+ translations by themselves. The signal name used is
+ ``<lower(basename)>_translations_config``, and the argument is a dict
+ with entries ``translation_manager``, ``site`` and
+ ``posts_per_classification_per_language``.
+ """
+ # Add translations
+ for record in site.config.get('{}_TRANSLATIONS'.format(basename), []):
+ self.add_translation(record)
+ # Add default translations
+ if site.config.get('{}_TRANSLATIONS_ADD_DEFAULTS'.format(basename), add_defaults_default):
+ self.add_defaults(posts_per_classification_per_language)
+ # Use blinker to inform interested parties (plugins) that they can add
+ # translations themselves
+ args = {'translation_manager': self, 'site': site,
+ 'posts_per_classification_per_language': posts_per_classification_per_language}
+ signal('{}_translations_config'.format(basename.lower())).send(args)
diff --git a/nikola/winutils.py b/nikola/winutils.py
index 6e341b8..a6506e6 100644
--- a/nikola/winutils.py
+++ b/nikola/winutils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright © 2012-2016 Roberto Alsina and others.
+# Copyright © 2012-2020 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,7 +26,6 @@
"""windows utilities to workaround problems with symlinks in a git clone."""
-from __future__ import print_function, unicode_literals
import os
import shutil
import io
@@ -67,11 +66,11 @@ def fix_all_git_symlinked(topdir):
"""
# Determine whether or not symlinks need fixing (they don’t if installing
# from a .tar.gz file)
- with io.open(topdir + r'\nikola\data\symlink-test-link.txt', 'r', encoding='utf-8') as f:
+ with io.open(topdir + r'\nikola\data\symlink-test-link.txt', 'r', encoding='utf-8-sig') as f:
text = f.read()
if text.startswith("NIKOLA_SYMLINKS=OK"):
return -1
- with io.open(topdir + r'\nikola\data\symlinked.txt', 'r', encoding='utf-8') as f:
+ with io.open(topdir + r'\nikola\data\symlinked.txt', 'r', encoding='utf-8-sig') as f:
text = f.read()
# expect each line a relpath from git or zip root,
# smoke test relpaths are relative to git root
@@ -93,7 +92,7 @@ def fix_all_git_symlinked(topdir):
continue
# build src path and do some basic validation
- with io.open(os.path.join(topdir, dst), 'r', encoding='utf-8') as f:
+ with io.open(os.path.join(topdir, dst), 'r', encoding='utf-8-sig') as f:
text = f.read()
dst_dir = os.path.dirname(dst)
try: