aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
commitca94afc07df55cb7fc6fe3b4f3011877b7881195 (patch)
treed81e1f275aa77545f33740723f307a83dde2e0b4
parentf794eee787e9cde54e6b8f53e45d69c9ddc9936a (diff)
downloadnikola-ca94afc07df55cb7fc6fe3b4f3011877b7881195.tar.bz2
nikola-ca94afc07df55cb7fc6fe3b4f3011877b7881195.tar.xz
nikola-ca94afc07df55cb7fc6fe3b4f3011877b7881195.tar.zst
Imported Upstream version 6.2.1upstream/6.2.1
-rw-r--r--.coveragerc14
-rw-r--r--.gitignore14
-rw-r--r--.travis.yml25
-rw-r--r--.tx/config1
-rw-r--r--AUTHORS.txt67
-rw-r--r--CHANGES.txt358
-rw-r--r--CONTRIBUTING.rst61
-rw-r--r--LICENSE.txt2
-rw-r--r--MANIFEST.in6
-rw-r--r--README.md31
-rw-r--r--README.rst68
-rw-r--r--docs/creating-a-site.txt42
-rw-r--r--docs/creating-a-theme.txt257
-rw-r--r--docs/extending.txt269
-rw-r--r--docs/internals.txt37
-rw-r--r--docs/man/nikola.1148
-rw-r--r--docs/manual.txt719
-rw-r--r--docs/social_buttons.txt202
-rw-r--r--docs/sphinx/Makefile177
-rw-r--r--docs/sphinx/conf.py263
l---------docs/sphinx/creating-a-site.txt1
l---------docs/sphinx/creating-a-theme.txt1
l---------docs/sphinx/extending.txt1
-rw-r--r--docs/sphinx/index.txt27
l---------docs/sphinx/internals.txt1
-rw-r--r--docs/sphinx/make.bat242
l---------docs/sphinx/manual.txt1
l---------docs/sphinx/social_buttons.txt1
l---------docs/sphinx/theming.txt1
l---------docs/sphinx/upgrading-to-v6.txt1
-rw-r--r--docs/theming.txt204
-rw-r--r--docs/upgrading-to-v6.txt81
-rw-r--r--nikola/__init__.py4
-rw-r--r--nikola/conf.py.in344
-rw-r--r--nikola/data/samplesite/README.txt2
-rw-r--r--nikola/data/samplesite/posts/1.meta5
-rw-r--r--nikola/data/samplesite/posts/1.rst (renamed from nikola/data/samplesite/posts/1.txt)10
-rw-r--r--nikola/data/samplesite/stories/1.meta4
-rw-r--r--nikola/data/samplesite/stories/1.rst11
-rw-r--r--nikola/data/samplesite/stories/1.txt4
-rw-r--r--nikola/data/samplesite/stories/a-study-in-scarlet.txt5139
-rw-r--r--nikola/data/samplesite/stories/bootstrap-demo.rst1047
-rw-r--r--nikola/data/samplesite/stories/charts.txt59
-rw-r--r--nikola/data/samplesite/stories/configsample.meta4
-rw-r--r--nikola/data/samplesite/stories/configsample.txt221
-rw-r--r--nikola/data/samplesite/stories/creating-a-theme.meta3
l---------nikola/data/samplesite/stories/creating-a-theme.rst (renamed from nikola/data/samplesite/stories/creating-a-theme.txt)0
l---------nikola/data/samplesite/stories/extending.txt1
l---------nikola/data/samplesite/stories/internals.txt1
-rw-r--r--nikola/data/samplesite/stories/listings-demo.rst (renamed from nikola/data/samplesite/stories/listings-demo.txt)0
-rw-r--r--nikola/data/samplesite/stories/manual.meta4
l---------nikola/data/samplesite/stories/manual.rst (renamed from nikola/data/samplesite/stories/manual.txt)0
-rw-r--r--nikola/data/samplesite/stories/quickref.meta4
-rw-r--r--nikola/data/samplesite/stories/quickref.rst (renamed from nikola/data/samplesite/stories/quickref.txt)7
-rw-r--r--nikola/data/samplesite/stories/quickstart.meta4
-rw-r--r--nikola/data/samplesite/stories/quickstart.rst (renamed from nikola/data/samplesite/stories/quickstart.txt)7
-rw-r--r--nikola/data/samplesite/stories/slides-demo.rst (renamed from nikola/data/samplesite/stories/slides-demo.txt)0
l---------nikola/data/samplesite/stories/social_buttons.txt1
-rw-r--r--nikola/data/samplesite/stories/theming.meta3
l---------nikola/data/samplesite/stories/theming.rst (renamed from nikola/data/samplesite/stories/theming.txt)0
l---------nikola/data/samplesite/stories/upgrading-to-v6.txt1
-rw-r--r--nikola/data/themes/base/README.md4
-rw-r--r--nikola/data/themes/base/assets/css/rst.css (renamed from nikola/data/themes/monospace/assets/css/rst.css)4
-rw-r--r--nikola/data/themes/base/assets/css/theme.css (renamed from nikola/data/themes/orphan/assets/css/theme.css)0
-rw-r--r--nikola/data/themes/base/assets/js/mathjax.js (renamed from nikola/data/themes/default/assets/js/mathjax.js)0
-rw-r--r--nikola/data/themes/base/bundles (renamed from nikola/data/themes/monospace/bundles)0
-rw-r--r--nikola/data/themes/base/engine (renamed from nikola/data/themes/default/engine)0
-rw-r--r--nikola/data/themes/base/messages/messages_bg.py25
-rw-r--r--nikola/data/themes/base/messages/messages_ca.py (renamed from nikola/data/themes/default/messages/messages_ca.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_de.py (renamed from nikola/data/themes/default/messages/messages_de.py)4
-rw-r--r--nikola/data/themes/base/messages/messages_el.py (renamed from nikola/data/themes/default/messages/messages_el.py)4
-rw-r--r--nikola/data/themes/base/messages/messages_en.py (renamed from nikola/data/themes/default/messages/messages_en.py)4
-rw-r--r--nikola/data/themes/base/messages/messages_eo.py25
-rw-r--r--nikola/data/themes/base/messages/messages_es.py (renamed from nikola/data/themes/default/messages/messages_es.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_fa.py25
-rw-r--r--nikola/data/themes/base/messages/messages_fi.py25
-rw-r--r--nikola/data/themes/base/messages/messages_fr.py (renamed from nikola/data/themes/default/messages/messages_fr.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_hr.py25
-rw-r--r--nikola/data/themes/base/messages/messages_it.py (renamed from nikola/data/themes/default/messages/messages_it.py)10
-rw-r--r--nikola/data/themes/base/messages/messages_ja.py (renamed from nikola/data/themes/default/messages/messages_ja.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_nl.py25
-rw-r--r--nikola/data/themes/base/messages/messages_pl.py (renamed from nikola/data/themes/default/messages/messages_pl.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_pt_br.py (renamed from nikola/data/themes/default/messages/messages_pt_br.py)2
-rw-r--r--nikola/data/themes/base/messages/messages_ru.py (renamed from nikola/data/themes/default/messages/messages_ru.py)14
-rw-r--r--nikola/data/themes/base/messages/messages_sl.py25
l---------nikola/data/themes/base/messages/messages_sl_si.py1
-rw-r--r--nikola/data/themes/base/messages/messages_tr_tr.py25
-rw-r--r--nikola/data/themes/base/messages/messages_zh_cn.py (renamed from nikola/data/themes/default/messages/messages_zh_cn.py)2
-rw-r--r--nikola/data/themes/base/templates/annotation_helper.tmpl16
-rw-r--r--nikola/data/themes/base/templates/base.tmpl40
-rw-r--r--nikola/data/themes/base/templates/base_helper.tmpl90
-rw-r--r--nikola/data/themes/base/templates/comments_helper.tmpl49
-rw-r--r--nikola/data/themes/base/templates/crumbs.tmpl9
-rw-r--r--nikola/data/themes/base/templates/disqus_helper.tmpl (renamed from nikola/data/themes/monospace/templates/disqus_helper.tmpl)33
-rw-r--r--nikola/data/themes/base/templates/facebook_helper.tmpl62
-rw-r--r--nikola/data/themes/base/templates/gallery.tmpl28
-rw-r--r--nikola/data/themes/base/templates/googleplus_helper.tmpl17
-rw-r--r--nikola/data/themes/base/templates/index.tmpl29
-rw-r--r--nikola/data/themes/base/templates/index_helper.tmpl (renamed from nikola/data/themes/default/templates/index_helper.tmpl)8
-rw-r--r--nikola/data/themes/base/templates/intensedebate_helper.tmpl25
-rw-r--r--nikola/data/themes/base/templates/list.tmpl (renamed from nikola/data/themes/default/templates/list.tmpl)0
-rw-r--r--nikola/data/themes/base/templates/list_post.tmpl (renamed from nikola/data/themes/default/templates/list_post.tmpl)0
-rw-r--r--nikola/data/themes/base/templates/listing.tmpl (renamed from nikola/data/themes/default/templates/listing.tmpl)7
-rw-r--r--nikola/data/themes/base/templates/livefyre_helper.tmpl37
-rw-r--r--nikola/data/themes/base/templates/moot_helper.tmpl13
-rw-r--r--nikola/data/themes/base/templates/mustache-comment-form.tmpl5
-rw-r--r--nikola/data/themes/base/templates/post.tmpl41
-rw-r--r--nikola/data/themes/base/templates/post_helper.tmpl (renamed from nikola/data/themes/orphan/templates/post_helper.tmpl)12
-rw-r--r--nikola/data/themes/base/templates/post_list_directive.tmpl14
-rw-r--r--nikola/data/themes/base/templates/slides.tmpl22
-rw-r--r--nikola/data/themes/base/templates/story.tmpl (renamed from nikola/data/themes/orphan/templates/story.tmpl)5
-rw-r--r--nikola/data/themes/base/templates/tag.tmpl (renamed from nikola/data/themes/default/templates/tag.tmpl)8
-rw-r--r--nikola/data/themes/base/templates/tags.tmpl25
-rw-r--r--nikola/data/themes/bootstrap/README.md (renamed from nikola/data/themes/site/README)17
-rw-r--r--nikola/data/themes/bootstrap/assets/css/colorbox.css (renamed from nikola/data/themes/default/assets/css/colorbox.css)154
-rw-r--r--nikola/data/themes/bootstrap/assets/css/images/border.png (renamed from nikola/data/themes/default/assets/css/images/border.png)bin112 -> 112 bytes
-rw-r--r--nikola/data/themes/bootstrap/assets/css/images/controls.png (renamed from nikola/data/themes/default/assets/css/images/controls.png)bin2893 -> 2893 bytes
-rw-r--r--nikola/data/themes/bootstrap/assets/css/images/loading.gif (renamed from nikola/data/themes/default/assets/css/images/loading.gif)bin9427 -> 9427 bytes
-rw-r--r--nikola/data/themes/bootstrap/assets/css/images/loading_background.png (renamed from nikola/data/themes/default/assets/css/images/loading_background.png)bin157 -> 157 bytes
-rw-r--r--nikola/data/themes/bootstrap/assets/css/images/overlay.png (renamed from nikola/data/themes/default/assets/css/images/overlay.png)bin182 -> 182 bytes
-rw-r--r--nikola/data/themes/bootstrap/assets/css/theme.css (renamed from nikola/data/themes/site/assets/css/theme.css)13
-rw-r--r--nikola/data/themes/bootstrap/assets/js/flowr.plugin.js265
-rw-r--r--nikola/data/themes/bootstrap/bundles (renamed from nikola/data/themes/default/bundles)2
-rw-r--r--nikola/data/themes/bootstrap/engine (renamed from nikola/data/themes/site-planetoid/engine)0
-rw-r--r--nikola/data/themes/bootstrap/parent1
-rw-r--r--nikola/data/themes/bootstrap/templates/base.tmpl (renamed from nikola/data/themes/site/templates/base.tmpl)48
-rw-r--r--nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl (renamed from nikola/data/themes/default/templates/base_helper.tmpl)66
-rw-r--r--nikola/data/themes/bootstrap/templates/gallery.tmpl86
-rw-r--r--nikola/data/themes/bootstrap/templates/slides.tmpl22
-rw-r--r--nikola/data/themes/default/assets/css/bootstrap-responsive.css1109
-rw-r--r--nikola/data/themes/default/assets/css/bootstrap.css6158
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.pngbin215 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.pngbin217 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.pngbin108 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.pngbin111 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.pngbin216 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/images/ie6/borderTopRight.pngbin214 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/css/rst.css312
-rw-r--r--nikola/data/themes/default/assets/css/theme.css73
-rw-r--r--nikola/data/themes/default/assets/img/glyphicons-halflings-white.pngbin8777 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/img/glyphicons-halflings.pngbin12799 -> 0 bytes
-rw-r--r--nikola/data/themes/default/assets/js/bootstrap.js2268
-rw-r--r--nikola/data/themes/default/messages/messages_gr.py22
-rw-r--r--nikola/data/themes/default/templates/base.tmpl60
-rw-r--r--nikola/data/themes/default/templates/disqus_helper.tmpl43
-rw-r--r--nikola/data/themes/default/templates/gallery.tmpl31
-rw-r--r--nikola/data/themes/default/templates/index.tmpl22
-rw-r--r--nikola/data/themes/default/templates/post.tmpl33
-rw-r--r--nikola/data/themes/default/templates/post_helper.tmpl73
-rw-r--r--nikola/data/themes/default/templates/story.tmpl16
-rw-r--r--nikola/data/themes/default/templates/tags.tmpl12
-rw-r--r--nikola/data/themes/jinja-default/README3
-rw-r--r--nikola/data/themes/jinja-default/engine1
-rw-r--r--nikola/data/themes/jinja-default/parent1
-rw-r--r--nikola/data/themes/jinja-default/templates/base.tmpl131
-rw-r--r--nikola/data/themes/jinja-default/templates/gallery.tmpl47
-rw-r--r--nikola/data/themes/jinja-default/templates/index.tmpl34
-rw-r--r--nikola/data/themes/jinja-default/templates/list.tmpl13
-rw-r--r--nikola/data/themes/jinja-default/templates/list_post.tmpl13
-rw-r--r--nikola/data/themes/jinja-default/templates/listing.tmpl9
-rw-r--r--nikola/data/themes/jinja-default/templates/post.tmpl61
-rw-r--r--nikola/data/themes/jinja-default/templates/story.tmpl25
-rw-r--r--nikola/data/themes/jinja-default/templates/tag.tmpl6
-rw-r--r--nikola/data/themes/jinja-default/templates/tags.tmpl13
-rw-r--r--nikola/data/themes/monospace/assets/css/theme.css14
-rw-r--r--nikola/data/themes/monospace/templates/base.tmpl45
-rw-r--r--nikola/data/themes/monospace/templates/base_helper.tmpl82
-rw-r--r--nikola/data/themes/monospace/templates/gallery.tmpl31
-rw-r--r--nikola/data/themes/monospace/templates/index.tmpl30
-rw-r--r--nikola/data/themes/monospace/templates/index_helper.tmpl17
-rw-r--r--nikola/data/themes/monospace/templates/list.tmpl14
-rw-r--r--nikola/data/themes/monospace/templates/list_post.tmpl14
-rw-r--r--nikola/data/themes/monospace/templates/listing.tmpl10
-rw-r--r--nikola/data/themes/monospace/templates/post.tmpl34
-rw-r--r--nikola/data/themes/monospace/templates/post_helper.tmpl73
-rw-r--r--nikola/data/themes/monospace/templates/story.tmpl15
-rw-r--r--nikola/data/themes/monospace/templates/tag.tmpl7
-rw-r--r--nikola/data/themes/monospace/templates/tags.tmpl14
l---------nikola/data/themes/orphan/assets/css/rst.css1
-rw-r--r--nikola/data/themes/orphan/bundles1
-rw-r--r--nikola/data/themes/orphan/templates/base.tmpl37
-rw-r--r--nikola/data/themes/orphan/templates/base_helper.tmpl82
-rw-r--r--nikola/data/themes/orphan/templates/disqus_helper.tmpl43
-rw-r--r--nikola/data/themes/orphan/templates/gallery.tmpl31
-rw-r--r--nikola/data/themes/orphan/templates/index.tmpl21
-rw-r--r--nikola/data/themes/orphan/templates/index_helper.tmpl17
-rw-r--r--nikola/data/themes/orphan/templates/list.tmpl14
-rw-r--r--nikola/data/themes/orphan/templates/list_post.tmpl14
-rw-r--r--nikola/data/themes/orphan/templates/listing.tmpl10
-rw-r--r--nikola/data/themes/orphan/templates/post.tmpl25
-rw-r--r--nikola/data/themes/orphan/templates/tag.tmpl7
-rw-r--r--nikola/data/themes/orphan/templates/tags.tmpl14
-rw-r--r--nikola/data/themes/site-planetoid/README1
-rw-r--r--nikola/data/themes/site-planetoid/parent1
-rw-r--r--nikola/data/themes/site-planetoid/templates/index.tmpl16
-rw-r--r--nikola/data/themes/site-planetoid/templates/post.tmpl9
-rw-r--r--nikola/data/themes/site-planetoid/templates/story.tmpl25
-rw-r--r--nikola/data/themes/site/engine1
-rw-r--r--nikola/data/themes/site/parent1
-rw-r--r--nikola/filters.py83
-rw-r--r--nikola/main.py126
-rw-r--r--nikola/nikola.py917
-rw-r--r--nikola/plugin_categories.py146
-rw-r--r--nikola/plugins/__init__.py3
-rw-r--r--nikola/plugins/basic_import.py166
-rw-r--r--nikola/plugins/command/__init__.py25
-rw-r--r--nikola/plugins/command/auto.plugin9
-rw-r--r--nikola/plugins/command/auto.py103
-rw-r--r--nikola/plugins/command/bootswatch_theme.plugin (renamed from nikola/plugins/command_bootswatch_theme.plugin)4
-rw-r--r--nikola/plugins/command/bootswatch_theme.py (renamed from nikola/plugins/command_bootswatch_theme.py)49
-rw-r--r--nikola/plugins/command/check.plugin (renamed from nikola/plugins/command_check.plugin)4
-rw-r--r--nikola/plugins/command/check.py204
-rw-r--r--nikola/plugins/command/console.plugin (renamed from nikola/plugins/command_console.plugin)4
-rw-r--r--nikola/plugins/command/console.py (renamed from nikola/plugins/command_console.py)27
-rw-r--r--nikola/plugins/command/deploy.plugin (renamed from nikola/plugins/command_deploy.plugin)4
-rw-r--r--nikola/plugins/command/deploy.py141
-rw-r--r--nikola/plugins/command/import_blogger.plugin (renamed from nikola/plugins/command_import_blogger.plugin)4
-rw-r--r--nikola/plugins/command/import_blogger.py (renamed from nikola/plugins/command_import_blogger.py)159
-rw-r--r--nikola/plugins/command/import_feed.plugin10
-rw-r--r--nikola/plugins/command/import_feed.py197
-rw-r--r--nikola/plugins/command/import_wordpress.plugin (renamed from nikola/plugins/command_import_wordpress.plugin)4
-rw-r--r--nikola/plugins/command/import_wordpress.py (renamed from nikola/plugins/command_import_wordpress.py)256
-rw-r--r--nikola/plugins/command/init.plugin (renamed from nikola/plugins/command_init.plugin)4
-rw-r--r--nikola/plugins/command/init.py (renamed from nikola/plugins/command_init.py)45
-rw-r--r--nikola/plugins/command/install_plugin.plugin10
-rw-r--r--nikola/plugins/command/install_plugin.py185
-rw-r--r--nikola/plugins/command/install_theme.plugin (renamed from nikola/plugins/command_install_theme.plugin)4
-rw-r--r--nikola/plugins/command/install_theme.py163
-rw-r--r--nikola/plugins/command/mincss.plugin10
-rw-r--r--nikola/plugins/command/mincss.py75
-rw-r--r--nikola/plugins/command/new_post.plugin (renamed from nikola/plugins/command_new_post.plugin)4
-rw-r--r--nikola/plugins/command/new_post.py (renamed from nikola/plugins/command_new_post.py)108
-rw-r--r--nikola/plugins/command/planetoid.plugin (renamed from nikola/plugins/command_planetoid.plugin)4
-rw-r--r--nikola/plugins/command/planetoid/__init__.py (renamed from nikola/plugins/command_planetoid/__init__.py)30
-rw-r--r--nikola/plugins/command/serve.plugin (renamed from nikola/plugins/command_serve.plugin)4
-rw-r--r--nikola/plugins/command/serve.py153
-rw-r--r--nikola/plugins/command/version.plugin9
-rw-r--r--nikola/plugins/command/version.py (renamed from nikola/plugins/compile_rest/dummy.py)28
-rw-r--r--nikola/plugins/command_check.py166
-rw-r--r--nikola/plugins/command_deploy.py65
-rw-r--r--nikola/plugins/command_install_theme.py105
-rw-r--r--nikola/plugins/command_serve.py79
-rw-r--r--nikola/plugins/compile/__init__.py0
-rw-r--r--nikola/plugins/compile/asciidoc.plugin10
-rw-r--r--nikola/plugins/compile/asciidoc.py65
-rw-r--r--nikola/plugins/compile/bbcode.plugin (renamed from nikola/plugins/compile_bbcode.plugin)4
-rw-r--r--nikola/plugins/compile/bbcode.py (renamed from nikola/plugins/compile_bbcode.py)24
-rw-r--r--nikola/plugins/compile/html.plugin (renamed from nikola/plugins/compile_html.plugin)4
-rw-r--r--nikola/plugins/compile/html.py (renamed from nikola/plugins/compile_html.py)21
-rw-r--r--nikola/plugins/compile/ipynb.plugin (renamed from nikola/plugins/compile_ipynb.plugin)4
-rw-r--r--nikola/plugins/compile/ipynb/README.txt44
-rw-r--r--nikola/plugins/compile/ipynb/__init__.py (renamed from nikola/plugins/compile_ipynb/__init__.py)46
-rw-r--r--nikola/plugins/compile/markdown.plugin (renamed from nikola/plugins/compile_markdown.plugin)4
-rw-r--r--nikola/plugins/compile/markdown/__init__.py (renamed from nikola/plugins/compile_markdown/__init__.py)34
-rw-r--r--nikola/plugins/compile/markdown/mdx_gist.py (renamed from nikola/plugins/compile_markdown/mdx_gist.py)122
-rw-r--r--nikola/plugins/compile/markdown/mdx_nikola.py (renamed from nikola/plugins/compile_markdown/mdx_nikola.py)4
-rw-r--r--nikola/plugins/compile/markdown/mdx_podcast.py (renamed from nikola/plugins/compile_markdown/mdx_podcast.py)0
-rw-r--r--nikola/plugins/compile/misaka.plugin (renamed from nikola/plugins/compile_misaka.plugin)2
-rw-r--r--nikola/plugins/compile/misaka.py (renamed from nikola/plugins/compile_misaka/__init__.py)29
-rw-r--r--nikola/plugins/compile/pandoc.plugin10
-rw-r--r--nikola/plugins/compile/pandoc.py65
-rw-r--r--nikola/plugins/compile/php.plugin10
-rw-r--r--nikola/plugins/compile/php.py62
-rw-r--r--nikola/plugins/compile/rest.plugin (renamed from nikola/plugins/compile_rest.plugin)4
-rw-r--r--nikola/plugins/compile/rest/__init__.py200
-rw-r--r--nikola/plugins/compile/rest/chart.plugin10
-rw-r--r--nikola/plugins/compile/rest/chart.py150
-rw-r--r--nikola/plugins/compile/rest/doc.plugin10
-rw-r--r--nikola/plugins/compile/rest/doc.py88
-rw-r--r--nikola/plugins/compile/rest/gist.plugin10
-rw-r--r--nikola/plugins/compile/rest/gist.py (renamed from nikola/plugins/compile_rest/gist_directive.py)48
-rw-r--r--nikola/plugins/compile/rest/listing.plugin10
-rw-r--r--nikola/plugins/compile/rest/listing.py (renamed from nikola/plugins/compile_rest/listing.py)230
-rw-r--r--nikola/plugins/compile/rest/media.plugin10
-rw-r--r--nikola/plugins/compile/rest/media.py63
-rw-r--r--nikola/plugins/compile/rest/post_list.plugin9
-rw-r--r--nikola/plugins/compile/rest/post_list.py165
-rw-r--r--nikola/plugins/compile/rest/slides.plugin10
-rw-r--r--nikola/plugins/compile/rest/slides.py (renamed from nikola/plugins/compile_rest/slides.py)54
-rw-r--r--nikola/plugins/compile/rest/soundcloud.plugin10
-rw-r--r--nikola/plugins/compile/rest/soundcloud.py (renamed from nikola/plugins/compile_rest/soundcloud.py)18
-rw-r--r--nikola/plugins/compile/rest/vimeo.plugin7
-rw-r--r--nikola/plugins/compile/rest/vimeo.py (renamed from nikola/plugins/compile_rest/vimeo.py)43
-rw-r--r--nikola/plugins/compile/rest/youtube.plugin8
-rw-r--r--nikola/plugins/compile/rest/youtube.py (renamed from nikola/plugins/compile_rest/youtube.py)20
-rw-r--r--nikola/plugins/compile/textile.plugin (renamed from nikola/plugins/compile_textile.plugin)4
-rw-r--r--nikola/plugins/compile/textile.py (renamed from nikola/plugins/compile_textile.py)22
-rw-r--r--nikola/plugins/compile/txt2tags.plugin (renamed from nikola/plugins/compile_txt2tags.plugin)4
-rw-r--r--nikola/plugins/compile/txt2tags.py (renamed from nikola/plugins/compile_txt2tags.py)19
-rw-r--r--nikola/plugins/compile/wiki.plugin (renamed from nikola/plugins/compile_wiki.plugin)4
-rw-r--r--nikola/plugins/compile/wiki.py (renamed from nikola/plugins/compile_wiki.py)25
-rw-r--r--nikola/plugins/compile_ipynb/README.txt35
-rw-r--r--nikola/plugins/compile_rest/__init__.py138
-rw-r--r--nikola/plugins/loghandler/smtp.plugin9
-rw-r--r--nikola/plugins/loghandler/smtp.py54
-rw-r--r--nikola/plugins/loghandler/stderr.plugin9
-rw-r--r--nikola/plugins/loghandler/stderr.py50
-rw-r--r--nikola/plugins/task/__init__.py0
-rw-r--r--nikola/plugins/task/archive.plugin (renamed from nikola/plugins/task_archive.plugin)4
-rw-r--r--nikola/plugins/task/archive.py (renamed from nikola/plugins/task_archive.py)104
-rw-r--r--nikola/plugins/task/build_less.plugin10
-rw-r--r--nikola/plugins/task/build_less.py99
-rw-r--r--nikola/plugins/task/build_sass.plugin9
-rw-r--r--nikola/plugins/task/build_sass.py117
-rw-r--r--nikola/plugins/task/bundles.plugin (renamed from nikola/plugins/task_create_bundles.plugin)4
-rw-r--r--nikola/plugins/task/bundles.py (renamed from nikola/plugins/task_create_bundles.py)46
-rw-r--r--nikola/plugins/task/copy_assets.plugin (renamed from nikola/plugins/task_copy_assets.plugin)4
-rw-r--r--nikola/plugins/task/copy_assets.py (renamed from nikola/plugins/task_copy_assets.py)18
-rw-r--r--nikola/plugins/task/copy_files.plugin (renamed from nikola/plugins/task_copy_files.plugin)4
-rw-r--r--nikola/plugins/task/copy_files.py (renamed from nikola/plugins/task_copy_files.py)12
-rw-r--r--nikola/plugins/task/galleries.plugin (renamed from nikola/plugins/task_render_galleries.plugin)4
-rw-r--r--nikola/plugins/task/galleries.py553
-rw-r--r--nikola/plugins/task/gzip.plugin10
-rw-r--r--nikola/plugins/task/gzip.py78
-rw-r--r--nikola/plugins/task/indexes.plugin (renamed from nikola/plugins/task_indexes.plugin)6
-rw-r--r--nikola/plugins/task/indexes.py (renamed from nikola/plugins/task_indexes.py)82
-rw-r--r--nikola/plugins/task/listings.plugin (renamed from nikola/plugins/task_render_listings.plugin)4
-rw-r--r--nikola/plugins/task/listings.py (renamed from nikola/plugins/task_render_listings.py)31
-rw-r--r--nikola/plugins/task/localsearch.plugin (renamed from nikola/plugins/task_localsearch.plugin)4
-rw-r--r--nikola/plugins/task/localsearch/MIT-LICENSE.txt (renamed from nikola/plugins/task_localsearch/MIT-LICENSE.txt)0
-rw-r--r--nikola/plugins/task/localsearch/__init__.py (renamed from nikola/plugins/task_localsearch/__init__.py)20
-rw-r--r--nikola/plugins/task/localsearch/files/assets/css/img/loader.gif (renamed from nikola/plugins/task_localsearch/files/assets/css/img/loader.gif)bin4178 -> 4178 bytes
-rwxr-xr-xnikola/plugins/task/localsearch/files/assets/css/img/search.pngbin0 -> 315 bytes
-rwxr-xr-xnikola/plugins/task/localsearch/files/assets/css/tipuesearch.css159
-rw-r--r--nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js)174
-rw-r--r--nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js (renamed from nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js)11
-rwxr-xr-xnikola/plugins/task/localsearch/files/tipue_search.html (renamed from nikola/plugins/task_localsearch/files/tipue_search.html)0
-rw-r--r--nikola/plugins/task/mustache.plugin (renamed from nikola/plugins/task_mustache.plugin)4
-rw-r--r--nikola/plugins/task/mustache/__init__.py (renamed from nikola/plugins/task_mustache/__init__.py)35
-rw-r--r--nikola/plugins/task/mustache/mustache-template.html (renamed from nikola/plugins/task_mustache/mustache-template.html)6
-rw-r--r--nikola/plugins/task/mustache/mustache.html (renamed from nikola/plugins/task_mustache/mustache.html)10
-rw-r--r--nikola/plugins/task/pages.plugin (renamed from nikola/plugins/task_render_pages.plugin)4
-rw-r--r--nikola/plugins/task/pages.py (renamed from nikola/plugins/task_render_pages.py)14
-rw-r--r--nikola/plugins/task/posts.plugin (renamed from nikola/plugins/task_render_posts.plugin)4
-rw-r--r--nikola/plugins/task/posts.py66
-rw-r--r--nikola/plugins/task/redirect.plugin (renamed from nikola/plugins/task_redirect.plugin)4
-rw-r--r--nikola/plugins/task/redirect.py (renamed from nikola/plugins/task_redirect.py)24
-rw-r--r--nikola/plugins/task/rss.plugin (renamed from nikola/plugins/task_render_rss.plugin)6
-rw-r--r--nikola/plugins/task/rss.py (renamed from nikola/plugins/task_render_rss.py)29
-rw-r--r--nikola/plugins/task/sitemap.plugin (renamed from nikola/plugins/task_sitemap.plugin)4
-rw-r--r--nikola/plugins/task/sitemap/__init__.py172
-rw-r--r--nikola/plugins/task/sources.plugin (renamed from nikola/plugins/task_render_sources.plugin)4
-rw-r--r--nikola/plugins/task/sources.py (renamed from nikola/plugins/task_render_sources.py)63
-rw-r--r--nikola/plugins/task/tags.plugin (renamed from nikola/plugins/task_render_tags.plugin)4
-rw-r--r--nikola/plugins/task/tags.py (renamed from nikola/plugins/task_render_tags.py)137
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/expand.pngbin424 -> 0 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/link.pngbin463 -> 0 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/search.gifbin208 -> 0 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/tipuesearch.css232
-rw-r--r--nikola/plugins/task_render_galleries.py338
-rw-r--r--nikola/plugins/task_render_posts.py140
-rw-r--r--nikola/plugins/task_sitemap/__init__.py105
-rw-r--r--nikola/plugins/template/__init__.py0
-rw-r--r--nikola/plugins/template/jinja.plugin (renamed from nikola/plugins/template_jinja.plugin)4
-rw-r--r--nikola/plugins/template/jinja.py (renamed from nikola/plugins/template_jinja.py)48
-rw-r--r--nikola/plugins/template/mako.plugin (renamed from nikola/plugins/template_mako.plugin)4
-rw-r--r--nikola/plugins/template/mako.py (renamed from nikola/plugins/template_mako.py)41
-rw-r--r--nikola/post.py389
-rw-r--r--nikola/rc4.py1
-rw-r--r--nikola/utils.py402
-rw-r--r--nikola/winutils.py99
-rw-r--r--requirements-full.txt15
-rw-r--r--requirements-tests.txt6
-rw-r--r--requirements.txt17
-rwxr-xr-x[-rw-r--r--]scripts/import_po.py2
-rwxr-xr-x[-rw-r--r--]scripts/nikola.bat0
-rwxr-xr-xscripts/set_version.py36
-rwxr-xr-xsetup.py112
-rw-r--r--tests/README.rst92
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/base.py109
-rw-r--r--tests/data/translated_titles/conf.py11
-rw-r--r--tests/import_wordpress_and_build_workflow.py5
-rw-r--r--tests/test_command_import_wordpress.py90
-rw-r--r--tests/test_command_init.py12
-rw-r--r--tests/test_compile_markdown.py10
-rw-r--r--tests/test_integration.py221
-rw-r--r--tests/test_locale.py241
-rw-r--r--tests/test_plugin_importing.py8
-rw-r--r--tests/test_rss_feeds.py28
-rw-r--r--tests/test_rst_compiler.py (renamed from tests/test_rst_extensions.py)208
-rw-r--r--tests/test_scheduling.py126
-rw-r--r--tests/wordpress_export_example.xml59
-rw-r--r--translations/nikola.messages/bg.po83
-rw-r--r--translations/nikola.messages/ca.po15
-rw-r--r--translations/nikola.messages/de.po25
-rw-r--r--translations/nikola.messages/el.po20
-rw-r--r--translations/nikola.messages/en.po13
-rw-r--r--translations/nikola.messages/eo.po83
-rw-r--r--translations/nikola.messages/es.po15
-rw-r--r--translations/nikola.messages/fa.po85
-rw-r--r--translations/nikola.messages/fi.po83
-rw-r--r--translations/nikola.messages/fr.po18
-rw-r--r--translations/nikola.messages/hr.po83
-rw-r--r--translations/nikola.messages/it.po24
-rw-r--r--translations/nikola.messages/ja.po18
-rw-r--r--translations/nikola.messages/nl.po83
-rw-r--r--translations/nikola.messages/pl.po16
-rw-r--r--translations/nikola.messages/pt_BR.po20
-rw-r--r--translations/nikola.messages/ru.po29
-rw-r--r--translations/nikola.messages/sl.po83
-rw-r--r--translations/nikola.messages/tr_TR.po84
-rw-r--r--translations/nikola.messages/zh_CN.po18
405 files changed, 19707 insertions, 15670 deletions
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0cca61d
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,14 @@
+[run]
+source = nikola
+omit = /tmp/*, _*
+[report]
+exclude_lines =
+ pragma: no cover
+ def __repr__
+ if self.debug:
+ if settings.DEBUG
+ raise AssertionError
+ raise NotImplementedError
+ if 0:
+ if __name__ == .__main__.:
+ except ImportError:
diff --git a/.gitignore b/.gitignore
index d556b59..968b271 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,19 @@
*.py[co]
*.db
+*~
+dist
+*.log
+*.diff
+*.patch
tmp/
output/
build/
+.idea/
+__pycache__/
*.egg-info/
-*~
+
+# For virtualenvwrapper
+.venv
+
+# coverage.py
+.coverage
diff --git a/.travis.yml b/.travis.yml
index a4c3081..6ef3dc6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,26 @@
+before_install:
+ - sudo apt-get update -qq || true
+ - sudo apt-get --reinstall install -qq language-pack-en language-pack-es
language: python
python:
- "2.6"
- "2.7"
- - "3.2"
- "3.3"
install:
- - "pip install -r requirements.txt --use-mirrors"
- - "pip install flake8 --use-mirrors"
+ - "pip install -r requirements-tests.txt --use-mirrors"
- "pip install . --use-mirrors"
-# We run tests and afterwards nikola to see if the command is executable.
+# We run tests, nikola (to see if the command is executable) and flake8.
script:
- - nosetests --with-doctest
- - nikola help
- - "flake8 --exit-zero nikola --ignore=E501"
+ - nosetests --with-coverage --cover-package=nikola --with-doctest --doctest-options=+NORMALIZE_WHITESPACE --logging-filter=-yapsy
+ - nikola help
+ - "flake8 --ignore=E501 ."
+after_success:
+ - coveralls
+notifications:
+ irc:
+ channels:
+ - "chat.freenode.net#nikola"
+ template:
+ - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
+ - "Change view: %{compare_url}"
+ - "Build details: %{build_url}"
diff --git a/.tx/config b/.tx/config
index 6a0ee7c..dd55732 100644
--- a/.tx/config
+++ b/.tx/config
@@ -3,5 +3,6 @@ host = https://www.transifex.com
[nikola.messages]
file_filter = translations/nikola.messages/<lang>.po
+source_file = translations/nikola.messages/en.po
source_lang = en
diff --git a/AUTHORS.txt b/AUTHORS.txt
index b241433..d627544 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -1,7 +1,66 @@
-Roberto Alsina <ralsina@kde.org>
+These are the people who have committed code according to GitHub.
+There is probably someone missing if he send me code directly, and
+for that I am sorry because I believe in giving credit.
+If you are here and prefer to be listed in a different way, just
+do a PR :-)
+
+ -- Roberto Alsina
+
+
+Aru Sahni <https://github.com/arusahni>
+Bluerise <https://github.com/Bluerise>
+Chris “Kwpolska” Warrick <https://github.com/Kwpolska>
+Daniel Aleksandersen <https://github.com/Aeyoun>
+DoctorMalboro <https://github.com/DoctorMalboro>
Eduardo Schettino <https://github.com/schettino72>
-Niko Wenselowski
+Kay Hayen <https://github.com/kayhayen>
+Niko Wenselowski <https://github.com/okin>
+Raimon Esteve <https://github.com/raimonesteve>
+Roberto Alsina <https://github.com/ralsina>
Roman Imankulov <https://github.com/imankulov>
-Kay Hayen <kay.hayen@gmail.com>
+StyXman <https://github.com/StyXman>
+Tordek <https://github.com/Tordek>
+Troy Toman <https://github.com/troytoman>
Zhaojun Meng <https://github.com/zhaojunmeng>
-Chris “Kwpolska” Warrick <kwpolska@gmail.com>
+agustinhenze <https://github.com/agustinhenze>
+areski <https://github.com/areski>
+bwhmather <https://github.com/bwhmather>
+camboris <https://github.com/camboris>
+claxo <https://github.com/claxo>
+clee <https://github.com/clee>
+damianavila <https://github.com/damianavila>
+dastagg <https://github.com/dastagg>
+dflock <https://github.com/dflock>
+dhruvbaldawa <https://github.com/dhruvbaldawa>
+dmoisset <https://github.com/dmoisset>
+edwinsteele <https://github.com/edwinsteele>
+fisadev <https://github.com/fisadev>
+fizyk <https://github.com/fizyk>
+ivanov <https://github.com/ivanov>
+ivanteoh <https://github.com/ivanteoh>
+jerrykan <https://github.com/jerrykan>
+kadefor <https://github.com/kadefor>
+koniiiik <https://github.com/koniiiik>
+kotnik <https://github.com/kotnik>
+localvoid <https://github.com/localvoid>
+marcelomd <https://github.com/marcelomd>
+marianoguerra <https://github.com/marianoguerra>
+mattgaviota <https://github.com/mattgaviota>
+mgaitan <https://github.com/mgaitan>
+mrabbitt <https://github.com/mrabbitt>
+neilmb <https://github.com/neilmb>
+notfoss <https://github.com/notfoss>
+numshub <https://github.com/numshub>
+pabluk <https://github.com/pabluk>
+paskal <https://github.com/paskal>
+punchagan <https://github.com/punchagan>
+quijot <https://github.com/quijot>
+quodlibetor <https://github.com/quodlibetor>
+quoth <https://github.com/quoth>
+rafacarrascosa <https://github.com/rafacarrascosa>
+rbistolfi <https://github.com/rbistolfi>
+snaewe <https://github.com/snaewe>
+tolusonaike <https://github.com/tolusonaike>
+wimpr1m <https://github.com/wimpr1m>
+yarko <https://github.com/yarko>
+neiesc <https://github.com/neiesc>
diff --git a/CHANGES.txt b/CHANGES.txt
index 9e7976d..c1fcb44 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,359 @@
+New in 6.2.1
+============
+
+Features
+--------
+
+* Default themes adds semantic meanings to posts using h-entry (a microformats2) and HTML 5 microdata (Issue #867)
+* New "hidetitle" metadata to suppress printing a title as heading (Issue #584)
+* Template systems can now render to and from a string. (Issue #881)
+* New event for newly deployed posts (Issue #882)
+
+
+Bugfixes
+--------
+
+* Fixed some locale problems in posix systems (Issues #886, #884, #875)
+* Don’t include BLOG_DESCRIPTION as meta tag on tag and gallery pages (Issue #876)
+
+New in 6.2.0
+============
+
+Features
+--------
+
+* RSS feeds for galleries (Issue #786)
+* New EXTRA_IMAGE_EXTENSIONS option, and support most common image extensions by default (Issue #857)
+* Add reStructuredText directive post_list (Issue #815)
+* Galleries support translations
+* Support a single archive per site (optional; instead of per-year or per-month archives) (Issue #853)
+* The locales used in testing can be specified in environment variables (Issue #818)
+* The locale to use with each language can be explicitly set (Issue #818)
+* Initial support for signals/hooks using Blinker (Issue #764)
+* Path/Link resolution mechanism for plugins (Issue #790)
+* Sass compiling (Issue #642)
+* Emit a signal when a new post is created (Issue #831)
+* New Finnish translation.
+
+Bugfixes
+--------
+
+* Don't create empty author elements, use dc:creator if author is not an email (Issue #868)
+* Added striphtml filter in mako templates, strip tags from titles (Issue #865)
+* Major refactoring of post creation (Issue #800)
+* Major refactoring of galleries (Ongoing, Issue #786)
+* Locale support in windows fixed (Issue #818)
+* Fix logging incompatibility in Yapsy (Issue #826)
+* Workaround for Mako cache folder unicode bug (Issue #825)
+* Ensure folder creation when importing wordpress (Issue #812)
+* Handle empty stories better and avoid Tipue crash (Issue #811)
+* Trigger conflict if two posts generate the same output (Issue #806)
+* Compilers now accept two of any line endings (\n, \r\n) instead of only Unix (\n) (Issue #832)
+* Include RSS feeds in sitemap (Issue #804)
+* Removed optional priority element from sitemap (Issue #838)
+* Avoid duplicate entries in the sitemap (Issue #836)
+* Include .xml in GZIP_EXTENSIONS (for RSS, sitemap) (Issue #844)
+* Post.base_path is now using forward slashes on Windows (Issue #856)
+* Custom CSS not detected in the files folder (Issue #862)
+* Exclude non-RSS XML files in sitemap (Issue #864)
+* Exclude "html" files without a doctype decleration in sitemap (Issue #864)
+* Do not include "Read more" links at the end of teasers when stripping HTML
+
+Other
+-----
+
+* the bpython console is now “slightly deprecated”
+
+New in 6.1.1
+============
+
+Features
+--------
+
+* New Slovenian translation
+
+Bugfixes
+--------
+
+* The build_less plugin was broken (Issue #802)
+* Every plugin now has its own LOGGER instance, with different names (Issue #797)
+* A standardized way of reporting missing requirements (Issue #797)
+* Don’t force requests for compile.rest.gist (Issue #795)
+
+New in 6.1.0
+============
+
+Features
+--------
+
+* Added slug support to the magic link:// URLs
+* New Esperanto translation.
+* New -q flag for quiet running (Issue #762)
+* Added author field in RSS items (Issue #767)
+* Annotations using annotateit.org (Issue #623)
+* New GALLERY_SORT_BY_DATE option, defaulting to True.
+ If set to False, sorts by name (Issue #667)
+
+Bugfixes
+--------
+
+* Fixed compatibility with Windows’ multiple partitions (Issues #776, #784)
+* Added language in the RSS feeds (Issue #777)
+* Don't fail for empty bundles.
+* Fix USE_BUNDLES compatibility with Python 3 (Issue #760)
+* Compatibility with doit 0.23.0 (Issues #756 #758)
+* Install enough of the theme chain to ensure themes are usable (Issue #748)
+* Support Gist URLs in the gist directive for compatibility with sphinx-gist-embed (Issue #754)
+* windows, dev - fix point pth to clone and run nikola without install (Issue #751)
+* windows - fix install from clone or from a Github download (Issue #747, Windows)
+* Make Listings directive inherit Include making it simpler and more powerful (Issue #744)
+* Install builtin themes (Issue #741)
+* LOGGER was incorrectly imported in planetoid.py
+* Order Monthly Archive properly, descending order (Issue #740)
+
+New in 6.0.4
+============
+
+Features
+--------
+
+* New :doc: role for reStructuredText compiler, creates a link to another post
+ / page inside the same site (Issue #724)
+
+Bugfixes
+--------
+
+* Added missing metadata in internals.txt and extending.txt (Issue #738)
+* Fix addthis sizing with bootstrap3 theme (Issue #731)
+* Refuse to install in unsupported Python versions (Issue #733)
+* Fix sitemap plugin compatibility with Python 3 (Issue #734)
+* Changed default PyGal style (Issue #726)
+* Add files used by the listings directive as dependencies (Issue #723)
+* Localsearch task now depends on all pages, for better accuracy.
+* More robust directory creation (Issue #208)
+* Proper logging (Issues #712 #704)
+* Be less agressive in hyphenating, also, split words differently. (Issue #718)
+* Fixed several blogger import issues.
+* Avoid double slashes in RSS link elements (Issue #716)
+* Fix Python 3 incompatibility (Issue #715)
+* Consider the current theme chain's parent, bundles and engine part of the file_dep for
+ files created by generic_page_renderer (Issue #711)
+
+New in 6.0.3
+============
+
+Features
+--------
+
+* New --strict option for build command, makes warnings abort the build (Issue #704)
+ Still doesn't really affect behaviour, because each warning needs to check it.
+
+Bugfixes
+--------
+
+* Use correct pandoc argument order (Issue #709)
+* Invalid POSTS and PAGES were created by the wordpress importer.
+* Normalize paths in task targets, helping detect conflicts (Issue #546)
+* Strip multiple digits in unslugify, used by image galleries (Issue #703)
+* Added missing defaults and make Nikola work with empty conf.py (Issue #697)
+* Add thumbnails as file dependencies of gallery indexes (Issue #551)
+* Copy original image as thumbnail if PIL fails to resize it (Issue #551)
+* Apply filters to files generated by the gallery tasks (Issue #708)
+* More robust handling of filter commands' shell quoting (Issue #705)
+
+New in 6.0.2
+============
+
+Features
+--------
+
+* Support for external gzip commands (Issue #351)
+
+Bugfixes
+--------
+
+* More meaningful error if conf.py is invalid (Issue #680)
+* Made compatible with Docutils<0.9 again (Issue #679)
+* Make footnote-references keep line height in rst.css
+* Make bootswatch_theme work under Python 3 (Issue #695)
+
+New in 6.0.1
+============
+
+Features
+--------
+
+* New Russian translation
+* Made ``private`` an alternate spelling of the ``retired`` tag (via Issue
+ #686)
+
+Bugfixes
+--------
+
+* Remove decoding errors if files are not proper UTF-8 (Issue #691)
+* Stop ignoring \*.JPG and \*.PNG by galleries (Issue #690)
+
+New in 6.0.0
+============
+
+Features
+--------
+
+* Deprecated post_compilers for COMPILERS (Issue #601)
+* Replaced post_pages option with POSTS and PAGES (Issue #601)
+* Support for bootswatch in boostrap 2 and 3 (Issue #599)
+* New ADDITIONAL_METADATA option to set extra metadata on all posts.
+* All reStructuredText extensions are now plugins (Issue #621)
+* New multiple comment system support (Issue #606, #634)
+
+ * Deprecated DISQUS_FORUM option for COMMENT_SYSTEM_ID
+ * New COMMENT_SYSTEM option to change comment systems, defaults to disqus
+
+* New bootstrap3 theme (by areski)
+* Added docs/upgrading-to-v6.txt for upgrade tips.
+* Theme cleanup (Issue #599)
+* Support for nested navigation links (Issue #104)
+* New media directive for reStructuredText (Issue #608)
+* New DEPLOY_DRAFTS and DEPLOY_FUTURE options (Issue #583)
+* New Dutch translation
+* Optional hyphenation (HYPHENATE option, Issue #576)
+* Made AddThis support replaceable through new SOCIAL_BUTTONS_CODE option.
+* categories
+* Added a FUTURE_IS_NOW option for publishing future-dated posts now (Issues #486, #577)
+* New typogrify filter (Issue #576)
+* New COPY_SOURCES option, defaults to True (Issue #544)
+* Customizable Read More (Issues #412, #533, #574)
+* Support for LESS/RECESS CSS compilers
+* Updated to Colorbox 1.4.27
+* New pandoc compiler.
+* Separate slides.tmpl template so that non-bootstrap themes can support them.
+* New ADDITIONAL_METADATA option to specify metadata to include in the new_post
+ command, alongside with the defaults (Issue #622)
+* Schedule new posts automatically by specifying an iCal recursive rule as
+ SCHEDULE_RULE (Issue #602)
+
+Bugfixes
+--------
+
+* Added missing rel="self" link suggested by feedvalidator.org (Issue #264)
+* Switched to semantic versioning (Issue #614)
+* Better figure styling (Issue #626)
+* Switched docutils math rendering to MathJax (Issue #620)
+* Refactored crumb bar into a separate tmpl to avoid code duplication (Issue #612)
+* Fix broken config dep for tags when NAVIGATION_LINKS is not set (Issue #610)
+* Fixed bundles in base theme
+* Deprecated ANALYTICS, replaced with BODY_END (Issue #601)
+* Deprecated SIDEBAR_LINKS, replaced with NAVIGATION_LINKS (Issue #104 #601)
+* Solve site theme's navbar overlapping Issues (Issue #585)
+* Deprecated ADD_THIS_BUTTONS option.
+* Show warning if unable to thumbnail an image (Issue #551)
+* Use absolute URLs in feeds (Issue #590)
+* Fix for mincss path munging (Issues #570 #589)
+* Added missing dependency in gallery indexes (Issue #536)
+* Anchors in listings had wrong filename slugs
+* Only warn about incomplete translations once (Issue #580)
+* Right-align image title in colorbox to avoid overlapping so much (Mentioned in Issue #570)
+* Added LICENSE in the footer (Issue #528)
+* Use random IDs for slides so you can have more than one in a page (Issue #572)
+
+New in 5.5.1
+============
+
+Features
+--------
+
+* New FEED_LENGTH option, defaults to 10 (Issue #549)
+* Added no-cache headers to ``nikola serve`` (Issue #545)
+* New mincss command, removes unused and redundant CSS (Issue #364)
+* New chart directive based on pygal
+* Update of IPython plugin to work with the upcoming IPython 1.0
+
+Bugfixes
+--------
+
+* Remove cache/ when running ``nikola clean`` (Issue #558)
+* Mark bundles as "cleanable" (Issue #558)
+* Made ``nikola help`` much more resilient against broken conf.py (Issue #550)
+* Show docinfo part of docutils output (Issue #556)
+* Exit with error code when there is an error (Issue #543)
+* Revamped ``requirements.txt`` — now ``requirements.txt`` contains the
+ required dependencies and ``requirements-full.txt`` contains the
+ optional ones (Issue #547)
+* Fixed bug in example usage of install_theme.
+* Better help text for bootswatch command.
+* Fixed installation under pip 1.4 or later (pytz and yapsy versions must be
+ hardcoded due to crazy versioning schemes)
+* Added .rst to the default reStructuredText extensions (via Issue #542)
+* Moved the sample site to the new one-file format (Issue #542)
+* Escape HTML in titles (Issue #537)
+* Fixed issues with index pages indices (Issue #532)
+* Fixed IPython plugin to work with the latest IPython.nbconvert machinery
+* Fixed failing build because of hidden folders and files inside post folder
+
+New in 5.5
+==========
+
+Features
+--------
+
+* New HIDE_SOURCELINK option (defaults to False)
+* Switched theme repo to http://themes.nikola.ralsina.com.ar (Issue #411)
+* New (untested) ASCIIDoc compiler
+* Display very annoying warning when deploying the nikolademo Disqus account
+* New Bulgarian translation
+* New man page (created with help2man nikola -N --version-string=5.5) (Issue #460)
+* Added "nikola version" command (Issue #504)
+* Added more search form examples in conf.py.in (Issue #515)
+* Template dependency support for Jinja (by koniiiik, Issue #511)
+* New --clean-files option for nikola check
+* Support --version on "run" command (Issue #504)
+* New Turkish translation.
+* New MARKDOWN_EXTENSIONS option, defaults to previopus behaviour (Issue #496)
+* Posts with dates in the future will be treated as drafts (Implemented by punchagan)
+* New "TaskMultiplier" plugin category, takes a task, returns 0 or more new tasks (Issue #483)
+* New 'LINK_CHECK_WHITELIST' option for link checker (Issue #477)
+* New Croatian translation.
+* Jinja-default and jinja-site themes now automatically generated from the Mako versions.
+* New Persian translation
+* RSS feed now includes post tags (Issue #462)
+* Experimental generic RSS/Atom importer (Issue #347)
+* PHP support (Issue #313)
+* New PRETTY_URLS option (generates slug/index.html instead of slug.html) (Issue #429)
+* Added "templates/" folder to template path.
+* Keyword metadata support in post pages (Issue #451)
+
+Bugfixes
+--------
+
+* Improve custom.css detection (Issue #526)
+* Fixed bad interaction between 2-file-format and untranslated posts in multilingual sites (Issue #525)
+* Workaround in site theme for navbar hiding targets
+* Flip index pages order (Issue #521)
+* Make txt2tags work again (Issue #520)
+* Updated to latest colorbox
+* Skip metadata when compiling one-file posts (Issue #508)
+* Use schema-relative URL for addthis to avoid insecure page warnings on HTTPS sites (Issue #510)
+* Made the gzip plugin generate less confusing task names (no more double colon)
+* Made "nikola check -f" respect OUTPUT_DIR
+* Customizable teasers were broken (Issue #502)
+* Be less invasive when processing rendered HTML (Issue #500)
+* Run all tasks that should run (Issue #494, #493)
+* Handle timezones in posterous imports (Issue #475)
+* Actually ensure LateTasks are run later than Tasks
+* Don't add drafts and retired posts to the sitemap (Issue #476)
+* Avoid __future__ in setup.py because it breaks buildout (Issue #478)
+* Use correct source links with PRETTY_URL=True (Issue #474)
+* Handle non-ascii path in bundles (Issue #473)
+* Handle non-ascii path in plugin loader (Issue #471)
+* Handle non-ascii arguments in main (Issue #470)
+* Make sitemap depend on file dates (reported by Kay Hayen)
+* Better support for non-ascii urlencoded paths in the wp importer (Issue #468)
+* Avoid crashing if user does a deploy with no commands
+* Strip HTML and BODY tags in Post.text() (Issue #464)
+* -f option broken in HTML compiler (Issue #463)
+* Don't map empty folders, map folders with index.html, and don't map index.html (Issue #430)
+* Wordpress import: write correct redirections for URLs not ending in an / (Issue #459)
+
New in 5.4.4
============
@@ -196,7 +552,7 @@ Features
* When using import_wordpress users can exclude drafts with the ``-d`` switch.
* New STORY_INDEX option to generate index.html in story folders.
* Sort tags case insensitive.
-* New polish translation.
+* New Polish translation.
* Add multi size favicon support.
* Use multilingual Disqus (although it doesn't seem to work)
* Add Simplified Chinese translations.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 0000000..f31f798
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,61 @@
+Patch submission guidelines [1]_
+--------------------------------
+
+Here are some guidelines about how you can contribute to Nikola:
+
+* First, make sure there is an open issue for your change. Perhaps,
+ if it's a new feature, you probably want to
+ `discuss it first <http://groups.google.com/group/nikola-discuss>`_
+
+* **Create a new Git branch specific to your change(s).** For example, if
+ you're adding a new feature to foo the bars, do something like the
+ following::
+
+ $ git checkout master
+ $ git pull
+ $ git checkout -b foo-the-bars
+ <hack hack hack>
+ $ git push origin HEAD
+ <submit pull request based on your new 'foo-the-bars' branch>
+
+ This makes life much easier for maintainers if you have (or ever plan to
+ have) additional changes in your own ``master`` branch.
+
+ Also, if you have commit rights to the main Nikola repository, we suggest
+ having your branch there, instead of a personal fork.
+
+.. admonition:: A corollary:
+
+ Please **don't put multiple fixes/features in the same
+ branch/pull request**! In other words, if you're hacking on new feature X
+ and find a bugfix that doesn't *require* new feature X, **make a new
+ distinct branch and PR** for the bugfix.
+
+* You may want to use the `Tim Pope’s Git commit messages standard
+ <http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_.
+ It’s not necessary, but if you are doing something big, we recommend
+ describing it there.
+* While working, **rebase instead of merging** (if possible). We encourage
+ using ``git rebase`` instead of ``git merge``. If you are using
+ ``git pull``, please run ``git config pull.rebase true`` to prevent merges
+ from happening and replace them with rebase goodness. There is also an
+ “emergency switch” in case rebases fail and you do not know what to do:
+ ``git pull --no-rebase``.
+* **Make sure documentation is updated** — at the very least, keep docstrings
+ current, and if necessary, update the reStructuredText documentation in ``docs/``.
+* **Add a changelog entry** at the top of ``CHANGES.txt`` mentioning issue number
+ and in the correct Features/Bugfixes section.
+* **Run flake8** for style consistency. Use ``flake8 --ignore=E501 .``
+* **Try writing some tests** if possible — again, following existing tests is
+ often easiest, and a good way to tell whether the feature you are modifying is
+ easily testable. You will find instructions in ``tests/README.rst``.
+ (alternatively you can push and wait for Travis to pick up and test your changes,
+ but we encourage to run them locally before pushing.)
+* Make sure to mention the issue it affects in the description of your pull request,
+ so it's clear what to test and how to do it.
+* There are some quirks to how Nikola's codebase is structured, and to how
+ some things need to be done [2]_ but don't worry, we'll guide you!
+
+.. [1] Very inspired by `fabric's <https://github.com/fabric/fabric/blob/master/CONTRIBUTING.rst>`_ thanks!
+
+.. [2] For example, logging, or always making sure directories are created using ``utils.makedirs()``
diff --git a/LICENSE.txt b/LICENSE.txt
index 19c37bb..2f1eda4 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2012 Roberto Alsina y otros.
+Copyright © 2012-2013 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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..40af1db
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+graft docs
+graft nikola
+graft scripts
+graft tests
+include *.txt
+include *.rst
diff --git a/README.md b/README.md
deleted file mode 100644
index edb7a7d..0000000
--- a/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-Nikola, a Static Site and Blog Generator
-========================================
-
-In goes content, out comes a website, ready to deploy.
-
-[![Build Status](https://travis-ci.org/ralsina/nikola.png)](https://travis-ci.org/ralsina/nikola)
-
-Why Static Websites?
---------------------
-
-Static websites are safer, use fewer resources, and avoid vendor and platform lock-in.
-You can read more about this in the [Nikola Handbook.](http://nikola.ralsina.com.ar/handbook.html#why-static)
-
-What Can Nikola Do?
--------------------
-
-It has many features, but here are some of the nicer ones:
-
-* `Blogs, with tags, feeds, archives, comments, etc. <some-sites-using-nikola.html>`_
-* `Themable <theming.html>`_
-* Fast builds, thanks to `doit <http://python-doit.sf.net>`_
-* Flexible, extensible via plugins
-* Small codebase (programmers can understand all of Nikola core in a day)
-* `reStructuredText <quickstart.html>`_ [`Cheatsheet <http://ubuntuone.com/1M7C5fdbLkggF7ptoUvTq8>`_] or Markdown as input language (also Wiki, BBCode, Textile, and HTML)
-* Easy `image galleries </galleries/demo/>`_ (just drop files in a folder!)
-* Syntax highlighting for almost any programming language or markup
-* Multilingual sites, `translated to 11 languages <https://www.transifex.com/projects/p/nikola/>`_.
-* Doesn't reinvent wheels, leverages existing tools.
-* Python 2 and 3 compatible.
-
-For more information, see http://nikola.ralsina.com.ar
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..afa5500
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,68 @@
+Nikola, a Static Site and Blog Generator
+========================================
+
+In goes content, out comes a website, ready to deploy.
+
+.. image:: https://travis-ci.org/getnikola/nikola.png
+ :target: https://travis-ci.org/getnikola/nikola
+
+.. image:: https://pypip.in/v/Nikola/badge.png
+ :target: https://crate.io/packages/Nikola
+
+.. image:: https://pypip.in/d/Nikola/badge.png
+ :target: https://crate.io/packages/Nikola
+
+.. image:: https://coveralls.io/repos/getnikola/nikola/badge.png?branch=master
+ :target: https://coveralls.io/r/getnikola/nikola?branch=master
+
+
+Why Static Websites?
+--------------------
+
+Static websites are safer, use fewer resources, and avoid vendor and platform lock-in.
+You can read more about this in the `Nikola Handbook`_
+
+
+What Can Nikola Do?
+-------------------
+
+It has many features, but here are some of the nicer ones:
+
+* `Blogs, with tags, feeds, archives, comments, etc.`__
+* `Themable`_
+* Fast builds, thanks to `doit`_
+* Flexible, extensible via plugins
+* Small codebase (programmers can understand all of Nikola core in a day)
+* `reStructuredText`_ or Markdown as input language (also Wiki, BBCode, Textile, and HTML)
+* Easy `image galleries`_ (just drop files in a folder!)
+* Syntax highlighting for almost any programming language or markup
+* Multilingual sites, `translated to 18 languages.`__
+* Doesn't reinvent wheels, leverages existing tools.
+* Python 2.6, 2.7 and 3.3 compatible.
+
+.. _Nikola Handbook: http://getnikola.com/handbook.html#why-static
+__ http://users.getnikola.com/
+.. _Themable: http://themes.getnikola.com
+.. _doit: http://pydoit.org
+.. _reStructuredText: http://getnikola.com/quickstart.html
+.. _image galleries: http://getnikola.com/galleries/demo/
+__ https://www.transifex.com/projects/p/nikola/
+
+Installation Instructions
+-------------------------
+
+Assuming you have pip installed::
+
+ git clone git://github.com/getnikola/nikola.git
+ cd nikola
+ pip install .
+
+Optionally (for markdown and lots of other features)::
+
+ pip install -r requirements.txt
+
+For even more stuff, like tests and very optional features::
+
+ pip install -r requirements-full.txt
+
+For more information, see http://getnikola.com/
diff --git a/docs/creating-a-site.txt b/docs/creating-a-site.txt
index 41985c9..a200d54 100644
--- a/docs/creating-a-site.txt
+++ b/docs/creating-a-site.txt
@@ -3,16 +3,17 @@
.. tags: nikola, python
.. link:
.. description:
+.. title: Creating a Site (Not a Blog) with Nikola
Creating a Site (Not a Blog) with Nikola
========================================
-One of the most frequent questions I get about Nikola is "but how do
+One of the most frequent questions I get about Nikola is "but how do
I create a site that's not a blog?". And of course, that's because the
documentation is heavily blog-oriented. This document will change that ;-)
Since it started, Nikola has had the capabilities to create generic sites. For example,
-Nikola's `own site <http://nikola.ralsina.com.ar>`_ is a fairly generic one. Let's go
+Nikola's `own site <http://getnikola.com>`_ is a fairly generic one. Let's go
step by step on how you can do something like that.
As usual when starting a nikola site, you start with ``nikola init`` which creates a
@@ -45,9 +46,13 @@ configuration file:
# Some things in the middle you don't really need to change...
#
- post_pages = (
- ("pages/*.txt", "", "story.tmpl", False),
- )
+ POSTS = []
+ PAGES = [("pages/*.txt", "", "story.tmpl")]
+
+ # And to avoid a conflict because blogs try to generate /index.html
+ INDEX_PATH = "blog"
+
+
And now we are ready to create our first page::
@@ -96,12 +101,12 @@ not-blogs, same for tags and link. Description is useful for SEO purposes
if you care for that.
And below, the content. By default you are expected to use
-`reStructured text <http://nikola.ralsina.com.ar/quickstart.html>`_ but
+`reStructuredText <http://getnikola.com/quickstart.html>`_ but
Nikola supports a ton of formats, including Markdown, plain HTML, BBCode,
Wiki, and Textile.
So, let's give the page a nicer title, and some fake content. Since the default
-Nikola theme (called "site") is based on `bootstrap <http://twitter.github.com/bootstrap/>`_
+Nikola theme (called "bootstrap") is based on `bootstrap 2 <http://getbootstrap.com/2.3.2/>`_
you can use anything you like from it:
.. code-block:: rest
@@ -144,36 +149,23 @@ you can use anything you like from it:
.. admonition:: TIP: Nice URLs
If you like your URLs without the ".html" then you want to create folders and
- put the pages in ``index.html`` inside them. Example::
-
- nikola new_post -p pages/foo/index.txt
-
- which will create a page you could access as "http://yoursite.com/foo"
+ put the pages in ``index.html`` inside them using the ``PRETTY_URLS`` option.
-And that's it. You will want to change the SIDEBAR_LINKS option to create a reasonable
+And that's it. You will want to change the NAVIGATION_LINKS option to create a reasonable
"menu" for your site, you will want to hack the theme (check ``nikola help bootswatch_theme``
for a quick & dirty solution), and you may want to add a blog later on, for company news
or whatever.
.. admonition:: TIP: So, how do I add a blog now?
- First, change the ``post_pages`` option like this:
-
- .. code-block:: python
-
- post_pages = (
- ("pages/*.txt", "", "story.tmpl", False),
- ("posts/*.txt", "blog", "post.tmpl", True),
- )
-
- And to avoid a conflict (because blogs try to generate ``/index.html``:
+ First, change the ``POSTS`` option like this:
.. code-block:: python
- INDEX_PATH = "blog"
+ POSTS = [("posts/*.txt", "blog", "post.tmpl", True)]
Create a post with ``nikola new_post`` and that's it, you now have a blog
- in http://yoursite.com/blog (you may want to add links to it in SIDEBAR_LINKS of course).
+ in http://yoursite.com/blog (you may want to add links to it in NAVIGATION_LINKS of course).
You can see the finished site in http://notablog.ralsina.com.ar and its full configuration in
http://ralsina.com.ar/listings/notablog/conf.py.html
diff --git a/docs/creating-a-theme.txt b/docs/creating-a-theme.txt
index 11428bd..1e67410 100644
--- a/docs/creating-a-theme.txt
+++ b/docs/creating-a-theme.txt
@@ -1,27 +1,40 @@
+.. title: Creating a Theme
+.. slug: creating-a-theme
+.. date: 2012/03/13 12:00
+.. tags:
+.. link:
+.. description:
+
Creating A Theme From Scratch (Almost)
======================================
There is some documentation about creating themes for Nikola, but maybe a tutorial is also a useful way
-to explain it. So, here it is. I'll explain how to create a theme (almost) from scratch. Alternatively,
-you can take an existing theme and modify only parts of it via inheritance, but that's for another
-document.
+to explain it. So, here it is. I'll explain how to create a theme (almost) from scratch. All themes
+in Nikola must inherit from the ``base`` theme. In this case, we will inherit from ``bootstrap``
+so we get good support for slides and galleries.
-I will try to create a theme that looks like `Vinicius Massuchetto's Monospace Theme <http://vinicius.soylocoporti.org.br/monospace-wordpress-theme/#.UN4e9lLzu3c>`_.
+I will try to create a theme that looks like `Vinicius Massuchetto's Monospace Theme <http://wordpress.org/themes/monospace>`_.
.. TEASER_END
Starting The Theme
------------------
-First, we create a testing site, and copy the orphan theme from nikola's sources into the right place::
+First we will create a testing site::
$ nikola init --demo monospace-site
A new site with some sample data has been created at monospace-site.
See README.txt in that folder for more information.
$ cd monospace-site/
+
+Our theme will inherit from the ``bootstrap`` theme, which is full-featured but boring.
+
+::
+
$ mkdir themes
- $ cp -RL ~/Desktop/proyectos/nikola/nikola/nikola/data/themes/orphan/ themes/monospace
+ $ mkdir themes/monospace
+ $ echo bootstrap > themes/monospace/parent
The next step is to make the testing site use this new theme, by editing ``conf.py`` and
changing the ``THEME`` option::
@@ -33,56 +46,99 @@ Now we can already build and test the site::
$ nikola build && nikola serve
-.. figure:: http://ralsina.com.ar/galleries/random/monospace-1.png
+.. figure:: http://ralsina.com.ar/galleries/monospace-tut/monospace-1.png
:height: 400px
- This is the almost completely unstyled "orphan" theme.
-
-Of course, the page layout is completely broken. To fix that, we need to get into templates.
+ This is the default "bootstrap" theme.
+
+Of course, the page layout is completely different from what we want. To fix that, we need to
+get into templates.
Templates: Page Layout
----------------------
The general page layout for the theme is done by the ``base.tmpl`` template, which is done using
-`Mako <http://www.makotemplates.org/>`_. This is orphan's ``base.tmpl``, it's not very big:
+`Mako <http://www.makotemplates.org/>`_. This is bootstrap's ``base.tmpl``, it's not very big:
.. code-block:: mako
## -*- coding: utf-8 -*-
- <%namespace file="base_helper.tmpl" import="*"/>
+ <%namespace name="base" file="base_helper.tmpl" import="*" />
+ <%namespace name="bootstrap" file="bootstrap_helper.tmpl" import="*" />
+ ${set_locale(lang)}
<!DOCTYPE html>
- <html lang="${lang}">
+ <html% if comment_system == 'facebook': xmlns:fb="http://ogp.me/ns/fb#" %endif lang="${lang}">
<head>
- ${html_head()}
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ ${bootstrap.html_head()}
<%block name="extra_head">
</%block>
+ ${extra_head_data}
</head>
<body>
- %if add_this_buttons:
- <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script>
- % endif
- <h1 id="blog-title">
- <a href="${abs_link('/')}" title="${blog_title}">${blog_title}</a>
- </h1>
- <%block name="belowtitle">
- %if len(translations) > 1:
- <small>
- ${(messages[lang][u"Also available in"])}:&nbsp;
- ${html_translations()}
- </small>
- %endif
- </%block>
- <%block name="content"></%block>
- <small>${content_footer}</small>
- <!--Sidebar content-->
- <ul class="unstyled">
- <li>${license}
- ${html_social()}
- ${html_sidebar_links()}
- <li>${search_form}
- </ul>
- ${analytics}
- <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
+ <!-- Menubar -->
+ <div class="navbar navbar-fixed-top" id="navbar">
+ <div class="navbar-inner">
+ <div class="container">
+
+ <!-- .btn-navbar is used as the toggle for collapsed navbar content -->
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+
+ <a class="brand" href="${abs_link('/')}">
+ ${blog_title}
+ </a>
+ <!-- Everything you want hidden at 940px or less, place within here -->
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ ${bootstrap.html_navigation_links()}
+ </ul>
+ %if search_form:
+ ${search_form}
+ %endif
+ <ul class="nav pull-right">
+ <%block name="belowtitle">
+ %if len(translations) > 1:
+ <li>${base.html_translations()}</li>
+ %endif
+ </%block>
+ % if not hide_sourcelink:
+ <li><%block name="sourcelink"></%block></li>
+ %endif
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- End of Menubar -->
+ <div class="container-fluid" id="container-fluid">
+ <!--Body content-->
+ <div class="row-fluid">
+ <div class="span2"></div>
+ <div class="span8">
+ <%block name="content"></%block>
+ </div>
+ </div>
+ <!--End of body content-->
+ </div>
+ <div class="footerbox">
+ ${content_footer}
+ </div>
+ ${bootstrap.late_load_js()}
+ ${base.html_social()}
+ <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});
+ $(window).on('hashchange', function(){
+ if (location.hash && $(location.hash)[0]) {
+ $('body').animate({scrollTop: $(location.hash).offset().top - $('#navbar').outerHeight(true)*1.2 }, 1);
+ }
+ });
+ $(document).ready(function(){$(window).trigger('hashchange')});
+ </script>
+ <%block name="extra_js"></%block>
+ ${body_end}
</body>
@@ -94,18 +150,18 @@ Monospace is a two-column-with-footer layout, so let's copy the basics from its
.. code-block:: mako
## -*- coding: utf-8 -*-
- <%namespace file="base_helper.tmpl" import="*"/>
+ <%namespace name="base" file="base_helper.tmpl" import="*"/>
+ <%namespace name="bootstrap" file="bootstrap_helper.tmpl" import="*" />
+ ${set_locale(lang)}
<!DOCTYPE html>
<html lang="${lang}">
<head>
- ${html_head()}
+ ${bootstrap.html_head()}
<%block name="extra_head">
</%block>
+ ${extra_head_data}
</head>
<body class="home blog">
- %if add_this_buttons:
- <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script>
- % endif
<div id="wrap" style="width:850px">
<div id="container" style="width:560px">
<%block name="content"></%block>
@@ -118,30 +174,32 @@ Monospace is a two-column-with-footer layout, so let's copy the basics from its
<%block name="belowtitle">
%if len(translations) > 1:
<small>
- ${(messages[lang][u"Also available in"])}:&nbsp;
- ${html_translations()}
+ ${(messages("Also available in"))}:&nbsp;
+ ${base.html_translations()}
</small>
%endif
</%block>
<ul class="unstyled">
<li>${license}
- ${html_social()}
- ${html_sidebar_links()}
+ ${base.html_social()}
+ ${bootstrap.html_navigation_links()}
<li>${search_form}
</ul>
</div>
<div id="footer">
${content_footer}
</div>
- </div>
- ${analytics}
- <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
+ </div>
+ ${bootstrap.late_load_js()}
+ <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});</script>
+ <%block name="extra_js"></%block>
+ ${body_end}
</body>
-.. figure:: http://ralsina.com.ar/galleries/random/monospace-2.png
+.. figure:: http://ralsina.com.ar/galleries/monospace-tut/monospace-2.png
Yikes!
-
+
This will get better quickly once we add some CSS
@@ -149,10 +207,11 @@ Base CSS
--------
The orphan theme includes just a little styling, specifically ``rest.css`` so
-the restructured text output looks reasonable, and code.css for code snippets.
+the reStructuredText output looks reasonable, and ``code.css`` for code snippets.
It also includes an empty ``assets/css/theme.css`` where you can add your own CSS.
-For example, this is taken from the original monospace theme:
+For example, this is taken from the original monospace theme, except for the last
+few selectors:
.. code-block:: css
@@ -167,44 +226,61 @@ For example, this is taken from the original monospace theme:
#sidebar > li { margin:20px 0px; }
#sidebar h1 { border-bottom:1px dotted #C8C8C8; }
#sidebar .description { display:block; width:100%; height:auto; margin:0px 0px 10px 0px; }
-
+ h1, h2, h3, h4, h5, h6, h7 { margin:0px; text-transform:uppercase; }
+ h4, h5, h6 { font-size:14px; }
+ #blog-title { margin-top: 0; line-height:48px;}
+ .literal-block {padding: .5em;}
+ div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
+ /* Issue 277 */
+ border: 1px solid #aaa;
+ border-radius: 5px;
+ width: 100%;
+ }
+ ul.breadcrumb > li:before {
+ content: " / ";
+ }
+
This will (after we rebuild it) make the site looks different of course, and getting closer to our goal:
-.. figure:: http://ralsina.com.ar/galleries/random/monospace-3.png
+.. figure:: http://ralsina.com.ar/galleries/monospace-tut/monospace-3.png
:height: 400px
Monospaced allright.
-
+
If you compare it to `the original <http://wp-themes.com/monospace/>`_, however, you will see that the layout of
the posts themselves is different, and that was not described in ``base.tmpl`` at all. But if you look, you'll see that
there is a placeholder called content: ``<%block name="content"></%block>``
That's because ``base.tmpl`` defines the *base* layout. The layout of more specific pages, like "the page that shows
-a list of posts" is defined in the other templates. Specifically, this is defined in ``index.tmpl``:
+a list of posts" is defined in the other templates. Specifically, this is defined in ``index.tmpl``.
+It turns out ``bootstrap`` doesn' have one of those! That's because it inherits that template from ``base``:
.. code-block:: mako
## -*- coding: utf-8 -*-
<%namespace name="helper" file="index_helper.tmpl"/>
+ <%namespace name="comments" file="comments_helper.tmpl"/>
<%inherit file="base.tmpl"/>
<%block name="content">
% for post in posts:
- <div class="post">
- <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a>
+ <div class="postbox">
+ <h1><a href="${post.permalink()}">${post.title()}</a>
<small>&nbsp;&nbsp;
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
-
+ ${messages("Posted")}: <time class="published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
</small></h1>
<hr>
- ${post.text(lang, index_teasers)}
- ${helper.html_disqus_link(post)}
+ ${post.text(teaser_only=index_teasers)}
+ % if not post.meta('nocomments'):
+ ${comments.comment_link(post.permalink(), post.base_path)}
+ % endif
</div>
% endfor
${helper.html_pager()}
- ${helper.html_disqus_script()}
+ ${comments.comment_link_script()}
+ ${helper.mathjax_script(posts)}
</%block>
-So, let's tweak that to be closer to the original. We put the post's metadata in a
+So, let's tweak that to be closer to the original. We put the post's metadata in a
box, add links for the posts tags, move the date there, etc.
.. code-block:: mako
@@ -216,53 +292,65 @@ box, add links for the posts tags, move the date there, etc.
<%block name="content">
% for post in posts:
<div class="postbox">
- <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a></h1>
+ <h1><a href="${post.permalink()}">${post.title()}</a></h1>
<div class="meta" style="background-color: rgb(234, 234, 234); ">
<span class="authordate">
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
+ ${messages("Posted")}: ${post.formatted_date(date_format)}
</span>
<br>
<span class="tags">Tags:&nbsp;
%if post.tags:
%for tag in post.tags:
- <a class="tag" href="${_link('tag', tag, lang)}"><span class="badge badge-info">${tag}</span></a>
+ <a class="tag" href="${_link('tag', tag)}"><span>${tag}</span></a>
%endfor
%endif
</span>
</div>
- ${post.text(lang, index_teasers)}
- ${disqus.html_disqus_link(post.permalink()+"#disqus_thread", post.base_path)}
+ ${post.text(teaser_only=index_teasers)}
+ % if not post.meta('nocomments'):
+ ${disqus.html_disqus_link(post.permalink()+"#disqus_thread", post.base_path)}
+ % endif
</div>
% endfor
${helper.html_pager()}
${disqus.html_disqus_script()}
</%block>
-.. figure:: http://ralsina.com.ar/galleries/random/monospace-4.png
+
+.. figure:: http://ralsina.com.ar/galleries/monospace-tut/monospace-4.png
:height: 400px
Close enough!
-
+
Then if we click on the post title, we will see some broken details in the metadata that can be fixed in ``post.tmpl``, and so on.
-
+
.. code-block:: mako
## -*- coding: utf-8 -*-
<%namespace name="helper" file="post_helper.tmpl"/>
<%namespace name="disqus" file="disqus_helper.tmpl"/>
<%inherit file="base.tmpl"/>
+ <%block name="extra_head">
+ ${helper.twitter_card_information(post)}
+ % if post.meta('keywords'):
+ <meta name="keywords" content="${post.meta('keywords')|h}"/>
+ % endif
+ </%block>
<%block name="content">
<div class="post">
${helper.html_title()}
<div class="meta" style="background-color: rgb(234, 234, 234); ">
<span class="authordate">
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)} [<a href="${post.pagenames[lang]+'.txt'}">${messages[lang]["Source"]}</a>]
+ ${messages("Posted")}: ${post.formatted_date(date_format)}
+ % if not post.meta('password'):
+ [<a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a>]
+ % endif
</span>
<br>
%if post.tags:
- <span class="tags">${messages[lang]["Tags"]}:&nbsp;
+ <span class="tags">${messages("Tags")}:&nbsp;
%for tag in post.tags:
- <a class="tag" href="${_link('tag', tag, lang)}"><span class="badge badge-info">${tag}</span></a>
+ <a class="tag" href="${_link('tag', tag)}"><span>${tag}</span></a>
%endfor
</span>
<br>
@@ -271,17 +359,20 @@ Then if we click on the post title, we will see some broken details in the metad
${helper.html_translations(post)}
</span>
</div>
- ${post.text(lang)}
+ ${post.text()}
${helper.html_pager(post)}
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
+ % if not post.meta('nocomments'):
+ ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
+ % endif
</div>
</%block>
-.. figure:: http://ralsina.com.ar/galleries/random/monospace-5.png
+
+.. figure:: http://ralsina.com.ar/galleries/monospace-tut/monospace-5.png
:height: 400px
Details, details.
-
+
The demo site exercises most of the features in Nikola, so if you make it look good, your site probably will look good too.
This monospace theme is included with nikola, if you want to use it or play with it.
-
+
diff --git a/docs/extending.txt b/docs/extending.txt
index 750ec98..f5c7aae 100644
--- a/docs/extending.txt
+++ b/docs/extending.txt
@@ -1,7 +1,14 @@
+.. title: Extending Nikola
+.. slug: extending
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
Extending Nikola
================
-:Version: 5
+:Version: 6.2.1
:Author: Roberto Alsina <ralsina@netmanagers.com.ar>
.. class:: alert alert-info pull-right
@@ -9,11 +16,6 @@ Extending Nikola
.. contents::
-.. note:: This is a draft
-
- I am not sure of the best way to do some things, including how
- to document this. Suggestions are welcome.
-
Nikola is extensible. Almost all its functionality is based on plugins,
and you can add your own or replace the provided ones.
@@ -32,20 +34,42 @@ Command Plugins
When you run ``nikola --help`` you will see something like this::
- $ nikola --help
- Usage: nikola command [options]
+ $ nikola help
+ Nikola is a tool to create static websites and blogs. For full documentation and more
+ information, please visit http://getnikola.com
- Available commands:
- nikola bootswatch_theme: Given a swatch name and a parent theme, creates a custom theme.
- nikola build: Build the site.
- nikola import_wordpress: Import a wordpress site from a XML dump.
- nikola init: Create a new site.
- nikola install_theme: Install a theme into the current site.
- nikola new_post: Create a new post.
- nikola serve: Start test server.
-
- For detailed help for a command, use nikola command --help
+ Available commands:
+ nikola auto automatically detect site changes, rebuild
+ and optionally refresh a browser
+ nikola bootswatch_theme given a swatch name from bootswatch.com and a
+ parent theme, creates a custom theme
+ nikola build run tasks
+ nikola check check links and files in the generated site
+ nikola clean clean action / remove targets
+ nikola console start an interactive python console with access to
+ your site and configuration
+ nikola deploy deploy the site
+ nikola dumpdb dump dependency DB
+ nikola forget clear successful run status from internal DB
+ nikola help show help
+ nikola ignore ignore task (skip) on subsequent runs
+ nikola import_blogger import a blogger dump
+ nikola import_feed import a RSS/Atom dump
+ nikola import_wordpress import a WordPress dump
+ nikola init create a Nikola site in the specified folder
+ nikola install_theme install theme into current site
+ nikola list list tasks from dodo file
+ nikola mincss apply mincss to the generated site
+ nikola new_post create a new blog post or site page
+ nikola run run tasks
+ nikola serve start the test webserver
+ nikola strace use strace to list file_deps and targets
+ nikola version print the Nikola version number
+
+ nikola help show help / reference
+ nikola help <command> show command usage
+ nikola help <task-name> show task usage
That will give you a list of all available commands in your version of Nikola.
Each and every one of those is a plugin. Let's look at a typical example:
@@ -56,17 +80,22 @@ First, the ``command_serve.plugin`` file:
[Core]
Name = serve
- Module = command_serve
+ Module = serve
[Documentation]
Author = Roberto Alsina
Version = 0.1
- Website = http://nikola.ralsina.com.ar
+ Website = http://getnikola.com
Description = Start test server.
+.. note:: If you want to publish your plugin on the Plugin Index, `read
+ the docs for the Index
+ <https://github.com/getnikola/plugins/blob/master/README.md>`__
+ (and the .plugin file examples and explanations).
+
For your own plugin, just change the values in a sensible way. The
``Module`` will be used to find the matching python module, in this case
-``command_serve.py``, from which this is the interesting bit:
+``serve.py``, from which this is the interesting bit:
.. code-block:: python
@@ -78,57 +107,52 @@ For your own plugin, just change the values in a sensible way. The
class CommandBuild(Command):
"""Start test server."""
- # This has to match the Name option in the .plugin file
-
name = "serve"
-
- # This is the function that does stuff
-
- def run(self, *args):
+ doc_usage = "[options]"
+ doc_purpose = "start the test webserver"
+
+ cmd_options = (
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ {
+ 'name': 'address',
+ 'short': 'a',
+ 'long': '--address',
+ 'type': str,
+ 'default': '127.0.0.1',
+ 'help': 'Address to bind (default: 127.0.0.1)',
+ },
+ )
+
+ def _execute(self, options, args):
"""Start test server."""
-
- # use OptionParser if you want your command to have options
-
- parser = OptionParser(usage="nikola %s [options]" % self.name)
- parser.add_option("-p", "--port", dest="port",
- help="Port numer (default: 8000)", default=8000,
- type="int")
- parser.add_option("-a", "--address", dest="address",
- help="Address to bind (default: 127.0.0.1)",
- default='127.0.0.1')
- (options, args) = parser.parse_args(list(args))
-
- # You can use self.site.config to access your
- # configuration options. self.site is an instance
- # of the Nikola class and contains all your site's
- # data.
-
out_dir = self.site.config['OUTPUT_FOLDER']
-
- # Then do something interesting. In this case,
- # it starts a webserver
-
if not os.path.isdir(out_dir):
- print "Error: Missing '%s' folder?" % out_dir
+ print("Error: Missing '{0}' folder?".format(out_dir))
else:
os.chdir(out_dir)
- httpd = HTTPServer((options.address, options.port),
- OurHTTPRequestHandler)
+ httpd = HTTPServer((options['address'], options['port']),
+ OurHTTPRequestHandler)
sa = httpd.socket.getsockname()
- print "Serving HTTP on", sa[0], "port", sa[1], "..."
+ print("Serving HTTP on", sa[0], "port", sa[1], "...")
httpd.serve_forever()
As mentioned above, a plugin can have options, which the user can see by doing
-``nikola command --help`` and can later use as ``nikola command --option``::
+``nikola help command`` and can later use, for example::
- $ nikola serve --help
- Usage: nikola serve [options]
+ $ nikola help serve
+ Purpose: start the test webserver
+ Usage: nikola serve [options]
Options:
- -h, --help show this help message and exit
- -p PORT, --port=PORT Port numer (default: 8000)
- -a ADDRESS, --address=ADDRESS
- Address to bind (default: 127.0.0.1)
+ -p ARG, --port=ARG Port nummber (default: 8000)
+ -a ARG, ----address=ARG Address to bind (default: 127.0.0.1)
$ nikola serve -p 9000
Serving HTTP on 127.0.0.1 port 9000 ...
@@ -148,14 +172,19 @@ First, you have to create a .plugin file. Here's the one for the Mako plugin:
[Core]
Name = mako
- Module = template_mako
+ Module = mako
[Documentation]
Author = Roberto Alsina
Version = 0.1
- Website = http://nikola.ralsina.com.ar
+ Website = http://getnikola.com
Description = Support for Mako templates.
+.. note:: If you want to publish your plugin on the Plugin Index, `read
+ the docs for the Index
+ <https://github.com/getnikola/plugins/blob/master/README.md>`__
+ (and the .plugin file examples and explanations).
+
You will have to replace "mako" with your template system's name, and other data
in the obvious ways.
@@ -187,13 +216,13 @@ a stub for a hypothetical system called "Templater":
# template loading tool that can use this.
def set_directories(self, directories):
- """Createa template lookup."""
+ """Create a template lookup."""
pass
# The method that does the actual rendering.
# template_name is the name of the template file,
# output_name is the file for the output, context
- # is a dictionary containing the data the template
+ # is a dictionary containing the data the template
# uses for rendering.
def render_template(self, template_name, output_name,
@@ -208,17 +237,27 @@ Task Plugins
If you want to do something that depends on the data in your site, you
probably want to do a Task plugin, which will make it be part of the
``nikola build`` command. There are the currently available tasks, all
-provided by plugins::
+provided by plugins:
- $ nikola list
+.. sidebar:: Other Tasks
+
+ There are also ``LateTask`` plugins, which are executed later,
+ and ``TaskMultiplier`` plugins that take a task and create
+ more tasks out of it.
+::
+
+ $ nikola list
+ Scanning posts....done!
build_bundles
+ build_less
copy_assets
copy_files
- deploy
+ post_render
redirect
render_archive
render_galleries
+ render_galleries_clean
render_indexes
render_listings
render_pages
@@ -249,9 +288,15 @@ in the logical ways:
[Documentation]
Author = Roberto Alsina
Version = 0.1
- Website = http://nikola.ralsina.com.ar
+ Website = http://getnikola.com
Description = Copy theme assets into output.
+
+.. note:: If you want to publish your plugin on the Plugin Index, `read
+ the docs for the Index
+ <https://github.com/getnikola/plugins/blob/master/README.md>`_
+ (and the .plugin file examples and explanations).
+
And the ``task_copy_assets.py`` file, in its entirety:
.. code-block:: python
@@ -299,3 +344,93 @@ And the ``task_copy_assets.py`` file, in its entirety:
task['basename'] = self.name
# If your task generates files, please do this.
yield utils.apply_filters(task, kw['filters'])
+
+PageCompiler Plugins
+--------------------
+
+These plugins implement markup languages, they take sources for posts or pages and
+create HTML or other output files. A good example is the ``misaka`` plugin.
+
+They must provide:
+
+``compile_html``
+ Function that builds a file.
+
+``create_post``
+ Function that creates an empty file with some metadata in it.
+
+If the compiler produces something other than HTML files, it should also implement ``extension`` which
+returns the preferred extension for the output file.
+
+RestExtension Plugins
+---------------------
+
+Implement directives for reStructuredText, see ``media.py`` for a simple example.
+
+SignalHandler Plugins
+---------------------
+
+These plugins extend the ``SignalHandler`` class and connect to one or more
+signals via `blinker <http://pythonhosted.org/blinker/>`_
+
+The easiest way to do this is to reimplement ``set_site()`` and just connect to
+whatever signals you want there.
+
+Currently Nikola emits the following signals:
+
+``sighandlers_loaded``
+ Right after SignalHandler plugin activation.
+``initialized``
+ Right after plugin activation
+``configured``
+ When all the configuration file is processed. Note that plugins are activated before this is emitted.
+``new_post``
+ When a new post is created, using the ``nikola new_post`` command. The signal
+ data contains the path of the file, and the metadata file (if there is one).
+``deployed``
+ When the ``nikola deploy`` command is run, and there is at least one new
+ entry/post since ``last_deploy``. The signal data is of the form ::
+
+ {
+ 'last_deploy: # datetime object for the last deployed time,
+ 'new_deploy': # datetime object for the current deployed time,
+ 'clean': # whether there was a record of a last deployment,
+ 'deployed': # all files deployed after the last deploy,
+ 'undeployed': # all files not deployed since they are either future posts/drafts
+ }
+
+
+Plugin Index
+============
+
+There is a `plugin index <http://plugins.getnikola.com/>`__, which stores all
+of the plugins for Nikola people wanted to share with the world.
+
+You may want to read the `README for the Index
+<https://github.com/getnikola/plugins/blob/master/README.md>`_ if you want to
+publish your package there.
+
+Path/Link Resolution Mechanism
+==============================
+
+Any plugin can register a function using ``Nikola.register_path_handler`` to
+allow resolution of paths and links. These are useful for templates, which
+can access them via _link.
+
+For example, you can always get a link to the path for the feed of the "foo" tag
+by using ``_link('tag_rss', 'foo')`` or the ``link://tag_rss/foo`` URL.
+
+Here's the relevant code from the tag plugin.
+
+.. code-block:: python
+
+ # In set_site
+ site.register_path_handler('tag_rss', self.tag_rss_path)
+
+ # And these always take name and lang as arguments and returl a list of
+ # path elements.
+ def tag_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_name(name) + ".xml"] if
+ _f]
+
diff --git a/docs/internals.txt b/docs/internals.txt
index 7a38f27..14de433 100644
--- a/docs/internals.txt
+++ b/docs/internals.txt
@@ -1,3 +1,10 @@
+.. title: Nikola Internals
+.. slug: internals
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
Nikola Internals
================
@@ -16,14 +23,15 @@ Nikola is a Pile of Plugins
an existing one.
There are several kinds of plugins, all implementing interfaces defined in
- ``nikola/plugin_categories.py``.
+ ``nikola/plugin_categories.py`` and documented in
+ `Extending Nikola <http://getnikola.com/extending.html>`_
If your plugin has a dependency, please make sure it doesn't make Nikola
throw an exception when the dependency is missing. Try to fail gracefully
with an informative message.
Commands are plugins
- When you use ``nikola foo`` you are using the plugin ``command_foo``. Those are
+ When you use ``nikola foo`` you are using the plugin ``command/foo``. Those are
used to extend Nikola's command line. Their interface is defined in the ``Command``
class. They take options and arguments and do whatever you want, so go wild.
@@ -36,7 +44,7 @@ The Build Command
Nikola's goal is similar, deep at heart, to a Makefile. Take sources, compile them
into something, in this case a website. Instead of a Makefile, Nikola uses
-`doit <http://pydoit.com>`_
+`doit <http://pydoit.org>`_
Doit has the concept of "tasks". The 1 minute summary of tasks is that they have:
@@ -77,8 +85,8 @@ one of the existing ones.
You can do doctests, you can do unit tests, you can do integration tests. There is support
for all of them.
-Post and Stories
-----------------
+Posts and Stories
+-----------------
Nikola has a concept of posts and stories. Both are more or less the same thing, except
posts are added into RSS feeds and stories are not. All of them are in a list called
@@ -100,17 +108,19 @@ the timeline, because it will cause consistency issues.
Your plugin can use the timeline to generate "stuff" (technical term). For example,
Nikola comes with plugins that use the timeline to create a website (surprised?).
-The workflow included with nikola is as follows:
+The workflow included with nikola is as follows (incomplete!):
-#. The post is assigned a compiler based on its extension and the ``post_compilers`` option.
+#. The post is assigned a compiler based on its extension and the ``COMPILERS`` option.
#. The compiler is applied to the post data and a "HTML fragment" is produced. That
- fragment is stored in a cache (the ``render_posts`` plugin).
+ fragment is stored in a cache (the ``posts`` plugin).
#. The configured theme has templates (and a template engine), which are applied to the post's
- HTML fragment and metadata (the ``render_pages`` plugin).
-#. The original sources for the post are copied to some accessible place (the ``render_sources`` plugin)
-#. If the post is tagged, some pages and RSS feeds for each tag are updated (the ``render_tags`` plugin)
-#. If the post is new, it's included in the blog's RSS feed (the ``render_rss`` plugin)
-#. The post is added in the right place in the index pages for the blog (the ``task_indexes`` plugin)
+ HTML fragment and metadata (the ``pages`` plugin).
+#. The original sources for the post are copied to some accessible place (the ``sources`` plugin)
+#. If the post is tagged, some pages and RSS feeds for each tag are updated (the ``tags`` plugin)
+#. If the post is new, it's included in the blog's RSS feed (the ``rss`` plugin)
+#. The post is added in the right place in the index pages for the blog (the ``indexes`` plugin)
+#. CSS/JS/Images for the theme are put in the right places (the ``copy_assets`` and ``bundles`` plugins)
+#. A File describing the whole site is created (the ``sitemap`` plugin)
You can add whatever you want to that list: just create a plugin for it.
@@ -128,4 +138,3 @@ themes
To change how the generated site looks, you can create custom themes.
And of course, you can also replace or extend each of the existing plugins.
-
diff --git a/docs/man/nikola.1 b/docs/man/nikola.1
index 8f77039..9385a2a 100644
--- a/docs/man/nikola.1
+++ b/docs/man/nikola.1
@@ -1,60 +1,88 @@
-======
-nikola
-======
-
---------------------------------
-A Static Site and Blog Generator
---------------------------------
-
-:Manual section: 1
-:Manual group: nikola
-
-SYNOPSIS
-========
-
-**nikola** command [*options*]
-
-
-DESCRIPTION
-===========
-
-nikola
- is a simple yet powerful and flexible static website and blog generator
-
-OPTIONS
-=======
-
-bootswatch_theme
- Given a swatch name and a parent theme, creates a custom theme.
-
-build
- Build the site.
-
-check
- Check the generated site
-
-deploy
- Deploy the site
-
-import_wordpress
- Import a wordpress site from a XML dump (requires markdown).
-
-init
- Create a new site.
-
-install_theme
- Install a theme into the current site.
-
-new_post
- Create a new post.
-
-serve
- Start test server.
-
-command --help
- Get detailed help for a command
-
-AUTHOR
-======
-
-Roberto Alsina <http://ralsina.com.ar/>
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.43.3.
+.TH NIKOLA "1" "November 2013" "nikola 6.2.1" "User Commands"
+.SH NAME
+nikola \- manual page for nikola 6.2.1
+.SH DESCRIPTION
+Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com
+.SS "Available commands:"
+.TP
+nikola auto
+automatically detect site changes, rebuild and optionally refresh a browser
+.TP
+nikola bootswatch_theme
+given a swatch name from bootswatch.com and a parent theme, creates a custom theme
+.TP
+nikola build
+run tasks
+.TP
+nikola check
+check links and files in the generated site
+.TP
+nikola clean
+clean action / remove targets
+.TP
+nikola console
+Start an interactive Python (IPython\->bpython\->plain) console with access to your site and configuration
+.TP
+nikola deploy
+deploy the site
+.TP
+nikola dumpdb
+dump dependency DB
+.TP
+nikola forget
+clear successful run status from internal DB
+.TP
+nikola help
+show help
+.TP
+nikola ignore
+ignore task (skip) on subsequent runs
+.TP
+nikola import_blogger
+import a blogger dump
+.TP
+nikola import_feed
+import a RSS/Atom dump
+.TP
+nikola import_wordpress
+import a WordPress dump
+.TP
+nikola init
+create a Nikola site in the specified folder
+.TP
+nikola install_plugin
+install plugin into current site
+.TP
+nikola install_theme
+install theme into current site
+.TP
+nikola list
+list tasks from dodo file
+.TP
+nikola mincss
+apply mincss to the generated site
+.TP
+nikola new_post
+create a new blog post or site page
+.TP
+nikola run
+run tasks
+.TP
+nikola serve
+start the test webserver
+.TP
+nikola strace
+use strace to list file_deps and targets
+.TP
+nikola version
+print the Nikola version number
+.TP
+nikola help
+show help / reference
+.TP
+nikola help <command>
+show command usage
+.TP
+nikola help <task\-name>
+show task usage
diff --git a/docs/manual.txt b/docs/manual.txt
index 4833eae..c186260 100644
--- a/docs/manual.txt
+++ b/docs/manual.txt
@@ -1,7 +1,14 @@
+.. title: The Nikola Handbook
+.. slug: handbook
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
The Nikola Handbook
===================
-:Version: 5.4.4
+:Version: 6.2.1
.. class:: alert alert-info pull-right
@@ -11,7 +18,7 @@ The Nikola Handbook
All You Need to Know
--------------------
-After you have Nikola installed:
+After you have Nikola `installed <#installing-nikola>`_:
Create a empty site:
``nikola init mysite``
@@ -57,10 +64,10 @@ instead of something the user generates.
Nikola can do:
-* A blog (`example <http://lateral.netmanagers.com.ar>`__)
+* A blog (`example <http://ralsina.me>`__)
* Your company's site
* Your personal site
-* A software project's site (`example <http://nikola.ralsina.com.ar>`__)
+* A software project's site (`example <http://getnikola.com>`__)
* A book's site
Since Nikola-based sites don't run any code on the server, there is no way to process
@@ -71,7 +78,7 @@ Nikola can't do:
* Twitter
* Facebook
* An Issue tracker
-* Anything with forms, really (except for comments_!)
+* Anything with forms, really (except for `comments <#comments-and-annotations>`_!)
Keep in mind that "static" doesn't mean **boring**. You can have animations, slides
or whatever fancy CSS/HTML5 thingie you like. It only means all that HTML is
@@ -83,10 +90,10 @@ Getting Help
------------
* Feel free to contact me at ralsina@netmanagers.com.ar for questions about Nikola.
-* You can file bugs at `the issue tracker <https://github.com/ralsina/nikola-site/issues>`__
+* You can file bugs at `the issue tracker <https://github.com/getnikola/nikola/issues>`__
* You can discuss Nikola at the `nikola-discuss google group <http://groups.google.com/group/nikola-discuss>`_
-* You can subscribe to `the Nikola Blog <http://nikola.ralsina.com.ar/blog>`_
-* You can follow `Nikola on Twitter <https://twitter.com/#!/nikolagenerator>`_
+* You can subscribe to `the Nikola Blog <http://getnikola.com/blog>`_
+* You can follow `Nikola on Twitter <https://twitter.com/nikolagenerator>`_
Why Static?
-----------
@@ -106,13 +113,13 @@ Security
Dynamic sites are prone to experience security issues. The solution for that
is constant vigilance, keeping the software behind the site updated, and
plain old good luck. The stack of software used to provide a static site,
- like those Nikola generates, is much smaller (Just a webserver).
+ like those Nikola generates, is much smaller (Just a web server).
A smaller software stack implies less security risk.
Obsolescense
- If you create a site using (for example) Wordpress, what happens when Wordpress
- releases a new version? You have to update your Wordpress. That is not optional,
+ If you create a site using (for example) WordPress, what happens when WordPress
+ releases a new version? You have to update your WordPress. That is not optional,
because of security and support issues. If I release a new version of Nikola, and
you don't update, *nothing* happens. You can continue to use the version you
have now forever, no problems.
@@ -142,7 +149,7 @@ Cost and Performance
Lockin
On server-side blog platforms, sometimes you can't export your own data, or
it's in strange formats you can't use in other services. I have switched
- blogging platforms from Advogato to PyCs to two homebrewed systems, to Nikola,
+ blogging platforms from Advogato to PyCs to two homebrew systems, to Nikola,
and have never lost a file, a URL, or a comment. That's because I have *always*
had my own data in a format of my choice.
@@ -151,7 +158,7 @@ Lockin
Features
--------
-Nikola has a very defined featureset: it has every feature I needed for my own sites.
+Nikola has a very defined feature set: it has every feature I needed for my own sites.
Hopefully, it will be enough for others, and anyway, I am open to suggestions.
If you want to create a blog or a site, Nikola provides:
@@ -168,7 +175,7 @@ If you want to create a blog or a site, Nikola provides:
* Google sitemap generation
* Custom deployment (if it's a command, you can use it)
* A (very) basic look and feel you can customize, and is even text-mode friendly
-* The input format is light markup (`reStructuredText <quickstart.html>`_ or
+* The input format is light markup (`reStructuredText <quickstart.html>`__ or
`Markdown <http://daringfireball.net/projects/markdown/>`_)
* Easy-to-create image galleries
* Support for displaying source code
@@ -177,7 +184,7 @@ If you want to create a blog or a site, Nikola provides:
Also:
-* A preview webserver
+* A preview web server
* "Live" re-rendering while you edit
* "Smart" builds: only what changed gets rebuilt (usually in seconds)
* Easy to extend with minimal Python knowledge.
@@ -189,28 +196,40 @@ This is currently lacking on detail. Considering the niche Nikola is aimed at,
I suspect that's not a problem yet. So, when I say "get", the specific details
of how to "get" something for your specific operating system are left to you.
-The short version is: ``pip install https://github.com/ralsina/nikola/archive/master.zip``
+The short version is: ``pip install nikola``
+
+Note that you need Python v2.6 or newer OR v3.3 or newer.
+
+For some features it may give you an error message telling you that you need to
+install something else. For example, if it tells you you need ``requests``::
+
+ pip install requests
+
+And so on.
Longer version:
-#. Get `Nikola <http://nikola.ralsina.com.ar/>`_
+#. Get `Nikola <http://getnikola.com/>`_
#. Install dependencies. To do that, either:
- #. ``pip install -r requirements.txt`` and ``pip install .`` or...
+ #. ``pip install -r requirements.txt`` (or ``requirements-full.txt``
+ for extra stuff) and ``pip install .`` or...
#. Install your distribution's packages for all the things
mentioned below, if they exist, or...
#. Get all of these manually (but why?, use pip):
- #. Get python, if you don't have it.
+ #. Get Python, if you don't have it.
#. Get `doit <http://pydoit.org>`_
#. Get `docutils <http://docutils.sf.net>`_
#. Get `Mako <http://makotemplates.org>`_
- #. Get `PIL <http://www.pythonware.com/products/pil/>`_ (or Pillow)
+ #. Get `Pillow <http://python-imaging.github.io/>`_
#. Get `Pygments <http://pygments.org/>`_
#. Get `unidecode <http://pypi.python.org/pypi/Unidecode/>`_
#. Get `lxml <http://lxml.de/>`_
#. Get `yapsy <http://yapsy.sourceforge.net>`_
- #. Get `configparser <http://pypi.python.org/pypi/configparser/3.2.0r3>`_
+ #. Get `PyRSS2Gen <http://www.dalkescientific.com/Python/PyRSS2Gen.html>`_
+ #. Get `pytz <http://pytz.sourceforge.net/>`_
+ #. If using Python 2, get `configparser <http://pypi.python.org/pypi/configparser/3.2.0r3>`_
#. run ``python setup.py install``
@@ -222,33 +241,65 @@ Nikola is packaged for some Linux distributions, you may get that instead.
*NOTE*: If you get a ``ERROR: /bin/sh: 1: xslt-config: not found`` or ``fatal error:
libxml/xmlversion.h: No such file or directory`` when running ``pip install -r requirements.txt``, install *libxml* and *libxslt* libraries, like so:
-Debian systems:
+Debian systems::
sudo apt-get install libxml2-dev
sudo apt-get install libxslt1-dev
-RHEL systems:
+Red Hat/RPM based systems::
yum install libxslt-devel libxml2-devel
+Installation on Linux, Mac OS X, \*BSD, and any other POSIX-compatible OS
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+(we obviously support all.)
+
+Using ``pip`` should suffice. You may also want to use distribution- or
+system-specific packages for our dependencies.
+
+There are **no known issues or caveats** on those OSes. Keep in mind that most
+of our developers run Linux on a daily basis and may not have the full
+knowledge required to resolve issues relating to your operating system.
+
+Installation on Windows and Windows support
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Nikola supports Windows! Keep in mind, though, that there are some
+caveats:
+
+1. ``lxml`` and ``Pillow`` require compiled extensions. Compiling them on
+Windows is hard for most people. Fortunately, compiled packages exist. Check
+their `PyPI <https://pypi.python.org/>`_ pages to find official packages, `the unofficial Gohlke binaries <http://www.lfd.uci.edu/~gohlke/pythonlibs/>`_ site, or get them somewhere else. If you are using virtualenvs, using those pre-built packages is possible through ``virtualenv --system-site-packages``.
+2. Windows has some differences over POSIX, which may cause some features to work incorrectly under Windows. If any problems occur, please do not hesitate to report them. Some of the differeces include:
+
+ * ``\`` as path separator (instead of ``/``)
+ * the concept of partitions
+ * some characters in paths are disallowed (although this shouldn’t cause
+ problems)
+ * CR+LF (aka ``\r\n``) as the line separator (instead of LF ``\n``)
+
+3. Windows also dislikes some characters in paths.
+4. Most of our developers run Linux on a daily basis and may not have the full knowledge required to resolve issues relating to Windows.
+
Getting Started
---------------
To create posts and pages in Nikola, you write them in one of the supported input formats.
Those source files are later converted to HTML
-The recommended formats are restructured text and Markdown, but there is also support
+The recommended formats are reStructuredText and Markdown, but there is also support
for textile and WikiCreole and even for just writing HTML.
-.. note:: There is a great `quick tutorial to learn restructured text. <quickstart.html>`_
+.. note:: There is a great `quick tutorial to learn reStructuredText. <quickstart.html>`__
First, let's see how you "build" your site. Nikola comes with a minimal site to get you started.
-The tool used to do builds is called `doit <http://pydoit.org>`_, and it rebuilds the
+The tool used to do builds is called `doit <http://pydoit.org>`__, and it rebuilds the
files that are not up to date, so your site always reflects your latest content. To do our
first build, just run "nikola build"::
$ nikola build
- Scanning posts . . done!
+ Scanning posts....done!
. render_posts:stories/manual.html
. render_posts:posts/1.html
. render_posts:stories/1.html
@@ -260,17 +311,17 @@ first build, just run "nikola build"::
. render_pages:output/stories/handbook.html
. render_rss:output/rss.xml
. render_sources:output/stories/about-nikola.txt
- :
- :
- :
+ ⋮
+ ⋮
+ ⋮
Nikola will print a line for every output file it generates. If we do it again, that
will be much much shorter::
$ nikola build
- Scanning posts . . done!
+ Scanning posts....done!
-That is because `doit <http://pydoit.org>`_ is smart enough not to generate
+That is because `doit <http://pydoit.org>`__ is smart enough not to generate
all the pages again, unless you changed something that the page requires. So, if you change
the text of a post, or its title, that post page, and all index pages where it is mentioned,
will be recreated. If you change the post page template, then all the post pages will be rebuilt.
@@ -278,7 +329,7 @@ will be recreated. If you change the post page template, then all the post pages
Nikola is mostly a series of doit *tasks*, and you can see them by doing ``nikola list``::
$ nikola list
- Scanning posts . . done!
+ Scanning posts....done!
build_bundles
copy_assets
copy_files
@@ -296,39 +347,46 @@ Nikola is mostly a series of doit *tasks*, and you can see them by doing ``nikol
render_tags
sitemap
-You can make Nikola redo everything by calling ``nikola build forget``, you can make it do just a specific
-part of the site using task names, for example ``nikola build render_pages``, and even individual files like
-``nikola build render_indexes:output/index.html``
+You can make Nikola redo everything by calling ``nikola forget`` and then ``nikola build`` (or ``nikola build -a``,
+you can make it do just a specific part of the site using task names, for example ``nikola build render_pages``,
+and even individual files like ``nikola build output/index.html``
Nikola also has other commands besides ``build``::
$ nikola help
- Nikola
- Available commands:
- nikola auto automatically execute tasks when a dependency changes
- nikola bootswatch_theme Given a swatch name and a parent theme, creates a custom theme.
- nikola build run tasks
- nikola check Check links and files in the generated site.
- nikola clean clean action / remove targets
- nikola console A short explanation.
- nikola deploy Deploy the site.
- nikola dumpdb dump dependency DB
- nikola forget clear successful run status from internal DB
- nikola help show help
- nikola ignore ignore task (skip) on subsequent runs
- nikola import_blogger Import a blogger dump.
- nikola import_wordpress Import a wordpress dump.
- nikola init Create a Nikola site in the specified folder.
- nikola install_theme Install theme into current site.
- nikola list list tasks from dodo file
- nikola new_post Create a new blog post or site page.
- nikola run run tasks
- nikola serve Start the test webserver.
- nikola strace use strace to list file_deps and targets
-
- nikola help show help / reference
- nikola help <command> show command usage
- nikola help <task-name> show task usage
+ Nikola is a tool to create static websites and blogs. For full documentation and more information,
+ please visit http://getnikola.com
+
+
+ Available commands:
+ nikola auto automatically detect site changes, rebuild and optionally refresh a browser
+ nikola bootswatch_theme given a swatch name from bootswatch.com and a parent theme, creates a custom theme
+ nikola build run tasks
+ nikola check check links and files in the generated site
+ nikola clean clean action / remove targets
+ nikola console start an interactive python console with access to your site and configuration
+ nikola deploy deploy the site
+ nikola dumpdb dump dependency DB
+ nikola forget clear successful run status from internal DB
+ nikola help show help
+ nikola ignore ignore task (skip) on subsequent runs
+ nikola import_blogger import a blogger dump
+ nikola import_feed import a RSS/Atom dump
+ nikola import_wordpress import a WordPress dump
+ nikola init create a Nikola site in the specified folder
+ nikola install_theme install theme into current site
+ nikola list list tasks from dodo file
+ nikola mincss apply mincss to the generated site
+ nikola new_post create a new blog post or site page
+ nikola run run tasks
+ nikola serve start the test webserver
+ nikola strace use strace to list file_deps and targets
+ nikola version print the Nikola version number
+
+ nikola help show help / reference
+ nikola help <command> show command usage
+ nikola help <task-name> show task usage
+
The ``serve`` command starts a web server so you can see the site you are creating::
@@ -340,7 +398,7 @@ After you do this, you can point your web browser to http://localhost:8000 and y
the sample site. This is useful as a "preview" of your work.
By default, the ``serve`` command runs the web server on port 8000 on the IP address 127.0.0.1.
-You can pass in an IP address and port number explicity using ``-a IP_ADDRESS``
+You can pass in an IP address and port number explicitly using ``-a IP_ADDRESS``
(short version of ``--address``) or ``-p PORT_NUMBER`` (short version of ``--port``)
Example usage::
@@ -358,14 +416,14 @@ By default, that file will contain also some extra information about your post (
It can be placed in a separate file by using the ``-2`` option, but it's generally
easier to keep it in a single location.
-The contents of your post have to be written (by default) in `restructured text <http://docutils.sf.net>`_
+The contents of your post have to be written (by default) in `reStructuredText <http://docutils.sf.net>`__
but you can use a lot of different markups using the ``-f`` option. Currently
Nikola supports bbcode, wiki, markdown, html, txt2tags and textile in addition
-to restructured text.
+to reStructuredText.
-You can control what markup compiler is used for each file extension with the ``post_compilers``
+You can control what markup compiler is used for each file extension with the ``COMPILERS``
option. The default configuration expects them to be placed in ``posts`` but that can be
-changed (see below, the post_pages option)
+changed (see below, the ``POSTS`` and ``PAGES`` options)
This is how it works::
@@ -387,7 +445,7 @@ The content of that file is as follows::
Write your post here.
-The ``slug`` is the pagename. Since often titles will have
+The ``slug`` is the page name. Since often titles will have
characters that look bad on URLs, it's generated as a "clean" version of the title.
The third line is the post's date, and is set to "now".
@@ -397,6 +455,15 @@ source for the content, and ``description`` is mostly useful for SEO.
You can add your own metadata fields in the same manner, if you use a theme that
supports them (for example: ``.. author: John Doe``)
+To add these metadata fields to all new posts by default, you can set the
+variable ``ADDITIONAL_METADATA`` in your configuration. For example, you can
+add the author metadata to all new posts by default, by adding the following
+to your configuration ::
+
+ ADDITIONAL_METADATA = {
+ 'author': 'John Doe'
+ }
+
.. sidebar:: Other Metadata Fields
Nikola will also use other metadata fields:
@@ -411,10 +478,28 @@ supports them (for example: ``.. author: John Doe``)
.. template: story.tmpl
+ That template needs to either be part of the theme, or be placed in a ``templates/``
+ folder inside your site.
+
password
The post will be encrypted and invisible until the reader enters the password.
Also, the post's sourcecode will not be available.
+ category
+ Like tags, except each post can have only one, and they usually have
+ more descriptive names.
+
+ annotations / noannotations
+ Override the value of the ``ANNOTATIONS`` option for this specific post or page.
+
+ author
+ Author of the post, will be used in the RSS feed and possibly in the post
+ display (theme-dependent)
+
+ hidetitle
+ Set "True" if you do not want to see the **story** title as a
+ heading of the page (does not work for posts).
+
.. note:: The Two-File Format
@@ -431,7 +516,7 @@ post file (for example: ``how-to-make-money.txt.es``). This one can replace
metadata of the default language, for example:
* The translated title for the post or page
-* A translated version of the pagename
+* A translated version of the page name
You can edit these files with your favourite text editor, and once you are happy
with the contents, generate the pages as explained in `Getting Started`_
@@ -450,19 +535,21 @@ Currently supported languages are
* Spanish
If you wish to add support for more languages, check out the instructions
-at the `theming guide <http://nikola.ralsina.com.ar/theming.html>`_.
+at the `theming guide <http://getnikola.com/theming.html>`_.
The post page is generated using the ``post.tmpl`` template, which you can use
to customize the output.
-The place where the post will be placed by ``new_post`` is based on the ``post_pages``
-configuration option::
+The place where the post will be placed by ``new_post`` is based on the ``POSTS``
+and ``PAGES`` configuration options::
- # post_pages contains (wildcard, destination, template, use_in_feed) tuples.
+ # POSTS and PAGES contains (wildcard, destination, template) tuples.
+ #
+ # The wildcard is used to generate a list of reSt source files
+ # (whatever/thing.txt).
#
- # The wildcard is used to generate a list of reSt source files (whatever/thing.txt)
- # That fragment must have an associated metadata file (whatever/thing.meta),
- # and opcionally translated files (example for spanish, with code "es"):
+ # That fragment could have an associated metadata file (whatever/thing.meta),
+ # and optionally translated files (example for spanish, with code "es"):
# whatever/thing.txt.es and whatever/thing.meta.es
#
# From those files, a set of HTML fragment files will be generated:
@@ -472,18 +559,24 @@ configuration option::
# pages, which will be placed at
# output / TRANSLATIONS[lang] / destination / pagename.html
#
- # where "pagename" is specified in the metadata file.
+ # where "pagename" is the "slug" specified in the metadata file.
#
- # if use_in_feed is True, then those posts will be added to the site's
- # rss feeds.
+ # The difference between POSTS and PAGES is that POSTS are added
+ # to feeds and are considered part of a blog, while PAGES are
+ # just independent HTML pages.
#
- post_pages = (
- ("posts/*.txt", "posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "story.tmpl", False),
+
+ POSTS = (
+ ("posts/*.txt", "posts", "post.tmpl"),
+ ("posts/*.rst", "posts", "post.tmpl"),
+ )
+ PAGES = (
+ ("stories/*.txt", "stories", "story.tmpl"),
+ ("stories/*.rst", "stories", "story.tmpl"),
)
-It will use the first location that has the last parameter set to True, or the last
-one in the list if all of them have it set to False.
+It will use the first location that has the last item in ``POSTS``, or the last
+one in ``PAGES`` if ``POSTS`` is empty.
The ``new_post`` command supports some options::
@@ -511,7 +604,7 @@ index page or in RSS feeds, but to display instead only the beginning of them.
If it's the case, you only need to add a "magical comment" in your post.
-In restructuredtext::
+In reStructuredText::
.. TEASER_END
@@ -530,12 +623,32 @@ change that text, you can use a custom teaser::
.. TEASER_END: click to read the rest of the article
+Or you can completely customize the link using the ``READ_MORE_LINK`` option::
+
+ # A HTML fragment with the Read more... link.
+ # The following tags exist and are replaced for you:
+ # {link} A link to the full post page.
+ # {read_more} The string “Read more” in the current language.
+ # {{ A literal { (U+007B LEFT CURLY BRACKET)
+ # }} A literal } (U+007D RIGHT CURLY BRACKET)
+ # READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
+
+
Drafts
~~~~~~
If you add a "draft" tag to a post, then it will not be shown in indexes and feeds.
It *will* be compiled, and if you deploy it it *will* be made available, so use
-with care.
+with care. If you wish your drafts to be not available in your deployed site, you
+can set ``DEPLOY_DRAFTS = False`` in your configuration.
+
+Also if a post has a date in the future, it will not be shown in indexes until
+you rebuild after that date. This behaviour can be disabled by setting
+``FUTURE_IS_NOW = True`` in your configuration, which will make future posts be
+published immediately. Posts dated in the future are *not* deployed by default
+(when ``FUTURE_IS_NOW = False``). To make future posts available in the
+deployed site, you can set ``DEPLOY_FUTURE = True`` in your configuration.
+Generally, you want FUTURE_IS_NOW and DEPLOY_FUTURE to be the same value.
Retired Posts
~~~~~~~~~~~~~
@@ -544,6 +657,57 @@ If you add a "retired" tag to a post, then it will not be shown in indexes and f
It *will* be compiled, and if you deploy it it *will* be made available, so it will
not generate 404s for people who had linked to it.
+Queuing Posts
+~~~~~~~~~~~~~
+
+Some blogs tend to have new posts based on a schedule (for example,
+every Mon, Wed, Fri) but the blog authors don't like to manually
+schedule their posts. You can schedule your blog posts based on a
+rule, by specifying a rule in the ``SCHEDULE_RULE`` in your
+configuration. You can either post specific blog posts according to
+this schedule by using the ``--schedule`` flag on the ``new_post``
+command or post all new posts according to this schedule by setting
+``SCHEDULE_ALL = True`` in your configuration. (Note: This feature
+requires that the ``FUTURE_IS_NOW`` setting is set to ``False``)
+
+For example, if you would like to schedule your posts to be on every
+Monday, Wednesday and Friday at 7am, add the following
+``SCHEDULE_RULE`` to your configuration ::
+
+ SCHEDULE_RULE = 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=7;BYMINUTE=0;BYSECOND=0'
+
+For more details on how to specify a recurrence rule, look at the
+`iCal specification <http://www.kanzaki.com/docs/ical/rrule.html>`_.
+
+Say, you get a free Sunday, and want to write a flurry of new posts,
+or at least posts for the rest of the week, you would run the
+``new_post`` command with the ``--schedule`` flag, as many times as
+you want::
+
+ $ nikola new_post --schedule
+ # Creates a new post to be posted on Monday, 7am.
+ $ nikola new_post -s
+ # Creates a new post to be posted on Wednesday, 7am.
+ $ nikola new_post -s
+ # Creates a new post to be posted on Friday, 7am.
+ .
+ .
+ .
+
+All these posts get queued up according to your schedule, but note
+that you will anyway need to build and deploy your site for the posts
+to appear online. You can have a cron job that does this regularly.
+
+An additional setting (``SCHEDULE_FORCE_TODAY = True``) lets you tell
+Nikola to make the post today, if you run the ``new_post --schedule``
+after the scheduled hour has passed, and there is no other post
+at/after the scheduled hour. Concretely, say, you run the ``nikola
+new_post -s`` command at 10am on a Monday (with the schedule rule set
+to the same as above), with no other post on Monday, at/after 7am,
+setting ``SCHEDULE_FORCE_TODAY = True`` will have your post scheduled
+to Monday, instead of being scheduled to Wednesday 7am.
+
+
Creating a Page
---------------
@@ -554,10 +718,10 @@ Pages are the same as posts, except that:
* They use the ``story.tmpl`` template instead of ``post.tmpl`` by default
The default configuration expects the page's metadata and text files to be on the
-``stories`` folder, but that can be changed (see post_pages option above).
+``stories`` folder, but that can be changed (see ``PAGES`` option above).
You can create the page's files manually or use the ``new_post`` command
-with the ``-p`` option, qhich will place the files in the folder that
+with the ``-p`` option, which will place the files in the folder that
has ``use_in_feed`` set to False.
Redirections
@@ -587,15 +751,13 @@ what Nikola does. Its syntax is python, but if you don't know the language, it
still should not be terribly hard to grasp.
The default ``conf.py`` you get with Nikola should be fairly complete, and is quite
-commented, but just in case, here is a full,
-`customized example configuration <sampleconfig.html>`_ (the one I use for
-`my site <http://lateral.netmanagers.com.ar>`_)
+commented.
You surely want to edit these options::
# Data about this site
BLOG_TITLE = "Demo Site"
- SITE_URL = "http://nikola.ralsina.com.ar"
+ SITE_URL = "http://getnikola.com"
BLOG_EMAIL = "joe@demo.site"
BLOG_DESCRIPTION = "This is a demo site for Nikola."
@@ -611,18 +773,35 @@ CSS tweaking
which is empty. Put your CSS there, for minimal disruption of the provided CSS files.
If you feel tempted to touch other files in assets, you probably will be better off
- with a `custom theme <theming.html>`_.
+ with a `custom theme <theming.html>`__.
Template tweaking
If you really want to change the pages radically, you will want to do a
- `custom theme <theming.html>`_.
+ `custom theme <theming.html>`__.
-Sidebar
- ``LICENSE`` is a HTML snippet for things like a CC badge, or whatever you prefer.
+Navigation Links
+ The 'NAVIGATION_LINKS' option lets you define what links go in a sidebar or menu
+ (depending on your theme) so you can link to important pages, or to other sites.
+
+ The format is a language-indexed dictionary, where each element is a tuple of
+ tuples which are one of:
+
+ 1. A (url, text) tuple, describing a link
+ 2. A ((url, text), (url, text), (url, text), title) tuple, describing a submenu / sublist.
+
+ Example::
+
+ NAVIGATION_LINKS = {
+ DEFAULT_LANG: (
+ ('/archive.html', 'Archives'),
+ ('/categories/index.html', 'Tags'),
+ ('/rss.xml', 'RSS'),
+ ((('/foo', 'FOO'),
+ ('/bar', 'BAR')), 'BAZ'),
+ ),
+ }
- The 'sidebar_links' option lets you define what links go in the right-hand
- sidebar, so you can link to important pages, or to other sites.
The ``SEARCH_FORM`` option contains the HTML code for a search form based on
duckduckgo.com which should always work, but feel free to change it to
@@ -630,20 +809,24 @@ Sidebar
Footer
``CONTENT_FOOTER`` is displayed, small at the bottom of all pages, I use it for
- the copyright notice.
+ the copyright notice. The default shows a text formed using ``BLOG_AUTHOR``,
+ ``BLOG_EMAIL``, the date and ``LICENSE``.
-Analytics
- This is probably a misleading name, but the ``ANALYTICS`` option lets you define
- a HTML snippet that will be added at the bottom of body. The main usage is
- a Google analytics snippet or something similar, but you can really put anything
- there.
+BODY_END
+ This option lets you define a HTML snippet that will be added at the bottom of body.
+ The main usage is a Google analytics snippet or something similar, but you can really
+ put anything there. Good place for JavaScript.
+SOCIAL_BUTTONS_CODE
+ The ``SOCIAL_BUTTONS_CODE`` option lets you define a HTML snippet that will be added
+ at the bottom of body. It defaults to a snippet for AddThis, but you can
+ really put anything there. See `social_buttons.html` for more details.
Adding Files
------------
Any files you want to be in ``output/`` but are not generated by Nikola (for example,
-``favicon.ico``, just put it in ``files/``. Everything there is copied into
+``favicon.ico``) just put it in ``files/``. Everything there is copied into
``output`` by the ``copy_files`` task. Remember that you can't have files that collide
with files Nikola generates (it will give an error).
@@ -664,36 +847,41 @@ change the FILES_FOLDERS option::
Getting More Themes
-------------------
-There are not so many themes for Nikola. On occasion, I port something I like, and make
-it available for download. Nikola has a builtin theme download/install mechanism, its
-``install_theme`` command::
+There are a few themes for Nikola. They are available at
+the `Themes Index <http://themes.getnikola.com/>`_.
+Nikola has a built-in theme download/install mechanism to install those themes — the ``install_theme`` command::
$ nikola install_theme -l
Themes:
-------
+ base-jinja
blogtxt
- readable
+ ⋮
+ ⋮
- $ nikola install_theme -n blogtxt
- Downloading: http://nikola.ralsina.com.ar/themes/blogtxt.zip
- Extracting: blogtxt into themes
+ $ nikola install_theme blogtxt
+ [2013-10-12T16:46:13Z] NOTICE: install_theme: Downloading:
+ http://themes.getnikola.com/v6/blogtxt.zip
+ [2013-10-12T16:46:15Z] NOTICE: install_theme: Extracting: blogtxt into themes
-And there you are, you now have themes/blogtxt installed. It's very rudimentary, but it
-should work in most cases.
+And there you are, you now have themes/blogtxt installed. It's very
+rudimentary, but it should work in most cases.
-If you create a nice theme, please share it! You can post about it on
-`the nikola forum <http://groups.google.com/group/nikola-discuss>`_ and I will
-make it available for download.
+If you create a nice theme, please share it! You can do it as a pull
+request in the `GitHub repository <https://github.com/getnikola/nikola-themes>`__.
One other option is to tweak an existing theme using a different color scheme,
typography and CSS in general. Nikola provides a ``bootswatch_theme`` option
to create a custom theme by downloading free CSS files from http://bootswatch.com::
- $ nikola bootswatch_theme -n custom_theme -s spruce -p site
- Creating 'custom_theme' theme from 'spruce' and 'site'
- Downloading: http://bootswatch.com/spruce/bootstrap.min.css
- Downloading: http://bootswatch.com/spruce/bootstrap.css
- Theme created. Change the THEME setting to "custom_theme" to use it.
+ $ nikola bootswatch_theme -n custom_theme -s spruce -p bootstrap3
+ [2013-10-12T16:46:58Z] NOTICE: bootswatch_theme: Creating 'custom_theme' theme
+ from 'spruce' and 'bootstrap3'
+ [2013-10-12T16:46:58Z] NOTICE: bootswatch_theme: Downloading:
+ http://bootswatch.com//spruce/bootstrap.min.css
+ [2013-10-12T16:46:58Z] NOTICE: bootswatch_theme: Downloading:
+ http://bootswatch.com//spruce/bootstrap.css
+ [2013-10-12T16:46:59Z] NOTICE: bootswatch_theme: Theme created. Change the THEME setting to "custom_theme" to use it.
You can even try what different swatches do on an existing site using
their handy `bootswatchlet <http://news.bootswatch.com/post/29555952123/a-bookmarklet-for-bootswatch>`_
@@ -714,26 +902,53 @@ Here is an example, from my own site's deployment script::
DEPLOY_COMMANDS = [
'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral',
- 'rdiff-backup output ~/bartleblog-backup',
+ 'rdiff-backup output ~/blog-backup',
"links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar'",
- 'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup',
]
Other interesting ideas are using
`git as a deployment mechanism <http://toroid.org/ams/git-website-howto>`_ (or any other VCS
-for that matter), using `lftp mirror <http://lftp.yar.ru/>`_ or unison, or dropbox, or
+for that matter), using `lftp mirror <http://lftp.yar.ru/>`_ or unison, or Dropbox, or
Ubuntu One. Any way you can think of to copy files from one place to another is good enough.
-Comments
---------
+Comments and Annotations
+------------------------
While Nikola creates static sites, there is a minimum level of user interaction you
are probably expecting: comments.
-The default templates contain support for `Disqus <http://disqus.com>`_. All you have
-to do is register a forum, put its short name in the ``DISQUS_FORUM`` option.
+Nikola supports several third party comment systems:
+
+* `DISQUS <http://disqus.com>`_
+* `IntenseDebate <http://www.intensedebate.com/>`_
+* `LiveFyre <http://www.livefyre.com/>`_
+* `Moot <http://moot.it>`_
+* `Google+ <http://plus.google.com>`_
+* `Facebook <http://facebook.com>`_
+
+By default it will use DISQUS, but you can change by setting ``COMMENT_SYSTEM``
+to one of "disqus", "intensedebate", "livefyre", "moot", "googleplus" or
+"facebook"
-Disqus is a good option because:
+.. sidebar:: ``COMMENT_SYSTEM_ID``
+
+ The value of ``COMMENT_SYSTEM_ID`` depends on what comment system you
+ are using and you can see it in the system's admin interface.
+
+ * For DISQUS it's called the **shortname**
+ * In IntenseDebate it's the **IntenseDebate site acct**
+ * In LiveFyre it's the **siteId**
+ * In Moot it's your **username**
+ * For Google Plus, ``COMMENT_SYSTEM_ID`` need not be set, but you must
+ `verify your authorship <https://plus.google.com/authorship>`_
+ * For Facebook, you need to `create an app
+ <https://developers.facebook.com/apps>` (turn off sandbox mode!)
+ and get an **App ID**
+
+To use comments in a visible site, you should register with the service and
+then set the ``COMMENT_SYSTEM_ID`` option.
+
+I recommend 3rd party comments, and specially DISQUS because:
1) It doesn't require any server-side software on your site
2) They offer you a way to export your comments, so you can take
@@ -745,12 +960,44 @@ You can disable comments for a post by adding a "nocomments" metadata field to i
.. nocomments: True
-.. admonition:: Important
+.. admonition:: DISQUS Support
In some cases, when you run the test site, you won't see the comments.
That can be fixed by adding the disqus_developer flag to the templates
but it's probably more trouble than it's worth.
+.. admonition:: Moot Support
+
+ Moot doesn't support comment counts on index pages, and it requires adding
+ this to your ``conf.py``:
+
+ .. code-block:: python
+
+ BODY_END = """
+ <script src="//cdn.moot.it/1/moot.min.js"></script>
+ """
+ EXTRA_HEAD_DATA = """
+ <link rel="stylesheet" type="text/css" href="//cdn.moot.it/1/moot.css">
+ <meta name="viewport" content="width=device-width">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ """
+
+.. admonition:: Facebook Support
+
+ You need jQuery, but not because Facebook wants it (see Issue
+ #639).
+
+An alternative or complement to comments are annotations. Nikola integrates
+the annotation service provided by `AnnotateIt. <annotateit.org>`_
+To use it, set the ``ANNOTATIONS`` option to True. This is specially useful
+if you want feedback on specific parts of your writing.
+
+You can enable or disable annotations for specific posts or pages using the
+``annotations`` and ``noannotations`` metadata.
+
+Annotations require JQuery and are therefore not supported in the base theme.
+You can check bootstrap theme's ``base.html`` for details on how to handle them in
+custom themes.
Image Galleries
---------------
@@ -773,9 +1020,12 @@ The ``conf.py`` options affecting gallery pages are these::
THUMBNAIL_SIZE = 180
MAX_IMAGE_SIZE = 1280
USE_FILENAME_AS_TITLE = True
+ GALLERY_SORT_BY_DATE = False
+ EXTRA_IMAGE_EXTENSIONS = []
If you add a file in ``galleries/gallery_name/index.txt`` its contents will be
-converted to HTML and inserted above the images in the gallery page.
+converted to HTML and inserted above the images in the gallery page. The
+format is the same as for posts.
If you add some image filenames in ``galleries/gallery_name/exclude.meta``, they
will be excluded in the gallery page.
@@ -809,12 +1059,55 @@ replace that with strings describing command lines, or arbitrary python function
If there's any specific thing you expect to be generally useful as a filter, contact
me and I will add it to the filters library so that more people use it.
+The currently available filters are:
+
+.. sidebar:: Creating your own filters
+
+ You can use any program name that works in place as a filter, like ``sed -i``
+ and you can use arbitrary python functions as filters, too.
+
+ If your program doesn't run in-place, then you can use Nikola's runinplace function.
+ For example, this is how the yui_compressor filter is implemented:
+
+ .. code-block:: python
+
+ def yui_compressor(infile):
+ return runinplace(r'yui-compressor --nomunge %1 -o %2', infile)
+
+ You can turn any function into a filter using ``apply_to_file``.
+ As a silly example, this would make everything uppercase and totally break
+ your website:
+
+ .. code-block:: python
+
+ import string
+ from nikola.filters import apply_to_file
+ FILTERS = {
+ ".html": [apply_to_file(string.upper)]
+ }
+
+yui_compressor
+ Compress files using `YUI compressor <http://yui.github.io/yuicompressor/>`_
+
+optipng
+ Compress PNG files using `optipng <http://optipng.sourceforge.net/>`_
+
+jpegoptim
+ Compress JPEG files using `jpegoptim <http://www.kokkonen.net/tjko/projects.html>`_
+
+tidy
+ Apply `tidy <http://tidy.sourceforge.net/>`_ to HTML files
+
+typogrify
+ Improve typography using `typogrify <https://github.com/mintchaos/typogrify>`_
+
+
Optimizing Your Website
-----------------------
One of the main goals of Nikola is to make your site fast and light. So here are a few
tips we have found when setting up Nikola with Apache. If you have more, or
-different ones, or about other webservers, please share!
+different ones, or about other web servers, please share!
#. Use a speed testing tool. I used Yahoo's YSlow but you can use any of them, and
it's probably a good idea to use more than one.
@@ -833,21 +1126,33 @@ different ones, or about other webservers, please share!
#. The webassets Nikola plugin can drastically decrease the number of CSS and JS files your site fetches.
#. Through the filters feature, you can run your files through arbitrary commands, so that images
- are recompressed, Javascript is minimized, etc.
+ are recompressed, JavaScript is minimized, etc.
-#. The USE_CDN option offloads standard Javascript and CSS files to a CDN so they are not
+#. The USE_CDN option offloads standard JavaScript and CSS files to a CDN so they are not
downloaded from your server.
-Restructured Text Extensions
-----------------------------
+reStructuredText Extensions
+---------------------------
-Nikola includes support for a few directives that are not part of docutils, but which
+Nikola includes support for a few directives and roles that are not part of docutils, but which
we think are handy for website development.
-Youtube
+Media
+~~~~~
+
+This directive lets you embed media from a variety of sites automatically by just passing the
+URL of the page. For example here are two random videos::
+
+ .. media:: http://vimeo.com/72425090
+
+ .. youtube:: http://www.youtube.com/watch?v=wyRpAat5oz0
+
+It supports Instagram, Flickr, Github gists, Funny or Die, and dozens more, thanks to `Micawber <https://github.com/coleifer/micawber>`_
+
+YouTube
~~~~~~~
-To link to a youtube video, you need the id of the video. For example, if the
+To link to a YouTube video, you need the id of the video. For example, if the
URL of the video is http://www.youtube.com/watch?v=8N_tupPBtWQ what you need is
**8N_tupPBtWQ**
@@ -858,15 +1163,14 @@ Once you have that, all you need to do is::
Vimeo
~~~~~
-To link to a vimeo video, you need the id of the video. For example, if the
+To link to a Vimeo video, you need the id of the video. For example, if the
URL of the video is http://www.vimeo.com/20241459 then the id is **20241459**
Once you have that, all you need to do is::
.. vimeo:: 20241459
-If you are running python 2.6 or later, or have the json module installed and
-have internet connectivity when generating your site, the height and width of
+If you have internet connectivity when generating your site, the height and width of
the embedded player will be set to the native height and width of the video.
You can override this if you wish::
@@ -879,7 +1183,7 @@ Soundcloud
This directive lets you share music from http://soundcloud.com You first need to get the
ID for the piece, which you can find in the "share" link. For example, if the
-Wordpress code starts like this::
+WordPress code starts like this::
[soundcloud url="http://api.soundcloud.com/tracks/78131362"
@@ -894,7 +1198,7 @@ The ``code`` directive has been included in docutils since version 0.9 and now
replaces Nikola's ``code-block`` directive. To ease the transition, two aliases
for ``code`` directive are provided: ``code-block`` and ``sourcecode``::
- .. code:: python
+ .. code-block:: python
:number-lines:
print("Our virtues and our failings are inseparable")
@@ -936,7 +1240,7 @@ Producing this:
.. gist:: 2395294
-This degrades gracefully if the browser doesn't support javascript.
+This degrades gracefully if the browser doesn't support JavaScript.
Slideshows
~~~~~~~~~~
@@ -951,17 +1255,65 @@ To create an image slideshow, you can use the ``slides`` directive. For example:
/galleries/demo/tesla_lightning1_lg.jpg
/galleries/demo/tesla_tower1_lg.jpg
+Chart
+~~~~~
+
+This directive is a thin wrapper around `Pygal <http://pygal.org/>`_ and will produce charts
+as SVG files embedded directly in your pages.
+
+Here's an example of how it works::
+
+ .. chart:: Bar
+ :title: 'Browser usage evolution (in %)'
+ :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"]
+
+ 'Firefox', [None, None, 0, 16.6, 25, 31]
+ 'Chrome', [None, None, None, None, None, None]
+ 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6]
+ 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4]
+
+The argument passed next to the directive (Bar in that example) is the type of chart, and can be one of
+Line, StackedLine, Bar, StackedBar, HorizontalBar, XY, DateY, Pie, Radar, Dot, Funnel, Gauge, Pyramid. For
+examples of what each kind of graph is, `check here <http://pygal.org/chart_types/>`_
-Importing Your Wordpress Site Into Nikola
+It can take *a lot* of options to let you customize the charts (in the example, title and x_labels).
+You can use any option described in `the pygal docs <http://pygal.org/basic_customizations/>`_
+
+Finally, the content of the directive is the actual data, in the form of a label and
+a list of values, one series per line.
+
+Doc
+~~~
+
+This role is useful to make links to other post or page inside the same site.
+
+Here's an example::
+
+ Take a look at :doc:`my other post <creating-a-theme>` about theme creating.
+
+In this case we are giving the portion of text we want to link. So, the result will be:
+
+ Take a look at :doc:`my other post <creating-a-theme>` about theme creating.
+
+If we want to use the post's title as the link's text, just do::
+
+ Take a look at :doc:`creating-a-theme` to know how to do it.
+
+and it will produce:
+
+ Take a look at :doc:`creating-a-theme` to know how to do it.
+
+
+Importing Your WordPress Site Into Nikola
-----------------------------------------
-If you like Nikola, and want to start using it, but you have a Wordpress blog, Nikola
+If you like Nikola, and want to start using it, but you have a WordPress blog, Nikola
supports importing it. Here's the steps to do it:
1) Get a XML dump of your site [#]_
-2) nikola import_wordpress -f mysite.wordpress.2012-12-20.xml
+2) nikola import_wordpress mysite.wordpress.2012-12-20.xml
-After some time, this will crate a ``new_site`` folder with all your data. It currently supports
+After some time, this will create a ``new_site`` folder with all your data. It currently supports
the following:
* All your posts and pages
@@ -971,11 +1323,11 @@ the following:
* Will try to add redirects that send the old post URLs to the new ones
* Will give you a url_map so you know where each old post was
- This is also useful for Disqus thread migration!
+ This is also useful for DISQUS thread migration!
* Will try to convert the content of your posts. This is *not* error free, because
- wordpress uses some unholy mix of HTML and strange things. Currently we are treating it
- as markdown, which does a reasonabe job of it.
+ WordPress uses some unholy mix of HTML and strange things. Currently we are treating it
+ as markdown, which does a reasonable job of it.
You will find your old posts in ``new_site/posts/post-title.wp`` in case you need to fix
any of them.
@@ -987,7 +1339,7 @@ about it.
.. [#] The dump needs to be in 1.2 format. You can check by reading it, it should say
``xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"`` near the top of the
file. If it says ``1.1`` instead of ``1.2`` you will have to update your
- wordpress before dumping.
+ WordPress before dumping.
Other versions may or may not work.
@@ -997,7 +1349,7 @@ Importing To A Custom Location Or Into An Existing Site
It is possible to either import into a location you desire or into an already existing Nikola site.
To do so you can specify a location after the dump.::
- $ nikola import_wordpress -f mysite.wordpress.2012-12-20.xml -o import_location
+ $ nikola import_wordpress mysite.wordpress.2012-12-20.xml -o import_location
With this command Nikola will import into the folder ``import_location``.
@@ -1075,14 +1427,13 @@ when they try to open a post.
Local Search
~~~~~~~~~~~~
-If you don't want to depend on google or duckduckgo to implement search for you,
-or just want it to wok even if you are offline, enable this plugin and the
+If you don't want to depend on Google or DuckDuckGo to implement search for you,
+or just want it to work even if you are offline, enable this plugin and the
search will be performed client side.
This plugin implements a Tipue-based local search for your site.
-To use it, copy task_localsearch.plugin and task_localsearch
-into a plugins/ folder in your nikola site.
+To use it, enable local_search in ENABLED_EXTRAS in your sites conf.py
After you build your site, you will have several new files in assets/css and assets/js
and a tipue_search.html that you can use as a basis for using this in your site.
@@ -1092,14 +1443,14 @@ docs at http://www.tipue.com/search/
Tipue is under an MIT license (see MIT-LICENSE.txt)
-Here's a set of example settings for conf.py that should work nicely with the "site" theme::
+Here's a set of example settings for conf.py that should work nicely with the "bootstrap" theme::
SEARCH_FORM = """
<span class="navbar-form pull-left">
<input type="text" id="tipue_search_input">
</span>"""
- ANALYTICS = """
+ BODY_END = """
<script type="text/javascript" src="/assets/js/tipuesearch_set.js"></script>
<script type="text/javascript" src="/assets/js/tipuesearch.js"></script>
<script type="text/javascript">
@@ -1118,6 +1469,9 @@ Here's a set of example settings for conf.py that should work nicely with the "s
<div id="tipue_search_content" style="margin-left: auto; margin-right: auto; padding: 20px;"></div>
"""
+ ENABLED_EXTRAS = [ 'local_search' ]
+
+
The <div> in EXTRA_HEAD_DATA is a hack but it will migrate into the <body> of the
documents thanks to magic, and will hold the search results after the user searches.
@@ -1128,11 +1482,52 @@ This task gives you a ``mustache.html`` file which lets you access your whole
blog without reloading the page, using client-side templates. Makes it much
faster and modern ;-)
+Custom Plugins
+--------------
+
+You can create your own plugins (see :doc:`extending`) and use them in your own
+site by putting them in a ``plugins/`` folder.
+
+
+Getting Extra Plugins
+---------------------
+
+If you want extra plugins, there is also the `Plugins Index <http://plugins.getnikola.com/>`_.
+
+Similarly to themes, there is a nice, built-in command to install them —
+``install_plugin``:
+
+ $ nikola install_plugin -l
+ Plugins:
+ --------
+ helloworld
+ tags
+ ⋮
+ ⋮
+
+ $ nikola install_plugin helloworld
+ [2013-10-12T16:51:56Z] NOTICE: install_plugin: Downloading: http://plugins.getnikola.com/v6/helloworld.zip
+ [2013-10-12T16:51:58Z] NOTICE: install_plugin: Extracting: helloworld into plugins
+ plugins/helloworld/requirements.txt
+ [2013-10-12T16:51:58Z] NOTICE: install_plugin: This plugin has Python dependencies.
+ [2013-10-12T16:51:58Z] NOTICE: install_plugin: Installing dependencies with pip...
+ ⋮
+ ⋮
+ [2013-10-12T16:51:59Z] NOTICE: install_plugin: Dependency installation succeeded.
+ [2013-10-12T16:51:59Z] NOTICE: install_plugin: This plugin has a sample config file.
+ Contents of the conf.py.sample file:
+
+ # Should the Hello World plugin say “BYE” instead?
+ BYE_WORLD = False
+
+
+You can also share plugins you created with the community! Visit the
+`GitHub repository <https://github.com/getnikola/plugins>`__ to find out more.
License
-------
-Nikola is released under a `MIT license <https://github.com/ralsina/nikola/blob/master/LICENSE.txt>`_ which
+Nikola is released under a `MIT license <https://github.com/getnikola/nikola/blob/master/LICENSE.txt>`_ which
is a free software license. Some components shipped along with Nikola, or required by it are
released under other licenses.
diff --git a/docs/social_buttons.txt b/docs/social_buttons.txt
new file mode 100644
index 0000000..efbf9df
--- /dev/null
+++ b/docs/social_buttons.txt
@@ -0,0 +1,202 @@
+.. title: Alternative Social Buttons
+.. slug: social_buttons
+.. date: 2013/08/19 23:00
+.. tags:
+.. link:
+.. description:
+
+Using Alternative Social Buttons with Nikola
+============================================
+
+:Version: 6.2.1
+
+.. class:: alert alert-info pull-right
+
+.. contents::
+
+
+The Default
+-----------
+
+By Default, the themes provided with Nikola will add to your pages a "slide in" widget at
+the bottom right of the page, provided by Addthis. This is the HTML code for that:
+
+.. code-block:: html
+
+ <!-- Social buttons -->
+ <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style
+ addthis_default_style addthis_label_style addthis_32x32_style">
+ <a class="addthis_button_more">Share</a>
+ <ul><li><a class="addthis_button_facebook"></a>
+ <li><a class="addthis_button_google_plusone_share"></a>
+ <li><a class="addthis_button_linkedin"></a>
+ <li><a class="addthis_button_twitter"></a>
+ </ul>
+ </div>
+ <script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
+ <!-- End of social buttons -->
+ """
+
+You can change that using the ``SOCIAL_BUTTONS_CODE`` option in your conf.py. In some cases, just
+doing that will be enough but in others, it won't. This document tries to describe all the bits
+involved in making this work correctly.
+
+Part 1: ``SOCIAL_BUTTONS_CODE``
+ Social sharing services like addthis and others will provide you a HTML snippet.
+ If it is self-contained, then just setting ``SOCIAL_BUTTONS_CODE`` may be enough.
+ Try :-)
+
+Part 2: The theme
+ The ``SOCIAL_BUTTONS_CODE`` HTML fragment will be embedded *somewhere* by the theme. Whether that
+ is the correct place or not is not something the theme author can truly know, so it is possible that
+ you may have to tweak the ``base.html`` template to make it look good.
+
+Part 3: ``BODY_END`` and ``EXTRA_HEAD_DATA``
+ Some social sharing code requires JS execution that depends on JQuery being available
+ (example: `SocialSharePrivacy <https://github.com/panzi/SocialSharePrivacy>`__). It's good
+ practice (and often, the only way that will work) to put those at the end of ``<BODY>``,
+ and one easy way to do that is to put them in ``BODY_END``
+
+ On the other hand, it's possible that it requires you to load some CSS files.
+ The right place for that is in the document's ``<HEAD>`` so they should be added
+ in ``EXTRA_HEAD_DATA``
+
+Part 4: assets
+ For sharing code that doesn't rely on a social sharing service, you may need to add CSS, Image, or JS
+ files to your site
+
+ShareNice
+---------
+
+`Sharenice <http://sharenice.org>`__ is "written in order to provide social sharing features to
+web developers and website administrators who wish to maintain and protect their users' privacy"
+which sounds cool to me.
+
+Let's go step by step into integrating the hosted version of ShareNice into a Nikola site.
+
+For testing purposes, let's do it on a demo site::
+
+ $ nikola init --demo sharenice_test
+ A new site with example data has been created at sharenice_test.
+ See README.txt in that folder for more information.
+ $ cd sharenice_test/
+
+To see what's going on, let's start Nikola in "auto mode". This should build the
+site and open a web browser showing the default configuration, with the AddThis widget::
+
+ $ nikola auto -b
+
+First, let's add the HTML snippet that will show the sharing options. In your conf.py, set this, which
+is the HTML code suggested by ShareNice:
+
+.. code-block:: python
+
+ SOCIAL_BUTTONS_CODE = """<div id="shareNice" data-share-label="Share"
+ data-color-scheme="black" data-icon-size="32" data-panel-bottom="plain"
+ data-services="plus.google.com,facebook.com,digg.com,email,delicious.com,twitter.com"
+ style="float:right"></div>"""
+
+ BODY_END = """<script src="http://graingert.co.uk/shareNice/code.js"
+ type="text/javascript"></script>"""
+
+And you should now see a sharing box at the bottom right of the page.
+
+Main problem remaining is that it doesn't really look good and integrated in the page layout.
+I suggest changing the code to this which looks nicer, but still has some placement issues:
+
+.. code-block:: python
+
+ SOCIAL_BUTTONS_CODE = """<div id="shareNice" data-share-label="Share"
+ data-color-scheme="black" data-icon-size="32" data-panel-bottom="plain"
+ data-services="plus.google.com,facebook.com,email,twitter.com"
+ style="position: absolute; left: 20px; top: 60px;"></div>"""
+
+If anyone comes up with a better idea of styling/placement, just let me know ;-)
+
+One bad bit of this so far is that you are now using a script from another site, and that
+doesn't let Nikola perform as many optimizations to your page as it could.
+So, if you really want to go the extra mile to save a few KB and round trips, you *could*
+install your own copy from the `github repo <https://github.com/mischat/shareNice>`_ and
+use that instead of the copy at sharenice.org.
+
+Then, you can create your own theme inheriting from the one you are using and add the CSS
+and JS files from ShareNice into your ``bundles`` configuration so they are combined and
+minified.
+
+SocialSharePrivacy
+------------------
+
+The Hard Way
+~~~~~~~~~~~~
+
+`SocialSharePrivacy <https://github.com/panzi/SocialSharePrivacy>`__ is "a jQuery plugin that
+lets you add social share buttons to your website that don't allow the social sites to track
+your users." Nice!
+
+Let's go step-by-step into integrating SocialSharePrivacy into a Nikola site. To improve
+privacy, they recommend you not use the hosted service so we'll do it the hard way, by
+getting and distributing everything in our own site.
+
+https://github.com/panzi/SocialSharePrivacy
+
+For testing purposes, let's do it on a demo site::
+
+ $ nikola init --demo ssp_test
+ A new site with example data has been created at ssp_test.
+ See README.txt in that folder for more information.
+ $ cd ssp_test/
+
+To see what's going on, let's start Nikola in "auto mode". This should build the
+site and open a web browser showing the default configuration, with the AddThis widget::
+
+ $ nikola auto -b
+
+Now, download `the current version <https://github.com/panzi/SocialSharePrivacy/archive/master.zip>`_
+and unzip it. You will have a ``SocialSharePrivacy-master`` folder with lots of stuff in it.
+
+First, we need to build it (this requires a working and modern uglifyjs, this may not be easy)::
+
+ $ cd SocialSharePrivacy-master
+ $ sh build.sh -m gplus,twitter,facebook,mail -s "/assets/css/socialshareprivacy.css" -a off
+
+You will now have several files in a ``build`` folder. We need to bring them into the site::
+
+ $ cp -Rv SocialSharePrivacy-master/build/* files/
+ $ cp -R SocialSharePrivacy-master/images/ files/assets/
+
+Edit your ``conf.py``:
+
+.. code-block:: python
+
+ BODY_END = """
+ <script type="text/javascript" src="/javascripts/jquery.socialshareprivacy.min.js"></script>
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('.share').socialSharePrivacy();
+ });
+ </script>
+ """
+
+ SOCIAL_BUTTONS_CODE = """<div class="share"></div>"""
+
+In my experience this produces a broken, duplicate, semi-working thing. YMMV and if you make it work correctly, let me know how :-)
+
+The Easy Way
+~~~~~~~~~~~~
+
+Go to http://panzi.github.io/SocialSharePrivacy/ and use the provided form to get the code. Make sure you check "I already use JQuery"
+if you are using one of the themes that require it, like site or default, select the services you want, and use your disqus name if
+you have one.
+
+It will give you 3 code snippets:
+
+"Insert this once in the head of your page"
+ Put it in ``BODY_END``
+
+"Insert this wherever you want a share widget displayed"
+ Put it in ``SOCIAL_BUTTONS_CODE``
+
+"Insert this once anywhere after the other code"
+ Put it in ``BODY_END``
+
+That should give you a working integration (not tested)
diff --git a/docs/sphinx/Makefile b/docs/sphinx/Makefile
new file mode 100644
index 0000000..4e8e36e
--- /dev/null
+++ b/docs/sphinx/Makefile
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Nikola.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Nikola.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/Nikola"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Nikola"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py
new file mode 100644
index 0000000..9e24c8f
--- /dev/null
+++ b/docs/sphinx/conf.py
@@ -0,0 +1,263 @@
+# -*- coding: utf-8 -*-
+#
+# Nikola documentation build configuration file, created by
+# sphinx-quickstart on Sun Sep 22 17:43:37 2013.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+from __future__ import unicode_literals
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+try:
+ import sphinxcontrib.gist # NOQA
+ extensions = ['sphinxcontrib.gist']
+except ImportError:
+ extensions = []
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'Nikola'
+copyright = '2013, The Nikola Contributors'
+
+# The version info for the project yo're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '6.2.0'
+# The full version, including alpha/beta/rc tags.
+release = '6.2.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#html_extra_path = []
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Nikoladoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'Nikola.tex', 'Nikola Documentation',
+ 'The Nikola Contributors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'nikola', 'Nikola Documentation',
+ ['The Nikola Contributors'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'Nikola', 'Nikola Documentation',
+ 'The Nikola Contributors', 'Nikola', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
+
+primary_domain = None
diff --git a/docs/sphinx/creating-a-site.txt b/docs/sphinx/creating-a-site.txt
new file mode 120000
index 0000000..85f2ab2
--- /dev/null
+++ b/docs/sphinx/creating-a-site.txt
@@ -0,0 +1 @@
+../creating-a-site.txt \ No newline at end of file
diff --git a/docs/sphinx/creating-a-theme.txt b/docs/sphinx/creating-a-theme.txt
new file mode 120000
index 0000000..6ddf35a
--- /dev/null
+++ b/docs/sphinx/creating-a-theme.txt
@@ -0,0 +1 @@
+../creating-a-theme.txt \ No newline at end of file
diff --git a/docs/sphinx/extending.txt b/docs/sphinx/extending.txt
new file mode 120000
index 0000000..1da5af1
--- /dev/null
+++ b/docs/sphinx/extending.txt
@@ -0,0 +1 @@
+../extending.txt \ No newline at end of file
diff --git a/docs/sphinx/index.txt b/docs/sphinx/index.txt
new file mode 100644
index 0000000..6d899ab
--- /dev/null
+++ b/docs/sphinx/index.txt
@@ -0,0 +1,27 @@
+Nikola Documentation
+====================
+
+Those are the docs for the current GitHub master. It might be incompatible
+with the stable release. The docs for the stable release are available `on
+the Nikola website <http://getnikola.com/documentation.html>`_.
+
+.. toctree::
+ :maxdepth: 2
+
+ manual
+ upgrading-to-v6
+ creating-a-site
+ creating-a-theme
+ theming
+ extending
+ internals
+ tests
+ social_buttons
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/sphinx/internals.txt b/docs/sphinx/internals.txt
new file mode 120000
index 0000000..f0f3f05
--- /dev/null
+++ b/docs/sphinx/internals.txt
@@ -0,0 +1 @@
+../internals.txt \ No newline at end of file
diff --git a/docs/sphinx/make.bat b/docs/sphinx/make.bat
new file mode 100644
index 0000000..8e3b6a1
--- /dev/null
+++ b/docs/sphinx/make.bat
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Nikola.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Nikola.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff --git a/docs/sphinx/manual.txt b/docs/sphinx/manual.txt
new file mode 120000
index 0000000..df591d6
--- /dev/null
+++ b/docs/sphinx/manual.txt
@@ -0,0 +1 @@
+../manual.txt \ No newline at end of file
diff --git a/docs/sphinx/social_buttons.txt b/docs/sphinx/social_buttons.txt
new file mode 120000
index 0000000..12021c1
--- /dev/null
+++ b/docs/sphinx/social_buttons.txt
@@ -0,0 +1 @@
+../social_buttons.txt \ No newline at end of file
diff --git a/docs/sphinx/theming.txt b/docs/sphinx/theming.txt
new file mode 120000
index 0000000..ed5f3b3
--- /dev/null
+++ b/docs/sphinx/theming.txt
@@ -0,0 +1 @@
+../theming.txt \ No newline at end of file
diff --git a/docs/sphinx/upgrading-to-v6.txt b/docs/sphinx/upgrading-to-v6.txt
new file mode 120000
index 0000000..1c7e16f
--- /dev/null
+++ b/docs/sphinx/upgrading-to-v6.txt
@@ -0,0 +1 @@
+../upgrading-to-v6.txt \ No newline at end of file
diff --git a/docs/theming.txt b/docs/theming.txt
index d767cae..99492ab 100644
--- a/docs/theming.txt
+++ b/docs/theming.txt
@@ -1,7 +1,14 @@
+.. title: Theming Nikola
+.. slug: theming
+.. date: 2012/03/13 12:00
+.. tags:
+.. link:
+.. description:
+
Theming Nikola
==============
-:Version: 5
+:Version: 6.2.1
:Author: Roberto Alsina <ralsina@netmanagers.com.ar>
.. class:: alert alert-info pull-right
@@ -14,10 +21,10 @@ This document is a reference about themes. If you want a tutorial, please read
The Structure
-------------
-Themes are located in the ``themes`` folder where Nikola is installed, one folder per theme.
-The folder name is the theme name.
+Themes are located in the ``themes`` folder where Nikola is installed, and in the ``themes`` folder
+of your site, one folder per theme. The folder name is the theme name.
-A Nikola theme consists of three folders:
+A Nikola theme consists of the following folders (they are *all* optional):
assets
This is where you would put your CSS, Javascript and image files. It will be copied
@@ -41,7 +48,13 @@ messages
Nikola tries to be multilingual. This is where you put the strings for your theme
so that it can be translated into other languages.
-And these optional files:
+less
+ Files to be compiled into CSS using `LESS <http://lesscss.org/>`__
+
+sass
+ Files to be compiled into CSS using `Sass <http://sass-lang.com/>`__
+
+This mandatory file:
parent
A text file that, on its first line, contains the name of the **parent theme**.
@@ -51,10 +64,19 @@ parent
The ``parent`` is so you don't have to create a full theme each time: just create an
empty theme, set the parent, and add the bits you want modified.
+ I recommend this:
+
+ * If your theme uses bootstrap, inherit the ``bootstrap`` theme.
+ * If your theme uses bootstrap3, inherit the ``bootstrap3`` theme.
+ * If your theme uses Jinja as a template engine, inherit ``base-jinja``
+ or ``bootstrap-jinja`` (available at http://themes.nikola.ralsina.com.ar)
+ * In any other case, inherit ``base``.
+
+And these optional files:
+
engine
A text file which, on the first line, contains the name of the template engine
this theme needs. Currently supported values are "mako" and "jinja".
- If this file is not given, "mako" is assumed.
bundles
A text file containing a list of files to be turned into bundles using WebAssets.
@@ -89,108 +111,74 @@ so ``post.tmpl`` only define the content, and the layout is inherited from ``bas
These are the templates that come with the included themes:
-base.tmpl
+``base.tmpl``
This template defines the basic page layout for the site. It's mostly plain HTML
- but defines a few blocks that can be re-defined by inheriting templates:
-
- * ``extra_head`` is a block that is added before ``</head>``, (ex: for adding extra CSS)
- * ``belowtitle`` is used by default to display a list of translations but you can put
- anything there.
- * ``content`` is where the inheriting templates will place the main content of the page.
- * ``permalink`` is an absolute path to the page (ex: "/archive/index.html")
-
- This template always receives the following variables you can use:
-
- * ``lang`` is the laguage for this page.
- * ``title`` is the page's title.
- * ``description`` is the page's description.
- * ``blog_title`` is the blog's title.
- * ``blog_author`` is the blog's author.
- * ``messages`` contains the theme's strings and translations.
- * ``_link`` is an utility function to create links to other pages in the site.
- It takes three arguments, kind, name, lang:
-
- kind is one of:
-
- * tag_index (name is ignored)
- * tag (and name is the tag name)
- * tag_rss (name is the tag name)
- * archive (and name is the year, or None for the main archive index)
- * index (name is the number in index-number)
- * rss (name is ignored)
- * gallery (name is the gallery name)
-
- The returned value is always an absolute path, like "/archive/index.html".
-
- * ``rel_link`` converts absolute paths to relative ones. You can use it with
- ``_link`` and ``permalink`` to create relative links, which makes the site
- able to work when moved inside the server. Example: ``rel_link(permalink, url)``
-
- * Anything you put in your ``GLOBAL_CONTEXT`` option in ``dodo.py``. This
- usually includes ``sidebar_links``, ``search_form``, and others.
-
- The included themes use at least these:
-
- * ``rss_link`` a link to custom RSS feed, although it may be empty)
- * ``site_url`` the URL for your site
- * ``blog_title`` the name of your site
- * ``content_footer`` things like copyright notices, disclaimers, etc.
- * ``license`` a larger license badge
- * ``analytics`` google scripts, or any JS you want to tack at the end of the body
- of the page.
- * ``disqus_forum``: a `Disqus <http://disqus.com>`_ ID you can use to enable comments.
-
- It's probably a bad idea to do a theme that *requires* more than this (please put
- a ``README`` in it saying what the user should add in its ``dodo.py``), but there is no
- problem in requiring less.
-
-post.tmpl
- Template used for blog posts. Can use everything ``base.tmpl`` uses, plus:
-
- * ``post``: a Post object. This has a number of members:
-
- * ``post.title(language)``: returns a localized title
- * ``post.date``
- * ``post.tags``: A list of tags
- * ``post.text(language)``: the translated text of the post
- * ``post.permalink(language, absolute)``: Link to the post in that language.
- If ``absolute`` is ``True`` the link contains the full URL. This is useful
- for things like Disqus comment forms.
- * ``post.next_post`` is None or a Post object that is next newest in the timeline.
- * ``post.prev_post`` is None or a Post object that is next oldest in the timeline.
-
-story.tmpl
- Used for pages that are not part of a blog, usually a cleaner, less
- intrusive layout than ``post.tmpl``, but same parameters.
+ but defines a few blocks that can be re-defined by inheriting templates.
+
+ It has some separate pieces defined in ``base_helper.tmpl`` so they can be
+ easily overriden. For example, the Bootstrap theme adds a ``bootstrap_helper.tmpl``
+ and then uses it to override things defined in base theme's ``base_helper.tmpl``
+
+``index.tmpl``
+ Template used to render the multipost indexes. The posts are in a ``posts`` variable.
+ Some functionality is in the ``index_helper.tmpl`` helper template.
-gallery.tmpl
- Template used for image galleries. Can use everything ``base.tmpl`` uses, plus:
+``comments_helper.tmpl``
+ This template handles comments. You should probably never touch it :-)
+ It uses a bunch of helper templates, one for each supported comment system:
+ ``disqus_helper.tmpl`` ``facebook_helper.tmpl`` ``googleplus_helper.tmpl``
+ ``intensedebate_helper.tmpl`` ``livefyre_helper.tmpl`` ``moot_helper.tmpl``
+
+``crumbs.tmpl`` ``slides.tmpl``
+ These templates help render specific UI items, and can be tweaked as needed.
+
+``gallery.tmpl``
+ Template used for image galleries. Interesting data includes:
* ``text``: A descriptive text for the gallery.
- * ``images``: A list of (thumbnail, image) paths.
+ * ``crumbs``: A list of ``link, crumb`` to implement a crumbbar.
+ * ``folders``: A list of folders to implement hierarchical gallery navigation.
+ * ``enable_comments``: To enable/disable comments in galleries.
+ * ``thumbnail_size``: The ``THUMBNAIL_SIZE`` option.
+ * ``photo_array``: a list of dictionaries, each containing:
+
+ + ``url``: URL for the full-sized image.
+ + ``url_thumb``: URL for the thumbnail.
+ + ``title``: The title of the image.
+ + ``size``: A dict containing ``w`` and ``h``, the real size of the thumbnail.
-index.tmpl
- Template used to render the multipost indexes. Can use everything ``base.tmpl`` uses, plus:
+ * ``photo_array_json``: a JSON dump of photo_array, used in the bootstrap theme by flowr.js
- * ``posts``: a list of Post objects, as described above.
- * ``prevlink``: a link to a previous page
- * ``nextlink``: a link to the next page
+``list.tmpl``
+ Template used to display generic lists of links, which it gets in ``items``,
+ a list of (text, link) elements.
-list.tmpl
- Template used to display generic lists of links. Can use everything ``base.tmpl`` uses, plus:
+``list_post.tmpl``
+ Template used to display generic lists of posts, which it gets in ``posts``.
- * ``items``: a list of (text, link) elements.
+``post.tmpl``
+ Template used by default for blog posts, gets the data in a ``post`` object
+ which is an instance of the Post class. Some functionality is in the
+ ``post_helper.tmpl`` template.
+``story.tmpl``
+ Used for pages that are not part of a blog, usually a cleaner, less
+ intrusive layout than ``post.tmpl``, but same parameters.
-list_post.tmpl
- Template used to display generic lists of links. Can use everything ``base.tmpl`` uses, plus:
+``listing.tmpl``
+ Used to display code listings.
- * ``posts``: a list of Post objects.
+``tags.tmpl``
+ Used to display the list of tags and categories. ``tag.tmpl`` is used to show the contents
+ of a single tag or category.
+You can add other templates for specific pages, which the user can then use in his ``POSTS``
+or ``PAGES`` option in ``conf.py``. Also, keep in mind that your theme is yours,
+there is no reason why you would need to maintain the inheritance as it is, or not
+require whatever data you want.
-You can add other templates for specific pages, which the user can the use in his ``post_pages``
-option in ``dodo.py``. Also, keep in mind that your theme is yours, there is no reason why
-you would need to maintain the inheritance as it is, or not require whatever data you want.
+Also, you can specify a custom template to be used by a post or page via the ``template`` metadata,
+and custom templates can be added in the ``templates/`` folder of your site.
Messages and Translations
-------------------------
@@ -200,3 +188,29 @@ at https://www.transifex.com/projects/p/nikola/
If you want to create a theme that has new strings, and you want those strings to be translatable,
then your theme will need a custom ``messages`` folder.
+
+`LESS <http://lesscss.org/>`__ and `Sass <http://sass-lang.com/>`__
+-------------------------------------------------------------------
+
+.. note::
+ The LESS and Sass compilers will be moved to the Plugins Index in
+ Nikola v7.0.0.
+
+If you want to use those CSS extensions, you can — just store your files
+in the ``less`` or ``sass`` directory of your theme.
+
+In order to have them work, you need to create a list of ``.less`` or
+``.scss/.sass`` files to compile — the list should be in a file named
+``targets`` in the respective directory (``less``/``sass``).
+
+The files listed in the ``targets`` file will be passed to the respective
+compiler, which you have to install manually (``lessc`` which comes from
+the Node.js package named ``less`` or ``sass`` from a Ruby package aptly
+named ``sass``). Whatever the compiler outputs will be saved as a CSS
+file in your rendered site, with the ``.css`` extension.
+
+.. note::
+ Conflicts may occur if you have two files with the same base name
+ but a different extension. Pay attention to how you name your files
+ or your site won’t build! (Nikola will tell you what’s wrong when
+ this happens)
diff --git a/docs/upgrading-to-v6.txt b/docs/upgrading-to-v6.txt
new file mode 100644
index 0000000..386d6ab
--- /dev/null
+++ b/docs/upgrading-to-v6.txt
@@ -0,0 +1,81 @@
+.. title: Upgrading to v6
+.. slug: upgrading-to-v6
+.. date: 2013/08/23 23:00
+.. tags:
+.. link:
+.. description:
+
+Upgrading to v6
+===============
+
+:Version: 6.2.1
+
+Nikola tries fairly hard to be compatible between versions. However, there were
+a few areas which were getting clunky, and needed fxing. So, here's what you may
+need to change in your site.
+
+If you need to change anything else, or something breaks, please write at nikola-discuss
+and I'll add further tweaks here.
+
+Themes
+------
+
+**NOTE**
+ The install_theme command is not working yet, get themes from https://github.com/getnikola/nikola-themes
+
+**NOTE**
+ There is no equivalent for the jinja-default theme yet. If you have a custom,
+ jinja-based theme, upgrading is probably a bad idea right now.
+
+
+Themes have been renamed:
+
+* site => bootstrap
+* orphan => base
+
+Theme added:
+
+* base-jinja
+
+Themes have been moved out of nikola and into nikola-themes:
+
+* default => oldfashioned at nikola-themes
+* jinja-default => not there yet, coming soon
+* monospace => monospace at nikola-themes
+* site-reveal => reveal at nikola-themes
+
+You may have to change your ``THEME`` setting, or change the ``parent`` if you are
+using a custom theme. Also, some templates have been tweaked, but nothing should
+break for you.
+
+Facebook comments support changed the HTML tag to:
+
+.. code-block: html
+
+ <html% if comment_system == 'facebook': xmlns:fb="http://ogp.me/ns/fb#"
+ %endif lang="${lang}">
+
+
+Comments
+~~~~~~~~
+
+If you want a custom theme to support comment systems other than disqus, you will need to:
+
+1) Replace mentions of disqus_helper.tmpl with comments_helper.tmpl
+2) Replace mentions of html_disqus with comment_form
+3) Replace mentions of html_disqus_link with comment_link
+4) Replace mentions of html_disqus_script with comment_link_script
+
+If you don't, your theme should work just fine, but support only disqus comments.
+
+
+Configuration
+-------------
+
+A number of options have been renamed. In all cases, the behaviour should be **exactly** as
+before. If it's not, let me know. Also, you will get warnings. That doesn't mean things
+broke. They are there to inform you of what's happening, and that you need to tweak
+your config.
+
+All the deprecated options will work during the v6 cycle, and only be removed in v7, when
+those warnings will become errors.
diff --git a/nikola/__init__.py b/nikola/__init__.py
index e6a5dd3..a04e3b9 100644
--- a/nikola/__init__.py
+++ b/nikola/__init__.py
@@ -1,4 +1,8 @@
+# -*- coding: utf-8 -*-
+
from __future__ import absolute_import
from .nikola import Nikola # NOQA
from . import plugins # NOQA
+
+__version__ = "6.2.1"
diff --git a/nikola/conf.py.in b/nikola/conf.py.in
index c2bf8ff..20605d8 100644
--- a/nikola/conf.py.in
+++ b/nikola/conf.py.in
@@ -1,13 +1,12 @@
+## -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
-<%text>
-# -*- coding: utf-8 -*-
+
from __future__ import unicode_literals
import time
##############################################
# Configuration, please edit
##############################################
-</%text>
# Data about this site
BLOG_AUTHOR = "${BLOG_AUTHOR}"
@@ -24,15 +23,26 @@ BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}"
# Nikola is multilingual!
#
# Currently supported languages are:
-# English -> en
-# Greek -> gr
-# German -> de
-# French -> fr
-# Polish -> pl
-# Russian -> ru
-# Spanish -> es
-# Italian -> it
-# Simplified Chinese -> zh-cn
+# bg Bulgarian
+# ca Catalan
+# de German
+# el Greek [NOT gr!]
+# en English
+# eo Esperanto
+# es Spanish
+# fa Persian
+# fi Finnish
+# fr French
+# hr Croatian
+# it Italian
+# jp Japanese
+# nl Dutch
+# pt_br Portuguese (Brasil)
+# pl Polish
+# ru Russian
+# sl Slovenian [NOT sl_si!]
+# tr_tr Turkish (Turkey)
+# zh_cn Chinese (Simplified)
#
# If you want to use Nikola with a non-supported language you have to provide
# a module containing the necessary translations
@@ -54,7 +64,7 @@ TRANSLATIONS = {
# Links for the sidebar / navigation bar.
# You should provide a key-value pair for each used language.
-SIDEBAR_LINKS = {
+NAVIGATION_LINKS = {
DEFAULT_LANG: (
('/archive.html', 'Archives'),
('/categories/index.html', 'Tags'),
@@ -62,18 +72,28 @@ SIDEBAR_LINKS = {
),
}
-<%text>
##############################################
# Below this point, everything is optional
##############################################
-</%text>
-# post_pages contains (wildcard, destination, template, use_in_feed) tuples.
+# 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 ommit 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.
+
+# POSTS and PAGES contains (wildcard, destination, template) tuples.
#
# The wildcard is used to generate a list of reSt source files
# (whatever/thing.txt).
-# That fragment must have an associated metadata file (whatever/thing.meta),
-# and opcionally translated files (example for spanish, with code "es"):
+#
+# That fragment could have an associated metadata file (whatever/thing.meta),
+# and optionally translated files (example for spanish, with code "es"):
# whatever/thing.txt.es and whatever/thing.meta.es
#
# From those files, a set of HTML fragment files will be generated:
@@ -83,13 +103,15 @@ SIDEBAR_LINKS = {
# pages, which will be placed at
# output / TRANSLATIONS[lang] / destination / pagename.html
#
-# where "pagename" is specified in the metadata file.
+# where "pagename" is the "slug" specified in the metadata file.
#
-# if use_in_feed is True, then those posts will be added to the site's
-# rss feeds.
+# The difference between POSTS and PAGES is that POSTS are added
+# to feeds and are considered part of a blog, while PAGES are
+# just independent HTML pages.
#
-post_pages = ${POST_PAGES}
+POSTS = ${POSTS}
+PAGES = ${PAGES}
# One or more folders containing files to be copied as-is into the output.
# The format is a dictionary of "source" "relative destination".
@@ -104,7 +126,7 @@ post_pages = ${POST_PAGES}
# 'rest' is reStructuredText
# 'markdown' is MarkDown
# 'html' assumes the file is html and just copies it
-post_compilers = ${POST_COMPILERS}
+COMPILERS = ${COMPILERS}
# Create by default posts in one file format?
# Set to False for two-file posts, with separate metadata.
@@ -134,6 +156,8 @@ post_compilers = ${POST_COMPILERS}
# Create per-month archives instead of per-year
# CREATE_MONTHLY_ARCHIVE = False
+# Create one large archive instead of per-year
+# CREATE_SINGLE_ARCHIVE = False
# Final locations for the archives are:
# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME
# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
@@ -145,6 +169,9 @@ post_compilers = ${POST_COMPILERS}
# output / TRANSLATION[lang] / RSS_PATH / rss.xml
# RSS_PATH = ""
+# Number of posts in RSS feeds
+# FEED_LENGTH = 10
+
# Slug the Tag URL easier for users to type, special characters are
# often removed or replaced as well.
# SLUG_TAG_PATH = True
@@ -161,7 +188,8 @@ post_compilers = ${POST_COMPILERS}
# Commands to execute to deploy. Can be anything, for example,
# you may use rsync:
# "rsync -rav output/* joe@my.site:/srv/www/site"
-# And then do a backup, or ping pingomatic.
+# And then do a backup, or run `nikola ping` from the `ping`
+# plugin (`nikola install_plugin ping`).
# To do manual deployment, set it to []
# DEPLOY_COMMANDS = []
@@ -192,14 +220,25 @@ post_compilers = ${POST_COMPILERS}
# argument.
#
# By default, there are no filters.
+#
+# Many filters are shipped with Nikola. A list is available in the manual:
+# <http://getnikola.com/handbook.html#post-processing-filters>
# FILTERS = {
# ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
# }
-# Create a gzipped copy of each generated file. Cheap server-side optimization.
+# 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
# File extensions that will be compressed
-# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json')
+# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml')
+# Use an external gzip command? None means no.
+# Example: GZIP_COMMAND = "pigz -k {filename}"
+# GZIP_COMMAND = None
+# Make sure the server does not return a "Accept-Ranges: bytes" header for
+# files compressed by this option! OR make sure that a ranged request does not
+# return partial content of another representation for these resources. Do not
+# use this feature if you do not understand what this means.
# #############################################################################
# Image Gallery Options
@@ -211,6 +250,10 @@ post_compilers = ${POST_COMPILERS}
# THUMBNAIL_SIZE = 180
# MAX_IMAGE_SIZE = 1280
# USE_FILENAME_AS_TITLE = True
+# EXTRA_IMAGE_EXTENSIONS = []
+#
+# If set to False, it will sort by filename instead. Defaults to True
+# GALLERY_SORT_BY_DATE = True
# #############################################################################
# HTML fragments and diverse things that are used by the templates
@@ -218,30 +261,36 @@ post_compilers = ${POST_COMPILERS}
# Data about post-per-page indexes
# INDEXES_TITLE = "" # If this is empty, the default is BLOG_TITLE
-# INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d' translated
+# INDEXES_PAGES = "" # If this is empty, the default is 'old posts page %d'
+# translated
# Name of the theme to use.
-# THEME = 'site'
+THEME = "${THEME}"
-# Color scheme to be used for code blocks. If your theme provide "assets/css/code.css" this
-# is ignored.
-# Can be any of autumn borland bw colorful default emacs friendly fruity manni monokai
-# murphy native pastie perldoc rrt tango trac vim vs
-# CODE_COLOR_SCHEME = default
+# Color scheme to be used for code blocks. If your theme provides
+# "assets/css/code.css" this is ignored.
+# Can be any of autumn borland bw colorful default emacs friendly fruity manni
+# monokai murphy native pastie perldoc rrt tango trac vim vs
+# CODE_COLOR_SCHEME = 'default'
# If you use 'site-reveal' theme you can select several subthemes
-# THEME_REVEAL_CONGIF_SUBTHEME = 'sky' # You can also use: beige/serif/simple/night/default
+# 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_CONGIF_TRANSITION = 'cube' # You can also use: page/concave/linear/none/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
-# date format used to display post dates. (str used by datetime.datetime.strftime)
+# date format used to display post dates.
+# (str used by datetime.datetime.strftime)
# DATE_FORMAT = '%Y-%m-%d %H:%M'
# FAVICONS contains (name, file, size) tuples.
# Used for create favicon link like this:
# <link rel="name" href="file" sizes="size"/>
-# about favicons, see: http://www.netmagazine.com/features/create-perfect-favicon
+# For creating favicons, take a look at:
+# http://www.netmagazine.com/features/create-perfect-favicon
# FAVICONS = {
# ("icon", "/favicon.ico", "16x16"),
# ("icon", "/icon_128x128.png", "128x128"),
@@ -250,7 +299,16 @@ post_compilers = ${POST_COMPILERS}
# Show only teasers in the index pages? Defaults to False.
# INDEX_TEASERS = False
-# A HTML fragment describing the license, for the sidebar. Default is "".
+# A HTML fragment with the Read more... link.
+# The following tags exist and are replaced for you:
+# {link} A link to the full post page.
+# {read_more} The string “Read more” in the current language.
+# {{ A literal { (U+007B LEFT CURLY BRACKET)
+# }} A literal } (U+007D RIGHT CURLY BRACKET)
+# READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
+
+# A HTML fragment describing the license, for the sidebar.
+LICENSE = ""
# I recommend using the Creative Commons' wizard:
# http://creativecommons.org/choose/
# LICENSE = """
@@ -261,16 +319,31 @@ post_compilers = ${POST_COMPILERS}
# A small copyright notice for the page footer (in HTML).
# Default is ''
-CONTENT_FOOTER = 'Contents &copy; {date} <a href="mailto:{email}">{author}</a> - Powered by <a href="http://nikola.ralsina.com.ar">Nikola</a>'
+CONTENT_FOOTER = 'Contents &copy; {date} \
+ <a href="mailto:{email}">{author}</a> - Powered by \
+ <a href="http://getnikola.com" rel="nofollow">Nikola</a> \
+ {license}'
CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
author=BLOG_AUTHOR,
- date=time.gmtime().tm_year)
-
-# To enable comments via Disqus, you need to create a forum at
-# http://disqus.com, and set DISQUS_FORUM to the short name you selected.
-# If you want to disable comments, set it to False.
-# Default is "nikolademo", used by the demo sites
-# DISQUS_FORUM = "nikolademo"
+ date=time.gmtime().tm_year,
+ license=LICENSE)
+
+# To use comments, you can choose between different third party comment
+# systems, one of "disqus", "livefyre", "intensedebate", "moot",
+# "googleplus" or "facebook"
+# COMMENT_SYSTEM = "disqus"
+# And you also need to add your COMMENT_SYSTEM_ID which
+# depends on what comment system you use. The default is
+# "nikolademo" which is a test account for Disqus. More information
+# is in the manual.
+# COMMENT_SYSTEM_ID = "nikolademo"
+
+# 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 story folders?
# STORY_INDEX = False
@@ -279,10 +352,52 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# Enable comments on picture gallery pages?
# COMMENTS_IN_GALLERIES = False
-# If a link ends in /index.html, drop the index.html part.
+# What file should be used for directory indexes?
+# Defaults to index.html
+# Common other alternatives: default.html for IIS, index.php
+# INDEX_FILE = "index.html"
+
+# If a link ends in /index.html, drop the index.html part.
# 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)
# Default = False
-# STRIP_INDEX_HTML = False
+# STRIP_INDEXES = False
+
+# 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
+
+# Instead of putting files in <slug>.html, put them in
+# <slug>/index.html. Also enables STRIP_INDEXES
+# This can be disabled on a per-page/post basis by adding
+# .. pretty_url: False
+# to the metadata
+# PRETTY_URLS = False
+
+# If True, publish future dated posts right away instead of scheduling them.
+# Defaults to False.
+# FUTURE_IS_NOW = False
+
+# If True, future dated posts are allowed in deployed output
+# Only the individual posts are published/deployed; not in indexes/sitemap
+# Generally, you want FUTURE_IS_NOW and DEPLOY_FUTURE to be the same value.
+# DEPLOY_FUTURE = False
+# If False, draft posts will not be deployed
+# DEPLOY_DRAFTS = True
+
+# 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
+# SCHEDULE_ALL = False
+# If True, schedules post to today if possible, even if scheduled hour is over
+# SCHEDULE_FORCE_TODAY = False
# Do you want a add a Mathjax config file?
# MATHJAX_CONFIG = ""
@@ -303,9 +418,38 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
#</script>
#"""
-# Enable Addthis social buttons?
-# Defaults to true
-# ADD_THIS_BUTTONS = True
+# Do you want to customize the nbconversion of your IPython notebook?
+# IPYNB_CONFIG = {}
+# With the following example configuracion you can use a custom jinja template
+# called `toggle.tpl` which has to be located in your site/blog main folder:
+# IPYNB_CONFIG = {'Exporter':{'template_file': 'toggle'}}
+
+# What MarkDown extensions to enable?
+# You will also get gist, nikola and podcast because those are
+# done in the code, hope you don't mind ;-)
+# MARKDOWN_EXTENSIONS = ['fenced_code', 'codehilite']
+
+# Social buttons. This is sample code for AddThis (which was the default for a
+# long time). Insert anything you want here, or even make it empty.
+# SOCIAL_BUTTONS_CODE = """
+# <!-- Social buttons -->
+# <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
+# <a class="addthis_button_more">Share</a>
+# <ul><li><a class="addthis_button_facebook"></a>
+# <li><a class="addthis_button_google_plusone_share"></a>
+# <li><a class="addthis_button_linkedin"></a>
+# <li><a class="addthis_button_twitter"></a>
+# </ul>
+# </div>
+# <script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
+# <!-- End of social buttons -->
+# """
+
+# Hide link to source for the posts?
+# HIDE_SOURCELINK = False
+# Copy the source files for your pages?
+# Setting it to False implies HIDE_SOURCELINK = True
+# COPY_SOURCES = True
# Modify the number of Post per Index Page
# Defaults to 10
@@ -325,8 +469,9 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# Default is no search form.
# SEARCH_FORM = ""
#
-# This search form works for any site and looks good in the "site" theme where it
-# appears on the navigation bar
+# This search form works for any site and looks good in the "site" theme where
+# it appears on the navigation bar:
+#
#SEARCH_FORM = """
#<!-- Custom search -->
#<form method="get" id="search" action="http://duckduckgo.com/"
@@ -340,9 +485,48 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
#<input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" />
#</form>
#<!-- End of custom search -->
-#""" % BLOG_URL
+#""" % SITE_URL
#
-# Also, there is a local search plugin you can use.
+# If you prefer a google search form, here's an example that should just work:
+#SEARCH_FORM = """
+#<!-- Custom search with google-->
+#<form id="search" action="http://google.com/search" method="get" class="navbar-form pull-left">
+#<input type="hidden" name="q" value="site:%s" />
+#<input type="text" name="q" maxlength="255" results="0" placeholder="Search"/>
+#</form>
+#<!-- End of custom search -->
+#""" % SITE_URL
+
+# Also, there is a local search plugin you can use, based on Tipue, but it requires setting several
+# options:
+
+# SEARCH_FORM = """
+# <span class="navbar-form pull-left">
+# <input type="text" id="tipue_search_input">
+# </span>"""
+#
+# BODY_END = """
+# <script type="text/javascript" src="/assets/js/tipuesearch_set.js"></script>
+# <script type="text/javascript" src="/assets/js/tipuesearch.js"></script>
+# <script type="text/javascript">
+# $(document).ready(function() {
+ # $('#tipue_search_input').tipuesearch({
+ # 'mode': 'json',
+ # 'contentLocation': '/assets/js/tipuesearch_content.json',
+ # 'showUrl': false
+ # });
+# });
+# </script>
+# """
+
+# EXTRA_HEAD_DATA = """
+# <link rel="stylesheet" type="text/css" href="/assets/css/tipuesearch.css">
+# <div id="tipue_search_content" style="margin-left: auto; margin-right: auto; padding: 20px;"></div>
+# """
+# ENABLED_EXTRAS = ['local_search']
+#
+###### End of local search example
+
# Use content distribution networks for jquery and twitter-bootstrap css and js
# If this is True, jquery is served from the Google CDN and twitter-bootstrap
@@ -356,7 +540,7 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# EXTRA_HEAD_DATA = ""
# Google analytics or whatever else you use. Added to the bottom of <body>
# in the default template (base.tmpl).
-# ANALYTICS = ""
+# BODY_END = ""
# The possibility to extract metadata from the filename by using a
# regular expression.
@@ -373,13 +557,17 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md'
# FILE_METADATA_REGEXP = None
+# Additional metadata that is added to a post when creating a new_post
+# ADDITIONAL_METADATA = {}
+
# Nikola supports Twitter Card summaries / Open Graph.
# Twitter cards make it possible for you to attach media to Tweets
# that link to your content.
#
# IMPORTANT:
# Please note, that you need to opt-in for using Twitter Cards!
-# To do this please visit https://dev.twitter.com/form/participate-twitter-cards
+# To do this please visit
+# https://dev.twitter.com/form/participate-twitter-cards
#
# Uncomment and modify to following lines to match your accounts.
# Specifying the id for either 'site' or 'creator' will be preferred
@@ -388,14 +576,19 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# TWITTER_CARD = {
# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph
# # 'site': '@website', # twitter nick for the website
-# # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead.
+# # 'site:id': 123456, # Same as site, but the website's Twitter user ID
+# # instead.
# # 'creator': '@username', # Username for the content creator / author.
# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID.
# }
-# If you want to use formatted post time in W3C-DTF Format(ex. 2012-03-30T23:00:00+02:00),
-# set timzone if you want a localized posted date.
+# Post's dates are considered in GMT by default, if you want to use
+# another timezone, please set TIMEZONE to match. Check the available
+# list from Wikipedia:
+# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+# Also, if you want to use a different timezone in some of your posts,
+# you can use W3C-DTF Format (ex. 2012-03-30T23:00:00+02:00)
#
# TIMEZONE = 'Europe/Zurich'
@@ -406,14 +599,37 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# DISABLED_PLUGINS = ["render_galleries"]
# Experimental plugins - use at your own risk.
-# They probably need some manual adjustments - please see their respective readme.
+# They probably need some manual adjustments - please see their respective
+# readme.
# ENABLED_EXTRAS = [
# 'planetoid',
# 'ipynb',
-# 'localsearch',
-# 'mustache',
+# 'local_search',
+# 'render_mustache',
# ]
+# List of regular expressions, links matching them will always be considered
+# valid by "nikola check -l"
+# LINK_CHECK_WHITELIST = []
+
+# If set to True, enable optional hyphenation in your posts (requires pyphen)
+# HYPHENATE = False
+
+# You can configure the logging handlers installed as plugins or change the
+# log level of the default stdout handler.
+LOGGING_HANDLERS = {
+ 'stderr': {'loglevel': 'WARNING', 'bubble': True},
+ #'smtp': {
+ # 'from_addr': 'test-errors@example.com',
+ # 'recipients': ('test@example.com'),
+ # 'credentials':('testusername', 'password'),
+ # 'server_addr': ('127.0.0.1', 25),
+ # 'secure': (),
+ # 'level': 'DEBUG',
+ # 'bubble': True
+ #}
+}
+
# Put in global_context things you want available on all your templates.
# It can be anything, data, functions, modules, etc.
diff --git a/nikola/data/samplesite/README.txt b/nikola/data/samplesite/README.txt
index a1220d0..aeea39c 100644
--- a/nikola/data/samplesite/README.txt
+++ b/nikola/data/samplesite/README.txt
@@ -1,6 +1,6 @@
This folder contains the source used to generate a static site by nikola.
-Installation and documentation at http://nikola.ralsina.com.ar
+Installation and documentation at http://getnikola.com
Configuration file for the site is `conf.py`.
diff --git a/nikola/data/samplesite/posts/1.meta b/nikola/data/samplesite/posts/1.meta
deleted file mode 100644
index 3bc104a..0000000
--- a/nikola/data/samplesite/posts/1.meta
+++ /dev/null
@@ -1,5 +0,0 @@
-Welcome to Nikola
-welcome-to-nikola
-2012/03/30 23:00
-nikola, python, demo, blog
-http://nikola.ralsina.com.ar
diff --git a/nikola/data/samplesite/posts/1.txt b/nikola/data/samplesite/posts/1.rst
index 2e6c3ba..25e56f8 100644
--- a/nikola/data/samplesite/posts/1.txt
+++ b/nikola/data/samplesite/posts/1.rst
@@ -1,3 +1,10 @@
+.. title: Welcome to Nikola
+.. slug: welcome-to-nikola
+.. date: 2012/03/30 23:00
+.. tags: nikola, python, demo, blog
+.. link: http://getnikola.com
+.. description:
+
.. figure:: http://farm1.staticflickr.com/138/352972944_4f9d568680.jpg
:target: http://farm1.staticflickr.com/138/352972944_4f9d568680_z.jpg?zz=1
:class: thumbnail
@@ -7,9 +14,10 @@ If you can see this in a web browser, it means you have managed to install Nikol
and build a site using it. Congratulations!
* You can read the manual `here </stories/handbook.html>`__
-* You can learn more about Nikola at http://nikola.ralsina.com.ar
+* You can learn more about Nikola at http://getnikola.com
* You can see a demo photo gallery `here </galleries/demo/index.html>`__
* Demo usage of listings `here </stories/listings-demo.html>`__
* Demo of slideshows `here </stories/slides-demo.html>`__
+* Demo of Bootstrap `here </stories/bootstrap-demo.html>`__
Send feedback to ralsina@netmanagers.com.ar!
diff --git a/nikola/data/samplesite/stories/1.meta b/nikola/data/samplesite/stories/1.meta
deleted file mode 100644
index e19bf10..0000000
--- a/nikola/data/samplesite/stories/1.meta
+++ /dev/null
@@ -1,4 +0,0 @@
-Nikola: it generates static
-about-nikola
-2012/03/30 23:00
-
diff --git a/nikola/data/samplesite/stories/1.rst b/nikola/data/samplesite/stories/1.rst
new file mode 100644
index 0000000..27c75d8
--- /dev/null
+++ b/nikola/data/samplesite/stories/1.rst
@@ -0,0 +1,11 @@
+.. title: Nikola: it generates static
+.. slug: about-nikola
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
+Hope you enjoy this software!
+
+* Home page at http://getnikola.com
+* Author's blog (and reason why Nikola exists): http://ralsina.me
diff --git a/nikola/data/samplesite/stories/1.txt b/nikola/data/samplesite/stories/1.txt
deleted file mode 100644
index 68b4008..0000000
--- a/nikola/data/samplesite/stories/1.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Hope you enjoy this software!
-
-* Home page at http://nikola.ralsina.com.ar
-* Author's blog (and reason why Nikola exists): http://lateral.netmanagers.com.ar
diff --git a/nikola/data/samplesite/stories/a-study-in-scarlet.txt b/nikola/data/samplesite/stories/a-study-in-scarlet.txt
new file mode 100644
index 0000000..10f9528
--- /dev/null
+++ b/nikola/data/samplesite/stories/a-study-in-scarlet.txt
@@ -0,0 +1,5139 @@
+.. link:
+.. description:
+.. tags:
+.. date: 2013/08/27 18:20:55
+.. title: A STUDY IN SCARLET.
+.. slug: a-study-in-scarlet
+
+===================
+A STUDY IN SCARLET.
+===================
+
+By A. Conan Doyle [1]_
+======================
+
+
+:Title: A Study In Scarlet
+:Author: Arthur Conan Doyle
+:Posting Date: July 12, 2008 [EBook #244]
+:Release Date: April, 1995 [Last updated: February 17, 2013]
+:Language: English
+:Produced by: Roger Squires
+
+.. class:: alert alert-info pull-right
+
+.. contents::
+
+------------------
+
+.. class:: pull-right
+
+.. note:: The Project Gutenberg EBook of A Study In Scarlet, by Arthur Conan Doyle
+
+
+ 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
+
+
+ Original Transcriber's Note: This etext is prepared directly
+ from an 1887 edition, and care has been taken to duplicate the
+ original exactly, including typographical and punctuation
+ vagaries.
+
+ Additions to the text include adding the underscore character to
+ indicate italics, and textual end-notes in square braces.
+
+ Project Gutenberg Editor's Note: In reproofing and moving old PG
+ files such as this to the present PG directory system it is the
+ policy to reformat the text to conform to present PG Standards.
+ In this case however, in consideration of the note above of the
+ original transcriber describing his care to try to duplicate the
+ original 1887 edition as to typography and punctuation vagaries,
+ no changes have been made in this ascii text file. However, in
+ the Latin-1 file and this html file, present standards are
+ followed and the several French and Spanish words have been
+ given their proper accents.
+
+ Part II, The Country of the Saints, deals much with the Mormon Church.
+
+
+PART I.
+-------
+
+(Being a reprint from the reminiscences of JOHN H. WATSON, M.D., late of the Army Medical Department.) [2]_
+
+
+CHAPTER I. MR. SHERLOCK HOLMES.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+IN the year 1878 I took my degree of Doctor of Medicine of the
+University of London, and proceeded to Netley to go through the course
+prescribed for surgeons in the army. Having completed my studies there,
+I was duly attached to the Fifth Northumberland Fusiliers as Assistant
+Surgeon. The regiment was stationed in India at the time, and before
+I could join it, the second Afghan war had broken out. On landing at
+Bombay, I learned that my corps had advanced through the passes, and
+was already deep in the enemy's country. I followed, however, with many
+other officers who were in the same situation as myself, and succeeded
+in reaching Candahar in safety, where I found my regiment, and at once
+entered upon my new duties.
+
+The campaign brought honours and promotion to many, but for me it had
+nothing but misfortune and disaster. I was removed from my brigade and
+attached to the Berkshires, with whom I served at the fatal battle of
+Maiwand. There I was struck on the shoulder by a Jezail bullet, which
+shattered the bone and grazed the subclavian artery. I should have
+fallen into the hands of the murderous Ghazis had it not been for the
+devotion and courage shown by Murray, my orderly, who threw me across a
+pack-horse, and succeeded in bringing me safely to the British lines.
+
+Worn with pain, and weak from the prolonged hardships which I had
+undergone, I was removed, with a great train of wounded sufferers, to
+the base hospital at Peshawar. Here I rallied, and had already improved
+so far as to be able to walk about the wards, and even to bask a little
+upon the verandah, when I was struck down by enteric fever, that curse
+of our Indian possessions. For months my life was despaired of, and
+when at last I came to myself and became convalescent, I was so weak and
+emaciated that a medical board determined that not a day should be lost
+in sending me back to England. I was dispatched, accordingly, in the
+troopship "Orontes," and landed a month later on Portsmouth jetty, with
+my health irretrievably ruined, but with permission from a paternal
+government to spend the next nine months in attempting to improve it.
+
+I had neither kith nor kin in England, and was therefore as free as
+air--or as free as an income of eleven shillings and sixpence a day will
+permit a man to be. Under such circumstances, I naturally gravitated to
+London, that great cesspool into which all the loungers and idlers of
+the Empire are irresistibly drained. There I stayed for some time at
+a private hotel in the Strand, leading a comfortless, meaningless
+existence, and spending such money as I had, considerably more freely
+than I ought. So alarming did the state of my finances become, that
+I soon realized that I must either leave the metropolis and rusticate
+somewhere in the country, or that I must make a complete alteration in
+my style of living. Choosing the latter alternative, I began by making
+up my mind to leave the hotel, and to take up my quarters in some less
+pretentious and less expensive domicile.
+
+On the very day that I had come to this conclusion, I was standing at
+the Criterion Bar, when some one tapped me on the shoulder, and turning
+round I recognized young Stamford, who had been a dresser under me at
+Barts. The sight of a friendly face in the great wilderness of London is
+a pleasant thing indeed to a lonely man. In old days Stamford had never
+been a particular crony of mine, but now I hailed him with enthusiasm,
+and he, in his turn, appeared to be delighted to see me. In the
+exuberance of my joy, I asked him to lunch with me at the Holborn, and
+we started off together in a hansom.
+
+"Whatever have you been doing with yourself, Watson?" he asked in
+undisguised wonder, as we rattled through the crowded London streets.
+"You are as thin as a lath and as brown as a nut."
+
+I gave him a short sketch of my adventures, and had hardly concluded it
+by the time that we reached our destination.
+
+"Poor devil!" he said, commiseratingly, after he had listened to my
+misfortunes. "What are you up to now?"
+
+"Looking for lodgings." [3]_ I answered. "Trying to solve the problem
+as to whether it is possible to get comfortable rooms at a reasonable
+price."
+
+"That's a strange thing," remarked my companion; "you are the second man
+to-day that has used that expression to me."
+
+"And who was the first?" I asked.
+
+"A fellow who is working at the chemical laboratory up at the hospital.
+He was bemoaning himself this morning because he could not get someone
+to go halves with him in some nice rooms which he had found, and which
+were too much for his purse."
+
+"By Jove!" I cried, "if he really wants someone to share the rooms and
+the expense, I am the very man for him. I should prefer having a partner
+to being alone."
+
+Young Stamford looked rather strangely at me over his wine-glass. "You
+don't know Sherlock Holmes yet," he said; "perhaps you would not care
+for him as a constant companion."
+
+"Why, what is there against him?"
+
+"Oh, I didn't say there was anything against him. He is a little queer
+in his ideas--an enthusiast in some branches of science. As far as I
+know he is a decent fellow enough."
+
+"A medical student, I suppose?" said I.
+
+"No--I have no idea what he intends to go in for. I believe he is well
+up in anatomy, and he is a first-class chemist; but, as far as I know,
+he has never taken out any systematic medical classes. His studies are
+very desultory and eccentric, but he has amassed a lot of out-of-the way
+knowledge which would astonish his professors."
+
+"Did you never ask him what he was going in for?" I asked.
+
+"No; he is not a man that it is easy to draw out, though he can be
+communicative enough when the fancy seizes him."
+
+"I should like to meet him," I said. "If I am to lodge with anyone, I
+should prefer a man of studious and quiet habits. I am not strong
+enough yet to stand much noise or excitement. I had enough of both in
+Afghanistan to last me for the remainder of my natural existence. How
+could I meet this friend of yours?"
+
+"He is sure to be at the laboratory," returned my companion. "He either
+avoids the place for weeks, or else he works there from morning to
+night. If you like, we shall drive round together after luncheon."
+
+"Certainly," I answered, and the conversation drifted away into other
+channels.
+
+As we made our way to the hospital after leaving the Holborn, Stamford
+gave me a few more particulars about the gentleman whom I proposed to
+take as a fellow-lodger.
+
+"You mustn't blame me if you don't get on with him," he said; "I know
+nothing more of him than I have learned from meeting him occasionally in
+the laboratory. You proposed this arrangement, so you must not hold me
+responsible."
+
+"If we don't get on it will be easy to part company," I answered. "It
+seems to me, Stamford," I added, looking hard at my companion, "that you
+have some reason for washing your hands of the matter. Is this fellow's
+temper so formidable, or what is it? Don't be mealy-mouthed about it."
+
+"It is not easy to express the inexpressible," he answered with a laugh.
+"Holmes is a little too scientific for my tastes--it approaches to
+cold-bloodedness. I could imagine his giving a friend a little pinch of
+the latest vegetable alkaloid, not out of malevolence, you understand,
+but simply out of a spirit of inquiry in order to have an accurate idea
+of the effects. To do him justice, I think that he would take it himself
+with the same readiness. He appears to have a passion for definite and
+exact knowledge."
+
+"Very right too."
+
+"Yes, but it may be pushed to excess. When it comes to beating the
+subjects in the dissecting-rooms with a stick, it is certainly taking
+rather a bizarre shape."
+
+"Beating the subjects!"
+
+"Yes, to verify how far bruises may be produced after death. I saw him
+at it with my own eyes."
+
+"And yet you say he is not a medical student?"
+
+"No. Heaven knows what the objects of his studies are. But here we
+are, and you must form your own impressions about him." As he spoke, we
+turned down a narrow lane and passed through a small side-door, which
+opened into a wing of the great hospital. It was familiar ground to me,
+and I needed no guiding as we ascended the bleak stone staircase and
+made our way down the long corridor with its vista of whitewashed
+wall and dun-coloured doors. Near the further end a low arched passage
+branched away from it and led to the chemical laboratory.
+
+This was a lofty chamber, lined and littered with countless bottles.
+Broad, low tables were scattered about, which bristled with retorts,
+test-tubes, and little Bunsen lamps, with their blue flickering flames.
+There was only one student in the room, who was bending over a distant
+table absorbed in his work. At the sound of our steps he glanced round
+and sprang to his feet with a cry of pleasure. "I've found it! I've
+found it," he shouted to my companion, running towards us with a
+test-tube in his hand. "I have found a re-agent which is precipitated
+by hoemoglobin, [4]_ and by nothing else." Had he discovered a gold mine,
+greater delight could not have shone upon his features.
+
+"Dr. Watson, Mr. Sherlock Holmes," said Stamford, introducing us.
+
+"How are you?" he said cordially, gripping my hand with a strength
+for which I should hardly have given him credit. "You have been in
+Afghanistan, I perceive."
+
+"How on earth did you know that?" I asked in astonishment.
+
+"Never mind," said he, chuckling to himself. "The question now is about
+hoemoglobin. No doubt you see the significance of this discovery of
+mine?"
+
+"It is interesting, chemically, no doubt," I answered, "but
+practically----"
+
+"Why, man, it is the most practical medico-legal discovery for years.
+Don't you see that it gives us an infallible test for blood stains. Come
+over here now!" He seized me by the coat-sleeve in his eagerness, and
+drew me over to the table at which he had been working. "Let us have
+some fresh blood," he said, digging a long bodkin into his finger, and
+drawing off the resulting drop of blood in a chemical pipette. "Now, I
+add this small quantity of blood to a litre of water. You perceive that
+the resulting mixture has the appearance of pure water. The proportion
+of blood cannot be more than one in a million. I have no doubt, however,
+that we shall be able to obtain the characteristic reaction." As he
+spoke, he threw into the vessel a few white crystals, and then added
+some drops of a transparent fluid. In an instant the contents assumed a
+dull mahogany colour, and a brownish dust was precipitated to the bottom
+of the glass jar.
+
+"Ha! ha!" he cried, clapping his hands, and looking as delighted as a
+child with a new toy. "What do you think of that?"
+
+"It seems to be a very delicate test," I remarked.
+
+"Beautiful! beautiful! The old Guiacum test was very clumsy and
+uncertain. So is the microscopic examination for blood corpuscles. The
+latter is valueless if the stains are a few hours old. Now, this appears
+to act as well whether the blood is old or new. Had this test been
+invented, there are hundreds of men now walking the earth who would long
+ago have paid the penalty of their crimes."
+
+"Indeed!" I murmured.
+
+"Criminal cases are continually hinging upon that one point. A man is
+suspected of a crime months perhaps after it has been committed. His
+linen or clothes are examined, and brownish stains discovered upon them.
+Are they blood stains, or mud stains, or rust stains, or fruit stains,
+or what are they? That is a question which has puzzled many an expert,
+and why? Because there was no reliable test. Now we have the Sherlock
+Holmes' test, and there will no longer be any difficulty."
+
+His eyes fairly glittered as he spoke, and he put his hand over his
+heart and bowed as if to some applauding crowd conjured up by his
+imagination.
+
+"You are to be congratulated," I remarked, considerably surprised at his
+enthusiasm.
+
+"There was the case of Von Bischoff at Frankfort last year. He would
+certainly have been hung had this test been in existence. Then there was
+Mason of Bradford, and the notorious Muller, and Lefevre of Montpellier,
+and Samson of New Orleans. I could name a score of cases in which it
+would have been decisive."
+
+"You seem to be a walking calendar of crime," said Stamford with a
+laugh. "You might start a paper on those lines. Call it the 'Police News
+of the Past.'"
+
+"Very interesting reading it might be made, too," remarked Sherlock
+Holmes, sticking a small piece of plaster over the prick on his finger.
+"I have to be careful," he continued, turning to me with a smile, "for I
+dabble with poisons a good deal." He held out his hand as he spoke, and
+I noticed that it was all mottled over with similar pieces of plaster,
+and discoloured with strong acids.
+
+"We came here on business," said Stamford, sitting down on a high
+three-legged stool, and pushing another one in my direction with
+his foot. "My friend here wants to take diggings, and as you were
+complaining that you could get no one to go halves with you, I thought
+that I had better bring you together."
+
+Sherlock Holmes seemed delighted at the idea of sharing his rooms with
+me. "I have my eye on a suite in Baker Street," he said, "which would
+suit us down to the ground. You don't mind the smell of strong tobacco,
+I hope?"
+
+"I always smoke 'ship's' myself," I answered.
+
+"That's good enough. I generally have chemicals about, and occasionally
+do experiments. Would that annoy you?"
+
+"By no means."
+
+"Let me see--what are my other shortcomings. I get in the dumps at
+times, and don't open my mouth for days on end. You must not think I am
+sulky when I do that. Just let me alone, and I'll soon be right. What
+have you to confess now? It's just as well for two fellows to know the
+worst of one another before they begin to live together."
+
+I laughed at this cross-examination. "I keep a bull pup," I said, "and
+I object to rows because my nerves are shaken, and I get up at all sorts
+of ungodly hours, and I am extremely lazy. I have another set of vices
+when I'm well, but those are the principal ones at present."
+
+"Do you include violin-playing in your category of rows?" he asked,
+anxiously.
+
+"It depends on the player," I answered. "A well-played violin is a treat
+for the gods--a badly-played one----"
+
+"Oh, that's all right," he cried, with a merry laugh. "I think we may
+consider the thing as settled--that is, if the rooms are agreeable to
+you."
+
+"When shall we see them?"
+
+"Call for me here at noon to-morrow, and we'll go together and settle
+everything," he answered.
+
+"All right--noon exactly," said I, shaking his hand.
+
+We left him working among his chemicals, and we walked together towards
+my hotel.
+
+"By the way," I asked suddenly, stopping and turning upon Stamford, "how
+the deuce did he know that I had come from Afghanistan?"
+
+My companion smiled an enigmatical smile. "That's just his little
+peculiarity," he said. "A good many people have wanted to know how he
+finds things out."
+
+"Oh! a mystery is it?" I cried, rubbing my hands. "This is very piquant.
+I am much obliged to you for bringing us together. 'The proper study of
+mankind is man,' you know."
+
+"You must study him, then," Stamford said, as he bade me good-bye.
+"You'll find him a knotty problem, though. I'll wager he learns more
+about you than you about him. Good-bye."
+
+"Good-bye," I answered, and strolled on to my hotel, considerably
+interested in my new acquaintance.
+
+
+
+
+CHAPTER II. THE SCIENCE OF DEDUCTION.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+WE met next day as he had arranged, and inspected the rooms at No. 221B,
+[5]_ Baker Street, of which he had spoken at our meeting. They
+consisted of a couple of comfortable bed-rooms and a single large
+airy sitting-room, cheerfully furnished, and illuminated by two broad
+windows. So desirable in every way were the apartments, and so moderate
+did the terms seem when divided between us, that the bargain was
+concluded upon the spot, and we at once entered into possession.
+That very evening I moved my things round from the hotel, and on the
+following morning Sherlock Holmes followed me with several boxes and
+portmanteaus. For a day or two we were busily employed in unpacking and
+laying out our property to the best advantage. That done, we
+gradually began to settle down and to accommodate ourselves to our new
+surroundings.
+
+Holmes was certainly not a difficult man to live with. He was quiet
+in his ways, and his habits were regular. It was rare for him to be
+up after ten at night, and he had invariably breakfasted and gone out
+before I rose in the morning. Sometimes he spent his day at the chemical
+laboratory, sometimes in the dissecting-rooms, and occasionally in long
+walks, which appeared to take him into the lowest portions of the City.
+Nothing could exceed his energy when the working fit was upon him; but
+now and again a reaction would seize him, and for days on end he would
+lie upon the sofa in the sitting-room, hardly uttering a word or moving
+a muscle from morning to night. On these occasions I have noticed such
+a dreamy, vacant expression in his eyes, that I might have suspected him
+of being addicted to the use of some narcotic, had not the temperance
+and cleanliness of his whole life forbidden such a notion.
+
+As the weeks went by, my interest in him and my curiosity as to his
+aims in life, gradually deepened and increased. His very person and
+appearance were such as to strike the attention of the most casual
+observer. In height he was rather over six feet, and so excessively
+lean that he seemed to be considerably taller. His eyes were sharp and
+piercing, save during those intervals of torpor to which I have alluded;
+and his thin, hawk-like nose gave his whole expression an air of
+alertness and decision. His chin, too, had the prominence and squareness
+which mark the man of determination. His hands were invariably
+blotted with ink and stained with chemicals, yet he was possessed of
+extraordinary delicacy of touch, as I frequently had occasion to observe
+when I watched him manipulating his fragile philosophical instruments.
+
+The reader may set me down as a hopeless busybody, when I confess how
+much this man stimulated my curiosity, and how often I endeavoured
+to break through the reticence which he showed on all that concerned
+himself. Before pronouncing judgment, however, be it remembered, how
+objectless was my life, and how little there was to engage my attention.
+My health forbade me from venturing out unless the weather was
+exceptionally genial, and I had no friends who would call upon me and
+break the monotony of my daily existence. Under these circumstances, I
+eagerly hailed the little mystery which hung around my companion, and
+spent much of my time in endeavouring to unravel it.
+
+He was not studying medicine. He had himself, in reply to a question,
+confirmed Stamford's opinion upon that point. Neither did he appear to
+have pursued any course of reading which might fit him for a degree in
+science or any other recognized portal which would give him an entrance
+into the learned world. Yet his zeal for certain studies was remarkable,
+and within eccentric limits his knowledge was so extraordinarily ample
+and minute that his observations have fairly astounded me. Surely no man
+would work so hard or attain such precise information unless he had some
+definite end in view. Desultory readers are seldom remarkable for the
+exactness of their learning. No man burdens his mind with small matters
+unless he has some very good reason for doing so.
+
+His ignorance was as remarkable as his knowledge. Of contemporary
+literature, philosophy and politics he appeared to know next to nothing.
+Upon my quoting Thomas Carlyle, he inquired in the naivest way who he
+might be and what he had done. My surprise reached a climax, however,
+when I found incidentally that he was ignorant of the Copernican Theory
+and of the composition of the Solar System. That any civilized human
+being in this nineteenth century should not be aware that the earth
+travelled round the sun appeared to be to me such an extraordinary fact
+that I could hardly realize it.
+
+"You appear to be astonished," he said, smiling at my expression of
+surprise. "Now that I do know it I shall do my best to forget it."
+
+"To forget it!"
+
+"You see," he explained, "I consider that a man's brain originally is
+like a little empty attic, and you have to stock it with such furniture
+as you choose. A fool takes in all the lumber of every sort that he
+comes across, so that the knowledge which might be useful to him gets
+crowded out, or at best is jumbled up with a lot of other things so that
+he has a difficulty in laying his hands upon it. Now the skilful workman
+is very careful indeed as to what he takes into his brain-attic. He will
+have nothing but the tools which may help him in doing his work, but of
+these he has a large assortment, and all in the most perfect order. It
+is a mistake to think that that little room has elastic walls and can
+distend to any extent. Depend upon it there comes a time when for every
+addition of knowledge you forget something that you knew before. It is
+of the highest importance, therefore, not to have useless facts elbowing
+out the useful ones."
+
+"But the Solar System!" I protested.
+
+"What the deuce is it to me?" he interrupted impatiently; "you say
+that we go round the sun. If we went round the moon it would not make a
+pennyworth of difference to me or to my work."
+
+I was on the point of asking him what that work might be, but something
+in his manner showed me that the question would be an unwelcome one. I
+pondered over our short conversation, however, and endeavoured to draw
+my deductions from it. He said that he would acquire no knowledge which
+did not bear upon his object. Therefore all the knowledge which he
+possessed was such as would be useful to him. I enumerated in my own
+mind all the various points upon which he had shown me that he was
+exceptionally well-informed. I even took a pencil and jotted them down.
+I could not help smiling at the document when I had completed it. It ran
+in this way--
+
+
+SHERLOCK HOLMES--his limits.
+
+1. Knowledge of Literature.--Nil.
+2. Philosophy.--Nil.
+3. Astronomy.--Nil.
+4. Politics.--Feeble.
+5. Botany.--Variable.
+ Well up in belladonna,
+ opium, and poisons generally.
+ Knows nothing of practical gardening.
+6. Geology.--Practical, but limited.
+ Tells at a glance different soils
+ from each other. After walks has
+ shown me splashes upon his trousers,
+ and told me by their colour and
+ consistence in what part of London
+ he had received them.
+7. Chemistry.--Profound.
+8. Anatomy.--Accurate, but unsystematic.
+9. Sensational Literature.--Immense. He appears to know every detail of every horror perpetrated in the century.
+10. Plays the violin well.
+11. Is an expert singlestick player, boxer, and swordsman.
+12. Has a good practical knowledge of British law.
+
+
+When I had got so far in my list I threw it into the fire in despair.
+"If I can only find what the fellow is driving at by reconciling all
+these accomplishments, and discovering a calling which needs them all,"
+I said to myself, "I may as well give up the attempt at once."
+
+I see that I have alluded above to his powers upon the violin. These
+were very remarkable, but as eccentric as all his other accomplishments.
+That he could play pieces, and difficult pieces, I knew well, because
+at my request he has played me some of Mendelssohn's Lieder, and other
+favourites. When left to himself, however, he would seldom produce any
+music or attempt any recognized air. Leaning back in his arm-chair of
+an evening, he would close his eyes and scrape carelessly at the fiddle
+which was thrown across his knee. Sometimes the chords were sonorous and
+melancholy. Occasionally they were fantastic and cheerful. Clearly they
+reflected the thoughts which possessed him, but whether the music aided
+those thoughts, or whether the playing was simply the result of a whim
+or fancy was more than I could determine. I might have rebelled against
+these exasperating solos had it not been that he usually terminated them
+by playing in quick succession a whole series of my favourite airs as a
+slight compensation for the trial upon my patience.
+
+During the first week or so we had no callers, and I had begun to think
+that my companion was as friendless a man as I was myself. Presently,
+however, I found that he had many acquaintances, and those in the most
+different classes of society. There was one little sallow rat-faced,
+dark-eyed fellow who was introduced to me as Mr. Lestrade, and who came
+three or four times in a single week. One morning a young girl called,
+fashionably dressed, and stayed for half an hour or more. The same
+afternoon brought a grey-headed, seedy visitor, looking like a Jew
+pedlar, who appeared to me to be much excited, and who was closely
+followed by a slip-shod elderly woman. On another occasion an old
+white-haired gentleman had an interview with my companion; and on
+another a railway porter in his velveteen uniform. When any of these
+nondescript individuals put in an appearance, Sherlock Holmes used to
+beg for the use of the sitting-room, and I would retire to my bed-room.
+He always apologized to me for putting me to this inconvenience. "I have
+to use this room as a place of business," he said, "and these people
+are my clients." Again I had an opportunity of asking him a point blank
+question, and again my delicacy prevented me from forcing another man to
+confide in me. I imagined at the time that he had some strong reason for
+not alluding to it, but he soon dispelled the idea by coming round to
+the subject of his own accord.
+
+It was upon the 4th of March, as I have good reason to remember, that I
+rose somewhat earlier than usual, and found that Sherlock Holmes had not
+yet finished his breakfast. The landlady had become so accustomed to my
+late habits that my place had not been laid nor my coffee prepared. With
+the unreasonable petulance of mankind I rang the bell and gave a curt
+intimation that I was ready. Then I picked up a magazine from the table
+and attempted to while away the time with it, while my companion munched
+silently at his toast. One of the articles had a pencil mark at the
+heading, and I naturally began to run my eye through it.
+
+Its somewhat ambitious title was "The Book of Life," and it attempted to
+show how much an observant man might learn by an accurate and systematic
+examination of all that came in his way. It struck me as being a
+remarkable mixture of shrewdness and of absurdity. The reasoning was
+close and intense, but the deductions appeared to me to be far-fetched
+and exaggerated. The writer claimed by a momentary expression, a twitch
+of a muscle or a glance of an eye, to fathom a man's inmost thoughts.
+Deceit, according to him, was an impossibility in the case of one
+trained to observation and analysis. His conclusions were as infallible
+as so many propositions of Euclid. So startling would his results appear
+to the uninitiated that until they learned the processes by which he had
+arrived at them they might well consider him as a necromancer.
+
+"From a drop of water," said the writer, "a logician could infer the
+possibility of an Atlantic or a Niagara without having seen or heard of
+one or the other. So all life is a great chain, the nature of which is
+known whenever we are shown a single link of it. Like all other arts,
+the Science of Deduction and Analysis is one which can only be acquired
+by long and patient study nor is life long enough to allow any mortal
+to attain the highest possible perfection in it. Before turning to
+those moral and mental aspects of the matter which present the greatest
+difficulties, let the enquirer begin by mastering more elementary
+problems. Let him, on meeting a fellow-mortal, learn at a glance to
+distinguish the history of the man, and the trade or profession to
+which he belongs. Puerile as such an exercise may seem, it sharpens the
+faculties of observation, and teaches one where to look and what to look
+for. By a man's finger nails, by his coat-sleeve, by his boot, by his
+trouser knees, by the callosities of his forefinger and thumb, by his
+expression, by his shirt cuffs--by each of these things a man's calling
+is plainly revealed. That all united should fail to enlighten the
+competent enquirer in any case is almost inconceivable."
+
+"What ineffable twaddle!" I cried, slapping the magazine down on the
+table, "I never read such rubbish in my life."
+
+"What is it?" asked Sherlock Holmes.
+
+"Why, this article," I said, pointing at it with my egg spoon as I sat
+down to my breakfast. "I see that you have read it since you have marked
+it. I don't deny that it is smartly written. It irritates me though. It
+is evidently the theory of some arm-chair lounger who evolves all these
+neat little paradoxes in the seclusion of his own study. It is not
+practical. I should like to see him clapped down in a third class
+carriage on the Underground, and asked to give the trades of all his
+fellow-travellers. I would lay a thousand to one against him."
+
+"You would lose your money," Sherlock Holmes remarked calmly. "As for
+the article I wrote it myself."
+
+"You!"
+
+"Yes, I have a turn both for observation and for deduction. The
+theories which I have expressed there, and which appear to you to be so
+chimerical are really extremely practical--so practical that I depend
+upon them for my bread and cheese."
+
+"And how?" I asked involuntarily.
+
+"Well, I have a trade of my own. I suppose I am the only one in the
+world. I'm a consulting detective, if you can understand what that is.
+Here in London we have lots of Government detectives and lots of private
+ones. When these fellows are at fault they come to me, and I manage to
+put them on the right scent. They lay all the evidence before me, and I
+am generally able, by the help of my knowledge of the history of
+crime, to set them straight. There is a strong family resemblance about
+misdeeds, and if you have all the details of a thousand at your finger
+ends, it is odd if you can't unravel the thousand and first. Lestrade
+is a well-known detective. He got himself into a fog recently over a
+forgery case, and that was what brought him here."
+
+"And these other people?"
+
+"They are mostly sent on by private inquiry agencies. They are
+all people who are in trouble about something, and want a little
+enlightening. I listen to their story, they listen to my comments, and
+then I pocket my fee."
+
+"But do you mean to say," I said, "that without leaving your room you
+can unravel some knot which other men can make nothing of, although they
+have seen every detail for themselves?"
+
+"Quite so. I have a kind of intuition that way. Now and again a case
+turns up which is a little more complex. Then I have to bustle about and
+see things with my own eyes. You see I have a lot of special knowledge
+which I apply to the problem, and which facilitates matters wonderfully.
+Those rules of deduction laid down in that article which aroused your
+scorn, are invaluable to me in practical work. Observation with me is
+second nature. You appeared to be surprised when I told you, on our
+first meeting, that you had come from Afghanistan."
+
+"You were told, no doubt."
+
+"Nothing of the sort. I _knew_ you came from Afghanistan. From long
+habit the train of thoughts ran so swiftly through my mind, that I
+arrived at the conclusion without being conscious of intermediate steps.
+There were such steps, however. The train of reasoning ran, 'Here is a
+gentleman of a medical type, but with the air of a military man. Clearly
+an army doctor, then. He has just come from the tropics, for his face is
+dark, and that is not the natural tint of his skin, for his wrists are
+fair. He has undergone hardship and sickness, as his haggard face says
+clearly. His left arm has been injured. He holds it in a stiff and
+unnatural manner. Where in the tropics could an English army doctor have
+seen much hardship and got his arm wounded? Clearly in Afghanistan.' The
+whole train of thought did not occupy a second. I then remarked that you
+came from Afghanistan, and you were astonished."
+
+"It is simple enough as you explain it," I said, smiling. "You remind
+me of Edgar Allen Poe's Dupin. I had no idea that such individuals did
+exist outside of stories."
+
+Sherlock Holmes rose and lit his pipe. "No doubt you think that you are
+complimenting me in comparing me to Dupin," he observed. "Now, in my
+opinion, Dupin was a very inferior fellow. That trick of his of breaking
+in on his friends' thoughts with an apropos remark after a quarter of
+an hour's silence is really very showy and superficial. He had some
+analytical genius, no doubt; but he was by no means such a phenomenon as
+Poe appeared to imagine."
+
+"Have you read Gaboriau's works?" I asked. "Does Lecoq come up to your
+idea of a detective?"
+
+Sherlock Holmes sniffed sardonically. "Lecoq was a miserable bungler,"
+he said, in an angry voice; "he had only one thing to recommend him, and
+that was his energy. That book made me positively ill. The question was
+how to identify an unknown prisoner. I could have done it in twenty-four
+hours. Lecoq took six months or so. It might be made a text-book for
+detectives to teach them what to avoid."
+
+I felt rather indignant at having two characters whom I had admired
+treated in this cavalier style. I walked over to the window, and stood
+looking out into the busy street. "This fellow may be very clever," I
+said to myself, "but he is certainly very conceited."
+
+"There are no crimes and no criminals in these days," he said,
+querulously. "What is the use of having brains in our profession. I know
+well that I have it in me to make my name famous. No man lives or has
+ever lived who has brought the same amount of study and of natural
+talent to the detection of crime which I have done. And what is the
+result? There is no crime to detect, or, at most, some bungling villainy
+with a motive so transparent that even a Scotland Yard official can see
+through it."
+
+I was still annoyed at his bumptious style of conversation. I thought it
+best to change the topic.
+
+"I wonder what that fellow is looking for?" I asked, pointing to a
+stalwart, plainly-dressed individual who was walking slowly down the
+other side of the street, looking anxiously at the numbers. He had
+a large blue envelope in his hand, and was evidently the bearer of a
+message.
+
+"You mean the retired sergeant of Marines," said Sherlock Holmes.
+
+"Brag and bounce!" thought I to myself. "He knows that I cannot verify
+his guess."
+
+The thought had hardly passed through my mind when the man whom we were
+watching caught sight of the number on our door, and ran rapidly across
+the roadway. We heard a loud knock, a deep voice below, and heavy steps
+ascending the stair.
+
+"For Mr. Sherlock Holmes," he said, stepping into the room and handing
+my friend the letter.
+
+Here was an opportunity of taking the conceit out of him. He little
+thought of this when he made that random shot. "May I ask, my lad," I
+said, in the blandest voice, "what your trade may be?"
+
+"Commissionaire, sir," he said, gruffly. "Uniform away for repairs."
+
+"And you were?" I asked, with a slightly malicious glance at my
+companion.
+
+"A sergeant, sir, Royal Marine Light Infantry, sir. No answer? Right,
+sir."
+
+He clicked his heels together, raised his hand in a salute, and was
+gone.
+
+
+
+
+CHAPTER III. THE LAURISTON GARDEN MYSTERY [6]_
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+I CONFESS that I was considerably startled by this fresh proof of the
+practical nature of my companion's theories. My respect for his powers
+of analysis increased wondrously. There still remained some lurking
+suspicion in my mind, however, that the whole thing was a pre-arranged
+episode, intended to dazzle me, though what earthly object he could have
+in taking me in was past my comprehension. When I looked at him he
+had finished reading the note, and his eyes had assumed the vacant,
+lack-lustre expression which showed mental abstraction.
+
+"How in the world did you deduce that?" I asked.
+
+"Deduce what?" said he, petulantly.
+
+"Why, that he was a retired sergeant of Marines."
+
+"I have no time for trifles," he answered, brusquely; then with a smile,
+"Excuse my rudeness. You broke the thread of my thoughts; but perhaps
+it is as well. So you actually were not able to see that that man was a
+sergeant of Marines?"
+
+"No, indeed."
+
+"It was easier to know it than to explain why I knew it. If you
+were asked to prove that two and two made four, you might find some
+difficulty, and yet you are quite sure of the fact. Even across the
+street I could see a great blue anchor tattooed on the back of the
+fellow's hand. That smacked of the sea. He had a military carriage,
+however, and regulation side whiskers. There we have the marine. He was
+a man with some amount of self-importance and a certain air of command.
+You must have observed the way in which he held his head and swung
+his cane. A steady, respectable, middle-aged man, too, on the face of
+him--all facts which led me to believe that he had been a sergeant."
+
+"Wonderful!" I ejaculated.
+
+"Commonplace," said Holmes, though I thought from his expression that he
+was pleased at my evident surprise and admiration. "I said just now that
+there were no criminals. It appears that I am wrong--look at this!" He
+threw me over the note which the commissionaire had brought. [7]_
+
+"Why," I cried, as I cast my eye over it, "this is terrible!"
+
+"It does seem to be a little out of the common," he remarked, calmly.
+"Would you mind reading it to me aloud?"
+
+This is the letter which I read to him----
+
+
+"MY DEAR MR. SHERLOCK HOLMES,--
+
+"There has been a bad business during the night at 3, Lauriston Gardens,
+off the Brixton Road. Our man on the beat saw a light there about two in
+the morning, and as the house was an empty one, suspected that something
+was amiss. He found the door open, and in the front room, which is bare
+of furniture, discovered the body of a gentleman, well dressed, and
+having cards in his pocket bearing the name of 'Enoch J. Drebber,
+Cleveland, Ohio, U.S.A.' There had been no robbery, nor is there any
+evidence as to how the man met his death. There are marks of blood in
+the room, but there is no wound upon his person. We are at a loss as to
+how he came into the empty house; indeed, the whole affair is a puzzler.
+If you can come round to the house any time before twelve, you will find
+me there. I have left everything *in statu quo* until I hear from you.
+If you are unable to come I shall give you fuller details, and would
+esteem it a great kindness if you would favour me with your opinion.
+Yours faithfully,
+
+"TOBIAS GREGSON."
+
+
+"Gregson is the smartest of the Scotland Yarders," my friend remarked;
+"he and Lestrade are the pick of a bad lot. They are both quick and
+energetic, but conventional--shockingly so. They have their knives
+into one another, too. They are as jealous as a pair of professional
+beauties. There will be some fun over this case if they are both put
+upon the scent."
+
+I was amazed at the calm way in which he rippled on. "Surely there is
+not a moment to be lost," I cried, "shall I go and order you a cab?"
+
+"I'm not sure about whether I shall go. I am the most incurably lazy
+devil that ever stood in shoe leather--that is, when the fit is on me,
+for I can be spry enough at times."
+
+"Why, it is just such a chance as you have been longing for."
+
+"My dear fellow, what does it matter to me. Supposing I unravel the
+whole matter, you may be sure that Gregson, Lestrade, and Co. will
+pocket all the credit. That comes of being an unofficial personage."
+
+"But he begs you to help him."
+
+"Yes. He knows that I am his superior, and acknowledges it to me; but
+he would cut his tongue out before he would own it to any third person.
+However, we may as well go and have a look. I shall work it out on my
+own hook. I may have a laugh at them if I have nothing else. Come on!"
+
+He hustled on his overcoat, and bustled about in a way that showed that
+an energetic fit had superseded the apathetic one.
+
+"Get your hat," he said.
+
+"You wish me to come?"
+
+"Yes, if you have nothing better to do." A minute later we were both in
+a hansom, driving furiously for the Brixton Road.
+
+It was a foggy, cloudy morning, and a dun-coloured veil hung over the
+house-tops, looking like the reflection of the mud-coloured streets
+beneath. My companion was in the best of spirits, and prattled away
+about Cremona fiddles, and the difference between a Stradivarius and
+an Amati. As for myself, I was silent, for the dull weather and the
+melancholy business upon which we were engaged, depressed my spirits.
+
+"You don't seem to give much thought to the matter in hand," I said at
+last, interrupting Holmes' musical disquisition.
+
+"No data yet," he answered. "It is a capital mistake to theorize before
+you have all the evidence. It biases the judgment."
+
+"You will have your data soon," I remarked, pointing with my finger;
+"this is the Brixton Road, and that is the house, if I am not very much
+mistaken."
+
+"So it is. Stop, driver, stop!" We were still a hundred yards or so from
+it, but he insisted upon our alighting, and we finished our journey upon
+foot.
+
+Number 3, Lauriston Gardens wore an ill-omened and minatory look. It was
+one of four which stood back some little way from the street, two being
+occupied and two empty. The latter looked out with three tiers of vacant
+melancholy windows, which were blank and dreary, save that here and
+there a "To Let" card had developed like a cataract upon the bleared
+panes. A small garden sprinkled over with a scattered eruption of sickly
+plants separated each of these houses from the street, and was traversed
+by a narrow pathway, yellowish in colour, and consisting apparently of a
+mixture of clay and of gravel. The whole place was very sloppy from the
+rain which had fallen through the night. The garden was bounded by a
+three-foot brick wall with a fringe of wood rails upon the top, and
+against this wall was leaning a stalwart police constable, surrounded by
+a small knot of loafers, who craned their necks and strained their eyes
+in the vain hope of catching some glimpse of the proceedings within.
+
+I had imagined that Sherlock Holmes would at once have hurried into the
+house and plunged into a study of the mystery. Nothing appeared to be
+further from his intention. With an air of nonchalance which, under the
+circumstances, seemed to me to border upon affectation, he lounged up
+and down the pavement, and gazed vacantly at the ground, the sky, the
+opposite houses and the line of railings. Having finished his scrutiny,
+he proceeded slowly down the path, or rather down the fringe of grass
+which flanked the path, keeping his eyes riveted upon the ground. Twice
+he stopped, and once I saw him smile, and heard him utter an exclamation
+of satisfaction. There were many marks of footsteps upon the wet clayey
+soil, but since the police had been coming and going over it, I was
+unable to see how my companion could hope to learn anything from it.
+Still I had had such extraordinary evidence of the quickness of his
+perceptive faculties, that I had no doubt that he could see a great deal
+which was hidden from me.
+
+At the door of the house we were met by a tall, white-faced,
+flaxen-haired man, with a notebook in his hand, who rushed forward and
+wrung my companion's hand with effusion. "It is indeed kind of you to
+come," he said, "I have had everything left untouched."
+
+"Except that!" my friend answered, pointing at the pathway. "If a herd
+of buffaloes had passed along there could not be a greater mess. No
+doubt, however, you had drawn your own conclusions, Gregson, before you
+permitted this."
+
+"I have had so much to do inside the house," the detective said
+evasively. "My colleague, Mr. Lestrade, is here. I had relied upon him
+to look after this."
+
+Holmes glanced at me and raised his eyebrows sardonically. "With two
+such men as yourself and Lestrade upon the ground, there will not be
+much for a third party to find out," he said.
+
+Gregson rubbed his hands in a self-satisfied way. "I think we have done
+all that can be done," he answered; "it's a queer case though, and I
+knew your taste for such things."
+
+"You did not come here in a cab?" asked Sherlock Holmes.
+
+"No, sir."
+
+"Nor Lestrade?"
+
+"No, sir."
+
+"Then let us go and look at the room." With which inconsequent remark he
+strode on into the house, followed by Gregson, whose features expressed
+his astonishment.
+
+A short passage, bare planked and dusty, led to the kitchen and offices.
+Two doors opened out of it to the left and to the right. One of these
+had obviously been closed for many weeks. The other belonged to the
+dining-room, which was the apartment in which the mysterious affair had
+occurred. Holmes walked in, and I followed him with that subdued feeling
+at my heart which the presence of death inspires.
+
+It was a large square room, looking all the larger from the absence
+of all furniture. A vulgar flaring paper adorned the walls, but it was
+blotched in places with mildew, and here and there great strips had
+become detached and hung down, exposing the yellow plaster beneath.
+Opposite the door was a showy fireplace, surmounted by a mantelpiece of
+imitation white marble. On one corner of this was stuck the stump of a
+red wax candle. The solitary window was so dirty that the light was
+hazy and uncertain, giving a dull grey tinge to everything, which was
+intensified by the thick layer of dust which coated the whole apartment.
+
+All these details I observed afterwards. At present my attention was
+centred upon the single grim motionless figure which lay stretched upon
+the boards, with vacant sightless eyes staring up at the discoloured
+ceiling. It was that of a man about forty-three or forty-four years of
+age, middle-sized, broad shouldered, with crisp curling black hair, and
+a short stubbly beard. He was dressed in a heavy broadcloth frock coat
+and waistcoat, with light-coloured trousers, and immaculate collar
+and cuffs. A top hat, well brushed and trim, was placed upon the floor
+beside him. His hands were clenched and his arms thrown abroad, while
+his lower limbs were interlocked as though his death struggle had been a
+grievous one. On his rigid face there stood an expression of horror,
+and as it seemed to me, of hatred, such as I have never seen upon human
+features. This malignant and terrible contortion, combined with the low
+forehead, blunt nose, and prognathous jaw gave the dead man a singularly
+simious and ape-like appearance, which was increased by his writhing,
+unnatural posture. I have seen death in many forms, but never has
+it appeared to me in a more fearsome aspect than in that dark grimy
+apartment, which looked out upon one of the main arteries of suburban
+London.
+
+Lestrade, lean and ferret-like as ever, was standing by the doorway, and
+greeted my companion and myself.
+
+"This case will make a stir, sir," he remarked. "It beats anything I
+have seen, and I am no chicken."
+
+"There is no clue?" said Gregson.
+
+"None at all," chimed in Lestrade.
+
+Sherlock Holmes approached the body, and, kneeling down, examined it
+intently. "You are sure that there is no wound?" he asked, pointing to
+numerous gouts and splashes of blood which lay all round.
+
+"Positive!" cried both detectives.
+
+"Then, of course, this blood belongs to a second individual--[8]_
+presumably the murderer, if murder has been committed. It reminds me of
+the circumstances attendant on the death of Van Jansen, in Utrecht, in
+the year '34. Do you remember the case, Gregson?"
+
+"No, sir."
+
+"Read it up--you really should. There is nothing new under the sun. It
+has all been done before."
+
+As he spoke, his nimble fingers were flying here, there, and everywhere,
+feeling, pressing, unbuttoning, examining, while his eyes wore the same
+far-away expression which I have already remarked upon. So swiftly was
+the examination made, that one would hardly have guessed the minuteness
+with which it was conducted. Finally, he sniffed the dead man's lips,
+and then glanced at the soles of his patent leather boots.
+
+"He has not been moved at all?" he asked.
+
+"No more than was necessary for the purposes of our examination."
+
+"You can take him to the mortuary now," he said. "There is nothing more
+to be learned."
+
+Gregson had a stretcher and four men at hand. At his call they entered
+the room, and the stranger was lifted and carried out. As they raised
+him, a ring tinkled down and rolled across the floor. Lestrade grabbed
+it up and stared at it with mystified eyes.
+
+"There's been a woman here," he cried. "It's a woman's wedding-ring."
+
+He held it out, as he spoke, upon the palm of his hand. We all gathered
+round him and gazed at it. There could be no doubt that that circlet of
+plain gold had once adorned the finger of a bride.
+
+"This complicates matters," said Gregson. "Heaven knows, they were
+complicated enough before."
+
+"You're sure it doesn't simplify them?" observed Holmes. "There's
+nothing to be learned by staring at it. What did you find in his
+pockets?"
+
+"We have it all here," said Gregson, pointing to a litter of objects
+upon one of the bottom steps of the stairs. "A gold watch, No. 97163, by
+Barraud, of London. Gold Albert chain, very heavy and solid. Gold ring,
+with masonic device. Gold pin--bull-dog's head, with rubies as eyes.
+Russian leather card-case, with cards of Enoch J. Drebber of Cleveland,
+corresponding with the E. J. D. upon the linen. No purse, but loose
+money to the extent of seven pounds thirteen. Pocket edition of
+Boccaccio's 'Decameron,' with name of Joseph Stangerson upon the
+fly-leaf. Two letters--one addressed to E. J. Drebber and one to Joseph
+Stangerson."
+
+"At what address?"
+
+"American Exchange, Strand--to be left till called for. They are both
+from the Guion Steamship Company, and refer to the sailing of their
+boats from Liverpool. It is clear that this unfortunate man was about to
+return to New York."
+
+"Have you made any inquiries as to this man, Stangerson?"
+
+"I did it at once, sir," said Gregson. "I have had advertisements
+sent to all the newspapers, and one of my men has gone to the American
+Exchange, but he has not returned yet."
+
+"Have you sent to Cleveland?"
+
+"We telegraphed this morning."
+
+"How did you word your inquiries?"
+
+"We simply detailed the circumstances, and said that we should be glad
+of any information which could help us."
+
+"You did not ask for particulars on any point which appeared to you to
+be crucial?"
+
+"I asked about Stangerson."
+
+"Nothing else? Is there no circumstance on which this whole case appears
+to hinge? Will you not telegraph again?"
+
+"I have said all I have to say," said Gregson, in an offended voice.
+
+Sherlock Holmes chuckled to himself, and appeared to be about to make
+some remark, when Lestrade, who had been in the front room while we
+were holding this conversation in the hall, reappeared upon the scene,
+rubbing his hands in a pompous and self-satisfied manner.
+
+"Mr. Gregson," he said, "I have just made a discovery of the highest
+importance, and one which would have been overlooked had I not made a
+careful examination of the walls."
+
+The little man's eyes sparkled as he spoke, and he was evidently in
+a state of suppressed exultation at having scored a point against his
+colleague.
+
+"Come here," he said, bustling back into the room, the atmosphere of
+which felt clearer since the removal of its ghastly inmate. "Now, stand
+there!"
+
+He struck a match on his boot and held it up against the wall.
+
+"Look at that!" he said, triumphantly.
+
+I have remarked that the paper had fallen away in parts. In this
+particular corner of the room a large piece had peeled off, leaving a
+yellow square of coarse plastering. Across this bare space there was
+scrawled in blood-red letters a single word--
+
+ RACHE.
+
+
+"What do you think of that?" cried the detective, with the air of a
+showman exhibiting his show. "This was overlooked because it was in the
+darkest corner of the room, and no one thought of looking there. The
+murderer has written it with his or her own blood. See this smear where
+it has trickled down the wall! That disposes of the idea of suicide
+anyhow. Why was that corner chosen to write it on? I will tell you. See
+that candle on the mantelpiece. It was lit at the time, and if it was
+lit this corner would be the brightest instead of the darkest portion of
+the wall."
+
+"And what does it mean now that you _have_ found it?" asked Gregson in a
+depreciatory voice.
+
+"Mean? Why, it means that the writer was going to put the female name
+Rachel, but was disturbed before he or she had time to finish. You mark
+my words, when this case comes to be cleared up you will find that a
+woman named Rachel has something to do with it. It's all very well for
+you to laugh, Mr. Sherlock Holmes. You may be very smart and clever, but
+the old hound is the best, when all is said and done."
+
+"I really beg your pardon!" said my companion, who had ruffled the
+little man's temper by bursting into an explosion of laughter. "You
+certainly have the credit of being the first of us to find this out,
+and, as you say, it bears every mark of having been written by the other
+participant in last night's mystery. I have not had time to examine this
+room yet, but with your permission I shall do so now."
+
+As he spoke, he whipped a tape measure and a large round magnifying
+glass from his pocket. With these two implements he trotted noiselessly
+about the room, sometimes stopping, occasionally kneeling, and once
+lying flat upon his face. So engrossed was he with his occupation that
+he appeared to have forgotten our presence, for he chattered away to
+himself under his breath the whole time, keeping up a running fire
+of exclamations, groans, whistles, and little cries suggestive of
+encouragement and of hope. As I watched him I was irresistibly reminded
+of a pure-blooded well-trained foxhound as it dashes backwards and
+forwards through the covert, whining in its eagerness, until it comes
+across the lost scent. For twenty minutes or more he continued his
+researches, measuring with the most exact care the distance between
+marks which were entirely invisible to me, and occasionally applying his
+tape to the walls in an equally incomprehensible manner. In one place
+he gathered up very carefully a little pile of grey dust from the floor,
+and packed it away in an envelope. Finally, he examined with his glass
+the word upon the wall, going over every letter of it with the most
+minute exactness. This done, he appeared to be satisfied, for he
+replaced his tape and his glass in his pocket.
+
+"They say that genius is an infinite capacity for taking pains," he
+remarked with a smile. "It's a very bad definition, but it does apply to
+detective work."
+
+Gregson and Lestrade had watched the manoeuvres [9]_ of their amateur
+companion with considerable curiosity and some contempt. They evidently
+failed to appreciate the fact, which I had begun to realize, that
+Sherlock Holmes' smallest actions were all directed towards some
+definite and practical end.
+
+"What do you think of it, sir?" they both asked.
+
+"It would be robbing you of the credit of the case if I was to presume
+to help you," remarked my friend. "You are doing so well now that it
+would be a pity for anyone to interfere." There was a world of
+sarcasm in his voice as he spoke. "If you will let me know how your
+investigations go," he continued, "I shall be happy to give you any help
+I can. In the meantime I should like to speak to the constable who found
+the body. Can you give me his name and address?"
+
+Lestrade glanced at his note-book. "John Rance," he said. "He is off
+duty now. You will find him at 46, Audley Court, Kennington Park Gate."
+
+Holmes took a note of the address.
+
+"Come along, Doctor," he said; "we shall go and look him up. I'll tell
+you one thing which may help you in the case," he continued, turning to
+the two detectives. "There has been murder done, and the murderer was a
+man. He was more than six feet high, was in the prime of life, had
+small feet for his height, wore coarse, square-toed boots and smoked a
+Trichinopoly cigar. He came here with his victim in a four-wheeled cab,
+which was drawn by a horse with three old shoes and one new one on his
+off fore leg. In all probability the murderer had a florid face, and the
+finger-nails of his right hand were remarkably long. These are only a
+few indications, but they may assist you."
+
+Lestrade and Gregson glanced at each other with an incredulous smile.
+
+"If this man was murdered, how was it done?" asked the former.
+
+"Poison," said Sherlock Holmes curtly, and strode off. "One other thing,
+Lestrade," he added, turning round at the door: "'Rache,' is the German
+for 'revenge;' so don't lose your time looking for Miss Rachel."
+
+With which Parthian shot he walked away, leaving the two rivals
+open-mouthed behind him.
+
+
+
+
+CHAPTER IV. WHAT JOHN RANCE HAD TO TELL.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+IT was one o'clock when we left No. 3, Lauriston Gardens. Sherlock
+Holmes led me to the nearest telegraph office, whence he dispatched a
+long telegram. He then hailed a cab, and ordered the driver to take us
+to the address given us by Lestrade.
+
+"There is nothing like first hand evidence," he remarked; "as a matter
+of fact, my mind is entirely made up upon the case, but still we may as
+well learn all that is to be learned."
+
+"You amaze me, Holmes," said I. "Surely you are not as sure as you
+pretend to be of all those particulars which you gave."
+
+"There's no room for a mistake," he answered. "The very first thing
+which I observed on arriving there was that a cab had made two ruts with
+its wheels close to the curb. Now, up to last night, we have had no rain
+for a week, so that those wheels which left such a deep impression must
+have been there during the night. There were the marks of the horse's
+hoofs, too, the outline of one of which was far more clearly cut than
+that of the other three, showing that that was a new shoe. Since the cab
+was there after the rain began, and was not there at any time during the
+morning--I have Gregson's word for that--it follows that it must have
+been there during the night, and, therefore, that it brought those two
+individuals to the house."
+
+"That seems simple enough," said I; "but how about the other man's
+height?"
+
+"Why, the height of a man, in nine cases out of ten, can be told from
+the length of his stride. It is a simple calculation enough, though
+there is no use my boring you with figures. I had this fellow's stride
+both on the clay outside and on the dust within. Then I had a way of
+checking my calculation. When a man writes on a wall, his instinct leads
+him to write about the level of his own eyes. Now that writing was just
+over six feet from the ground. It was child's play."
+
+"And his age?" I asked.
+
+"Well, if a man can stride four and a-half feet without the smallest
+effort, he can't be quite in the sere and yellow. That was the breadth
+of a puddle on the garden walk which he had evidently walked across.
+Patent-leather boots had gone round, and Square-toes had hopped over.
+There is no mystery about it at all. I am simply applying to ordinary
+life a few of those precepts of observation and deduction which I
+advocated in that article. Is there anything else that puzzles you?"
+
+"The finger nails and the Trichinopoly," I suggested.
+
+"The writing on the wall was done with a man's forefinger dipped in
+blood. My glass allowed me to observe that the plaster was slightly
+scratched in doing it, which would not have been the case if the man's
+nail had been trimmed. I gathered up some scattered ash from the floor.
+It was dark in colour and flakey--such an ash as is only made by a
+Trichinopoly. I have made a special study of cigar ashes--in fact, I
+have written a monograph upon the subject. I flatter myself that I can
+distinguish at a glance the ash of any known brand, either of cigar
+or of tobacco. It is just in such details that the skilled detective
+differs from the Gregson and Lestrade type."
+
+"And the florid face?" I asked.
+
+"Ah, that was a more daring shot, though I have no doubt that I was
+right. You must not ask me that at the present state of the affair."
+
+I passed my hand over my brow. "My head is in a whirl," I remarked; "the
+more one thinks of it the more mysterious it grows. How came these two
+men--if there were two men--into an empty house? What has become of the
+cabman who drove them? How could one man compel another to take poison?
+Where did the blood come from? What was the object of the murderer,
+since robbery had no part in it? How came the woman's ring there? Above
+all, why should the second man write up the German word RACHE before
+decamping? I confess that I cannot see any possible way of reconciling
+all these facts."
+
+My companion smiled approvingly.
+
+"You sum up the difficulties of the situation succinctly and well," he
+said. "There is much that is still obscure, though I have quite made up
+my mind on the main facts. As to poor Lestrade's discovery it was simply
+a blind intended to put the police upon a wrong track, by suggesting
+Socialism and secret societies. It was not done by a German. The A, if
+you noticed, was printed somewhat after the German fashion. Now, a real
+German invariably prints in the Latin character, so that we may safely
+say that this was not written by one, but by a clumsy imitator who
+overdid his part. It was simply a ruse to divert inquiry into a wrong
+channel. I'm not going to tell you much more of the case, Doctor. You
+know a conjuror gets no credit when once he has explained his trick,
+and if I show you too much of my method of working, you will come to the
+conclusion that I am a very ordinary individual after all."
+
+"I shall never do that," I answered; "you have brought detection as near
+an exact science as it ever will be brought in this world."
+
+My companion flushed up with pleasure at my words, and the earnest way
+in which I uttered them. I had already observed that he was as sensitive
+to flattery on the score of his art as any girl could be of her beauty.
+
+"I'll tell you one other thing," he said. "Patent leathers [10]_ and
+Square-toes came in the same cab, and they walked down the pathway
+together as friendly as possible--arm-in-arm, in all probability.
+When they got inside they walked up and down the room--or rather,
+Patent-leathers stood still while Square-toes walked up and down. I
+could read all that in the dust; and I could read that as he walked he
+grew more and more excited. That is shown by the increased length of his
+strides. He was talking all the while, and working himself up, no doubt,
+into a fury. Then the tragedy occurred. I've told you all I know myself
+now, for the rest is mere surmise and conjecture. We have a good working
+basis, however, on which to start. We must hurry up, for I want to go to
+Halle's concert to hear Norman Neruda this afternoon."
+
+This conversation had occurred while our cab had been threading its way
+through a long succession of dingy streets and dreary by-ways. In the
+dingiest and dreariest of them our driver suddenly came to a stand.
+"That's Audley Court in there," he said, pointing to a narrow slit in
+the line of dead-coloured brick. "You'll find me here when you come
+back."
+
+Audley Court was not an attractive locality. The narrow passage led us
+into a quadrangle paved with flags and lined by sordid dwellings. We
+picked our way among groups of dirty children, and through lines of
+discoloured linen, until we came to Number 46, the door of which
+was decorated with a small slip of brass on which the name Rance was
+engraved. On enquiry we found that the constable was in bed, and we were
+shown into a little front parlour to await his coming.
+
+He appeared presently, looking a little irritable at being disturbed in
+his slumbers. "I made my report at the office," he said.
+
+Holmes took a half-sovereign from his pocket and played with it
+pensively. "We thought that we should like to hear it all from your own
+lips," he said.
+
+"I shall be most happy to tell you anything I can," the constable
+answered with his eyes upon the little golden disk.
+
+"Just let us hear it all in your own way as it occurred."
+
+Rance sat down on the horsehair sofa, and knitted his brows as though
+determined not to omit anything in his narrative.
+
+"I'll tell it ye from the beginning," he said. "My time is from ten at
+night to six in the morning. At eleven there was a fight at the 'White
+Hart'; but bar that all was quiet enough on the beat. At one o'clock it
+began to rain, and I met Harry Murcher--him who has the Holland Grove
+beat--and we stood together at the corner of Henrietta Street a-talkin'.
+Presently--maybe about two or a little after--I thought I would take
+a look round and see that all was right down the Brixton Road. It was
+precious dirty and lonely. Not a soul did I meet all the way down,
+though a cab or two went past me. I was a strollin' down, thinkin'
+between ourselves how uncommon handy a four of gin hot would be, when
+suddenly the glint of a light caught my eye in the window of that same
+house. Now, I knew that them two houses in Lauriston Gardens was empty
+on account of him that owns them who won't have the drains seen to,
+though the very last tenant what lived in one of them died o' typhoid
+fever. I was knocked all in a heap therefore at seeing a light in
+the window, and I suspected as something was wrong. When I got to the
+door----"
+
+"You stopped, and then walked back to the garden gate," my companion
+interrupted. "What did you do that for?"
+
+Rance gave a violent jump, and stared at Sherlock Holmes with the utmost
+amazement upon his features.
+
+"Why, that's true, sir," he said; "though how you come to know it,
+Heaven only knows. Ye see, when I got up to the door it was so still and
+so lonesome, that I thought I'd be none the worse for some one with me.
+I ain't afeared of anything on this side o' the grave; but I thought
+that maybe it was him that died o' the typhoid inspecting the drains
+what killed him. The thought gave me a kind o' turn, and I walked back
+to the gate to see if I could see Murcher's lantern, but there wasn't no
+sign of him nor of anyone else."
+
+"There was no one in the street?"
+
+"Not a livin' soul, sir, nor as much as a dog. Then I pulled myself
+together and went back and pushed the door open. All was quiet inside,
+so I went into the room where the light was a-burnin'. There was a
+candle flickerin' on the mantelpiece--a red wax one--and by its light I
+saw----"
+
+"Yes, I know all that you saw. You walked round the room several times,
+and you knelt down by the body, and then you walked through and tried
+the kitchen door, and then----"
+
+John Rance sprang to his feet with a frightened face and suspicion in
+his eyes. "Where was you hid to see all that?" he cried. "It seems to me
+that you knows a deal more than you should."
+
+Holmes laughed and threw his card across the table to the constable.
+"Don't get arresting me for the murder," he said. "I am one of the
+hounds and not the wolf; Mr. Gregson or Mr. Lestrade will answer for
+that. Go on, though. What did you do next?"
+
+Rance resumed his seat, without however losing his mystified expression.
+"I went back to the gate and sounded my whistle. That brought Murcher
+and two more to the spot."
+
+"Was the street empty then?"
+
+"Well, it was, as far as anybody that could be of any good goes."
+
+"What do you mean?"
+
+The constable's features broadened into a grin. "I've seen many a drunk
+chap in my time," he said, "but never anyone so cryin' drunk as
+that cove. He was at the gate when I came out, a-leanin' up agin the
+railings, and a-singin' at the pitch o' his lungs about Columbine's
+New-fangled Banner, or some such stuff. He couldn't stand, far less
+help."
+
+"What sort of a man was he?" asked Sherlock Holmes.
+
+John Rance appeared to be somewhat irritated at this digression. "He was
+an uncommon drunk sort o' man," he said. "He'd ha' found hisself in the
+station if we hadn't been so took up."
+
+"His face--his dress--didn't you notice them?" Holmes broke in
+impatiently.
+
+"I should think I did notice them, seeing that I had to prop him up--me
+and Murcher between us. He was a long chap, with a red face, the lower
+part muffled round----"
+
+"That will do," cried Holmes. "What became of him?"
+
+"We'd enough to do without lookin' after him," the policeman said, in an
+aggrieved voice. "I'll wager he found his way home all right."
+
+"How was he dressed?"
+
+"A brown overcoat."
+
+"Had he a whip in his hand?"
+
+"A whip--no."
+
+"He must have left it behind," muttered my companion. "You didn't happen
+to see or hear a cab after that?"
+
+"No."
+
+"There's a half-sovereign for you," my companion said, standing up and
+taking his hat. "I am afraid, Rance, that you will never rise in the
+force. That head of yours should be for use as well as ornament. You
+might have gained your sergeant's stripes last night. The man whom you
+held in your hands is the man who holds the clue of this mystery, and
+whom we are seeking. There is no use of arguing about it now; I tell you
+that it is so. Come along, Doctor."
+
+We started off for the cab together, leaving our informant incredulous,
+but obviously uncomfortable.
+
+"The blundering fool," Holmes said, bitterly, as we drove back to our
+lodgings. "Just to think of his having such an incomparable bit of good
+luck, and not taking advantage of it."
+
+"I am rather in the dark still. It is true that the description of this
+man tallies with your idea of the second party in this mystery. But why
+should he come back to the house after leaving it? That is not the way
+of criminals."
+
+"The ring, man, the ring: that was what he came back for. If we have no
+other way of catching him, we can always bait our line with the ring. I
+shall have him, Doctor--I'll lay you two to one that I have him. I must
+thank you for it all. I might not have gone but for you, and so have
+missed the finest study I ever came across: a study in scarlet, eh?
+Why shouldn't we use a little art jargon. There's the scarlet thread of
+murder running through the colourless skein of life, and our duty is
+to unravel it, and isolate it, and expose every inch of it. And now
+for lunch, and then for Norman Neruda. Her attack and her bowing
+are splendid. What's that little thing of Chopin's she plays so
+magnificently: Tra-la-la-lira-lira-lay."
+
+Leaning back in the cab, this amateur bloodhound carolled away like a
+lark while I meditated upon the many-sidedness of the human mind.
+
+
+
+
+CHAPTER V. OUR ADVERTISEMENT BRINGS A VISITOR.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+OUR morning's exertions had been too much for my weak health, and I was
+tired out in the afternoon. After Holmes' departure for the concert, I
+lay down upon the sofa and endeavoured to get a couple of hours' sleep.
+It was a useless attempt. My mind had been too much excited by all that
+had occurred, and the strangest fancies and surmises crowded into
+it. Every time that I closed my eyes I saw before me the distorted
+baboon-like countenance of the murdered man. So sinister was the
+impression which that face had produced upon me that I found it
+difficult to feel anything but gratitude for him who had removed its
+owner from the world. If ever human features bespoke vice of the most
+malignant type, they were certainly those of Enoch J. Drebber, of
+Cleveland. Still I recognized that justice must be done, and that the
+depravity of the victim was no condonment [11]_ in the eyes of the law.
+
+The more I thought of it the more extraordinary did my companion's
+hypothesis, that the man had been poisoned, appear. I remembered how he
+had sniffed his lips, and had no doubt that he had detected something
+which had given rise to the idea. Then, again, if not poison, what
+had caused the man's death, since there was neither wound nor marks of
+strangulation? But, on the other hand, whose blood was that which lay so
+thickly upon the floor? There were no signs of a struggle, nor had the
+victim any weapon with which he might have wounded an antagonist. As
+long as all these questions were unsolved, I felt that sleep would be
+no easy matter, either for Holmes or myself. His quiet self-confident
+manner convinced me that he had already formed a theory which explained
+all the facts, though what it was I could not for an instant conjecture.
+
+He was very late in returning--so late, that I knew that the concert
+could not have detained him all the time. Dinner was on the table before
+he appeared.
+
+"It was magnificent," he said, as he took his seat. "Do you remember
+what Darwin says about music? He claims that the power of producing and
+appreciating it existed among the human race long before the power of
+speech was arrived at. Perhaps that is why we are so subtly influenced
+by it. There are vague memories in our souls of those misty centuries
+when the world was in its childhood."
+
+"That's rather a broad idea," I remarked.
+
+"One's ideas must be as broad as Nature if they are to interpret
+Nature," he answered. "What's the matter? You're not looking quite
+yourself. This Brixton Road affair has upset you."
+
+"To tell the truth, it has," I said. "I ought to be more case-hardened
+after my Afghan experiences. I saw my own comrades hacked to pieces at
+Maiwand without losing my nerve."
+
+"I can understand. There is a mystery about this which stimulates the
+imagination; where there is no imagination there is no horror. Have you
+seen the evening paper?"
+
+"No."
+
+"It gives a fairly good account of the affair. It does not mention the
+fact that when the man was raised up, a woman's wedding ring fell upon
+the floor. It is just as well it does not."
+
+"Why?"
+
+"Look at this advertisement," he answered. "I had one sent to every
+paper this morning immediately after the affair."
+
+He threw the paper across to me and I glanced at the place indicated. It
+was the first announcement in the "Found" column. "In Brixton Road,
+this morning," it ran, "a plain gold wedding ring, found in the roadway
+between the 'White Hart' Tavern and Holland Grove. Apply Dr. Watson,
+221B, Baker Street, between eight and nine this evening."
+
+"Excuse my using your name," he said. "If I used my own some of these
+dunderheads would recognize it, and want to meddle in the affair."
+
+"That is all right," I answered. "But supposing anyone applies, I have
+no ring."
+
+"Oh yes, you have," said he, handing me one. "This will do very well. It
+is almost a facsimile."
+
+"And who do you expect will answer this advertisement."
+
+"Why, the man in the brown coat--our florid friend with the square toes.
+If he does not come himself he will send an accomplice."
+
+"Would he not consider it as too dangerous?"
+
+"Not at all. If my view of the case is correct, and I have every reason
+to believe that it is, this man would rather risk anything than lose the
+ring. According to my notion he dropped it while stooping over Drebber's
+body, and did not miss it at the time. After leaving the house he
+discovered his loss and hurried back, but found the police already in
+possession, owing to his own folly in leaving the candle burning. He had
+to pretend to be drunk in order to allay the suspicions which might have
+been aroused by his appearance at the gate. Now put yourself in that
+man's place. On thinking the matter over, it must have occurred to him
+that it was possible that he had lost the ring in the road after leaving
+the house. What would he do, then? He would eagerly look out for the
+evening papers in the hope of seeing it among the articles found. His
+eye, of course, would light upon this. He would be overjoyed. Why should
+he fear a trap? There would be no reason in his eyes why the finding
+of the ring should be connected with the murder. He would come. He will
+come. You shall see him within an hour?"
+
+"And then?" I asked.
+
+"Oh, you can leave me to deal with him then. Have you any arms?"
+
+"I have my old service revolver and a few cartridges."
+
+"You had better clean it and load it. He will be a desperate man,
+and though I shall take him unawares, it is as well to be ready for
+anything."
+
+I went to my bedroom and followed his advice. When I returned with
+the pistol the table had been cleared, and Holmes was engaged in his
+favourite occupation of scraping upon his violin.
+
+"The plot thickens," he said, as I entered; "I have just had an answer
+to my American telegram. My view of the case is the correct one."
+
+"And that is?" I asked eagerly.
+
+"My fiddle would be the better for new strings," he remarked. "Put your
+pistol in your pocket. When the fellow comes speak to him in an ordinary
+way. Leave the rest to me. Don't frighten him by looking at him too
+hard."
+
+"It is eight o'clock now," I said, glancing at my watch.
+
+"Yes. He will probably be here in a few minutes. Open the door slightly.
+That will do. Now put the key on the inside. Thank you! This is a
+queer old book I picked up at a stall yesterday--'De Jure inter
+Gentes'--published in Latin at Liege in the Lowlands, in 1642. Charles'
+head was still firm on his shoulders when this little brown-backed
+volume was struck off."
+
+"Who is the printer?"
+
+"Philippe de Croy, whoever he may have been. On the fly-leaf, in very
+faded ink, is written 'Ex libris Guliolmi Whyte.' I wonder who William
+Whyte was. Some pragmatical seventeenth century lawyer, I suppose. His
+writing has a legal twist about it. Here comes our man, I think."
+
+As he spoke there was a sharp ring at the bell. Sherlock Holmes rose
+softly and moved his chair in the direction of the door. We heard the
+servant pass along the hall, and the sharp click of the latch as she
+opened it.
+
+"Does Dr. Watson live here?" asked a clear but rather harsh voice. We
+could not hear the servant's reply, but the door closed, and some one
+began to ascend the stairs. The footfall was an uncertain and shuffling
+one. A look of surprise passed over the face of my companion as he
+listened to it. It came slowly along the passage, and there was a feeble
+tap at the door.
+
+"Come in," I cried.
+
+At my summons, instead of the man of violence whom we expected, a very
+old and wrinkled woman hobbled into the apartment. She appeared to be
+dazzled by the sudden blaze of light, and after dropping a curtsey, she
+stood blinking at us with her bleared eyes and fumbling in her pocket
+with nervous, shaky fingers. I glanced at my companion, and his face
+had assumed such a disconsolate expression that it was all I could do to
+keep my countenance.
+
+The old crone drew out an evening paper, and pointed at our
+advertisement. "It's this as has brought me, good gentlemen," she said,
+dropping another curtsey; "a gold wedding ring in the Brixton Road. It
+belongs to my girl Sally, as was married only this time twelvemonth,
+which her husband is steward aboard a Union boat, and what he'd say if
+he come 'ome and found her without her ring is more than I can think, he
+being short enough at the best o' times, but more especially when he
+has the drink. If it please you, she went to the circus last night along
+with----"
+
+"Is that her ring?" I asked.
+
+"The Lord be thanked!" cried the old woman; "Sally will be a glad woman
+this night. That's the ring."
+
+"And what may your address be?" I inquired, taking up a pencil.
+
+"13, Duncan Street, Houndsditch. A weary way from here."
+
+"The Brixton Road does not lie between any circus and Houndsditch," said
+Sherlock Holmes sharply.
+
+The old woman faced round and looked keenly at him from her little
+red-rimmed eyes. "The gentleman asked me for _my_ address," she said.
+"Sally lives in lodgings at 3, Mayfield Place, Peckham."
+
+"And your name is----?"
+
+"My name is Sawyer--her's is Dennis, which Tom Dennis married her--and
+a smart, clean lad, too, as long as he's at sea, and no steward in the
+company more thought of; but when on shore, what with the women and what
+with liquor shops----"
+
+"Here is your ring, Mrs. Sawyer," I interrupted, in obedience to a sign
+from my companion; "it clearly belongs to your daughter, and I am glad
+to be able to restore it to the rightful owner."
+
+With many mumbled blessings and protestations of gratitude the old crone
+packed it away in her pocket, and shuffled off down the stairs. Sherlock
+Holmes sprang to his feet the moment that she was gone and rushed into
+his room. He returned in a few seconds enveloped in an ulster and
+a cravat. "I'll follow her," he said, hurriedly; "she must be an
+accomplice, and will lead me to him. Wait up for me." The hall door had
+hardly slammed behind our visitor before Holmes had descended the stair.
+Looking through the window I could see her walking feebly along the
+other side, while her pursuer dogged her some little distance behind.
+"Either his whole theory is incorrect," I thought to myself, "or else he
+will be led now to the heart of the mystery." There was no need for him
+to ask me to wait up for him, for I felt that sleep was impossible until
+I heard the result of his adventure.
+
+It was close upon nine when he set out. I had no idea how long he might
+be, but I sat stolidly puffing at my pipe and skipping over the pages
+of Henri Murger's "Vie de Bohème." Ten o'clock passed, and I heard the
+footsteps of the maid as they pattered off to bed. Eleven, and the
+more stately tread of the landlady passed my door, bound for the same
+destination. It was close upon twelve before I heard the sharp sound of
+his latch-key. The instant he entered I saw by his face that he had not
+been successful. Amusement and chagrin seemed to be struggling for the
+mastery, until the former suddenly carried the day, and he burst into a
+hearty laugh.
+
+"I wouldn't have the Scotland Yarders know it for the world," he cried,
+dropping into his chair; "I have chaffed them so much that they would
+never have let me hear the end of it. I can afford to laugh, because I
+know that I will be even with them in the long run."
+
+"What is it then?" I asked.
+
+"Oh, I don't mind telling a story against myself. That creature had
+gone a little way when she began to limp and show every sign of being
+foot-sore. Presently she came to a halt, and hailed a four-wheeler which
+was passing. I managed to be close to her so as to hear the address, but
+I need not have been so anxious, for she sang it out loud enough to
+be heard at the other side of the street, 'Drive to 13, Duncan Street,
+Houndsditch,' she cried. This begins to look genuine, I thought, and
+having seen her safely inside, I perched myself behind. That's an art
+which every detective should be an expert at. Well, away we rattled, and
+never drew rein until we reached the street in question. I hopped off
+before we came to the door, and strolled down the street in an easy,
+lounging way. I saw the cab pull up. The driver jumped down, and I saw
+him open the door and stand expectantly. Nothing came out though. When
+I reached him he was groping about frantically in the empty cab, and
+giving vent to the finest assorted collection of oaths that ever I
+listened to. There was no sign or trace of his passenger, and I fear it
+will be some time before he gets his fare. On inquiring at Number 13
+we found that the house belonged to a respectable paperhanger, named
+Keswick, and that no one of the name either of Sawyer or Dennis had ever
+been heard of there."
+
+"You don't mean to say," I cried, in amazement, "that that tottering,
+feeble old woman was able to get out of the cab while it was in motion,
+without either you or the driver seeing her?"
+
+"Old woman be damned!" said Sherlock Holmes, sharply. "We were the old
+women to be so taken in. It must have been a young man, and an
+active one, too, besides being an incomparable actor. The get-up was
+inimitable. He saw that he was followed, no doubt, and used this means
+of giving me the slip. It shows that the man we are after is not as
+lonely as I imagined he was, but has friends who are ready to risk
+something for him. Now, Doctor, you are looking done-up. Take my advice
+and turn in."
+
+I was certainly feeling very weary, so I obeyed his injunction. I
+left Holmes seated in front of the smouldering fire, and long into the
+watches of the night I heard the low, melancholy wailings of his violin,
+and knew that he was still pondering over the strange problem which he
+had set himself to unravel.
+
+
+
+
+CHAPTER VI. TOBIAS GREGSON SHOWS WHAT HE CAN DO.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+THE papers next day were full of the "Brixton Mystery," as they termed
+it. Each had a long account of the affair, and some had leaders upon it
+in addition. There was some information in them which was new to me. I
+still retain in my scrap-book numerous clippings and extracts bearing
+upon the case. Here is a condensation of a few of them:--
+
+The *Daily Telegraph* remarked that in the history of crime there had
+seldom been a tragedy which presented stranger features. The German
+name of the victim, the absence of all other motive, and the sinister
+inscription on the wall, all pointed to its perpetration by political
+refugees and revolutionists. The Socialists had many branches in
+America, and the deceased had, no doubt, infringed their unwritten laws,
+and been tracked down by them. After alluding airily to the Vehmgericht,
+aqua tofana, Carbonari, the Marchioness de Brinvilliers, the Darwinian
+theory, the principles of Malthus, and the Ratcliff Highway murders, the
+article concluded by admonishing the Government and advocating a closer
+watch over foreigners in England.
+
+The _Standard_ commented upon the fact that lawless outrages of the sort
+usually occurred under a Liberal Administration. They arose from the
+unsettling of the minds of the masses, and the consequent weakening
+of all authority. The deceased was an American gentleman who had
+been residing for some weeks in the Metropolis. He had stayed at the
+boarding-house of Madame Charpentier, in Torquay Terrace, Camberwell.
+He was accompanied in his travels by his private secretary, Mr. Joseph
+Stangerson. The two bade adieu to their landlady upon Tuesday, the
+4th inst., and departed to Euston Station with the avowed intention of
+catching the Liverpool express. They were afterwards seen together upon
+the platform. Nothing more is known of them until Mr. Drebber's body
+was, as recorded, discovered in an empty house in the Brixton Road,
+many miles from Euston. How he came there, or how he met his fate, are
+questions which are still involved in mystery. Nothing is known of the
+whereabouts of Stangerson. We are glad to learn that Mr. Lestrade and
+Mr. Gregson, of Scotland Yard, are both engaged upon the case, and it
+is confidently anticipated that these well-known officers will speedily
+throw light upon the matter.
+
+The *Daily News* observed that there was no doubt as to the crime being
+a political one. The despotism and hatred of Liberalism which animated
+the Continental Governments had had the effect of driving to our shores
+a number of men who might have made excellent citizens were they not
+soured by the recollection of all that they had undergone. Among these
+men there was a stringent code of honour, any infringement of which was
+punished by death. Every effort should be made to find the secretary,
+Stangerson, and to ascertain some particulars of the habits of the
+deceased. A great step had been gained by the discovery of the address
+of the house at which he had boarded--a result which was entirely due to
+the acuteness and energy of Mr. Gregson of Scotland Yard.
+
+Sherlock Holmes and I read these notices over together at breakfast, and
+they appeared to afford him considerable amusement.
+
+"I told you that, whatever happened, Lestrade and Gregson would be sure
+to score."
+
+"That depends on how it turns out."
+
+"Oh, bless you, it doesn't matter in the least. If the man is caught, it
+will be *on account* of their exertions; if he escapes, it will be *in
+spite* of their exertions. It's heads I win and tails you lose. Whatever
+they do, they will have followers. 'Un sot trouve toujours un plus sot
+qui l'admire.'"
+
+"What on earth is this?" I cried, for at this moment there came the
+pattering of many steps in the hall and on the stairs, accompanied by
+audible expressions of disgust upon the part of our landlady.
+
+"It's the Baker Street division of the detective police force," said my
+companion, gravely; and as he spoke there rushed into the room half a
+dozen of the dirtiest and most ragged street Arabs that ever I clapped
+eyes on.
+
+"'Tention!" cried Holmes, in a sharp tone, and the six dirty little
+scoundrels stood in a line like so many disreputable statuettes. "In
+future you shall send up Wiggins alone to report, and the rest of you
+must wait in the street. Have you found it, Wiggins?"
+
+"No, sir, we hain't," said one of the youths.
+
+"I hardly expected you would. You must keep on until you do. Here are
+your wages." [13]_ He handed each of them a shilling.
+
+"Now, off you go, and come back with a better report next time."
+
+He waved his hand, and they scampered away downstairs like so many rats,
+and we heard their shrill voices next moment in the street.
+
+"There's more work to be got out of one of those little beggars than
+out of a dozen of the force," Holmes remarked. "The mere sight of an
+official-looking person seals men's lips. These youngsters, however, go
+everywhere and hear everything. They are as sharp as needles, too; all
+they want is organisation."
+
+"Is it on this Brixton case that you are employing them?" I asked.
+
+"Yes; there is a point which I wish to ascertain. It is merely a matter
+of time. Hullo! we are going to hear some news now with a vengeance!
+Here is Gregson coming down the road with beatitude written upon every
+feature of his face. Bound for us, I know. Yes, he is stopping. There he
+is!"
+
+There was a violent peal at the bell, and in a few seconds the
+fair-haired detective came up the stairs, three steps at a time, and
+burst into our sitting-room.
+
+"My dear fellow," he cried, wringing Holmes' unresponsive hand,
+"congratulate me! I have made the whole thing as clear as day."
+
+A shade of anxiety seemed to me to cross my companion's expressive face.
+
+"Do you mean that you are on the right track?" he asked.
+
+"The right track! Why, sir, we have the man under lock and key."
+
+"And his name is?"
+
+"Arthur Charpentier, sub-lieutenant in Her Majesty's navy," cried
+Gregson, pompously, rubbing his fat hands and inflating his chest.
+
+Sherlock Holmes gave a sigh of relief, and relaxed into a smile.
+
+"Take a seat, and try one of these cigars," he said. "We are anxious to
+know how you managed it. Will you have some whiskey and water?"
+
+"I don't mind if I do," the detective answered. "The tremendous
+exertions which I have gone through during the last day or two have worn
+me out. Not so much bodily exertion, you understand, as the strain upon
+the mind. You will appreciate that, Mr. Sherlock Holmes, for we are both
+brain-workers."
+
+"You do me too much honour," said Holmes, gravely. "Let us hear how you
+arrived at this most gratifying result."
+
+The detective seated himself in the arm-chair, and puffed complacently
+at his cigar. Then suddenly he slapped his thigh in a paroxysm of
+amusement.
+
+"The fun of it is," he cried, "that that fool Lestrade, who thinks
+himself so smart, has gone off upon the wrong track altogether. He is
+after the secretary Stangerson, who had no more to do with the crime
+than the babe unborn. I have no doubt that he has caught him by this
+time."
+
+The idea tickled Gregson so much that he laughed until he choked.
+
+"And how did you get your clue?"
+
+"Ah, I'll tell you all about it. Of course, Doctor Watson, this is
+strictly between ourselves. The first difficulty which we had to contend
+with was the finding of this American's antecedents. Some people would
+have waited until their advertisements were answered, or until parties
+came forward and volunteered information. That is not Tobias Gregson's
+way of going to work. You remember the hat beside the dead man?"
+
+"Yes," said Holmes; "by John Underwood and Sons, 129, Camberwell Road."
+
+Gregson looked quite crest-fallen.
+
+"I had no idea that you noticed that," he said. "Have you been there?"
+
+"No."
+
+"Ha!" cried Gregson, in a relieved voice; "you should never neglect a
+chance, however small it may seem."
+
+"To a great mind, nothing is little," remarked Holmes, sententiously.
+
+"Well, I went to Underwood, and asked him if he had sold a hat of that
+size and description. He looked over his books, and came on it at once.
+He had sent the hat to a Mr. Drebber, residing at Charpentier's Boarding
+Establishment, Torquay Terrace. Thus I got at his address."
+
+"Smart--very smart!" murmured Sherlock Holmes.
+
+"I next called upon Madame Charpentier," continued the detective.
+"I found her very pale and distressed. Her daughter was in the room,
+too--an uncommonly fine girl she is, too; she was looking red about
+the eyes and her lips trembled as I spoke to her. That didn't escape
+my notice. I began to smell a rat. You know the feeling, Mr. Sherlock
+Holmes, when you come upon the right scent--a kind of thrill in your
+nerves. 'Have you heard of the mysterious death of your late boarder Mr.
+Enoch J. Drebber, of Cleveland?' I asked.
+
+"The mother nodded. She didn't seem able to get out a word. The daughter
+burst into tears. I felt more than ever that these people knew something
+of the matter.
+
+"'At what o'clock did Mr. Drebber leave your house for the train?' I
+asked.
+
+"'At eight o'clock,' she said, gulping in her throat to keep down her
+agitation. 'His secretary, Mr. Stangerson, said that there were two
+trains--one at 9.15 and one at 11. He was to catch the first. [14]_
+
+"'And was that the last which you saw of him?'
+
+"A terrible change came over the woman's face as I asked the question.
+Her features turned perfectly livid. It was some seconds before she
+could get out the single word 'Yes'--and when it did come it was in a
+husky unnatural tone.
+
+"There was silence for a moment, and then the daughter spoke in a calm
+clear voice.
+
+"'No good can ever come of falsehood, mother,' she said. 'Let us be
+frank with this gentleman. We _did_ see Mr. Drebber again.'
+
+"'God forgive you!' cried Madame Charpentier, throwing up her hands and
+sinking back in her chair. 'You have murdered your brother.'
+
+"'Arthur would rather that we spoke the truth,' the girl answered
+firmly.
+
+"'You had best tell me all about it now,' I said. 'Half-confidences are
+worse than none. Besides, you do not know how much we know of it.'
+
+"'On your head be it, Alice!' cried her mother; and then, turning to me,
+'I will tell you all, sir. Do not imagine that my agitation on behalf
+of my son arises from any fear lest he should have had a hand in this
+terrible affair. He is utterly innocent of it. My dread is, however,
+that in your eyes and in the eyes of others he may appear to be
+compromised. That however is surely impossible. His high character, his
+profession, his antecedents would all forbid it.'
+
+"'Your best way is to make a clean breast of the facts,' I answered.
+'Depend upon it, if your son is innocent he will be none the worse.'
+
+"'Perhaps, Alice, you had better leave us together,' she said, and her
+daughter withdrew. 'Now, sir,' she continued, 'I had no intention of
+telling you all this, but since my poor daughter has disclosed it I
+have no alternative. Having once decided to speak, I will tell you all
+without omitting any particular.'
+
+"'It is your wisest course,' said I.
+
+"'Mr. Drebber has been with us nearly three weeks. He and his secretary,
+Mr. Stangerson, had been travelling on the Continent. I noticed a
+"Copenhagen" label upon each of their trunks, showing that that had been
+their last stopping place. Stangerson was a quiet reserved man, but his
+employer, I am sorry to say, was far otherwise. He was coarse in his
+habits and brutish in his ways. The very night of his arrival he became
+very much the worse for drink, and, indeed, after twelve o'clock in the
+day he could hardly ever be said to be sober. His manners towards the
+maid-servants were disgustingly free and familiar. Worst of all, he
+speedily assumed the same attitude towards my daughter, Alice, and spoke
+to her more than once in a way which, fortunately, she is too innocent
+to understand. On one occasion he actually seized her in his arms and
+embraced her--an outrage which caused his own secretary to reproach him
+for his unmanly conduct.'
+
+"'But why did you stand all this,' I asked. 'I suppose that you can get
+rid of your boarders when you wish.'
+
+"Mrs. Charpentier blushed at my pertinent question. 'Would to God that
+I had given him notice on the very day that he came,' she said. 'But
+it was a sore temptation. They were paying a pound a day each--fourteen
+pounds a week, and this is the slack season. I am a widow, and my boy in
+the Navy has cost me much. I grudged to lose the money. I acted for the
+best. This last was too much, however, and I gave him notice to leave on
+account of it. That was the reason of his going.'
+
+"'Well?'
+
+"'My heart grew light when I saw him drive away. My son is on leave
+just now, but I did not tell him anything of all this, for his temper
+is violent, and he is passionately fond of his sister. When I closed the
+door behind them a load seemed to be lifted from my mind. Alas, in
+less than an hour there was a ring at the bell, and I learned that Mr.
+Drebber had returned. He was much excited, and evidently the worse for
+drink. He forced his way into the room, where I was sitting with my
+daughter, and made some incoherent remark about having missed his train.
+He then turned to Alice, and before my very face, proposed to her that
+she should fly with him. "You are of age," he said, "and there is no law
+to stop you. I have money enough and to spare. Never mind the old girl
+here, but come along with me now straight away. You shall live like a
+princess." Poor Alice was so frightened that she shrunk away from him,
+but he caught her by the wrist and endeavoured to draw her towards the
+door. I screamed, and at that moment my son Arthur came into the room.
+What happened then I do not know. I heard oaths and the confused sounds
+of a scuffle. I was too terrified to raise my head. When I did look up
+I saw Arthur standing in the doorway laughing, with a stick in his hand.
+"I don't think that fine fellow will trouble us again," he said. "I will
+just go after him and see what he does with himself." With those words
+he took his hat and started off down the street. The next morning we
+heard of Mr. Drebber's mysterious death.'
+
+"This statement came from Mrs. Charpentier's lips with many gasps and
+pauses. At times she spoke so low that I could hardly catch the words. I
+made shorthand notes of all that she said, however, so that there should
+be no possibility of a mistake."
+
+"It's quite exciting," said Sherlock Holmes, with a yawn. "What happened
+next?"
+
+"When Mrs. Charpentier paused," the detective continued, "I saw that the
+whole case hung upon one point. Fixing her with my eye in a way which
+I always found effective with women, I asked her at what hour her son
+returned.
+
+"'I do not know,' she answered.
+
+"'Not know?'
+
+"'No; he has a latch-key, and he let himself in.'
+
+"'After you went to bed?'
+
+"'Yes.'
+
+"'When did you go to bed?'
+
+"'About eleven.'
+
+"'So your son was gone at least two hours?'
+
+"'Yes.'
+
+"'Possibly four or five?'
+
+"'Yes.'
+
+"'What was he doing during that time?'
+
+"'I do not know,' she answered, turning white to her very lips.
+
+"Of course after that there was nothing more to be done. I found
+out where Lieutenant Charpentier was, took two officers with me, and
+arrested him. When I touched him on the shoulder and warned him to come
+quietly with us, he answered us as bold as brass, 'I suppose you
+are arresting me for being concerned in the death of that scoundrel
+Drebber,' he said. We had said nothing to him about it, so that his
+alluding to it had a most suspicious aspect."
+
+"Very," said Holmes.
+
+"He still carried the heavy stick which the mother described him as
+having with him when he followed Drebber. It was a stout oak cudgel."
+
+"What is your theory, then?"
+
+"Well, my theory is that he followed Drebber as far as the Brixton Road.
+When there, a fresh altercation arose between them, in the course of
+which Drebber received a blow from the stick, in the pit of the stomach,
+perhaps, which killed him without leaving any mark. The night was so
+wet that no one was about, so Charpentier dragged the body of his victim
+into the empty house. As to the candle, and the blood, and the writing
+on the wall, and the ring, they may all be so many tricks to throw the
+police on to the wrong scent."
+
+"Well done!" said Holmes in an encouraging voice. "Really, Gregson, you
+are getting along. We shall make something of you yet."
+
+"I flatter myself that I have managed it rather neatly," the detective
+answered proudly. "The young man volunteered a statement, in which he
+said that after following Drebber some time, the latter perceived him,
+and took a cab in order to get away from him. On his way home he met an
+old shipmate, and took a long walk with him. On being asked where this
+old shipmate lived, he was unable to give any satisfactory reply. I
+think the whole case fits together uncommonly well. What amuses me is to
+think of Lestrade, who had started off upon the wrong scent. I am afraid
+he won't make much of [15]_ Why, by Jove, here's the very man himself!"
+
+It was indeed Lestrade, who had ascended the stairs while we were
+talking, and who now entered the room. The assurance and jauntiness
+which generally marked his demeanour and dress were, however, wanting.
+His face was disturbed and troubled, while his clothes were disarranged
+and untidy. He had evidently come with the intention of consulting
+with Sherlock Holmes, for on perceiving his colleague he appeared to be
+embarrassed and put out. He stood in the centre of the room, fumbling
+nervously with his hat and uncertain what to do. "This is a most
+extraordinary case," he said at last--"a most incomprehensible affair."
+
+"Ah, you find it so, Mr. Lestrade!" cried Gregson, triumphantly. "I
+thought you would come to that conclusion. Have you managed to find the
+Secretary, Mr. Joseph Stangerson?"
+
+"The Secretary, Mr. Joseph Stangerson," said Lestrade gravely, "was
+murdered at Halliday's Private Hotel about six o'clock this morning."
+
+
+
+
+CHAPTER VII. LIGHT IN THE DARKNESS.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+THE intelligence with which Lestrade greeted us was so momentous and so
+unexpected, that we were all three fairly dumfoundered. Gregson sprang
+out of his chair and upset the remainder of his whiskey and water. I
+stared in silence at Sherlock Holmes, whose lips were compressed and his
+brows drawn down over his eyes.
+
+"Stangerson too!" he muttered. "The plot thickens."
+
+"It was quite thick enough before," grumbled Lestrade, taking a chair.
+"I seem to have dropped into a sort of council of war."
+
+"Are you--are you sure of this piece of intelligence?" stammered
+Gregson.
+
+"I have just come from his room," said Lestrade. "I was the first to
+discover what had occurred."
+
+"We have been hearing Gregson's view of the matter," Holmes observed.
+"Would you mind letting us know what you have seen and done?"
+
+"I have no objection," Lestrade answered, seating himself. "I freely
+confess that I was of the opinion that Stangerson was concerned in
+the death of Drebber. This fresh development has shown me that I was
+completely mistaken. Full of the one idea, I set myself to find out
+what had become of the Secretary. They had been seen together at Euston
+Station about half-past eight on the evening of the third. At two in the
+morning Drebber had been found in the Brixton Road. The question which
+confronted me was to find out how Stangerson had been employed between
+8.30 and the time of the crime, and what had become of him afterwards.
+I telegraphed to Liverpool, giving a description of the man, and warning
+them to keep a watch upon the American boats. I then set to work calling
+upon all the hotels and lodging-houses in the vicinity of Euston. You
+see, I argued that if Drebber and his companion had become separated,
+the natural course for the latter would be to put up somewhere in the
+vicinity for the night, and then to hang about the station again next
+morning."
+
+"They would be likely to agree on some meeting-place beforehand,"
+remarked Holmes.
+
+"So it proved. I spent the whole of yesterday evening in making
+enquiries entirely without avail. This morning I began very early, and
+at eight o'clock I reached Halliday's Private Hotel, in Little George
+Street. On my enquiry as to whether a Mr. Stangerson was living there,
+they at once answered me in the affirmative.
+
+"'No doubt you are the gentleman whom he was expecting,' they said. 'He
+has been waiting for a gentleman for two days.'
+
+"'Where is he now?' I asked.
+
+"'He is upstairs in bed. He wished to be called at nine.'
+
+"'I will go up and see him at once,' I said.
+
+"It seemed to me that my sudden appearance might shake his nerves and
+lead him to say something unguarded. The Boots volunteered to show me
+the room: it was on the second floor, and there was a small corridor
+leading up to it. The Boots pointed out the door to me, and was about to
+go downstairs again when I saw something that made me feel sickish, in
+spite of my twenty years' experience. From under the door there curled
+a little red ribbon of blood, which had meandered across the passage and
+formed a little pool along the skirting at the other side. I gave a cry,
+which brought the Boots back. He nearly fainted when he saw it. The door
+was locked on the inside, but we put our shoulders to it, and knocked it
+in. The window of the room was open, and beside the window, all huddled
+up, lay the body of a man in his nightdress. He was quite dead, and had
+been for some time, for his limbs were rigid and cold. When we turned
+him over, the Boots recognized him at once as being the same gentleman
+who had engaged the room under the name of Joseph Stangerson. The cause
+of death was a deep stab in the left side, which must have penetrated
+the heart. And now comes the strangest part of the affair. What do you
+suppose was above the murdered man?"
+
+I felt a creeping of the flesh, and a presentiment of coming horror,
+even before Sherlock Holmes answered.
+
+"The word RACHE, written in letters of blood," he said.
+
+"That was it," said Lestrade, in an awe-struck voice; and we were all
+silent for a while.
+
+There was something so methodical and so incomprehensible about the
+deeds of this unknown assassin, that it imparted a fresh ghastliness to
+his crimes. My nerves, which were steady enough on the field of battle
+tingled as I thought of it.
+
+"The man was seen," continued Lestrade. "A milk boy, passing on his way
+to the dairy, happened to walk down the lane which leads from the mews
+at the back of the hotel. He noticed that a ladder, which usually lay
+there, was raised against one of the windows of the second floor, which
+was wide open. After passing, he looked back and saw a man descend the
+ladder. He came down so quietly and openly that the boy imagined him to
+be some carpenter or joiner at work in the hotel. He took no particular
+notice of him, beyond thinking in his own mind that it was early for him
+to be at work. He has an impression that the man was tall, had a reddish
+face, and was dressed in a long, brownish coat. He must have stayed in
+the room some little time after the murder, for we found blood-stained
+water in the basin, where he had washed his hands, and marks on the
+sheets where he had deliberately wiped his knife."
+
+I glanced at Holmes on hearing the description of the murderer, which
+tallied so exactly with his own. There was, however, no trace of
+exultation or satisfaction upon his face.
+
+"Did you find nothing in the room which could furnish a clue to the
+murderer?" he asked.
+
+"Nothing. Stangerson had Drebber's purse in his pocket, but it seems
+that this was usual, as he did all the paying. There was eighty odd
+pounds in it, but nothing had been taken. Whatever the motives of these
+extraordinary crimes, robbery is certainly not one of them. There were
+no papers or memoranda in the murdered man's pocket, except a single
+telegram, dated from Cleveland about a month ago, and containing
+the words, 'J. H. is in Europe.' There was no name appended to this
+message."
+
+"And there was nothing else?" Holmes asked.
+
+"Nothing of any importance. The man's novel, with which he had read
+himself to sleep was lying upon the bed, and his pipe was on a chair
+beside him. There was a glass of water on the table, and on the
+window-sill a small chip ointment box containing a couple of pills."
+
+Sherlock Holmes sprang from his chair with an exclamation of delight.
+
+"The last link," he cried, exultantly. "My case is complete."
+
+The two detectives stared at him in amazement.
+
+"I have now in my hands," my companion said, confidently, "all the
+threads which have formed such a tangle. There are, of course, details
+to be filled in, but I am as certain of all the main facts, from the
+time that Drebber parted from Stangerson at the station, up to the
+discovery of the body of the latter, as if I had seen them with my own
+eyes. I will give you a proof of my knowledge. Could you lay your hand
+upon those pills?"
+
+"I have them," said Lestrade, producing a small white box; "I took them
+and the purse and the telegram, intending to have them put in a place of
+safety at the Police Station. It was the merest chance my taking these
+pills, for I am bound to say that I do not attach any importance to
+them."
+
+"Give them here," said Holmes. "Now, Doctor," turning to me, "are those
+ordinary pills?"
+
+They certainly were not. They were of a pearly grey colour, small,
+round, and almost transparent against the light. "From their lightness
+and transparency, I should imagine that they are soluble in water," I
+remarked.
+
+"Precisely so," answered Holmes. "Now would you mind going down and
+fetching that poor little devil of a terrier which has been bad so long,
+and which the landlady wanted you to put out of its pain yesterday."
+
+I went downstairs and carried the dog upstair in my arms. It's laboured
+breathing and glazing eye showed that it was not far from its end.
+Indeed, its snow-white muzzle proclaimed that it had already exceeded
+the usual term of canine existence. I placed it upon a cushion on the
+rug.
+
+"I will now cut one of these pills in two," said Holmes, and drawing his
+penknife he suited the action to the word. "One half we return into the
+box for future purposes. The other half I will place in this wine glass,
+in which is a teaspoonful of water. You perceive that our friend, the
+Doctor, is right, and that it readily dissolves."
+
+"This may be very interesting," said Lestrade, in the injured tone of
+one who suspects that he is being laughed at, "I cannot see, however,
+what it has to do with the death of Mr. Joseph Stangerson."
+
+"Patience, my friend, patience! You will find in time that it has
+everything to do with it. I shall now add a little milk to make the
+mixture palatable, and on presenting it to the dog we find that he laps
+it up readily enough."
+
+As he spoke he turned the contents of the wine glass into a saucer and
+placed it in front of the terrier, who speedily licked it dry. Sherlock
+Holmes' earnest demeanour had so far convinced us that we all sat in
+silence, watching the animal intently, and expecting some startling
+effect. None such appeared, however. The dog continued to lie stretched
+upon tho [16]_ cushion, breathing in a laboured way, but apparently
+neither the better nor the worse for its draught.
+
+Holmes had taken out his watch, and as minute followed minute without
+result, an expression of the utmost chagrin and disappointment appeared
+upon his features. He gnawed his lip, drummed his fingers upon the
+table, and showed every other symptom of acute impatience. So great
+was his emotion, that I felt sincerely sorry for him, while the two
+detectives smiled derisively, by no means displeased at this check which
+he had met.
+
+"It can't be a coincidence," he cried, at last springing from his chair
+and pacing wildly up and down the room; "it is impossible that it should
+be a mere coincidence. The very pills which I suspected in the case of
+Drebber are actually found after the death of Stangerson. And yet they
+are inert. What can it mean? Surely my whole chain of reasoning cannot
+have been false. It is impossible! And yet this wretched dog is none the
+worse. Ah, I have it! I have it!" With a perfect shriek of delight he
+rushed to the box, cut the other pill in two, dissolved it, added milk,
+and presented it to the terrier. The unfortunate creature's tongue
+seemed hardly to have been moistened in it before it gave a convulsive
+shiver in every limb, and lay as rigid and lifeless as if it had been
+struck by lightning.
+
+Sherlock Holmes drew a long breath, and wiped the perspiration from his
+forehead. "I should have more faith," he said; "I ought to know by
+this time that when a fact appears to be opposed to a long train of
+deductions, it invariably proves to be capable of bearing some other
+interpretation. Of the two pills in that box one was of the most deadly
+poison, and the other was entirely harmless. I ought to have known that
+before ever I saw the box at all."
+
+This last statement appeared to me to be so startling, that I could
+hardly believe that he was in his sober senses. There was the dead dog,
+however, to prove that his conjecture had been correct. It seemed to me
+that the mists in my own mind were gradually clearing away, and I began
+to have a dim, vague perception of the truth.
+
+"All this seems strange to you," continued Holmes, "because you failed
+at the beginning of the inquiry to grasp the importance of the single
+real clue which was presented to you. I had the good fortune to seize
+upon that, and everything which has occurred since then has served to
+confirm my original supposition, and, indeed, was the logical sequence
+of it. Hence things which have perplexed you and made the case more
+obscure, have served to enlighten me and to strengthen my conclusions.
+It is a mistake to confound strangeness with mystery. The most
+commonplace crime is often the most mysterious because it presents no
+new or special features from which deductions may be drawn. This murder
+would have been infinitely more difficult to unravel had the body of
+the victim been simply found lying in the roadway without any of
+those _outré_ and sensational accompaniments which have rendered
+it remarkable. These strange details, far from making the case more
+difficult, have really had the effect of making it less so."
+
+Mr. Gregson, who had listened to this address with considerable
+impatience, could contain himself no longer. "Look here, Mr. Sherlock
+Holmes," he said, "we are all ready to acknowledge that you are a smart
+man, and that you have your own methods of working. We want something
+more than mere theory and preaching now, though. It is a case of taking
+the man. I have made my case out, and it seems I was wrong. Young
+Charpentier could not have been engaged in this second affair. Lestrade
+went after his man, Stangerson, and it appears that he was wrong too.
+You have thrown out hints here, and hints there, and seem to know more
+than we do, but the time has come when we feel that we have a right to
+ask you straight how much you do know of the business. Can you name the
+man who did it?"
+
+"I cannot help feeling that Gregson is right, sir," remarked Lestrade.
+"We have both tried, and we have both failed. You have remarked more
+than once since I have been in the room that you had all the evidence
+which you require. Surely you will not withhold it any longer."
+
+"Any delay in arresting the assassin," I observed, "might give him time
+to perpetrate some fresh atrocity."
+
+Thus pressed by us all, Holmes showed signs of irresolution. He
+continued to walk up and down the room with his head sunk on his chest
+and his brows drawn down, as was his habit when lost in thought.
+
+"There will be no more murders," he said at last, stopping abruptly and
+facing us. "You can put that consideration out of the question. You have
+asked me if I know the name of the assassin. I do. The mere knowing of
+his name is a small thing, however, compared with the power of laying
+our hands upon him. This I expect very shortly to do. I have good hopes
+of managing it through my own arrangements; but it is a thing which
+needs delicate handling, for we have a shrewd and desperate man to deal
+with, who is supported, as I have had occasion to prove, by another who
+is as clever as himself. As long as this man has no idea that anyone
+can have a clue there is some chance of securing him; but if he had the
+slightest suspicion, he would change his name, and vanish in an instant
+among the four million inhabitants of this great city. Without meaning
+to hurt either of your feelings, I am bound to say that I consider these
+men to be more than a match for the official force, and that is why I
+have not asked your assistance. If I fail I shall, of course, incur all
+the blame due to this omission; but that I am prepared for. At present
+I am ready to promise that the instant that I can communicate with you
+without endangering my own combinations, I shall do so."
+
+Gregson and Lestrade seemed to be far from satisfied by this assurance,
+or by the depreciating allusion to the detective police. The former had
+flushed up to the roots of his flaxen hair, while the other's beady eyes
+glistened with curiosity and resentment. Neither of them had time to
+speak, however, before there was a tap at the door, and the spokesman
+of the street Arabs, young Wiggins, introduced his insignificant and
+unsavoury person.
+
+"Please, sir," he said, touching his forelock, "I have the cab
+downstairs."
+
+"Good boy," said Holmes, blandly. "Why don't you introduce this pattern
+at Scotland Yard?" he continued, taking a pair of steel handcuffs from
+a drawer. "See how beautifully the spring works. They fasten in an
+instant."
+
+"The old pattern is good enough," remarked Lestrade, "if we can only
+find the man to put them on."
+
+"Very good, very good," said Holmes, smiling. "The cabman may as well
+help me with my boxes. Just ask him to step up, Wiggins."
+
+I was surprised to find my companion speaking as though he were about
+to set out on a journey, since he had not said anything to me about it.
+There was a small portmanteau in the room, and this he pulled out and
+began to strap. He was busily engaged at it when the cabman entered the
+room.
+
+"Just give me a help with this buckle, cabman," he said, kneeling over
+his task, and never turning his head.
+
+The fellow came forward with a somewhat sullen, defiant air, and put
+down his hands to assist. At that instant there was a sharp click, the
+jangling of metal, and Sherlock Holmes sprang to his feet again.
+
+"Gentlemen," he cried, with flashing eyes, "let me introduce you to Mr.
+Jefferson Hope, the murderer of Enoch Drebber and of Joseph Stangerson."
+
+The whole thing occurred in a moment--so quickly that I had no time
+to realize it. I have a vivid recollection of that instant, of Holmes'
+triumphant expression and the ring of his voice, of the cabman's
+dazed, savage face, as he glared at the glittering handcuffs, which had
+appeared as if by magic upon his wrists. For a second or two we might
+have been a group of statues. Then, with an inarticulate roar of fury,
+the prisoner wrenched himself free from Holmes's grasp, and hurled
+himself through the window. Woodwork and glass gave way before him; but
+before he got quite through, Gregson, Lestrade, and Holmes sprang upon
+him like so many staghounds. He was dragged back into the room, and then
+commenced a terrific conflict. So powerful and so fierce was he, that
+the four of us were shaken off again and again. He appeared to have the
+convulsive strength of a man in an epileptic fit. His face and hands
+were terribly mangled by his passage through the glass, but loss of
+blood had no effect in diminishing his resistance. It was not until
+Lestrade succeeded in getting his hand inside his neckcloth and
+half-strangling him that we made him realize that his struggles were of
+no avail; and even then we felt no security until we had pinioned his
+feet as well as his hands. That done, we rose to our feet breathless and
+panting.
+
+"We have his cab," said Sherlock Holmes. "It will serve to take him to
+Scotland Yard. And now, gentlemen," he continued, with a pleasant smile,
+"we have reached the end of our little mystery. You are very welcome to
+put any questions that you like to me now, and there is no danger that I
+will refuse to answer them."
+
+
+
+
+
+PART II. *The Country of the Saints.*
+-------------------------------------
+
+
+
+CHAPTER I. ON THE GREAT ALKALI PLAIN.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+IN the central portion of the great North American Continent there lies
+an arid and repulsive desert, which for many a long year served as a
+barrier against the advance of civilisation. From the Sierra Nevada to
+Nebraska, and from the Yellowstone River in the north to the Colorado
+upon the south, is a region of desolation and silence. Nor is Nature
+always in one mood throughout this grim district. It comprises
+snow-capped and lofty mountains, and dark and gloomy valleys. There are
+swift-flowing rivers which dash through jagged cañons; and there are
+enormous plains, which in winter are white with snow, and in summer are
+grey with the saline alkali dust. They all preserve, however, the common
+characteristics of barrenness, inhospitality, and misery.
+
+There are no inhabitants of this land of despair. A band of Pawnees
+or of Blackfeet may occasionally traverse it in order to reach other
+hunting-grounds, but the hardiest of the braves are glad to lose sight
+of those awesome plains, and to find themselves once more upon their
+prairies. The coyote skulks among the scrub, the buzzard flaps heavily
+through the air, and the clumsy grizzly bear lumbers through the dark
+ravines, and picks up such sustenance as it can amongst the rocks. These
+are the sole dwellers in the wilderness.
+
+In the whole world there can be no more dreary view than that from
+the northern slope of the Sierra Blanco. As far as the eye can reach
+stretches the great flat plain-land, all dusted over with patches of
+alkali, and intersected by clumps of the dwarfish chaparral bushes. On
+the extreme verge of the horizon lie a long chain of mountain peaks,
+with their rugged summits flecked with snow. In this great stretch of
+country there is no sign of life, nor of anything appertaining to life.
+There is no bird in the steel-blue heaven, no movement upon the dull,
+grey earth--above all, there is absolute silence. Listen as one may,
+there is no shadow of a sound in all that mighty wilderness; nothing but
+silence--complete and heart-subduing silence.
+
+It has been said there is nothing appertaining to life upon the broad
+plain. That is hardly true. Looking down from the Sierra Blanco, one
+sees a pathway traced out across the desert, which winds away and is
+lost in the extreme distance. It is rutted with wheels and trodden down
+by the feet of many adventurers. Here and there there are scattered
+white objects which glisten in the sun, and stand out against the dull
+deposit of alkali. Approach, and examine them! They are bones: some
+large and coarse, others smaller and more delicate. The former have
+belonged to oxen, and the latter to men. For fifteen hundred miles one
+may trace this ghastly caravan route by these scattered remains of those
+who had fallen by the wayside.
+
+Looking down on this very scene, there stood upon the fourth of May,
+eighteen hundred and forty-seven, a solitary traveller. His appearance
+was such that he might have been the very genius or demon of the region.
+An observer would have found it difficult to say whether he was nearer
+to forty or to sixty. His face was lean and haggard, and the brown
+parchment-like skin was drawn tightly over the projecting bones; his
+long, brown hair and beard were all flecked and dashed with white; his
+eyes were sunken in his head, and burned with an unnatural lustre; while
+the hand which grasped his rifle was hardly more fleshy than that of a
+skeleton. As he stood, he leaned upon his weapon for support, and yet
+his tall figure and the massive framework of his bones suggested a wiry
+and vigorous constitution. His gaunt face, however, and his clothes,
+which hung so baggily over his shrivelled limbs, proclaimed what it
+was that gave him that senile and decrepit appearance. The man was
+dying--dying from hunger and from thirst.
+
+He had toiled painfully down the ravine, and on to this little
+elevation, in the vain hope of seeing some signs of water. Now the great
+salt plain stretched before his eyes, and the distant belt of savage
+mountains, without a sign anywhere of plant or tree, which might
+indicate the presence of moisture. In all that broad landscape there
+was no gleam of hope. North, and east, and west he looked with wild
+questioning eyes, and then he realised that his wanderings had come to
+an end, and that there, on that barren crag, he was about to die. "Why
+not here, as well as in a feather bed, twenty years hence," he muttered,
+as he seated himself in the shelter of a boulder.
+
+Before sitting down, he had deposited upon the ground his useless rifle,
+and also a large bundle tied up in a grey shawl, which he had carried
+slung over his right shoulder. It appeared to be somewhat too heavy for
+his strength, for in lowering it, it came down on the ground with some
+little violence. Instantly there broke from the grey parcel a little
+moaning cry, and from it there protruded a small, scared face, with very
+bright brown eyes, and two little speckled, dimpled fists.
+
+"You've hurt me!" said a childish voice reproachfully.
+
+"Have I though," the man answered penitently, "I didn't go for to do
+it." As he spoke he unwrapped the grey shawl and extricated a pretty
+little girl of about five years of age, whose dainty shoes and smart
+pink frock with its little linen apron all bespoke a mother's care. The
+child was pale and wan, but her healthy arms and legs showed that she
+had suffered less than her companion.
+
+"How is it now?" he answered anxiously, for she was still rubbing the
+towsy golden curls which covered the back of her head.
+
+"Kiss it and make it well," she said, with perfect gravity, shoving
+[19]_ the injured part up to him. "That's what mother used to do. Where's
+mother?"
+
+"Mother's gone. I guess you'll see her before long."
+
+"Gone, eh!" said the little girl. "Funny, she didn't say good-bye; she
+'most always did if she was just goin' over to Auntie's for tea, and now
+she's been away three days. Say, it's awful dry, ain't it? Ain't there
+no water, nor nothing to eat?"
+
+"No, there ain't nothing, dearie. You'll just need to be patient awhile,
+and then you'll be all right. Put your head up agin me like that, and
+then you'll feel bullier. It ain't easy to talk when your lips is like
+leather, but I guess I'd best let you know how the cards lie. What's
+that you've got?"
+
+"Pretty things! fine things!" cried the little girl enthusiastically,
+holding up two glittering fragments of mica. "When we goes back to home
+I'll give them to brother Bob."
+
+"You'll see prettier things than them soon," said the man confidently.
+"You just wait a bit. I was going to tell you though--you remember when
+we left the river?"
+
+"Oh, yes."
+
+"Well, we reckoned we'd strike another river soon, d'ye see. But there
+was somethin' wrong; compasses, or map, or somethin', and it didn't
+turn up. Water ran out. Just except a little drop for the likes of you
+and--and----"
+
+"And you couldn't wash yourself," interrupted his companion gravely,
+staring up at his grimy visage.
+
+"No, nor drink. And Mr. Bender, he was the fust to go, and then Indian
+Pete, and then Mrs. McGregor, and then Johnny Hones, and then, dearie,
+your mother."
+
+"Then mother's a deader too," cried the little girl dropping her face in
+her pinafore and sobbing bitterly.
+
+"Yes, they all went except you and me. Then I thought there was some
+chance of water in this direction, so I heaved you over my shoulder and
+we tramped it together. It don't seem as though we've improved matters.
+There's an almighty small chance for us now!"
+
+"Do you mean that we are going to die too?" asked the child, checking
+her sobs, and raising her tear-stained face.
+
+"I guess that's about the size of it."
+
+"Why didn't you say so before?" she said, laughing gleefully. "You gave
+me such a fright. Why, of course, now as long as we die we'll be with
+mother again."
+
+"Yes, you will, dearie."
+
+"And you too. I'll tell her how awful good you've been. I'll bet she
+meets us at the door of Heaven with a big pitcher of water, and a lot
+of buckwheat cakes, hot, and toasted on both sides, like Bob and me was
+fond of. How long will it be first?"
+
+"I don't know--not very long." The man's eyes were fixed upon the
+northern horizon. In the blue vault of the heaven there had appeared
+three little specks which increased in size every moment, so rapidly did
+they approach. They speedily resolved themselves into three large brown
+birds, which circled over the heads of the two wanderers, and then
+settled upon some rocks which overlooked them. They were buzzards, the
+vultures of the west, whose coming is the forerunner of death.
+
+"Cocks and hens," cried the little girl gleefully, pointing at their
+ill-omened forms, and clapping her hands to make them rise. "Say, did
+God make this country?"
+
+"In course He did," said her companion, rather startled by this
+unexpected question.
+
+"He made the country down in Illinois, and He made the Missouri," the
+little girl continued. "I guess somebody else made the country in these
+parts. It's not nearly so well done. They forgot the water and the
+trees."
+
+"What would ye think of offering up prayer?" the man asked diffidently.
+
+"It ain't night yet," she answered.
+
+"It don't matter. It ain't quite regular, but He won't mind that, you
+bet. You say over them ones that you used to say every night in the
+waggon when we was on the Plains."
+
+"Why don't you say some yourself?" the child asked, with wondering eyes.
+
+"I disremember them," he answered. "I hain't said none since I was half
+the height o' that gun. I guess it's never too late. You say them out,
+and I'll stand by and come in on the choruses."
+
+"Then you'll need to kneel down, and me too," she said, laying the shawl
+out for that purpose. "You've got to put your hands up like this. It
+makes you feel kind o' good."
+
+It was a strange sight had there been anything but the buzzards to see
+it. Side by side on the narrow shawl knelt the two wanderers, the little
+prattling child and the reckless, hardened adventurer. Her chubby face,
+and his haggard, angular visage were both turned up to the cloudless
+heaven in heartfelt entreaty to that dread being with whom they were
+face to face, while the two voices--the one thin and clear, the other
+deep and harsh--united in the entreaty for mercy and forgiveness. The
+prayer finished, they resumed their seat in the shadow of the boulder
+until the child fell asleep, nestling upon the broad breast of her
+protector. He watched over her slumber for some time, but Nature proved
+to be too strong for him. For three days and three nights he had allowed
+himself neither rest nor repose. Slowly the eyelids drooped over the
+tired eyes, and the head sunk lower and lower upon the breast, until the
+man's grizzled beard was mixed with the gold tresses of his companion,
+and both slept the same deep and dreamless slumber.
+
+Had the wanderer remained awake for another half hour a strange sight
+would have met his eyes. Far away on the extreme verge of the alkali
+plain there rose up a little spray of dust, very slight at first, and
+hardly to be distinguished from the mists of the distance, but gradually
+growing higher and broader until it formed a solid, well-defined cloud.
+This cloud continued to increase in size until it became evident that it
+could only be raised by a great multitude of moving creatures. In more
+fertile spots the observer would have come to the conclusion that one
+of those great herds of bisons which graze upon the prairie land was
+approaching him. This was obviously impossible in these arid wilds. As
+the whirl of dust drew nearer to the solitary bluff upon which the two
+castaways were reposing, the canvas-covered tilts of waggons and the
+figures of armed horsemen began to show up through the haze, and the
+apparition revealed itself as being a great caravan upon its journey for
+the West. But what a caravan! When the head of it had reached the base
+of the mountains, the rear was not yet visible on the horizon. Right
+across the enormous plain stretched the straggling array, waggons
+and carts, men on horseback, and men on foot. Innumerable women who
+staggered along under burdens, and children who toddled beside the
+waggons or peeped out from under the white coverings. This was evidently
+no ordinary party of immigrants, but rather some nomad people who had
+been compelled from stress of circumstances to seek themselves a new
+country. There rose through the clear air a confused clattering and
+rumbling from this great mass of humanity, with the creaking of wheels
+and the neighing of horses. Loud as it was, it was not sufficient to
+rouse the two tired wayfarers above them.
+
+At the head of the column there rode a score or more of grave ironfaced
+men, clad in sombre homespun garments and armed with rifles. On reaching
+the base of the bluff they halted, and held a short council among
+themselves.
+
+"The wells are to the right, my brothers," said one, a hard-lipped,
+clean-shaven man with grizzly hair.
+
+"To the right of the Sierra Blanco--so we shall reach the Rio Grande,"
+said another.
+
+"Fear not for water," cried a third. "He who could draw it from the
+rocks will not now abandon His own chosen people."
+
+"Amen! Amen!" responded the whole party.
+
+They were about to resume their journey when one of the youngest and
+keenest-eyed uttered an exclamation and pointed up at the rugged crag
+above them. From its summit there fluttered a little wisp of pink,
+showing up hard and bright against the grey rocks behind. At the sight
+there was a general reining up of horses and unslinging of guns, while
+fresh horsemen came galloping up to reinforce the vanguard. The word
+'Redskins' was on every lip.
+
+"There can't be any number of Injuns here," said the elderly man who
+appeared to be in command. "We have passed the Pawnees, and there are no
+other tribes until we cross the great mountains."
+
+"Shall I go forward and see, Brother Stangerson," asked one of the band.
+
+"And I," "and I," cried a dozen voices.
+
+"Leave your horses below and we will await you here," the Elder
+answered. In a moment the young fellows had dismounted, fastened their
+horses, and were ascending the precipitous slope which led up to the
+object which had excited their curiosity. They advanced rapidly and
+noiselessly, with the confidence and dexterity of practised scouts.
+The watchers from the plain below could see them flit from rock to rock
+until their figures stood out against the skyline. The young man who had
+first given the alarm was leading them. Suddenly his followers saw him
+throw up his hands, as though overcome with astonishment, and on joining
+him they were affected in the same way by the sight which met their
+eyes.
+
+On the little plateau which crowned the barren hill there stood a
+single giant boulder, and against this boulder there lay a tall man,
+long-bearded and hard-featured, but of an excessive thinness. His placid
+face and regular breathing showed that he was fast asleep. Beside him
+lay a little child, with her round white arms encircling his brown
+sinewy neck, and her golden haired head resting upon the breast of his
+velveteen tunic. Her rosy lips were parted, showing the regular line of
+snow-white teeth within, and a playful smile played over her infantile
+features. Her plump little white legs terminating in white socks and
+neat shoes with shining buckles, offered a strange contrast to the long
+shrivelled members of her companion. On the ledge of rock above this
+strange couple there stood three solemn buzzards, who, at the sight of
+the new comers uttered raucous screams of disappointment and flapped
+sullenly away.
+
+The cries of the foul birds awoke the two sleepers who stared about [20]_
+them in bewilderment. The man staggered to his feet and looked down upon
+the plain which had been so desolate when sleep had overtaken him, and
+which was now traversed by this enormous body of men and of beasts. His
+face assumed an expression of incredulity as he gazed, and he passed his
+boney hand over his eyes. "This is what they call delirium, I guess,"
+he muttered. The child stood beside him, holding on to the skirt of
+his coat, and said nothing but looked all round her with the wondering
+questioning gaze of childhood.
+
+The rescuing party were speedily able to convince the two castaways that
+their appearance was no delusion. One of them seized the little girl,
+and hoisted her upon his shoulder, while two others supported her gaunt
+companion, and assisted him towards the waggons.
+
+"My name is John Ferrier," the wanderer explained; "me and that little
+un are all that's left o' twenty-one people. The rest is all dead o'
+thirst and hunger away down in the south."
+
+"Is she your child?" asked someone.
+
+"I guess she is now," the other cried, defiantly; "she's mine 'cause I
+saved her. No man will take her from me. She's Lucy Ferrier from this
+day on. Who are you, though?" he continued, glancing with curiosity at
+his stalwart, sunburned rescuers; "there seems to be a powerful lot of
+ye."
+
+"Nigh upon ten thousand," said one of the young men; "we are the
+persecuted children of God--the chosen of the Angel Merona."
+
+"I never heard tell on him," said the wanderer. "He appears to have
+chosen a fair crowd of ye."
+
+"Do not jest at that which is sacred," said the other sternly. "We are
+of those who believe in those sacred writings, drawn in Egyptian letters
+on plates of beaten gold, which were handed unto the holy Joseph Smith
+at Palmyra. We have come from Nauvoo, in the State of Illinois, where we
+had founded our temple. We have come to seek a refuge from the violent
+man and from the godless, even though it be the heart of the desert."
+
+The name of Nauvoo evidently recalled recollections to John Ferrier. "I
+see," he said, "you are the Mormons."
+
+"We are the Mormons," answered his companions with one voice.
+
+"And where are you going?"
+
+"We do not know. The hand of God is leading us under the person of our
+Prophet. You must come before him. He shall say what is to be done with
+you."
+
+They had reached the base of the hill by this time, and were surrounded
+by crowds of the pilgrims--pale-faced meek-looking women, strong
+laughing children, and anxious earnest-eyed men. Many were the cries
+of astonishment and of commiseration which arose from them when they
+perceived the youth of one of the strangers and the destitution of the
+other. Their escort did not halt, however, but pushed on, followed by
+a great crowd of Mormons, until they reached a waggon, which was
+conspicuous for its great size and for the gaudiness and smartness of
+its appearance. Six horses were yoked to it, whereas the others were
+furnished with two, or, at most, four a-piece. Beside the driver there
+sat a man who could not have been more than thirty years of age, but
+whose massive head and resolute expression marked him as a leader. He
+was reading a brown-backed volume, but as the crowd approached he laid
+it aside, and listened attentively to an account of the episode. Then he
+turned to the two castaways.
+
+"If we take you with us," he said, in solemn words, "it can only be as
+believers in our own creed. We shall have no wolves in our fold. Better
+far that your bones should bleach in this wilderness than that you
+should prove to be that little speck of decay which in time corrupts the
+whole fruit. Will you come with us on these terms?"
+
+"Guess I'll come with you on any terms," said Ferrier, with such
+emphasis that the grave Elders could not restrain a smile. The leader
+alone retained his stern, impressive expression.
+
+"Take him, Brother Stangerson," he said, "give him food and drink,
+and the child likewise. Let it be your task also to teach him our holy
+creed. We have delayed long enough. Forward! On, on to Zion!"
+
+"On, on to Zion!" cried the crowd of Mormons, and the words rippled down
+the long caravan, passing from mouth to mouth until they died away in a
+dull murmur in the far distance. With a cracking of whips and a creaking
+of wheels the great waggons got into motion, and soon the whole caravan
+was winding along once more. The Elder to whose care the two waifs
+had been committed, led them to his waggon, where a meal was already
+awaiting them.
+
+"You shall remain here," he said. "In a few days you will have recovered
+from your fatigues. In the meantime, remember that now and for ever you
+are of our religion. Brigham Young has said it, and he has spoken with
+the voice of Joseph Smith, which is the voice of God."
+
+
+
+
+CHAPTER II. THE FLOWER OF UTAH.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+THIS is not the place to commemorate the trials and privations endured
+by the immigrant Mormons before they came to their final haven. From the
+shores of the Mississippi to the western slopes of the Rocky Mountains
+they had struggled on with a constancy almost unparalleled in history.
+The savage man, and the savage beast, hunger, thirst, fatigue, and
+disease--every impediment which Nature could place in the way, had all
+been overcome with Anglo-Saxon tenacity. Yet the long journey and the
+accumulated terrors had shaken the hearts of the stoutest among them.
+There was not one who did not sink upon his knees in heartfelt prayer
+when they saw the broad valley of Utah bathed in the sunlight beneath
+them, and learned from the lips of their leader that this was the
+promised land, and that these virgin acres were to be theirs for
+evermore.
+
+Young speedily proved himself to be a skilful administrator as well as a
+resolute chief. Maps were drawn and charts prepared, in which the future
+city was sketched out. All around farms were apportioned and allotted in
+proportion to the standing of each individual. The tradesman was put
+to his trade and the artisan to his calling. In the town streets and
+squares sprang up, as if by magic. In the country there was draining
+and hedging, planting and clearing, until the next summer saw the whole
+country golden with the wheat crop. Everything prospered in the strange
+settlement. Above all, the great temple which they had erected in the
+centre of the city grew ever taller and larger. From the first blush of
+dawn until the closing of the twilight, the clatter of the hammer
+and the rasp of the saw was never absent from the monument which the
+immigrants erected to Him who had led them safe through many dangers.
+
+The two castaways, John Ferrier and the little girl who had shared his
+fortunes and had been adopted as his daughter, accompanied the Mormons
+to the end of their great pilgrimage. Little Lucy Ferrier was borne
+along pleasantly enough in Elder Stangerson's waggon, a retreat which
+she shared with the Mormon's three wives and with his son, a headstrong
+forward boy of twelve. Having rallied, with the elasticity of childhood,
+from the shock caused by her mother's death, she soon became a pet
+with the women, and reconciled herself to this new life in her moving
+canvas-covered home. In the meantime Ferrier having recovered from his
+privations, distinguished himself as a useful guide and an indefatigable
+hunter. So rapidly did he gain the esteem of his new companions, that
+when they reached the end of their wanderings, it was unanimously agreed
+that he should be provided with as large and as fertile a tract of land
+as any of the settlers, with the exception of Young himself, and of
+Stangerson, Kemball, Johnston, and Drebber, who were the four principal
+Elders.
+
+On the farm thus acquired John Ferrier built himself a substantial
+log-house, which received so many additions in succeeding years that it
+grew into a roomy villa. He was a man of a practical turn of mind,
+keen in his dealings and skilful with his hands. His iron constitution
+enabled him to work morning and evening at improving and tilling his
+lands. Hence it came about that his farm and all that belonged to
+him prospered exceedingly. In three years he was better off than his
+neighbours, in six he was well-to-do, in nine he was rich, and in twelve
+there were not half a dozen men in the whole of Salt Lake City who could
+compare with him. From the great inland sea to the distant Wahsatch
+Mountains there was no name better known than that of John Ferrier.
+
+There was one way and only one in which he offended the susceptibilities
+of his co-religionists. No argument or persuasion could ever induce him
+to set up a female establishment after the manner of his companions. He
+never gave reasons for this persistent refusal, but contented himself by
+resolutely and inflexibly adhering to his determination. There were some
+who accused him of lukewarmness in his adopted religion, and others who
+put it down to greed of wealth and reluctance to incur expense. Others,
+again, spoke of some early love affair, and of a fair-haired girl who
+had pined away on the shores of the Atlantic. Whatever the reason,
+Ferrier remained strictly celibate. In every other respect he conformed
+to the religion of the young settlement, and gained the name of being an
+orthodox and straight-walking man.
+
+Lucy Ferrier grew up within the log-house, and assisted her adopted
+father in all his undertakings. The keen air of the mountains and the
+balsamic odour of the pine trees took the place of nurse and mother to
+the young girl. As year succeeded to year she grew taller and stronger,
+her cheek more rudy, and her step more elastic. Many a wayfarer upon
+the high road which ran by Ferrier's farm felt long-forgotten thoughts
+revive in their mind as they watched her lithe girlish figure tripping
+through the wheatfields, or met her mounted upon her father's mustang,
+and managing it with all the ease and grace of a true child of the West.
+So the bud blossomed into a flower, and the year which saw her father
+the richest of the farmers left her as fair a specimen of American
+girlhood as could be found in the whole Pacific slope.
+
+It was not the father, however, who first discovered that the child had
+developed into the woman. It seldom is in such cases. That mysterious
+change is too subtle and too gradual to be measured by dates. Least of
+all does the maiden herself know it until the tone of a voice or the
+touch of a hand sets her heart thrilling within her, and she learns,
+with a mixture of pride and of fear, that a new and a larger nature has
+awoken within her. There are few who cannot recall that day and remember
+the one little incident which heralded the dawn of a new life. In the
+case of Lucy Ferrier the occasion was serious enough in itself, apart
+from its future influence on her destiny and that of many besides.
+
+It was a warm June morning, and the Latter Day Saints were as busy as
+the bees whose hive they have chosen for their emblem. In the fields and
+in the streets rose the same hum of human industry. Down the dusty high
+roads defiled long streams of heavily-laden mules, all heading to the
+west, for the gold fever had broken out in California, and the Overland
+Route lay through the City of the Elect. There, too, were droves of
+sheep and bullocks coming in from the outlying pasture lands, and trains
+of tired immigrants, men and horses equally weary of their interminable
+journey. Through all this motley assemblage, threading her way with the
+skill of an accomplished rider, there galloped Lucy Ferrier, her fair
+face flushed with the exercise and her long chestnut hair floating out
+behind her. She had a commission from her father in the City, and was
+dashing in as she had done many a time before, with all the fearlessness
+of youth, thinking only of her task and how it was to be performed. The
+travel-stained adventurers gazed after her in astonishment, and even
+the unemotional Indians, journeying in with their pelties, relaxed their
+accustomed stoicism as they marvelled at the beauty of the pale-faced
+maiden.
+
+She had reached the outskirts of the city when she found the road
+blocked by a great drove of cattle, driven by a half-dozen wild-looking
+herdsmen from the plains. In her impatience she endeavoured to pass this
+obstacle by pushing her horse into what appeared to be a gap. Scarcely
+had she got fairly into it, however, before the beasts closed in behind
+her, and she found herself completely imbedded in the moving stream of
+fierce-eyed, long-horned bullocks. Accustomed as she was to deal with
+cattle, she was not alarmed at her situation, but took advantage of
+every opportunity to urge her horse on in the hopes of pushing her way
+through the cavalcade. Unfortunately the horns of one of the creatures,
+either by accident or design, came in violent contact with the flank of
+the mustang, and excited it to madness. In an instant it reared up upon
+its hind legs with a snort of rage, and pranced and tossed in a way that
+would have unseated any but a most skilful rider. The situation was full
+of peril. Every plunge of the excited horse brought it against the horns
+again, and goaded it to fresh madness. It was all that the girl could
+do to keep herself in the saddle, yet a slip would mean a terrible death
+under the hoofs of the unwieldy and terrified animals. Unaccustomed to
+sudden emergencies, her head began to swim, and her grip upon the bridle
+to relax. Choked by the rising cloud of dust and by the steam from the
+struggling creatures, she might have abandoned her efforts in despair,
+but for a kindly voice at her elbow which assured her of assistance. At
+the same moment a sinewy brown hand caught the frightened horse by
+the curb, and forcing a way through the drove, soon brought her to the
+outskirts.
+
+"You're not hurt, I hope, miss," said her preserver, respectfully.
+
+She looked up at his dark, fierce face, and laughed saucily. "I'm awful
+frightened," she said, naively; "whoever would have thought that Poncho
+would have been so scared by a lot of cows?"
+
+"Thank God you kept your seat," the other said earnestly. He was a tall,
+savage-looking young fellow, mounted on a powerful roan horse, and
+clad in the rough dress of a hunter, with a long rifle slung over his
+shoulders. "I guess you are the daughter of John Ferrier," he remarked,
+"I saw you ride down from his house. When you see him, ask him if he
+remembers the Jefferson Hopes of St. Louis. If he's the same Ferrier, my
+father and he were pretty thick."
+
+"Hadn't you better come and ask yourself?" she asked, demurely.
+
+The young fellow seemed pleased at the suggestion, and his dark eyes
+sparkled with pleasure. "I'll do so," he said, "we've been in the
+mountains for two months, and are not over and above in visiting
+condition. He must take us as he finds us."
+
+"He has a good deal to thank you for, and so have I," she answered,
+"he's awful fond of me. If those cows had jumped on me he'd have never
+got over it."
+
+"Neither would I," said her companion.
+
+"You! Well, I don't see that it would make much matter to you, anyhow.
+You ain't even a friend of ours."
+
+The young hunter's dark face grew so gloomy over this remark that Lucy
+Ferrier laughed aloud.
+
+"There, I didn't mean that," she said; "of course, you are a friend now.
+You must come and see us. Now I must push along, or father won't trust
+me with his business any more. Good-bye!"
+
+"Good-bye," he answered, raising his broad sombrero, and bending over
+her little hand. She wheeled her mustang round, gave it a cut with her
+riding-whip, and darted away down the broad road in a rolling cloud of
+dust.
+
+Young Jefferson Hope rode on with his companions, gloomy and taciturn.
+He and they had been among the Nevada Mountains prospecting for silver,
+and were returning to Salt Lake City in the hope of raising capital
+enough to work some lodes which they had discovered. He had been as keen
+as any of them upon the business until this sudden incident had drawn
+his thoughts into another channel. The sight of the fair young girl,
+as frank and wholesome as the Sierra breezes, had stirred his volcanic,
+untamed heart to its very depths. When she had vanished from his sight,
+he realized that a crisis had come in his life, and that neither silver
+speculations nor any other questions could ever be of such importance to
+him as this new and all-absorbing one. The love which had sprung up in
+his heart was not the sudden, changeable fancy of a boy, but rather the
+wild, fierce passion of a man of strong will and imperious temper. He
+had been accustomed to succeed in all that he undertook. He swore in
+his heart that he would not fail in this if human effort and human
+perseverance could render him successful.
+
+He called on John Ferrier that night, and many times again, until
+his face was a familiar one at the farm-house. John, cooped up in the
+valley, and absorbed in his work, had had little chance of learning
+the news of the outside world during the last twelve years. All this
+Jefferson Hope was able to tell him, and in a style which interested
+Lucy as well as her father. He had been a pioneer in California, and
+could narrate many a strange tale of fortunes made and fortunes lost
+in those wild, halcyon days. He had been a scout too, and a trapper, a
+silver explorer, and a ranchman. Wherever stirring adventures were to be
+had, Jefferson Hope had been there in search of them. He soon became a
+favourite with the old farmer, who spoke eloquently of his virtues. On
+such occasions, Lucy was silent, but her blushing cheek and her bright,
+happy eyes, showed only too clearly that her young heart was no longer
+her own. Her honest father may not have observed these symptoms,
+but they were assuredly not thrown away upon the man who had won her
+affections.
+
+It was a summer evening when he came galloping down the road and pulled
+up at the gate. She was at the doorway, and came down to meet him. He
+threw the bridle over the fence and strode up the pathway.
+
+"I am off, Lucy," he said, taking her two hands in his, and gazing
+tenderly down into her face; "I won't ask you to come with me now, but
+will you be ready to come when I am here again?"
+
+"And when will that be?" she asked, blushing and laughing.
+
+"A couple of months at the outside. I will come and claim you then, my
+darling. There's no one who can stand between us."
+
+"And how about father?" she asked.
+
+"He has given his consent, provided we get these mines working all
+right. I have no fear on that head."
+
+"Oh, well; of course, if you and father have arranged it all, there's
+no more to be said," she whispered, with her cheek against his broad
+breast.
+
+"Thank God!" he said, hoarsely, stooping and kissing her. "It is
+settled, then. The longer I stay, the harder it will be to go. They are
+waiting for me at the cañon. Good-bye, my own darling--good-bye. In two
+months you shall see me."
+
+He tore himself from her as he spoke, and, flinging himself upon his
+horse, galloped furiously away, never even looking round, as though
+afraid that his resolution might fail him if he took one glance at
+what he was leaving. She stood at the gate, gazing after him until
+he vanished from her sight. Then she walked back into the house, the
+happiest girl in all Utah.
+
+
+
+
+CHAPTER III. JOHN FERRIER TALKS WITH THE PROPHET.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+THREE weeks had passed since Jefferson Hope and his comrades had
+departed from Salt Lake City. John Ferrier's heart was sore within him
+when he thought of the young man's return, and of the impending loss of
+his adopted child. Yet her bright and happy face reconciled him to
+the arrangement more than any argument could have done. He had always
+determined, deep down in his resolute heart, that nothing would ever
+induce him to allow his daughter to wed a Mormon. Such a marriage he
+regarded as no marriage at all, but as a shame and a disgrace. Whatever
+he might think of the Mormon doctrines, upon that one point he was
+inflexible. He had to seal his mouth on the subject, however, for to
+express an unorthodox opinion was a dangerous matter in those days in
+the Land of the Saints.
+
+Yes, a dangerous matter--so dangerous that even the most saintly dared
+only whisper their religious opinions with bated breath, lest something
+which fell from their lips might be misconstrued, and bring down a
+swift retribution upon them. The victims of persecution had now turned
+persecutors on their own account, and persecutors of the most
+terrible description. Not the Inquisition of Seville, nor the German
+Vehm-gericht, nor the Secret Societies of Italy, were ever able to put
+a more formidable machinery in motion than that which cast a cloud over
+the State of Utah.
+
+Its invisibility, and the mystery which was attached to it, made
+this organization doubly terrible. It appeared to be omniscient and
+omnipotent, and yet was neither seen nor heard. The man who held out
+against the Church vanished away, and none knew whither he had gone or
+what had befallen him. His wife and his children awaited him at home,
+but no father ever returned to tell them how he had fared at the
+hands of his secret judges. A rash word or a hasty act was followed
+by annihilation, and yet none knew what the nature might be of this
+terrible power which was suspended over them. No wonder that men
+went about in fear and trembling, and that even in the heart of the
+wilderness they dared not whisper the doubts which oppressed them.
+
+At first this vague and terrible power was exercised only upon the
+recalcitrants who, having embraced the Mormon faith, wished afterwards
+to pervert or to abandon it. Soon, however, it took a wider range. The
+supply of adult women was running short, and polygamy without a female
+population on which to draw was a barren doctrine indeed. Strange
+rumours began to be bandied about--rumours of murdered immigrants and
+rifled camps in regions where Indians had never been seen. Fresh women
+appeared in the harems of the Elders--women who pined and wept, and
+bore upon their faces the traces of an unextinguishable horror. Belated
+wanderers upon the mountains spoke of gangs of armed men, masked,
+stealthy, and noiseless, who flitted by them in the darkness. These
+tales and rumours took substance and shape, and were corroborated and
+re-corroborated, until they resolved themselves into a definite name.
+To this day, in the lonely ranches of the West, the name of the Danite
+Band, or the Avenging Angels, is a sinister and an ill-omened one.
+
+Fuller knowledge of the organization which produced such terrible
+results served to increase rather than to lessen the horror which it
+inspired in the minds of men. None knew who belonged to this ruthless
+society. The names of the participators in the deeds of blood and
+violence done under the name of religion were kept profoundly secret.
+The very friend to whom you communicated your misgivings as to the
+Prophet and his mission, might be one of those who would come forth at
+night with fire and sword to exact a terrible reparation. Hence every
+man feared his neighbour, and none spoke of the things which were
+nearest his heart.
+
+One fine morning, John Ferrier was about to set out to his wheatfields,
+when he heard the click of the latch, and, looking through the window,
+saw a stout, sandy-haired, middle-aged man coming up the pathway. His
+heart leapt to his mouth, for this was none other than the great Brigham
+Young himself. Full of trepidation--for he knew that such a visit boded
+him little good--Ferrier ran to the door to greet the Mormon chief. The
+latter, however, received his salutations coldly, and followed him with
+a stern face into the sitting-room.
+
+"Brother Ferrier," he said, taking a seat, and eyeing the farmer keenly
+from under his light-coloured eyelashes, "the true believers have been
+good friends to you. We picked you up when you were starving in the
+desert, we shared our food with you, led you safe to the Chosen Valley,
+gave you a goodly share of land, and allowed you to wax rich under our
+protection. Is not this so?"
+
+"It is so," answered John Ferrier.
+
+"In return for all this we asked but one condition: that was, that you
+should embrace the true faith, and conform in every way to its usages.
+This you promised to do, and this, if common report says truly, you have
+neglected."
+
+"And how have I neglected it?" asked Ferrier, throwing out his hands in
+expostulation. "Have I not given to the common fund? Have I not attended
+at the Temple? Have I not----?"
+
+"Where are your wives?" asked Young, looking round him. "Call them in,
+that I may greet them."
+
+"It is true that I have not married," Ferrier answered. "But women
+were few, and there were many who had better claims than I. I was not a
+lonely man: I had my daughter to attend to my wants."
+
+"It is of that daughter that I would speak to you," said the leader
+of the Mormons. "She has grown to be the flower of Utah, and has found
+favour in the eyes of many who are high in the land."
+
+John Ferrier groaned internally.
+
+"There are stories of her which I would fain disbelieve--stories that
+she is sealed to some Gentile. This must be the gossip of idle tongues.
+What is the thirteenth rule in the code of the sainted Joseph Smith?
+'Let every maiden of the true faith marry one of the elect; for if
+she wed a Gentile, she commits a grievous sin.' This being so, it is
+impossible that you, who profess the holy creed, should suffer your
+daughter to violate it."
+
+John Ferrier made no answer, but he played nervously with his
+riding-whip.
+
+"Upon this one point your whole faith shall be tested--so it has been
+decided in the Sacred Council of Four. The girl is young, and we would
+not have her wed grey hairs, neither would we deprive her of all
+choice. We Elders have many heifers, [29]_ but our children must also
+be provided. Stangerson has a son, and Drebber has a son, and either of
+them would gladly welcome your daughter to their house. Let her choose
+between them. They are young and rich, and of the true faith. What say
+you to that?"
+
+Ferrier remained silent for some little time with his brows knitted.
+
+"You will give us time," he said at last. "My daughter is very
+young--she is scarce of an age to marry."
+
+"She shall have a month to choose," said Young, rising from his seat.
+"At the end of that time she shall give her answer."
+
+He was passing through the door, when he turned, with flushed face and
+flashing eyes. "It were better for you, John Ferrier," he thundered,
+"that you and she were now lying blanched skeletons upon the Sierra
+Blanco, than that you should put your weak wills against the orders of
+the Holy Four!"
+
+With a threatening gesture of his hand, he turned from the door, and
+Ferrier heard his heavy step scrunching along the shingly path.
+
+He was still sitting with his elbows upon his knees, considering how he
+should broach the matter to his daughter when a soft hand was laid upon
+his, and looking up, he saw her standing beside him. One glance at her
+pale, frightened face showed him that she had heard what had passed.
+
+"I could not help it," she said, in answer to his look. "His voice rang
+through the house. Oh, father, father, what shall we do?"
+
+"Don't you scare yourself," he answered, drawing her to him, and passing
+his broad, rough hand caressingly over her chestnut hair. "We'll fix it
+up somehow or another. You don't find your fancy kind o' lessening for
+this chap, do you?"
+
+A sob and a squeeze of his hand was her only answer.
+
+"No; of course not. I shouldn't care to hear you say you did. He's a
+likely lad, and he's a Christian, which is more than these folk here, in
+spite o' all their praying and preaching. There's a party starting for
+Nevada to-morrow, and I'll manage to send him a message letting him know
+the hole we are in. If I know anything o' that young man, he'll be back
+here with a speed that would whip electro-telegraphs."
+
+Lucy laughed through her tears at her father's description.
+
+"When he comes, he will advise us for the best. But it is for you that
+I am frightened, dear. One hears--one hears such dreadful stories about
+those who oppose the Prophet: something terrible always happens to
+them."
+
+"But we haven't opposed him yet," her father answered. "It will be time
+to look out for squalls when we do. We have a clear month before us; at
+the end of that, I guess we had best shin out of Utah."
+
+"Leave Utah!"
+
+"That's about the size of it."
+
+"But the farm?"
+
+"We will raise as much as we can in money, and let the rest go. To tell
+the truth, Lucy, it isn't the first time I have thought of doing it. I
+don't care about knuckling under to any man, as these folk do to their
+darned prophet. I'm a free-born American, and it's all new to me. Guess
+I'm too old to learn. If he comes browsing about this farm, he might
+chance to run up against a charge of buckshot travelling in the opposite
+direction."
+
+"But they won't let us leave," his daughter objected.
+
+"Wait till Jefferson comes, and we'll soon manage that. In the meantime,
+don't you fret yourself, my dearie, and don't get your eyes swelled up,
+else he'll be walking into me when he sees you. There's nothing to be
+afeared about, and there's no danger at all."
+
+John Ferrier uttered these consoling remarks in a very confident tone,
+but she could not help observing that he paid unusual care to the
+fastening of the doors that night, and that he carefully cleaned and
+loaded the rusty old shotgun which hung upon the wall of his bedroom.
+
+
+
+
+CHAPTER IV. A FLIGHT FOR LIFE.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ON the morning which followed his interview with the Mormon Prophet,
+John Ferrier went in to Salt Lake City, and having found his
+acquaintance, who was bound for the Nevada Mountains, he entrusted him
+with his message to Jefferson Hope. In it he told the young man of the
+imminent danger which threatened them, and how necessary it was that he
+should return. Having done thus he felt easier in his mind, and returned
+home with a lighter heart.
+
+As he approached his farm, he was surprised to see a horse hitched to
+each of the posts of the gate. Still more surprised was he on entering
+to find two young men in possession of his sitting-room. One, with a
+long pale face, was leaning back in the rocking-chair, with his feet
+cocked up upon the stove. The other, a bull-necked youth with coarse
+bloated features, was standing in front of the window with his hands in
+his pocket, whistling a popular hymn. Both of them nodded to Ferrier as
+he entered, and the one in the rocking-chair commenced the conversation.
+
+"Maybe you don't know us," he said. "This here is the son of Elder
+Drebber, and I'm Joseph Stangerson, who travelled with you in the desert
+when the Lord stretched out His hand and gathered you into the true
+fold."
+
+"As He will all the nations in His own good time," said the other in a
+nasal voice; "He grindeth slowly but exceeding small."
+
+John Ferrier bowed coldly. He had guessed who his visitors were.
+
+"We have come," continued Stangerson, "at the advice of our fathers to
+solicit the hand of your daughter for whichever of us may seem good to
+you and to her. As I have but four wives and Brother Drebber here has
+seven, it appears to me that my claim is the stronger one."
+
+"Nay, nay, Brother Stangerson," cried the other; "the question is not
+how many wives we have, but how many we can keep. My father has now
+given over his mills to me, and I am the richer man."
+
+"But my prospects are better," said the other, warmly. "When the
+Lord removes my father, I shall have his tanning yard and his leather
+factory. Then I am your elder, and am higher in the Church."
+
+"It will be for the maiden to decide," rejoined young Drebber, smirking
+at his own reflection in the glass. "We will leave it all to her
+decision."
+
+During this dialogue, John Ferrier had stood fuming in the doorway,
+hardly able to keep his riding-whip from the backs of his two visitors.
+
+"Look here," he said at last, striding up to them, "when my daughter
+summons you, you can come, but until then I don't want to see your faces
+again."
+
+The two young Mormons stared at him in amazement. In their eyes this
+competition between them for the maiden's hand was the highest of
+honours both to her and her father.
+
+"There are two ways out of the room," cried Ferrier; "there is the door,
+and there is the window. Which do you care to use?"
+
+His brown face looked so savage, and his gaunt hands so threatening,
+that his visitors sprang to their feet and beat a hurried retreat. The
+old farmer followed them to the door.
+
+"Let me know when you have settled which it is to be," he said,
+sardonically.
+
+"You shall smart for this!" Stangerson cried, white with rage. "You have
+defied the Prophet and the Council of Four. You shall rue it to the end
+of your days."
+
+"The hand of the Lord shall be heavy upon you," cried young Drebber; "He
+will arise and smite you!"
+
+"Then I'll start the smiting," exclaimed Ferrier furiously, and would
+have rushed upstairs for his gun had not Lucy seized him by the arm and
+restrained him. Before he could escape from her, the clatter of horses'
+hoofs told him that they were beyond his reach.
+
+"The young canting rascals!" he exclaimed, wiping the perspiration from
+his forehead; "I would sooner see you in your grave, my girl, than the
+wife of either of them."
+
+"And so should I, father," she answered, with spirit; "but Jefferson
+will soon be here."
+
+"Yes. It will not be long before he comes. The sooner the better, for we
+do not know what their next move may be."
+
+It was, indeed, high time that someone capable of giving advice and
+help should come to the aid of the sturdy old farmer and his adopted
+daughter. In the whole history of the settlement there had never been
+such a case of rank disobedience to the authority of the Elders. If
+minor errors were punished so sternly, what would be the fate of this
+arch rebel. Ferrier knew that his wealth and position would be of no
+avail to him. Others as well known and as rich as himself had been
+spirited away before now, and their goods given over to the Church. He
+was a brave man, but he trembled at the vague, shadowy terrors which
+hung over him. Any known danger he could face with a firm lip, but
+this suspense was unnerving. He concealed his fears from his daughter,
+however, and affected to make light of the whole matter, though she,
+with the keen eye of love, saw plainly that he was ill at ease.
+
+He expected that he would receive some message or remonstrance from
+Young as to his conduct, and he was not mistaken, though it came in an
+unlooked-for manner. Upon rising next morning he found, to his surprise,
+a small square of paper pinned on to the coverlet of his bed just over
+his chest. On it was printed, in bold straggling letters:--
+
+"Twenty-nine days are given you for amendment, and then----"
+
+The dash was more fear-inspiring than any threat could have been. How
+this warning came into his room puzzled John Ferrier sorely, for his
+servants slept in an outhouse, and the doors and windows had all been
+secured. He crumpled the paper up and said nothing to his daughter, but
+the incident struck a chill into his heart. The twenty-nine days were
+evidently the balance of the month which Young had promised. What
+strength or courage could avail against an enemy armed with such
+mysterious powers? The hand which fastened that pin might have struck
+him to the heart, and he could never have known who had slain him.
+
+Still more shaken was he next morning. They had sat down to their
+breakfast when Lucy with a cry of surprise pointed upwards. In the
+centre of the ceiling was scrawled, with a burned stick apparently,
+the number 28. To his daughter it was unintelligible, and he did not
+enlighten her. That night he sat up with his gun and kept watch and
+ward. He saw and he heard nothing, and yet in the morning a great 27 had
+been painted upon the outside of his door.
+
+Thus day followed day; and as sure as morning came he found that his
+unseen enemies had kept their register, and had marked up in some
+conspicuous position how many days were still left to him out of the
+month of grace. Sometimes the fatal numbers appeared upon the walls,
+sometimes upon the floors, occasionally they were on small placards
+stuck upon the garden gate or the railings. With all his vigilance John
+Ferrier could not discover whence these daily warnings proceeded. A
+horror which was almost superstitious came upon him at the sight of
+them. He became haggard and restless, and his eyes had the troubled look
+of some hunted creature. He had but one hope in life now, and that was
+for the arrival of the young hunter from Nevada.
+
+Twenty had changed to fifteen and fifteen to ten, but there was no news
+of the absentee. One by one the numbers dwindled down, and still there
+came no sign of him. Whenever a horseman clattered down the road, or a
+driver shouted at his team, the old farmer hurried to the gate thinking
+that help had arrived at last. At last, when he saw five give way to
+four and that again to three, he lost heart, and abandoned all hope of
+escape. Single-handed, and with his limited knowledge of the mountains
+which surrounded the settlement, he knew that he was powerless. The
+more-frequented roads were strictly watched and guarded, and none could
+pass along them without an order from the Council. Turn which way he
+would, there appeared to be no avoiding the blow which hung over him.
+Yet the old man never wavered in his resolution to part with life itself
+before he consented to what he regarded as his daughter's dishonour.
+
+He was sitting alone one evening pondering deeply over his troubles, and
+searching vainly for some way out of them. That morning had shown the
+figure 2 upon the wall of his house, and the next day would be the last
+of the allotted time. What was to happen then? All manner of vague and
+terrible fancies filled his imagination. And his daughter--what was to
+become of her after he was gone? Was there no escape from the invisible
+network which was drawn all round them. He sank his head upon the table
+and sobbed at the thought of his own impotence.
+
+What was that? In the silence he heard a gentle scratching sound--low,
+but very distinct in the quiet of the night. It came from the door of
+the house. Ferrier crept into the hall and listened intently. There
+was a pause for a few moments, and then the low insidious sound was
+repeated. Someone was evidently tapping very gently upon one of the
+panels of the door. Was it some midnight assassin who had come to carry
+out the murderous orders of the secret tribunal? Or was it some agent
+who was marking up that the last day of grace had arrived. John Ferrier
+felt that instant death would be better than the suspense which shook
+his nerves and chilled his heart. Springing forward he drew the bolt and
+threw the door open.
+
+Outside all was calm and quiet. The night was fine, and the stars were
+twinkling brightly overhead. The little front garden lay before the
+farmer's eyes bounded by the fence and gate, but neither there nor on
+the road was any human being to be seen. With a sigh of relief, Ferrier
+looked to right and to left, until happening to glance straight down at
+his own feet he saw to his astonishment a man lying flat upon his face
+upon the ground, with arms and legs all asprawl.
+
+So unnerved was he at the sight that he leaned up against the wall with
+his hand to his throat to stifle his inclination to call out. His first
+thought was that the prostrate figure was that of some wounded or dying
+man, but as he watched it he saw it writhe along the ground and into the
+hall with the rapidity and noiselessness of a serpent. Once within the
+house the man sprang to his feet, closed the door, and revealed to the
+astonished farmer the fierce face and resolute expression of Jefferson
+Hope.
+
+"Good God!" gasped John Ferrier. "How you scared me! Whatever made you
+come in like that."
+
+"Give me food," the other said, hoarsely. "I have had no time for bite
+or sup for eight-and-forty hours." He flung himself upon the [21]_ cold
+meat and bread which were still lying upon the table from his host's
+supper, and devoured it voraciously. "Does Lucy bear up well?" he asked,
+when he had satisfied his hunger.
+
+"Yes. She does not know the danger," her father answered.
+
+"That is well. The house is watched on every side. That is why I crawled
+my way up to it. They may be darned sharp, but they're not quite sharp
+enough to catch a Washoe hunter."
+
+John Ferrier felt a different man now that he realized that he had
+a devoted ally. He seized the young man's leathery hand and wrung it
+cordially. "You're a man to be proud of," he said. "There are not many
+who would come to share our danger and our troubles."
+
+"You've hit it there, pard," the young hunter answered. "I have a
+respect for you, but if you were alone in this business I'd think twice
+before I put my head into such a hornet's nest. It's Lucy that brings me
+here, and before harm comes on her I guess there will be one less o' the
+Hope family in Utah."
+
+"What are we to do?"
+
+"To-morrow is your last day, and unless you act to-night you are lost.
+I have a mule and two horses waiting in the Eagle Ravine. How much money
+have you?"
+
+"Two thousand dollars in gold, and five in notes."
+
+"That will do. I have as much more to add to it. We must push for Carson
+City through the mountains. You had best wake Lucy. It is as well that
+the servants do not sleep in the house."
+
+While Ferrier was absent, preparing his daughter for the approaching
+journey, Jefferson Hope packed all the eatables that he could find into
+a small parcel, and filled a stoneware jar with water, for he knew by
+experience that the mountain wells were few and far between. He had
+hardly completed his arrangements before the farmer returned with his
+daughter all dressed and ready for a start. The greeting between the
+lovers was warm, but brief, for minutes were precious, and there was
+much to be done.
+
+"We must make our start at once," said Jefferson Hope, speaking in a low
+but resolute voice, like one who realizes the greatness of the peril,
+but has steeled his heart to meet it. "The front and back entrances are
+watched, but with caution we may get away through the side window and
+across the fields. Once on the road we are only two miles from the
+Ravine where the horses are waiting. By daybreak we should be half-way
+through the mountains."
+
+"What if we are stopped," asked Ferrier.
+
+Hope slapped the revolver butt which protruded from the front of his
+tunic. "If they are too many for us we shall take two or three of them
+with us," he said with a sinister smile.
+
+The lights inside the house had all been extinguished, and from the
+darkened window Ferrier peered over the fields which had been his own,
+and which he was now about to abandon for ever. He had long nerved
+himself to the sacrifice, however, and the thought of the honour and
+happiness of his daughter outweighed any regret at his ruined fortunes.
+All looked so peaceful and happy, the rustling trees and the broad
+silent stretch of grain-land, that it was difficult to realize that
+the spirit of murder lurked through it all. Yet the white face and set
+expression of the young hunter showed that in his approach to the house
+he had seen enough to satisfy him upon that head.
+
+Ferrier carried the bag of gold and notes, Jefferson Hope had the scanty
+provisions and water, while Lucy had a small bundle containing a few
+of her more valued possessions. Opening the window very slowly and
+carefully, they waited until a dark cloud had somewhat obscured the
+night, and then one by one passed through into the little garden. With
+bated breath and crouching figures they stumbled across it, and gained
+the shelter of the hedge, which they skirted until they came to the gap
+which opened into the cornfields. They had just reached this point when
+the young man seized his two companions and dragged them down into the
+shadow, where they lay silent and trembling.
+
+It was as well that his prairie training had given Jefferson Hope the
+ears of a lynx. He and his friends had hardly crouched down before the
+melancholy hooting of a mountain owl was heard within a few yards
+of them, which was immediately answered by another hoot at a small
+distance. At the same moment a vague shadowy figure emerged from the
+gap for which they had been making, and uttered the plaintive signal cry
+again, on which a second man appeared out of the obscurity.
+
+"To-morrow at midnight," said the first who appeared to be in authority.
+"When the Whip-poor-Will calls three times."
+
+"It is well," returned the other. "Shall I tell Brother Drebber?"
+
+"Pass it on to him, and from him to the others. Nine to seven!"
+
+"Seven to five!" repeated the other, and the two figures flitted away
+in different directions. Their concluding words had evidently been some
+form of sign and countersign. The instant that their footsteps had died
+away in the distance, Jefferson Hope sprang to his feet, and helping his
+companions through the gap, led the way across the fields at the top
+of his speed, supporting and half-carrying the girl when her strength
+appeared to fail her.
+
+"Hurry on! hurry on!" he gasped from time to time. "We are through the
+line of sentinels. Everything depends on speed. Hurry on!"
+
+Once on the high road they made rapid progress. Only once did they
+meet anyone, and then they managed to slip into a field, and so avoid
+recognition. Before reaching the town the hunter branched away into a
+rugged and narrow footpath which led to the mountains. Two dark jagged
+peaks loomed above them through the darkness, and the defile which led
+between them was the Eagle Cañon in which the horses were awaiting them.
+With unerring instinct Jefferson Hope picked his way among the great
+boulders and along the bed of a dried-up watercourse, until he came to
+the retired corner, screened with rocks, where the faithful animals had
+been picketed. The girl was placed upon the mule, and old Ferrier upon
+one of the horses, with his money-bag, while Jefferson Hope led the
+other along the precipitous and dangerous path.
+
+It was a bewildering route for anyone who was not accustomed to face
+Nature in her wildest moods. On the one side a great crag towered up a
+thousand feet or more, black, stern, and menacing, with long basaltic
+columns upon its rugged surface like the ribs of some petrified monster.
+On the other hand a wild chaos of boulders and debris made all advance
+impossible. Between the two ran the irregular track, so narrow in places
+that they had to travel in Indian file, and so rough that only practised
+riders could have traversed it at all. Yet in spite of all dangers and
+difficulties, the hearts of the fugitives were light within them,
+for every step increased the distance between them and the terrible
+despotism from which they were flying.
+
+They soon had a proof, however, that they were still within the
+jurisdiction of the Saints. They had reached the very wildest and most
+desolate portion of the pass when the girl gave a startled cry, and
+pointed upwards. On a rock which overlooked the track, showing out dark
+and plain against the sky, there stood a solitary sentinel. He saw them
+as soon as they perceived him, and his military challenge of "Who goes
+there?" rang through the silent ravine.
+
+"Travellers for Nevada," said Jefferson Hope, with his hand upon the
+rifle which hung by his saddle.
+
+They could see the lonely watcher fingering his gun, and peering down at
+them as if dissatisfied at their reply.
+
+"By whose permission?" he asked.
+
+"The Holy Four," answered Ferrier. His Mormon experiences had taught him
+that that was the highest authority to which he could refer.
+
+"Nine from seven," cried the sentinel.
+
+"Seven from five," returned Jefferson Hope promptly, remembering the
+countersign which he had heard in the garden.
+
+"Pass, and the Lord go with you," said the voice from above. Beyond his
+post the path broadened out, and the horses were able to break into a
+trot. Looking back, they could see the solitary watcher leaning upon
+his gun, and knew that they had passed the outlying post of the chosen
+people, and that freedom lay before them.
+
+
+
+
+CHAPTER V. THE AVENGING ANGELS.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ALL night their course lay through intricate defiles and over irregular
+and rock-strewn paths. More than once they lost their way, but Hope's
+intimate knowledge of the mountains enabled them to regain the track
+once more. When morning broke, a scene of marvellous though savage
+beauty lay before them. In every direction the great snow-capped peaks
+hemmed them in, peeping over each other's shoulders to the far horizon.
+So steep were the rocky banks on either side of them, that the larch
+and the pine seemed to be suspended over their heads, and to need only a
+gust of wind to come hurtling down upon them. Nor was the fear entirely
+an illusion, for the barren valley was thickly strewn with trees and
+boulders which had fallen in a similar manner. Even as they passed,
+a great rock came thundering down with a hoarse rattle which woke
+the echoes in the silent gorges, and startled the weary horses into a
+gallop.
+
+As the sun rose slowly above the eastern horizon, the caps of the great
+mountains lit up one after the other, like lamps at a festival, until
+they were all ruddy and glowing. The magnificent spectacle cheered the
+hearts of the three fugitives and gave them fresh energy. At a wild
+torrent which swept out of a ravine they called a halt and watered their
+horses, while they partook of a hasty breakfast. Lucy and her father
+would fain have rested longer, but Jefferson Hope was inexorable. "They
+will be upon our track by this time," he said. "Everything depends upon
+our speed. Once safe in Carson we may rest for the remainder of our
+lives."
+
+During the whole of that day they struggled on through the defiles, and
+by evening they calculated that they were more than thirty miles from
+their enemies. At night-time they chose the base of a beetling crag,
+where the rocks offered some protection from the chill wind, and there
+huddled together for warmth, they enjoyed a few hours' sleep. Before
+daybreak, however, they were up and on their way once more. They had
+seen no signs of any pursuers, and Jefferson Hope began to think that
+they were fairly out of the reach of the terrible organization whose
+enmity they had incurred. He little knew how far that iron grasp could
+reach, or how soon it was to close upon them and crush them.
+
+About the middle of the second day of their flight their scanty store
+of provisions began to run out. This gave the hunter little uneasiness,
+however, for there was game to be had among the mountains, and he had
+frequently before had to depend upon his rifle for the needs of life.
+Choosing a sheltered nook, he piled together a few dried branches and
+made a blazing fire, at which his companions might warm themselves, for
+they were now nearly five thousand feet above the sea level, and the air
+was bitter and keen. Having tethered the horses, and bade Lucy adieu,
+he threw his gun over his shoulder, and set out in search of whatever
+chance might throw in his way. Looking back he saw the old man and the
+young girl crouching over the blazing fire, while the three animals
+stood motionless in the back-ground. Then the intervening rocks hid them
+from his view.
+
+He walked for a couple of miles through one ravine after another without
+success, though from the marks upon the bark of the trees, and other
+indications, he judged that there were numerous bears in the vicinity.
+At last, after two or three hours' fruitless search, he was thinking of
+turning back in despair, when casting his eyes upwards he saw a sight
+which sent a thrill of pleasure through his heart. On the edge of a
+jutting pinnacle, three or four hundred feet above him, there stood a
+creature somewhat resembling a sheep in appearance, but armed with a
+pair of gigantic horns. The big-horn--for so it is called--was acting,
+probably, as a guardian over a flock which were invisible to the hunter;
+but fortunately it was heading in the opposite direction, and had not
+perceived him. Lying on his face, he rested his rifle upon a rock, and
+took a long and steady aim before drawing the trigger. The animal sprang
+into the air, tottered for a moment upon the edge of the precipice, and
+then came crashing down into the valley beneath.
+
+The creature was too unwieldy to lift, so the hunter contented himself
+with cutting away one haunch and part of the flank. With this trophy
+over his shoulder, he hastened to retrace his steps, for the evening was
+already drawing in. He had hardly started, however, before he realized
+the difficulty which faced him. In his eagerness he had wandered far
+past the ravines which were known to him, and it was no easy matter
+to pick out the path which he had taken. The valley in which he found
+himself divided and sub-divided into many gorges, which were so like
+each other that it was impossible to distinguish one from the other.
+He followed one for a mile or more until he came to a mountain torrent
+which he was sure that he had never seen before. Convinced that he had
+taken the wrong turn, he tried another, but with the same result. Night
+was coming on rapidly, and it was almost dark before he at last found
+himself in a defile which was familiar to him. Even then it was no easy
+matter to keep to the right track, for the moon had not yet risen, and
+the high cliffs on either side made the obscurity more profound. Weighed
+down with his burden, and weary from his exertions, he stumbled along,
+keeping up his heart by the reflection that every step brought him
+nearer to Lucy, and that he carried with him enough to ensure them food
+for the remainder of their journey.
+
+He had now come to the mouth of the very defile in which he had left
+them. Even in the darkness he could recognize the outline of the cliffs
+which bounded it. They must, he reflected, be awaiting him anxiously,
+for he had been absent nearly five hours. In the gladness of his heart
+he put his hands to his mouth and made the glen re-echo to a loud halloo
+as a signal that he was coming. He paused and listened for an answer.
+None came save his own cry, which clattered up the dreary silent
+ravines, and was borne back to his ears in countless repetitions. Again
+he shouted, even louder than before, and again no whisper came back from
+the friends whom he had left such a short time ago. A vague, nameless
+dread came over him, and he hurried onwards frantically, dropping the
+precious food in his agitation.
+
+When he turned the corner, he came full in sight of the spot where the
+fire had been lit. There was still a glowing pile of wood ashes there,
+but it had evidently not been tended since his departure. The same
+dead silence still reigned all round. With his fears all changed to
+convictions, he hurried on. There was no living creature near the
+remains of the fire: animals, man, maiden, all were gone. It was only
+too clear that some sudden and terrible disaster had occurred during
+his absence--a disaster which had embraced them all, and yet had left no
+traces behind it.
+
+Bewildered and stunned by this blow, Jefferson Hope felt his head spin
+round, and had to lean upon his rifle to save himself from falling. He
+was essentially a man of action, however, and speedily recovered from
+his temporary impotence. Seizing a half-consumed piece of wood from the
+smouldering fire, he blew it into a flame, and proceeded with its help
+to examine the little camp. The ground was all stamped down by the feet
+of horses, showing that a large party of mounted men had overtaken
+the fugitives, and the direction of their tracks proved that they had
+afterwards turned back to Salt Lake City. Had they carried back both of
+his companions with them? Jefferson Hope had almost persuaded himself
+that they must have done so, when his eye fell upon an object which made
+every nerve of his body tingle within him. A little way on one side of
+the camp was a low-lying heap of reddish soil, which had assuredly
+not been there before. There was no mistaking it for anything but a
+newly-dug grave. As the young hunter approached it, he perceived that a
+stick had been planted on it, with a sheet of paper stuck in the cleft
+fork of it. The inscription upon the paper was brief, but to the point:
+
+ JOHN FERRIER,
+ FORMERLY OF SALT LAKE CITY, [22]_
+ Died August 4th, 1860.
+
+The sturdy old man, whom he had left so short a time before, was gone,
+then, and this was all his epitaph. Jefferson Hope looked wildly round
+to see if there was a second grave, but there was no sign of one. Lucy
+had been carried back by their terrible pursuers to fulfil her original
+destiny, by becoming one of the harem of the Elder's son. As the young
+fellow realized the certainty of her fate, and his own powerlessness to
+prevent it, he wished that he, too, was lying with the old farmer in his
+last silent resting-place.
+
+Again, however, his active spirit shook off the lethargy which springs
+from despair. If there was nothing else left to him, he could at least
+devote his life to revenge. With indomitable patience and perseverance,
+Jefferson Hope possessed also a power of sustained vindictiveness, which
+he may have learned from the Indians amongst whom he had lived. As he
+stood by the desolate fire, he felt that the only one thing which could
+assuage his grief would be thorough and complete retribution, brought
+by his own hand upon his enemies. His strong will and untiring energy
+should, he determined, be devoted to that one end. With a grim, white
+face, he retraced his steps to where he had dropped the food, and having
+stirred up the smouldering fire, he cooked enough to last him for a
+few days. This he made up into a bundle, and, tired as he was, he
+set himself to walk back through the mountains upon the track of the
+avenging angels.
+
+For five days he toiled footsore and weary through the defiles which he
+had already traversed on horseback. At night he flung himself down among
+the rocks, and snatched a few hours of sleep; but before daybreak he was
+always well on his way. On the sixth day, he reached the Eagle Cañon,
+from which they had commenced their ill-fated flight. Thence he could
+look down upon the home of the saints. Worn and exhausted, he leaned
+upon his rifle and shook his gaunt hand fiercely at the silent
+widespread city beneath him. As he looked at it, he observed that
+there were flags in some of the principal streets, and other signs of
+festivity. He was still speculating as to what this might mean when he
+heard the clatter of horse's hoofs, and saw a mounted man riding towards
+him. As he approached, he recognized him as a Mormon named Cowper, to
+whom he had rendered services at different times. He therefore accosted
+him when he got up to him, with the object of finding out what Lucy
+Ferrier's fate had been.
+
+"I am Jefferson Hope," he said. "You remember me."
+
+The Mormon looked at him with undisguised astonishment--indeed, it was
+difficult to recognize in this tattered, unkempt wanderer, with ghastly
+white face and fierce, wild eyes, the spruce young hunter of former
+days. Having, however, at last, satisfied himself as to his identity,
+the man's surprise changed to consternation.
+
+"You are mad to come here," he cried. "It is as much as my own life is
+worth to be seen talking with you. There is a warrant against you from
+the Holy Four for assisting the Ferriers away."
+
+"I don't fear them, or their warrant," Hope said, earnestly. "You must
+know something of this matter, Cowper. I conjure you by everything you
+hold dear to answer a few questions. We have always been friends. For
+God's sake, don't refuse to answer me."
+
+"What is it?" the Mormon asked uneasily. "Be quick. The very rocks have
+ears and the trees eyes."
+
+"What has become of Lucy Ferrier?"
+
+"She was married yesterday to young Drebber. Hold up, man, hold up, you
+have no life left in you."
+
+"Don't mind me," said Hope faintly. He was white to the very lips, and
+had sunk down on the stone against which he had been leaning. "Married,
+you say?"
+
+"Married yesterday--that's what those flags are for on the Endowment
+House. There was some words between young Drebber and young Stangerson
+as to which was to have her. They'd both been in the party that followed
+them, and Stangerson had shot her father, which seemed to give him the
+best claim; but when they argued it out in council, Drebber's party was
+the stronger, so the Prophet gave her over to him. No one won't have
+her very long though, for I saw death in her face yesterday. She is more
+like a ghost than a woman. Are you off, then?"
+
+"Yes, I am off," said Jefferson Hope, who had risen from his seat. His
+face might have been chiselled out of marble, so hard and set was its
+expression, while its eyes glowed with a baleful light.
+
+"Where are you going?"
+
+"Never mind," he answered; and, slinging his weapon over his shoulder,
+strode off down the gorge and so away into the heart of the mountains to
+the haunts of the wild beasts. Amongst them all there was none so fierce
+and so dangerous as himself.
+
+The prediction of the Mormon was only too well fulfilled. Whether it was
+the terrible death of her father or the effects of the hateful marriage
+into which she had been forced, poor Lucy never held up her head again,
+but pined away and died within a month. Her sottish husband, who had
+married her principally for the sake of John Ferrier's property, did not
+affect any great grief at his bereavement; but his other wives mourned
+over her, and sat up with her the night before the burial, as is the
+Mormon custom. They were grouped round the bier in the early hours of
+the morning, when, to their inexpressible fear and astonishment,
+the door was flung open, and a savage-looking, weather-beaten man in
+tattered garments strode into the room. Without a glance or a word to
+the cowering women, he walked up to the white silent figure which had
+once contained the pure soul of Lucy Ferrier. Stooping over her, he
+pressed his lips reverently to her cold forehead, and then, snatching
+up her hand, he took the wedding-ring from her finger. "She shall not be
+buried in that," he cried with a fierce snarl, and before an alarm could
+be raised sprang down the stairs and was gone. So strange and so brief
+was the episode, that the watchers might have found it hard to believe
+it themselves or persuade other people of it, had it not been for the
+undeniable fact that the circlet of gold which marked her as having been
+a bride had disappeared.
+
+For some months Jefferson Hope lingered among the mountains, leading
+a strange wild life, and nursing in his heart the fierce desire for
+vengeance which possessed him. Tales were told in the City of the weird
+figure which was seen prowling about the suburbs, and which haunted
+the lonely mountain gorges. Once a bullet whistled through Stangerson's
+window and flattened itself upon the wall within a foot of him. On
+another occasion, as Drebber passed under a cliff a great boulder
+crashed down on him, and he only escaped a terrible death by throwing
+himself upon his face. The two young Mormons were not long in
+discovering the reason of these attempts upon their lives, and led
+repeated expeditions into the mountains in the hope of capturing or
+killing their enemy, but always without success. Then they adopted the
+precaution of never going out alone or after nightfall, and of having
+their houses guarded. After a time they were able to relax these
+measures, for nothing was either heard or seen of their opponent, and
+they hoped that time had cooled his vindictiveness.
+
+Far from doing so, it had, if anything, augmented it. The hunter's mind
+was of a hard, unyielding nature, and the predominant idea of revenge
+had taken such complete possession of it that there was no room for
+any other emotion. He was, however, above all things practical. He soon
+realized that even his iron constitution could not stand the incessant
+strain which he was putting upon it. Exposure and want of wholesome food
+were wearing him out. If he died like a dog among the mountains, what
+was to become of his revenge then? And yet such a death was sure to
+overtake him if he persisted. He felt that that was to play his enemy's
+game, so he reluctantly returned to the old Nevada mines, there to
+recruit his health and to amass money enough to allow him to pursue his
+object without privation.
+
+His intention had been to be absent a year at the most, but a
+combination of unforeseen circumstances prevented his leaving the mines
+for nearly five. At the end of that time, however, his memory of
+his wrongs and his craving for revenge were quite as keen as on that
+memorable night when he had stood by John Ferrier's grave. Disguised,
+and under an assumed name, he returned to Salt Lake City, careless
+what became of his own life, as long as he obtained what he knew to
+be justice. There he found evil tidings awaiting him. There had been a
+schism among the Chosen People a few months before, some of the younger
+members of the Church having rebelled against the authority of the
+Elders, and the result had been the secession of a certain number of the
+malcontents, who had left Utah and become Gentiles. Among these had been
+Drebber and Stangerson; and no one knew whither they had gone. Rumour
+reported that Drebber had managed to convert a large part of his
+property into money, and that he had departed a wealthy man, while his
+companion, Stangerson, was comparatively poor. There was no clue at all,
+however, as to their whereabouts.
+
+Many a man, however vindictive, would have abandoned all thought of
+revenge in the face of such a difficulty, but Jefferson Hope never
+faltered for a moment. With the small competence he possessed, eked out
+by such employment as he could pick up, he travelled from town to town
+through the United States in quest of his enemies. Year passed into
+year, his black hair turned grizzled, but still he wandered on, a human
+bloodhound, with his mind wholly set upon the one object upon which he
+had devoted his life. At last his perseverance was rewarded. It was
+but a glance of a face in a window, but that one glance told him that
+Cleveland in Ohio possessed the men whom he was in pursuit of. He
+returned to his miserable lodgings with his plan of vengeance all
+arranged. It chanced, however, that Drebber, looking from his window,
+had recognized the vagrant in the street, and had read murder in
+his eyes. He hurried before a justice of the peace, accompanied by
+Stangerson, who had become his private secretary, and represented to him
+that they were in danger of their lives from the jealousy and hatred of
+an old rival. That evening Jefferson Hope was taken into custody, and
+not being able to find sureties, was detained for some weeks. When at
+last he was liberated, it was only to find that Drebber's house was
+deserted, and that he and his secretary had departed for Europe.
+
+Again the avenger had been foiled, and again his concentrated hatred
+urged him to continue the pursuit. Funds were wanting, however, and
+for some time he had to return to work, saving every dollar for his
+approaching journey. At last, having collected enough to keep life in
+him, he departed for Europe, and tracked his enemies from city to
+city, working his way in any menial capacity, but never overtaking the
+fugitives. When he reached St. Petersburg they had departed for Paris;
+and when he followed them there he learned that they had just set off
+for Copenhagen. At the Danish capital he was again a few days late, for
+they had journeyed on to London, where he at last succeeded in running
+them to earth. As to what occurred there, we cannot do better than quote
+the old hunter's own account, as duly recorded in Dr. Watson's Journal,
+to which we are already under such obligations.
+
+
+
+
+CHAPTER VI. A CONTINUATION OF THE REMINISCENCES OF JOHN WATSON, M.D.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+OUR prisoner's furious resistance did not apparently indicate any
+ferocity in his disposition towards ourselves, for on finding himself
+powerless, he smiled in an affable manner, and expressed his hopes that
+he had not hurt any of us in the scuffle. "I guess you're going to take
+me to the police-station," he remarked to Sherlock Holmes. "My cab's at
+the door. If you'll loose my legs I'll walk down to it. I'm not so light
+to lift as I used to be."
+
+Gregson and Lestrade exchanged glances as if they thought this
+proposition rather a bold one; but Holmes at once took the prisoner at
+his word, and loosened the towel which we had bound round his ancles.
+[23]_ He rose and stretched his legs, as though to assure himself that
+they were free once more. I remember that I thought to myself, as I eyed
+him, that I had seldom seen a more powerfully built man; and his dark
+sunburned face bore an expression of determination and energy which was
+as formidable as his personal strength.
+
+"If there's a vacant place for a chief of the police, I reckon you
+are the man for it," he said, gazing with undisguised admiration at my
+fellow-lodger. "The way you kept on my trail was a caution."
+
+"You had better come with me," said Holmes to the two detectives.
+
+"I can drive you," said Lestrade.
+
+"Good! and Gregson can come inside with me. You too, Doctor, you have
+taken an interest in the case and may as well stick to us."
+
+I assented gladly, and we all descended together. Our prisoner made no
+attempt at escape, but stepped calmly into the cab which had been his,
+and we followed him. Lestrade mounted the box, whipped up the horse, and
+brought us in a very short time to our destination. We were ushered into
+a small chamber where a police Inspector noted down our prisoner's name
+and the names of the men with whose murder he had been charged. The
+official was a white-faced unemotional man, who went through his
+duties in a dull mechanical way. "The prisoner will be put before the
+magistrates in the course of the week," he said; "in the mean time, Mr.
+Jefferson Hope, have you anything that you wish to say? I must warn you
+that your words will be taken down, and may be used against you."
+
+"I've got a good deal to say," our prisoner said slowly. "I want to tell
+you gentlemen all about it."
+
+"Hadn't you better reserve that for your trial?" asked the Inspector.
+
+"I may never be tried," he answered. "You needn't look startled. It
+isn't suicide I am thinking of. Are you a Doctor?" He turned his fierce
+dark eyes upon me as he asked this last question.
+
+"Yes; I am," I answered.
+
+"Then put your hand here," he said, with a smile, motioning with his
+manacled wrists towards his chest.
+
+I did so; and became at once conscious of an extraordinary throbbing and
+commotion which was going on inside. The walls of his chest seemed to
+thrill and quiver as a frail building would do inside when some powerful
+engine was at work. In the silence of the room I could hear a dull
+humming and buzzing noise which proceeded from the same source.
+
+"Why," I cried, "you have an aortic aneurism!"
+
+"That's what they call it," he said, placidly. "I went to a Doctor last
+week about it, and he told me that it is bound to burst before many days
+passed. It has been getting worse for years. I got it from over-exposure
+and under-feeding among the Salt Lake Mountains. I've done my work now,
+and I don't care how soon I go, but I should like to leave some account
+of the business behind me. I don't want to be remembered as a common
+cut-throat."
+
+The Inspector and the two detectives had a hurried discussion as to the
+advisability of allowing him to tell his story.
+
+"Do you consider, Doctor, that there is immediate danger?" the former
+asked, [24]_
+
+"Most certainly there is," I answered.
+
+"In that case it is clearly our duty, in the interests of justice, to
+take his statement," said the Inspector. "You are at liberty, sir, to
+give your account, which I again warn you will be taken down."
+
+"I'll sit down, with your leave," the prisoner said, suiting the action
+to the word. "This aneurism of mine makes me easily tired, and the
+tussle we had half an hour ago has not mended matters. I'm on the brink
+of the grave, and I am not likely to lie to you. Every word I say is the
+absolute truth, and how you use it is a matter of no consequence to me."
+
+With these words, Jefferson Hope leaned back in his chair and began
+the following remarkable statement. He spoke in a calm and methodical
+manner, as though the events which he narrated were commonplace enough.
+I can vouch for the accuracy of the subjoined account, for I have had
+access to Lestrade's note-book, in which the prisoner's words were taken
+down exactly as they were uttered.
+
+"It don't much matter to you why I hated these men," he said; "it's
+enough that they were guilty of the death of two human beings--a father
+and a daughter--and that they had, therefore, forfeited their own
+lives. After the lapse of time that has passed since their crime, it was
+impossible for me to secure a conviction against them in any court. I
+knew of their guilt though, and I determined that I should be judge,
+jury, and executioner all rolled into one. You'd have done the same, if
+you have any manhood in you, if you had been in my place.
+
+"That girl that I spoke of was to have married me twenty years ago. She
+was forced into marrying that same Drebber, and broke her heart over
+it. I took the marriage ring from her dead finger, and I vowed that his
+dying eyes should rest upon that very ring, and that his last thoughts
+should be of the crime for which he was punished. I have carried
+it about with me, and have followed him and his accomplice over two
+continents until I caught them. They thought to tire me out, but they
+could not do it. If I die to-morrow, as is likely enough, I die knowing
+that my work in this world is done, and well done. They have perished,
+and by my hand. There is nothing left for me to hope for, or to desire.
+
+"They were rich and I was poor, so that it was no easy matter for me to
+follow them. When I got to London my pocket was about empty, and I found
+that I must turn my hand to something for my living. Driving and riding
+are as natural to me as walking, so I applied at a cabowner's office,
+and soon got employment. I was to bring a certain sum a week to the
+owner, and whatever was over that I might keep for myself. There was
+seldom much over, but I managed to scrape along somehow. The hardest job
+was to learn my way about, for I reckon that of all the mazes that ever
+were contrived, this city is the most confusing. I had a map beside me
+though, and when once I had spotted the principal hotels and stations, I
+got on pretty well.
+
+"It was some time before I found out where my two gentlemen were living;
+but I inquired and inquired until at last I dropped across them. They
+were at a boarding-house at Camberwell, over on the other side of the
+river. When once I found them out I knew that I had them at my mercy. I
+had grown my beard, and there was no chance of their recognizing me.
+I would dog them and follow them until I saw my opportunity. I was
+determined that they should not escape me again.
+
+"They were very near doing it for all that. Go where they would about
+London, I was always at their heels. Sometimes I followed them on my
+cab, and sometimes on foot, but the former was the best, for then they
+could not get away from me. It was only early in the morning or late
+at night that I could earn anything, so that I began to get behind hand
+with my employer. I did not mind that, however, as long as I could lay
+my hand upon the men I wanted.
+
+"They were very cunning, though. They must have thought that there was
+some chance of their being followed, for they would never go out alone,
+and never after nightfall. During two weeks I drove behind them every
+day, and never once saw them separate. Drebber himself was drunk half
+the time, but Stangerson was not to be caught napping. I watched them
+late and early, but never saw the ghost of a chance; but I was not
+discouraged, for something told me that the hour had almost come. My
+only fear was that this thing in my chest might burst a little too soon
+and leave my work undone.
+
+"At last, one evening I was driving up and down Torquay Terrace, as the
+street was called in which they boarded, when I saw a cab drive up to
+their door. Presently some luggage was brought out, and after a time
+Drebber and Stangerson followed it, and drove off. I whipped up my horse
+and kept within sight of them, feeling very ill at ease, for I feared
+that they were going to shift their quarters. At Euston Station they
+got out, and I left a boy to hold my horse, and followed them on to the
+platform. I heard them ask for the Liverpool train, and the guard answer
+that one had just gone and there would not be another for some hours.
+Stangerson seemed to be put out at that, but Drebber was rather pleased
+than otherwise. I got so close to them in the bustle that I could hear
+every word that passed between them. Drebber said that he had a little
+business of his own to do, and that if the other would wait for him he
+would soon rejoin him. His companion remonstrated with him, and reminded
+him that they had resolved to stick together. Drebber answered that the
+matter was a delicate one, and that he must go alone. I could not catch
+what Stangerson said to that, but the other burst out swearing, and
+reminded him that he was nothing more than his paid servant, and that he
+must not presume to dictate to him. On that the Secretary gave it up
+as a bad job, and simply bargained with him that if he missed the last
+train he should rejoin him at Halliday's Private Hotel; to which Drebber
+answered that he would be back on the platform before eleven, and made
+his way out of the station.
+
+"The moment for which I had waited so long had at last come. I had my
+enemies within my power. Together they could protect each other,
+but singly they were at my mercy. I did not act, however, with undue
+precipitation. My plans were already formed. There is no satisfaction in
+vengeance unless the offender has time to realize who it is that strikes
+him, and why retribution has come upon him. I had my plans arranged by
+which I should have the opportunity of making the man who had wronged me
+understand that his old sin had found him out. It chanced that some days
+before a gentleman who had been engaged in looking over some houses in
+the Brixton Road had dropped the key of one of them in my carriage. It
+was claimed that same evening, and returned; but in the interval I had
+taken a moulding of it, and had a duplicate constructed. By means of
+this I had access to at least one spot in this great city where I could
+rely upon being free from interruption. How to get Drebber to that house
+was the difficult problem which I had now to solve.
+
+"He walked down the road and went into one or two liquor shops, staying
+for nearly half-an-hour in the last of them. When he came out he
+staggered in his walk, and was evidently pretty well on. There was a
+hansom just in front of me, and he hailed it. I followed it so close
+that the nose of my horse was within a yard of his driver the whole way.
+We rattled across Waterloo Bridge and through miles of streets, until,
+to my astonishment, we found ourselves back in the Terrace in which he
+had boarded. I could not imagine what his intention was in returning
+there; but I went on and pulled up my cab a hundred yards or so from
+the house. He entered it, and his hansom drove away. Give me a glass of
+water, if you please. My mouth gets dry with the talking."
+
+I handed him the glass, and he drank it down.
+
+"That's better," he said. "Well, I waited for a quarter of an hour, or
+more, when suddenly there came a noise like people struggling inside the
+house. Next moment the door was flung open and two men appeared, one of
+whom was Drebber, and the other was a young chap whom I had never seen
+before. This fellow had Drebber by the collar, and when they came to
+the head of the steps he gave him a shove and a kick which sent him half
+across the road. 'You hound,' he cried, shaking his stick at him; 'I'll
+teach you to insult an honest girl!' He was so hot that I think he would
+have thrashed Drebber with his cudgel, only that the cur staggered away
+down the road as fast as his legs would carry him. He ran as far as the
+corner, and then, seeing my cab, he hailed me and jumped in. 'Drive me
+to Halliday's Private Hotel,' said he.
+
+"When I had him fairly inside my cab, my heart jumped so with joy that
+I feared lest at this last moment my aneurism might go wrong. I drove
+along slowly, weighing in my own mind what it was best to do. I might
+take him right out into the country, and there in some deserted lane
+have my last interview with him. I had almost decided upon this, when he
+solved the problem for me. The craze for drink had seized him again, and
+he ordered me to pull up outside a gin palace. He went in, leaving word
+that I should wait for him. There he remained until closing time, and
+when he came out he was so far gone that I knew the game was in my own
+hands.
+
+"Don't imagine that I intended to kill him in cold blood. It would only
+have been rigid justice if I had done so, but I could not bring myself
+to do it. I had long determined that he should have a show for his life
+if he chose to take advantage of it. Among the many billets which I
+have filled in America during my wandering life, I was once janitor and
+sweeper out of the laboratory at York College. One day the professor was
+lecturing on poisions, [25]_ and he showed his students some alkaloid,
+as he called it, which he had extracted from some South American arrow
+poison, and which was so powerful that the least grain meant instant
+death. I spotted the bottle in which this preparation was kept, and when
+they were all gone, I helped myself to a little of it. I was a fairly
+good dispenser, so I worked this alkaloid into small, soluble pills, and
+each pill I put in a box with a similar pill made without the poison.
+I determined at the time that when I had my chance, my gentlemen should
+each have a draw out of one of these boxes, while I ate the pill that
+remained. It would be quite as deadly, and a good deal less noisy than
+firing across a handkerchief. From that day I had always my pill boxes
+about with me, and the time had now come when I was to use them.
+
+"It was nearer one than twelve, and a wild, bleak night, blowing hard
+and raining in torrents. Dismal as it was outside, I was glad within--so
+glad that I could have shouted out from pure exultation. If any of you
+gentlemen have ever pined for a thing, and longed for it during twenty
+long years, and then suddenly found it within your reach, you would
+understand my feelings. I lit a cigar, and puffed at it to steady my
+nerves, but my hands were trembling, and my temples throbbing with
+excitement. As I drove, I could see old John Ferrier and sweet Lucy
+looking at me out of the darkness and smiling at me, just as plain as I
+see you all in this room. All the way they were ahead of me, one on each
+side of the horse until I pulled up at the house in the Brixton Road.
+
+"There was not a soul to be seen, nor a sound to be heard, except the
+dripping of the rain. When I looked in at the window, I found Drebber
+all huddled together in a drunken sleep. I shook him by the arm, 'It's
+time to get out,' I said.
+
+"'All right, cabby,' said he.
+
+"I suppose he thought we had come to the hotel that he had mentioned,
+for he got out without another word, and followed me down the garden.
+I had to walk beside him to keep him steady, for he was still a little
+top-heavy. When we came to the door, I opened it, and led him into the
+front room. I give you my word that all the way, the father and the
+daughter were walking in front of us.
+
+"'It's infernally dark,' said he, stamping about.
+
+"'We'll soon have a light,' I said, striking a match and putting it to
+a wax candle which I had brought with me. 'Now, Enoch Drebber,' I
+continued, turning to him, and holding the light to my own face, 'who am
+I?'
+
+"He gazed at me with bleared, drunken eyes for a moment, and then I
+saw a horror spring up in them, and convulse his whole features, which
+showed me that he knew me. He staggered back with a livid face, and I
+saw the perspiration break out upon his brow, while his teeth chattered
+in his head. At the sight, I leaned my back against the door and laughed
+loud and long. I had always known that vengeance would be sweet, but I
+had never hoped for the contentment of soul which now possessed me.
+
+"'You dog!' I said; 'I have hunted you from Salt Lake City to St.
+Petersburg, and you have always escaped me. Now, at last your wanderings
+have come to an end, for either you or I shall never see to-morrow's sun
+rise.' He shrunk still further away as I spoke, and I could see on his
+face that he thought I was mad. So I was for the time. The pulses in my
+temples beat like sledge-hammers, and I believe I would have had a fit
+of some sort if the blood had not gushed from my nose and relieved me.
+
+"'What do you think of Lucy Ferrier now?' I cried, locking the door, and
+shaking the key in his face. 'Punishment has been slow in coming, but it
+has overtaken you at last.' I saw his coward lips tremble as I spoke. He
+would have begged for his life, but he knew well that it was useless.
+
+"'Would you murder me?' he stammered.
+
+"'There is no murder,' I answered. 'Who talks of murdering a mad dog?
+What mercy had you upon my poor darling, when you dragged her from her
+slaughtered father, and bore her away to your accursed and shameless
+harem.'
+
+"'It was not I who killed her father,' he cried.
+
+"'But it was you who broke her innocent heart,' I shrieked, thrusting
+the box before him. 'Let the high God judge between us. Choose and
+eat. There is death in one and life in the other. I shall take what you
+leave. Let us see if there is justice upon the earth, or if we are ruled
+by chance.'
+
+"He cowered away with wild cries and prayers for mercy, but I drew my
+knife and held it to his throat until he had obeyed me. Then I swallowed
+the other, and we stood facing one another in silence for a minute or
+more, waiting to see which was to live and which was to die. Shall I
+ever forget the look which came over his face when the first warning
+pangs told him that the poison was in his system? I laughed as I saw
+it, and held Lucy's marriage ring in front of his eyes. It was but for
+a moment, for the action of the alkaloid is rapid. A spasm of pain
+contorted his features; he threw his hands out in front of him,
+staggered, and then, with a hoarse cry, fell heavily upon the floor. I
+turned him over with my foot, and placed my hand upon his heart. There
+was no movement. He was dead!
+
+"The blood had been streaming from my nose, but I had taken no notice of
+it. I don't know what it was that put it into my head to write upon the
+wall with it. Perhaps it was some mischievous idea of setting the police
+upon a wrong track, for I felt light-hearted and cheerful. I remembered
+a German being found in New York with RACHE written up above him, and it
+was argued at the time in the newspapers that the secret societies must
+have done it. I guessed that what puzzled the New Yorkers would puzzle
+the Londoners, so I dipped my finger in my own blood and printed it on
+a convenient place on the wall. Then I walked down to my cab and found
+that there was nobody about, and that the night was still very wild. I
+had driven some distance when I put my hand into the pocket in which
+I usually kept Lucy's ring, and found that it was not there. I was
+thunderstruck at this, for it was the only memento that I had of her.
+Thinking that I might have dropped it when I stooped over Drebber's
+body, I drove back, and leaving my cab in a side street, I went boldly
+up to the house--for I was ready to dare anything rather than lose
+the ring. When I arrived there, I walked right into the arms of a
+police-officer who was coming out, and only managed to disarm his
+suspicions by pretending to be hopelessly drunk.
+
+"That was how Enoch Drebber came to his end. All I had to do then was
+to do as much for Stangerson, and so pay off John Ferrier's debt. I knew
+that he was staying at Halliday's Private Hotel, and I hung about all
+day, but he never came out. [26]_ fancy that he suspected something when
+Drebber failed to put in an appearance. He was cunning, was Stangerson,
+and always on his guard. If he thought he could keep me off by staying
+indoors he was very much mistaken. I soon found out which was the window
+of his bedroom, and early next morning I took advantage of some ladders
+which were lying in the lane behind the hotel, and so made my way into
+his room in the grey of the dawn. I woke him up and told him that the
+hour had come when he was to answer for the life he had taken so long
+before. I described Drebber's death to him, and I gave him the same
+choice of the poisoned pills. Instead of grasping at the chance of
+safety which that offered him, he sprang from his bed and flew at my
+throat. In self-defence I stabbed him to the heart. It would have been
+the same in any case, for Providence would never have allowed his guilty
+hand to pick out anything but the poison.
+
+"I have little more to say, and it's as well, for I am about done up.
+I went on cabbing it for a day or so, intending to keep at it until I
+could save enough to take me back to America. I was standing in the
+yard when a ragged youngster asked if there was a cabby there called
+Jefferson Hope, and said that his cab was wanted by a gentleman at 221B,
+Baker Street. I went round, suspecting no harm, and the next thing I
+knew, this young man here had the bracelets on my wrists, and as neatly
+snackled [27]_ as ever I saw in my life. That's the whole of my story,
+gentlemen. You may consider me to be a murderer; but I hold that I am
+just as much an officer of justice as you are."
+
+So thrilling had the man's narrative been, and his manner was so
+impressive that we had sat silent and absorbed. Even the professional
+detectives, _blasé_ as they were in every detail of crime, appeared to
+be keenly interested in the man's story. When he finished we sat for
+some minutes in a stillness which was only broken by the scratching
+of Lestrade's pencil as he gave the finishing touches to his shorthand
+account.
+
+"There is only one point on which I should like a little more
+information," Sherlock Holmes said at last. "Who was your accomplice who
+came for the ring which I advertised?"
+
+The prisoner winked at my friend jocosely. "I can tell my own secrets,"
+he said, "but I don't get other people into trouble. I saw your
+advertisement, and I thought it might be a plant, or it might be the
+ring which I wanted. My friend volunteered to go and see. I think you'll
+own he did it smartly."
+
+"Not a doubt of that," said Holmes heartily.
+
+"Now, gentlemen," the Inspector remarked gravely, "the forms of the law
+must be complied with. On Thursday the prisoner will be brought before
+the magistrates, and your attendance will be required. Until then I will
+be responsible for him." He rang the bell as he spoke, and Jefferson
+Hope was led off by a couple of warders, while my friend and I made our
+way out of the Station and took a cab back to Baker Street.
+
+
+
+
+CHAPTER VII. THE CONCLUSION.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+WE had all been warned to appear before the magistrates upon the
+Thursday; but when the Thursday came there was no occasion for our
+testimony. A higher Judge had taken the matter in hand, and Jefferson
+Hope had been summoned before a tribunal where strict justice would
+be meted out to him. On the very night after his capture the aneurism
+burst, and he was found in the morning stretched upon the floor of the
+cell, with a placid smile upon his face, as though he had been able
+in his dying moments to look back upon a useful life, and on work well
+done.
+
+"Gregson and Lestrade will be wild about his death," Holmes remarked, as
+we chatted it over next evening. "Where will their grand advertisement
+be now?"
+
+"I don't see that they had very much to do with his capture," I
+answered.
+
+"What you do in this world is a matter of no consequence," returned my
+companion, bitterly. "The question is, what can you make people believe
+that you have done. Never mind," he continued, more brightly, after a
+pause. "I would not have missed the investigation for anything. There
+has been no better case within my recollection. Simple as it was, there
+were several most instructive points about it."
+
+"Simple!" I ejaculated.
+
+"Well, really, it can hardly be described as otherwise," said Sherlock
+Holmes, smiling at my surprise. "The proof of its intrinsic simplicity
+is, that without any help save a few very ordinary deductions I was able
+to lay my hand upon the criminal within three days."
+
+"That is true," said I.
+
+"I have already explained to you that what is out of the common is
+usually a guide rather than a hindrance. In solving a problem of this
+sort, the grand thing is to be able to reason backwards. That is a very
+useful accomplishment, and a very easy one, but people do not practise
+it much. In the every-day affairs of life it is more useful to reason
+forwards, and so the other comes to be neglected. There are fifty who
+can reason synthetically for one who can reason analytically."
+
+"I confess," said I, "that I do not quite follow you."
+
+"I hardly expected that you would. Let me see if I can make it clearer.
+Most people, if you describe a train of events to them, will tell you
+what the result would be. They can put those events together in their
+minds, and argue from them that something will come to pass. There are
+few people, however, who, if you told them a result, would be able to
+evolve from their own inner consciousness what the steps were which led
+up to that result. This power is what I mean when I talk of reasoning
+backwards, or analytically."
+
+"I understand," said I.
+
+"Now this was a case in which you were given the result and had to
+find everything else for yourself. Now let me endeavour to show you the
+different steps in my reasoning. To begin at the beginning. I approached
+the house, as you know, on foot, and with my mind entirely free from all
+impressions. I naturally began by examining the roadway, and there, as I
+have already explained to you, I saw clearly the marks of a cab, which,
+I ascertained by inquiry, must have been there during the night. I
+satisfied myself that it was a cab and not a private carriage by the
+narrow gauge of the wheels. The ordinary London growler is considerably
+less wide than a gentleman's brougham.
+
+"This was the first point gained. I then walked slowly down the garden
+path, which happened to be composed of a clay soil, peculiarly suitable
+for taking impressions. No doubt it appeared to you to be a mere
+trampled line of slush, but to my trained eyes every mark upon its
+surface had a meaning. There is no branch of detective science which
+is so important and so much neglected as the art of tracing footsteps.
+Happily, I have always laid great stress upon it, and much practice
+has made it second nature to me. I saw the heavy footmarks of the
+constables, but I saw also the track of the two men who had first passed
+through the garden. It was easy to tell that they had been before the
+others, because in places their marks had been entirely obliterated by
+the others coming upon the top of them. In this way my second link was
+formed, which told me that the nocturnal visitors were two in number,
+one remarkable for his height (as I calculated from the length of his
+stride), and the other fashionably dressed, to judge from the small and
+elegant impression left by his boots.
+
+"On entering the house this last inference was confirmed. My well-booted
+man lay before me. The tall one, then, had done the murder, if murder
+there was. There was no wound upon the dead man's person, but the
+agitated expression upon his face assured me that he had foreseen his
+fate before it came upon him. Men who die from heart disease, or any
+sudden natural cause, never by any chance exhibit agitation upon their
+features. Having sniffed the dead man's lips I detected a slightly sour
+smell, and I came to the conclusion that he had had poison forced upon
+him. Again, I argued that it had been forced upon him from the hatred
+and fear expressed upon his face. By the method of exclusion, I had
+arrived at this result, for no other hypothesis would meet the facts.
+Do not imagine that it was a very unheard of idea. The forcible
+administration of poison is by no means a new thing in criminal annals.
+The cases of Dolsky in Odessa, and of Leturier in Montpellier, will
+occur at once to any toxicologist.
+
+"And now came the great question as to the reason why. Robbery had not
+been the object of the murder, for nothing was taken. Was it politics,
+then, or was it a woman? That was the question which confronted me.
+I was inclined from the first to the latter supposition. Political
+assassins are only too glad to do their work and to fly. This murder
+had, on the contrary, been done most deliberately, and the perpetrator
+had left his tracks all over the room, showing that he had been there
+all the time. It must have been a private wrong, and not a political
+one, which called for such a methodical revenge. When the inscription
+was discovered upon the wall I was more inclined than ever to my
+opinion. The thing was too evidently a blind. When the ring was found,
+however, it settled the question. Clearly the murderer had used it to
+remind his victim of some dead or absent woman. It was at this point
+that I asked Gregson whether he had enquired in his telegram to
+Cleveland as to any particular point in Mr. Drebber's former career. He
+answered, you remember, in the negative.
+
+"I then proceeded to make a careful examination of the room, which
+confirmed me in my opinion as to the murderer's height, and furnished me
+with the additional details as to the Trichinopoly cigar and the length
+of his nails. I had already come to the conclusion, since there were no
+signs of a struggle, that the blood which covered the floor had burst
+from the murderer's nose in his excitement. I could perceive that the
+track of blood coincided with the track of his feet. It is seldom that
+any man, unless he is very full-blooded, breaks out in this way through
+emotion, so I hazarded the opinion that the criminal was probably a
+robust and ruddy-faced man. Events proved that I had judged correctly.
+
+"Having left the house, I proceeded to do what Gregson had neglected. I
+telegraphed to the head of the police at Cleveland, limiting my enquiry
+to the circumstances connected with the marriage of Enoch Drebber. The
+answer was conclusive. It told me that Drebber had already applied for
+the protection of the law against an old rival in love, named Jefferson
+Hope, and that this same Hope was at present in Europe. I knew now that
+I held the clue to the mystery in my hand, and all that remained was to
+secure the murderer.
+
+"I had already determined in my own mind that the man who had walked
+into the house with Drebber, was none other than the man who had driven
+the cab. The marks in the road showed me that the horse had wandered
+on in a way which would have been impossible had there been anyone in
+charge of it. Where, then, could the driver be, unless he were inside
+the house? Again, it is absurd to suppose that any sane man would carry
+out a deliberate crime under the very eyes, as it were, of a third
+person, who was sure to betray him. Lastly, supposing one man wished
+to dog another through London, what better means could he adopt than
+to turn cabdriver. All these considerations led me to the irresistible
+conclusion that Jefferson Hope was to be found among the jarveys of the
+Metropolis.
+
+"If he had been one there was no reason to believe that he had ceased to
+be. On the contrary, from his point of view, any sudden change would be
+likely to draw attention to himself. He would, probably, for a time at
+least, continue to perform his duties. There was no reason to suppose
+that he was going under an assumed name. Why should he change his name
+in a country where no one knew his original one? I therefore organized
+my Street Arab detective corps, and sent them systematically to every
+cab proprietor in London until they ferreted out the man that I wanted.
+How well they succeeded, and how quickly I took advantage of it, are
+still fresh in your recollection. The murder of Stangerson was an
+incident which was entirely unexpected, but which could hardly in
+any case have been prevented. Through it, as you know, I came into
+possession of the pills, the existence of which I had already surmised.
+You see the whole thing is a chain of logical sequences without a break
+or flaw."
+
+"It is wonderful!" I cried. "Your merits should be publicly recognized.
+You should publish an account of the case. If you won't, I will for
+you."
+
+"You may do what you like, Doctor," he answered. "See here!" he
+continued, handing a paper over to me, "look at this!"
+
+It was the _Echo_ for the day, and the paragraph to which he pointed was
+devoted to the case in question.
+
+"The public," it said, "have lost a sensational treat through the sudden
+death of the man Hope, who was suspected of the murder of Mr. Enoch
+Drebber and of Mr. Joseph Stangerson. The details of the case will
+probably be never known now, though we are informed upon good authority
+that the crime was the result of an old standing and romantic feud, in
+which love and Mormonism bore a part. It seems that both the victims
+belonged, in their younger days, to the Latter Day Saints, and Hope, the
+deceased prisoner, hails also from Salt Lake City. If the case has had
+no other effect, it, at least, brings out in the most striking manner
+the efficiency of our detective police force, and will serve as a lesson
+to all foreigners that they will do wisely to settle their feuds at
+home, and not to carry them on to British soil. It is an open secret
+that the credit of this smart capture belongs entirely to the well-known
+Scotland Yard officials, Messrs. Lestrade and Gregson. The man was
+apprehended, it appears, in the rooms of a certain Mr. Sherlock Holmes,
+who has himself, as an amateur, shown some talent in the detective
+line, and who, with such instructors, may hope in time to attain to some
+degree of their skill. It is expected that a testimonial of some sort
+will be presented to the two officers as a fitting recognition of their
+services."
+
+"Didn't I tell you so when we started?" cried Sherlock Holmes with a
+laugh. "That's the result of all our Study in Scarlet: to get them a
+testimonial!"
+
+"Never mind," I answered, "I have all the facts in my journal, and the
+public shall know them. In the meantime you must make yourself contented
+by the consciousness of success, like the Roman miser--
+
+ "'Populus me sibilat, at mihi plaudo
+ Ipse domi simul ac nummos contemplor in arca.'"
+
+
+
+
+
+ORIGINAL TRANSCRIBER'S NOTES:
+-----------------------------
+
+.. [1] Frontispiece, with the caption: "He examined with his glass
+ the word upon the wall, going over every letter of it with the most
+ minute exactness."
+
+.. [2] "JOHN H. WATSON, M.D.": the initial letters in the name are
+ capitalized, the other letters in small caps. All chapter titles are in
+ small caps. The initial words of chapters are in small caps with first
+ letter capitalized.
+
+.. [3] "lodgings.": the period should be a comma, as in later
+ editions.
+
+.. [4] "hoemoglobin": should be haemoglobin. The o&e are
+ concatenated.
+
+.. [5] "221B": the B is in small caps
+
+.. [6] "THE LAURISTON GARDEN MYSTERY": the table-of-contents
+ lists this chapter as "...GARDENS MYSTERY"--plural, and probably more
+ correct.
+
+.. [7] "brought."": the text has an extra double-quote mark
+
+.. [8] "individual--": illustration this page, with the
+ caption: "As he spoke, his nimble fingers were flying here, there, and
+ everywhere."
+
+.. [9] "manoeuvres": the o&e are concatenated.
+
+.. [10] "Patent leathers": the hyphen is missing.
+
+.. [11] "condonment": should be condonement.
+
+.. [13] "wages.": ending quote is missing.
+
+.. [14] "the first.": ending quote is missing.
+
+.. [15] "make much of...": Other editions complete this sentence
+ with an "it." But there is a gap in the text at this point, and, given
+ the context, it may have actually been an interjection, a dash. The gap
+ is just the right size for the characters "it." and the start of a new
+ sentence, or for a "----"
+
+.. [16] "tho cushion": "tho" should be "the"
+
+.. [19] "shoving": later editions have "showing". The original is
+ clearly superior.
+
+.. [20] "stared about...": illustration, with the caption: "One of
+ them seized the little girl, and hoisted her upon his shoulder."
+
+.. [21] "upon the": illustration, with the caption: "As he watched
+ it he saw it writhe along the ground."
+
+.. [22] "FORMERLY...": F,S,L,C in caps, other letters in this line
+ in small caps.
+
+.. [23] "ancles": ankles.
+
+.. [24] "asked,": should be "asked."
+
+.. [25] "poisions": should be "poisons"
+
+.. [26] "...fancy": should be "I fancy". There is a gap in the text.
+
+.. [27] "snackled": "shackled" in later texts.
+
+.. [29] Heber C. Kemball, in one of his sermons, alludes to his hundred wives under this endearing epithet.
+
+
+END OF THIS PROJECT GUTENBERG EBOOK A STUDY IN SCARLET
+------------------------------------------------------
+
+***** This file should be named 244-8.txt or 244-8.zip *****
+This and all associated files of various formats will be found in:
+http://www.gutenberg.org/2/4/244/
+
+Produced by Roger Squires
+
+Updated editions will replace the previous one--the old editions
+will be renamed.
+
+Creating the works from public domain print editions means that no
+one owns a United States copyright in these works, so the Foundation
+(and you!) can copy and distribute it in the United States without
+permission and without paying copyright royalties. Special rules,
+set forth in the General Terms of Use part of this license, apply to
+copying and distributing Project Gutenberg-tm electronic works to
+protect the PROJECT GUTENBERG-tm concept and trademark. Project
+Gutenberg is a registered trademark, and may not be used if you
+charge for the eBooks, unless you receive specific permission. If you
+do not charge anything for copies of this eBook, complying with the
+rules is very easy. You may use this eBook for nearly any purpose
+such as creation of derivative works, reports, performances and
+research. They may be modified and printed and given away--you may do
+practically ANYTHING with public domain eBooks. Redistribution is
+subject to the trademark license, especially commercial
+redistribution.
+
+
+
+*** START: FULL LICENSE ***
+
+THE FULL PROJECT GUTENBERG LICENSE
+PLEASE READ THIS BEFORE YOU DISTRIBUTE OR USE THIS WORK
+
+To protect the Project Gutenberg-tm mission of promoting the free
+distribution of electronic works, by using or distributing this work
+(or any other work associated in any way with the phrase "Project
+Gutenberg"), you agree to comply with all the terms of the Full Project
+Gutenberg-tm License (available with this file or online at
+http://gutenberg.org/license).
+
+
+Section 1. General Terms of Use and Redistributing Project Gutenberg-tm
+electronic works
+
+1.A. By reading or using any part of this Project Gutenberg-tm
+electronic work, you indicate that you have read, understand, agree to
+and accept all the terms of this license and intellectual property
+(trademark/copyright) agreement. If you do not agree to abide by all
+the terms of this agreement, you must cease using and return or destroy
+all copies of Project Gutenberg-tm electronic works in your possession.
+If you paid a fee for obtaining a copy of or access to a Project
+Gutenberg-tm electronic work and you do not agree to be bound by the
+terms of this agreement, you may obtain a refund from the person or
+entity to whom you paid the fee as set forth in paragraph 1.E.8.
+
+1.B. "Project Gutenberg" is a registered trademark. It may only be
+used on or associated in any way with an electronic work by people who
+agree to be bound by the terms of this agreement. There are a few
+things that you can do with most Project Gutenberg-tm electronic works
+even without complying with the full terms of this agreement. See
+paragraph 1.C below. There are a lot of things you can do with Project
+Gutenberg-tm electronic works if you follow the terms of this agreement
+and help preserve free future access to Project Gutenberg-tm electronic
+works. See paragraph 1.E below.
+
+1.C. The Project Gutenberg Literary Archive Foundation ("the Foundation"
+or PGLAF), owns a compilation copyright in the collection of Project
+Gutenberg-tm electronic works. Nearly all the individual works in the
+collection are in the public domain in the United States. If an
+individual work is in the public domain in the United States and you are
+located in the United States, we do not claim a right to prevent you from
+copying, distributing, performing, displaying or creating derivative
+works based on the work as long as all references to Project Gutenberg
+are removed. Of course, we hope that you will support the Project
+Gutenberg-tm mission of promoting free access to electronic works by
+freely sharing Project Gutenberg-tm works in compliance with the terms of
+this agreement for keeping the Project Gutenberg-tm name associated with
+the work. You can easily comply with the terms of this agreement by
+keeping this work in the same format with its attached full Project
+Gutenberg-tm License when you share it without charge with others.
+
+1.D. The copyright laws of the place where you are located also govern
+what you can do with this work. Copyright laws in most countries are in
+a constant state of change. If you are outside the United States, check
+the laws of your country in addition to the terms of this agreement
+before downloading, copying, displaying, performing, distributing or
+creating derivative works based on this work or any other Project
+Gutenberg-tm work. The Foundation makes no representations concerning
+the copyright status of any work in any country outside the United
+States.
+
+1.E. Unless you have removed all references to Project Gutenberg:
+
+1.E.1. The following sentence, with active links to, or other immediate
+access to, the full Project Gutenberg-tm License must appear prominently
+whenever any copy of a Project Gutenberg-tm work (any work on which the
+phrase "Project Gutenberg" appears, or with which the phrase "Project
+Gutenberg" is associated) is accessed, displayed, performed, viewed,
+copied or distributed:
+
+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
+
+1.E.2. If an individual Project Gutenberg-tm electronic work is derived
+from the public domain (does not contain a notice indicating that it is
+posted with permission of the copyright holder), the work can be copied
+and distributed to anyone in the United States without paying any fees
+or charges. If you are redistributing or providing access to a work
+with the phrase "Project Gutenberg" associated with or appearing on the
+work, you must comply either with the requirements of paragraphs 1.E.1
+through 1.E.7 or obtain permission for the use of the work and the
+Project Gutenberg-tm trademark as set forth in paragraphs 1.E.8 or
+1.E.9.
+
+1.E.3. If an individual Project Gutenberg-tm electronic work is posted
+with the permission of the copyright holder, your use and distribution
+must comply with both paragraphs 1.E.1 through 1.E.7 and any additional
+terms imposed by the copyright holder. Additional terms will be linked
+to the Project Gutenberg-tm License for all works posted with the
+permission of the copyright holder found at the beginning of this work.
+
+1.E.4. Do not unlink or detach or remove the full Project Gutenberg-tm
+License terms from this work, or any files containing a part of this
+work or any other work associated with Project Gutenberg-tm.
+
+1.E.5. Do not copy, display, perform, distribute or redistribute this
+electronic work, or any part of this electronic work, without
+prominently displaying the sentence set forth in paragraph 1.E.1 with
+active links or immediate access to the full terms of the Project
+Gutenberg-tm License.
+
+1.E.6. You may convert to and distribute this work in any binary,
+compressed, marked up, nonproprietary or proprietary form, including any
+word processing or hypertext form. However, if you provide access to or
+distribute copies of a Project Gutenberg-tm work in a format other than
+"Plain Vanilla ASCII" or other format used in the official version
+posted on the official Project Gutenberg-tm web site (www.gutenberg.org),
+you must, at no additional cost, fee or expense to the user, provide a
+copy, a means of exporting a copy, or a means of obtaining a copy upon
+request, of the work in its original "Plain Vanilla ASCII" or other
+form. Any alternate format must include the full Project Gutenberg-tm
+License as specified in paragraph 1.E.1.
+
+1.E.7. Do not charge a fee for access to, viewing, displaying,
+performing, copying or distributing any Project Gutenberg-tm works
+unless you comply with paragraph 1.E.8 or 1.E.9.
+
+1.E.8. You may charge a reasonable fee for copies of or providing
+access to or distributing Project Gutenberg-tm electronic works provided
+that
+
+- You pay a royalty fee of 20% of the gross profits you derive from
+ the use of Project Gutenberg-tm works calculated using the method
+ you already use to calculate your applicable taxes. The fee is
+ owed to the owner of the Project Gutenberg-tm trademark, but he
+ has agreed to donate royalties under this paragraph to the
+ Project Gutenberg Literary Archive Foundation. Royalty payments
+ must be paid within 60 days following each date on which you
+ prepare (or are legally required to prepare) your periodic tax
+ returns. Royalty payments should be clearly marked as such and
+ sent to the Project Gutenberg Literary Archive Foundation at the
+ address specified in Section 4, "Information about donations to
+ the Project Gutenberg Literary Archive Foundation."
+
+- You provide a full refund of any money paid by a user who notifies
+ you in writing (or by e-mail) within 30 days of receipt that s/he
+ does not agree to the terms of the full Project Gutenberg-tm
+ License. You must require such a user to return or
+ destroy all copies of the works possessed in a physical medium
+ and discontinue all use of and all access to other copies of
+ Project Gutenberg-tm works.
+
+- You provide, in accordance with paragraph 1.F.3, a full refund of any
+ money paid for a work or a replacement copy, if a defect in the
+ electronic work is discovered and reported to you within 90 days
+ of receipt of the work.
+
+- You comply with all other terms of this agreement for free
+ distribution of Project Gutenberg-tm works.
+
+1.E.9. If you wish to charge a fee or distribute a Project Gutenberg-tm
+electronic work or group of works on different terms than are set
+forth in this agreement, you must obtain permission in writing from
+both the Project Gutenberg Literary Archive Foundation and Michael
+Hart, the owner of the Project Gutenberg-tm trademark. Contact the
+Foundation as set forth in Section 3 below.
+
+1.F.
+
+1.F.1. Project Gutenberg volunteers and employees expend considerable
+effort to identify, do copyright research on, transcribe and proofread
+public domain works in creating the Project Gutenberg-tm
+collection. Despite these efforts, Project Gutenberg-tm electronic
+works, and the medium on which they may be stored, may contain
+"Defects," such as, but not limited to, incomplete, inaccurate or
+corrupt data, transcription errors, a copyright or other intellectual
+property infringement, a defective or damaged disk or other medium, a
+computer virus, or computer codes that damage or cannot be read by
+your equipment.
+
+1.F.2. LIMITED WARRANTY, DISCLAIMER OF DAMAGES - Except for the "Right
+of Replacement or Refund" described in paragraph 1.F.3, the Project
+Gutenberg Literary Archive Foundation, the owner of the Project
+Gutenberg-tm trademark, and any other party distributing a Project
+Gutenberg-tm electronic work under this agreement, disclaim all
+liability to you for damages, costs and expenses, including legal
+fees. YOU AGREE THAT YOU HAVE NO REMEDIES FOR NEGLIGENCE, STRICT
+LIABILITY, BREACH OF WARRANTY OR BREACH OF CONTRACT EXCEPT THOSE
+PROVIDED IN PARAGRAPH F3. YOU AGREE THAT THE FOUNDATION, THE
+TRADEMARK OWNER, AND ANY DISTRIBUTOR UNDER THIS AGREEMENT WILL NOT BE
+LIABLE TO YOU FOR ACTUAL, DIRECT, INDIRECT, CONSEQUENTIAL, PUNITIVE OR
+INCIDENTAL DAMAGES EVEN IF YOU GIVE NOTICE OF THE POSSIBILITY OF SUCH
+DAMAGE.
+
+1.F.3. LIMITED RIGHT OF REPLACEMENT OR REFUND - If you discover a
+defect in this electronic work within 90 days of receiving it, you can
+receive a refund of the money (if any) you paid for it by sending a
+written explanation to the person you received the work from. If you
+received the work on a physical medium, you must return the medium with
+your written explanation. The person or entity that provided you with
+the defective work may elect to provide a replacement copy in lieu of a
+refund. If you received the work electronically, the person or entity
+providing it to you may choose to give you a second opportunity to
+receive the work electronically in lieu of a refund. If the second copy
+is also defective, you may demand a refund in writing without further
+opportunities to fix the problem.
+
+1.F.4. Except for the limited right of replacement or refund set forth
+in paragraph 1.F.3, this work is provided to you 'AS-IS' WITH NO OTHER
+WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+WARRANTIES OF MERCHANTIBILITY OR FITNESS FOR ANY PURPOSE.
+
+1.F.5. Some states do not allow disclaimers of certain implied
+warranties or the exclusion or limitation of certain types of damages.
+If any disclaimer or limitation set forth in this agreement violates the
+law of the state applicable to this agreement, the agreement shall be
+interpreted to make the maximum disclaimer or limitation permitted by
+the applicable state law. The invalidity or unenforceability of any
+provision of this agreement shall not void the remaining provisions.
+
+1.F.6. INDEMNITY - You agree to indemnify and hold the Foundation, the
+trademark owner, any agent or employee of the Foundation, anyone
+providing copies of Project Gutenberg-tm electronic works in accordance
+with this agreement, and any volunteers associated with the production,
+promotion and distribution of Project Gutenberg-tm electronic works,
+harmless from all liability, costs and expenses, including legal fees,
+that arise directly or indirectly from any of the following which you do
+or cause to occur: (a) distribution of this or any Project Gutenberg-tm
+work, (b) alteration, modification, or additions or deletions to any
+Project Gutenberg-tm work, and (c) any Defect you cause.
+
+
+Section 2. Information about the Mission of Project Gutenberg-tm
+
+Project Gutenberg-tm is synonymous with the free distribution of
+electronic works in formats readable by the widest variety of computers
+including obsolete, old, middle-aged and new computers. It exists
+because of the efforts of hundreds of volunteers and donations from
+people in all walks of life.
+
+Volunteers and financial support to provide volunteers with the
+assistance they need, is critical to reaching Project Gutenberg-tm's
+goals and ensuring that the Project Gutenberg-tm collection will
+remain freely available for generations to come. In 2001, the Project
+Gutenberg Literary Archive Foundation was created to provide a secure
+and permanent future for Project Gutenberg-tm and future generations.
+To learn more about the Project Gutenberg Literary Archive Foundation
+and how your efforts and donations can help, see Sections 3 and 4
+and the Foundation web page at http://www.pglaf.org.
+
+
+Section 3. Information about the Project Gutenberg Literary Archive
+Foundation
+
+The Project Gutenberg Literary Archive Foundation is a non profit
+501(c)(3) educational corporation organized under the laws of the
+state of Mississippi and granted tax exempt status by the Internal
+Revenue Service. The Foundation's EIN or federal tax identification
+number is 64-6221541. Its 501(c)(3) letter is posted at
+http://pglaf.org/fundraising. Contributions to the Project Gutenberg
+Literary Archive Foundation are tax deductible to the full extent
+permitted by U.S. federal laws and your state's laws.
+
+The Foundation's principal office is located at 4557 Melan Dr. S.
+Fairbanks, AK, 99712., but its volunteers and employees are scattered
+throughout numerous locations. Its business office is located at
+809 North 1500 West, Salt Lake City, UT 84116, (801) 596-1887, email
+business@pglaf.org. Email contact links and up to date contact
+information can be found at the Foundation's web site and official
+page at http://pglaf.org
+
+For additional contact information:
+ Dr. Gregory B. Newby
+ Chief Executive and Director
+ gbnewby@pglaf.org
+
+
+Section 4. Information about Donations to the Project Gutenberg
+Literary Archive Foundation
+
+Project Gutenberg-tm depends upon and cannot survive without wide
+spread public support and donations to carry out its mission of
+increasing the number of public domain and licensed works that can be
+freely distributed in machine readable form accessible by the widest
+array of equipment including outdated equipment. Many small donations
+($1 to $5,000) are particularly important to maintaining tax exempt
+status with the IRS.
+
+The Foundation is committed to complying with the laws regulating
+charities and charitable donations in all 50 states of the United
+States. Compliance requirements are not uniform and it takes a
+considerable effort, much paperwork and many fees to meet and keep up
+with these requirements. We do not solicit donations in locations
+where we have not received written confirmation of compliance. To
+SEND DONATIONS or determine the status of compliance for any
+particular state visit http://pglaf.org
+
+While we cannot and do not solicit contributions from states where we
+have not met the solicitation requirements, we know of no prohibition
+against accepting unsolicited donations from donors in such states who
+approach us with offers to donate.
+
+International donations are gratefully accepted, but we cannot make
+any statements concerning tax treatment of donations received from
+outside the United States. U.S. laws alone swamp our small staff.
+
+Please check the Project Gutenberg Web pages for current donation
+methods and addresses. Donations are accepted in a number of other
+ways including checks, online payments and credit card donations.
+To donate, please visit: http://pglaf.org/donate
+
+
+Section 5. General Information About Project Gutenberg-tm electronic
+works.
+
+Professor Michael S. Hart is the originator of the Project Gutenberg-tm
+concept of a library of electronic works that could be freely shared
+with anyone. For thirty years, he produced and distributed Project
+Gutenberg-tm eBooks with only a loose network of volunteer support.
+
+
+Project Gutenberg-tm eBooks are often created from several printed
+editions, all of which are confirmed as Public Domain in the U.S.
+unless a copyright notice is included. Thus, we do not necessarily
+keep eBooks in compliance with any particular paper edition.
+
+
+Most people start at our Web site which has the main PG search facility:
+
+ http://www.gutenberg.org
+
+This Web site includes information about Project Gutenberg-tm,
+including how to make donations to the Project Gutenberg Literary
+Archive Foundation, how to help produce our new eBooks, and how to
+subscribe to our email newsletter to hear about new eBooks.
diff --git a/nikola/data/samplesite/stories/bootstrap-demo.rst b/nikola/data/samplesite/stories/bootstrap-demo.rst
new file mode 100644
index 0000000..520e4b0
--- /dev/null
+++ b/nikola/data/samplesite/stories/bootstrap-demo.rst
@@ -0,0 +1,1047 @@
+.. title: Bootstrap Demo
+.. slug: bootstrap-demo
+.. date: 2012/03/30 23:00
+.. tags: bootstrap, demo
+.. link: http://getnikola.com
+.. description:
+
+
+.. raw:: html
+
+ <!-- Navbar
+ ================================================== -->
+ <div class="bs-docs-section clearfix">
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="navbar">Navbar</h1>
+ </div>
+
+ <div class="bs-example">
+ <div class="navbar navbar-default">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-responsive-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="#">Brand</a>
+ </div>
+ <div class="navbar-collapse collapse navbar-responsive-collapse">
+ <ul class="nav navbar-nav">
+ <li class="active"><a href="#">Active</a></li>
+ <li><a href="#">Link</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-header">Dropdown header</li>
+ <li><a href="#">Separated link</a></li>
+ <li><a href="#">One more separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ <form class="navbar-form navbar-left">
+ <input type="text" class="form-control col-lg-8" placeholder="Search">
+ </form>
+ <ul class="nav navbar-nav navbar-right">
+ <li><a href="#">Link</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div><!-- /.nav-collapse -->
+ </div><!-- /.container -->
+ </div><!-- /.navbar -->
+
+
+ <div class="navbar navbar-inverse">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-responsive-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="#">Brand</a>
+ </div>
+ <div class="navbar-collapse collapse navbar-inverse-collapse">
+ <ul class="nav navbar-nav">
+ <li class="active"><a href="#">Active</a></li>
+ <li><a href="#">Link</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li class="dropdown-header">Dropdown header</li>
+ <li><a href="#">Separated link</a></li>
+ <li><a href="#">One more separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ <form class="navbar-form navbar-left">
+ <input type="text" class="form-control col-lg-8" placeholder="Search">
+ </form>
+ <ul class="nav navbar-nav navbar-right">
+ <li><a href="#">Link</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div><!-- /.nav-collapse -->
+ </div><!-- /.container -->
+ </div><!-- /.navbar -->
+ </div><!-- /example -->
+
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Buttons
+ ================================================== -->
+ <div class="bs-docs-section">
+ <div class="page-header">
+ <div class="row">
+ <div class="col-lg-12">
+ <h1 id="buttons">Buttons</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-6">
+
+ <div class="bs-example">
+ <p>
+ <button type="button" class="btn btn-default">Default</button>
+ <button type="button" class="btn btn-primary">Primary</button>
+ <button type="button" class="btn btn-success">Success</button>
+ <button type="button" class="btn btn-info">Info</button>
+ <button type="button" class="btn btn-warning">Warning</button>
+ <button type="button" class="btn btn-danger">Danger</button>
+ <button type="button" class="btn btn-link">Link</button>
+ </p>
+ </div>
+
+ <div class="bs-example">
+ <p>
+ <button type="button" class="btn btn-default disabled">Default</button>
+ <button type="button" class="btn btn-primary disabled">Primary</button>
+ <button type="button" class="btn btn-success disabled">Success</button>
+ <button type="button" class="btn btn-info disabled">Info</button>
+ <button type="button" class="btn btn-warning disabled">Warning</button>
+ <button type="button" class="btn btn-danger disabled">Danger</button>
+ <button type="button" class="btn btn-link disabled">Link</button>
+ </p>
+ </div>
+
+
+ <div class="bs-example" style="margin-bottom: 15px;">
+ <div class="btn-toolbar" style="margin: 0;">
+ <div class="btn-group">
+ <button type="button" class="btn btn-default">Default</button>
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ <div class="btn-group">
+ <button type="button" class="btn btn-primary">Primary</button>
+ <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ <div class="btn-group">
+ <button type="button" class="btn btn-success">Success</button>
+ <button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ <div class="btn-group">
+ <button type="button" class="btn btn-info">Info</button>
+ <button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ <div class="btn-group">
+ <button type="button" class="btn btn-warning">Warning</button>
+ <button type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </div><!-- /btn-group -->
+ </div><!-- /btn-toolbar -->
+ </div>
+
+ <div class="bs-example">
+ <p>
+ <button type="button" class="btn btn-primary btn-lg">Large button</button>
+ <button type="button" class="btn btn-primary">Default button</button>
+ <button type="button" class="btn btn-primary btn-sm">Small button</button>
+ <button type="button" class="btn btn-primary btn-xs">Mini button</button>
+ </p>
+ </div>
+
+ </div>
+ <div class="col-lg-6">
+
+ <div class="bs-example">
+ <p>
+ <button type="button" class="btn btn-default btn-lg btn-block">Block level button</button>
+ </p>
+ </div>
+
+
+ <div class="bs-example" style="margin-bottom: 15px;">
+ <div class="btn-group btn-group-justified">
+ <a href="#" class="btn btn-default">Left</a>
+ <a href="#" class="btn btn-default">Right</a>
+ <a href="#" class="btn btn-default">Middle</a>
+ </div>
+ </div>
+
+ <div class="bs-example" style="margin-bottom: 15px;">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button type="button" class="btn btn-default">1</button>
+ <button type="button" class="btn btn-default">2</button>
+ <button type="button" class="btn btn-default">3</button>
+ <button type="button" class="btn btn-default">4</button>
+ </div>
+ <div class="btn-group">
+ <button type="button" class="btn btn-default">5</button>
+ <button type="button" class="btn btn-default">6</button>
+ <button type="button" class="btn btn-default">7</button>
+ </div>
+ <div class="btn-group">
+ <button type="button" class="btn btn-default">8</button>
+
+ <div class="btn-group">
+ <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+ Dropdown
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ <li><a href="#">Dropdown link</a></li>
+ <li><a href="#">Dropdown link</a></li>
+ <li><a href="#">Dropdown link</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="bs-example">
+ <div class="btn-group-vertical">
+ <button type="button" class="btn btn-default">Button</button>
+ <button type="button" class="btn btn-default">Button</button>
+ <button type="button" class="btn btn-default">Button</button>
+ <button type="button" class="btn btn-default">Button</button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+ <!-- Typography
+ ================================================== -->
+ <div class="bs-docs-section">
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="type">Typography</h1>
+ </div>
+ </div>
+ </div>
+
+ <!-- Headings -->
+
+ <div class="row">
+ <div class="col-lg-4">
+ <div class="bs-example bs-example-type">
+ <h1>Heading 1</h1>
+ <h2>Heading 2</h2>
+ <h3>Heading 3</h3>
+ <h4>Heading 4</h4>
+ <h5>Heading 5</h5>
+ <h6>Heading 6</h6>
+ </div>
+ <div class="bs-example">
+ <p class="lead">Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="bs-example">
+ <h2>Example body text</h2>
+ <p>Nullam quis risus eget <a href="#">urna mollis ornare</a> vel eu leo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam id dolor id nibh ultricies vehicula.</p>
+ <p><small>This line of text is meant to be treated as fine print.</small></p>
+ <p>The following snippet of text is <strong>rendered as bold text</strong>.</p>
+ <p>The following snippet of text is <em>rendered as italicized text</em>.</p>
+ <p>An abbreviation of the word attribute is <abbr title="attribute">attr</abbr>.</p>
+ </div>
+
+ </div>
+ <div class="col-lg-4">
+
+ <h2>Emphasis classes</h2>
+ <div class="bs-example">
+ <p class="text-muted">Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.</p>
+ <p class="text-primary">Nullam id dolor id nibh ultricies vehicula ut id elit.</p>
+ <p class="text-warning">Etiam porta sem malesuada magna mollis euismod.</p>
+ <p class="text-danger">Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p class="text-success">Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+ <p class="text-info">Maecenas sed diam eget risus varius blandit sit amet non magna.</p>
+ </div>
+
+ </div>
+ </div>
+
+ <!-- Blockquotes -->
+
+ <div class="row">
+ <div class="col-lg-12">
+ <h2 id="type-blockquotes">Blockquotes</h2>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <blockquote>
+ <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>
+ </div>
+ <div class="col-lg-6">
+ <blockquote class="pull-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>
+ </div>
+ </div>
+ </div>
+
+ <!-- Tables
+ ================================================== -->
+ <div class="bs-docs-section">
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="tables">Tables</h1>
+ </div>
+
+ <div class="bs-example">
+ <table class="table table-striped table-bordered table-hover">
+ <thead>
+ <tr>
+ <th>#</th>
+ <th>Column heading</th>
+ <th>Column heading</th>
+ <th>Column heading</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr>
+ <td>2</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr>
+ <td>3</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr class="success">
+ <td>4</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr class="danger">
+ <td>5</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr class="warning">
+ <td>6</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ <tr class="active">
+ <td>7</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ <td>Column content</td>
+ </tr>
+ </tbody>
+ </table>
+ </div><!-- /example -->
+ </div>
+ </div>
+ </div>
+
+ <!-- Forms
+ ================================================== -->
+ <div class="bs-docs-section">
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="forms">Forms</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-6">
+ <div class="well">
+ <form class="bs-example form-horizontal">
+ <fieldset>
+ <legend>Legend</legend>
+ <div class="form-group">
+ <label for="inputEmail" class="col-lg-2 control-label">Email</label>
+ <div class="col-lg-10">
+ <input type="text" class="form-control" id="inputEmail" placeholder="Email">
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="inputPassword" class="col-lg-2 control-label">Password</label>
+ <div class="col-lg-10">
+ <input type="password" class="form-control" id="inputPassword" placeholder="Password">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox"> Checkbox
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="textArea" class="col-lg-2 control-label">Textarea</label>
+ <div class="col-lg-10">
+ <textarea class="form-control" rows="3" id="textArea"></textarea>
+ <span class="help-block">A longer block of help text that breaks onto a new line and may extend beyond one line.</span>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-lg-2 control-label">Radios</label>
+ <div class="col-lg-10">
+ <div class="radio">
+ <label>
+ <input type="radio" name="optionsRadios" id="optionsRadios1" value="option1" checked="">
+ Option one is this
+ </label>
+ </div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="optionsRadios" id="optionsRadios2" value="option2">
+ Option two can be something else
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="select" class="col-lg-2 control-label">Selects</label>
+ <div class="col-lg-10">
+ <select class="form-control" id="select">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ </select>
+ <br>
+ <select multiple="" class="form-control">
+ <option>1</option>
+ <option>2</option>
+ <option>3</option>
+ <option>4</option>
+ <option>5</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group">
+ <div class="col-lg-10 col-lg-offset-2">
+ <button class="btn btn-default">Cancel</button>
+ <button type="submit" class="btn btn-primary">Submit</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ </div>
+ <div class="col-lg-4 col-lg-offset-1">
+
+ <form class="bs-example">
+ <div class="form-group">
+ <label class="control-label" for="focusedInput">Focused input</label>
+ <input class="form-control" id="focusedInput" type="text" value="This is focused...">
+ </div>
+ <div class="form-group">
+ <label class="control-label" for="disabledInput">Disabled input</label>
+ <input class="form-control" id="disabledInput" type="text" placeholder="Disabled input here..." disabled="">
+ </div>
+ <div class="form-group has-warning">
+ <label class="control-label" for="inputWarning">Input warning</label>
+ <input type="text" class="form-control" id="inputWarning">
+ </div>
+ <div class="form-group has-error">
+ <label class="control-label" for="inputError">Input error</label>
+ <input type="text" class="form-control" id="inputError">
+ </div>
+ <div class="form-group has-success">
+ <label class="control-label" for="inputSuccess">Input success</label>
+ <input type="text" class="form-control" id="inputSuccess">
+ </div>
+ <div class="form-group">
+ <label class="control-label" for="inputLarge">Large input</label>
+ <input class="form-control input-lg" type="text" id="inputLarge">
+ </div>
+ <div class="form-group">
+ <label class="control-label" for="inputDefault">Default input</label>
+ <input type="text" class="form-control" id="inputDefault">
+ </div>
+ <div class="form-group">
+ <label class="control-label" for="inputSmall">Small input</label>
+ <input class="form-control input-sm" type="text" id="inputSmall">
+ </div>
+ <div class="form-group">
+ <label class="control-label">Input addons</label>
+ <div class="input-group">
+ <span class="input-group-addon">$</span>
+ <input type="text" class="form-control">
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="button">Button</button>
+ </span>
+ </div>
+ </div>
+ </form>
+
+ </div>
+ </div>
+ </div>
+
+ <!-- Navs
+ ================================================== -->
+ <div class="bs-docs-section">
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="nav">Navs</h1>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-4">
+ <h2 id="nav-tabs">Tabs</h2>
+ <div class="bs-example">
+ <ul class="nav nav-tabs" style="margin-bottom: 15px;">
+ <li class="active"><a href="#home" data-toggle="tab">Home</a></li>
+ <li><a href="#profile" data-toggle="tab">Profile</a></li>
+ <li class="disabled"><a>Disabled</a></li>
+ <li class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+ Dropdown <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="#dropdown1" data-toggle="tab">Action</a></li>
+ <li class="divider"></li>
+ <li><a href="#dropdown2" data-toggle="tab">Another action</a></li>
+ </ul>
+ </li>
+ </ul>
+ <div id="myTabContent" class="tab-content">
+ <div class="tab-pane fade active in" id="home">
+ <p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p>
+ </div>
+ <div class="tab-pane fade" id="profile">
+ <p>Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit.</p>
+ </div>
+ <div class="tab-pane fade" id="dropdown1">
+ <p>Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred pitchfork.</p>
+ </div>
+ <div class="tab-pane fade" id="dropdown2">
+ <p>Trust fund seitan letterpress, keytar raw denim keffiyeh etsy art party before they sold out master cleanse gluten-free squid scenester freegan cosby sweater. Fanny pack portland seitan DIY, art party locavore wolf cliche high life echo park Austin. Cred vinyl keffiyeh DIY salvia PBR, banh mi before they sold out farm-to-table VHS viral locavore cosby sweater.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <h2 id="nav-pills">Pills</h2>
+ <div class="bs-example">
+ <ul class="nav nav-pills">
+ <li class="active"><a href="#">Home</a></li>
+ <li><a href="#">Profile</a></li>
+ <li class="disabled"><a href="#">Disabled</a></li>
+ <li class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+ Dropdown <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ <br>
+ <div class="bs-example">
+ <ul class="nav nav-pills nav-stacked" style="max-width: 300px;">
+ <li class="active"><a href="#">Home</a></li>
+ <li><a href="#">Profile</a></li>
+ <li class="disabled"><a href="#">Disabled</a></li>
+ <li class="dropdown">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+ Dropdown <span class="caret"></span>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Action</a></li>
+ <li><a href="#">Another action</a></li>
+ <li><a href="#">Something else here</a></li>
+ <li class="divider"></li>
+ <li><a href="#">Separated link</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <h2 id="nav-breadcrumbs">Breadcrumbs</h2>
+ <div class="bs-example">
+ <ul class="breadcrumb">
+ <li class="active">Home</li>
+ </ul>
+ <ul class="breadcrumb">
+ <li><a href="#">Home</a></li>
+ <li class="active">Library</li>
+ </ul>
+ <ul class="breadcrumb" style="margin-bottom: 5px;">
+ <li><a href="#">Home</a></li>
+ <li><a href="#">Library</a></li>
+ <li class="active">Data</li>
+ </ul>
+ </div>
+
+ </div>
+ </div>
+
+
+ <div class="row">
+ <div class="col-lg-4">
+ <h2 id="pagination">Pagination</h2>
+ <div class="bs-example">
+ <ul class="pagination">
+ <li class="disabled"><a href="#">&laquo;</a></li>
+ <li class="active"><a href="#">1</a></li>
+ <li><a href="#">2</a></li>
+ <li><a href="#">3</a></li>
+ <li><a href="#">4</a></li>
+ <li><a href="#">5</a></li>
+ <li><a href="#">&raquo;</a></li>
+ </ul>
+ <ul class="pagination pagination-lg">
+ <li class="disabled"><a href="#">&laquo;</a></li>
+ <li class="active"><a href="#">1</a></li>
+ <li><a href="#">2</a></li>
+ <li><a href="#">3</a></li>
+ <li><a href="#">&raquo;</a></li>
+ </ul>
+ <ul class="pagination pagination-sm">
+ <li class="disabled"><a href="#">&laquo;</a></li>
+ <li class="active"><a href="#">1</a></li>
+ <li><a href="#">2</a></li>
+ <li><a href="#">3</a></li>
+ <li><a href="#">4</a></li>
+ <li><a href="#">5</a></li>
+ <li><a href="#">&raquo;</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <h2 id="pager">Pager</h2>
+ <div class="bs-example">
+ <ul class="pager">
+ <li><a href="#">Previous</a></li>
+ <li><a href="#">Next</a></li>
+ </ul>
+ </div>
+ <div class="bs-example">
+ <ul class="pager">
+ <li class="previous disabled"><a href="#">&larr; Older</a></li>
+ <li class="next"><a href="#">Newer &rarr;</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-lg-4">
+
+ </div>
+ </div>
+ </div>
+
+ <!-- Indicators
+ ================================================== -->
+ <div class="bs-docs-section">
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="indicators">Indicators</h1>
+ </div>
+ </div>
+ </div>
+
+
+ <div class="row">
+ <div class="col-lg-12">
+ <h2>Alerts</h2>
+ <div class="bs-example">
+ <div class="alert alert-dismissable alert-warning">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <h4>Warning!</h4>
+ <p>Best check yo self, you're not looking too good. Nulla vitae elit libero, a pharetra augue. Praesent commodo cursus magna, <a href="#" class="alert-link">vel scelerisque nisl consectetur et</a>.</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4">
+ <div class="alert alert-dismissable alert-danger">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <strong>Oh snap!</strong> <a href="#" class="alert-link">Change a few things up</a> and try submitting again.
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="alert alert-dismissable alert-success">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <strong>Well done!</strong> You successfully read <a href="#" class="alert-link">this important alert message</a>.
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="alert alert-dismissable alert-info">
+ <button type="button" class="close" data-dismiss="alert">&times;</button>
+ <strong>Heads up!</strong> This <a href="#" class="alert-link">alert needs your attention</a>, but it's not super important.
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4">
+ <h2>Labels</h2>
+ <div class="bs-example" style="margin-bottom: 40px;">
+ <span class="label label-default">Default</span>
+ <span class="label label-primary">Primary</span>
+ <span class="label label-success">Success</span>
+ <span class="label label-warning">Warning</span>
+ <span class="label label-danger">Danger</span>
+ <span class="label label-info">Info</span>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <h2>Badges</h2>
+ <div class="bs-example">
+ <ul class="nav nav-pills">
+ <li class="active"><a href="#">Home <span class="badge">42</span></a></li>
+ <li><a href="#">Profile <span class="badge"></span></a></li>
+ <li><a href="#">Messages <span class="badge">3</span></a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Progress bars
+ ================================================== -->
+ <div class="bs-docs-section">
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="progress">Progress bars</h1>
+ </div>
+
+
+ <h3 id="progress-basic">Basic</h3>
+ <div class="bs-example">
+ <div class="progress">
+ <div class="progress-bar" style="width: 60%;"></div>
+ </div>
+ </div>
+
+ <h3 id="progress-alternatives">Contextual alternatives</h3>
+ <div class="bs-example">
+ <div class="progress" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-info" style="width: 20%"></div>
+ </div>
+ <div class="progress" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-success" style="width: 40%"></div>
+ </div>
+ <div class="progress" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-warning" style="width: 60%"></div>
+ </div>
+ <div class="progress">
+ <div class="progress-bar progress-bar-danger" style="width: 80%"></div>
+ </div>
+ </div>
+
+ <h3 id="progress-striped">Striped</h3>
+ <div class="bs-example">
+ <div class="progress progress-striped" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-info" style="width: 20%"></div>
+ </div>
+ <div class="progress progress-striped" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-success" style="width: 40%"></div>
+ </div>
+ <div class="progress progress-striped" style="margin-bottom: 9px;">
+ <div class="progress-bar progress-bar-warning" style="width: 60%"></div>
+ </div>
+ <div class="progress progress-striped">
+ <div class="progress-bar progress-bar-danger" style="width: 80%"></div>
+ </div>
+ </div>
+
+ <h3 id="progress-animated">Animated</h3>
+ <div class="bs-example">
+ <div class="progress progress-striped active">
+ <div class="progress-bar" style="width: 45%"></div>
+ </div>
+ </div>
+
+ <h3 id="progress-stacked">Stacked</h3>
+ <div class="bs-example">
+ <div class="progress">
+ <div class="progress-bar progress-bar-success" style="width: 35%"></div>
+ <div class="progress-bar progress-bar-warning" style="width: 20%"></div>
+ <div class="progress-bar progress-bar-danger" style="width: 10%"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Containers
+ ================================================== -->
+ <div class="bs-docs-section">
+
+ <div class="row">
+ <div class="col-lg-12">
+ <div class="page-header">
+ <h1 id="container">Containers</h1>
+ </div>
+ <div class="bs-example">
+ <div class="jumbotron">
+ <h1>Jumbotron</h1>
+ <p>This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or information.</p>
+ <p><a class="btn btn-primary btn-lg">Learn more</a></p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <div class="row">
+ <div class="col-lg-12">
+ <h2>List groups</h2>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4">
+ <div class="bs-example">
+ <ul class="list-group">
+ <li class="list-group-item">
+ <span class="badge">14</span>
+ Cras justo odio
+ </li>
+ <li class="list-group-item">
+ <span class="badge">2</span>
+ Dapibus ac facilisis in
+ </li>
+ <li class="list-group-item">
+ <span class="badge">1</span>
+ Morbi leo risus
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="bs-example">
+ <div class="list-group">
+ <a href="#" class="list-group-item active">
+ Cras justo odio
+ </a>
+ <a href="#" class="list-group-item">Dapibus ac facilisis in
+ </a>
+ <a href="#" class="list-group-item">Morbi leo risus
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="bs-example">
+ <div class="list-group">
+ <a href="#" class="list-group-item">
+ <h4 class="list-group-item-heading">List group item heading</h4>
+ <p class="list-group-item-text">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
+ </a>
+ <a href="#" class="list-group-item">
+ <h4 class="list-group-item-heading">List group item heading</h4>
+ <p class="list-group-item-text">Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit.</p>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <div class="row">
+ <div class="col-lg-12">
+ <h2>Panels</h2>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4">
+ <div class="panel panel-default">
+ <div class="panel-body">
+ Basic panel
+ </div>
+ </div>
+ <div class="panel panel-default">
+ <div class="panel-heading">Panel heading</div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ <div class="panel panel-default">
+ <div class="panel-body">
+ Panel content
+ </div>
+ <div class="panel-footer">Panel footer</div>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="panel panel-primary">
+ <div class="panel-heading">
+ <h3 class="panel-title">Panel primary</h3>
+ </div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ <div class="panel panel-success">
+ <div class="panel-heading">
+ <h3 class="panel-title">Panel success</h3>
+ </div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ <div class="panel panel-warning">
+ <div class="panel-heading">
+ <h3 class="panel-title">Panel warning</h3>
+ </div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="panel panel-danger">
+ <div class="panel-heading">
+ <h3 class="panel-title">Panel danger</h3>
+ </div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ <div class="panel panel-info">
+ <div class="panel-heading">
+ <h3 class="panel-title">Panel info</h3>
+ </div>
+ <div class="panel-body">
+ Panel content
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-12">
+ <h2>Wells</h2>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-4">
+ <div class="well">
+ Look, I'm in a well!
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="well well-sm">
+ Look, I'm in a small well!
+ </div>
+ </div>
+ <div class="col-lg-4">
+ <div class="well well-lg">
+ Look, I'm in a large well!
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <hr/>
diff --git a/nikola/data/samplesite/stories/charts.txt b/nikola/data/samplesite/stories/charts.txt
new file mode 100644
index 0000000..2c90fdf
--- /dev/null
+++ b/nikola/data/samplesite/stories/charts.txt
@@ -0,0 +1,59 @@
+.. link:
+.. description:
+.. tags:
+.. date: 2013/08/27 18:20:55
+.. title: Charts
+.. slug: charts
+
+If you are using reStructuredText and install pygal, Nikola has support for rather nice charts
+with little effort, and i's even semi-interactive (hover your pointer over the legend!):
+
+.. code:: rest
+
+ .. chart:: StackedLine
+ :title: 'Browser usage evolution (in %)'
+ :fill: True
+ :x_labels: ['2002','2003','2004','2005','2006','2007','2008','2009','2010','2011','2012']
+ :width: 600
+ :height: 400
+ :explicit_size: True
+ :style: BlueStyle
+
+ ('Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4, 8.9, 5.8, 6.7, 6.8, 7.5])
+ ('IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6, 54.7, 44.8, 36.2, 26.6, 20.1])
+ ('Firefox', [None, None, None, 16.6, 25, 31, 36.4, 45.5, 46.3, 42.8, 37.1])
+ ('Chrome', [None, None, None, None, None, None, 0, 3.9, 10.8, 23.8, 35.3])
+
+.. raw:: html
+
+ <div style="text-align: center;">
+
+.. chart:: StackedLine
+ :title: 'Browser usage evolution (in %)'
+ :fill: True
+ :x_labels: ['2002','2003','2004','2005','2006','2007','2008','2009','2010','2011','2012']
+ :width: 600
+ :height: 400
+ :explicit_size: True
+ :style: BlueStyle
+
+ ('Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4, 8.9, 5.8, 6.7, 6.8, 7.5])
+ ('IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6, 54.7, 44.8, 36.2, 26.6, 20.1])
+ ('Firefox', [None, None, None, 16.6, 25, 31, 36.4, 45.5, 46.3, 42.8, 37.1])
+ ('Chrome', [None, None, None, None, None, None, 0, 3.9, 10.8, 23.8, 35.3])
+
+.. raw:: html
+
+ </div>
+
+
+Here's how it works:
+
+* Next to the directive, use the `chart type you want <http://pygal.org/chart_types/>`_
+* Any option you can set in a chart? Use it like ``:title:`` in this example. Syntax on
+ the value is just like in the pygal examples.
+* For each data series do it like the line that says ``Firefox`` in this example. The first element
+ is the label, then comes the data.
+
+Easy, right? Please explore `the pygal site <http://pygal.org>`_ for more information, and just
+take this example and tweak stuff.
diff --git a/nikola/data/samplesite/stories/configsample.meta b/nikola/data/samplesite/stories/configsample.meta
deleted file mode 100644
index 530d05f..0000000
--- a/nikola/data/samplesite/stories/configsample.meta
+++ /dev/null
@@ -1,4 +0,0 @@
-Sample Nikola Config File
-sampleconfig
-2012/03/30 23:00
-
diff --git a/nikola/data/samplesite/stories/configsample.txt b/nikola/data/samplesite/stories/configsample.txt
deleted file mode 100644
index a148942..0000000
--- a/nikola/data/samplesite/stories/configsample.txt
+++ /dev/null
@@ -1,221 +0,0 @@
-.. code-block:: python
-
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
-
- ########################################
- # Configuration, please edit
- ########################################
-
- # post_pages contains (wildcard, destination, template, use_in_feed) tuples.
- #
- # The wildcard is used to generate a list of reSt source files (whatever/thing.txt)
- # That fragment must have an associated metadata file (whatever/thing.meta),
- # and opcionally translated files (example for spanish, with code "es"):
- # whatever/thing.txt.es and whatever/thing.meta.es
- #
- # From those files, a set of HTML fragment files will be generated:
- # whatever/thing.html (and maybe whatever/thing.html.es)
- #
- # These files are combinated with the template to produce rendered
- # pages, which will be placed at
- # output / TRANSLATIONS[lang] / destination / pagename.html
- #
- # where "pagename" is specified in the metadata file.
- #
- # if use_in_feed is True, then those posts will be added to the site's
- # rss feeds.
- #
-
- post_pages = (
- ("posts/*.txt", "weblog/posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "post.tmpl", False),
- )
-
- # What is the default language?
-
- DEFAULT_LANG = "en"
-
- # What languages do you have?
- # If a specific post is not translated to a language, then the version
- # in the default language will be shown instead.
- # The format is {"translationcode" : "path/to/translation" }
- # the path will be used as a prefix for the generated pages location
-
- TRANSLATIONS = {
- "en": "",
- "es": "tr/es",
- }
-
- # 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)
- TAG_PATH = "categories"
- # Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html
- INDEX_PATH = "weblog"
- # Final locations for the archives are:
- # output / TRANSLATION[lang] / ARCHIVE_PATH / archive.html
- # output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
- ARCHIVE_PATH = "weblog"
- # Final locations are:
- # output / TRANSLATION[lang] / RSS_PATH / rss.xml
- RSS_PATH = "weblog"
-
- # A list of redirection tuples, [("foo/from.html", "/bar/to.html")].
- #
- # A HTML file will be created in output/foo/from.html that redirects
- # to the "/bar/to.html" URL. notice that the "from" side MUST be a
- # relative URL.
- #
- # If you don't need any of these, just set to []
-
- REDIRECTIONS = [("index.html", "/weblog/index.html")]
-
- # Commands to execute to deploy. Can be anything, for example,
- # you may use rsync:
- # "rsync -rav output/* joe@my.site:/srv/www/site"
- # And then do a backup, or ping pingomatic.
- # To do manual deployment, set it to []
- DEPLOY_COMMANDS = [
- r'rsync -rav --delete output/* ralsina@lateral.netmanagers.com.ar:/srv/www/lateral',
- r'rdiff-backup output ~/bartleblog-backup',
- r"links -dump 'http://www.twingly.com/ping2?url=lateral.netmanagers.com.ar' > /dev/null",
- r"links -dump 'http://pingomatic.com/ping/?title=Lateral+Opinion&blogurl=http%%3A%%2F%%2Flateral.netmanagers.com.ar&rssurl=http%%3A%%2F%%2F&chk_weblogscom=on&chk_blogs=on&chk_technorati=on&chk_feedburner=on&chk_syndic8=on&chk_newsgator=on&chk_myyahoo=on&chk_pubsubcom=on&chk_blogdigger=on&chk_blogrolling=on&chk_blogstreet=on&chk_moreover=on&chk_weblogalot=on&chk_icerocket=on&chk_newsisfree=on&chk_topicexchange=on&chk_google=on&chk_tailrank=on&chk_bloglines=on&chk_aiderss=on&chk_skygrid=on&chk_bitacoras=on&chk_collecta=on' > /dev/null",
- r'rsync -rav ~/bartleblog-backup/* ralsina@netmanagers.com.ar:bartleblog-backup',
- ]
-
- ##############################################################################
- # Image Gallery Options
- ##############################################################################
-
- # Galleries are folders in galleries/
- # Final location of galleries will be output / GALLERY_PATH / gallery_name
- GALLERY_PATH = "galleries"
- THUMBNAIL_SIZE = 256
-
-
- ##############################################################################
- # HTML fragments and diverse things that are used by the templates
- ##############################################################################
-
- # Data about this site
- BLOG_TITLE = "Lateral Opinion"
- BLOG_URL = "http://lateral.netmanagers.com.ar"
- BLOG_EMAIL = "ralsina@kde.org"
- BLOG_DESCRIPTION = "I write free software. I have an opinion on almost "\
- "everything. I write quickly. A weblog was inevitable."
-
- # A HTML fragment describing the license, for the sidebar.
- # I recomment using Creative Commons' wizard: http://creativecommons.org/choose/
- LICENSE = """
- <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/ar/">
- <img alt="Creative Commons License" style="border-width:0; margin-bottom:12px;"
- src="http://i.creativecommons.org/l/by-nc-sa/2.5/ar/88x31.png"></a>"""
-
- # A small copyright notice for the page footer
- CONTENT_FOOTER = u"Contents © 2000-2012 Roberto Alsina <ralsina@kde.org>"
-
- # To enable comments via Disqus, you need to create a forum at http://disqus.com,
- # and set DISQUS_FORUM to the short name you selected.
- # If you want to disable comments, set it to False.
- DISQUS_FORUM = "lateralopinion"
-
- # 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.
-
- #RSS_LINK = """
- #<link rel="alternate" type="application/rss+xml" title="RSS" href="http://feeds.feedburner.com/LateralOpinion">
- #<link rel="alternate" type="application/rss+xml" title="RSS en Espanol" href="http://feeds.feedburner.com/LateralOpinionEsp">
- #"""
- RSS_LINK = None
-
- # A search form to search this site, for the sidebar. You can use a google
- # custom search (http://www.google.com/cse/)
- # Or a duckduckgo search: https://duckduckgo.com/search_box.html
- # This example should work for pretty much any site we generate.
- SEARCH_FORM = """
- <!-- Custom search -->
- <form method="get" id="search" action="http://duckduckgo.com/">
- <input type="hidden" name="sites" value="%s"/>
- <input type="hidden" name="k8" value="#444444"/>
- <input type="hidden" name="k9" value="#D51920"/>
- <input type="hidden" name="kt" value="h"/>
- <input type="text" name="q" maxlength="255" placeholder="Search&hellip;" class="span2" style="margin-top: 4px;"/>
- <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" />
- </form>
- <!-- End of custom search -->
- """ % BLOG_URL
-
- # Google analytics or whatever else you use. Added to the bottom of <body>
- # in the default template (base.tmpl).
- ANALYTICS = """
- <!-- Start of StatCounter Code -->
- <script type="text/javascript">
- sc_project=1436219;
- sc_invisible=1;
- sc_partition=13;
- sc_security="b91cd70a";
- </script>
- <script type="text/javascript" src="http://www.statcounter.com/counter/counter.js"></script>
- <noscript>
- <div class="statcounter">
- <a title="free hit counters" href="http://www.statcounter.com/" target="_blank"><img class="statcounter" src="http://c14.statcounter.com/1436219/0/b91cd70a/1/" alt="free hit counters" ></a>
- </div>
- </noscript>
- <!-- End of StatCounter Code -->
- <!-- Start of Google Analytics -->
- <script src="http://www.google-analytics.com/urchin.js" type="text/javascript">
- </script>
- <script type="text/javascript">
- _uacct = "UA-1639287-1";
- urchinTracker();
- </script>
- <!-- End of Google Analytics -->
- """
-
- # Put in global_context things you want available on all your templates.
- # It can be anything, data, functions, modules, etc.
- GLOBAL_CONTEXT = {
- 'analytics': ANALYTICS,
- 'blog_title': BLOG_TITLE,
- 'blog_url': BLOG_URL,
- 'translations': TRANSLATIONS,
- 'license': LICENSE,
- 'search_form': SEARCH_FORM,
- 'disqus_forum': DISQUS_FORUM,
- 'content_footer': CONTENT_FOOTER,
- 'rss_path': RSS_PATH,
- 'rss_link': RSS_LINK,
- # Locale-dependent links for the sidebar
- 'sidebar_links': {
- 'en': (
- ('/archive.html', 'Archives'),
- ('/categories/index.html', 'Tags'),
- ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'),
- ),
- 'es': (
- ('/archive.html', 'Archivos'),
- ('/categories/index.html', 'Tags'),
- ('http://nikola.ralsina.com.ar', 'Powered by Nikola!'),
- )
- }
- }
-
- # Sorry, this is magic. It just is.
- execfile("nikola/nikola.py")
- if __name__ == "__main__":
- nikola_main()
- # End of magic.
-
- # To disable tasks, just delete them. For example, if you really
- # do not want to do a sitemap:
- # del(task_sitemap)
-
- # You can also replace the provided tasks with your own by redefining them
- # below this point. For a list of current tasks, run "doit list", and for
- # help on their syntax, refer to the doit handbook at http://pydoit.org
diff --git a/nikola/data/samplesite/stories/creating-a-theme.meta b/nikola/data/samplesite/stories/creating-a-theme.meta
deleted file mode 100644
index fe9b69a..0000000
--- a/nikola/data/samplesite/stories/creating-a-theme.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-Creating a Theme
-creating-a-theme
-2012/03/13 12:00
diff --git a/nikola/data/samplesite/stories/creating-a-theme.txt b/nikola/data/samplesite/stories/creating-a-theme.rst
index 108a192..108a192 120000
--- a/nikola/data/samplesite/stories/creating-a-theme.txt
+++ b/nikola/data/samplesite/stories/creating-a-theme.rst
diff --git a/nikola/data/samplesite/stories/extending.txt b/nikola/data/samplesite/stories/extending.txt
new file mode 120000
index 0000000..f545532
--- /dev/null
+++ b/nikola/data/samplesite/stories/extending.txt
@@ -0,0 +1 @@
+../../../../docs/extending.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/stories/internals.txt b/nikola/data/samplesite/stories/internals.txt
new file mode 120000
index 0000000..b955b57
--- /dev/null
+++ b/nikola/data/samplesite/stories/internals.txt
@@ -0,0 +1 @@
+../../../../docs/internals.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/stories/listings-demo.txt b/nikola/data/samplesite/stories/listings-demo.rst
index 7875f17..7875f17 100644
--- a/nikola/data/samplesite/stories/listings-demo.txt
+++ b/nikola/data/samplesite/stories/listings-demo.rst
diff --git a/nikola/data/samplesite/stories/manual.meta b/nikola/data/samplesite/stories/manual.meta
deleted file mode 100644
index b5e43ec..0000000
--- a/nikola/data/samplesite/stories/manual.meta
+++ /dev/null
@@ -1,4 +0,0 @@
-The Nikola Handbook
-handbook
-2012/03/30 23:00
-
diff --git a/nikola/data/samplesite/stories/manual.txt b/nikola/data/samplesite/stories/manual.rst
index 9992900..9992900 120000
--- a/nikola/data/samplesite/stories/manual.txt
+++ b/nikola/data/samplesite/stories/manual.rst
diff --git a/nikola/data/samplesite/stories/quickref.meta b/nikola/data/samplesite/stories/quickref.meta
deleted file mode 100644
index f8b0659..0000000
--- a/nikola/data/samplesite/stories/quickref.meta
+++ /dev/null
@@ -1,4 +0,0 @@
-A reStructuredText Reference
-quickref
-2012/03/30 23:00
-
diff --git a/nikola/data/samplesite/stories/quickref.txt b/nikola/data/samplesite/stories/quickref.rst
index 9a06d74..52e786f 100644
--- a/nikola/data/samplesite/stories/quickref.txt
+++ b/nikola/data/samplesite/stories/quickref.rst
@@ -1,3 +1,10 @@
+.. title: A reStructuredText Reference
+.. slug: quickref
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
.. raw:: html
<div class="alert alert-info pull-right" style="margin-left: 2em;">
diff --git a/nikola/data/samplesite/stories/quickstart.meta b/nikola/data/samplesite/stories/quickstart.meta
deleted file mode 100644
index ec9e8b5..0000000
--- a/nikola/data/samplesite/stories/quickstart.meta
+++ /dev/null
@@ -1,4 +0,0 @@
-A reStructuredText Primer
-quickstart
-2012/03/30 23:00
-
diff --git a/nikola/data/samplesite/stories/quickstart.txt b/nikola/data/samplesite/stories/quickstart.rst
index 1a0f330..4282b23 100644
--- a/nikola/data/samplesite/stories/quickstart.txt
+++ b/nikola/data/samplesite/stories/quickstart.rst
@@ -1,3 +1,10 @@
+.. title: A reStructuredText Primer
+.. slug: quickstart
+.. date: 2012/03/30 23:00
+.. tags:
+.. link:
+.. description:
+
A ReStructuredText Primer
=========================
diff --git a/nikola/data/samplesite/stories/slides-demo.txt b/nikola/data/samplesite/stories/slides-demo.rst
index fb1356b..fb1356b 100644
--- a/nikola/data/samplesite/stories/slides-demo.txt
+++ b/nikola/data/samplesite/stories/slides-demo.rst
diff --git a/nikola/data/samplesite/stories/social_buttons.txt b/nikola/data/samplesite/stories/social_buttons.txt
new file mode 120000
index 0000000..b60d598
--- /dev/null
+++ b/nikola/data/samplesite/stories/social_buttons.txt
@@ -0,0 +1 @@
+../../../../docs/social_buttons.txt \ No newline at end of file
diff --git a/nikola/data/samplesite/stories/theming.meta b/nikola/data/samplesite/stories/theming.meta
deleted file mode 100644
index db0832f..0000000
--- a/nikola/data/samplesite/stories/theming.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-Theming Nikola
-theming
-2012/03/13 12:00
diff --git a/nikola/data/samplesite/stories/theming.txt b/nikola/data/samplesite/stories/theming.rst
index d2dddb6..d2dddb6 120000
--- a/nikola/data/samplesite/stories/theming.txt
+++ b/nikola/data/samplesite/stories/theming.rst
diff --git a/nikola/data/samplesite/stories/upgrading-to-v6.txt b/nikola/data/samplesite/stories/upgrading-to-v6.txt
new file mode 120000
index 0000000..b514b70
--- /dev/null
+++ b/nikola/data/samplesite/stories/upgrading-to-v6.txt
@@ -0,0 +1 @@
+../../../../docs/upgrading-to-v6.txt \ No newline at end of file
diff --git a/nikola/data/themes/base/README.md b/nikola/data/themes/base/README.md
new file mode 100644
index 0000000..f92f490
--- /dev/null
+++ b/nikola/data/themes/base/README.md
@@ -0,0 +1,4 @@
+This theme has almost no styling, it's meant as a basis from which other
+teams can be developed.
+
+Therefore, most "advanced" features, such as slides or galleries, are broken.
diff --git a/nikola/data/themes/monospace/assets/css/rst.css b/nikola/data/themes/base/assets/css/rst.css
index cf73111..855b9fb 100644
--- a/nikola/data/themes/monospace/assets/css/rst.css
+++ b/nikola/data/themes/base/assets/css/rst.css
@@ -310,3 +310,7 @@ h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
ul.auto-toc {
list-style-type: none }
+
+a.footnote-reference {
+ line-height: 0px;
+}
diff --git a/nikola/data/themes/orphan/assets/css/theme.css b/nikola/data/themes/base/assets/css/theme.css
index e69de29..e69de29 100644
--- a/nikola/data/themes/orphan/assets/css/theme.css
+++ b/nikola/data/themes/base/assets/css/theme.css
diff --git a/nikola/data/themes/default/assets/js/mathjax.js b/nikola/data/themes/base/assets/js/mathjax.js
index 2f4e773..2f4e773 100644
--- a/nikola/data/themes/default/assets/js/mathjax.js
+++ b/nikola/data/themes/base/assets/js/mathjax.js
diff --git a/nikola/data/themes/monospace/bundles b/nikola/data/themes/base/bundles
index 4760181..4760181 100644
--- a/nikola/data/themes/monospace/bundles
+++ b/nikola/data/themes/base/bundles
diff --git a/nikola/data/themes/default/engine b/nikola/data/themes/base/engine
index 2951cdd..2951cdd 100644
--- a/nikola/data/themes/default/engine
+++ b/nikola/data/themes/base/engine
diff --git a/nikola/data/themes/base/messages/messages_bg.py b/nikola/data/themes/base/messages/messages_bg.py
new file mode 100644
index 0000000..2d36959
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_bg.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Също достъпно в",
+ "Archive": "Архив",
+ "Categories": "Категории",
+ "LANGUAGE": "Български",
+ "More posts about": "Още публикации относно ",
+ "Newer posts": "Нови публикации",
+ "Next post": "Следваща публикация",
+ "Older posts": "Стари публикации",
+ "Original site": "Оригиналния сайт",
+ "Posted": "Публиковано",
+ "Posts about %s": "Публикации относно %s",
+ "Posts for year %s": "Публикации за %s година",
+ "Posts for {month} {year}": "Публикации за {month} {year}",
+ "Previous post": "Предишна публикация",
+ "Read in English": "Прочетете на български",
+ "Read more": "Прочети още",
+ "Source": "Source",
+ "Tags and Categories": "Тагове и Категории",
+ "Tags": "Тагове",
+ "old posts page %d": "стари публикации страница %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_ca.py b/nikola/data/themes/base/messages/messages_ca.py
index a709f1b..80c4ee4 100644
--- a/nikola/data/themes/default/messages/messages_ca.py
+++ b/nikola/data/themes/base/messages/messages_ca.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "També disponibles en",
"Archive": "Arxiu",
+ "Categories": "",
"LANGUAGE": "Català",
"More posts about": "Més entrades sobre",
"Newer posts": "Entrades posteriors",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "Llegeix-ho en català",
"Read more": "Llegeix-ne més",
"Source": "Codi",
+ "Tags and Categories": "",
"Tags": "Etiquetes",
"old posts page %d": "entrades antigues pàgina %d",
}
diff --git a/nikola/data/themes/default/messages/messages_de.py b/nikola/data/themes/base/messages/messages_de.py
index 57c784f..c885304 100644
--- a/nikola/data/themes/default/messages/messages_de.py
+++ b/nikola/data/themes/base/messages/messages_de.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Auch verfügbar in",
"Archive": "Archiv",
+ "Categories": "Kategorien",
"LANGUAGE": "Deutsch",
"More posts about": "Weitere Einträge über",
"Newer posts": "Neuere Einträge",
@@ -13,11 +14,12 @@ MESSAGES = {
"Posted": "Veröffentlicht",
"Posts about %s": "Einträge über %s",
"Posts for year %s": "Einträge aus dem Jahr %s",
- "Posts for {month} {year}": "Einträge für {month} {year}",
+ "Posts for {month} {year}": "Einträge aus {month} {year}",
"Previous post": "Vorheriger Eintrag",
"Read in English": "Auf Deutsch lesen",
"Read more": "Weiterlesen",
"Source": "Source",
+ "Tags and Categories": "Tags und Kategorien",
"Tags": "Tags",
"old posts page %d": "Vorherige Einträge %d",
}
diff --git a/nikola/data/themes/default/messages/messages_el.py b/nikola/data/themes/base/messages/messages_el.py
index a00f88f..a1745fa 100644
--- a/nikola/data/themes/default/messages/messages_el.py
+++ b/nikola/data/themes/base/messages/messages_el.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Διαθέσιμο και στα",
"Archive": "Αρχείο",
+ "Categories": "Κατηγορίες",
"LANGUAGE": "Ελληνικά",
"More posts about": "Περισσότερες αναρτήσεις για",
"Newer posts": "Νεότερες αναρτήσεις",
@@ -13,11 +14,12 @@ MESSAGES = {
"Posted": "Αναρτήθηκε",
"Posts about %s": "Αναρτήσεις για %s",
"Posts for year %s": "Αναρτήσεις για το έτος %s",
- "Posts for {month} {year}": "",
+ "Posts for {month} {year}": "Αναρτήσεις για τον {month} του {year}",
"Previous post": "Προηγούμενη ανάρτηση",
"Read in English": "Διαβάστε στα Ελληνικά",
"Read more": "Διαβάστε περισσότερα",
"Source": "Πηγαίος κώδικας",
+ "Tags and Categories": "",
"Tags": "Ετικέτες",
"old posts page %d": "σελίδα παλαιότερων αναρτήσεων %d",
}
diff --git a/nikola/data/themes/default/messages/messages_en.py b/nikola/data/themes/base/messages/messages_en.py
index 982b654..1e695ac 100644
--- a/nikola/data/themes/default/messages/messages_en.py
+++ b/nikola/data/themes/base/messages/messages_en.py
@@ -4,10 +4,13 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Also available in",
"Archive": "Archive",
+ "Categories": "Categories",
"LANGUAGE": "English",
"More posts about": "More posts about",
"Newer posts": "Newer posts",
"Next post": "Next post",
+ "No content": "",
+ "No title": "",
"Older posts": "Older posts",
"Original site": "Original site",
"Posted": "Posted",
@@ -18,6 +21,7 @@ MESSAGES = {
"Read in English": "Read in English",
"Read more": "Read more",
"Source": "Source",
+ "Tags and Categories": "Tags and Categories",
"Tags": "Tags",
"old posts page %d": "old posts page %d",
}
diff --git a/nikola/data/themes/base/messages/messages_eo.py b/nikola/data/themes/base/messages/messages_eo.py
new file mode 100644
index 0000000..50bd25a
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_eo.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Ankaŭ disponebla en",
+ "Archive": "Arĥivo",
+ "Categories": "Kategorioj",
+ "LANGUAGE": "Anglalingve",
+ "More posts about": "Pli artikoloj pri...",
+ "Newer posts": "Pli novaj artikoloj",
+ "Next post": "Venonta artikolo",
+ "Older posts": "Pli malnovaj artikoloj",
+ "Original site": "Originala interretejo",
+ "Posted": "Skribita",
+ "Posts about %s": "Artikoloj pri %s",
+ "Posts for year %s": "Artikoloj de la jaro %s",
+ "Posts for {month} {year}": "Artikoloj skribitaj en {month} {year}",
+ "Previous post": "Antaŭa artikolo",
+ "Read in English": "Legu ĝin en Esperanto",
+ "Read more": "Legu plu",
+ "Source": "Fonto",
+ "Tags and Categories": "Etikedoj kaj Kategorioj",
+ "Tags": "Etikedoj",
+ "old posts page %d": "paĝo de malnovaj artikoloj %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_es.py b/nikola/data/themes/base/messages/messages_es.py
index 4b73d47..79ffac6 100644
--- a/nikola/data/themes/default/messages/messages_es.py
+++ b/nikola/data/themes/base/messages/messages_es.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "También disponible en",
"Archive": "Archivo",
+ "Categories": "Categorías",
"LANGUAGE": "Español",
"More posts about": "Más posts sobre",
"Newer posts": "Posts posteriores",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "Leer en español",
"Read more": "Leer más",
"Source": "Código",
+ "Tags and Categories": "Tags y Categorías",
"Tags": "Tags",
"old posts page %d": "posts antiguos página %d",
}
diff --git a/nikola/data/themes/base/messages/messages_fa.py b/nikola/data/themes/base/messages/messages_fa.py
new file mode 100644
index 0000000..786893c
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_fa.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "همچنین قابل دسترس از",
+ "Archive": "آرشیو",
+ "Categories": "دسته‌ها",
+ "LANGUAGE": "فارسی",
+ "More posts about": "ارسال‌های بیشتر دربارهٔ",
+ "Newer posts": "ارسال‌های جدید‌تر",
+ "Next post": "ارسال بعدی",
+ "Older posts": "پست‌های قدیمی‌تر",
+ "Original site": "سایت اصلی",
+ "Posted": "ارسال شده",
+ "Posts about %s": "ارسال‌ها دربارهٔ %s",
+ "Posts for year %s": "ارسال‌ها برای سال %s",
+ "Posts for {month} {year}": "ارسال برای {month} {year}",
+ "Previous post": "ارسال پیشین",
+ "Read in English": "به فارسی بخوانید",
+ "Read more": "بیشتر بخوانید",
+ "Source": "منبع",
+ "Tags and Categories": "برچسب‌ها و دسته‌ها",
+ "Tags": "برچسب‌ها",
+ "old posts page %d": "صفحهٔ ارسال‌های قدیمی %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_fi.py b/nikola/data/themes/base/messages/messages_fi.py
new file mode 100644
index 0000000..ca64919
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_fi.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Saatavilla myös",
+ "Archive": "Arkisto",
+ "Categories": "Kategoriat",
+ "LANGUAGE": "Suomi",
+ "More posts about": "Lisää postauksia aiheesta",
+ "Newer posts": "Uudempia postauksia",
+ "Next post": "Seuraava postaus",
+ "Older posts": "Vanhempia postauksia",
+ "Original site": "Alkuperäinen sivusto",
+ "Posted": "Postattu",
+ "Posts about %s": "Postauksia aiheesta %s",
+ "Posts for year %s": "Postauksia vuodelta %s",
+ "Posts for {month} {year}": "Postauksia ajalle {month} {year}",
+ "Previous post": "Vanhempia postauksia",
+ "Read in English": "Lue suomeksi",
+ "Read more": "Lue lisää",
+ "Source": "Lähde",
+ "Tags and Categories": "Tagit ja kategoriat",
+ "Tags": "Tagit",
+ "old posts page %d": "vanhojen postauksien sivu %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_fr.py b/nikola/data/themes/base/messages/messages_fr.py
index 94e9f46..45c7474 100644
--- a/nikola/data/themes/default/messages/messages_fr.py
+++ b/nikola/data/themes/base/messages/messages_fr.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Disponible aussi en",
"Archive": "Archives",
+ "Categories": "Catégories",
"LANGUAGE": "Français",
"More posts about": "Plus de billets sur",
"Newer posts": "Billets récents",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "Lire en français",
"Read more": "Lire la suite",
"Source": "Source",
+ "Tags and Categories": "Étiquettes et catégories",
"Tags": "Étiquettes",
"old posts page %d": "anciens billets page %d",
}
diff --git a/nikola/data/themes/base/messages/messages_hr.py b/nikola/data/themes/base/messages/messages_hr.py
new file mode 100644
index 0000000..420159d
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_hr.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Također dostupno i u",
+ "Archive": "Arhiva",
+ "Categories": "",
+ "LANGUAGE": "hrvatski",
+ "More posts about": "Više postova o",
+ "Newer posts": "Noviji postovi",
+ "Next post": "Sljedeći post",
+ "Older posts": "Stariji postovi",
+ "Original site": "Izvorna stranica",
+ "Posted": "Objavljeno",
+ "Posts about %s": "Postovi o %s",
+ "Posts for year %s": "Postovi za godinu %s",
+ "Posts for {month} {year}": "Postovi za {month} {year}",
+ "Previous post": "Prethodni post",
+ "Read in English": "Čitaj na hrvatskom",
+ "Read more": "Čitaj dalje",
+ "Source": "Izvor",
+ "Tags and Categories": "",
+ "Tags": "Tagovi",
+ "old posts page %d": "stari postovi stranice %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_it.py b/nikola/data/themes/base/messages/messages_it.py
index c41a20c..b460f22 100644
--- a/nikola/data/themes/default/messages/messages_it.py
+++ b/nikola/data/themes/base/messages/messages_it.py
@@ -4,20 +4,22 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Anche disponibile in",
"Archive": "Archivio",
+ "Categories": "Categorie",
"LANGUAGE": "Italiano",
- "More posts about": "Altri articoli s",
+ "More posts about": "Altri articoli collegati",
"Newer posts": "Articoli recenti",
"Next post": "Articolo successivo",
- "Older posts": "Articoli vecchi",
+ "Older posts": "Articoli precedenti",
"Original site": "Sito originale",
"Posted": "Pubblicato",
"Posts about %s": "Articoli su %s",
"Posts for year %s": "Articoli per l'anno %s",
- "Posts for {month} {year}": "",
+ "Posts for {month} {year}": "Articoli per {month} {year}",
"Previous post": "Articolo precedente",
"Read in English": "Leggi in italiano",
"Read more": "Espandi",
- "Source": "Source",
+ "Source": "Sorgente",
+ "Tags and Categories": "Tags e Categorie",
"Tags": "Tags",
"old posts page %d": "pagina dei vecchi articoli %d",
}
diff --git a/nikola/data/themes/default/messages/messages_ja.py b/nikola/data/themes/base/messages/messages_ja.py
index 8dd1521..4a238cc 100644
--- a/nikola/data/themes/default/messages/messages_ja.py
+++ b/nikola/data/themes/base/messages/messages_ja.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "他の言語で読む",
"Archive": "過去の記事",
+ "Categories": "",
"LANGUAGE": "日本語",
"More posts about": "タグ",
"Newer posts": "新しい記事",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "日本語で読む",
"Read more": "続きを読む",
"Source": "ソース",
+ "Tags and Categories": "",
"Tags": "タグ",
"old posts page %d": "前の記事 %dページ目",
}
diff --git a/nikola/data/themes/base/messages/messages_nl.py b/nikola/data/themes/base/messages/messages_nl.py
new file mode 100644
index 0000000..a2da822
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_nl.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Ook beschikbaar in",
+ "Archive": "Archief",
+ "Categories": "Categorieën",
+ "LANGUAGE": "Nederlands",
+ "More posts about": "Meer berichten over",
+ "Newer posts": "Nieuwere berichten",
+ "Next post": "Volgend bericht",
+ "Older posts": "Oudere berichten",
+ "Original site": "Originele site",
+ "Posted": "Geplaatst",
+ "Posts about %s": "Berichten over %s",
+ "Posts for year %s": "Berichten voor het jaar %s",
+ "Posts for {month} {year}": "Berichten voor {month} {year}",
+ "Previous post": "Vorig bericht",
+ "Read in English": "Lees in het Nederlands",
+ "Read more": "Lees verder",
+ "Source": "Bron",
+ "Tags and Categories": "Tags en Categorieën",
+ "Tags": "Tags",
+ "old posts page %d": "oude berichten pagina %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_pl.py b/nikola/data/themes/base/messages/messages_pl.py
index 876b5f2..cf5c51c 100644
--- a/nikola/data/themes/default/messages/messages_pl.py
+++ b/nikola/data/themes/base/messages/messages_pl.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Również dostępny w",
"Archive": "Archiwum",
+ "Categories": "Kategorie",
"LANGUAGE": "polski",
"More posts about": "Więcej postów o",
"Newer posts": "Nowsze posty",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "Czytaj po polsku",
"Read more": "Czytaj więcej",
"Source": "Źródło",
+ "Tags and Categories": "Tagi i Kategorie",
"Tags": "Tags",
"old posts page %d": "stare posty, strona %d",
}
diff --git a/nikola/data/themes/default/messages/messages_pt_br.py b/nikola/data/themes/base/messages/messages_pt_br.py
index 12dafb5..ebf642a 100644
--- a/nikola/data/themes/default/messages/messages_pt_br.py
+++ b/nikola/data/themes/base/messages/messages_pt_br.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "Também disponível em",
"Archive": "Arquivo",
+ "Categories": "Categorias",
"LANGUAGE": "Português",
"More posts about": "Mais posts sobre",
"Newer posts": "Posts mais recentes",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "Ler em português",
"Read more": "Leia mais",
"Source": "Código",
+ "Tags and Categories": "Tags e Categorias",
"Tags": "Tags",
"old posts page %d": "Posts antigos página %d",
}
diff --git a/nikola/data/themes/default/messages/messages_ru.py b/nikola/data/themes/base/messages/messages_ru.py
index 1d7e41c..44ce1f2 100644
--- a/nikola/data/themes/default/messages/messages_ru.py
+++ b/nikola/data/themes/base/messages/messages_ru.py
@@ -2,8 +2,9 @@
from __future__ import unicode_literals
MESSAGES = {
- "Also available in": "Также доступно в",
+ "Also available in": "Также доступно на",
"Archive": "Архив",
+ "Categories": "Категории",
"LANGUAGE": "Русский",
"More posts about": "Больше записей о",
"Newer posts": "Новые записи",
@@ -11,13 +12,14 @@ MESSAGES = {
"Older posts": "Старые записи",
"Original site": "Оригинальный сайт",
"Posted": "Опубликовано",
- "Posts about %s": "Записи с тэгом %s:",
+ "Posts about %s": "Записи о %s",
"Posts for year %s": "Записи за %s год",
- "Posts for {month} {year}": "",
+ "Posts for {month} {year}": "Записи за {month} {year}",
"Previous post": "Предыдущая запись",
"Read in English": "Прочесть по-русски",
- "Read more": "Продолжить чтение",
- "Source": "Source",
+ "Read more": "Читать далее",
+ "Source": "Источник",
+ "Tags and Categories": "Тэги и категории",
"Tags": "Тэги",
- "old posts page %d": "страница со старыми записями %d",
+ "old posts page %d": "%d страница со старыми записями",
}
diff --git a/nikola/data/themes/base/messages/messages_sl.py b/nikola/data/themes/base/messages/messages_sl.py
new file mode 100644
index 0000000..bad3327
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_sl.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Na voljo tudi v",
+ "Archive": "Arhiv",
+ "Categories": "Kategorije",
+ "LANGUAGE": "Slovenščina",
+ "More posts about": "Več objav o",
+ "Newer posts": "Novejše objave",
+ "Next post": "Naslednja objava",
+ "Older posts": "Starejše objave",
+ "Original site": "Izvorna spletna stran",
+ "Posted": "Objavljeno",
+ "Posts about %s": "Objave o %s",
+ "Posts for year %s": "Objave za leto %s",
+ "Posts for {month} {year}": "Objave za {month} {year}",
+ "Previous post": "Prejšnja objava",
+ "Read in English": "Beri v slovenščini",
+ "Read more": "Več o tem",
+ "Source": "Izvor",
+ "Tags and Categories": "Značke in kategorije",
+ "Tags": "Značke",
+ "old posts page %d": "stare objave, stran %d",
+}
diff --git a/nikola/data/themes/base/messages/messages_sl_si.py b/nikola/data/themes/base/messages/messages_sl_si.py
new file mode 120000
index 0000000..152e151
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_sl_si.py
@@ -0,0 +1 @@
+messages_sl.py \ No newline at end of file
diff --git a/nikola/data/themes/base/messages/messages_tr_tr.py b/nikola/data/themes/base/messages/messages_tr_tr.py
new file mode 100644
index 0000000..7cf0e9f
--- /dev/null
+++ b/nikola/data/themes/base/messages/messages_tr_tr.py
@@ -0,0 +1,25 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Şu dilde de mevcut:",
+ "Archive": "Arşiv",
+ "Categories": "Kategoriler",
+ "LANGUAGE": "Türkçe",
+ "More posts about": "ilgili diğer yazılar",
+ "Newer posts": "Daha yeni yazılar",
+ "Next post": "Sonraki yazı",
+ "Older posts": "Daha eski yazılar",
+ "Original site": "Orjinal web sayfası",
+ "Posted": "Yayın tarihi",
+ "Posts about %s": "%s ile ilgili yazılar",
+ "Posts for year %s": "%s yılındaki yazılar",
+ "Posts for {month} {year}": "{month} {year} göre yazılar",
+ "Previous post": "Önceki yazı",
+ "Read in English": "Türkçe olarak oku",
+ "Read more": "Devamını oku",
+ "Source": "Kaynak",
+ "Tags and Categories": "Etiketler ve Kategoriler",
+ "Tags": "Etiketler",
+ "old posts page %d": "eski yazılar sayfa %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_zh_cn.py b/nikola/data/themes/base/messages/messages_zh_cn.py
index aa69a96..fc79313 100644
--- a/nikola/data/themes/default/messages/messages_zh_cn.py
+++ b/nikola/data/themes/base/messages/messages_zh_cn.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
MESSAGES = {
"Also available in": "其他语言版本",
"Archive": "文章存档",
+ "Categories": "",
"LANGUAGE": "简体中文",
"More posts about": "更多相关文章:",
"Newer posts": "新一篇",
@@ -18,6 +19,7 @@ MESSAGES = {
"Read in English": "中文版",
"Read more": "更多",
"Source": "源代码",
+ "Tags and Categories": "",
"Tags": "标签",
"old posts page %d": "旧文章页 %d",
}
diff --git a/nikola/data/themes/base/templates/annotation_helper.tmpl b/nikola/data/themes/base/templates/annotation_helper.tmpl
new file mode 100644
index 0000000..70077ab
--- /dev/null
+++ b/nikola/data/themes/base/templates/annotation_helper.tmpl
@@ -0,0 +1,16 @@
+<%def name="css()">
+ <link rel="stylesheet" href="http://assets.annotateit.org/annotator/v1.2.5/annotator.min.css">
+</%def>
+
+<%def name="code()">
+ <script src="http://code.jquery.com/jquery-migrate-1.2.1.js"></script>
+ <script src="http://assets.annotateit.org/annotator/v1.2.7/annotator-full.js"></script>
+ <script>
+ jQuery(function ($) {
+ $('body').annotator().annotator('setupPlugins', {}, {
+ // Disable filter bar
+ Filter: false
+ });
+ });
+ </script>
+</%def>
diff --git a/nikola/data/themes/base/templates/base.tmpl b/nikola/data/themes/base/templates/base.tmpl
new file mode 100644
index 0000000..31c1747
--- /dev/null
+++ b/nikola/data/themes/base/templates/base.tmpl
@@ -0,0 +1,40 @@
+## -*- coding: utf-8 -*-
+<%namespace name="base" file="base_helper.tmpl" import="*"/>
+<%namespace name="annotations" file="annotation_helper.tmpl"/>
+${set_locale(lang)}
+<!DOCTYPE html>
+<html
+%if comment_system == 'facebook':
+xmlns:fb="http://ogp.me/ns/fb#"
+%endif
+lang="${lang}">
+<head>
+ ${base.html_head()}
+ <%block name="extra_head">
+ </%block>
+ ${extra_head_data}
+</head>
+<body>
+ <h1 id="blog-title">
+ <a href="${abs_link('/')}" title="${blog_title}" rel="home">${blog_title}</a>
+ </h1>
+ <%block name="belowtitle">
+ %if len(translations) > 1:
+ <small>
+ ${(messages("Also available in"))}:&nbsp;
+ ${base.html_translations()}
+ </small>
+ %endif
+ </%block>
+ <%block name="content"></%block>
+ <small>${content_footer}</small>
+ <!--Sidebar content-->
+ <ul class="unstyled">
+ <li>${license}
+ ${base.html_social()}
+ ${base.html_navigation_links()}
+ <li>${search_form}
+ </ul>
+ ${base.late_load_js()}
+ ${social_buttons_code}
+</body>
diff --git a/nikola/data/themes/base/templates/base_helper.tmpl b/nikola/data/themes/base/templates/base_helper.tmpl
new file mode 100644
index 0000000..ac1dea3
--- /dev/null
+++ b/nikola/data/themes/base/templates/base_helper.tmpl
@@ -0,0 +1,90 @@
+## -*- coding: utf-8 -*-
+<%def name="html_head()">
+ <meta charset="utf-8">
+ %if description:
+ <meta name="description" content="${description}">
+ %endif
+ <meta name="author" content="${blog_author}">
+ <title>${title|striphtml} | ${blog_title|striphtml}</title>
+ ${mathjax_config}
+ %if use_bundles:
+ %if use_cdn:
+ <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
+ %else:
+ <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
+ %endif
+ %else:
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
+ %if has_custom_css:
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ %endif
+ %endif
+ <!--[if lt IE 9]>
+ <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
+ <![endif]-->
+ %if rss_link:
+ ${rss_link}
+ %else:
+ %if len(translations) > 1:
+ %for language in translations:
+ <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
+ %endfor
+ %else:
+ <link rel="alternate" type="application/rss+xml" title="RSS" href="${_link('rss', None)}">
+ %endif
+ %endif
+ %if favicons:
+ %for name, file, size in favicons:
+ <link rel="${name}" href="${file}" sizes="${size}"/>
+ %endfor
+ %endif
+ % if comment_system == 'facebook':
+ <meta property="fb:app_id" content="${comment_system_id}">
+ % endif
+</%def>
+
+<%def name="late_load_js()">
+</%def>
+
+<%def name="html_social()">
+ ${social_buttons_code}
+</%def>
+
+<!--FIXME: remove in v7 -->
+<%def name="html_sidebar_links()">
+ ${html_navigation_links()}
+</%def>
+
+<%def name="html_navigation_links()">
+ %for url, text in navigation_links[lang]:
+ % if isinstance(url, tuple):
+ <li> ${text}
+ <ul>
+ %for suburl, text in url:
+ % if rel_link(permalink, suburl) == "#":
+ <li class="active"><a href="${suburl}">${text}</a>
+ %else:
+ <li><a href="${suburl}">${text}</a>
+ %endif
+ %endfor
+ </ul>
+ % else:
+ % if rel_link(permalink, url) == "#":
+ <li class="active"><a href="${url}">${text}</a>
+ %else:
+ <li><a href="${url}">${text}</a>
+ %endif
+ % endif
+ %endfor
+</%def>
+
+
+<%def name="html_translations()">
+ %for langname in translations.keys():
+ %if langname != lang:
+ <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
+ %endif
+ %endfor
+</%def>
diff --git a/nikola/data/themes/base/templates/comments_helper.tmpl b/nikola/data/themes/base/templates/comments_helper.tmpl
new file mode 100644
index 0000000..e74a146
--- /dev/null
+++ b/nikola/data/themes/base/templates/comments_helper.tmpl
@@ -0,0 +1,49 @@
+## -*- coding: utf-8 -*-
+
+<%namespace name="disqus" file="disqus_helper.tmpl"/>
+<%namespace name="livefyre" file="livefyre_helper.tmpl"/>
+<%namespace name="intensedebate" file="intensedebate_helper.tmpl"/>
+<%namespace name="moot" file="moot_helper.tmpl"/>
+<%namespace name="googleplus" file="googleplus_helper.tmpl"/>
+
+<%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 == 'moot':
+ ${moot.comment_form(url, title, identifier)}
+ % elif comment_system == 'googleplus':
+ ${googleplus.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 == 'moot':
+ ${moot.comment_link(link, identifier)}
+ % elif comment_system == 'googleplus':
+ ${googleplus.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 == 'moot':
+ ${moot.comment_link_script()}
+ % elif comment_system == 'googleplus':
+ ${googleplus.comment_link_script()}
+ %endif
+</%def>
diff --git a/nikola/data/themes/base/templates/crumbs.tmpl b/nikola/data/themes/base/templates/crumbs.tmpl
new file mode 100644
index 0000000..c458cbe
--- /dev/null
+++ b/nikola/data/themes/base/templates/crumbs.tmpl
@@ -0,0 +1,9 @@
+## -*- coding: utf-8 -*-
+
+<%def name="bar(crumbs)">
+<ul class="breadcrumb">
+ % for link, text in crumbs:
+ <li><a href="${link}">${text}</a></li>
+ % endfor
+</ul>
+</%def>
diff --git a/nikola/data/themes/monospace/templates/disqus_helper.tmpl b/nikola/data/themes/base/templates/disqus_helper.tmpl
index 4c60f85..211f27a 100644
--- a/nikola/data/themes/monospace/templates/disqus_helper.tmpl
+++ b/nikola/data/themes/base/templates/disqus_helper.tmpl
@@ -5,11 +5,12 @@
'es': 'es_ES',
}
%>
-<%def name="html_disqus(url, title, identifier)">
- %if disqus_forum:
+
+<%def name="comment_form(url, title, identifier)">
+ %if comment_system_id:
<div id="disqus_thread"></div>
<script type="text/javascript">
- var disqus_shortname ="${disqus_forum}";
+ var disqus_shortname ="${comment_system_id}";
%if url:
var disqus_url="${url}";
%endif
@@ -25,19 +26,33 @@
})();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
+ <a href="http://disqus.com" class="dsq-brlink">Comments powered by <span class="logo-disqus">Disqus</span></a>
%endif
</%def>
-<%def name="html_disqus_link(link, identifier)">
+<%def name="comment_link(link, identifier)">
<p>
- %if disqus_forum:
- <a href="${link}" data-disqus-identifier="${identifier}">Comments</a>
+ %if comment_system_id:
+ <a href="${link}#disqus_thread" data-disqus-identifier="${identifier}">Comments</a>
%endif
</%def>
-<%def name="html_disqus_script()">
- %if disqus_forum:
- <script type="text/javascript">var disqus_shortname="${disqus_forum}";(function(){var a=document.createElement("script");a.async=true;a.type="text/javascript";a.src="http://"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("HEAD")[0]||document.getElementsByTagName("BODY")[0]).appendChild(a)}());</script>
+<%def name="comment_link_script()">
+ %if comment_system_id:
+ <script type="text/javascript">var disqus_shortname="${comment_system_id}";(function(){var a=document.createElement("script");a.async=true;a.type="text/javascript";a.src="http://"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("HEAD")[0]||document.getElementsByTagName("BODY")[0]).appendChild(a)}());</script>
%endif
</%def>
+
+## FIXME: remove aliases in v7
+<%def name="html_disqus(url, title, identifier)">
+ ${comment_form(url, title, identifier)}
+</%def>
+
+<%def name="html_disqus_link(link, identifier)">
+ ${comment_link(link, identifier)}
+</%def>
+
+<%def name="html_disqus_script()">
+ ${comment_link_script()}
+</%def>
diff --git a/nikola/data/themes/base/templates/facebook_helper.tmpl b/nikola/data/themes/base/templates/facebook_helper.tmpl
new file mode 100644
index 0000000..d6059a1
--- /dev/null
+++ b/nikola/data/themes/base/templates/facebook_helper.tmpl
@@ -0,0 +1,62 @@
+## -*- coding: utf-8 -*-
+<%def name="comment_form(url, title, identifier)">
+<div id="fb-root"></div>
+<script>
+ window.fbAsyncInit = function() {
+ // init the FB JS SDK
+ FB.init({
+ appId : '${comment_system_id}',
+ status : true,
+ xfbml : true
+ });
+
+ };
+
+ // Load the SDK asynchronously
+ (function(d, s, id){
+ var js, fjs = d.getElementsByTagName(s)[0];
+ if (d.getElementById(id)) {return;}
+ js = d.createElement(s); js.id = id;
+ js.src = "//connect.facebook.net/en_US/all.js";
+ fjs.parentNode.insertBefore(js, fjs);
+ }(document, 'script', 'facebook-jssdk'));
+</script>
+
+<div class="fb-comments" data-href="${url}" data-width="470"></div>
+</%def>
+
+<%def name="comment_link(link, identifier)">
+<span class="fb-comments-count" data-url="${link}">
+</%def>
+
+<%def name="comment_link_script()">
+<div id="fb-root"></div>
+<script>
+ // thank lxml for this
+ $('.fb-comments-count').each(function(i, obj) {
+ var url = obj.attributes['data-url'].value;
+ // change here if you dislike the default way of displaying
+ // this
+ obj.innerHTML = '<fb:comments-count href="' + url + '"></fb:comments-count> comments';
+ });
+
+ window.fbAsyncInit = function() {
+ // init the FB JS SDK
+ FB.init({
+ appId : '${comment_system_id}',
+ status : true,
+ xfbml : true
+ });
+
+ };
+
+ // Load the SDK asynchronously
+ (function(d, s, id){
+ var js, fjs = d.getElementsByTagName(s)[0];
+ if (d.getElementById(id)) {return;}
+ js = d.createElement(s); js.id = id;
+ js.src = "//connect.facebook.net/en_US/all.js";
+ fjs.parentNode.insertBefore(js, fjs);
+ }(document, 'script', 'facebook-jssdk'));
+</script>
+</%def>
diff --git a/nikola/data/themes/base/templates/gallery.tmpl b/nikola/data/themes/base/templates/gallery.tmpl
new file mode 100644
index 0000000..be9cc6f
--- /dev/null
+++ b/nikola/data/themes/base/templates/gallery.tmpl
@@ -0,0 +1,28 @@
+## -*- 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 text:
+ <p>
+ ${text}
+ </p>
+ %endif
+ <ul>
+ % for folder in folders:
+ <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
+ % endfor
+ </ul>
+ <ul class="thumbnails">
+ %for image in photo_array:
+ <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}">
+ <img src="${image['url_thumb']}" alt="${image['title']}" /></a>
+ %endfor
+ </ul>
+%if enable_comments:
+ ${comments.comment_form(None, permalink, title)}
+%endif
+</%block>
diff --git a/nikola/data/themes/base/templates/googleplus_helper.tmpl b/nikola/data/themes/base/templates/googleplus_helper.tmpl
new file mode 100644
index 0000000..5a5c4d7
--- /dev/null
+++ b/nikola/data/themes/base/templates/googleplus_helper.tmpl
@@ -0,0 +1,17 @@
+## -*- 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/index.tmpl b/nikola/data/themes/base/templates/index.tmpl
new file mode 100644
index 0000000..6de79e2
--- /dev/null
+++ b/nikola/data/themes/base/templates/index.tmpl
@@ -0,0 +1,29 @@
+## -*- coding: utf-8 -*-
+<%namespace name="helper" file="index_helper.tmpl"/>
+<%namespace name="comments" file="comments_helper.tmpl"/>
+<%inherit file="base.tmpl"/>
+<%block name="content">
+ % for post in posts:
+ <div class="postbox h-entry">
+ <h1 class="p-name"><a href="${post.permalink()}" class="u-url">${post.title()}</a>
+ <small>&nbsp;&nbsp;
+ ${messages("Posted")}: <time class="published dt-published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
+ </small></h1>
+ <hr>
+ %if index_teasers:
+ <div class="p-summary">
+ ${post.text(teaser_only=True)}
+ %else:
+ <div class="e-content">
+ ${post.text(teaser_only=False)}
+ %endif
+ </div>
+ % if not post.meta('nocomments'):
+ ${comments.comment_link(post.permalink(), post._base_path)}
+ % endif
+ </div>
+ % endfor
+ ${helper.html_pager()}
+ ${comments.comment_link_script()}
+ ${helper.mathjax_script(posts)}
+</%block>
diff --git a/nikola/data/themes/default/templates/index_helper.tmpl b/nikola/data/themes/base/templates/index_helper.tmpl
index 7859972..a914846 100644
--- a/nikola/data/themes/default/templates/index_helper.tmpl
+++ b/nikola/data/themes/base/templates/index_helper.tmpl
@@ -4,17 +4,19 @@
<ul class="pager">
%if prevlink:
<li class="previous">
- <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
+ <a href="${prevlink}" rel="prev">&larr; ${messages("Newer posts")}</a>
+ </li>
%endif
%if nextlink:
<li class="next">
- <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
+ <a href="${nextlink}" rel="next">${messages("Older posts")} &rarr;</a>
+ </li>
%endif
</ul>
</div>
</%def>
-<%def name="mathjax_script(post)">
+<%def name="mathjax_script(posts)">
%if any(post.is_mathjax for post in posts):
<script src="/assets/js/mathjax.js" type="text/javascript"></script>
%endif
diff --git a/nikola/data/themes/base/templates/intensedebate_helper.tmpl b/nikola/data/themes/base/templates/intensedebate_helper.tmpl
new file mode 100644
index 0000000..6462f74
--- /dev/null
+++ b/nikola/data/themes/base/templates/intensedebate_helper.tmpl
@@ -0,0 +1,25 @@
+## -*- coding: utf-8 -*-
+<%def name="comment_form(url, title, identifier)">
+<script>
+var idcomments_acct = '${comment_system_id}';
+var idcomments_post_id = "${identifier}";
+var idcomments_post_url = "${url}";
+</script>
+<span id="IDCommentsPostTitle" style="display:none"></span>
+<script type='text/javascript' src='http://www.intensedebate.com/js/genericCommentWrapperV2.js'></script>
+</script>
+</%def>
+
+<%def name="comment_link(link, identifier)">
+<a href="{link}" onclick="this.href='${link}'; this.target='_self';"><span class='IDCommentsReplace' style='display:none'>${identifier}</span>
+<script>
+var idcomments_acct = '${comment_system_id}';
+var idcomments_post_id = "${identifier}";
+var idcomments_post_url = "${link}";
+</script>
+<script type="text/javascript" src="http://www.intensedebate.com/js/genericLinkWrapperV2.js"></script>
+</a>
+</%def>
+
+<%def name="comment_link_script()">
+</%def>
diff --git a/nikola/data/themes/default/templates/list.tmpl b/nikola/data/themes/base/templates/list.tmpl
index a60b508..a60b508 100644
--- a/nikola/data/themes/default/templates/list.tmpl
+++ b/nikola/data/themes/base/templates/list.tmpl
diff --git a/nikola/data/themes/default/templates/list_post.tmpl b/nikola/data/themes/base/templates/list_post.tmpl
index f0e159d..f0e159d 100644
--- a/nikola/data/themes/default/templates/list_post.tmpl
+++ b/nikola/data/themes/base/templates/list_post.tmpl
diff --git a/nikola/data/themes/default/templates/listing.tmpl b/nikola/data/themes/base/templates/listing.tmpl
index f279af0..b6ca83f 100644
--- a/nikola/data/themes/default/templates/listing.tmpl
+++ b/nikola/data/themes/base/templates/listing.tmpl
@@ -1,11 +1,8 @@
## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
+<%namespace name="ui" file="crumbs.tmpl" import="bar"/>
<%block name="content">
-<ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/${crumb}</a>
- % endfor
-</ul>
+${ui.bar(crumbs)}
<ul class="unstyled">
% for name in folders:
<li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a>
diff --git a/nikola/data/themes/base/templates/livefyre_helper.tmpl b/nikola/data/themes/base/templates/livefyre_helper.tmpl
new file mode 100644
index 0000000..6916459
--- /dev/null
+++ b/nikola/data/themes/base/templates/livefyre_helper.tmpl
@@ -0,0 +1,37 @@
+## -*- coding: utf-8 -*-
+<%def name="comment_form(url, title, identifier)">
+<div id="livefyre-comments"></div>
+<script type="text/javascript" src="http://zor.livefyre.com/wjs/v3.0/javascripts/livefyre.js"></script>
+<script type="text/javascript">
+(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)">
+ <p>
+ <a href="${link}">
+ <span class="livefyre-commentcount" data-lf-site-id="${comment_system_id}" data-lf-article-id="${identifier}">
+ 0 Comments
+ </span></a>
+</%def>
+
+
+<%def name="comment_link_script()">
+<script
+ type="text/javascript"
+ src="http://zor.livefyre.com/wjs/v1.0/javascripts/CommentCount.js">
+</script>
+</%def>
diff --git a/nikola/data/themes/base/templates/moot_helper.tmpl b/nikola/data/themes/base/templates/moot_helper.tmpl
new file mode 100644
index 0000000..951553e
--- /dev/null
+++ b/nikola/data/themes/base/templates/moot_helper.tmpl
@@ -0,0 +1,13 @@
+## -*- coding: utf-8 -*-
+
+<%def name="comment_form(url, title, identifier)">
+ <a class="moot" href="https://moot.it/i/${comment_system_id}/${identifier}">${comment_system_id} forums</a>
+</%def>
+
+<%def name="comment_link(link, identifier)">
+</%def>
+
+
+<%def name="comment_link_script()">
+<script src="https://cdn.moot.it/1/moot.min.js"></script>
+</%def>
diff --git a/nikola/data/themes/base/templates/mustache-comment-form.tmpl b/nikola/data/themes/base/templates/mustache-comment-form.tmpl
new file mode 100644
index 0000000..593d0aa
--- /dev/null
+++ b/nikola/data/themes/base/templates/mustache-comment-form.tmpl
@@ -0,0 +1,5 @@
+## -*- 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/post.tmpl b/nikola/data/themes/base/templates/post.tmpl
new file mode 100644
index 0000000..fd6316d
--- /dev/null
+++ b/nikola/data/themes/base/templates/post.tmpl
@@ -0,0 +1,41 @@
+## -*- coding: utf-8 -*-
+<%namespace name="helper" file="post_helper.tmpl"/>
+<%namespace name="comments" file="comments_helper.tmpl"/>
+<%inherit file="base.tmpl"/>
+<%block name="extra_head">
+${helper.twitter_card_information(post)}
+% if post.meta('keywords'):
+ <meta name="keywords" content="${post.meta('keywords')|h}">
+% endif
+</%block>
+<%block name="content">
+ <div class="postbox">
+ <div class="h-entry" itemscope="itemscope" itemtype="http://schema.org/Article">
+ ${helper.html_title()}
+ <hr>
+ <small>
+ ${messages("Posted")}: <time class="published dt-published" datetime="${post.date.isoformat()}" itemprop="datePublished">${post.formatted_date(date_format)}</time>
+ ${helper.html_translations(post)}
+ ${helper.html_tags(post)}
+ &nbsp;&nbsp;|&nbsp;&nbsp;
+ <%block name="sourcelink">
+ % if not post.meta('password'):
+ <a href="${post.source_link()}" id="sourcelink">${messages("Source")}</a>
+ % endif
+ </%block>
+ </small>
+ <hr>
+ <div class="e-content" itemprop="articleBody text">
+ ${post.text()}
+ </div>
+ %if post.description():
+ <meta content="${post.description()}" itemprop="description">
+ %endif
+ </div>
+ ${helper.html_pager(post)}
+ % if not post.meta('nocomments'):
+ ${comments.comment_form(post.permalink(absolute=True), post.title(), post._base_path)}
+ % endif
+ ${helper.mathjax_script(post)}
+ </div>
+</%block>
diff --git a/nikola/data/themes/orphan/templates/post_helper.tmpl b/nikola/data/themes/base/templates/post_helper.tmpl
index cce0ecf..f6b0535 100644
--- a/nikola/data/themes/orphan/templates/post_helper.tmpl
+++ b/nikola/data/themes/base/templates/post_helper.tmpl
@@ -1,6 +1,6 @@
## -*- coding: utf-8 -*-
<%def name="html_title()">
- <h1>${title}</h1>
+ <h1 class="p-name" itemprop="headline name">${title|h}</h1>
% if link:
<p><a href='${link}'>${messages("Original site")}</a></p>
% endif
@@ -22,9 +22,11 @@
<%def name="html_tags(post)">
%if post.tags:
&nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
+ <span itemprop="keywords">
%for tag in post.tags:
- <a class="tag" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
+ <a class="tag p-category" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
%endfor
+ </span>
%endif
</%def>
@@ -32,12 +34,12 @@
<ul class="pager">
%if post.prev_post:
<li class="previous">
- <a href="${post.prev_post.permalink()}">&larr; ${messages("Previous post")}</a>
+ <a href="${post.prev_post.permalink()}" rel="prev">&larr; ${messages("Previous post")}</a>
</li>
%endif
%if post.next_post:
<li class="next">
- <a href="${post.next_post.permalink()}">${messages("Next post")} &rarr;</a>
+ <a href="${post.next_post.permalink()}" rel="next">${messages("Next post")} &rarr;</a>
</li>
%endif
</ul>
@@ -68,6 +70,6 @@
<%def name="mathjax_script(post)">
%if post.is_mathjax:
- <script src="/assets/js/mathjax.js" type="text/javascript"></script>
+ <script src="/assets/js/mathjax.js"></script>
%endif
</%def>
diff --git a/nikola/data/themes/base/templates/post_list_directive.tmpl b/nikola/data/themes/base/templates/post_list_directive.tmpl
new file mode 100644
index 0000000..3345ae4
--- /dev/null
+++ b/nikola/data/themes/base/templates/post_list_directive.tmpl
@@ -0,0 +1,14 @@
+## -*- coding: utf-8 -*-
+<!-- Begin post-list ${post_list_id} -->
+<div id="${post_list_id}" class="post-list">
+ <ul class="post-list">
+ % for post in posts:
+ <li class="post-list-item">
+ ${post.formatted_date(date_format)}
+ &nbsp;
+ <a href="${post.permalink(lang)}">${post.title(lang)}</a>
+ </li>
+ % endfor
+ </ul>
+</div>
+<!-- End post-list ${post_list_id} -->
diff --git a/nikola/data/themes/base/templates/slides.tmpl b/nikola/data/themes/base/templates/slides.tmpl
new file mode 100644
index 0000000..14983ad
--- /dev/null
+++ b/nikola/data/themes/base/templates/slides.tmpl
@@ -0,0 +1,22 @@
+<div id="${carousel_id}" class="carousel slide">
+ <ol class="carousel-indicators">
+ % for i in range(len(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(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>
diff --git a/nikola/data/themes/orphan/templates/story.tmpl b/nikola/data/themes/base/templates/story.tmpl
index 21d0e2f..7406f05 100644
--- a/nikola/data/themes/orphan/templates/story.tmpl
+++ b/nikola/data/themes/base/templates/story.tmpl
@@ -1,15 +1,16 @@
## -*- coding: utf-8 -*-
<%inherit file="post.tmpl"/>
<%namespace name="helper" file="post_helper.tmpl"/>
+<%namespace name="comments" file="comments_helper.tmpl"/>
<%block name="extra_head">
${helper.twitter_card_information(post)}
</%block>
<%block name="content">
-%if title:
+%if title and not post.meta('hidetitle'):
<h1>${title}</h1>
%endif
${post.text()}
%if enable_comments and not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
+ ${comments.comment_form(post.permalink(absolute=True), post.title(), post.base_path)}
%endif
</%block>
diff --git a/nikola/data/themes/default/templates/tag.tmpl b/nikola/data/themes/base/templates/tag.tmpl
index 7fb43c0..2ca9db4 100644
--- a/nikola/data/themes/default/templates/tag.tmpl
+++ b/nikola/data/themes/base/templates/tag.tmpl
@@ -3,10 +3,10 @@
<%block name="extra_head">
%if len(translations) > 1:
%for language in translations:
- <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag} (${language})" href="${_link("tag_rss", tag, language)}">
+ <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for ${kind} ${tag} (${language})" href="${_link(kind + "_rss", tag, language)}">
%endfor
%else:
- <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag}" href="${_link("tag_rss", tag)}">
+ <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for ${kind} ${tag}" href="${_link(kind + "_rss", tag)}">
%endif
</%block>
@@ -16,10 +16,10 @@
<h1>${title}</h1>
%if len(translations) > 1:
%for language in translations:
- <a href="${_link("tag_rss", tag, language)}">RSS (${language})</a>&nbsp;
+ <a href="${_link(kind + "_rss", tag, language)}">RSS (${language})</a>&nbsp;
%endfor
%else:
- <a href="${_link("tag_rss", tag)}">RSS</a>
+ <a href="${_link(kind + "_rss", tag)}">RSS</a>
%endif
<br>
<ul class="unstyled">
diff --git a/nikola/data/themes/base/templates/tags.tmpl b/nikola/data/themes/base/templates/tags.tmpl
new file mode 100644
index 0000000..6c8c5e9
--- /dev/null
+++ b/nikola/data/themes/base/templates/tags.tmpl
@@ -0,0 +1,25 @@
+## -*- coding: utf-8 -*-
+<%inherit file="base.tmpl"/>
+<%block name="content">
+ <h1>${title}</h1>
+ % if cat_items:
+ <h2>${messages("Categories")}</h2>
+ <ul class="unstyled bricks">
+ % for text, link in cat_items:
+ % if text:
+ <li><a class="reference" href="${link}">${text}</a></li>
+ % endif
+ % endfor
+ </ul>
+ % if items:
+ <h2>${messages("Tags")}</h2>
+ % endif
+ %endif
+ % if items:
+ <ul class="unstyled bricks">
+ % for text, link in items:
+ <li><a class="reference" href="${link}">${text}</a></li>
+ % endfor
+ </ul>
+ % endif
+</%block>
diff --git a/nikola/data/themes/site/README b/nikola/data/themes/bootstrap/README.md
index c89543c..5340fe2 100644
--- a/nikola/data/themes/site/README
+++ b/nikola/data/themes/bootstrap/README.md
@@ -1,18 +1,9 @@
-A more "website-done-with-bootstrap" theme, so to speak. This theme should
-be completely compatible with the default theme and need no configuration
-changes, except for the search form, as explained below.
+A "website-done-with-bootstrap" theme, so to speak.
+Has a fixed navigation bar at top that displays the NAVIGATION_LINKS
+setting and supports nested menus.
-Has a fixed navigation bar at top that displays:
-
-* Site name
-* navigation links (taken from sidebar_links in dodo.py)
-* search form (taken from SEARCH_FORM in dodo.py)
-* Link to page's source code (if available)
-* Floating social bar on the left margin
-* The below-title block is displayed flush right
-
-This theme is used in Nikola's website: http://nikola.ralsina.com.ar
+This theme is used in Nikola's website: http://getnikola.com
Important: To fit in the bootstrap navigation bar, the search form needs the
navbar-form and pull-left CSS classes applied. Here is an example with Nikola's
diff --git a/nikola/data/themes/default/assets/css/colorbox.css b/nikola/data/themes/bootstrap/assets/css/colorbox.css
index f67c346..13c3308 100644
--- a/nikola/data/themes/default/assets/css/colorbox.css
+++ b/nikola/data/themes/bootstrap/assets/css/colorbox.css
@@ -1,85 +1,69 @@
-/*
- ColorBox Core Style:
- The following CSS is consistent between example themes and should not be altered.
-*/
-#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
-#cboxOverlay{position:fixed; width:100%; height:100%;}
-#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
-#cboxContent{position:relative;}
-#cboxLoadedContent{overflow:auto;}
-#cboxTitle{margin:0;}
-#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
-#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
-.cboxPhoto{float:left; margin:auto; border:0; display:block;}
-.cboxIframe{width:100%; height:100%; display:block; border:0;}
-
-/*
- User Style:
- Change the following styles to modify the appearance of ColorBox. They are
- ordered & tabbed in a way that represents the nesting of the generated HTML.
-*/
-#cboxOverlay{background:url(images/overlay.png) repeat 0 0;}
-#colorbox{}
- #cboxTopLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px 0;}
- #cboxTopRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px 0;}
- #cboxBottomLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px -29px;}
- #cboxBottomRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px -29px;}
- #cboxMiddleLeft{width:21px; background:url(images/controls.png) left top repeat-y;}
- #cboxMiddleRight{width:21px; background:url(images/controls.png) right top repeat-y;}
- #cboxTopCenter{height:21px; background:url(images/border.png) 0 0 repeat-x;}
- #cboxBottomCenter{height:21px; background:url(images/border.png) 0 -29px repeat-x;}
- #cboxContent{background:#fff; overflow:hidden;}
- .cboxIframe{background:#fff;}
- #cboxError{padding:50px; border:1px solid #ccc;}
- #cboxLoadedContent{margin-bottom:28px;}
- #cboxTitle{position:absolute; bottom:4px; left:0; text-align:center; width:100%; color:#949494;}
- #cboxCurrent{position:absolute; bottom:4px; left:58px; color:#949494;}
- #cboxSlideshow{position:absolute; bottom:4px; right:30px; color:#0092ef;}
- #cboxPrevious{position:absolute; bottom:0; left:0; background:url(images/controls.png) no-repeat -75px 0; width:25px; height:25px; text-indent:-9999px;}
- #cboxPrevious:hover{background-position:-75px -25px;}
- #cboxNext{position:absolute; bottom:0; left:27px; background:url(images/controls.png) no-repeat -50px 0; width:25px; height:25px; text-indent:-9999px;}
- #cboxNext:hover{background-position:-50px -25px;}
- #cboxLoadingOverlay{background:url(images/loading_background.png) no-repeat center center;}
- #cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;}
- #cboxClose{position:absolute; bottom:0; right:0; background:url(images/controls.png) no-repeat -25px 0; width:25px; height:25px; text-indent:-9999px;}
- #cboxClose:hover{background-position:-25px -25px;}
-
-/*
- The following fixes a problem where IE7 and IE8 replace a PNG's alpha transparency with a black fill
- when an alpha filter (opacity change) is set on the element or ancestor element. This style is not applied to or needed in IE9.
- See: http://jacklmoore.com/notes/ie-transparency-problems/
-*/
-.cboxIE #cboxTopLeft,
-.cboxIE #cboxTopCenter,
-.cboxIE #cboxTopRight,
-.cboxIE #cboxBottomLeft,
-.cboxIE #cboxBottomCenter,
-.cboxIE #cboxBottomRight,
-.cboxIE #cboxMiddleLeft,
-.cboxIE #cboxMiddleRight {
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF);
-}
-
-/*
- The following provides PNG transparency support for IE6
- Feel free to remove this and the /ie6/ directory if you have dropped IE6 support.
-*/
-.cboxIE6 #cboxTopLeft{background:url(images/ie6/borderTopLeft.png);}
-.cboxIE6 #cboxTopCenter{background:url(images/ie6/borderTopCenter.png);}
-.cboxIE6 #cboxTopRight{background:url(images/ie6/borderTopRight.png);}
-.cboxIE6 #cboxBottomLeft{background:url(images/ie6/borderBottomLeft.png);}
-.cboxIE6 #cboxBottomCenter{background:url(images/ie6/borderBottomCenter.png);}
-.cboxIE6 #cboxBottomRight{background:url(images/ie6/borderBottomRight.png);}
-.cboxIE6 #cboxMiddleLeft{background:url(images/ie6/borderMiddleLeft.png);}
-.cboxIE6 #cboxMiddleRight{background:url(images/ie6/borderMiddleRight.png);}
-
-.cboxIE6 #cboxTopLeft,
-.cboxIE6 #cboxTopCenter,
-.cboxIE6 #cboxTopRight,
-.cboxIE6 #cboxBottomLeft,
-.cboxIE6 #cboxBottomCenter,
-.cboxIE6 #cboxBottomRight,
-.cboxIE6 #cboxMiddleLeft,
-.cboxIE6 #cboxMiddleRight {
- _behavior: expression(this.src = this.src ? this.src : this.currentStyle.backgroundImage.split('"')[1], this.style.background = "none", this.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src=" + this.src + ", sizingMethod='scale')");
-}
+/*
+ Colorbox Core Style:
+ The following CSS is consistent between example themes and should not be altered.
+*/
+#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
+#cboxOverlay{position:fixed; width:100%; height:100%;}
+#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
+#cboxContent{position:relative;}
+#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;}
+#cboxTitle{margin:0;}
+#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
+#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
+.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none; -ms-interpolation-mode:bicubic;}
+.cboxIframe{width:100%; height:100%; display:block; border:0;}
+#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;}
+
+/*
+ User Style:
+ Change the following styles to modify the appearance of Colorbox. They are
+ ordered & tabbed in a way that represents the nesting of the generated HTML.
+*/
+#cboxOverlay{background:url(images/overlay.png) repeat 0 0;}
+#colorbox{outline:0;}
+ #cboxTopLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px 0;}
+ #cboxTopRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px 0;}
+ #cboxBottomLeft{width:21px; height:21px; background:url(images/controls.png) no-repeat -101px -29px;}
+ #cboxBottomRight{width:21px; height:21px; background:url(images/controls.png) no-repeat -130px -29px;}
+ #cboxMiddleLeft{width:21px; background:url(images/controls.png) left top repeat-y;}
+ #cboxMiddleRight{width:21px; background:url(images/controls.png) right top repeat-y;}
+ #cboxTopCenter{height:21px; background:url(images/border.png) 0 0 repeat-x;}
+ #cboxBottomCenter{height:21px; background:url(images/border.png) 0 -29px repeat-x;}
+ #cboxContent{background:#fff; overflow:hidden;}
+ .cboxIframe{background:#fff;}
+ #cboxError{padding:50px; border:1px solid #ccc;}
+ #cboxLoadedContent{margin-bottom:28px;}
+ #cboxTitle{position:absolute; bottom:4px; right: 29px; text-align: right; width:100%; color:#949494;}
+ #cboxCurrent{position:absolute; bottom:4px; left:58px; color:#949494;}
+ #cboxLoadingOverlay{background:url(images/loading_background.png) no-repeat center center;}
+ #cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;}
+
+ /* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */
+ #cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; width:auto; background:none; }
+
+ /* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */
+ #cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;}
+
+ #cboxSlideshow{position:absolute; bottom:4px; right:30px; color:#0092ef;}
+ #cboxPrevious{position:absolute; bottom:0; left:0; background:url(images/controls.png) no-repeat -75px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxPrevious:hover{background-position:-75px -25px;}
+ #cboxNext{position:absolute; bottom:0; left:27px; background:url(images/controls.png) no-repeat -50px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxNext:hover{background-position:-50px -25px;}
+ #cboxClose{position:absolute; bottom:0; right:0; background:url(images/controls.png) no-repeat -25px 0; width:25px; height:25px; text-indent:-9999px;}
+ #cboxClose:hover{background-position:-25px -25px;}
+
+/*
+ The following fixes a problem where IE7 and IE8 replace a PNG's alpha transparency with a black fill
+ when an alpha filter (opacity change) is set on the element or ancestor element. This style is not applied to or needed in IE9.
+ See: http://jacklmoore.com/notes/ie-transparency-problems/
+*/
+.cboxIE #cboxTopLeft,
+.cboxIE #cboxTopCenter,
+.cboxIE #cboxTopRight,
+.cboxIE #cboxBottomLeft,
+.cboxIE #cboxBottomCenter,
+.cboxIE #cboxBottomRight,
+.cboxIE #cboxMiddleLeft,
+.cboxIE #cboxMiddleRight {
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#00FFFFFF,endColorstr=#00FFFFFF);
+}
diff --git a/nikola/data/themes/default/assets/css/images/border.png b/nikola/data/themes/bootstrap/assets/css/images/border.png
index f463a10..f463a10 100644
--- a/nikola/data/themes/default/assets/css/images/border.png
+++ b/nikola/data/themes/bootstrap/assets/css/images/border.png
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/controls.png b/nikola/data/themes/bootstrap/assets/css/images/controls.png
index dcfd6fb..dcfd6fb 100644
--- a/nikola/data/themes/default/assets/css/images/controls.png
+++ b/nikola/data/themes/bootstrap/assets/css/images/controls.png
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/loading.gif b/nikola/data/themes/bootstrap/assets/css/images/loading.gif
index b4695d8..b4695d8 100644
--- a/nikola/data/themes/default/assets/css/images/loading.gif
+++ b/nikola/data/themes/bootstrap/assets/css/images/loading.gif
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/loading_background.png b/nikola/data/themes/bootstrap/assets/css/images/loading_background.png
index 6ae83e6..6ae83e6 100644
--- a/nikola/data/themes/default/assets/css/images/loading_background.png
+++ b/nikola/data/themes/bootstrap/assets/css/images/loading_background.png
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/overlay.png b/nikola/data/themes/bootstrap/assets/css/images/overlay.png
index 53ea98f..53ea98f 100644
--- a/nikola/data/themes/default/assets/css/images/overlay.png
+++ b/nikola/data/themes/bootstrap/assets/css/images/overlay.png
Binary files differ
diff --git a/nikola/data/themes/site/assets/css/theme.css b/nikola/data/themes/bootstrap/assets/css/theme.css
index 24072ac..2ff1a80 100644
--- a/nikola/data/themes/site/assets/css/theme.css
+++ b/nikola/data/themes/bootstrap/assets/css/theme.css
@@ -43,8 +43,10 @@ td.label {
.caption {
/* Issue 292 */
text-align: center;
+ padding-top: 1em;
}
+div.figure > img,
div.figure > a > img {
/* Issue 292 */
display: block;
@@ -74,7 +76,12 @@ ul.bricks > li {
margin: 3px;
}
-h1, h2, h3, h4, h5, h6, h7 {
- margin-top: -40px;
- padding-top: 40px;
+ul.breadcrumb > li:before {
+ content: " / ";
}
+
+pre, pre code {
+ white-space: pre;
+ word-wrap: normal;
+ overflow: auto;
+} \ No newline at end of file
diff --git a/nikola/data/themes/bootstrap/assets/js/flowr.plugin.js b/nikola/data/themes/bootstrap/assets/js/flowr.plugin.js
new file mode 100644
index 0000000..c0d986b
--- /dev/null
+++ b/nikola/data/themes/bootstrap/assets/js/flowr.plugin.js
@@ -0,0 +1,265 @@
+/**
+ * Flowr.js - Simple jQuery plugin to emulate Flickr's justified view
+ * For usage information refer to http://github.com/kalyan02/flowr-js
+ *
+ *
+ * @author: Kalyan Chakravarthy (http://KalyanChakravarthy.net)
+ * @version: v0.1
+ */
+(function($){
+ //$("#container2").css( 'border', '1px solid #ccc');
+ $.fn.flowr = function(options) {
+
+ $this = this;
+ var ROW_CLASS_NAME = 'flowr-row'; // Class name for the row of flowy
+ var MAX_LAST_ROW_GAP = 25; // If the width of last row is lesser than max-width, recalculation is needed
+ var NO_COPY_FIELDS = [ 'complete', 'data', 'responsive' ]; // these attributes will not be carried forward for append related calls
+ var DEFAULTS = {
+ 'data' : [],
+ 'padding' : 5, // whats the padding between flowy items
+ 'height' : 240, // Minimum height an image row should take
+ 'render' : null, // callback function to get the tag
+ 'append' : false, // TODO
+ 'widthAttr' : 'width', // a custom data structure can specify which attribute refers to height/width
+ 'heightAttr' : 'height',
+ 'maxScale' : 1.5, // In case there is only 1 elment in last row
+ 'maxWidth' : this.width()-1, // 1px is just for offset
+ 'itemWidth' : null, // callback function for width
+ 'itemHeight' : null, // callback function for height
+ 'complete' : null, // complete callback
+ 'rowClassName' : ROW_CLASS_NAME,
+ 'rows' : -1, // Maximum number of rows to render. -1 for no limit.
+ 'responsive' : true // make content responsive
+ };
+ var settings = $.extend( DEFAULTS, options);
+
+ // If data is being appended, we already have settings
+ // If we already have settings, retrieve them
+ if( settings.append && $this.data('lastSettings') ) {
+ lastSettings = $this.data('lastSettings');
+
+ // Copy over the settings from previous init
+ for( attr in DEFAULTS ) {
+ if( NO_COPY_FIELDS.indexOf(attr)<0 && settings[attr] == DEFAULTS[attr] ) {
+ settings[attr] = lastSettings[attr];
+ }
+ }
+
+ // Check if we have an incomplete last row
+ lastRow = $this.data('lastRow');
+ if( lastRow.data.length > 0 && settings.maxWidth-lastRow.width > MAX_LAST_ROW_GAP ) {
+ // Prepend the incomplete row to newly loaded data and redraw
+ lastRowData = lastSettings.data.slice( lastSettings.data.length - lastRow.data.length - 1 );
+ settings.data = lastRowData.concat(settings.data);
+
+ // Remove the incomplete row
+ // TODO: Don't reload this stuff later. Reattach to new row.
+ $( '.' + settings.rowClassName + ':last', $this ).detach();
+ } else {
+ // console.log( lastRow.data.length );
+ // console.log( lastRow.width );
+ }
+ }
+
+ // only on the first initial call
+ if( !settings.responsive && !settings.append )
+ $this.width( $this.width() );
+
+ // Basic sanity checks
+ if( !(settings.data instanceof Array) )
+ return;
+
+ if( typeof(settings.padding) != 'number' )
+ settings.padding = parseInt( settings.padding );
+
+ if( typeof(settings.itemWidth) != 'function' ) {
+ settings.itemWidth = function(data) {
+ return data[ settings.widthAttr ];
+ }
+ }
+
+ if( typeof(settings.itemHeight) != 'function' ) {
+ settings.itemHeight = function(data) {
+ return data[ settings.heightAttr ];
+ }
+ }
+
+ // A standalone utility to calculate the item widths for a particular row
+ // Returns rowWidth: width occupied & data : the items in the new row
+ var utils = {
+ getNextRow : function( data, settings ) {
+ var itemIndex = 0;
+ var itemsLength = data.length;
+ var lineItems = [];
+ var lineWidth = 0;
+ var maxWidth = settings.maxWidth;
+ var paddingSize = settings.padding;
+
+ // console.log( 'maxItems=' + data.length );
+
+ requiredPadding = function() {
+ var extraPads = arguments.length == 1 ? arguments[0] : 0;
+ return (lineItems.length - 1 + extraPads) * settings.padding;
+ }
+
+ while( lineWidth + requiredPadding() < settings.maxWidth && (itemIndex < itemsLength) ) {
+ var itemData = data[ itemIndex ];
+ var itemWidth = settings.itemWidth.call( $this, itemData );
+ var itemHeight = settings.itemHeight.call( $this, itemData );
+
+ var minHeight = settings.height;
+ var minWidth = Math.floor( itemWidth * settings.height / itemHeight );
+
+ var newLineWidth = lineWidth + minWidth + requiredPadding(1);
+
+ if (minWidth > settings.maxWidth) {
+ // very short+wide images like panoramas
+ // show them even if ugly, as wide as possible
+ minWidth = settings.maxWidth-1;
+ minHeight = settings.height * minHeight / minWidth;
+ }
+
+ // console.log( 'lineWidth = ' + lineWidth );
+ // console.log( 'newLineWidth = ' + newLineWidth );
+ if( newLineWidth < settings.maxWidth ) {
+ lineItems.push({
+ 'height' : minHeight,
+ 'width' : minWidth,
+ 'itemData' : itemData
+ });
+
+ lineWidth += minWidth;
+ itemIndex ++;
+ } else {
+ // We'd have exceeded width. So break off to scale.
+ // console.log( 'breaking off = ' + itemIndex );
+ // console.log( 'leave off size = ' + lineItems.length );
+ break;
+ }
+ } //while
+
+ // Scale the size to max width
+ testWidth=0;
+ if( lineWidth < settings.maxWidth ) {
+ var fullScaleWidth = settings.maxWidth - requiredPadding() - 10;
+ var currScaleWidth = lineWidth;
+ var scaleFactor = fullScaleWidth / currScaleWidth;
+ if( scaleFactor > settings.maxScale )
+ scaleFactor = 1;
+
+ var newHeight = Math.round( settings.height * scaleFactor );
+ for( i=0; i<lineItems.length; i++ ) {
+ var lineItem = lineItems[ i ];
+ lineItem.width = Math.floor(lineItem.width * scaleFactor);
+ lineItem.height = newHeight;
+
+ testWidth += lineItem.width;
+ }
+ }
+
+ return {
+ data : lineItems,
+ width : testWidth + requiredPadding()
+ };
+ }, //getNextRow
+ reorderContent : function(){
+ /*
+ TODO: optimize for faster resizing by reusing dom objects instead of killing the dom
+ */
+ var _initialWidth = $this.data('width');
+ var _newWidth = $this.width();
+ var _change = _initialWidth - _newWidth;
+
+ if(_initialWidth!=_newWidth) {
+ $this.html('');
+ var _settings = $this.data( 'lastSettings' );
+ _settings.data = $this.data( 'data' );
+ _settings.maxWidth = $this.width() - 1;
+ $this.flowr( _settings );
+ }
+ }
+ } //utils
+
+ // If the resposive var is set to true then listen for resize method
+ // and prevent resizing from happening twice if responsive is set again during append phase!
+ if( settings.responsive && !$this.data('__responsive') ) {
+ $(window).resize(function(){
+ initialWidth = $this.data('width');
+ newWidth = $this.width();
+
+ //initiate resize
+ if( initialWidth != newWidth ) {
+ var task_id = $this.data('task_id');
+ if( task_id ) {
+ task_id = clearTimeout( task_id );
+ task_id = null;
+ }
+ task_id = setTimeout( utils.reorderContent, 80 );
+ $this.data('task_id', task_id );
+ }
+ });
+ $this.data('__responsive',true);
+ }
+
+
+ return this.each(function(){
+
+ // Get a copy of original data. 1 level deep copy is sufficient.
+ var data = settings.data.slice(0);
+ var rowData = null;
+ var currentRow = 0;
+ var currentItem = 0;
+
+ // Store all the data
+ var allData = $this.data( 'data' ) || [];
+ for(i=0;i<data.length;i++) {
+ allData.push( data[i] );
+ }
+ $this.data( 'data', allData );
+
+ // While we have a new row
+ while( ( rowData = utils.getNextRow(data,settings) ) != null && rowData.data.length > 0 ) {
+ if( settings.rows > 0 && currentRow >= settings.rows )
+ break;
+ // remove the number of elements in the new row from the top of data stack
+ data.splice( 0, rowData.data.length );
+
+ // Create a new row div, add class, append the htmls and insert the flowy items
+ var $row = $('<div>').addClass(settings.rowClassName);
+ for( i=0; i<rowData.data.length; i++ ) {
+ var displayData = rowData.data[i];
+ // Get the HTML object from custom render function passed as argument
+ var displayObject = settings.render.call( $this, displayData );
+ displayObject = $(displayObject);
+ // Set some basic stuff
+ displayObject
+ .css( 'width', displayData.width )
+ .css( 'height', displayData.height )
+ .css( 'margin-left', i==0 ? '0' : settings.padding + "px" ); //TODO:Refactor
+ $row.append( displayObject );
+
+ currentItem++;
+ }
+ $this.append( $row );
+ // console.log ( "I> rowData.data.length="+rowData.data.length +" rowData.width="+rowData.width );
+
+ currentRow++;
+ $this.data('lastRow', rowData );
+ }
+ // store the current state of settings and the items in last row
+ // we'll need this info when we append more items
+ $this.data('lastSettings', settings );
+
+ // onComplete callback
+ // pass back info about list of rows and items rendered
+ if( typeof (settings.complete) == 'function' ) {
+ var completeData = {
+ renderedRows : currentRow,
+ renderedItems : currentItem
+ }
+ settings.complete.call( $this, completeData );
+ }
+ });
+ };
+
+})(jQuery);
diff --git a/nikola/data/themes/default/bundles b/nikola/data/themes/bootstrap/bundles
index 2b81ea8..14124a3 100644
--- a/nikola/data/themes/default/bundles
+++ b/nikola/data/themes/bootstrap/bundles
@@ -1,4 +1,4 @@
assets/css/all-nocdn.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,theme.css,custom.css
assets/css/all.css=rst.css,code.css,colorbox.css,theme.css,custom.css
-assets/js/all-nocdn.js=jquery-1.7.2.min.js,bootstrap.min.js,jquery.colorbox-min.js
+assets/js/all-nocdn.js=jquery-1.10.2.min.js,bootstrap.min.js,jquery.colorbox-min.js
assets/js/all.js=jquery.colorbox-min.js
diff --git a/nikola/data/themes/site-planetoid/engine b/nikola/data/themes/bootstrap/engine
index 2951cdd..2951cdd 100644
--- a/nikola/data/themes/site-planetoid/engine
+++ b/nikola/data/themes/bootstrap/engine
diff --git a/nikola/data/themes/bootstrap/parent b/nikola/data/themes/bootstrap/parent
new file mode 100644
index 0000000..df967b9
--- /dev/null
+++ b/nikola/data/themes/bootstrap/parent
@@ -0,0 +1 @@
+base
diff --git a/nikola/data/themes/site/templates/base.tmpl b/nikola/data/themes/bootstrap/templates/base.tmpl
index 4efd0ad..8cb2e43 100644
--- a/nikola/data/themes/site/templates/base.tmpl
+++ b/nikola/data/themes/bootstrap/templates/base.tmpl
@@ -1,18 +1,29 @@
## -*- coding: utf-8 -*-
-<%namespace file="base_helper.tmpl" import="*"/>
+<%namespace name="base" file="base_helper.tmpl" import="*" />
+<%namespace name="bootstrap" file="bootstrap_helper.tmpl" import="*" />
+<%namespace name="notes" file="annotation_helper.tmpl" import="*" />
${set_locale(lang)}
<!DOCTYPE html>
-<html lang="${lang}">
+<html
+%if comment_system == 'facebook':
+xmlns:fb="http://ogp.me/ns/fb#"
+%endif
+lang="${lang}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- ${html_head()}
+ ${bootstrap.html_head()}
<%block name="extra_head">
</%block>
+ % if annotations and post and not post.meta('noannotations'):
+ ${notes.css()}
+ % elif not annotations and post and post.meta('annotations'):
+ ${notes.css()}
+ % endif
${extra_head_data}
</head>
<body>
<!-- Menubar -->
-<div class="navbar navbar-fixed-top">
+<div class="navbar navbar-fixed-top" id="navbar">
<div class="navbar-inner">
<div class="container">
@@ -29,7 +40,7 @@ ${set_locale(lang)}
<!-- Everything you want hidden at 940px or less, place within here -->
<div class="nav-collapse collapse">
<ul class="nav">
- ${html_sidebar_links()}
+ ${bootstrap.html_navigation_links()}
</ul>
%if search_form:
${search_form}
@@ -37,10 +48,12 @@ ${set_locale(lang)}
<ul class="nav pull-right">
<%block name="belowtitle">
%if len(translations) > 1:
- <li>${html_translations()}</li>
+ <li>${base.html_translations()}</li>
%endif
</%block>
- <%block name="sourcelink"> </%block>
+ % if not hide_sourcelink:
+ <li><%block name="sourcelink"></%block></li>
+ %endif
</ul>
</div>
</div>
@@ -60,8 +73,21 @@ ${set_locale(lang)}
<div class="footerbox">
${content_footer}
</div>
-${html_social()}
-${late_load_js()}
-${analytics}
- <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
+${bootstrap.late_load_js()}
+${base.html_social()}
+ <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"100%",maxHeight:"100%",scalePhotos:true});
+ $(window).on('hashchange', function(){
+ if (location.hash && $(location.hash)[0]) {
+ $('body').animate({scrollTop: $(location.hash).offset().top - $('#navbar').outerHeight(true)*1.2 }, 1);
+ }
+ });
+ $(document).ready(function(){$(window).trigger('hashchange')});
+ </script>
+ <%block name="extra_js"></%block>
+ % if annotations and post and not post.meta('noannotations'):
+ ${notes.code()}
+ % elif not annotations and post and post.meta('annotations'):
+ ${notes.code()}
+ % endif
+ ${body_end}
</body>
diff --git a/nikola/data/themes/default/templates/base_helper.tmpl b/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl
index a833c51..763ac20 100644
--- a/nikola/data/themes/default/templates/base_helper.tmpl
+++ b/nikola/data/themes/bootstrap/templates/bootstrap_helper.tmpl
@@ -1,9 +1,11 @@
-## -*- coding: utf-8 -*-
+## Override only the functions that differ from base_helper.tmpl
<%def name="html_head()">
<meta charset="utf-8">
- <meta name="description" content="${description}" >
+ %if description:
+ <meta name="description" content="${description}">
+ %endif
<meta name="author" content="${blog_author}">
- <title>${title} | ${blog_title}</title>
+ <title>${title|striphtml} | ${blog_title|striphtml}</title>
${mathjax_config}
%if use_bundles:
%if use_cdn:
@@ -51,7 +53,7 @@
<%def name="late_load_js()">
%if use_bundles:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" type="text/javascript"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
<script src="/assets/js/all.js" type="text/javascript"></script>
%else:
@@ -59,48 +61,36 @@
%endif
%else:
%if use_cdn:
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js" type="text/javascript"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
%else:
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script>
+ <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script>
<script src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
%endif
<script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
%endif
</%def>
-<%def name="html_social()">
-%if add_this_buttons:
- <!-- Social buttons -->
- <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
- <a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a>
- <li><a class="addthis_button_google_plusone_share"></a>
- <li><a class="addthis_button_linkedin"></a>
- <li><a class="addthis_button_twitter"></a>
- </ul>
- </div>
- <script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
- <!-- End of social buttons -->
-%endif
-</%def>
-
-<%def name="html_sidebar_links()">
- %for url, text in sidebar_links[lang]:
- % if rel_link(permalink, url) == "#":
- <li class="active"><a href="${url}">${text}</a>
- %else:
- <li><a href="${url}">${text}</a>
- %endif
- %endfor
-</%def>
-
-
-<%def name="html_translations()">
- %for langname in translations.keys():
- %if langname != lang:
- <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
- %endif
+<%def name="html_navigation_links()">
+ %for url, text in navigation_links[lang]:
+ % if isinstance(url, tuple):
+ <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">${text}<b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ %for suburl, text in url:
+ % if rel_link(permalink, suburl) == "#":
+ <li class="active"><a href="${suburl}">${text}</a>
+ %else:
+ <li><a href="${suburl}">${text}</a>
+ %endif
+ %endfor
+ </ul>
+ % else:
+ % if rel_link(permalink, url) == "#":
+ <li class="active"><a href="${url}">${text}</a>
+ %else:
+ <li><a href="${url}">${text}</a>
+ %endif
+ % endif
%endfor
</%def>
diff --git a/nikola/data/themes/bootstrap/templates/gallery.tmpl b/nikola/data/themes/bootstrap/templates/gallery.tmpl
new file mode 100644
index 0000000..17ae61c
--- /dev/null
+++ b/nikola/data/themes/bootstrap/templates/gallery.tmpl
@@ -0,0 +1,86 @@
+## -*- 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 text:
+ <p>
+ ${text}
+ </p>
+ %endif
+ <ul>
+ % for folder in folders:
+ <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
+ % endfor
+ </ul>
+
+ <div id="gallery_container"></div>
+ <noscript>
+ <ul class="thumbnails">
+ %for image in photo_array:
+ <li><a href="${image['url']}" class="thumbnail image-reference" title="${image['title']}">
+ <img src="${image['url_thumb']}" alt="${image['title']}" /></a>
+ %endfor
+ </ul>
+ </noscript>
+%if enable_comments:
+ ${comments.comment_form(None, permalink, title)}
+%endif
+</%block>
+
+
+<%block name="extra_head">
+<style type="text/css">
+ .image-block {
+ display: inline-block;
+ }
+ .flowr_row {
+ width: 100%;
+ }
+ </style>
+</%block>
+
+
+<%block name="extra_js">
+<script src="/assets/js/flowr.plugin.js"></script>
+<script>
+jsonContent = ${photo_array_json};
+$("#gallery_container").flowr({
+ data : jsonContent,
+ height : ${thumbnail_size}*.6,
+ padding: 5,
+ rows: -1,
+ render : function(params) {
+ // Just return a div, string or a dom object, anything works fine
+ img = $("<img />").attr({
+ 'src': params.itemData.url_thumb,
+ 'width' : params.width,
+ 'height' : params.height
+ }).css('max-width', '100%');
+ link = $( "<a></a>").attr({
+ 'href': params.itemData.url,
+ 'class': 'image-reference'
+ });
+ div = $("<div />").addClass('image-block').attr({
+ 'title': params.itemData.title,
+ 'data-toggle': "tooltip",
+ });
+ link.append(img);
+ div.append(link);
+ div.hover(div.tooltip());
+ return div;
+ },
+ itemWidth : function(data) { return data.size.w; },
+ itemHeight : function(data) { return data.size.h; },
+ complete : function(params) {
+ if( jsonContent.length > params.renderedItems ) {
+ nextRenderList = jsonContent.slice( params.renderedItems );
+ }
+ }
+ });
+$("a.image-reference").colorbox({rel:"gal", maxWidth:"100%",maxHeight:"100%",scalePhotos:true});
+</script>
+</%block>
diff --git a/nikola/data/themes/bootstrap/templates/slides.tmpl b/nikola/data/themes/bootstrap/templates/slides.tmpl
new file mode 100644
index 0000000..14983ad
--- /dev/null
+++ b/nikola/data/themes/bootstrap/templates/slides.tmpl
@@ -0,0 +1,22 @@
+<div id="${carousel_id}" class="carousel slide">
+ <ol class="carousel-indicators">
+ % for i in range(len(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(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>
diff --git a/nikola/data/themes/default/assets/css/bootstrap-responsive.css b/nikola/data/themes/default/assets/css/bootstrap-responsive.css
deleted file mode 100644
index 5215a5d..0000000
--- a/nikola/data/themes/default/assets/css/bootstrap-responsive.css
+++ /dev/null
@@ -1,1109 +0,0 @@
-/*!
- * Bootstrap Responsive v2.3.0
- *
- * Copyright 2012 Twitter, Inc
- * Licensed under the Apache License v2.0
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Designed and built with all the love in the world @twitter by @mdo and @fat.
- */
-
-.clearfix {
- *zoom: 1;
-}
-
-.clearfix:before,
-.clearfix:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.clearfix:after {
- clear: both;
-}
-
-.hide-text {
- font: 0/0 a;
- color: transparent;
- text-shadow: none;
- background-color: transparent;
- border: 0;
-}
-
-.input-block-level {
- display: block;
- width: 100%;
- min-height: 30px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-@-ms-viewport {
- width: device-width;
-}
-
-.hidden {
- display: none;
- visibility: hidden;
-}
-
-.visible-phone {
- display: none !important;
-}
-
-.visible-tablet {
- display: none !important;
-}
-
-.hidden-desktop {
- display: none !important;
-}
-
-.visible-desktop {
- display: inherit !important;
-}
-
-@media (min-width: 768px) and (max-width: 979px) {
- .hidden-desktop {
- display: inherit !important;
- }
- .visible-desktop {
- display: none !important ;
- }
- .visible-tablet {
- display: inherit !important;
- }
- .hidden-tablet {
- display: none !important;
- }
-}
-
-@media (max-width: 767px) {
- .hidden-desktop {
- display: inherit !important;
- }
- .visible-desktop {
- display: none !important;
- }
- .visible-phone {
- display: inherit !important;
- }
- .hidden-phone {
- display: none !important;
- }
-}
-
-.visible-print {
- display: none !important;
-}
-
-@media print {
- .visible-print {
- display: inherit !important;
- }
- .hidden-print {
- display: none !important;
- }
-}
-
-@media (min-width: 1200px) {
- .row {
- margin-left: -30px;
- *zoom: 1;
- }
- .row:before,
- .row:after {
- display: table;
- line-height: 0;
- content: "";
- }
- .row:after {
- clear: both;
- }
- [class*="span"] {
- float: left;
- min-height: 1px;
- margin-left: 30px;
- }
- .container,
- .navbar-static-top .container,
- .navbar-fixed-top .container,
- .navbar-fixed-bottom .container {
- width: 1170px;
- }
- .span12 {
- width: 1170px;
- }
- .span11 {
- width: 1070px;
- }
- .span10 {
- width: 970px;
- }
- .span9 {
- width: 870px;
- }
- .span8 {
- width: 770px;
- }
- .span7 {
- width: 670px;
- }
- .span6 {
- width: 570px;
- }
- .span5 {
- width: 470px;
- }
- .span4 {
- width: 370px;
- }
- .span3 {
- width: 270px;
- }
- .span2 {
- width: 170px;
- }
- .span1 {
- width: 70px;
- }
- .offset12 {
- margin-left: 1230px;
- }
- .offset11 {
- margin-left: 1130px;
- }
- .offset10 {
- margin-left: 1030px;
- }
- .offset9 {
- margin-left: 930px;
- }
- .offset8 {
- margin-left: 830px;
- }
- .offset7 {
- margin-left: 730px;
- }
- .offset6 {
- margin-left: 630px;
- }
- .offset5 {
- margin-left: 530px;
- }
- .offset4 {
- margin-left: 430px;
- }
- .offset3 {
- margin-left: 330px;
- }
- .offset2 {
- margin-left: 230px;
- }
- .offset1 {
- margin-left: 130px;
- }
- .row-fluid {
- width: 100%;
- *zoom: 1;
- }
- .row-fluid:before,
- .row-fluid:after {
- display: table;
- line-height: 0;
- content: "";
- }
- .row-fluid:after {
- clear: both;
- }
- .row-fluid [class*="span"] {
- display: block;
- float: left;
- width: 100%;
- min-height: 30px;
- margin-left: 2.564102564102564%;
- *margin-left: 2.5109110747408616%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- }
- .row-fluid [class*="span"]:first-child {
- margin-left: 0;
- }
- .row-fluid .controls-row [class*="span"] + [class*="span"] {
- margin-left: 2.564102564102564%;
- }
- .row-fluid .span12 {
- width: 100%;
- *width: 99.94680851063829%;
- }
- .row-fluid .span11 {
- width: 91.45299145299145%;
- *width: 91.39979996362975%;
- }
- .row-fluid .span10 {
- width: 82.90598290598291%;
- *width: 82.8527914166212%;
- }
- .row-fluid .span9 {
- width: 74.35897435897436%;
- *width: 74.30578286961266%;
- }
- .row-fluid .span8 {
- width: 65.81196581196582%;
- *width: 65.75877432260411%;
- }
- .row-fluid .span7 {
- width: 57.26495726495726%;
- *width: 57.21176577559556%;
- }
- .row-fluid .span6 {
- width: 48.717948717948715%;
- *width: 48.664757228587014%;
- }
- .row-fluid .span5 {
- width: 40.17094017094017%;
- *width: 40.11774868157847%;
- }
- .row-fluid .span4 {
- width: 31.623931623931625%;
- *width: 31.570740134569924%;
- }
- .row-fluid .span3 {
- width: 23.076923076923077%;
- *width: 23.023731587561375%;
- }
- .row-fluid .span2 {
- width: 14.52991452991453%;
- *width: 14.476723040552828%;
- }
- .row-fluid .span1 {
- width: 5.982905982905983%;
- *width: 5.929714493544281%;
- }
- .row-fluid .offset12 {
- margin-left: 105.12820512820512%;
- *margin-left: 105.02182214948171%;
- }
- .row-fluid .offset12:first-child {
- margin-left: 102.56410256410257%;
- *margin-left: 102.45771958537915%;
- }
- .row-fluid .offset11 {
- margin-left: 96.58119658119658%;
- *margin-left: 96.47481360247316%;
- }
- .row-fluid .offset11:first-child {
- margin-left: 94.01709401709402%;
- *margin-left: 93.91071103837061%;
- }
- .row-fluid .offset10 {
- margin-left: 88.03418803418803%;
- *margin-left: 87.92780505546462%;
- }
- .row-fluid .offset10:first-child {
- margin-left: 85.47008547008548%;
- *margin-left: 85.36370249136206%;
- }
- .row-fluid .offset9 {
- margin-left: 79.48717948717949%;
- *margin-left: 79.38079650845607%;
- }
- .row-fluid .offset9:first-child {
- margin-left: 76.92307692307693%;
- *margin-left: 76.81669394435352%;
- }
- .row-fluid .offset8 {
- margin-left: 70.94017094017094%;
- *margin-left: 70.83378796144753%;
- }
- .row-fluid .offset8:first-child {
- margin-left: 68.37606837606839%;
- *margin-left: 68.26968539734497%;
- }
- .row-fluid .offset7 {
- margin-left: 62.393162393162385%;
- *margin-left: 62.28677941443899%;
- }
- .row-fluid .offset7:first-child {
- margin-left: 59.82905982905982%;
- *margin-left: 59.72267685033642%;
- }
- .row-fluid .offset6 {
- margin-left: 53.84615384615384%;
- *margin-left: 53.739770867430444%;
- }
- .row-fluid .offset6:first-child {
- margin-left: 51.28205128205128%;
- *margin-left: 51.175668303327875%;
- }
- .row-fluid .offset5 {
- margin-left: 45.299145299145295%;
- *margin-left: 45.1927623204219%;
- }
- .row-fluid .offset5:first-child {
- margin-left: 42.73504273504273%;
- *margin-left: 42.62865975631933%;
- }
- .row-fluid .offset4 {
- margin-left: 36.75213675213675%;
- *margin-left: 36.645753773413354%;
- }
- .row-fluid .offset4:first-child {
- margin-left: 34.18803418803419%;
- *margin-left: 34.081651209310785%;
- }
- .row-fluid .offset3 {
- margin-left: 28.205128205128204%;
- *margin-left: 28.0987452264048%;
- }
- .row-fluid .offset3:first-child {
- margin-left: 25.641025641025642%;
- *margin-left: 25.53464266230224%;
- }
- .row-fluid .offset2 {
- margin-left: 19.65811965811966%;
- *margin-left: 19.551736679396257%;
- }
- .row-fluid .offset2:first-child {
- margin-left: 17.094017094017094%;
- *margin-left: 16.98763411529369%;
- }
- .row-fluid .offset1 {
- margin-left: 11.11111111111111%;
- *margin-left: 11.004728132387708%;
- }
- .row-fluid .offset1:first-child {
- margin-left: 8.547008547008547%;
- *margin-left: 8.440625568285142%;
- }
- input,
- textarea,
- .uneditable-input {
- margin-left: 0;
- }
- .controls-row [class*="span"] + [class*="span"] {
- margin-left: 30px;
- }
- input.span12,
- textarea.span12,
- .uneditable-input.span12 {
- width: 1156px;
- }
- input.span11,
- textarea.span11,
- .uneditable-input.span11 {
- width: 1056px;
- }
- input.span10,
- textarea.span10,
- .uneditable-input.span10 {
- width: 956px;
- }
- input.span9,
- textarea.span9,
- .uneditable-input.span9 {
- width: 856px;
- }
- input.span8,
- textarea.span8,
- .uneditable-input.span8 {
- width: 756px;
- }
- input.span7,
- textarea.span7,
- .uneditable-input.span7 {
- width: 656px;
- }
- input.span6,
- textarea.span6,
- .uneditable-input.span6 {
- width: 556px;
- }
- input.span5,
- textarea.span5,
- .uneditable-input.span5 {
- width: 456px;
- }
- input.span4,
- textarea.span4,
- .uneditable-input.span4 {
- width: 356px;
- }
- input.span3,
- textarea.span3,
- .uneditable-input.span3 {
- width: 256px;
- }
- input.span2,
- textarea.span2,
- .uneditable-input.span2 {
- width: 156px;
- }
- input.span1,
- textarea.span1,
- .uneditable-input.span1 {
- width: 56px;
- }
- .thumbnails {
- margin-left: -30px;
- }
- .thumbnails > li {
- margin-left: 30px;
- }
- .row-fluid .thumbnails {
- margin-left: 0;
- }
-}
-
-@media (min-width: 768px) and (max-width: 979px) {
- .row {
- margin-left: -20px;
- *zoom: 1;
- }
- .row:before,
- .row:after {
- display: table;
- line-height: 0;
- content: "";
- }
- .row:after {
- clear: both;
- }
- [class*="span"] {
- float: left;
- min-height: 1px;
- margin-left: 20px;
- }
- .container,
- .navbar-static-top .container,
- .navbar-fixed-top .container,
- .navbar-fixed-bottom .container {
- width: 724px;
- }
- .span12 {
- width: 724px;
- }
- .span11 {
- width: 662px;
- }
- .span10 {
- width: 600px;
- }
- .span9 {
- width: 538px;
- }
- .span8 {
- width: 476px;
- }
- .span7 {
- width: 414px;
- }
- .span6 {
- width: 352px;
- }
- .span5 {
- width: 290px;
- }
- .span4 {
- width: 228px;
- }
- .span3 {
- width: 166px;
- }
- .span2 {
- width: 104px;
- }
- .span1 {
- width: 42px;
- }
- .offset12 {
- margin-left: 764px;
- }
- .offset11 {
- margin-left: 702px;
- }
- .offset10 {
- margin-left: 640px;
- }
- .offset9 {
- margin-left: 578px;
- }
- .offset8 {
- margin-left: 516px;
- }
- .offset7 {
- margin-left: 454px;
- }
- .offset6 {
- margin-left: 392px;
- }
- .offset5 {
- margin-left: 330px;
- }
- .offset4 {
- margin-left: 268px;
- }
- .offset3 {
- margin-left: 206px;
- }
- .offset2 {
- margin-left: 144px;
- }
- .offset1 {
- margin-left: 82px;
- }
- .row-fluid {
- width: 100%;
- *zoom: 1;
- }
- .row-fluid:before,
- .row-fluid:after {
- display: table;
- line-height: 0;
- content: "";
- }
- .row-fluid:after {
- clear: both;
- }
- .row-fluid [class*="span"] {
- display: block;
- float: left;
- width: 100%;
- min-height: 30px;
- margin-left: 2.7624309392265194%;
- *margin-left: 2.709239449864817%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- }
- .row-fluid [class*="span"]:first-child {
- margin-left: 0;
- }
- .row-fluid .controls-row [class*="span"] + [class*="span"] {
- margin-left: 2.7624309392265194%;
- }
- .row-fluid .span12 {
- width: 100%;
- *width: 99.94680851063829%;
- }
- .row-fluid .span11 {
- width: 91.43646408839778%;
- *width: 91.38327259903608%;
- }
- .row-fluid .span10 {
- width: 82.87292817679558%;
- *width: 82.81973668743387%;
- }
- .row-fluid .span9 {
- width: 74.30939226519337%;
- *width: 74.25620077583166%;
- }
- .row-fluid .span8 {
- width: 65.74585635359117%;
- *width: 65.69266486422946%;
- }
- .row-fluid .span7 {
- width: 57.18232044198895%;
- *width: 57.12912895262725%;
- }
- .row-fluid .span6 {
- width: 48.61878453038674%;
- *width: 48.56559304102504%;
- }
- .row-fluid .span5 {
- width: 40.05524861878453%;
- *width: 40.00205712942283%;
- }
- .row-fluid .span4 {
- width: 31.491712707182323%;
- *width: 31.43852121782062%;
- }
- .row-fluid .span3 {
- width: 22.92817679558011%;
- *width: 22.87498530621841%;
- }
- .row-fluid .span2 {
- width: 14.3646408839779%;
- *width: 14.311449394616199%;
- }
- .row-fluid .span1 {
- width: 5.801104972375691%;
- *width: 5.747913483013988%;
- }
- .row-fluid .offset12 {
- margin-left: 105.52486187845304%;
- *margin-left: 105.41847889972962%;
- }
- .row-fluid .offset12:first-child {
- margin-left: 102.76243093922652%;
- *margin-left: 102.6560479605031%;
- }
- .row-fluid .offset11 {
- margin-left: 96.96132596685082%;
- *margin-left: 96.8549429881274%;
- }
- .row-fluid .offset11:first-child {
- margin-left: 94.1988950276243%;
- *margin-left: 94.09251204890089%;
- }
- .row-fluid .offset10 {
- margin-left: 88.39779005524862%;
- *margin-left: 88.2914070765252%;
- }
- .row-fluid .offset10:first-child {
- margin-left: 85.6353591160221%;
- *margin-left: 85.52897613729868%;
- }
- .row-fluid .offset9 {
- margin-left: 79.8342541436464%;
- *margin-left: 79.72787116492299%;
- }
- .row-fluid .offset9:first-child {
- margin-left: 77.07182320441989%;
- *margin-left: 76.96544022569647%;
- }
- .row-fluid .offset8 {
- margin-left: 71.2707182320442%;
- *margin-left: 71.16433525332079%;
- }
- .row-fluid .offset8:first-child {
- margin-left: 68.50828729281768%;
- *margin-left: 68.40190431409427%;
- }
- .row-fluid .offset7 {
- margin-left: 62.70718232044199%;
- *margin-left: 62.600799341718584%;
- }
- .row-fluid .offset7:first-child {
- margin-left: 59.94475138121547%;
- *margin-left: 59.838368402492065%;
- }
- .row-fluid .offset6 {
- margin-left: 54.14364640883978%;
- *margin-left: 54.037263430116376%;
- }
- .row-fluid .offset6:first-child {
- margin-left: 51.38121546961326%;
- *margin-left: 51.27483249088986%;
- }
- .row-fluid .offset5 {
- margin-left: 45.58011049723757%;
- *margin-left: 45.47372751851417%;
- }
- .row-fluid .offset5:first-child {
- margin-left: 42.81767955801105%;
- *margin-left: 42.71129657928765%;
- }
- .row-fluid .offset4 {
- margin-left: 37.01657458563536%;
- *margin-left: 36.91019160691196%;
- }
- .row-fluid .offset4:first-child {
- margin-left: 34.25414364640884%;
- *margin-left: 34.14776066768544%;
- }
- .row-fluid .offset3 {
- margin-left: 28.45303867403315%;
- *margin-left: 28.346655695309746%;
- }
- .row-fluid .offset3:first-child {
- margin-left: 25.69060773480663%;
- *margin-left: 25.584224756083227%;
- }
- .row-fluid .offset2 {
- margin-left: 19.88950276243094%;
- *margin-left: 19.783119783707537%;
- }
- .row-fluid .offset2:first-child {
- margin-left: 17.12707182320442%;
- *margin-left: 17.02068884448102%;
- }
- .row-fluid .offset1 {
- margin-left: 11.32596685082873%;
- *margin-left: 11.219583872105325%;
- }
- .row-fluid .offset1:first-child {
- margin-left: 8.56353591160221%;
- *margin-left: 8.457152932878806%;
- }
- input,
- textarea,
- .uneditable-input {
- margin-left: 0;
- }
- .controls-row [class*="span"] + [class*="span"] {
- margin-left: 20px;
- }
- input.span12,
- textarea.span12,
- .uneditable-input.span12 {
- width: 710px;
- }
- input.span11,
- textarea.span11,
- .uneditable-input.span11 {
- width: 648px;
- }
- input.span10,
- textarea.span10,
- .uneditable-input.span10 {
- width: 586px;
- }
- input.span9,
- textarea.span9,
- .uneditable-input.span9 {
- width: 524px;
- }
- input.span8,
- textarea.span8,
- .uneditable-input.span8 {
- width: 462px;
- }
- input.span7,
- textarea.span7,
- .uneditable-input.span7 {
- width: 400px;
- }
- input.span6,
- textarea.span6,
- .uneditable-input.span6 {
- width: 338px;
- }
- input.span5,
- textarea.span5,
- .uneditable-input.span5 {
- width: 276px;
- }
- input.span4,
- textarea.span4,
- .uneditable-input.span4 {
- width: 214px;
- }
- input.span3,
- textarea.span3,
- .uneditable-input.span3 {
- width: 152px;
- }
- input.span2,
- textarea.span2,
- .uneditable-input.span2 {
- width: 90px;
- }
- input.span1,
- textarea.span1,
- .uneditable-input.span1 {
- width: 28px;
- }
-}
-
-@media (max-width: 767px) {
- body {
- padding-right: 20px;
- padding-left: 20px;
- }
- .navbar-fixed-top,
- .navbar-fixed-bottom,
- .navbar-static-top {
- margin-right: -20px;
- margin-left: -20px;
- }
- .container-fluid {
- padding: 0;
- }
- .dl-horizontal dt {
- float: none;
- width: auto;
- clear: none;
- text-align: left;
- }
- .dl-horizontal dd {
- margin-left: 0;
- }
- .container {
- width: auto;
- }
- .row-fluid {
- width: 100%;
- }
- .row,
- .thumbnails {
- margin-left: 0;
- }
- .thumbnails > li {
- float: none;
- margin-left: 0;
- }
- [class*="span"],
- .uneditable-input[class*="span"],
- .row-fluid [class*="span"] {
- display: block;
- float: none;
- width: 100%;
- margin-left: 0;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- }
- .span12,
- .row-fluid .span12 {
- width: 100%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- }
- .row-fluid [class*="offset"]:first-child {
- margin-left: 0;
- }
- .input-large,
- .input-xlarge,
- .input-xxlarge,
- input[class*="span"],
- select[class*="span"],
- textarea[class*="span"],
- .uneditable-input {
- display: block;
- width: 100%;
- min-height: 30px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- }
- .input-prepend input,
- .input-append input,
- .input-prepend input[class*="span"],
- .input-append input[class*="span"] {
- display: inline-block;
- width: auto;
- }
- .controls-row [class*="span"] + [class*="span"] {
- margin-left: 0;
- }
- .modal {
- position: fixed;
- top: 20px;
- right: 20px;
- left: 20px;
- width: auto;
- margin: 0;
- }
- .modal.fade {
- top: -100px;
- }
- .modal.fade.in {
- top: 20px;
- }
-}
-
-@media (max-width: 480px) {
- .nav-collapse {
- -webkit-transform: translate3d(0, 0, 0);
- }
- .page-header h1 small {
- display: block;
- line-height: 20px;
- }
- input[type="checkbox"],
- input[type="radio"] {
- border: 1px solid #ccc;
- }
- .form-horizontal .control-label {
- float: none;
- width: auto;
- padding-top: 0;
- text-align: left;
- }
- .form-horizontal .controls {
- margin-left: 0;
- }
- .form-horizontal .control-list {
- padding-top: 0;
- }
- .form-horizontal .form-actions {
- padding-right: 10px;
- padding-left: 10px;
- }
- .media .pull-left,
- .media .pull-right {
- display: block;
- float: none;
- margin-bottom: 10px;
- }
- .media-object {
- margin-right: 0;
- margin-left: 0;
- }
- .modal {
- top: 10px;
- right: 10px;
- left: 10px;
- }
- .modal-header .close {
- padding: 10px;
- margin: -10px;
- }
- .carousel-caption {
- position: static;
- }
-}
-
-@media (max-width: 979px) {
- body {
- padding-top: 0;
- }
- .navbar-fixed-top,
- .navbar-fixed-bottom {
- position: static;
- }
- .navbar-fixed-top {
- margin-bottom: 20px;
- }
- .navbar-fixed-bottom {
- margin-top: 20px;
- }
- .navbar-fixed-top .navbar-inner,
- .navbar-fixed-bottom .navbar-inner {
- padding: 5px;
- }
- .navbar .container {
- width: auto;
- padding: 0;
- }
- .navbar .brand {
- padding-right: 10px;
- padding-left: 10px;
- margin: 0 0 0 -5px;
- }
- .nav-collapse {
- clear: both;
- }
- .nav-collapse .nav {
- float: none;
- margin: 0 0 10px;
- }
- .nav-collapse .nav > li {
- float: none;
- }
- .nav-collapse .nav > li > a {
- margin-bottom: 2px;
- }
- .nav-collapse .nav > .divider-vertical {
- display: none;
- }
- .nav-collapse .nav .nav-header {
- color: #777777;
- text-shadow: none;
- }
- .nav-collapse .nav > li > a,
- .nav-collapse .dropdown-menu a {
- padding: 9px 15px;
- font-weight: bold;
- color: #777777;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
- }
- .nav-collapse .btn {
- padding: 4px 10px 4px;
- font-weight: normal;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- }
- .nav-collapse .dropdown-menu li + li a {
- margin-bottom: 2px;
- }
- .nav-collapse .nav > li > a:hover,
- .nav-collapse .nav > li > a:focus,
- .nav-collapse .dropdown-menu a:hover,
- .nav-collapse .dropdown-menu a:focus {
- background-color: #f2f2f2;
- }
- .navbar-inverse .nav-collapse .nav > li > a,
- .navbar-inverse .nav-collapse .dropdown-menu a {
- color: #999999;
- }
- .navbar-inverse .nav-collapse .nav > li > a:hover,
- .navbar-inverse .nav-collapse .nav > li > a:focus,
- .navbar-inverse .nav-collapse .dropdown-menu a:hover,
- .navbar-inverse .nav-collapse .dropdown-menu a:focus {
- background-color: #111111;
- }
- .nav-collapse.in .btn-group {
- padding: 0;
- margin-top: 5px;
- }
- .nav-collapse .dropdown-menu {
- position: static;
- top: auto;
- left: auto;
- display: none;
- float: none;
- max-width: none;
- padding: 0;
- margin: 0 15px;
- background-color: transparent;
- border: none;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
- }
- .nav-collapse .open > .dropdown-menu {
- display: block;
- }
- .nav-collapse .dropdown-menu:before,
- .nav-collapse .dropdown-menu:after {
- display: none;
- }
- .nav-collapse .dropdown-menu .divider {
- display: none;
- }
- .nav-collapse .nav > li > .dropdown-menu:before,
- .nav-collapse .nav > li > .dropdown-menu:after {
- display: none;
- }
- .nav-collapse .navbar-form,
- .nav-collapse .navbar-search {
- float: none;
- padding: 10px 15px;
- margin: 10px 0;
- border-top: 1px solid #f2f2f2;
- border-bottom: 1px solid #f2f2f2;
- -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
- -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
- }
- .navbar-inverse .nav-collapse .navbar-form,
- .navbar-inverse .nav-collapse .navbar-search {
- border-top-color: #111111;
- border-bottom-color: #111111;
- }
- .navbar .nav-collapse .nav.pull-right {
- float: none;
- margin-left: 0;
- }
- .nav-collapse,
- .nav-collapse.collapse {
- height: 0;
- overflow: hidden;
- }
- .navbar .btn-navbar {
- display: block;
- }
- .navbar-static .navbar-inner {
- padding-right: 10px;
- padding-left: 10px;
- }
-}
-
-@media (min-width: 980px) {
- .nav-collapse.collapse {
- height: auto !important;
- overflow: visible !important;
- }
-}
diff --git a/nikola/data/themes/default/assets/css/bootstrap.css b/nikola/data/themes/default/assets/css/bootstrap.css
deleted file mode 100644
index b255056..0000000
--- a/nikola/data/themes/default/assets/css/bootstrap.css
+++ /dev/null
@@ -1,6158 +0,0 @@
-/*!
- * Bootstrap v2.3.0
- *
- * Copyright 2012 Twitter, Inc
- * Licensed under the Apache License v2.0
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Designed and built with all the love in the world @twitter by @mdo and @fat.
- */
-
-.clearfix {
- *zoom: 1;
-}
-
-.clearfix:before,
-.clearfix:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.clearfix:after {
- clear: both;
-}
-
-.hide-text {
- font: 0/0 a;
- color: transparent;
- text-shadow: none;
- background-color: transparent;
- border: 0;
-}
-
-.input-block-level {
- display: block;
- width: 100%;
- min-height: 30px;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-article,
-aside,
-details,
-figcaption,
-figure,
-footer,
-header,
-hgroup,
-nav,
-section {
- display: block;
-}
-
-audio,
-canvas,
-video {
- display: inline-block;
- *display: inline;
- *zoom: 1;
-}
-
-audio:not([controls]) {
- display: none;
-}
-
-html {
- font-size: 100%;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
-}
-
-a:focus {
- outline: thin dotted #333;
- outline: 5px auto -webkit-focus-ring-color;
- outline-offset: -2px;
-}
-
-a:hover,
-a:active {
- outline: 0;
-}
-
-sub,
-sup {
- position: relative;
- font-size: 75%;
- line-height: 0;
- vertical-align: baseline;
-}
-
-sup {
- top: -0.5em;
-}
-
-sub {
- bottom: -0.25em;
-}
-
-img {
- width: auto\9;
- height: auto;
- max-width: 100%;
- vertical-align: middle;
- border: 0;
- -ms-interpolation-mode: bicubic;
-}
-
-#map_canvas img,
-.google-maps img {
- max-width: none;
-}
-
-button,
-input,
-select,
-textarea {
- margin: 0;
- font-size: 100%;
- vertical-align: middle;
-}
-
-button,
-input {
- *overflow: visible;
- line-height: normal;
-}
-
-button::-moz-focus-inner,
-input::-moz-focus-inner {
- padding: 0;
- border: 0;
-}
-
-button,
-html input[type="button"],
-input[type="reset"],
-input[type="submit"] {
- cursor: pointer;
- -webkit-appearance: button;
-}
-
-label,
-select,
-button,
-input[type="button"],
-input[type="reset"],
-input[type="submit"],
-input[type="radio"],
-input[type="checkbox"] {
- cursor: pointer;
-}
-
-input[type="search"] {
- -webkit-box-sizing: content-box;
- -moz-box-sizing: content-box;
- box-sizing: content-box;
- -webkit-appearance: textfield;
-}
-
-input[type="search"]::-webkit-search-decoration,
-input[type="search"]::-webkit-search-cancel-button {
- -webkit-appearance: none;
-}
-
-textarea {
- overflow: auto;
- vertical-align: top;
-}
-
-@media print {
- * {
- color: #000 !important;
- text-shadow: none !important;
- background: transparent !important;
- box-shadow: none !important;
- }
- a,
- a:visited {
- text-decoration: underline;
- }
- a[href]:after {
- content: " (" attr(href) ")";
- }
- abbr[title]:after {
- content: " (" attr(title) ")";
- }
- .ir a:after,
- a[href^="javascript:"]:after,
- a[href^="#"]:after {
- content: "";
- }
- pre,
- blockquote {
- border: 1px solid #999;
- page-break-inside: avoid;
- }
- thead {
- display: table-header-group;
- }
- tr,
- img {
- page-break-inside: avoid;
- }
- img {
- max-width: 100% !important;
- }
- @page {
- margin: 0.5cm;
- }
- p,
- h2,
- h3 {
- orphans: 3;
- widows: 3;
- }
- h2,
- h3 {
- page-break-after: avoid;
- }
-}
-
-body {
- margin: 0;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- font-size: 14px;
- line-height: 20px;
- color: #333333;
- background-color: #ffffff;
-}
-
-a {
- color: #0088cc;
- text-decoration: none;
-}
-
-a:hover,
-a:focus {
- color: #005580;
- text-decoration: underline;
-}
-
-.img-rounded {
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
-}
-
-.img-polaroid {
- padding: 4px;
- background-color: #fff;
- border: 1px solid #ccc;
- border: 1px solid rgba(0, 0, 0, 0.2);
- -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-
-.img-circle {
- -webkit-border-radius: 500px;
- -moz-border-radius: 500px;
- border-radius: 500px;
-}
-
-.row {
- margin-left: -20px;
- *zoom: 1;
-}
-
-.row:before,
-.row:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.row:after {
- clear: both;
-}
-
-[class*="span"] {
- float: left;
- min-height: 1px;
- margin-left: 20px;
-}
-
-.container,
-.navbar-static-top .container,
-.navbar-fixed-top .container,
-.navbar-fixed-bottom .container {
- width: 940px;
-}
-
-.span12 {
- width: 940px;
-}
-
-.span11 {
- width: 860px;
-}
-
-.span10 {
- width: 780px;
-}
-
-.span9 {
- width: 700px;
-}
-
-.span8 {
- width: 620px;
-}
-
-.span7 {
- width: 540px;
-}
-
-.span6 {
- width: 460px;
-}
-
-.span5 {
- width: 380px;
-}
-
-.span4 {
- width: 300px;
-}
-
-.span3 {
- width: 220px;
-}
-
-.span2 {
- width: 140px;
-}
-
-.span1 {
- width: 60px;
-}
-
-.offset12 {
- margin-left: 980px;
-}
-
-.offset11 {
- margin-left: 900px;
-}
-
-.offset10 {
- margin-left: 820px;
-}
-
-.offset9 {
- margin-left: 740px;
-}
-
-.offset8 {
- margin-left: 660px;
-}
-
-.offset7 {
- margin-left: 580px;
-}
-
-.offset6 {
- margin-left: 500px;
-}
-
-.offset5 {
- margin-left: 420px;
-}
-
-.offset4 {
- margin-left: 340px;
-}
-
-.offset3 {
- margin-left: 260px;
-}
-
-.offset2 {
- margin-left: 180px;
-}
-
-.offset1 {
- margin-left: 100px;
-}
-
-.row-fluid {
- width: 100%;
- *zoom: 1;
-}
-
-.row-fluid:before,
-.row-fluid:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.row-fluid:after {
- clear: both;
-}
-
-.row-fluid [class*="span"] {
- display: block;
- float: left;
- width: 100%;
- min-height: 30px;
- margin-left: 2.127659574468085%;
- *margin-left: 2.074468085106383%;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.row-fluid [class*="span"]:first-child {
- margin-left: 0;
-}
-
-.row-fluid .controls-row [class*="span"] + [class*="span"] {
- margin-left: 2.127659574468085%;
-}
-
-.row-fluid .span12 {
- width: 100%;
- *width: 99.94680851063829%;
-}
-
-.row-fluid .span11 {
- width: 91.48936170212765%;
- *width: 91.43617021276594%;
-}
-
-.row-fluid .span10 {
- width: 82.97872340425532%;
- *width: 82.92553191489361%;
-}
-
-.row-fluid .span9 {
- width: 74.46808510638297%;
- *width: 74.41489361702126%;
-}
-
-.row-fluid .span8 {
- width: 65.95744680851064%;
- *width: 65.90425531914893%;
-}
-
-.row-fluid .span7 {
- width: 57.44680851063829%;
- *width: 57.39361702127659%;
-}
-
-.row-fluid .span6 {
- width: 48.93617021276595%;
- *width: 48.88297872340425%;
-}
-
-.row-fluid .span5 {
- width: 40.42553191489362%;
- *width: 40.37234042553192%;
-}
-
-.row-fluid .span4 {
- width: 31.914893617021278%;
- *width: 31.861702127659576%;
-}
-
-.row-fluid .span3 {
- width: 23.404255319148934%;
- *width: 23.351063829787233%;
-}
-
-.row-fluid .span2 {
- width: 14.893617021276595%;
- *width: 14.840425531914894%;
-}
-
-.row-fluid .span1 {
- width: 6.382978723404255%;
- *width: 6.329787234042553%;
-}
-
-.row-fluid .offset12 {
- margin-left: 104.25531914893617%;
- *margin-left: 104.14893617021275%;
-}
-
-.row-fluid .offset12:first-child {
- margin-left: 102.12765957446808%;
- *margin-left: 102.02127659574467%;
-}
-
-.row-fluid .offset11 {
- margin-left: 95.74468085106382%;
- *margin-left: 95.6382978723404%;
-}
-
-.row-fluid .offset11:first-child {
- margin-left: 93.61702127659574%;
- *margin-left: 93.51063829787232%;
-}
-
-.row-fluid .offset10 {
- margin-left: 87.23404255319149%;
- *margin-left: 87.12765957446807%;
-}
-
-.row-fluid .offset10:first-child {
- margin-left: 85.1063829787234%;
- *margin-left: 84.99999999999999%;
-}
-
-.row-fluid .offset9 {
- margin-left: 78.72340425531914%;
- *margin-left: 78.61702127659572%;
-}
-
-.row-fluid .offset9:first-child {
- margin-left: 76.59574468085106%;
- *margin-left: 76.48936170212764%;
-}
-
-.row-fluid .offset8 {
- margin-left: 70.2127659574468%;
- *margin-left: 70.10638297872339%;
-}
-
-.row-fluid .offset8:first-child {
- margin-left: 68.08510638297872%;
- *margin-left: 67.9787234042553%;
-}
-
-.row-fluid .offset7 {
- margin-left: 61.70212765957446%;
- *margin-left: 61.59574468085106%;
-}
-
-.row-fluid .offset7:first-child {
- margin-left: 59.574468085106375%;
- *margin-left: 59.46808510638297%;
-}
-
-.row-fluid .offset6 {
- margin-left: 53.191489361702125%;
- *margin-left: 53.085106382978715%;
-}
-
-.row-fluid .offset6:first-child {
- margin-left: 51.063829787234035%;
- *margin-left: 50.95744680851063%;
-}
-
-.row-fluid .offset5 {
- margin-left: 44.68085106382979%;
- *margin-left: 44.57446808510638%;
-}
-
-.row-fluid .offset5:first-child {
- margin-left: 42.5531914893617%;
- *margin-left: 42.4468085106383%;
-}
-
-.row-fluid .offset4 {
- margin-left: 36.170212765957444%;
- *margin-left: 36.06382978723405%;
-}
-
-.row-fluid .offset4:first-child {
- margin-left: 34.04255319148936%;
- *margin-left: 33.93617021276596%;
-}
-
-.row-fluid .offset3 {
- margin-left: 27.659574468085104%;
- *margin-left: 27.5531914893617%;
-}
-
-.row-fluid .offset3:first-child {
- margin-left: 25.53191489361702%;
- *margin-left: 25.425531914893618%;
-}
-
-.row-fluid .offset2 {
- margin-left: 19.148936170212764%;
- *margin-left: 19.04255319148936%;
-}
-
-.row-fluid .offset2:first-child {
- margin-left: 17.02127659574468%;
- *margin-left: 16.914893617021278%;
-}
-
-.row-fluid .offset1 {
- margin-left: 10.638297872340425%;
- *margin-left: 10.53191489361702%;
-}
-
-.row-fluid .offset1:first-child {
- margin-left: 8.51063829787234%;
- *margin-left: 8.404255319148938%;
-}
-
-[class*="span"].hide,
-.row-fluid [class*="span"].hide {
- display: none;
-}
-
-[class*="span"].pull-right,
-.row-fluid [class*="span"].pull-right {
- float: right;
-}
-
-.container {
- margin-right: auto;
- margin-left: auto;
- *zoom: 1;
-}
-
-.container:before,
-.container:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.container:after {
- clear: both;
-}
-
-.container-fluid {
- padding-right: 20px;
- padding-left: 20px;
- *zoom: 1;
-}
-
-.container-fluid:before,
-.container-fluid:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.container-fluid:after {
- clear: both;
-}
-
-p {
- margin: 0 0 10px;
-}
-
-.lead {
- margin-bottom: 20px;
- font-size: 21px;
- font-weight: 200;
- line-height: 30px;
-}
-
-small {
- font-size: 85%;
-}
-
-strong {
- font-weight: bold;
-}
-
-em {
- font-style: italic;
-}
-
-cite {
- font-style: normal;
-}
-
-.muted {
- color: #999999;
-}
-
-a.muted:hover,
-a.muted:focus {
- color: #808080;
-}
-
-.text-warning {
- color: #c09853;
-}
-
-a.text-warning:hover,
-a.text-warning:focus {
- color: #a47e3c;
-}
-
-.text-error {
- color: #b94a48;
-}
-
-a.text-error:hover,
-a.text-error:focus {
- color: #953b39;
-}
-
-.text-info {
- color: #3a87ad;
-}
-
-a.text-info:hover,
-a.text-info:focus {
- color: #2d6987;
-}
-
-.text-success {
- color: #468847;
-}
-
-a.text-success:hover,
-a.text-success:focus {
- color: #356635;
-}
-
-.text-left {
- text-align: left;
-}
-
-.text-right {
- text-align: right;
-}
-
-.text-center {
- text-align: center;
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- margin: 10px 0;
- font-family: inherit;
- font-weight: bold;
- line-height: 20px;
- color: inherit;
- text-rendering: optimizelegibility;
-}
-
-h1 small,
-h2 small,
-h3 small,
-h4 small,
-h5 small,
-h6 small {
- font-weight: normal;
- line-height: 1;
- color: #999999;
-}
-
-h1,
-h2,
-h3 {
- line-height: 40px;
-}
-
-h1 {
- font-size: 38.5px;
-}
-
-h2 {
- font-size: 31.5px;
-}
-
-h3 {
- font-size: 24.5px;
-}
-
-h4 {
- font-size: 17.5px;
-}
-
-h5 {
- font-size: 14px;
-}
-
-h6 {
- font-size: 11.9px;
-}
-
-h1 small {
- font-size: 24.5px;
-}
-
-h2 small {
- font-size: 17.5px;
-}
-
-h3 small {
- font-size: 14px;
-}
-
-h4 small {
- font-size: 14px;
-}
-
-.page-header {
- padding-bottom: 9px;
- margin: 20px 0 30px;
- border-bottom: 1px solid #eeeeee;
-}
-
-ul,
-ol {
- padding: 0;
- margin: 0 0 10px 25px;
-}
-
-ul ul,
-ul ol,
-ol ol,
-ol ul {
- margin-bottom: 0;
-}
-
-li {
- line-height: 20px;
-}
-
-ul.unstyled,
-ol.unstyled {
- margin-left: 0;
- list-style: none;
-}
-
-ul.inline,
-ol.inline {
- margin-left: 0;
- list-style: none;
-}
-
-ul.inline > li,
-ol.inline > li {
- display: inline-block;
- *display: inline;
- padding-right: 5px;
- padding-left: 5px;
- *zoom: 1;
-}
-
-dl {
- margin-bottom: 20px;
-}
-
-dt,
-dd {
- line-height: 20px;
-}
-
-dt {
- font-weight: bold;
-}
-
-dd {
- margin-left: 10px;
-}
-
-.dl-horizontal {
- *zoom: 1;
-}
-
-.dl-horizontal:before,
-.dl-horizontal:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.dl-horizontal:after {
- clear: both;
-}
-
-.dl-horizontal dt {
- float: left;
- width: 160px;
- overflow: hidden;
- clear: left;
- text-align: right;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.dl-horizontal dd {
- margin-left: 180px;
-}
-
-hr {
- margin: 20px 0;
- border: 0;
- border-top: 1px solid #eeeeee;
- border-bottom: 1px solid #ffffff;
-}
-
-abbr[title],
-abbr[data-original-title] {
- cursor: help;
- border-bottom: 1px dotted #999999;
-}
-
-abbr.initialism {
- font-size: 90%;
- text-transform: uppercase;
-}
-
-blockquote {
- padding: 0 0 0 15px;
- margin: 0 0 20px;
- border-left: 5px solid #eeeeee;
-}
-
-blockquote p {
- margin-bottom: 0;
- font-size: 17.5px;
- font-weight: 300;
- line-height: 1.25;
-}
-
-blockquote small {
- display: block;
- line-height: 20px;
- color: #999999;
-}
-
-blockquote small:before {
- content: '\2014 \00A0';
-}
-
-blockquote.pull-right {
- float: right;
- padding-right: 15px;
- padding-left: 0;
- border-right: 5px solid #eeeeee;
- border-left: 0;
-}
-
-blockquote.pull-right p,
-blockquote.pull-right small {
- text-align: right;
-}
-
-blockquote.pull-right small:before {
- content: '';
-}
-
-blockquote.pull-right small:after {
- content: '\00A0 \2014';
-}
-
-q:before,
-q:after,
-blockquote:before,
-blockquote:after {
- content: "";
-}
-
-address {
- display: block;
- margin-bottom: 20px;
- font-style: normal;
- line-height: 20px;
-}
-
-code,
-pre {
- padding: 0 3px 2px;
- font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
- font-size: 12px;
- color: #333333;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-
-code {
- padding: 2px 4px;
- color: #d14;
- white-space: nowrap;
- background-color: #f7f7f9;
- border: 1px solid #e1e1e8;
-}
-
-pre {
- display: block;
- padding: 9.5px;
- margin: 0 0 10px;
- font-size: 13px;
- line-height: 20px;
- word-break: break-all;
- word-wrap: break-word;
- white-space: pre;
- white-space: pre-wrap;
- background-color: #f5f5f5;
- border: 1px solid #ccc;
- border: 1px solid rgba(0, 0, 0, 0.15);
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-pre.prettyprint {
- margin-bottom: 20px;
-}
-
-pre code {
- padding: 0;
- color: inherit;
- white-space: pre;
- white-space: pre-wrap;
- background-color: transparent;
- border: 0;
-}
-
-.pre-scrollable {
- max-height: 340px;
- overflow-y: scroll;
-}
-
-form {
- margin: 0 0 20px;
-}
-
-fieldset {
- padding: 0;
- margin: 0;
- border: 0;
-}
-
-legend {
- display: block;
- width: 100%;
- padding: 0;
- margin-bottom: 20px;
- font-size: 21px;
- line-height: 40px;
- color: #333333;
- border: 0;
- border-bottom: 1px solid #e5e5e5;
-}
-
-legend small {
- font-size: 15px;
- color: #999999;
-}
-
-label,
-input,
-button,
-select,
-textarea {
- font-size: 14px;
- font-weight: normal;
- line-height: 20px;
-}
-
-input,
-button,
-select,
-textarea {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-}
-
-label {
- display: block;
- margin-bottom: 5px;
-}
-
-select,
-textarea,
-input[type="text"],
-input[type="password"],
-input[type="datetime"],
-input[type="datetime-local"],
-input[type="date"],
-input[type="month"],
-input[type="time"],
-input[type="week"],
-input[type="number"],
-input[type="email"],
-input[type="url"],
-input[type="search"],
-input[type="tel"],
-input[type="color"],
-.uneditable-input {
- display: inline-block;
- height: 20px;
- padding: 4px 6px;
- margin-bottom: 10px;
- font-size: 14px;
- line-height: 20px;
- color: #555555;
- vertical-align: middle;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-input,
-textarea,
-.uneditable-input {
- width: 206px;
-}
-
-textarea {
- height: auto;
-}
-
-textarea,
-input[type="text"],
-input[type="password"],
-input[type="datetime"],
-input[type="datetime-local"],
-input[type="date"],
-input[type="month"],
-input[type="time"],
-input[type="week"],
-input[type="number"],
-input[type="email"],
-input[type="url"],
-input[type="search"],
-input[type="tel"],
-input[type="color"],
-.uneditable-input {
- background-color: #ffffff;
- border: 1px solid #cccccc;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
- -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
- -o-transition: border linear 0.2s, box-shadow linear 0.2s;
- transition: border linear 0.2s, box-shadow linear 0.2s;
-}
-
-textarea:focus,
-input[type="text"]:focus,
-input[type="password"]:focus,
-input[type="datetime"]:focus,
-input[type="datetime-local"]:focus,
-input[type="date"]:focus,
-input[type="month"]:focus,
-input[type="time"]:focus,
-input[type="week"]:focus,
-input[type="number"]:focus,
-input[type="email"]:focus,
-input[type="url"]:focus,
-input[type="search"]:focus,
-input[type="tel"]:focus,
-input[type="color"]:focus,
-.uneditable-input:focus {
- border-color: rgba(82, 168, 236, 0.8);
- outline: 0;
- outline: thin dotted \9;
- /* IE6-9 */
-
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-}
-
-input[type="radio"],
-input[type="checkbox"] {
- margin: 4px 0 0;
- margin-top: 1px \9;
- *margin-top: 0;
- line-height: normal;
-}
-
-input[type="file"],
-input[type="image"],
-input[type="submit"],
-input[type="reset"],
-input[type="button"],
-input[type="radio"],
-input[type="checkbox"] {
- width: auto;
-}
-
-select,
-input[type="file"] {
- height: 30px;
- /* In IE7, the height of the select element cannot be changed by height, only font-size */
-
- *margin-top: 4px;
- /* For IE7, add top margin to align select with labels */
-
- line-height: 30px;
-}
-
-select {
- width: 220px;
- background-color: #ffffff;
- border: 1px solid #cccccc;
-}
-
-select[multiple],
-select[size] {
- height: auto;
-}
-
-select:focus,
-input[type="file"]:focus,
-input[type="radio"]:focus,
-input[type="checkbox"]:focus {
- outline: thin dotted #333;
- outline: 5px auto -webkit-focus-ring-color;
- outline-offset: -2px;
-}
-
-.uneditable-input,
-.uneditable-textarea {
- color: #999999;
- cursor: not-allowed;
- background-color: #fcfcfc;
- border-color: #cccccc;
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
- -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
-}
-
-.uneditable-input {
- overflow: hidden;
- white-space: nowrap;
-}
-
-.uneditable-textarea {
- width: auto;
- height: auto;
-}
-
-input:-moz-placeholder,
-textarea:-moz-placeholder {
- color: #999999;
-}
-
-input:-ms-input-placeholder,
-textarea:-ms-input-placeholder {
- color: #999999;
-}
-
-input::-webkit-input-placeholder,
-textarea::-webkit-input-placeholder {
- color: #999999;
-}
-
-.radio,
-.checkbox {
- min-height: 20px;
- padding-left: 20px;
-}
-
-.radio input[type="radio"],
-.checkbox input[type="checkbox"] {
- float: left;
- margin-left: -20px;
-}
-
-.controls > .radio:first-child,
-.controls > .checkbox:first-child {
- padding-top: 5px;
-}
-
-.radio.inline,
-.checkbox.inline {
- display: inline-block;
- padding-top: 5px;
- margin-bottom: 0;
- vertical-align: middle;
-}
-
-.radio.inline + .radio.inline,
-.checkbox.inline + .checkbox.inline {
- margin-left: 10px;
-}
-
-.input-mini {
- width: 60px;
-}
-
-.input-small {
- width: 90px;
-}
-
-.input-medium {
- width: 150px;
-}
-
-.input-large {
- width: 210px;
-}
-
-.input-xlarge {
- width: 270px;
-}
-
-.input-xxlarge {
- width: 530px;
-}
-
-input[class*="span"],
-select[class*="span"],
-textarea[class*="span"],
-.uneditable-input[class*="span"],
-.row-fluid input[class*="span"],
-.row-fluid select[class*="span"],
-.row-fluid textarea[class*="span"],
-.row-fluid .uneditable-input[class*="span"] {
- float: none;
- margin-left: 0;
-}
-
-.input-append input[class*="span"],
-.input-append .uneditable-input[class*="span"],
-.input-prepend input[class*="span"],
-.input-prepend .uneditable-input[class*="span"],
-.row-fluid input[class*="span"],
-.row-fluid select[class*="span"],
-.row-fluid textarea[class*="span"],
-.row-fluid .uneditable-input[class*="span"],
-.row-fluid .input-prepend [class*="span"],
-.row-fluid .input-append [class*="span"] {
- display: inline-block;
-}
-
-input,
-textarea,
-.uneditable-input {
- margin-left: 0;
-}
-
-.controls-row [class*="span"] + [class*="span"] {
- margin-left: 20px;
-}
-
-input.span12,
-textarea.span12,
-.uneditable-input.span12 {
- width: 926px;
-}
-
-input.span11,
-textarea.span11,
-.uneditable-input.span11 {
- width: 846px;
-}
-
-input.span10,
-textarea.span10,
-.uneditable-input.span10 {
- width: 766px;
-}
-
-input.span9,
-textarea.span9,
-.uneditable-input.span9 {
- width: 686px;
-}
-
-input.span8,
-textarea.span8,
-.uneditable-input.span8 {
- width: 606px;
-}
-
-input.span7,
-textarea.span7,
-.uneditable-input.span7 {
- width: 526px;
-}
-
-input.span6,
-textarea.span6,
-.uneditable-input.span6 {
- width: 446px;
-}
-
-input.span5,
-textarea.span5,
-.uneditable-input.span5 {
- width: 366px;
-}
-
-input.span4,
-textarea.span4,
-.uneditable-input.span4 {
- width: 286px;
-}
-
-input.span3,
-textarea.span3,
-.uneditable-input.span3 {
- width: 206px;
-}
-
-input.span2,
-textarea.span2,
-.uneditable-input.span2 {
- width: 126px;
-}
-
-input.span1,
-textarea.span1,
-.uneditable-input.span1 {
- width: 46px;
-}
-
-.controls-row {
- *zoom: 1;
-}
-
-.controls-row:before,
-.controls-row:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.controls-row:after {
- clear: both;
-}
-
-.controls-row [class*="span"],
-.row-fluid .controls-row [class*="span"] {
- float: left;
-}
-
-.controls-row .checkbox[class*="span"],
-.controls-row .radio[class*="span"] {
- padding-top: 5px;
-}
-
-input[disabled],
-select[disabled],
-textarea[disabled],
-input[readonly],
-select[readonly],
-textarea[readonly] {
- cursor: not-allowed;
- background-color: #eeeeee;
-}
-
-input[type="radio"][disabled],
-input[type="checkbox"][disabled],
-input[type="radio"][readonly],
-input[type="checkbox"][readonly] {
- background-color: transparent;
-}
-
-.control-group.warning .control-label,
-.control-group.warning .help-block,
-.control-group.warning .help-inline {
- color: #c09853;
-}
-
-.control-group.warning .checkbox,
-.control-group.warning .radio,
-.control-group.warning input,
-.control-group.warning select,
-.control-group.warning textarea {
- color: #c09853;
-}
-
-.control-group.warning input,
-.control-group.warning select,
-.control-group.warning textarea {
- border-color: #c09853;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-}
-
-.control-group.warning input:focus,
-.control-group.warning select:focus,
-.control-group.warning textarea:focus {
- border-color: #a47e3c;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;
-}
-
-.control-group.warning .input-prepend .add-on,
-.control-group.warning .input-append .add-on {
- color: #c09853;
- background-color: #fcf8e3;
- border-color: #c09853;
-}
-
-.control-group.error .control-label,
-.control-group.error .help-block,
-.control-group.error .help-inline {
- color: #b94a48;
-}
-
-.control-group.error .checkbox,
-.control-group.error .radio,
-.control-group.error input,
-.control-group.error select,
-.control-group.error textarea {
- color: #b94a48;
-}
-
-.control-group.error input,
-.control-group.error select,
-.control-group.error textarea {
- border-color: #b94a48;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-}
-
-.control-group.error input:focus,
-.control-group.error select:focus,
-.control-group.error textarea:focus {
- border-color: #953b39;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;
-}
-
-.control-group.error .input-prepend .add-on,
-.control-group.error .input-append .add-on {
- color: #b94a48;
- background-color: #f2dede;
- border-color: #b94a48;
-}
-
-.control-group.success .control-label,
-.control-group.success .help-block,
-.control-group.success .help-inline {
- color: #468847;
-}
-
-.control-group.success .checkbox,
-.control-group.success .radio,
-.control-group.success input,
-.control-group.success select,
-.control-group.success textarea {
- color: #468847;
-}
-
-.control-group.success input,
-.control-group.success select,
-.control-group.success textarea {
- border-color: #468847;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-}
-
-.control-group.success input:focus,
-.control-group.success select:focus,
-.control-group.success textarea:focus {
- border-color: #356635;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;
-}
-
-.control-group.success .input-prepend .add-on,
-.control-group.success .input-append .add-on {
- color: #468847;
- background-color: #dff0d8;
- border-color: #468847;
-}
-
-.control-group.info .control-label,
-.control-group.info .help-block,
-.control-group.info .help-inline {
- color: #3a87ad;
-}
-
-.control-group.info .checkbox,
-.control-group.info .radio,
-.control-group.info input,
-.control-group.info select,
-.control-group.info textarea {
- color: #3a87ad;
-}
-
-.control-group.info input,
-.control-group.info select,
-.control-group.info textarea {
- border-color: #3a87ad;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-}
-
-.control-group.info input:focus,
-.control-group.info select:focus,
-.control-group.info textarea:focus {
- border-color: #2d6987;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;
-}
-
-.control-group.info .input-prepend .add-on,
-.control-group.info .input-append .add-on {
- color: #3a87ad;
- background-color: #d9edf7;
- border-color: #3a87ad;
-}
-
-input:focus:invalid,
-textarea:focus:invalid,
-select:focus:invalid {
- color: #b94a48;
- border-color: #ee5f5b;
-}
-
-input:focus:invalid:focus,
-textarea:focus:invalid:focus,
-select:focus:invalid:focus {
- border-color: #e9322d;
- -webkit-box-shadow: 0 0 6px #f8b9b7;
- -moz-box-shadow: 0 0 6px #f8b9b7;
- box-shadow: 0 0 6px #f8b9b7;
-}
-
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- background-color: #f5f5f5;
- border-top: 1px solid #e5e5e5;
- *zoom: 1;
-}
-
-.form-actions:before,
-.form-actions:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.form-actions:after {
- clear: both;
-}
-
-.help-block,
-.help-inline {
- color: #595959;
-}
-
-.help-block {
- display: block;
- margin-bottom: 10px;
-}
-
-.help-inline {
- display: inline-block;
- *display: inline;
- padding-left: 5px;
- vertical-align: middle;
- *zoom: 1;
-}
-
-.input-append,
-.input-prepend {
- display: inline-block;
- margin-bottom: 10px;
- font-size: 0;
- white-space: nowrap;
- vertical-align: middle;
-}
-
-.input-append input,
-.input-prepend input,
-.input-append select,
-.input-prepend select,
-.input-append .uneditable-input,
-.input-prepend .uneditable-input,
-.input-append .dropdown-menu,
-.input-prepend .dropdown-menu,
-.input-append .popover,
-.input-prepend .popover {
- font-size: 14px;
-}
-
-.input-append input,
-.input-prepend input,
-.input-append select,
-.input-prepend select,
-.input-append .uneditable-input,
-.input-prepend .uneditable-input {
- position: relative;
- margin-bottom: 0;
- *margin-left: 0;
- vertical-align: top;
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.input-append input:focus,
-.input-prepend input:focus,
-.input-append select:focus,
-.input-prepend select:focus,
-.input-append .uneditable-input:focus,
-.input-prepend .uneditable-input:focus {
- z-index: 2;
-}
-
-.input-append .add-on,
-.input-prepend .add-on {
- display: inline-block;
- width: auto;
- height: 20px;
- min-width: 16px;
- padding: 4px 5px;
- font-size: 14px;
- font-weight: normal;
- line-height: 20px;
- text-align: center;
- text-shadow: 0 1px 0 #ffffff;
- background-color: #eeeeee;
- border: 1px solid #ccc;
-}
-
-.input-append .add-on,
-.input-prepend .add-on,
-.input-append .btn,
-.input-prepend .btn,
-.input-append .btn-group > .dropdown-toggle,
-.input-prepend .btn-group > .dropdown-toggle {
- vertical-align: top;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.input-append .active,
-.input-prepend .active {
- background-color: #a9dba9;
- border-color: #46a546;
-}
-
-.input-prepend .add-on,
-.input-prepend .btn {
- margin-right: -1px;
-}
-
-.input-prepend .add-on:first-child,
-.input-prepend .btn:first-child {
- -webkit-border-radius: 4px 0 0 4px;
- -moz-border-radius: 4px 0 0 4px;
- border-radius: 4px 0 0 4px;
-}
-
-.input-append input,
-.input-append select,
-.input-append .uneditable-input {
- -webkit-border-radius: 4px 0 0 4px;
- -moz-border-radius: 4px 0 0 4px;
- border-radius: 4px 0 0 4px;
-}
-
-.input-append input + .btn-group .btn:last-child,
-.input-append select + .btn-group .btn:last-child,
-.input-append .uneditable-input + .btn-group .btn:last-child {
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.input-append .add-on,
-.input-append .btn,
-.input-append .btn-group {
- margin-left: -1px;
-}
-
-.input-append .add-on:last-child,
-.input-append .btn:last-child,
-.input-append .btn-group:last-child > .dropdown-toggle {
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.input-prepend.input-append input,
-.input-prepend.input-append select,
-.input-prepend.input-append .uneditable-input {
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.input-prepend.input-append input + .btn-group .btn,
-.input-prepend.input-append select + .btn-group .btn,
-.input-prepend.input-append .uneditable-input + .btn-group .btn {
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.input-prepend.input-append .add-on:first-child,
-.input-prepend.input-append .btn:first-child {
- margin-right: -1px;
- -webkit-border-radius: 4px 0 0 4px;
- -moz-border-radius: 4px 0 0 4px;
- border-radius: 4px 0 0 4px;
-}
-
-.input-prepend.input-append .add-on:last-child,
-.input-prepend.input-append .btn:last-child {
- margin-left: -1px;
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.input-prepend.input-append .btn-group:first-child {
- margin-left: 0;
-}
-
-input.search-query {
- padding-right: 14px;
- padding-right: 4px \9;
- padding-left: 14px;
- padding-left: 4px \9;
- /* IE7-8 doesn't have border-radius, so don't indent the padding */
-
- margin-bottom: 0;
- -webkit-border-radius: 15px;
- -moz-border-radius: 15px;
- border-radius: 15px;
-}
-
-/* Allow for input prepend/append in search forms */
-
-.form-search .input-append .search-query,
-.form-search .input-prepend .search-query {
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.form-search .input-append .search-query {
- -webkit-border-radius: 14px 0 0 14px;
- -moz-border-radius: 14px 0 0 14px;
- border-radius: 14px 0 0 14px;
-}
-
-.form-search .input-append .btn {
- -webkit-border-radius: 0 14px 14px 0;
- -moz-border-radius: 0 14px 14px 0;
- border-radius: 0 14px 14px 0;
-}
-
-.form-search .input-prepend .search-query {
- -webkit-border-radius: 0 14px 14px 0;
- -moz-border-radius: 0 14px 14px 0;
- border-radius: 0 14px 14px 0;
-}
-
-.form-search .input-prepend .btn {
- -webkit-border-radius: 14px 0 0 14px;
- -moz-border-radius: 14px 0 0 14px;
- border-radius: 14px 0 0 14px;
-}
-
-.form-search input,
-.form-inline input,
-.form-horizontal input,
-.form-search textarea,
-.form-inline textarea,
-.form-horizontal textarea,
-.form-search select,
-.form-inline select,
-.form-horizontal select,
-.form-search .help-inline,
-.form-inline .help-inline,
-.form-horizontal .help-inline,
-.form-search .uneditable-input,
-.form-inline .uneditable-input,
-.form-horizontal .uneditable-input,
-.form-search .input-prepend,
-.form-inline .input-prepend,
-.form-horizontal .input-prepend,
-.form-search .input-append,
-.form-inline .input-append,
-.form-horizontal .input-append {
- display: inline-block;
- *display: inline;
- margin-bottom: 0;
- vertical-align: middle;
- *zoom: 1;
-}
-
-.form-search .hide,
-.form-inline .hide,
-.form-horizontal .hide {
- display: none;
-}
-
-.form-search label,
-.form-inline label,
-.form-search .btn-group,
-.form-inline .btn-group {
- display: inline-block;
-}
-
-.form-search .input-append,
-.form-inline .input-append,
-.form-search .input-prepend,
-.form-inline .input-prepend {
- margin-bottom: 0;
-}
-
-.form-search .radio,
-.form-search .checkbox,
-.form-inline .radio,
-.form-inline .checkbox {
- padding-left: 0;
- margin-bottom: 0;
- vertical-align: middle;
-}
-
-.form-search .radio input[type="radio"],
-.form-search .checkbox input[type="checkbox"],
-.form-inline .radio input[type="radio"],
-.form-inline .checkbox input[type="checkbox"] {
- float: left;
- margin-right: 3px;
- margin-left: 0;
-}
-
-.control-group {
- margin-bottom: 10px;
-}
-
-legend + .control-group {
- margin-top: 20px;
- -webkit-margin-top-collapse: separate;
-}
-
-.form-horizontal .control-group {
- margin-bottom: 20px;
- *zoom: 1;
-}
-
-.form-horizontal .control-group:before,
-.form-horizontal .control-group:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.form-horizontal .control-group:after {
- clear: both;
-}
-
-.form-horizontal .control-label {
- float: left;
- width: 160px;
- padding-top: 5px;
- text-align: right;
-}
-
-.form-horizontal .controls {
- *display: inline-block;
- *padding-left: 20px;
- margin-left: 180px;
- *margin-left: 0;
-}
-
-.form-horizontal .controls:first-child {
- *padding-left: 180px;
-}
-
-.form-horizontal .help-block {
- margin-bottom: 0;
-}
-
-.form-horizontal input + .help-block,
-.form-horizontal select + .help-block,
-.form-horizontal textarea + .help-block,
-.form-horizontal .uneditable-input + .help-block,
-.form-horizontal .input-prepend + .help-block,
-.form-horizontal .input-append + .help-block {
- margin-top: 10px;
-}
-
-.form-horizontal .form-actions {
- padding-left: 180px;
-}
-
-table {
- max-width: 100%;
- background-color: transparent;
- border-collapse: collapse;
- border-spacing: 0;
-}
-
-.table {
- width: 100%;
- margin-bottom: 20px;
-}
-
-.table th,
-.table td {
- padding: 8px;
- line-height: 20px;
- text-align: left;
- vertical-align: top;
- border-top: 1px solid #dddddd;
-}
-
-.table th {
- font-weight: bold;
-}
-
-.table thead th {
- vertical-align: bottom;
-}
-
-.table caption + thead tr:first-child th,
-.table caption + thead tr:first-child td,
-.table colgroup + thead tr:first-child th,
-.table colgroup + thead tr:first-child td,
-.table thead:first-child tr:first-child th,
-.table thead:first-child tr:first-child td {
- border-top: 0;
-}
-
-.table tbody + tbody {
- border-top: 2px solid #dddddd;
-}
-
-.table .table {
- background-color: #ffffff;
-}
-
-.table-condensed th,
-.table-condensed td {
- padding: 4px 5px;
-}
-
-.table-bordered {
- border: 1px solid #dddddd;
- border-collapse: separate;
- *border-collapse: collapse;
- border-left: 0;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.table-bordered th,
-.table-bordered td {
- border-left: 1px solid #dddddd;
-}
-
-.table-bordered caption + thead tr:first-child th,
-.table-bordered caption + tbody tr:first-child th,
-.table-bordered caption + tbody tr:first-child td,
-.table-bordered colgroup + thead tr:first-child th,
-.table-bordered colgroup + tbody tr:first-child th,
-.table-bordered colgroup + tbody tr:first-child td,
-.table-bordered thead:first-child tr:first-child th,
-.table-bordered tbody:first-child tr:first-child th,
-.table-bordered tbody:first-child tr:first-child td {
- border-top: 0;
-}
-
-.table-bordered thead:first-child tr:first-child > th:first-child,
-.table-bordered tbody:first-child tr:first-child > td:first-child,
-.table-bordered tbody:first-child tr:first-child > th:first-child {
- -webkit-border-top-left-radius: 4px;
- border-top-left-radius: 4px;
- -moz-border-radius-topleft: 4px;
-}
-
-.table-bordered thead:first-child tr:first-child > th:last-child,
-.table-bordered tbody:first-child tr:first-child > td:last-child,
-.table-bordered tbody:first-child tr:first-child > th:last-child {
- -webkit-border-top-right-radius: 4px;
- border-top-right-radius: 4px;
- -moz-border-radius-topright: 4px;
-}
-
-.table-bordered thead:last-child tr:last-child > th:first-child,
-.table-bordered tbody:last-child tr:last-child > td:first-child,
-.table-bordered tbody:last-child tr:last-child > th:first-child,
-.table-bordered tfoot:last-child tr:last-child > td:first-child,
-.table-bordered tfoot:last-child tr:last-child > th:first-child {
- -webkit-border-bottom-left-radius: 4px;
- border-bottom-left-radius: 4px;
- -moz-border-radius-bottomleft: 4px;
-}
-
-.table-bordered thead:last-child tr:last-child > th:last-child,
-.table-bordered tbody:last-child tr:last-child > td:last-child,
-.table-bordered tbody:last-child tr:last-child > th:last-child,
-.table-bordered tfoot:last-child tr:last-child > td:last-child,
-.table-bordered tfoot:last-child tr:last-child > th:last-child {
- -webkit-border-bottom-right-radius: 4px;
- border-bottom-right-radius: 4px;
- -moz-border-radius-bottomright: 4px;
-}
-
-.table-bordered tfoot + tbody:last-child tr:last-child td:first-child {
- -webkit-border-bottom-left-radius: 0;
- border-bottom-left-radius: 0;
- -moz-border-radius-bottomleft: 0;
-}
-
-.table-bordered tfoot + tbody:last-child tr:last-child td:last-child {
- -webkit-border-bottom-right-radius: 0;
- border-bottom-right-radius: 0;
- -moz-border-radius-bottomright: 0;
-}
-
-.table-bordered caption + thead tr:first-child th:first-child,
-.table-bordered caption + tbody tr:first-child td:first-child,
-.table-bordered colgroup + thead tr:first-child th:first-child,
-.table-bordered colgroup + tbody tr:first-child td:first-child {
- -webkit-border-top-left-radius: 4px;
- border-top-left-radius: 4px;
- -moz-border-radius-topleft: 4px;
-}
-
-.table-bordered caption + thead tr:first-child th:last-child,
-.table-bordered caption + tbody tr:first-child td:last-child,
-.table-bordered colgroup + thead tr:first-child th:last-child,
-.table-bordered colgroup + tbody tr:first-child td:last-child {
- -webkit-border-top-right-radius: 4px;
- border-top-right-radius: 4px;
- -moz-border-radius-topright: 4px;
-}
-
-.table-striped tbody > tr:nth-child(odd) > td,
-.table-striped tbody > tr:nth-child(odd) > th {
- background-color: #f9f9f9;
-}
-
-.table-hover tbody tr:hover > td,
-.table-hover tbody tr:hover > th {
- background-color: #f5f5f5;
-}
-
-table td[class*="span"],
-table th[class*="span"],
-.row-fluid table td[class*="span"],
-.row-fluid table th[class*="span"] {
- display: table-cell;
- float: none;
- margin-left: 0;
-}
-
-.table td.span1,
-.table th.span1 {
- float: none;
- width: 44px;
- margin-left: 0;
-}
-
-.table td.span2,
-.table th.span2 {
- float: none;
- width: 124px;
- margin-left: 0;
-}
-
-.table td.span3,
-.table th.span3 {
- float: none;
- width: 204px;
- margin-left: 0;
-}
-
-.table td.span4,
-.table th.span4 {
- float: none;
- width: 284px;
- margin-left: 0;
-}
-
-.table td.span5,
-.table th.span5 {
- float: none;
- width: 364px;
- margin-left: 0;
-}
-
-.table td.span6,
-.table th.span6 {
- float: none;
- width: 444px;
- margin-left: 0;
-}
-
-.table td.span7,
-.table th.span7 {
- float: none;
- width: 524px;
- margin-left: 0;
-}
-
-.table td.span8,
-.table th.span8 {
- float: none;
- width: 604px;
- margin-left: 0;
-}
-
-.table td.span9,
-.table th.span9 {
- float: none;
- width: 684px;
- margin-left: 0;
-}
-
-.table td.span10,
-.table th.span10 {
- float: none;
- width: 764px;
- margin-left: 0;
-}
-
-.table td.span11,
-.table th.span11 {
- float: none;
- width: 844px;
- margin-left: 0;
-}
-
-.table td.span12,
-.table th.span12 {
- float: none;
- width: 924px;
- margin-left: 0;
-}
-
-.table tbody tr.success > td {
- background-color: #dff0d8;
-}
-
-.table tbody tr.error > td {
- background-color: #f2dede;
-}
-
-.table tbody tr.warning > td {
- background-color: #fcf8e3;
-}
-
-.table tbody tr.info > td {
- background-color: #d9edf7;
-}
-
-.table-hover tbody tr.success:hover > td {
- background-color: #d0e9c6;
-}
-
-.table-hover tbody tr.error:hover > td {
- background-color: #ebcccc;
-}
-
-.table-hover tbody tr.warning:hover > td {
- background-color: #faf2cc;
-}
-
-.table-hover tbody tr.info:hover > td {
- background-color: #c4e3f3;
-}
-
-[class^="icon-"],
-[class*=" icon-"] {
- display: inline-block;
- width: 14px;
- height: 14px;
- margin-top: 1px;
- *margin-right: .3em;
- line-height: 14px;
- vertical-align: text-top;
- background-image: url("../img/glyphicons-halflings.png");
- background-position: 14px 14px;
- background-repeat: no-repeat;
-}
-
-/* White icons with optional class, or on hover/focus/active states of certain elements */
-
-.icon-white,
-.nav-pills > .active > a > [class^="icon-"],
-.nav-pills > .active > a > [class*=" icon-"],
-.nav-list > .active > a > [class^="icon-"],
-.nav-list > .active > a > [class*=" icon-"],
-.navbar-inverse .nav > .active > a > [class^="icon-"],
-.navbar-inverse .nav > .active > a > [class*=" icon-"],
-.dropdown-menu > li > a:hover > [class^="icon-"],
-.dropdown-menu > li > a:focus > [class^="icon-"],
-.dropdown-menu > li > a:hover > [class*=" icon-"],
-.dropdown-menu > li > a:focus > [class*=" icon-"],
-.dropdown-menu > .active > a > [class^="icon-"],
-.dropdown-menu > .active > a > [class*=" icon-"],
-.dropdown-submenu:hover > a > [class^="icon-"],
-.dropdown-submenu:focus > a > [class^="icon-"],
-.dropdown-submenu:hover > a > [class*=" icon-"],
-.dropdown-submenu:focus > a > [class*=" icon-"] {
- background-image: url("../img/glyphicons-halflings-white.png");
-}
-
-.icon-glass {
- background-position: 0 0;
-}
-
-.icon-music {
- background-position: -24px 0;
-}
-
-.icon-search {
- background-position: -48px 0;
-}
-
-.icon-envelope {
- background-position: -72px 0;
-}
-
-.icon-heart {
- background-position: -96px 0;
-}
-
-.icon-star {
- background-position: -120px 0;
-}
-
-.icon-star-empty {
- background-position: -144px 0;
-}
-
-.icon-user {
- background-position: -168px 0;
-}
-
-.icon-film {
- background-position: -192px 0;
-}
-
-.icon-th-large {
- background-position: -216px 0;
-}
-
-.icon-th {
- background-position: -240px 0;
-}
-
-.icon-th-list {
- background-position: -264px 0;
-}
-
-.icon-ok {
- background-position: -288px 0;
-}
-
-.icon-remove {
- background-position: -312px 0;
-}
-
-.icon-zoom-in {
- background-position: -336px 0;
-}
-
-.icon-zoom-out {
- background-position: -360px 0;
-}
-
-.icon-off {
- background-position: -384px 0;
-}
-
-.icon-signal {
- background-position: -408px 0;
-}
-
-.icon-cog {
- background-position: -432px 0;
-}
-
-.icon-trash {
- background-position: -456px 0;
-}
-
-.icon-home {
- background-position: 0 -24px;
-}
-
-.icon-file {
- background-position: -24px -24px;
-}
-
-.icon-time {
- background-position: -48px -24px;
-}
-
-.icon-road {
- background-position: -72px -24px;
-}
-
-.icon-download-alt {
- background-position: -96px -24px;
-}
-
-.icon-download {
- background-position: -120px -24px;
-}
-
-.icon-upload {
- background-position: -144px -24px;
-}
-
-.icon-inbox {
- background-position: -168px -24px;
-}
-
-.icon-play-circle {
- background-position: -192px -24px;
-}
-
-.icon-repeat {
- background-position: -216px -24px;
-}
-
-.icon-refresh {
- background-position: -240px -24px;
-}
-
-.icon-list-alt {
- background-position: -264px -24px;
-}
-
-.icon-lock {
- background-position: -287px -24px;
-}
-
-.icon-flag {
- background-position: -312px -24px;
-}
-
-.icon-headphones {
- background-position: -336px -24px;
-}
-
-.icon-volume-off {
- background-position: -360px -24px;
-}
-
-.icon-volume-down {
- background-position: -384px -24px;
-}
-
-.icon-volume-up {
- background-position: -408px -24px;
-}
-
-.icon-qrcode {
- background-position: -432px -24px;
-}
-
-.icon-barcode {
- background-position: -456px -24px;
-}
-
-.icon-tag {
- background-position: 0 -48px;
-}
-
-.icon-tags {
- background-position: -25px -48px;
-}
-
-.icon-book {
- background-position: -48px -48px;
-}
-
-.icon-bookmark {
- background-position: -72px -48px;
-}
-
-.icon-print {
- background-position: -96px -48px;
-}
-
-.icon-camera {
- background-position: -120px -48px;
-}
-
-.icon-font {
- background-position: -144px -48px;
-}
-
-.icon-bold {
- background-position: -167px -48px;
-}
-
-.icon-italic {
- background-position: -192px -48px;
-}
-
-.icon-text-height {
- background-position: -216px -48px;
-}
-
-.icon-text-width {
- background-position: -240px -48px;
-}
-
-.icon-align-left {
- background-position: -264px -48px;
-}
-
-.icon-align-center {
- background-position: -288px -48px;
-}
-
-.icon-align-right {
- background-position: -312px -48px;
-}
-
-.icon-align-justify {
- background-position: -336px -48px;
-}
-
-.icon-list {
- background-position: -360px -48px;
-}
-
-.icon-indent-left {
- background-position: -384px -48px;
-}
-
-.icon-indent-right {
- background-position: -408px -48px;
-}
-
-.icon-facetime-video {
- background-position: -432px -48px;
-}
-
-.icon-picture {
- background-position: -456px -48px;
-}
-
-.icon-pencil {
- background-position: 0 -72px;
-}
-
-.icon-map-marker {
- background-position: -24px -72px;
-}
-
-.icon-adjust {
- background-position: -48px -72px;
-}
-
-.icon-tint {
- background-position: -72px -72px;
-}
-
-.icon-edit {
- background-position: -96px -72px;
-}
-
-.icon-share {
- background-position: -120px -72px;
-}
-
-.icon-check {
- background-position: -144px -72px;
-}
-
-.icon-move {
- background-position: -168px -72px;
-}
-
-.icon-step-backward {
- background-position: -192px -72px;
-}
-
-.icon-fast-backward {
- background-position: -216px -72px;
-}
-
-.icon-backward {
- background-position: -240px -72px;
-}
-
-.icon-play {
- background-position: -264px -72px;
-}
-
-.icon-pause {
- background-position: -288px -72px;
-}
-
-.icon-stop {
- background-position: -312px -72px;
-}
-
-.icon-forward {
- background-position: -336px -72px;
-}
-
-.icon-fast-forward {
- background-position: -360px -72px;
-}
-
-.icon-step-forward {
- background-position: -384px -72px;
-}
-
-.icon-eject {
- background-position: -408px -72px;
-}
-
-.icon-chevron-left {
- background-position: -432px -72px;
-}
-
-.icon-chevron-right {
- background-position: -456px -72px;
-}
-
-.icon-plus-sign {
- background-position: 0 -96px;
-}
-
-.icon-minus-sign {
- background-position: -24px -96px;
-}
-
-.icon-remove-sign {
- background-position: -48px -96px;
-}
-
-.icon-ok-sign {
- background-position: -72px -96px;
-}
-
-.icon-question-sign {
- background-position: -96px -96px;
-}
-
-.icon-info-sign {
- background-position: -120px -96px;
-}
-
-.icon-screenshot {
- background-position: -144px -96px;
-}
-
-.icon-remove-circle {
- background-position: -168px -96px;
-}
-
-.icon-ok-circle {
- background-position: -192px -96px;
-}
-
-.icon-ban-circle {
- background-position: -216px -96px;
-}
-
-.icon-arrow-left {
- background-position: -240px -96px;
-}
-
-.icon-arrow-right {
- background-position: -264px -96px;
-}
-
-.icon-arrow-up {
- background-position: -289px -96px;
-}
-
-.icon-arrow-down {
- background-position: -312px -96px;
-}
-
-.icon-share-alt {
- background-position: -336px -96px;
-}
-
-.icon-resize-full {
- background-position: -360px -96px;
-}
-
-.icon-resize-small {
- background-position: -384px -96px;
-}
-
-.icon-plus {
- background-position: -408px -96px;
-}
-
-.icon-minus {
- background-position: -433px -96px;
-}
-
-.icon-asterisk {
- background-position: -456px -96px;
-}
-
-.icon-exclamation-sign {
- background-position: 0 -120px;
-}
-
-.icon-gift {
- background-position: -24px -120px;
-}
-
-.icon-leaf {
- background-position: -48px -120px;
-}
-
-.icon-fire {
- background-position: -72px -120px;
-}
-
-.icon-eye-open {
- background-position: -96px -120px;
-}
-
-.icon-eye-close {
- background-position: -120px -120px;
-}
-
-.icon-warning-sign {
- background-position: -144px -120px;
-}
-
-.icon-plane {
- background-position: -168px -120px;
-}
-
-.icon-calendar {
- background-position: -192px -120px;
-}
-
-.icon-random {
- width: 16px;
- background-position: -216px -120px;
-}
-
-.icon-comment {
- background-position: -240px -120px;
-}
-
-.icon-magnet {
- background-position: -264px -120px;
-}
-
-.icon-chevron-up {
- background-position: -288px -120px;
-}
-
-.icon-chevron-down {
- background-position: -313px -119px;
-}
-
-.icon-retweet {
- background-position: -336px -120px;
-}
-
-.icon-shopping-cart {
- background-position: -360px -120px;
-}
-
-.icon-folder-close {
- width: 16px;
- background-position: -384px -120px;
-}
-
-.icon-folder-open {
- width: 16px;
- background-position: -408px -120px;
-}
-
-.icon-resize-vertical {
- background-position: -432px -119px;
-}
-
-.icon-resize-horizontal {
- background-position: -456px -118px;
-}
-
-.icon-hdd {
- background-position: 0 -144px;
-}
-
-.icon-bullhorn {
- background-position: -24px -144px;
-}
-
-.icon-bell {
- background-position: -48px -144px;
-}
-
-.icon-certificate {
- background-position: -72px -144px;
-}
-
-.icon-thumbs-up {
- background-position: -96px -144px;
-}
-
-.icon-thumbs-down {
- background-position: -120px -144px;
-}
-
-.icon-hand-right {
- background-position: -144px -144px;
-}
-
-.icon-hand-left {
- background-position: -168px -144px;
-}
-
-.icon-hand-up {
- background-position: -192px -144px;
-}
-
-.icon-hand-down {
- background-position: -216px -144px;
-}
-
-.icon-circle-arrow-right {
- background-position: -240px -144px;
-}
-
-.icon-circle-arrow-left {
- background-position: -264px -144px;
-}
-
-.icon-circle-arrow-up {
- background-position: -288px -144px;
-}
-
-.icon-circle-arrow-down {
- background-position: -312px -144px;
-}
-
-.icon-globe {
- background-position: -336px -144px;
-}
-
-.icon-wrench {
- background-position: -360px -144px;
-}
-
-.icon-tasks {
- background-position: -384px -144px;
-}
-
-.icon-filter {
- background-position: -408px -144px;
-}
-
-.icon-briefcase {
- background-position: -432px -144px;
-}
-
-.icon-fullscreen {
- background-position: -456px -144px;
-}
-
-.dropup,
-.dropdown {
- position: relative;
-}
-
-.dropdown-toggle {
- *margin-bottom: -3px;
-}
-
-.dropdown-toggle:active,
-.open .dropdown-toggle {
- outline: 0;
-}
-
-.caret {
- display: inline-block;
- width: 0;
- height: 0;
- vertical-align: top;
- border-top: 4px solid #000000;
- border-right: 4px solid transparent;
- border-left: 4px solid transparent;
- content: "";
-}
-
-.dropdown .caret {
- margin-top: 8px;
- margin-left: 2px;
-}
-
-.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- display: none;
- float: left;
- min-width: 160px;
- padding: 5px 0;
- margin: 2px 0 0;
- list-style: none;
- background-color: #ffffff;
- border: 1px solid #ccc;
- border: 1px solid rgba(0, 0, 0, 0.2);
- *border-right-width: 2px;
- *border-bottom-width: 2px;
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
- -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- -webkit-background-clip: padding-box;
- -moz-background-clip: padding;
- background-clip: padding-box;
-}
-
-.dropdown-menu.pull-right {
- right: 0;
- left: auto;
-}
-
-.dropdown-menu .divider {
- *width: 100%;
- height: 1px;
- margin: 9px 1px;
- *margin: -5px 0 5px;
- overflow: hidden;
- background-color: #e5e5e5;
- border-bottom: 1px solid #ffffff;
-}
-
-.dropdown-menu > li > a {
- display: block;
- padding: 3px 20px;
- clear: both;
- font-weight: normal;
- line-height: 20px;
- color: #333333;
- white-space: nowrap;
-}
-
-.dropdown-menu > li > a:hover,
-.dropdown-menu > li > a:focus,
-.dropdown-submenu:hover > a,
-.dropdown-submenu:focus > a {
- color: #ffffff;
- text-decoration: none;
- background-color: #0081c2;
- background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
- background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
- background-image: -o-linear-gradient(top, #0088cc, #0077b3);
- background-image: linear-gradient(to bottom, #0088cc, #0077b3);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
-}
-
-.dropdown-menu > .active > a,
-.dropdown-menu > .active > a:hover,
-.dropdown-menu > .active > a:focus {
- color: #ffffff;
- text-decoration: none;
- background-color: #0081c2;
- background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
- background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
- background-image: -o-linear-gradient(top, #0088cc, #0077b3);
- background-image: linear-gradient(to bottom, #0088cc, #0077b3);
- background-repeat: repeat-x;
- outline: 0;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);
-}
-
-.dropdown-menu > .disabled > a,
-.dropdown-menu > .disabled > a:hover,
-.dropdown-menu > .disabled > a:focus {
- color: #999999;
-}
-
-.dropdown-menu > .disabled > a:hover,
-.dropdown-menu > .disabled > a:focus {
- text-decoration: none;
- cursor: default;
- background-color: transparent;
- background-image: none;
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.open {
- *z-index: 1000;
-}
-
-.open > .dropdown-menu {
- display: block;
-}
-
-.pull-right > .dropdown-menu {
- right: 0;
- left: auto;
-}
-
-.dropup .caret,
-.navbar-fixed-bottom .dropdown .caret {
- border-top: 0;
- border-bottom: 4px solid #000000;
- content: "";
-}
-
-.dropup .dropdown-menu,
-.navbar-fixed-bottom .dropdown .dropdown-menu {
- top: auto;
- bottom: 100%;
- margin-bottom: 1px;
-}
-
-.dropdown-submenu {
- position: relative;
-}
-
-.dropdown-submenu > .dropdown-menu {
- top: 0;
- left: 100%;
- margin-top: -6px;
- margin-left: -1px;
- -webkit-border-radius: 0 6px 6px 6px;
- -moz-border-radius: 0 6px 6px 6px;
- border-radius: 0 6px 6px 6px;
-}
-
-.dropdown-submenu:hover > .dropdown-menu {
- display: block;
-}
-
-.dropup .dropdown-submenu > .dropdown-menu {
- top: auto;
- bottom: 0;
- margin-top: 0;
- margin-bottom: -2px;
- -webkit-border-radius: 5px 5px 5px 0;
- -moz-border-radius: 5px 5px 5px 0;
- border-radius: 5px 5px 5px 0;
-}
-
-.dropdown-submenu > a:after {
- display: block;
- float: right;
- width: 0;
- height: 0;
- margin-top: 5px;
- margin-right: -10px;
- border-color: transparent;
- border-left-color: #cccccc;
- border-style: solid;
- border-width: 5px 0 5px 5px;
- content: " ";
-}
-
-.dropdown-submenu:hover > a:after {
- border-left-color: #ffffff;
-}
-
-.dropdown-submenu.pull-left {
- float: none;
-}
-
-.dropdown-submenu.pull-left > .dropdown-menu {
- left: -100%;
- margin-left: 10px;
- -webkit-border-radius: 6px 0 6px 6px;
- -moz-border-radius: 6px 0 6px 6px;
- border-radius: 6px 0 6px 6px;
-}
-
-.dropdown .dropdown-menu .nav-header {
- padding-right: 20px;
- padding-left: 20px;
-}
-
-.typeahead {
- z-index: 1051;
- margin-top: 2px;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.well {
- min-height: 20px;
- padding: 19px;
- margin-bottom: 20px;
- background-color: #f5f5f5;
- border: 1px solid #e3e3e3;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.well blockquote {
- border-color: #ddd;
- border-color: rgba(0, 0, 0, 0.15);
-}
-
-.well-large {
- padding: 24px;
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
-}
-
-.well-small {
- padding: 9px;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-
-.fade {
- opacity: 0;
- -webkit-transition: opacity 0.15s linear;
- -moz-transition: opacity 0.15s linear;
- -o-transition: opacity 0.15s linear;
- transition: opacity 0.15s linear;
-}
-
-.fade.in {
- opacity: 1;
-}
-
-.collapse {
- position: relative;
- height: 0;
- overflow: hidden;
- -webkit-transition: height 0.35s ease;
- -moz-transition: height 0.35s ease;
- -o-transition: height 0.35s ease;
- transition: height 0.35s ease;
-}
-
-.collapse.in {
- height: auto;
-}
-
-.close {
- float: right;
- font-size: 20px;
- font-weight: bold;
- line-height: 20px;
- color: #000000;
- text-shadow: 0 1px 0 #ffffff;
- opacity: 0.2;
- filter: alpha(opacity=20);
-}
-
-.close:hover,
-.close:focus {
- color: #000000;
- text-decoration: none;
- cursor: pointer;
- opacity: 0.4;
- filter: alpha(opacity=40);
-}
-
-button.close {
- padding: 0;
- cursor: pointer;
- background: transparent;
- border: 0;
- -webkit-appearance: none;
-}
-
-.btn {
- display: inline-block;
- *display: inline;
- padding: 4px 12px;
- margin-bottom: 0;
- *margin-left: .3em;
- font-size: 14px;
- line-height: 20px;
- color: #333333;
- text-align: center;
- text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
- vertical-align: middle;
- cursor: pointer;
- background-color: #f5f5f5;
- *background-color: #e6e6e6;
- background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
- background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
- background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
- background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
- background-repeat: repeat-x;
- border: 1px solid #cccccc;
- *border: 0;
- border-color: #e6e6e6 #e6e6e6 #bfbfbf;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- border-bottom-color: #b3b3b3;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
- *zoom: 1;
- -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.btn:hover,
-.btn:focus,
-.btn:active,
-.btn.active,
-.btn.disabled,
-.btn[disabled] {
- color: #333333;
- background-color: #e6e6e6;
- *background-color: #d9d9d9;
-}
-
-.btn:active,
-.btn.active {
- background-color: #cccccc \9;
-}
-
-.btn:first-child {
- *margin-left: 0;
-}
-
-.btn:hover,
-.btn:focus {
- color: #333333;
- text-decoration: none;
- background-position: 0 -15px;
- -webkit-transition: background-position 0.1s linear;
- -moz-transition: background-position 0.1s linear;
- -o-transition: background-position 0.1s linear;
- transition: background-position 0.1s linear;
-}
-
-.btn:focus {
- outline: thin dotted #333;
- outline: 5px auto -webkit-focus-ring-color;
- outline-offset: -2px;
-}
-
-.btn.active,
-.btn:active {
- background-image: none;
- outline: 0;
- -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.btn.disabled,
-.btn[disabled] {
- cursor: default;
- background-image: none;
- opacity: 0.65;
- filter: alpha(opacity=65);
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
-}
-
-.btn-large {
- padding: 11px 19px;
- font-size: 17.5px;
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
-}
-
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
- margin-top: 4px;
-}
-
-.btn-small {
- padding: 2px 10px;
- font-size: 11.9px;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
- margin-top: 0;
-}
-
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
- margin-top: -1px;
-}
-
-.btn-mini {
- padding: 0 6px;
- font-size: 10.5px;
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-
-.btn-block {
- display: block;
- width: 100%;
- padding-right: 0;
- padding-left: 0;
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-.btn-block + .btn-block {
- margin-top: 5px;
-}
-
-input[type="submit"].btn-block,
-input[type="reset"].btn-block,
-input[type="button"].btn-block {
- width: 100%;
-}
-
-.btn-primary.active,
-.btn-warning.active,
-.btn-danger.active,
-.btn-success.active,
-.btn-info.active,
-.btn-inverse.active {
- color: rgba(255, 255, 255, 0.75);
-}
-
-.btn-primary {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #006dcc;
- *background-color: #0044cc;
- background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
- background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
- background-image: -o-linear-gradient(top, #0088cc, #0044cc);
- background-image: linear-gradient(to bottom, #0088cc, #0044cc);
- background-repeat: repeat-x;
- border-color: #0044cc #0044cc #002a80;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-primary:hover,
-.btn-primary:focus,
-.btn-primary:active,
-.btn-primary.active,
-.btn-primary.disabled,
-.btn-primary[disabled] {
- color: #ffffff;
- background-color: #0044cc;
- *background-color: #003bb3;
-}
-
-.btn-primary:active,
-.btn-primary.active {
- background-color: #003399 \9;
-}
-
-.btn-warning {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #faa732;
- *background-color: #f89406;
- background-image: -moz-linear-gradient(top, #fbb450, #f89406);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));
- background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
- background-image: -o-linear-gradient(top, #fbb450, #f89406);
- background-image: linear-gradient(to bottom, #fbb450, #f89406);
- background-repeat: repeat-x;
- border-color: #f89406 #f89406 #ad6704;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-warning:hover,
-.btn-warning:focus,
-.btn-warning:active,
-.btn-warning.active,
-.btn-warning.disabled,
-.btn-warning[disabled] {
- color: #ffffff;
- background-color: #f89406;
- *background-color: #df8505;
-}
-
-.btn-warning:active,
-.btn-warning.active {
- background-color: #c67605 \9;
-}
-
-.btn-danger {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #da4f49;
- *background-color: #bd362f;
- background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));
- background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f);
- background-image: -o-linear-gradient(top, #ee5f5b, #bd362f);
- background-image: linear-gradient(to bottom, #ee5f5b, #bd362f);
- background-repeat: repeat-x;
- border-color: #bd362f #bd362f #802420;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-danger:hover,
-.btn-danger:focus,
-.btn-danger:active,
-.btn-danger.active,
-.btn-danger.disabled,
-.btn-danger[disabled] {
- color: #ffffff;
- background-color: #bd362f;
- *background-color: #a9302a;
-}
-
-.btn-danger:active,
-.btn-danger.active {
- background-color: #942a25 \9;
-}
-
-.btn-success {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #5bb75b;
- *background-color: #51a351;
- background-image: -moz-linear-gradient(top, #62c462, #51a351);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));
- background-image: -webkit-linear-gradient(top, #62c462, #51a351);
- background-image: -o-linear-gradient(top, #62c462, #51a351);
- background-image: linear-gradient(to bottom, #62c462, #51a351);
- background-repeat: repeat-x;
- border-color: #51a351 #51a351 #387038;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-success:hover,
-.btn-success:focus,
-.btn-success:active,
-.btn-success.active,
-.btn-success.disabled,
-.btn-success[disabled] {
- color: #ffffff;
- background-color: #51a351;
- *background-color: #499249;
-}
-
-.btn-success:active,
-.btn-success.active {
- background-color: #408140 \9;
-}
-
-.btn-info {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #49afcd;
- *background-color: #2f96b4;
- background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));
- background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4);
- background-image: -o-linear-gradient(top, #5bc0de, #2f96b4);
- background-image: linear-gradient(to bottom, #5bc0de, #2f96b4);
- background-repeat: repeat-x;
- border-color: #2f96b4 #2f96b4 #1f6377;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-info:hover,
-.btn-info:focus,
-.btn-info:active,
-.btn-info.active,
-.btn-info.disabled,
-.btn-info[disabled] {
- color: #ffffff;
- background-color: #2f96b4;
- *background-color: #2a85a0;
-}
-
-.btn-info:active,
-.btn-info.active {
- background-color: #24748c \9;
-}
-
-.btn-inverse {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #363636;
- *background-color: #222222;
- background-image: -moz-linear-gradient(top, #444444, #222222);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222));
- background-image: -webkit-linear-gradient(top, #444444, #222222);
- background-image: -o-linear-gradient(top, #444444, #222222);
- background-image: linear-gradient(to bottom, #444444, #222222);
- background-repeat: repeat-x;
- border-color: #222222 #222222 #000000;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.btn-inverse:hover,
-.btn-inverse:focus,
-.btn-inverse:active,
-.btn-inverse.active,
-.btn-inverse.disabled,
-.btn-inverse[disabled] {
- color: #ffffff;
- background-color: #222222;
- *background-color: #151515;
-}
-
-.btn-inverse:active,
-.btn-inverse.active {
- background-color: #080808 \9;
-}
-
-button.btn,
-input[type="submit"].btn {
- *padding-top: 3px;
- *padding-bottom: 3px;
-}
-
-button.btn::-moz-focus-inner,
-input[type="submit"].btn::-moz-focus-inner {
- padding: 0;
- border: 0;
-}
-
-button.btn.btn-large,
-input[type="submit"].btn.btn-large {
- *padding-top: 7px;
- *padding-bottom: 7px;
-}
-
-button.btn.btn-small,
-input[type="submit"].btn.btn-small {
- *padding-top: 3px;
- *padding-bottom: 3px;
-}
-
-button.btn.btn-mini,
-input[type="submit"].btn.btn-mini {
- *padding-top: 1px;
- *padding-bottom: 1px;
-}
-
-.btn-link,
-.btn-link:active,
-.btn-link[disabled] {
- background-color: transparent;
- background-image: none;
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
-}
-
-.btn-link {
- color: #0088cc;
- cursor: pointer;
- border-color: transparent;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.btn-link:hover,
-.btn-link:focus {
- color: #005580;
- text-decoration: underline;
- background-color: transparent;
-}
-
-.btn-link[disabled]:hover,
-.btn-link[disabled]:focus {
- color: #333333;
- text-decoration: none;
-}
-
-.btn-group {
- position: relative;
- display: inline-block;
- *display: inline;
- *margin-left: .3em;
- font-size: 0;
- white-space: nowrap;
- vertical-align: middle;
- *zoom: 1;
-}
-
-.btn-group:first-child {
- *margin-left: 0;
-}
-
-.btn-group + .btn-group {
- margin-left: 5px;
-}
-
-.btn-toolbar {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 0;
-}
-
-.btn-toolbar > .btn + .btn,
-.btn-toolbar > .btn-group + .btn,
-.btn-toolbar > .btn + .btn-group {
- margin-left: 5px;
-}
-
-.btn-group > .btn {
- position: relative;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.btn-group > .btn + .btn {
- margin-left: -1px;
-}
-
-.btn-group > .btn,
-.btn-group > .dropdown-menu,
-.btn-group > .popover {
- font-size: 14px;
-}
-
-.btn-group > .btn-mini {
- font-size: 10.5px;
-}
-
-.btn-group > .btn-small {
- font-size: 11.9px;
-}
-
-.btn-group > .btn-large {
- font-size: 17.5px;
-}
-
-.btn-group > .btn:first-child {
- margin-left: 0;
- -webkit-border-bottom-left-radius: 4px;
- border-bottom-left-radius: 4px;
- -webkit-border-top-left-radius: 4px;
- border-top-left-radius: 4px;
- -moz-border-radius-bottomleft: 4px;
- -moz-border-radius-topleft: 4px;
-}
-
-.btn-group > .btn:last-child,
-.btn-group > .dropdown-toggle {
- -webkit-border-top-right-radius: 4px;
- border-top-right-radius: 4px;
- -webkit-border-bottom-right-radius: 4px;
- border-bottom-right-radius: 4px;
- -moz-border-radius-topright: 4px;
- -moz-border-radius-bottomright: 4px;
-}
-
-.btn-group > .btn.large:first-child {
- margin-left: 0;
- -webkit-border-bottom-left-radius: 6px;
- border-bottom-left-radius: 6px;
- -webkit-border-top-left-radius: 6px;
- border-top-left-radius: 6px;
- -moz-border-radius-bottomleft: 6px;
- -moz-border-radius-topleft: 6px;
-}
-
-.btn-group > .btn.large:last-child,
-.btn-group > .large.dropdown-toggle {
- -webkit-border-top-right-radius: 6px;
- border-top-right-radius: 6px;
- -webkit-border-bottom-right-radius: 6px;
- border-bottom-right-radius: 6px;
- -moz-border-radius-topright: 6px;
- -moz-border-radius-bottomright: 6px;
-}
-
-.btn-group > .btn:hover,
-.btn-group > .btn:focus,
-.btn-group > .btn:active,
-.btn-group > .btn.active {
- z-index: 2;
-}
-
-.btn-group .dropdown-toggle:active,
-.btn-group.open .dropdown-toggle {
- outline: 0;
-}
-
-.btn-group > .btn + .dropdown-toggle {
- *padding-top: 5px;
- padding-right: 8px;
- *padding-bottom: 5px;
- padding-left: 8px;
- -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.btn-group > .btn-mini + .dropdown-toggle {
- *padding-top: 2px;
- padding-right: 5px;
- *padding-bottom: 2px;
- padding-left: 5px;
-}
-
-.btn-group > .btn-small + .dropdown-toggle {
- *padding-top: 5px;
- *padding-bottom: 4px;
-}
-
-.btn-group > .btn-large + .dropdown-toggle {
- *padding-top: 7px;
- padding-right: 12px;
- *padding-bottom: 7px;
- padding-left: 12px;
-}
-
-.btn-group.open .dropdown-toggle {
- background-image: none;
- -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.btn-group.open .btn.dropdown-toggle {
- background-color: #e6e6e6;
-}
-
-.btn-group.open .btn-primary.dropdown-toggle {
- background-color: #0044cc;
-}
-
-.btn-group.open .btn-warning.dropdown-toggle {
- background-color: #f89406;
-}
-
-.btn-group.open .btn-danger.dropdown-toggle {
- background-color: #bd362f;
-}
-
-.btn-group.open .btn-success.dropdown-toggle {
- background-color: #51a351;
-}
-
-.btn-group.open .btn-info.dropdown-toggle {
- background-color: #2f96b4;
-}
-
-.btn-group.open .btn-inverse.dropdown-toggle {
- background-color: #222222;
-}
-
-.btn .caret {
- margin-top: 8px;
- margin-left: 0;
-}
-
-.btn-large .caret {
- margin-top: 6px;
-}
-
-.btn-large .caret {
- border-top-width: 5px;
- border-right-width: 5px;
- border-left-width: 5px;
-}
-
-.btn-mini .caret,
-.btn-small .caret {
- margin-top: 8px;
-}
-
-.dropup .btn-large .caret {
- border-bottom-width: 5px;
-}
-
-.btn-primary .caret,
-.btn-warning .caret,
-.btn-danger .caret,
-.btn-info .caret,
-.btn-success .caret,
-.btn-inverse .caret {
- border-top-color: #ffffff;
- border-bottom-color: #ffffff;
-}
-
-.btn-group-vertical {
- display: inline-block;
- *display: inline;
- /* IE7 inline-block hack */
-
- *zoom: 1;
-}
-
-.btn-group-vertical > .btn {
- display: block;
- float: none;
- max-width: 100%;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.btn-group-vertical > .btn + .btn {
- margin-top: -1px;
- margin-left: 0;
-}
-
-.btn-group-vertical > .btn:first-child {
- -webkit-border-radius: 4px 4px 0 0;
- -moz-border-radius: 4px 4px 0 0;
- border-radius: 4px 4px 0 0;
-}
-
-.btn-group-vertical > .btn:last-child {
- -webkit-border-radius: 0 0 4px 4px;
- -moz-border-radius: 0 0 4px 4px;
- border-radius: 0 0 4px 4px;
-}
-
-.btn-group-vertical > .btn-large:first-child {
- -webkit-border-radius: 6px 6px 0 0;
- -moz-border-radius: 6px 6px 0 0;
- border-radius: 6px 6px 0 0;
-}
-
-.btn-group-vertical > .btn-large:last-child {
- -webkit-border-radius: 0 0 6px 6px;
- -moz-border-radius: 0 0 6px 6px;
- border-radius: 0 0 6px 6px;
-}
-
-.alert {
- padding: 8px 35px 8px 14px;
- margin-bottom: 20px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- background-color: #fcf8e3;
- border: 1px solid #fbeed5;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.alert,
-.alert h4 {
- color: #c09853;
-}
-
-.alert h4 {
- margin: 0;
-}
-
-.alert .close {
- position: relative;
- top: -2px;
- right: -21px;
- line-height: 20px;
-}
-
-.alert-success {
- color: #468847;
- background-color: #dff0d8;
- border-color: #d6e9c6;
-}
-
-.alert-success h4 {
- color: #468847;
-}
-
-.alert-danger,
-.alert-error {
- color: #b94a48;
- background-color: #f2dede;
- border-color: #eed3d7;
-}
-
-.alert-danger h4,
-.alert-error h4 {
- color: #b94a48;
-}
-
-.alert-info {
- color: #3a87ad;
- background-color: #d9edf7;
- border-color: #bce8f1;
-}
-
-.alert-info h4 {
- color: #3a87ad;
-}
-
-.alert-block {
- padding-top: 14px;
- padding-bottom: 14px;
-}
-
-.alert-block > p,
-.alert-block > ul {
- margin-bottom: 0;
-}
-
-.alert-block p + p {
- margin-top: 5px;
-}
-
-.nav {
- margin-bottom: 20px;
- margin-left: 0;
- list-style: none;
-}
-
-.nav > li > a {
- display: block;
-}
-
-.nav > li > a:hover,
-.nav > li > a:focus {
- text-decoration: none;
- background-color: #eeeeee;
-}
-
-.nav > li > a > img {
- max-width: none;
-}
-
-.nav > .pull-right {
- float: right;
-}
-
-.nav-header {
- display: block;
- padding: 3px 15px;
- font-size: 11px;
- font-weight: bold;
- line-height: 20px;
- color: #999999;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- text-transform: uppercase;
-}
-
-.nav li + .nav-header {
- margin-top: 9px;
-}
-
-.nav-list {
- padding-right: 15px;
- padding-left: 15px;
- margin-bottom: 0;
-}
-
-.nav-list > li > a,
-.nav-list .nav-header {
- margin-right: -15px;
- margin-left: -15px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
-}
-
-.nav-list > li > a {
- padding: 3px 15px;
-}
-
-.nav-list > .active > a,
-.nav-list > .active > a:hover,
-.nav-list > .active > a:focus {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
- background-color: #0088cc;
-}
-
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
- margin-right: 2px;
-}
-
-.nav-list .divider {
- *width: 100%;
- height: 1px;
- margin: 9px 1px;
- *margin: -5px 0 5px;
- overflow: hidden;
- background-color: #e5e5e5;
- border-bottom: 1px solid #ffffff;
-}
-
-.nav-tabs,
-.nav-pills {
- *zoom: 1;
-}
-
-.nav-tabs:before,
-.nav-pills:before,
-.nav-tabs:after,
-.nav-pills:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.nav-tabs:after,
-.nav-pills:after {
- clear: both;
-}
-
-.nav-tabs > li,
-.nav-pills > li {
- float: left;
-}
-
-.nav-tabs > li > a,
-.nav-pills > li > a {
- padding-right: 12px;
- padding-left: 12px;
- margin-right: 2px;
- line-height: 14px;
-}
-
-.nav-tabs {
- border-bottom: 1px solid #ddd;
-}
-
-.nav-tabs > li {
- margin-bottom: -1px;
-}
-
-.nav-tabs > li > a {
- padding-top: 8px;
- padding-bottom: 8px;
- line-height: 20px;
- border: 1px solid transparent;
- -webkit-border-radius: 4px 4px 0 0;
- -moz-border-radius: 4px 4px 0 0;
- border-radius: 4px 4px 0 0;
-}
-
-.nav-tabs > li > a:hover,
-.nav-tabs > li > a:focus {
- border-color: #eeeeee #eeeeee #dddddd;
-}
-
-.nav-tabs > .active > a,
-.nav-tabs > .active > a:hover,
-.nav-tabs > .active > a:focus {
- color: #555555;
- cursor: default;
- background-color: #ffffff;
- border: 1px solid #ddd;
- border-bottom-color: transparent;
-}
-
-.nav-pills > li > a {
- padding-top: 8px;
- padding-bottom: 8px;
- margin-top: 2px;
- margin-bottom: 2px;
- -webkit-border-radius: 5px;
- -moz-border-radius: 5px;
- border-radius: 5px;
-}
-
-.nav-pills > .active > a,
-.nav-pills > .active > a:hover,
-.nav-pills > .active > a:focus {
- color: #ffffff;
- background-color: #0088cc;
-}
-
-.nav-stacked > li {
- float: none;
-}
-
-.nav-stacked > li > a {
- margin-right: 0;
-}
-
-.nav-tabs.nav-stacked {
- border-bottom: 0;
-}
-
-.nav-tabs.nav-stacked > li > a {
- border: 1px solid #ddd;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.nav-tabs.nav-stacked > li:first-child > a {
- -webkit-border-top-right-radius: 4px;
- border-top-right-radius: 4px;
- -webkit-border-top-left-radius: 4px;
- border-top-left-radius: 4px;
- -moz-border-radius-topright: 4px;
- -moz-border-radius-topleft: 4px;
-}
-
-.nav-tabs.nav-stacked > li:last-child > a {
- -webkit-border-bottom-right-radius: 4px;
- border-bottom-right-radius: 4px;
- -webkit-border-bottom-left-radius: 4px;
- border-bottom-left-radius: 4px;
- -moz-border-radius-bottomright: 4px;
- -moz-border-radius-bottomleft: 4px;
-}
-
-.nav-tabs.nav-stacked > li > a:hover,
-.nav-tabs.nav-stacked > li > a:focus {
- z-index: 2;
- border-color: #ddd;
-}
-
-.nav-pills.nav-stacked > li > a {
- margin-bottom: 3px;
-}
-
-.nav-pills.nav-stacked > li:last-child > a {
- margin-bottom: 1px;
-}
-
-.nav-tabs .dropdown-menu {
- -webkit-border-radius: 0 0 6px 6px;
- -moz-border-radius: 0 0 6px 6px;
- border-radius: 0 0 6px 6px;
-}
-
-.nav-pills .dropdown-menu {
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
-}
-
-.nav .dropdown-toggle .caret {
- margin-top: 6px;
- border-top-color: #0088cc;
- border-bottom-color: #0088cc;
-}
-
-.nav .dropdown-toggle:hover .caret,
-.nav .dropdown-toggle:focus .caret {
- border-top-color: #005580;
- border-bottom-color: #005580;
-}
-
-/* move down carets for tabs */
-
-.nav-tabs .dropdown-toggle .caret {
- margin-top: 8px;
-}
-
-.nav .active .dropdown-toggle .caret {
- border-top-color: #fff;
- border-bottom-color: #fff;
-}
-
-.nav-tabs .active .dropdown-toggle .caret {
- border-top-color: #555555;
- border-bottom-color: #555555;
-}
-
-.nav > .dropdown.active > a:hover,
-.nav > .dropdown.active > a:focus {
- cursor: pointer;
-}
-
-.nav-tabs .open .dropdown-toggle,
-.nav-pills .open .dropdown-toggle,
-.nav > li.dropdown.open.active > a:hover,
-.nav > li.dropdown.open.active > a:focus {
- color: #ffffff;
- background-color: #999999;
- border-color: #999999;
-}
-
-.nav li.dropdown.open .caret,
-.nav li.dropdown.open.active .caret,
-.nav li.dropdown.open a:hover .caret,
-.nav li.dropdown.open a:focus .caret {
- border-top-color: #ffffff;
- border-bottom-color: #ffffff;
- opacity: 1;
- filter: alpha(opacity=100);
-}
-
-.tabs-stacked .open > a:hover,
-.tabs-stacked .open > a:focus {
- border-color: #999999;
-}
-
-.tabbable {
- *zoom: 1;
-}
-
-.tabbable:before,
-.tabbable:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.tabbable:after {
- clear: both;
-}
-
-.tab-content {
- overflow: auto;
-}
-
-.tabs-below > .nav-tabs,
-.tabs-right > .nav-tabs,
-.tabs-left > .nav-tabs {
- border-bottom: 0;
-}
-
-.tab-content > .tab-pane,
-.pill-content > .pill-pane {
- display: none;
-}
-
-.tab-content > .active,
-.pill-content > .active {
- display: block;
-}
-
-.tabs-below > .nav-tabs {
- border-top: 1px solid #ddd;
-}
-
-.tabs-below > .nav-tabs > li {
- margin-top: -1px;
- margin-bottom: 0;
-}
-
-.tabs-below > .nav-tabs > li > a {
- -webkit-border-radius: 0 0 4px 4px;
- -moz-border-radius: 0 0 4px 4px;
- border-radius: 0 0 4px 4px;
-}
-
-.tabs-below > .nav-tabs > li > a:hover,
-.tabs-below > .nav-tabs > li > a:focus {
- border-top-color: #ddd;
- border-bottom-color: transparent;
-}
-
-.tabs-below > .nav-tabs > .active > a,
-.tabs-below > .nav-tabs > .active > a:hover,
-.tabs-below > .nav-tabs > .active > a:focus {
- border-color: transparent #ddd #ddd #ddd;
-}
-
-.tabs-left > .nav-tabs > li,
-.tabs-right > .nav-tabs > li {
- float: none;
-}
-
-.tabs-left > .nav-tabs > li > a,
-.tabs-right > .nav-tabs > li > a {
- min-width: 74px;
- margin-right: 0;
- margin-bottom: 3px;
-}
-
-.tabs-left > .nav-tabs {
- float: left;
- margin-right: 19px;
- border-right: 1px solid #ddd;
-}
-
-.tabs-left > .nav-tabs > li > a {
- margin-right: -1px;
- -webkit-border-radius: 4px 0 0 4px;
- -moz-border-radius: 4px 0 0 4px;
- border-radius: 4px 0 0 4px;
-}
-
-.tabs-left > .nav-tabs > li > a:hover,
-.tabs-left > .nav-tabs > li > a:focus {
- border-color: #eeeeee #dddddd #eeeeee #eeeeee;
-}
-
-.tabs-left > .nav-tabs .active > a,
-.tabs-left > .nav-tabs .active > a:hover,
-.tabs-left > .nav-tabs .active > a:focus {
- border-color: #ddd transparent #ddd #ddd;
- *border-right-color: #ffffff;
-}
-
-.tabs-right > .nav-tabs {
- float: right;
- margin-left: 19px;
- border-left: 1px solid #ddd;
-}
-
-.tabs-right > .nav-tabs > li > a {
- margin-left: -1px;
- -webkit-border-radius: 0 4px 4px 0;
- -moz-border-radius: 0 4px 4px 0;
- border-radius: 0 4px 4px 0;
-}
-
-.tabs-right > .nav-tabs > li > a:hover,
-.tabs-right > .nav-tabs > li > a:focus {
- border-color: #eeeeee #eeeeee #eeeeee #dddddd;
-}
-
-.tabs-right > .nav-tabs .active > a,
-.tabs-right > .nav-tabs .active > a:hover,
-.tabs-right > .nav-tabs .active > a:focus {
- border-color: #ddd #ddd #ddd transparent;
- *border-left-color: #ffffff;
-}
-
-.nav > .disabled > a {
- color: #999999;
-}
-
-.nav > .disabled > a:hover,
-.nav > .disabled > a:focus {
- text-decoration: none;
- cursor: default;
- background-color: transparent;
-}
-
-.navbar {
- *position: relative;
- *z-index: 2;
- margin-bottom: 20px;
- overflow: visible;
-}
-
-.navbar-inner {
- min-height: 40px;
- padding-right: 20px;
- padding-left: 20px;
- background-color: #fafafa;
- background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));
- background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2);
- background-image: -o-linear-gradient(top, #ffffff, #f2f2f2);
- background-image: linear-gradient(to bottom, #ffffff, #f2f2f2);
- background-repeat: repeat-x;
- border: 1px solid #d4d4d4;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);
- *zoom: 1;
- -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
- -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
-}
-
-.navbar-inner:before,
-.navbar-inner:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.navbar-inner:after {
- clear: both;
-}
-
-.navbar .container {
- width: auto;
-}
-
-.nav-collapse.collapse {
- height: auto;
- overflow: visible;
-}
-
-.navbar .brand {
- display: block;
- float: left;
- padding: 10px 20px 10px;
- margin-left: -20px;
- font-size: 20px;
- font-weight: 200;
- color: #777777;
- text-shadow: 0 1px 0 #ffffff;
-}
-
-.navbar .brand:hover,
-.navbar .brand:focus {
- text-decoration: none;
-}
-
-.navbar-text {
- margin-bottom: 0;
- line-height: 40px;
- color: #777777;
-}
-
-.navbar-link {
- color: #777777;
-}
-
-.navbar-link:hover,
-.navbar-link:focus {
- color: #333333;
-}
-
-.navbar .divider-vertical {
- height: 40px;
- margin: 0 9px;
- border-right: 1px solid #ffffff;
- border-left: 1px solid #f2f2f2;
-}
-
-.navbar .btn,
-.navbar .btn-group {
- margin-top: 5px;
-}
-
-.navbar .btn-group .btn,
-.navbar .input-prepend .btn,
-.navbar .input-append .btn,
-.navbar .input-prepend .btn-group,
-.navbar .input-append .btn-group {
- margin-top: 0;
-}
-
-.navbar-form {
- margin-bottom: 0;
- *zoom: 1;
-}
-
-.navbar-form:before,
-.navbar-form:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.navbar-form:after {
- clear: both;
-}
-
-.navbar-form input,
-.navbar-form select,
-.navbar-form .radio,
-.navbar-form .checkbox {
- margin-top: 5px;
-}
-
-.navbar-form input,
-.navbar-form select,
-.navbar-form .btn {
- display: inline-block;
- margin-bottom: 0;
-}
-
-.navbar-form input[type="image"],
-.navbar-form input[type="checkbox"],
-.navbar-form input[type="radio"] {
- margin-top: 3px;
-}
-
-.navbar-form .input-append,
-.navbar-form .input-prepend {
- margin-top: 5px;
- white-space: nowrap;
-}
-
-.navbar-form .input-append input,
-.navbar-form .input-prepend input {
- margin-top: 0;
-}
-
-.navbar-search {
- position: relative;
- float: left;
- margin-top: 5px;
- margin-bottom: 0;
-}
-
-.navbar-search .search-query {
- padding: 4px 14px;
- margin-bottom: 0;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 1;
- -webkit-border-radius: 15px;
- -moz-border-radius: 15px;
- border-radius: 15px;
-}
-
-.navbar-static-top {
- position: static;
- margin-bottom: 0;
-}
-
-.navbar-static-top .navbar-inner {
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.navbar-fixed-top,
-.navbar-fixed-bottom {
- position: fixed;
- right: 0;
- left: 0;
- z-index: 1030;
- margin-bottom: 0;
-}
-
-.navbar-fixed-top .navbar-inner,
-.navbar-static-top .navbar-inner {
- border-width: 0 0 1px;
-}
-
-.navbar-fixed-bottom .navbar-inner {
- border-width: 1px 0 0;
-}
-
-.navbar-fixed-top .navbar-inner,
-.navbar-fixed-bottom .navbar-inner {
- padding-right: 0;
- padding-left: 0;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
-}
-
-.navbar-static-top .container,
-.navbar-fixed-top .container,
-.navbar-fixed-bottom .container {
- width: 940px;
-}
-
-.navbar-fixed-top {
- top: 0;
-}
-
-.navbar-fixed-top .navbar-inner,
-.navbar-static-top .navbar-inner {
- -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
- box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1);
-}
-
-.navbar-fixed-bottom {
- bottom: 0;
-}
-
-.navbar-fixed-bottom .navbar-inner {
- -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
- box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1);
-}
-
-.navbar .nav {
- position: relative;
- left: 0;
- display: block;
- float: left;
- margin: 0 10px 0 0;
-}
-
-.navbar .nav.pull-right {
- float: right;
- margin-right: 0;
-}
-
-.navbar .nav > li {
- float: left;
-}
-
-.navbar .nav > li > a {
- float: none;
- padding: 10px 15px 10px;
- color: #777777;
- text-decoration: none;
- text-shadow: 0 1px 0 #ffffff;
-}
-
-.navbar .nav .dropdown-toggle .caret {
- margin-top: 8px;
-}
-
-.navbar .nav > li > a:focus,
-.navbar .nav > li > a:hover {
- color: #333333;
- text-decoration: none;
- background-color: transparent;
-}
-
-.navbar .nav > .active > a,
-.navbar .nav > .active > a:hover,
-.navbar .nav > .active > a:focus {
- color: #555555;
- text-decoration: none;
- background-color: #e5e5e5;
- -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
- -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
- box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125);
-}
-
-.navbar .btn-navbar {
- display: none;
- float: right;
- padding: 7px 10px;
- margin-right: 5px;
- margin-left: 5px;
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #ededed;
- *background-color: #e5e5e5;
- background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5));
- background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5);
- background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5);
- background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5);
- background-repeat: repeat-x;
- border-color: #e5e5e5 #e5e5e5 #bfbfbf;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
- -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
- -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
-}
-
-.navbar .btn-navbar:hover,
-.navbar .btn-navbar:focus,
-.navbar .btn-navbar:active,
-.navbar .btn-navbar.active,
-.navbar .btn-navbar.disabled,
-.navbar .btn-navbar[disabled] {
- color: #ffffff;
- background-color: #e5e5e5;
- *background-color: #d9d9d9;
-}
-
-.navbar .btn-navbar:active,
-.navbar .btn-navbar.active {
- background-color: #cccccc \9;
-}
-
-.navbar .btn-navbar .icon-bar {
- display: block;
- width: 18px;
- height: 2px;
- background-color: #f5f5f5;
- -webkit-border-radius: 1px;
- -moz-border-radius: 1px;
- border-radius: 1px;
- -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
- -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
-}
-
-.btn-navbar .icon-bar + .icon-bar {
- margin-top: 3px;
-}
-
-.navbar .nav > li > .dropdown-menu:before {
- position: absolute;
- top: -7px;
- left: 9px;
- display: inline-block;
- border-right: 7px solid transparent;
- border-bottom: 7px solid #ccc;
- border-left: 7px solid transparent;
- border-bottom-color: rgba(0, 0, 0, 0.2);
- content: '';
-}
-
-.navbar .nav > li > .dropdown-menu:after {
- position: absolute;
- top: -6px;
- left: 10px;
- display: inline-block;
- border-right: 6px solid transparent;
- border-bottom: 6px solid #ffffff;
- border-left: 6px solid transparent;
- content: '';
-}
-
-.navbar-fixed-bottom .nav > li > .dropdown-menu:before {
- top: auto;
- bottom: -7px;
- border-top: 7px solid #ccc;
- border-bottom: 0;
- border-top-color: rgba(0, 0, 0, 0.2);
-}
-
-.navbar-fixed-bottom .nav > li > .dropdown-menu:after {
- top: auto;
- bottom: -6px;
- border-top: 6px solid #ffffff;
- border-bottom: 0;
-}
-
-.navbar .nav li.dropdown > a:hover .caret,
-.navbar .nav li.dropdown > a:focus .caret {
- border-top-color: #333333;
- border-bottom-color: #333333;
-}
-
-.navbar .nav li.dropdown.open > .dropdown-toggle,
-.navbar .nav li.dropdown.active > .dropdown-toggle,
-.navbar .nav li.dropdown.open.active > .dropdown-toggle {
- color: #555555;
- background-color: #e5e5e5;
-}
-
-.navbar .nav li.dropdown > .dropdown-toggle .caret {
- border-top-color: #777777;
- border-bottom-color: #777777;
-}
-
-.navbar .nav li.dropdown.open > .dropdown-toggle .caret,
-.navbar .nav li.dropdown.active > .dropdown-toggle .caret,
-.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret {
- border-top-color: #555555;
- border-bottom-color: #555555;
-}
-
-.navbar .pull-right > li > .dropdown-menu,
-.navbar .nav > li > .dropdown-menu.pull-right {
- right: 0;
- left: auto;
-}
-
-.navbar .pull-right > li > .dropdown-menu:before,
-.navbar .nav > li > .dropdown-menu.pull-right:before {
- right: 12px;
- left: auto;
-}
-
-.navbar .pull-right > li > .dropdown-menu:after,
-.navbar .nav > li > .dropdown-menu.pull-right:after {
- right: 13px;
- left: auto;
-}
-
-.navbar .pull-right > li > .dropdown-menu .dropdown-menu,
-.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu {
- right: 100%;
- left: auto;
- margin-right: -1px;
- margin-left: 0;
- -webkit-border-radius: 6px 0 6px 6px;
- -moz-border-radius: 6px 0 6px 6px;
- border-radius: 6px 0 6px 6px;
-}
-
-.navbar-inverse .navbar-inner {
- background-color: #1b1b1b;
- background-image: -moz-linear-gradient(top, #222222, #111111);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
- background-image: -webkit-linear-gradient(top, #222222, #111111);
- background-image: -o-linear-gradient(top, #222222, #111111);
- background-image: linear-gradient(to bottom, #222222, #111111);
- background-repeat: repeat-x;
- border-color: #252525;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
-}
-
-.navbar-inverse .brand,
-.navbar-inverse .nav > li > a {
- color: #999999;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-
-.navbar-inverse .brand:hover,
-.navbar-inverse .nav > li > a:hover,
-.navbar-inverse .brand:focus,
-.navbar-inverse .nav > li > a:focus {
- color: #ffffff;
-}
-
-.navbar-inverse .brand {
- color: #999999;
-}
-
-.navbar-inverse .navbar-text {
- color: #999999;
-}
-
-.navbar-inverse .nav > li > a:focus,
-.navbar-inverse .nav > li > a:hover {
- color: #ffffff;
- background-color: transparent;
-}
-
-.navbar-inverse .nav .active > a,
-.navbar-inverse .nav .active > a:hover,
-.navbar-inverse .nav .active > a:focus {
- color: #ffffff;
- background-color: #111111;
-}
-
-.navbar-inverse .navbar-link {
- color: #999999;
-}
-
-.navbar-inverse .navbar-link:hover,
-.navbar-inverse .navbar-link:focus {
- color: #ffffff;
-}
-
-.navbar-inverse .divider-vertical {
- border-right-color: #222222;
- border-left-color: #111111;
-}
-
-.navbar-inverse .nav li.dropdown.open > .dropdown-toggle,
-.navbar-inverse .nav li.dropdown.active > .dropdown-toggle,
-.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle {
- color: #ffffff;
- background-color: #111111;
-}
-
-.navbar-inverse .nav li.dropdown > a:hover .caret,
-.navbar-inverse .nav li.dropdown > a:focus .caret {
- border-top-color: #ffffff;
- border-bottom-color: #ffffff;
-}
-
-.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret {
- border-top-color: #999999;
- border-bottom-color: #999999;
-}
-
-.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret,
-.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret,
-.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret {
- border-top-color: #ffffff;
- border-bottom-color: #ffffff;
-}
-
-.navbar-inverse .navbar-search .search-query {
- color: #ffffff;
- background-color: #515151;
- border-color: #111111;
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
- -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);
- -webkit-transition: none;
- -moz-transition: none;
- -o-transition: none;
- transition: none;
-}
-
-.navbar-inverse .navbar-search .search-query:-moz-placeholder {
- color: #cccccc;
-}
-
-.navbar-inverse .navbar-search .search-query:-ms-input-placeholder {
- color: #cccccc;
-}
-
-.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder {
- color: #cccccc;
-}
-
-.navbar-inverse .navbar-search .search-query:focus,
-.navbar-inverse .navbar-search .search-query.focused {
- padding: 5px 15px;
- color: #333333;
- text-shadow: 0 1px 0 #ffffff;
- background-color: #ffffff;
- border: 0;
- outline: 0;
- -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
- -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
- box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
-}
-
-.navbar-inverse .btn-navbar {
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #0e0e0e;
- *background-color: #040404;
- background-image: -moz-linear-gradient(top, #151515, #040404);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));
- background-image: -webkit-linear-gradient(top, #151515, #040404);
- background-image: -o-linear-gradient(top, #151515, #040404);
- background-image: linear-gradient(to bottom, #151515, #040404);
- background-repeat: repeat-x;
- border-color: #040404 #040404 #000000;
- border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0);
- filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
-}
-
-.navbar-inverse .btn-navbar:hover,
-.navbar-inverse .btn-navbar:focus,
-.navbar-inverse .btn-navbar:active,
-.navbar-inverse .btn-navbar.active,
-.navbar-inverse .btn-navbar.disabled,
-.navbar-inverse .btn-navbar[disabled] {
- color: #ffffff;
- background-color: #040404;
- *background-color: #000000;
-}
-
-.navbar-inverse .btn-navbar:active,
-.navbar-inverse .btn-navbar.active {
- background-color: #000000 \9;
-}
-
-.breadcrumb {
- padding: 8px 15px;
- margin: 0 0 20px;
- list-style: none;
- background-color: #f5f5f5;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.breadcrumb > li {
- display: inline-block;
- *display: inline;
- text-shadow: 0 1px 0 #ffffff;
- *zoom: 1;
-}
-
-.breadcrumb > li > .divider {
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > .active {
- color: #999999;
-}
-
-.pagination {
- margin: 20px 0;
-}
-
-.pagination ul {
- display: inline-block;
- *display: inline;
- margin-bottom: 0;
- margin-left: 0;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- *zoom: 1;
- -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
- -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.pagination ul > li {
- display: inline;
-}
-
-.pagination ul > li > a,
-.pagination ul > li > span {
- float: left;
- padding: 4px 12px;
- line-height: 20px;
- text-decoration: none;
- background-color: #ffffff;
- border: 1px solid #dddddd;
- border-left-width: 0;
-}
-
-.pagination ul > li > a:hover,
-.pagination ul > li > a:focus,
-.pagination ul > .active > a,
-.pagination ul > .active > span {
- background-color: #f5f5f5;
-}
-
-.pagination ul > .active > a,
-.pagination ul > .active > span {
- color: #999999;
- cursor: default;
-}
-
-.pagination ul > .disabled > span,
-.pagination ul > .disabled > a,
-.pagination ul > .disabled > a:hover,
-.pagination ul > .disabled > a:focus {
- color: #999999;
- cursor: default;
- background-color: transparent;
-}
-
-.pagination ul > li:first-child > a,
-.pagination ul > li:first-child > span {
- border-left-width: 1px;
- -webkit-border-bottom-left-radius: 4px;
- border-bottom-left-radius: 4px;
- -webkit-border-top-left-radius: 4px;
- border-top-left-radius: 4px;
- -moz-border-radius-bottomleft: 4px;
- -moz-border-radius-topleft: 4px;
-}
-
-.pagination ul > li:last-child > a,
-.pagination ul > li:last-child > span {
- -webkit-border-top-right-radius: 4px;
- border-top-right-radius: 4px;
- -webkit-border-bottom-right-radius: 4px;
- border-bottom-right-radius: 4px;
- -moz-border-radius-topright: 4px;
- -moz-border-radius-bottomright: 4px;
-}
-
-.pagination-centered {
- text-align: center;
-}
-
-.pagination-right {
- text-align: right;
-}
-
-.pagination-large ul > li > a,
-.pagination-large ul > li > span {
- padding: 11px 19px;
- font-size: 17.5px;
-}
-
-.pagination-large ul > li:first-child > a,
-.pagination-large ul > li:first-child > span {
- -webkit-border-bottom-left-radius: 6px;
- border-bottom-left-radius: 6px;
- -webkit-border-top-left-radius: 6px;
- border-top-left-radius: 6px;
- -moz-border-radius-bottomleft: 6px;
- -moz-border-radius-topleft: 6px;
-}
-
-.pagination-large ul > li:last-child > a,
-.pagination-large ul > li:last-child > span {
- -webkit-border-top-right-radius: 6px;
- border-top-right-radius: 6px;
- -webkit-border-bottom-right-radius: 6px;
- border-bottom-right-radius: 6px;
- -moz-border-radius-topright: 6px;
- -moz-border-radius-bottomright: 6px;
-}
-
-.pagination-mini ul > li:first-child > a,
-.pagination-small ul > li:first-child > a,
-.pagination-mini ul > li:first-child > span,
-.pagination-small ul > li:first-child > span {
- -webkit-border-bottom-left-radius: 3px;
- border-bottom-left-radius: 3px;
- -webkit-border-top-left-radius: 3px;
- border-top-left-radius: 3px;
- -moz-border-radius-bottomleft: 3px;
- -moz-border-radius-topleft: 3px;
-}
-
-.pagination-mini ul > li:last-child > a,
-.pagination-small ul > li:last-child > a,
-.pagination-mini ul > li:last-child > span,
-.pagination-small ul > li:last-child > span {
- -webkit-border-top-right-radius: 3px;
- border-top-right-radius: 3px;
- -webkit-border-bottom-right-radius: 3px;
- border-bottom-right-radius: 3px;
- -moz-border-radius-topright: 3px;
- -moz-border-radius-bottomright: 3px;
-}
-
-.pagination-small ul > li > a,
-.pagination-small ul > li > span {
- padding: 2px 10px;
- font-size: 11.9px;
-}
-
-.pagination-mini ul > li > a,
-.pagination-mini ul > li > span {
- padding: 0 6px;
- font-size: 10.5px;
-}
-
-.pager {
- margin: 20px 0;
- text-align: center;
- list-style: none;
- *zoom: 1;
-}
-
-.pager:before,
-.pager:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.pager:after {
- clear: both;
-}
-
-.pager li {
- display: inline;
-}
-
-.pager li > a,
-.pager li > span {
- display: inline-block;
- padding: 5px 14px;
- background-color: #fff;
- border: 1px solid #ddd;
- -webkit-border-radius: 15px;
- -moz-border-radius: 15px;
- border-radius: 15px;
-}
-
-.pager li > a:hover,
-.pager li > a:focus {
- text-decoration: none;
- background-color: #f5f5f5;
-}
-
-.pager .next > a,
-.pager .next > span {
- float: right;
-}
-
-.pager .previous > a,
-.pager .previous > span {
- float: left;
-}
-
-.pager .disabled > a,
-.pager .disabled > a:hover,
-.pager .disabled > a:focus,
-.pager .disabled > span {
- color: #999999;
- cursor: default;
- background-color: #fff;
-}
-
-.modal-backdrop {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 1040;
- background-color: #000000;
-}
-
-.modal-backdrop.fade {
- opacity: 0;
-}
-
-.modal-backdrop,
-.modal-backdrop.fade.in {
- opacity: 0.8;
- filter: alpha(opacity=80);
-}
-
-.modal {
- position: fixed;
- top: 10%;
- left: 50%;
- z-index: 1050;
- width: 560px;
- margin-left: -280px;
- background-color: #ffffff;
- border: 1px solid #999;
- border: 1px solid rgba(0, 0, 0, 0.3);
- *border: 1px solid #999;
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
- outline: none;
- -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
- -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
- box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
- -webkit-background-clip: padding-box;
- -moz-background-clip: padding-box;
- background-clip: padding-box;
-}
-
-.modal.fade {
- top: -25%;
- -webkit-transition: opacity 0.3s linear, top 0.3s ease-out;
- -moz-transition: opacity 0.3s linear, top 0.3s ease-out;
- -o-transition: opacity 0.3s linear, top 0.3s ease-out;
- transition: opacity 0.3s linear, top 0.3s ease-out;
-}
-
-.modal.fade.in {
- top: 10%;
-}
-
-.modal-header {
- padding: 9px 15px;
- border-bottom: 1px solid #eee;
-}
-
-.modal-header .close {
- margin-top: 2px;
-}
-
-.modal-header h3 {
- margin: 0;
- line-height: 30px;
-}
-
-.modal-body {
- position: relative;
- max-height: 400px;
- padding: 15px;
- overflow-y: auto;
-}
-
-.modal-form {
- margin-bottom: 0;
-}
-
-.modal-footer {
- padding: 14px 15px 15px;
- margin-bottom: 0;
- text-align: right;
- background-color: #f5f5f5;
- border-top: 1px solid #ddd;
- -webkit-border-radius: 0 0 6px 6px;
- -moz-border-radius: 0 0 6px 6px;
- border-radius: 0 0 6px 6px;
- *zoom: 1;
- -webkit-box-shadow: inset 0 1px 0 #ffffff;
- -moz-box-shadow: inset 0 1px 0 #ffffff;
- box-shadow: inset 0 1px 0 #ffffff;
-}
-
-.modal-footer:before,
-.modal-footer:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.modal-footer:after {
- clear: both;
-}
-
-.modal-footer .btn + .btn {
- margin-bottom: 0;
- margin-left: 5px;
-}
-
-.modal-footer .btn-group .btn + .btn {
- margin-left: -1px;
-}
-
-.modal-footer .btn-block + .btn-block {
- margin-left: 0;
-}
-
-.tooltip {
- position: absolute;
- z-index: 1030;
- display: block;
- font-size: 11px;
- line-height: 1.4;
- opacity: 0;
- filter: alpha(opacity=0);
- visibility: visible;
-}
-
-.tooltip.in {
- opacity: 0.8;
- filter: alpha(opacity=80);
-}
-
-.tooltip.top {
- padding: 5px 0;
- margin-top: -3px;
-}
-
-.tooltip.right {
- padding: 0 5px;
- margin-left: 3px;
-}
-
-.tooltip.bottom {
- padding: 5px 0;
- margin-top: 3px;
-}
-
-.tooltip.left {
- padding: 0 5px;
- margin-left: -3px;
-}
-
-.tooltip-inner {
- max-width: 200px;
- padding: 8px;
- color: #ffffff;
- text-align: center;
- text-decoration: none;
- background-color: #000000;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.tooltip-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
-}
-
-.tooltip.top .tooltip-arrow {
- bottom: 0;
- left: 50%;
- margin-left: -5px;
- border-top-color: #000000;
- border-width: 5px 5px 0;
-}
-
-.tooltip.right .tooltip-arrow {
- top: 50%;
- left: 0;
- margin-top: -5px;
- border-right-color: #000000;
- border-width: 5px 5px 5px 0;
-}
-
-.tooltip.left .tooltip-arrow {
- top: 50%;
- right: 0;
- margin-top: -5px;
- border-left-color: #000000;
- border-width: 5px 0 5px 5px;
-}
-
-.tooltip.bottom .tooltip-arrow {
- top: 0;
- left: 50%;
- margin-left: -5px;
- border-bottom-color: #000000;
- border-width: 0 5px 5px;
-}
-
-.popover {
- position: absolute;
- top: 0;
- left: 0;
- z-index: 1010;
- display: none;
- max-width: 276px;
- padding: 1px;
- text-align: left;
- white-space: normal;
- background-color: #ffffff;
- border: 1px solid #ccc;
- border: 1px solid rgba(0, 0, 0, 0.2);
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
- -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
- -webkit-background-clip: padding-box;
- -moz-background-clip: padding;
- background-clip: padding-box;
-}
-
-.popover.top {
- margin-top: -10px;
-}
-
-.popover.right {
- margin-left: 10px;
-}
-
-.popover.bottom {
- margin-top: 10px;
-}
-
-.popover.left {
- margin-left: -10px;
-}
-
-.popover-title {
- padding: 8px 14px;
- margin: 0;
- font-size: 14px;
- font-weight: normal;
- line-height: 18px;
- background-color: #f7f7f7;
- border-bottom: 1px solid #ebebeb;
- -webkit-border-radius: 5px 5px 0 0;
- -moz-border-radius: 5px 5px 0 0;
- border-radius: 5px 5px 0 0;
-}
-
-.popover-title:empty {
- display: none;
-}
-
-.popover-content {
- padding: 9px 14px;
-}
-
-.popover .arrow,
-.popover .arrow:after {
- position: absolute;
- display: block;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
-}
-
-.popover .arrow {
- border-width: 11px;
-}
-
-.popover .arrow:after {
- border-width: 10px;
- content: "";
-}
-
-.popover.top .arrow {
- bottom: -11px;
- left: 50%;
- margin-left: -11px;
- border-top-color: #999;
- border-top-color: rgba(0, 0, 0, 0.25);
- border-bottom-width: 0;
-}
-
-.popover.top .arrow:after {
- bottom: 1px;
- margin-left: -10px;
- border-top-color: #ffffff;
- border-bottom-width: 0;
-}
-
-.popover.right .arrow {
- top: 50%;
- left: -11px;
- margin-top: -11px;
- border-right-color: #999;
- border-right-color: rgba(0, 0, 0, 0.25);
- border-left-width: 0;
-}
-
-.popover.right .arrow:after {
- bottom: -10px;
- left: 1px;
- border-right-color: #ffffff;
- border-left-width: 0;
-}
-
-.popover.bottom .arrow {
- top: -11px;
- left: 50%;
- margin-left: -11px;
- border-bottom-color: #999;
- border-bottom-color: rgba(0, 0, 0, 0.25);
- border-top-width: 0;
-}
-
-.popover.bottom .arrow:after {
- top: 1px;
- margin-left: -10px;
- border-bottom-color: #ffffff;
- border-top-width: 0;
-}
-
-.popover.left .arrow {
- top: 50%;
- right: -11px;
- margin-top: -11px;
- border-left-color: #999;
- border-left-color: rgba(0, 0, 0, 0.25);
- border-right-width: 0;
-}
-
-.popover.left .arrow:after {
- right: 1px;
- bottom: -10px;
- border-left-color: #ffffff;
- border-right-width: 0;
-}
-
-.thumbnails {
- margin-left: -20px;
- list-style: none;
- *zoom: 1;
-}
-
-.thumbnails:before,
-.thumbnails:after {
- display: table;
- line-height: 0;
- content: "";
-}
-
-.thumbnails:after {
- clear: both;
-}
-
-.row-fluid .thumbnails {
- margin-left: 0;
-}
-
-.thumbnails > li {
- float: left;
- margin-bottom: 20px;
- margin-left: 20px;
-}
-
-.thumbnail {
- display: block;
- padding: 4px;
- line-height: 20px;
- border: 1px solid #ddd;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
- -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055);
- -webkit-transition: all 0.2s ease-in-out;
- -moz-transition: all 0.2s ease-in-out;
- -o-transition: all 0.2s ease-in-out;
- transition: all 0.2s ease-in-out;
-}
-
-a.thumbnail:hover,
-a.thumbnail:focus {
- border-color: #0088cc;
- -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
- -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
- box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25);
-}
-
-.thumbnail > img {
- display: block;
- max-width: 100%;
- margin-right: auto;
- margin-left: auto;
-}
-
-.thumbnail .caption {
- padding: 9px;
- color: #555555;
-}
-
-.media,
-.media-body {
- overflow: hidden;
- *overflow: visible;
- zoom: 1;
-}
-
-.media,
-.media .media {
- margin-top: 15px;
-}
-
-.media:first-child {
- margin-top: 0;
-}
-
-.media-object {
- display: block;
-}
-
-.media-heading {
- margin: 0 0 5px;
-}
-
-.media > .pull-left {
- margin-right: 10px;
-}
-
-.media > .pull-right {
- margin-left: 10px;
-}
-
-.media-list {
- margin-left: 0;
- list-style: none;
-}
-
-.label,
-.badge {
- display: inline-block;
- padding: 2px 4px;
- font-size: 11.844px;
- font-weight: bold;
- line-height: 14px;
- color: #ffffff;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- white-space: nowrap;
- vertical-align: baseline;
- background-color: #999999;
-}
-
-.label {
- -webkit-border-radius: 3px;
- -moz-border-radius: 3px;
- border-radius: 3px;
-}
-
-.badge {
- padding-right: 9px;
- padding-left: 9px;
- -webkit-border-radius: 9px;
- -moz-border-radius: 9px;
- border-radius: 9px;
-}
-
-.label:empty,
-.badge:empty {
- display: none;
-}
-
-a.label:hover,
-a.label:focus,
-a.badge:hover,
-a.badge:focus {
- color: #ffffff;
- text-decoration: none;
- cursor: pointer;
-}
-
-.label-important,
-.badge-important {
- background-color: #b94a48;
-}
-
-.label-important[href],
-.badge-important[href] {
- background-color: #953b39;
-}
-
-.label-warning,
-.badge-warning {
- background-color: #f89406;
-}
-
-.label-warning[href],
-.badge-warning[href] {
- background-color: #c67605;
-}
-
-.label-success,
-.badge-success {
- background-color: #468847;
-}
-
-.label-success[href],
-.badge-success[href] {
- background-color: #356635;
-}
-
-.label-info,
-.badge-info {
- background-color: #3a87ad;
-}
-
-.label-info[href],
-.badge-info[href] {
- background-color: #2d6987;
-}
-
-.label-inverse,
-.badge-inverse {
- background-color: #333333;
-}
-
-.label-inverse[href],
-.badge-inverse[href] {
- background-color: #1a1a1a;
-}
-
-.btn .label,
-.btn .badge {
- position: relative;
- top: -1px;
-}
-
-.btn-mini .label,
-.btn-mini .badge {
- top: 0;
-}
-
-@-webkit-keyframes progress-bar-stripes {
- from {
- background-position: 40px 0;
- }
- to {
- background-position: 0 0;
- }
-}
-
-@-moz-keyframes progress-bar-stripes {
- from {
- background-position: 40px 0;
- }
- to {
- background-position: 0 0;
- }
-}
-
-@-ms-keyframes progress-bar-stripes {
- from {
- background-position: 40px 0;
- }
- to {
- background-position: 0 0;
- }
-}
-
-@-o-keyframes progress-bar-stripes {
- from {
- background-position: 0 0;
- }
- to {
- background-position: 40px 0;
- }
-}
-
-@keyframes progress-bar-stripes {
- from {
- background-position: 40px 0;
- }
- to {
- background-position: 0 0;
- }
-}
-
-.progress {
- height: 20px;
- margin-bottom: 20px;
- overflow: hidden;
- background-color: #f7f7f7;
- background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));
- background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9);
- background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9);
- background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
- background-repeat: repeat-x;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
- -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
-}
-
-.progress .bar {
- float: left;
- width: 0;
- height: 100%;
- font-size: 12px;
- color: #ffffff;
- text-align: center;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
- background-color: #0e90d2;
- background-image: -moz-linear-gradient(top, #149bdf, #0480be);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));
- background-image: -webkit-linear-gradient(top, #149bdf, #0480be);
- background-image: -o-linear-gradient(top, #149bdf, #0480be);
- background-image: linear-gradient(to bottom, #149bdf, #0480be);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
- -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
- -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
- box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
- -webkit-box-sizing: border-box;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
- -webkit-transition: width 0.6s ease;
- -moz-transition: width 0.6s ease;
- -o-transition: width 0.6s ease;
- transition: width 0.6s ease;
-}
-
-.progress .bar + .bar {
- -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
- -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
- box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-}
-
-.progress-striped .bar {
- background-color: #149bdf;
- background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
- background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- -webkit-background-size: 40px 40px;
- -moz-background-size: 40px 40px;
- -o-background-size: 40px 40px;
- background-size: 40px 40px;
-}
-
-.progress.active .bar {
- -webkit-animation: progress-bar-stripes 2s linear infinite;
- -moz-animation: progress-bar-stripes 2s linear infinite;
- -ms-animation: progress-bar-stripes 2s linear infinite;
- -o-animation: progress-bar-stripes 2s linear infinite;
- animation: progress-bar-stripes 2s linear infinite;
-}
-
-.progress-danger .bar,
-.progress .bar-danger {
- background-color: #dd514c;
- background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));
- background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35);
- background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
- background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);
-}
-
-.progress-danger.progress-striped .bar,
-.progress-striped .bar-danger {
- background-color: #ee5f5b;
- background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
- background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
-}
-
-.progress-success .bar,
-.progress .bar-success {
- background-color: #5eb95e;
- background-image: -moz-linear-gradient(top, #62c462, #57a957);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));
- background-image: -webkit-linear-gradient(top, #62c462, #57a957);
- background-image: -o-linear-gradient(top, #62c462, #57a957);
- background-image: linear-gradient(to bottom, #62c462, #57a957);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);
-}
-
-.progress-success.progress-striped .bar,
-.progress-striped .bar-success {
- background-color: #62c462;
- background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
- background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
-}
-
-.progress-info .bar,
-.progress .bar-info {
- background-color: #4bb1cf;
- background-image: -moz-linear-gradient(top, #5bc0de, #339bb9);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));
- background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9);
- background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
- background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);
-}
-
-.progress-info.progress-striped .bar,
-.progress-striped .bar-info {
- background-color: #5bc0de;
- background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
- background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
-}
-
-.progress-warning .bar,
-.progress .bar-warning {
- background-color: #faa732;
- background-image: -moz-linear-gradient(top, #fbb450, #f89406);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));
- background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
- background-image: -o-linear-gradient(top, #fbb450, #f89406);
- background-image: linear-gradient(to bottom, #fbb450, #f89406);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);
-}
-
-.progress-warning.progress-striped .bar,
-.progress-striped .bar-warning {
- background-color: #fbb450;
- background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
- background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
- background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
-}
-
-.accordion {
- margin-bottom: 20px;
-}
-
-.accordion-group {
- margin-bottom: 2px;
- border: 1px solid #e5e5e5;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
-}
-
-.accordion-heading {
- border-bottom: 0;
-}
-
-.accordion-heading .accordion-toggle {
- display: block;
- padding: 8px 15px;
-}
-
-.accordion-toggle {
- cursor: pointer;
-}
-
-.accordion-inner {
- padding: 9px 15px;
- border-top: 1px solid #e5e5e5;
-}
-
-.carousel {
- position: relative;
- margin-bottom: 20px;
- line-height: 1;
-}
-
-.carousel-inner {
- position: relative;
- width: 100%;
- overflow: hidden;
-}
-
-.carousel-inner > .item {
- position: relative;
- display: none;
- -webkit-transition: 0.6s ease-in-out left;
- -moz-transition: 0.6s ease-in-out left;
- -o-transition: 0.6s ease-in-out left;
- transition: 0.6s ease-in-out left;
-}
-
-.carousel-inner > .item > img,
-.carousel-inner > .item > a > img {
- display: block;
- line-height: 1;
-}
-
-.carousel-inner > .active,
-.carousel-inner > .next,
-.carousel-inner > .prev {
- display: block;
-}
-
-.carousel-inner > .active {
- left: 0;
-}
-
-.carousel-inner > .next,
-.carousel-inner > .prev {
- position: absolute;
- top: 0;
- width: 100%;
-}
-
-.carousel-inner > .next {
- left: 100%;
-}
-
-.carousel-inner > .prev {
- left: -100%;
-}
-
-.carousel-inner > .next.left,
-.carousel-inner > .prev.right {
- left: 0;
-}
-
-.carousel-inner > .active.left {
- left: -100%;
-}
-
-.carousel-inner > .active.right {
- left: 100%;
-}
-
-.carousel-control {
- position: absolute;
- top: 40%;
- left: 15px;
- width: 40px;
- height: 40px;
- margin-top: -20px;
- font-size: 60px;
- font-weight: 100;
- line-height: 30px;
- color: #ffffff;
- text-align: center;
- background: #222222;
- border: 3px solid #ffffff;
- -webkit-border-radius: 23px;
- -moz-border-radius: 23px;
- border-radius: 23px;
- opacity: 0.5;
- filter: alpha(opacity=50);
-}
-
-.carousel-control.right {
- right: 15px;
- left: auto;
-}
-
-.carousel-control:hover,
-.carousel-control:focus {
- color: #ffffff;
- text-decoration: none;
- opacity: 0.9;
- filter: alpha(opacity=90);
-}
-
-.carousel-indicators {
- position: absolute;
- top: 15px;
- right: 15px;
- z-index: 5;
- margin: 0;
- list-style: none;
-}
-
-.carousel-indicators li {
- display: block;
- float: left;
- width: 10px;
- height: 10px;
- margin-left: 5px;
- text-indent: -999px;
- background-color: #ccc;
- background-color: rgba(255, 255, 255, 0.25);
- border-radius: 5px;
-}
-
-.carousel-indicators .active {
- background-color: #fff;
-}
-
-.carousel-caption {
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 15px;
- background: #333333;
- background: rgba(0, 0, 0, 0.75);
-}
-
-.carousel-caption h4,
-.carousel-caption p {
- line-height: 20px;
- color: #ffffff;
-}
-
-.carousel-caption h4 {
- margin: 0 0 5px;
-}
-
-.carousel-caption p {
- margin-bottom: 0;
-}
-
-.hero-unit {
- padding: 60px;
- margin-bottom: 30px;
- font-size: 18px;
- font-weight: 200;
- line-height: 30px;
- color: inherit;
- background-color: #eeeeee;
- -webkit-border-radius: 6px;
- -moz-border-radius: 6px;
- border-radius: 6px;
-}
-
-.hero-unit h1 {
- margin-bottom: 0;
- font-size: 60px;
- line-height: 1;
- letter-spacing: -1px;
- color: inherit;
-}
-
-.hero-unit li {
- line-height: 30px;
-}
-
-.pull-right {
- float: right;
-}
-
-.pull-left {
- float: left;
-}
-
-.hide {
- display: none;
-}
-
-.show {
- display: block;
-}
-
-.invisible {
- visibility: hidden;
-}
-
-.affix {
- position: fixed;
-}
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png
deleted file mode 100644
index 0d4475e..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderBottomCenter.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png
deleted file mode 100644
index 2775eba..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderBottomLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png
deleted file mode 100644
index f7f5137..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderBottomRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png
deleted file mode 100644
index a2d63d1..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png
deleted file mode 100644
index fd7c3e8..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderMiddleRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png
deleted file mode 100644
index 2937a9c..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderTopCenter.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png
deleted file mode 100644
index f9d458b..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderTopLeft.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png b/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png
deleted file mode 100644
index 74b8583..0000000
--- a/nikola/data/themes/default/assets/css/images/ie6/borderTopRight.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/css/rst.css b/nikola/data/themes/default/assets/css/rst.css
deleted file mode 100644
index cf73111..0000000
--- a/nikola/data/themes/default/assets/css/rst.css
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
-:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 7514 2012-09-14 14:27:12Z milde $
-:Copyright: This stylesheet has been placed in the public domain.
-
-Default cascading style sheet for the HTML output of Docutils.
-
-See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
-customize this style sheet.
-*/
-
-/* used to remove borders from tables and images */
-.borderless, table.borderless td, table.borderless th {
- border: 0 }
-
-table.borderless td, table.borderless th {
- /* Override padding for "table.docutils td" with "! important".
- The right padding separates the table cells. */
- padding: 0 0.5em 0 0 ! important }
-
-.first {
- /* Override more specific margin styles with "! important". */
- margin-top: 0 ! important }
-
-.last, .with-subtitle {
- margin-bottom: 0 ! important }
-
-.hidden {
- display: none }
-
-a.toc-backref {
- text-decoration: none ;
- color: black }
-
-blockquote.epigraph {
- margin: 2em 5em ; }
-
-dl.docutils dd {
- margin-bottom: 0.5em }
-
-object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
- overflow: hidden;
-}
-
-/* Uncomment (and remove this text!) to get bold-faced definition list terms
-dl.docutils dt {
- font-weight: bold }
-*/
-
-div.abstract {
- margin: 2em 5em }
-
-div.abstract p.topic-title {
- font-weight: bold ;
- text-align: center }
-
-div.admonition, div.attention, div.caution, div.danger, div.error,
-div.hint, div.important, div.note, div.tip, div.warning {
- margin: 2em ;
- border: medium outset ;
- padding: 1em }
-
-div.admonition p.admonition-title, div.hint p.admonition-title,
-div.important p.admonition-title, div.note p.admonition-title,
-div.tip p.admonition-title {
- font-weight: bold ;
- font-family: sans-serif }
-
-div.attention p.admonition-title, div.caution p.admonition-title,
-div.danger p.admonition-title, div.error p.admonition-title,
-div.warning p.admonition-title, .code .error {
- color: red ;
- font-weight: bold ;
- font-family: sans-serif }
-
-/* Uncomment (and remove this text!) to get reduced vertical space in
- compound paragraphs.
-div.compound .compound-first, div.compound .compound-middle {
- margin-bottom: 0.5em }
-
-div.compound .compound-last, div.compound .compound-middle {
- margin-top: 0.5em }
-*/
-
-div.dedication {
- margin: 2em 5em ;
- text-align: center ;
- font-style: italic }
-
-div.dedication p.topic-title {
- font-weight: bold ;
- font-style: normal }
-
-div.figure {
- margin-left: 2em ;
- margin-right: 2em }
-
-div.footer, div.header {
- clear: both;
- font-size: smaller }
-
-div.line-block {
- display: block ;
- margin-top: 1em ;
- margin-bottom: 1em }
-
-div.line-block div.line-block {
- margin-top: 0 ;
- margin-bottom: 0 ;
- margin-left: 1.5em }
-
-div.sidebar {
- margin: 0 0 0.5em 1em ;
- border: medium outset ;
- padding: 1em ;
- background-color: #ffffee ;
- width: 40% ;
- float: right ;
- clear: right }
-
-div.sidebar p.rubric {
- font-family: sans-serif ;
- font-size: medium }
-
-div.system-messages {
- margin: 5em }
-
-div.system-messages h1 {
- color: red }
-
-div.system-message {
- border: medium outset ;
- padding: 1em }
-
-div.system-message p.system-message-title {
- color: red ;
- font-weight: bold }
-
-div.topic {
- margin: 2em }
-
-h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
-h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
- margin-top: 0.4em }
-
-h1.title {
- text-align: center }
-
-h2.subtitle {
- text-align: center }
-
-hr.docutils {
- width: 75% }
-
-img.align-left, .figure.align-left, object.align-left {
- clear: left ;
- float: left ;
- margin-right: 1em }
-
-img.align-right, .figure.align-right, object.align-right {
- clear: right ;
- float: right ;
- margin-left: 1em }
-
-img.align-center, .figure.align-center, object.align-center {
- display: block;
- margin-left: auto;
- margin-right: auto;
-}
-
-.align-left {
- text-align: left }
-
-.align-center {
- clear: both ;
- text-align: center }
-
-.align-right {
- text-align: right }
-
-/* reset inner alignment in figures */
-div.align-right {
- text-align: inherit }
-
-/* div.align-center * { */
-/* text-align: left } */
-
-ol.simple, ul.simple {
- margin-bottom: 1em }
-
-ol.arabic {
- list-style: decimal }
-
-ol.loweralpha {
- list-style: lower-alpha }
-
-ol.upperalpha {
- list-style: upper-alpha }
-
-ol.lowerroman {
- list-style: lower-roman }
-
-ol.upperroman {
- list-style: upper-roman }
-
-p.attribution {
- text-align: right ;
- margin-left: 50% }
-
-p.caption {
- font-style: italic }
-
-p.credits {
- font-style: italic ;
- font-size: smaller }
-
-p.label {
- white-space: nowrap }
-
-p.rubric {
- font-weight: bold ;
- font-size: larger ;
- color: maroon ;
- text-align: center }
-
-p.sidebar-title {
- font-family: sans-serif ;
- font-weight: bold ;
- font-size: larger }
-
-p.sidebar-subtitle {
- font-family: sans-serif ;
- font-weight: bold }
-
-p.topic-title {
- font-weight: bold }
-
-pre.address {
- margin-bottom: 0 ;
- margin-top: 0 ;
- font: inherit }
-
-pre.literal-block, pre.doctest-block, pre.math, pre.code {
- margin-left: 2em ;
- margin-right: 2em }
-
-pre.code .ln { color: grey; } /* line numbers */
-pre.code, code { background-color: #eeeeee }
-pre.code .comment, code .comment { color: #5C6576 }
-pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
-pre.code .literal.string, code .literal.string { color: #0C5404 }
-pre.code .name.builtin, code .name.builtin { color: #352B84 }
-pre.code .deleted, code .deleted { background-color: #DEB0A1}
-pre.code .inserted, code .inserted { background-color: #A3D289}
-
-span.classifier {
- font-family: sans-serif ;
- font-style: oblique }
-
-span.classifier-delimiter {
- font-family: sans-serif ;
- font-weight: bold }
-
-span.interpreted {
- font-family: sans-serif }
-
-span.option {
- white-space: nowrap }
-
-span.pre {
- white-space: pre }
-
-span.problematic {
- color: red }
-
-span.section-subtitle {
- /* font-size relative to parent (h1..h6 element) */
- font-size: 80% }
-
-table.citation {
- border-left: solid 1px gray;
- margin-left: 1px }
-
-table.docinfo {
- margin: 2em 4em }
-
-table.docutils {
- margin-top: 0.5em ;
- margin-bottom: 0.5em }
-
-table.footnote {
- border-left: solid 1px black;
- margin-left: 1px }
-
-table.docutils td, table.docutils th,
-table.docinfo td, table.docinfo th {
- padding-left: 0.5em ;
- padding-right: 0.5em ;
- vertical-align: top }
-
-table.docutils th.field-name, table.docinfo th.docinfo-name {
- font-weight: bold ;
- text-align: left ;
- white-space: nowrap ;
- padding-left: 0 }
-
-h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
-h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
- font-size: 100% }
-
-ul.auto-toc {
- list-style-type: none }
diff --git a/nikola/data/themes/default/assets/css/theme.css b/nikola/data/themes/default/assets/css/theme.css
deleted file mode 100644
index 08a71f3..0000000
--- a/nikola/data/themes/default/assets/css/theme.css
+++ /dev/null
@@ -1,73 +0,0 @@
-#container {
- width: 960px;
- margin: 0 auto;
-}
-
-#contentcolumn {
- max-width: 760px;
-}
-#q {
- width: 150px;
-}
-
-img {
- max-width: 90%;
-}
-
-.postbox {
- border-bottom: 2px solid darkgrey;
- margin-bottom: 12px;
-}
-
-.titlebox {
- text-align: right;
-}
-
-#addthisbox {margin-bottom: 12px;}
-
-td.label {
- /* Issue #290 */
- background-color: inherit;
-}
-
-.footnote-reference {
- /* Issue 290 */
- vertical-align: super;
- font-size: xx-small;
-}
-
-
-.caption {
- /* Issue 292 */
- text-align: center;
-}
-
-div.figure > a > img {
- /* Issue 292 */
- display: block;
- margin-left: auto;
- margin-right: auto;
-}
-
-div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning {
- /* Issue 277 */
- border: 1px solid #aaa;
- border-radius: 5px;
-}
-
-blockquote p, blockquote {
- font-size: 17.5px;
- font-weight: 300;
- line-height: 1.25;
-}
-
-ul.bricks > li {
- display: inline;
- background-color: lightblue;
- padding: 8px;
- border-radius: 5px;
- line-height: 3;
- white-space:nowrap;
- margin: 3px;
-}
-
diff --git a/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png b/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png
deleted file mode 100644
index 3bf6484..0000000
--- a/nikola/data/themes/default/assets/img/glyphicons-halflings-white.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/img/glyphicons-halflings.png b/nikola/data/themes/default/assets/img/glyphicons-halflings.png
deleted file mode 100644
index a996999..0000000
--- a/nikola/data/themes/default/assets/img/glyphicons-halflings.png
+++ /dev/null
Binary files differ
diff --git a/nikola/data/themes/default/assets/js/bootstrap.js b/nikola/data/themes/default/assets/js/bootstrap.js
deleted file mode 100644
index a81171b..0000000
--- a/nikola/data/themes/default/assets/js/bootstrap.js
+++ /dev/null
@@ -1,2268 +0,0 @@
-/* ===================================================
- * bootstrap-transition.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#transitions
- * ===================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* CSS TRANSITION SUPPORT (http://www.modernizr.com/)
- * ======================================================= */
-
- $(function () {
-
- $.support.transition = (function () {
-
- var transitionEnd = (function () {
-
- var el = document.createElement('bootstrap')
- , transEndEventNames = {
- 'WebkitTransition' : 'webkitTransitionEnd'
- , 'MozTransition' : 'transitionend'
- , 'OTransition' : 'oTransitionEnd otransitionend'
- , 'transition' : 'transitionend'
- }
- , name
-
- for (name in transEndEventNames){
- if (el.style[name] !== undefined) {
- return transEndEventNames[name]
- }
- }
-
- }())
-
- return transitionEnd && {
- end: transitionEnd
- }
-
- })()
-
- })
-
-}(window.jQuery);/* ==========================================================
- * bootstrap-alert.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#alerts
- * ==========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* ALERT CLASS DEFINITION
- * ====================== */
-
- var dismiss = '[data-dismiss="alert"]'
- , Alert = function (el) {
- $(el).on('click', dismiss, this.close)
- }
-
- Alert.prototype.close = function (e) {
- var $this = $(this)
- , selector = $this.attr('data-target')
- , $parent
-
- if (!selector) {
- selector = $this.attr('href')
- selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
- }
-
- $parent = $(selector)
-
- e && e.preventDefault()
-
- $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent())
-
- $parent.trigger(e = $.Event('close'))
-
- if (e.isDefaultPrevented()) return
-
- $parent.removeClass('in')
-
- function removeElement() {
- $parent
- .trigger('closed')
- .remove()
- }
-
- $.support.transition && $parent.hasClass('fade') ?
- $parent.on($.support.transition.end, removeElement) :
- removeElement()
- }
-
-
- /* ALERT PLUGIN DEFINITION
- * ======================= */
-
- var old = $.fn.alert
-
- $.fn.alert = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('alert')
- if (!data) $this.data('alert', (data = new Alert(this)))
- if (typeof option == 'string') data[option].call($this)
- })
- }
-
- $.fn.alert.Constructor = Alert
-
-
- /* ALERT NO CONFLICT
- * ================= */
-
- $.fn.alert.noConflict = function () {
- $.fn.alert = old
- return this
- }
-
-
- /* ALERT DATA-API
- * ============== */
-
- $(document).on('click.alert.data-api', dismiss, Alert.prototype.close)
-
-}(window.jQuery);/* ============================================================
- * bootstrap-button.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#buttons
- * ============================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============================================================ */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* BUTTON PUBLIC CLASS DEFINITION
- * ============================== */
-
- var Button = function (element, options) {
- this.$element = $(element)
- this.options = $.extend({}, $.fn.button.defaults, options)
- }
-
- Button.prototype.setState = function (state) {
- var d = 'disabled'
- , $el = this.$element
- , data = $el.data()
- , val = $el.is('input') ? 'val' : 'html'
-
- state = state + 'Text'
- data.resetText || $el.data('resetText', $el[val]())
-
- $el[val](data[state] || this.options[state])
-
- // push to event loop to allow forms to submit
- setTimeout(function () {
- state == 'loadingText' ?
- $el.addClass(d).attr(d, d) :
- $el.removeClass(d).removeAttr(d)
- }, 0)
- }
-
- Button.prototype.toggle = function () {
- var $parent = this.$element.closest('[data-toggle="buttons-radio"]')
-
- $parent && $parent
- .find('.active')
- .removeClass('active')
-
- this.$element.toggleClass('active')
- }
-
-
- /* BUTTON PLUGIN DEFINITION
- * ======================== */
-
- var old = $.fn.button
-
- $.fn.button = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('button')
- , options = typeof option == 'object' && option
- if (!data) $this.data('button', (data = new Button(this, options)))
- if (option == 'toggle') data.toggle()
- else if (option) data.setState(option)
- })
- }
-
- $.fn.button.defaults = {
- loadingText: 'loading...'
- }
-
- $.fn.button.Constructor = Button
-
-
- /* BUTTON NO CONFLICT
- * ================== */
-
- $.fn.button.noConflict = function () {
- $.fn.button = old
- return this
- }
-
-
- /* BUTTON DATA-API
- * =============== */
-
- $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) {
- var $btn = $(e.target)
- if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
- $btn.button('toggle')
- })
-
-}(window.jQuery);/* ==========================================================
- * bootstrap-carousel.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#carousel
- * ==========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* CAROUSEL CLASS DEFINITION
- * ========================= */
-
- var Carousel = function (element, options) {
- this.$element = $(element)
- this.$indicators = this.$element.find('.carousel-indicators')
- this.options = options
- this.options.pause == 'hover' && this.$element
- .on('mouseenter', $.proxy(this.pause, this))
- .on('mouseleave', $.proxy(this.cycle, this))
- }
-
- Carousel.prototype = {
-
- cycle: function (e) {
- if (!e) this.paused = false
- if (this.interval) clearInterval(this.interval);
- this.options.interval
- && !this.paused
- && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
- return this
- }
-
- , getActiveIndex: function () {
- this.$active = this.$element.find('.item.active')
- this.$items = this.$active.parent().children()
- return this.$items.index(this.$active)
- }
-
- , to: function (pos) {
- var activeIndex = this.getActiveIndex()
- , that = this
-
- if (pos > (this.$items.length - 1) || pos < 0) return
-
- if (this.sliding) {
- return this.$element.one('slid', function () {
- that.to(pos)
- })
- }
-
- if (activeIndex == pos) {
- return this.pause().cycle()
- }
-
- return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
- }
-
- , pause: function (e) {
- if (!e) this.paused = true
- if (this.$element.find('.next, .prev').length && $.support.transition.end) {
- this.$element.trigger($.support.transition.end)
- this.cycle()
- }
- clearInterval(this.interval)
- this.interval = null
- return this
- }
-
- , next: function () {
- if (this.sliding) return
- return this.slide('next')
- }
-
- , prev: function () {
- if (this.sliding) return
- return this.slide('prev')
- }
-
- , slide: function (type, next) {
- var $active = this.$element.find('.item.active')
- , $next = next || $active[type]()
- , isCycling = this.interval
- , direction = type == 'next' ? 'left' : 'right'
- , fallback = type == 'next' ? 'first' : 'last'
- , that = this
- , e
-
- this.sliding = true
-
- isCycling && this.pause()
-
- $next = $next.length ? $next : this.$element.find('.item')[fallback]()
-
- e = $.Event('slide', {
- relatedTarget: $next[0]
- , direction: direction
- })
-
- if ($next.hasClass('active')) return
-
- if (this.$indicators.length) {
- this.$indicators.find('.active').removeClass('active')
- this.$element.one('slid', function () {
- var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
- $nextIndicator && $nextIndicator.addClass('active')
- })
- }
-
- if ($.support.transition && this.$element.hasClass('slide')) {
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
- $next.addClass(type)
- $next[0].offsetWidth // force reflow
- $active.addClass(direction)
- $next.addClass(direction)
- this.$element.one($.support.transition.end, function () {
- $next.removeClass([type, direction].join(' ')).addClass('active')
- $active.removeClass(['active', direction].join(' '))
- that.sliding = false
- setTimeout(function () { that.$element.trigger('slid') }, 0)
- })
- } else {
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
- $active.removeClass('active')
- $next.addClass('active')
- this.sliding = false
- this.$element.trigger('slid')
- }
-
- isCycling && this.cycle()
-
- return this
- }
-
- }
-
-
- /* CAROUSEL PLUGIN DEFINITION
- * ========================== */
-
- var old = $.fn.carousel
-
- $.fn.carousel = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('carousel')
- , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option)
- , action = typeof option == 'string' ? option : options.slide
- if (!data) $this.data('carousel', (data = new Carousel(this, options)))
- if (typeof option == 'number') data.to(option)
- else if (action) data[action]()
- else if (options.interval) data.pause().cycle()
- })
- }
-
- $.fn.carousel.defaults = {
- interval: 5000
- , pause: 'hover'
- }
-
- $.fn.carousel.Constructor = Carousel
-
-
- /* CAROUSEL NO CONFLICT
- * ==================== */
-
- $.fn.carousel.noConflict = function () {
- $.fn.carousel = old
- return this
- }
-
- /* CAROUSEL DATA-API
- * ================= */
-
- $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
- var $this = $(this), href
- , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
- , options = $.extend({}, $target.data(), $this.data())
- , slideIndex
-
- $target.carousel(options)
-
- if (slideIndex = $this.attr('data-slide-to')) {
- $target.data('carousel').pause().to(slideIndex).cycle()
- }
-
- e.preventDefault()
- })
-
-}(window.jQuery);/* =============================================================
- * bootstrap-collapse.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#collapse
- * =============================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============================================================ */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* COLLAPSE PUBLIC CLASS DEFINITION
- * ================================ */
-
- var Collapse = function (element, options) {
- this.$element = $(element)
- this.options = $.extend({}, $.fn.collapse.defaults, options)
-
- if (this.options.parent) {
- this.$parent = $(this.options.parent)
- }
-
- this.options.toggle && this.toggle()
- }
-
- Collapse.prototype = {
-
- constructor: Collapse
-
- , dimension: function () {
- var hasWidth = this.$element.hasClass('width')
- return hasWidth ? 'width' : 'height'
- }
-
- , show: function () {
- var dimension
- , scroll
- , actives
- , hasData
-
- if (this.transitioning || this.$element.hasClass('in')) return
-
- dimension = this.dimension()
- scroll = $.camelCase(['scroll', dimension].join('-'))
- actives = this.$parent && this.$parent.find('> .accordion-group > .in')
-
- if (actives && actives.length) {
- hasData = actives.data('collapse')
- if (hasData && hasData.transitioning) return
- actives.collapse('hide')
- hasData || actives.data('collapse', null)
- }
-
- this.$element[dimension](0)
- this.transition('addClass', $.Event('show'), 'shown')
- $.support.transition && this.$element[dimension](this.$element[0][scroll])
- }
-
- , hide: function () {
- var dimension
- if (this.transitioning || !this.$element.hasClass('in')) return
- dimension = this.dimension()
- this.reset(this.$element[dimension]())
- this.transition('removeClass', $.Event('hide'), 'hidden')
- this.$element[dimension](0)
- }
-
- , reset: function (size) {
- var dimension = this.dimension()
-
- this.$element
- .removeClass('collapse')
- [dimension](size || 'auto')
- [0].offsetWidth
-
- this.$element[size !== null ? 'addClass' : 'removeClass']('collapse')
-
- return this
- }
-
- , transition: function (method, startEvent, completeEvent) {
- var that = this
- , complete = function () {
- if (startEvent.type == 'show') that.reset()
- that.transitioning = 0
- that.$element.trigger(completeEvent)
- }
-
- this.$element.trigger(startEvent)
-
- if (startEvent.isDefaultPrevented()) return
-
- this.transitioning = 1
-
- this.$element[method]('in')
-
- $.support.transition && this.$element.hasClass('collapse') ?
- this.$element.one($.support.transition.end, complete) :
- complete()
- }
-
- , toggle: function () {
- this[this.$element.hasClass('in') ? 'hide' : 'show']()
- }
-
- }
-
-
- /* COLLAPSE PLUGIN DEFINITION
- * ========================== */
-
- var old = $.fn.collapse
-
- $.fn.collapse = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('collapse')
- , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option)
- if (!data) $this.data('collapse', (data = new Collapse(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.collapse.defaults = {
- toggle: true
- }
-
- $.fn.collapse.Constructor = Collapse
-
-
- /* COLLAPSE NO CONFLICT
- * ==================== */
-
- $.fn.collapse.noConflict = function () {
- $.fn.collapse = old
- return this
- }
-
-
- /* COLLAPSE DATA-API
- * ================= */
-
- $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) {
- var $this = $(this), href
- , target = $this.attr('data-target')
- || e.preventDefault()
- || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
- , option = $(target).data('collapse') ? 'toggle' : $this.data()
- $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
- $(target).collapse(option)
- })
-
-}(window.jQuery);/* ============================================================
- * bootstrap-dropdown.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#dropdowns
- * ============================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============================================================ */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* DROPDOWN CLASS DEFINITION
- * ========================= */
-
- var toggle = '[data-toggle=dropdown]'
- , Dropdown = function (element) {
- var $el = $(element).on('click.dropdown.data-api', this.toggle)
- $('html').on('click.dropdown.data-api', function () {
- $el.parent().removeClass('open')
- })
- }
-
- Dropdown.prototype = {
-
- constructor: Dropdown
-
- , toggle: function (e) {
- var $this = $(this)
- , $parent
- , isActive
-
- if ($this.is('.disabled, :disabled')) return
-
- $parent = getParent($this)
-
- isActive = $parent.hasClass('open')
-
- clearMenus()
-
- if (!isActive) {
- $parent.toggleClass('open')
- }
-
- $this.focus()
-
- return false
- }
-
- , keydown: function (e) {
- var $this
- , $items
- , $active
- , $parent
- , isActive
- , index
-
- if (!/(38|40|27)/.test(e.keyCode)) return
-
- $this = $(this)
-
- e.preventDefault()
- e.stopPropagation()
-
- if ($this.is('.disabled, :disabled')) return
-
- $parent = getParent($this)
-
- isActive = $parent.hasClass('open')
-
- if (!isActive || (isActive && e.keyCode == 27)) {
- if (e.which == 27) $parent.find(toggle).focus()
- return $this.click()
- }
-
- $items = $('[role=menu] li:not(.divider):visible a', $parent)
-
- if (!$items.length) return
-
- index = $items.index($items.filter(':focus'))
-
- if (e.keyCode == 38 && index > 0) index-- // up
- if (e.keyCode == 40 && index < $items.length - 1) index++ // down
- if (!~index) index = 0
-
- $items
- .eq(index)
- .focus()
- }
-
- }
-
- function clearMenus() {
- $(toggle).each(function () {
- getParent($(this)).removeClass('open')
- })
- }
-
- function getParent($this) {
- var selector = $this.attr('data-target')
- , $parent
-
- if (!selector) {
- selector = $this.attr('href')
- selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
- }
-
- $parent = selector && $(selector)
-
- if (!$parent || !$parent.length) $parent = $this.parent()
-
- return $parent
- }
-
-
- /* DROPDOWN PLUGIN DEFINITION
- * ========================== */
-
- var old = $.fn.dropdown
-
- $.fn.dropdown = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('dropdown')
- if (!data) $this.data('dropdown', (data = new Dropdown(this)))
- if (typeof option == 'string') data[option].call($this)
- })
- }
-
- $.fn.dropdown.Constructor = Dropdown
-
-
- /* DROPDOWN NO CONFLICT
- * ==================== */
-
- $.fn.dropdown.noConflict = function () {
- $.fn.dropdown = old
- return this
- }
-
-
- /* APPLY TO STANDARD DROPDOWN ELEMENTS
- * =================================== */
-
- $(document)
- .on('click.dropdown.data-api', clearMenus)
- .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
- .on('.dropdown-menu', function (e) { e.stopPropagation() })
- .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
- .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
-
-}(window.jQuery);
-/* =========================================================
- * bootstrap-modal.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#modals
- * =========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================= */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* MODAL CLASS DEFINITION
- * ====================== */
-
- var Modal = function (element, options) {
- this.options = options
- this.$element = $(element)
- .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
- this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
- }
-
- Modal.prototype = {
-
- constructor: Modal
-
- , toggle: function () {
- return this[!this.isShown ? 'show' : 'hide']()
- }
-
- , show: function () {
- var that = this
- , e = $.Event('show')
-
- this.$element.trigger(e)
-
- if (this.isShown || e.isDefaultPrevented()) return
-
- this.isShown = true
-
- this.escape()
-
- this.backdrop(function () {
- var transition = $.support.transition && that.$element.hasClass('fade')
-
- if (!that.$element.parent().length) {
- that.$element.appendTo(document.body) //don't move modals dom position
- }
-
- that.$element.show()
-
- if (transition) {
- that.$element[0].offsetWidth // force reflow
- }
-
- that.$element
- .addClass('in')
- .attr('aria-hidden', false)
-
- that.enforceFocus()
-
- transition ?
- that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) :
- that.$element.focus().trigger('shown')
-
- })
- }
-
- , hide: function (e) {
- e && e.preventDefault()
-
- var that = this
-
- e = $.Event('hide')
-
- this.$element.trigger(e)
-
- if (!this.isShown || e.isDefaultPrevented()) return
-
- this.isShown = false
-
- this.escape()
-
- $(document).off('focusin.modal')
-
- this.$element
- .removeClass('in')
- .attr('aria-hidden', true)
-
- $.support.transition && this.$element.hasClass('fade') ?
- this.hideWithTransition() :
- this.hideModal()
- }
-
- , enforceFocus: function () {
- var that = this
- $(document).on('focusin.modal', function (e) {
- if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
- that.$element.focus()
- }
- })
- }
-
- , escape: function () {
- var that = this
- if (this.isShown && this.options.keyboard) {
- this.$element.on('keyup.dismiss.modal', function ( e ) {
- e.which == 27 && that.hide()
- })
- } else if (!this.isShown) {
- this.$element.off('keyup.dismiss.modal')
- }
- }
-
- , hideWithTransition: function () {
- var that = this
- , timeout = setTimeout(function () {
- that.$element.off($.support.transition.end)
- that.hideModal()
- }, 500)
-
- this.$element.one($.support.transition.end, function () {
- clearTimeout(timeout)
- that.hideModal()
- })
- }
-
- , hideModal: function () {
- var that = this
- this.$element.hide()
- this.backdrop(function () {
- that.removeBackdrop()
- that.$element.trigger('hidden')
- })
- }
-
- , removeBackdrop: function () {
- this.$backdrop.remove()
- this.$backdrop = null
- }
-
- , backdrop: function (callback) {
- var that = this
- , animate = this.$element.hasClass('fade') ? 'fade' : ''
-
- if (this.isShown && this.options.backdrop) {
- var doAnimate = $.support.transition && animate
-
- this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
- .appendTo(document.body)
-
- this.$backdrop.click(
- this.options.backdrop == 'static' ?
- $.proxy(this.$element[0].focus, this.$element[0])
- : $.proxy(this.hide, this)
- )
-
- if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
-
- this.$backdrop.addClass('in')
-
- if (!callback) return
-
- doAnimate ?
- this.$backdrop.one($.support.transition.end, callback) :
- callback()
-
- } else if (!this.isShown && this.$backdrop) {
- this.$backdrop.removeClass('in')
-
- $.support.transition && this.$element.hasClass('fade')?
- this.$backdrop.one($.support.transition.end, callback) :
- callback()
-
- } else if (callback) {
- callback()
- }
- }
- }
-
-
- /* MODAL PLUGIN DEFINITION
- * ======================= */
-
- var old = $.fn.modal
-
- $.fn.modal = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('modal')
- , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
- if (!data) $this.data('modal', (data = new Modal(this, options)))
- if (typeof option == 'string') data[option]()
- else if (options.show) data.show()
- })
- }
-
- $.fn.modal.defaults = {
- backdrop: true
- , keyboard: true
- , show: true
- }
-
- $.fn.modal.Constructor = Modal
-
-
- /* MODAL NO CONFLICT
- * ================= */
-
- $.fn.modal.noConflict = function () {
- $.fn.modal = old
- return this
- }
-
-
- /* MODAL DATA-API
- * ============== */
-
- $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) {
- var $this = $(this)
- , href = $this.attr('href')
- , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
- , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())
-
- e.preventDefault()
-
- $target
- .modal(option)
- .one('hide', function () {
- $this.focus()
- })
- })
-
-}(window.jQuery);
-/* ===========================================================
- * bootstrap-tooltip.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#tooltips
- * Inspired by the original jQuery.tipsy by Jason Frame
- * ===========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* TOOLTIP PUBLIC CLASS DEFINITION
- * =============================== */
-
- var Tooltip = function (element, options) {
- this.init('tooltip', element, options)
- }
-
- Tooltip.prototype = {
-
- constructor: Tooltip
-
- , init: function (type, element, options) {
- var eventIn
- , eventOut
- , triggers
- , trigger
- , i
-
- this.type = type
- this.$element = $(element)
- this.options = this.getOptions(options)
- this.enabled = true
-
- triggers = this.options.trigger.split(' ')
-
- for (i = triggers.length; i--;) {
- trigger = triggers[i]
- if (trigger == 'click') {
- this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
- } else if (trigger != 'manual') {
- eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
- eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
- this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
- this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
- }
- }
-
- this.options.selector ?
- (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
- this.fixTitle()
- }
-
- , getOptions: function (options) {
- options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options)
-
- if (options.delay && typeof options.delay == 'number') {
- options.delay = {
- show: options.delay
- , hide: options.delay
- }
- }
-
- return options
- }
-
- , enter: function (e) {
- var self = $(e.currentTarget)[this.type](this._options).data(this.type)
-
- if (!self.options.delay || !self.options.delay.show) return self.show()
-
- clearTimeout(this.timeout)
- self.hoverState = 'in'
- this.timeout = setTimeout(function() {
- if (self.hoverState == 'in') self.show()
- }, self.options.delay.show)
- }
-
- , leave: function (e) {
- var self = $(e.currentTarget)[this.type](this._options).data(this.type)
-
- if (this.timeout) clearTimeout(this.timeout)
- if (!self.options.delay || !self.options.delay.hide) return self.hide()
-
- self.hoverState = 'out'
- this.timeout = setTimeout(function() {
- if (self.hoverState == 'out') self.hide()
- }, self.options.delay.hide)
- }
-
- , show: function () {
- var $tip
- , pos
- , actualWidth
- , actualHeight
- , placement
- , tp
- , e = $.Event('show')
-
- if (this.hasContent() && this.enabled) {
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
- $tip = this.tip()
- this.setContent()
-
- if (this.options.animation) {
- $tip.addClass('fade')
- }
-
- placement = typeof this.options.placement == 'function' ?
- this.options.placement.call(this, $tip[0], this.$element[0]) :
- this.options.placement
-
- $tip
- .detach()
- .css({ top: 0, left: 0, display: 'block' })
-
- this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
-
- pos = this.getPosition()
-
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
-
- switch (placement) {
- case 'bottom':
- tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
- break
- case 'top':
- tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
- break
- case 'left':
- tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
- break
- case 'right':
- tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
- break
- }
-
- this.applyPlacement(tp, placement)
- this.$element.trigger('shown')
- }
- }
-
- , applyPlacement: function(offset, placement){
- var $tip = this.tip()
- , width = $tip[0].offsetWidth
- , height = $tip[0].offsetHeight
- , actualWidth
- , actualHeight
- , delta
- , replace
-
- $tip
- .offset(offset)
- .addClass(placement)
- .addClass('in')
-
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
-
- if (placement == 'top' && actualHeight != height) {
- offset.top = offset.top + height - actualHeight
- replace = true
- }
-
- if (placement == 'bottom' || placement == 'top') {
- delta = 0
-
- if (offset.left < 0){
- delta = offset.left * -2
- offset.left = 0
- $tip.offset(offset)
- actualWidth = $tip[0].offsetWidth
- actualHeight = $tip[0].offsetHeight
- }
-
- this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
- } else {
- this.replaceArrow(actualHeight - height, actualHeight, 'top')
- }
-
- if (replace) $tip.offset(offset)
- }
-
- , replaceArrow: function(delta, dimension, position){
- this
- .arrow()
- .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
- }
-
- , setContent: function () {
- var $tip = this.tip()
- , title = this.getTitle()
-
- $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
- $tip.removeClass('fade in top bottom left right')
- }
-
- , hide: function () {
- var that = this
- , $tip = this.tip()
- , e = $.Event('hide')
-
- this.$element.trigger(e)
- if (e.isDefaultPrevented()) return
-
- $tip.removeClass('in')
-
- function removeWithAnimation() {
- var timeout = setTimeout(function () {
- $tip.off($.support.transition.end).detach()
- }, 500)
-
- $tip.one($.support.transition.end, function () {
- clearTimeout(timeout)
- $tip.detach()
- })
- }
-
- $.support.transition && this.$tip.hasClass('fade') ?
- removeWithAnimation() :
- $tip.detach()
-
- this.$element.trigger('hidden')
-
- return this
- }
-
- , fixTitle: function () {
- var $e = this.$element
- if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
- $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
- }
- }
-
- , hasContent: function () {
- return this.getTitle()
- }
-
- , getPosition: function () {
- var el = this.$element[0]
- return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
- width: el.offsetWidth
- , height: el.offsetHeight
- }, this.$element.offset())
- }
-
- , getTitle: function () {
- var title
- , $e = this.$element
- , o = this.options
-
- title = $e.attr('data-original-title')
- || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
-
- return title
- }
-
- , tip: function () {
- return this.$tip = this.$tip || $(this.options.template)
- }
-
- , arrow: function(){
- return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow")
- }
-
- , validate: function () {
- if (!this.$element[0].parentNode) {
- this.hide()
- this.$element = null
- this.options = null
- }
- }
-
- , enable: function () {
- this.enabled = true
- }
-
- , disable: function () {
- this.enabled = false
- }
-
- , toggleEnabled: function () {
- this.enabled = !this.enabled
- }
-
- , toggle: function (e) {
- var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this
- self.tip().hasClass('in') ? self.hide() : self.show()
- }
-
- , destroy: function () {
- this.hide().$element.off('.' + this.type).removeData(this.type)
- }
-
- }
-
-
- /* TOOLTIP PLUGIN DEFINITION
- * ========================= */
-
- var old = $.fn.tooltip
-
- $.fn.tooltip = function ( option ) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('tooltip')
- , options = typeof option == 'object' && option
- if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.tooltip.Constructor = Tooltip
-
- $.fn.tooltip.defaults = {
- animation: true
- , placement: 'top'
- , selector: false
- , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- , trigger: 'hover focus'
- , title: ''
- , delay: 0
- , html: false
- , container: false
- }
-
-
- /* TOOLTIP NO CONFLICT
- * =================== */
-
- $.fn.tooltip.noConflict = function () {
- $.fn.tooltip = old
- return this
- }
-
-}(window.jQuery);
-/* ===========================================================
- * bootstrap-popover.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#popovers
- * ===========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * =========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* POPOVER PUBLIC CLASS DEFINITION
- * =============================== */
-
- var Popover = function (element, options) {
- this.init('popover', element, options)
- }
-
-
- /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js
- ========================================== */
-
- Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, {
-
- constructor: Popover
-
- , setContent: function () {
- var $tip = this.tip()
- , title = this.getTitle()
- , content = this.getContent()
-
- $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
- $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content)
-
- $tip.removeClass('fade top bottom left right in')
- }
-
- , hasContent: function () {
- return this.getTitle() || this.getContent()
- }
-
- , getContent: function () {
- var content
- , $e = this.$element
- , o = this.options
-
- content = (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)
- || $e.attr('data-content')
-
- return content
- }
-
- , tip: function () {
- if (!this.$tip) {
- this.$tip = $(this.options.template)
- }
- return this.$tip
- }
-
- , destroy: function () {
- this.hide().$element.off('.' + this.type).removeData(this.type)
- }
-
- })
-
-
- /* POPOVER PLUGIN DEFINITION
- * ======================= */
-
- var old = $.fn.popover
-
- $.fn.popover = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('popover')
- , options = typeof option == 'object' && option
- if (!data) $this.data('popover', (data = new Popover(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.popover.Constructor = Popover
-
- $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
- placement: 'right'
- , trigger: 'click'
- , content: ''
- , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
- })
-
-
- /* POPOVER NO CONFLICT
- * =================== */
-
- $.fn.popover.noConflict = function () {
- $.fn.popover = old
- return this
- }
-
-}(window.jQuery);
-/* =============================================================
- * bootstrap-scrollspy.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#scrollspy
- * =============================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* SCROLLSPY CLASS DEFINITION
- * ========================== */
-
- function ScrollSpy(element, options) {
- var process = $.proxy(this.process, this)
- , $element = $(element).is('body') ? $(window) : $(element)
- , href
- this.options = $.extend({}, $.fn.scrollspy.defaults, options)
- this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process)
- this.selector = (this.options.target
- || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
- || '') + ' .nav li > a'
- this.$body = $('body')
- this.refresh()
- this.process()
- }
-
- ScrollSpy.prototype = {
-
- constructor: ScrollSpy
-
- , refresh: function () {
- var self = this
- , $targets
-
- this.offsets = $([])
- this.targets = $([])
-
- $targets = this.$body
- .find(this.selector)
- .map(function () {
- var $el = $(this)
- , href = $el.data('target') || $el.attr('href')
- , $href = /^#\w/.test(href) && $(href)
- return ( $href
- && $href.length
- && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null
- })
- .sort(function (a, b) { return a[0] - b[0] })
- .each(function () {
- self.offsets.push(this[0])
- self.targets.push(this[1])
- })
- }
-
- , process: function () {
- var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
- , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
- , maxScroll = scrollHeight - this.$scrollElement.height()
- , offsets = this.offsets
- , targets = this.targets
- , activeTarget = this.activeTarget
- , i
-
- if (scrollTop >= maxScroll) {
- return activeTarget != (i = targets.last()[0])
- && this.activate ( i )
- }
-
- for (i = offsets.length; i--;) {
- activeTarget != targets[i]
- && scrollTop >= offsets[i]
- && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
- && this.activate( targets[i] )
- }
- }
-
- , activate: function (target) {
- var active
- , selector
-
- this.activeTarget = target
-
- $(this.selector)
- .parent('.active')
- .removeClass('active')
-
- selector = this.selector
- + '[data-target="' + target + '"],'
- + this.selector + '[href="' + target + '"]'
-
- active = $(selector)
- .parent('li')
- .addClass('active')
-
- if (active.parent('.dropdown-menu').length) {
- active = active.closest('li.dropdown').addClass('active')
- }
-
- active.trigger('activate')
- }
-
- }
-
-
- /* SCROLLSPY PLUGIN DEFINITION
- * =========================== */
-
- var old = $.fn.scrollspy
-
- $.fn.scrollspy = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('scrollspy')
- , options = typeof option == 'object' && option
- if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.scrollspy.Constructor = ScrollSpy
-
- $.fn.scrollspy.defaults = {
- offset: 10
- }
-
-
- /* SCROLLSPY NO CONFLICT
- * ===================== */
-
- $.fn.scrollspy.noConflict = function () {
- $.fn.scrollspy = old
- return this
- }
-
-
- /* SCROLLSPY DATA-API
- * ================== */
-
- $(window).on('load', function () {
- $('[data-spy="scroll"]').each(function () {
- var $spy = $(this)
- $spy.scrollspy($spy.data())
- })
- })
-
-}(window.jQuery);/* ========================================================
- * bootstrap-tab.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#tabs
- * ========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ======================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* TAB CLASS DEFINITION
- * ==================== */
-
- var Tab = function (element) {
- this.element = $(element)
- }
-
- Tab.prototype = {
-
- constructor: Tab
-
- , show: function () {
- var $this = this.element
- , $ul = $this.closest('ul:not(.dropdown-menu)')
- , selector = $this.attr('data-target')
- , previous
- , $target
- , e
-
- if (!selector) {
- selector = $this.attr('href')
- selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
- }
-
- if ( $this.parent('li').hasClass('active') ) return
-
- previous = $ul.find('.active:last a')[0]
-
- e = $.Event('show', {
- relatedTarget: previous
- })
-
- $this.trigger(e)
-
- if (e.isDefaultPrevented()) return
-
- $target = $(selector)
-
- this.activate($this.parent('li'), $ul)
- this.activate($target, $target.parent(), function () {
- $this.trigger({
- type: 'shown'
- , relatedTarget: previous
- })
- })
- }
-
- , activate: function ( element, container, callback) {
- var $active = container.find('> .active')
- , transition = callback
- && $.support.transition
- && $active.hasClass('fade')
-
- function next() {
- $active
- .removeClass('active')
- .find('> .dropdown-menu > .active')
- .removeClass('active')
-
- element.addClass('active')
-
- if (transition) {
- element[0].offsetWidth // reflow for transition
- element.addClass('in')
- } else {
- element.removeClass('fade')
- }
-
- if ( element.parent('.dropdown-menu') ) {
- element.closest('li.dropdown').addClass('active')
- }
-
- callback && callback()
- }
-
- transition ?
- $active.one($.support.transition.end, next) :
- next()
-
- $active.removeClass('in')
- }
- }
-
-
- /* TAB PLUGIN DEFINITION
- * ===================== */
-
- var old = $.fn.tab
-
- $.fn.tab = function ( option ) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('tab')
- if (!data) $this.data('tab', (data = new Tab(this)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.tab.Constructor = Tab
-
-
- /* TAB NO CONFLICT
- * =============== */
-
- $.fn.tab.noConflict = function () {
- $.fn.tab = old
- return this
- }
-
-
- /* TAB DATA-API
- * ============ */
-
- $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
- e.preventDefault()
- $(this).tab('show')
- })
-
-}(window.jQuery);/* =============================================================
- * bootstrap-typeahead.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#typeahead
- * =============================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============================================================ */
-
-
-!function($){
-
- "use strict"; // jshint ;_;
-
-
- /* TYPEAHEAD PUBLIC CLASS DEFINITION
- * ================================= */
-
- var Typeahead = function (element, options) {
- this.$element = $(element)
- this.options = $.extend({}, $.fn.typeahead.defaults, options)
- this.matcher = this.options.matcher || this.matcher
- this.sorter = this.options.sorter || this.sorter
- this.highlighter = this.options.highlighter || this.highlighter
- this.updater = this.options.updater || this.updater
- this.source = this.options.source
- this.$menu = $(this.options.menu)
- this.shown = false
- this.listen()
- }
-
- Typeahead.prototype = {
-
- constructor: Typeahead
-
- , select: function () {
- var val = this.$menu.find('.active').attr('data-value')
- this.$element
- .val(this.updater(val))
- .change()
- return this.hide()
- }
-
- , updater: function (item) {
- return item
- }
-
- , show: function () {
- var pos = $.extend({}, this.$element.position(), {
- height: this.$element[0].offsetHeight
- })
-
- this.$menu
- .insertAfter(this.$element)
- .css({
- top: pos.top + pos.height
- , left: pos.left
- })
- .show()
-
- this.shown = true
- return this
- }
-
- , hide: function () {
- this.$menu.hide()
- this.shown = false
- return this
- }
-
- , lookup: function (event) {
- var items
-
- this.query = this.$element.val()
-
- if (!this.query || this.query.length < this.options.minLength) {
- return this.shown ? this.hide() : this
- }
-
- items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
-
- return items ? this.process(items) : this
- }
-
- , process: function (items) {
- var that = this
-
- items = $.grep(items, function (item) {
- return that.matcher(item)
- })
-
- items = this.sorter(items)
-
- if (!items.length) {
- return this.shown ? this.hide() : this
- }
-
- return this.render(items.slice(0, this.options.items)).show()
- }
-
- , matcher: function (item) {
- return ~item.toLowerCase().indexOf(this.query.toLowerCase())
- }
-
- , sorter: function (items) {
- var beginswith = []
- , caseSensitive = []
- , caseInsensitive = []
- , item
-
- while (item = items.shift()) {
- if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
- else if (~item.indexOf(this.query)) caseSensitive.push(item)
- else caseInsensitive.push(item)
- }
-
- return beginswith.concat(caseSensitive, caseInsensitive)
- }
-
- , highlighter: function (item) {
- var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
- return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
- return '<strong>' + match + '</strong>'
- })
- }
-
- , render: function (items) {
- var that = this
-
- items = $(items).map(function (i, item) {
- i = $(that.options.item).attr('data-value', item)
- i.find('a').html(that.highlighter(item))
- return i[0]
- })
-
- items.first().addClass('active')
- this.$menu.html(items)
- return this
- }
-
- , next: function (event) {
- var active = this.$menu.find('.active').removeClass('active')
- , next = active.next()
-
- if (!next.length) {
- next = $(this.$menu.find('li')[0])
- }
-
- next.addClass('active')
- }
-
- , prev: function (event) {
- var active = this.$menu.find('.active').removeClass('active')
- , prev = active.prev()
-
- if (!prev.length) {
- prev = this.$menu.find('li').last()
- }
-
- prev.addClass('active')
- }
-
- , listen: function () {
- this.$element
- .on('focus', $.proxy(this.focus, this))
- .on('blur', $.proxy(this.blur, this))
- .on('keypress', $.proxy(this.keypress, this))
- .on('keyup', $.proxy(this.keyup, this))
-
- if (this.eventSupported('keydown')) {
- this.$element.on('keydown', $.proxy(this.keydown, this))
- }
-
- this.$menu
- .on('click', $.proxy(this.click, this))
- .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
- .on('mouseleave', 'li', $.proxy(this.mouseleave, this))
- }
-
- , eventSupported: function(eventName) {
- var isSupported = eventName in this.$element
- if (!isSupported) {
- this.$element.setAttribute(eventName, 'return;')
- isSupported = typeof this.$element[eventName] === 'function'
- }
- return isSupported
- }
-
- , move: function (e) {
- if (!this.shown) return
-
- switch(e.keyCode) {
- case 9: // tab
- case 13: // enter
- case 27: // escape
- e.preventDefault()
- break
-
- case 38: // up arrow
- e.preventDefault()
- this.prev()
- break
-
- case 40: // down arrow
- e.preventDefault()
- this.next()
- break
- }
-
- e.stopPropagation()
- }
-
- , keydown: function (e) {
- this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27])
- this.move(e)
- }
-
- , keypress: function (e) {
- if (this.suppressKeyPressRepeat) return
- this.move(e)
- }
-
- , keyup: function (e) {
- switch(e.keyCode) {
- case 40: // down arrow
- case 38: // up arrow
- case 16: // shift
- case 17: // ctrl
- case 18: // alt
- break
-
- case 9: // tab
- case 13: // enter
- if (!this.shown) return
- this.select()
- break
-
- case 27: // escape
- if (!this.shown) return
- this.hide()
- break
-
- default:
- this.lookup()
- }
-
- e.stopPropagation()
- e.preventDefault()
- }
-
- , focus: function (e) {
- this.focused = true
- }
-
- , blur: function (e) {
- this.focused = false
- if (!this.mousedover && this.shown) this.hide()
- }
-
- , click: function (e) {
- e.stopPropagation()
- e.preventDefault()
- this.select()
- this.$element.focus()
- }
-
- , mouseenter: function (e) {
- this.mousedover = true
- this.$menu.find('.active').removeClass('active')
- $(e.currentTarget).addClass('active')
- }
-
- , mouseleave: function (e) {
- this.mousedover = false
- if (!this.focused && this.shown) this.hide()
- }
-
- }
-
-
- /* TYPEAHEAD PLUGIN DEFINITION
- * =========================== */
-
- var old = $.fn.typeahead
-
- $.fn.typeahead = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('typeahead')
- , options = typeof option == 'object' && option
- if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.typeahead.defaults = {
- source: []
- , items: 8
- , menu: '<ul class="typeahead dropdown-menu"></ul>'
- , item: '<li><a href="#"></a></li>'
- , minLength: 1
- }
-
- $.fn.typeahead.Constructor = Typeahead
-
-
- /* TYPEAHEAD NO CONFLICT
- * =================== */
-
- $.fn.typeahead.noConflict = function () {
- $.fn.typeahead = old
- return this
- }
-
-
- /* TYPEAHEAD DATA-API
- * ================== */
-
- $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
- var $this = $(this)
- if ($this.data('typeahead')) return
- $this.typeahead($this.data())
- })
-
-}(window.jQuery);
-/* ==========================================================
- * bootstrap-affix.js v2.3.0
- * http://twitter.github.com/bootstrap/javascript.html#affix
- * ==========================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================================================== */
-
-
-!function ($) {
-
- "use strict"; // jshint ;_;
-
-
- /* AFFIX CLASS DEFINITION
- * ====================== */
-
- var Affix = function (element, options) {
- this.options = $.extend({}, $.fn.affix.defaults, options)
- this.$window = $(window)
- .on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
- .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this))
- this.$element = $(element)
- this.checkPosition()
- }
-
- Affix.prototype.checkPosition = function () {
- if (!this.$element.is(':visible')) return
-
- var scrollHeight = $(document).height()
- , scrollTop = this.$window.scrollTop()
- , position = this.$element.offset()
- , offset = this.options.offset
- , offsetBottom = offset.bottom
- , offsetTop = offset.top
- , reset = 'affix affix-top affix-bottom'
- , affix
-
- if (typeof offset != 'object') offsetBottom = offsetTop = offset
- if (typeof offsetTop == 'function') offsetTop = offset.top()
- if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
-
- affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
- false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
- 'bottom' : offsetTop != null && scrollTop <= offsetTop ?
- 'top' : false
-
- if (this.affixed === affix) return
-
- this.affixed = affix
- this.unpin = affix == 'bottom' ? position.top - scrollTop : null
-
- this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
- }
-
-
- /* AFFIX PLUGIN DEFINITION
- * ======================= */
-
- var old = $.fn.affix
-
- $.fn.affix = function (option) {
- return this.each(function () {
- var $this = $(this)
- , data = $this.data('affix')
- , options = typeof option == 'object' && option
- if (!data) $this.data('affix', (data = new Affix(this, options)))
- if (typeof option == 'string') data[option]()
- })
- }
-
- $.fn.affix.Constructor = Affix
-
- $.fn.affix.defaults = {
- offset: 0
- }
-
-
- /* AFFIX NO CONFLICT
- * ================= */
-
- $.fn.affix.noConflict = function () {
- $.fn.affix = old
- return this
- }
-
-
- /* AFFIX DATA-API
- * ============== */
-
- $(window).on('load', function () {
- $('[data-spy="affix"]').each(function () {
- var $spy = $(this)
- , data = $spy.data()
-
- data.offset = data.offset || {}
-
- data.offsetBottom && (data.offset.bottom = data.offsetBottom)
- data.offsetTop && (data.offset.top = data.offsetTop)
-
- $spy.affix(data)
- })
- })
-
-
-}(window.jQuery); \ No newline at end of file
diff --git a/nikola/data/themes/default/messages/messages_gr.py b/nikola/data/themes/default/messages/messages_gr.py
deleted file mode 100644
index c6135f3..0000000
--- a/nikola/data/themes/default/messages/messages_gr.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# -*- encoding:utf-8 -*-
-from __future__ import unicode_literals
-
-MESSAGES = {
- "LANGUAGE": "Ελληνικά",
- "Posts for year %s": "Αναρτήσεις για τη χρονιά %s",
- "Archive": "Αρχείο",
- "Posts about %s": "Αναρτήσεις για %s",
- "Tags": "Ετικέτες",
- "Also available in": "Διαθέσιμο και στο",
- "More posts about": "Περισσότερες αναρτήσεις για",
- "Posted": "Αναρτήθηκε",
- "Original site": "Ιστοσελίδα αρχικής ανάρτησης",
- "Read in English": "Διαβάστε στα Ελληνικά",
- "Newer posts": "Νεότερες αναρτήσεις",
- "Older posts": "Παλαιότερες αναρτήσεις",
- "Previous post": "Προηγούμενη ανάρτηση",
- "Next post": "Επόμενη ανάρτηση",
- "old posts page %d": "σελίδα παλαιότερων αναρτήσεων %d",
- "Read more": "Read more",
- "Source": "Source",
-}
diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl
deleted file mode 100644
index 72a4077..0000000
--- a/nikola/data/themes/default/templates/base.tmpl
+++ /dev/null
@@ -1,60 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace file="base_helper.tmpl" import="*"/>
-${set_locale(lang)}
-<!DOCTYPE html>
-<html lang="${lang}">
-<head>
- ${html_head()}
- <%block name="extra_head">
- </%block>
- ${extra_head_data}
-</head>
-<body>
- %if add_this_buttons:
- <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script>
- % endif
-<div class="container-fluid" id="container">
- <div class="row-fluid" id="titlerow">
- <div class="span12" id="titlecolumn">
- <!-- Banner-like substance !-->
- <div class="titlebox">
- <h1 id="blog-title">
- <a href="${abs_link('/')}" title="${blog_title}">${blog_title}</a>
- </h1>
- <%block name="belowtitle">
- %if len(translations) > 1:
- <small>
- ${(messages("Also available in"))}:&nbsp;
- ${html_translations()}
- </small>
- %endif
- </%block>
- <hr>
- </div>
- <!-- End of banner-like substance !-->
- <div class="row-fluid" id="contentrow">
- <div class="span10" id="contentcolumn">
- <!--Body content-->
- <%block name="content"></%block>
- <!--End of body content-->
- <hr>
- <small>${content_footer}</small>
- </div>
- <div class="span2" id="sidebar">
- <!--Sidebar content-->
- <ul class="unstyled">
- <li>${license}
- ${html_social()}
- ${html_sidebar_links()}
- <li>${search_form}
- </ul>
- <!--End of sidebar content-->
- </div>
- </div>
- </div>
- </div>
-</div>
- ${late_load_js()}
- ${analytics}
- <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
-</body>
diff --git a/nikola/data/themes/default/templates/disqus_helper.tmpl b/nikola/data/themes/default/templates/disqus_helper.tmpl
deleted file mode 100644
index 4c60f85..0000000
--- a/nikola/data/themes/default/templates/disqus_helper.tmpl
+++ /dev/null
@@ -1,43 +0,0 @@
-## -*- coding: utf-8 -*-
-<%!
- import json
- translations = {
- 'es': 'es_ES',
- }
-%>
-<%def name="html_disqus(url, title, identifier)">
- %if disqus_forum:
- <div id="disqus_thread"></div>
- <script type="text/javascript">
- var disqus_shortname ="${disqus_forum}";
- %if url:
- var disqus_url="${url}";
- %endif
- var disqus_title=${json.dumps(title)};
- var disqus_identifier="${identifier}";
- var disqus_config = function () {
- this.language = "${translations.get(lang, lang)}";
- };
- (function() {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- })();
- </script>
- <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
- %endif
-</%def>
-
-<%def name="html_disqus_link(link, identifier)">
- <p>
- %if disqus_forum:
- <a href="${link}" data-disqus-identifier="${identifier}">Comments</a>
- %endif
-</%def>
-
-
-<%def name="html_disqus_script()">
- %if disqus_forum:
- <script type="text/javascript">var disqus_shortname="${disqus_forum}";(function(){var a=document.createElement("script");a.async=true;a.type="text/javascript";a.src="http://"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("HEAD")[0]||document.getElementsByTagName("BODY")[0]).appendChild(a)}());</script>
- %endif
-</%def>
diff --git a/nikola/data/themes/default/templates/gallery.tmpl b/nikola/data/themes/default/templates/gallery.tmpl
deleted file mode 100644
index 09c25cc..0000000
--- a/nikola/data/themes/default/templates/gallery.tmpl
+++ /dev/null
@@ -1,31 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%block name="sourcelink"></%block>
-
-<%block name="content">
- <ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a>
- % endfor
- </ul>
- %if text:
- <p>
- ${text}
- </p>
- %endif
- <ul>
- % for folder in folders:
- <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a>
- % endfor
- </ul>
- <ul class="thumbnails">
- %for image in images:
- <li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}>
- <img src="${image[1]}" /></a>
- %endfor
- </ul>
-%if enable_comments:
- ${disqus.html_disqus(None, permalink, title)}
-%endif
-</%block>
diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl
deleted file mode 100644
index b49e764..0000000
--- a/nikola/data/themes/default/templates/index.tmpl
+++ /dev/null
@@ -1,22 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="index_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- % for post in posts:
- <div class="postbox">
- <h1><a href="${post.permalink()}">${post.title()}</a>
- <small>&nbsp;&nbsp;
- ${messages("Posted")}: <time class="published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
- </small></h1>
- <hr>
- ${post.text(teaser_only=index_teasers)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus_link(post.permalink()+"#disqus_thread", post.base_path)}
- % endif
- </div>
- % endfor
- ${helper.html_pager()}
- ${disqus.html_disqus_script()}
- ${helper.mathjax_script(post)}
-</%block>
diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl
deleted file mode 100644
index f9e24d2..0000000
--- a/nikola/data/themes/default/templates/post.tmpl
+++ /dev/null
@@ -1,33 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="extra_head">
-${helper.twitter_card_information(post)}
-</%block>
-<%block name="content">
- <div class="postbox">
- ${helper.html_title()}
- <hr>
- <small>
- ${messages("Posted")}: <time class="published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
- ${helper.html_translations(post)}
- ${helper.html_tags(post)}
- </small>
- <hr>
- ${post.text()}
- ${helper.html_pager(post)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
- % endif
- ${helper.mathjax_script(post)}
- </div>
-</%block>
-
-<%block name="sourcelink">
-% if not post.meta('password'):
- <li>
- <a href="${post.meta('slug')+post.source_ext()}" id="sourcelink">${messages("Source")}</a>
- </li>
-% endif
-</%block>
diff --git a/nikola/data/themes/default/templates/post_helper.tmpl b/nikola/data/themes/default/templates/post_helper.tmpl
deleted file mode 100644
index cce0ecf..0000000
--- a/nikola/data/themes/default/templates/post_helper.tmpl
+++ /dev/null
@@ -1,73 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_title()">
- <h1>${title}</h1>
- % if link:
- <p><a href='${link}'>${messages("Original site")}</a></p>
- % endif
-</%def>
-
-
-<%def name="html_translations(post)">
- %if len(translations) > 1:
- %for langname in translations.keys():
- %if langname != lang and post.is_translation_available(langname):
- &nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.permalink(langname)}">${messages("Read in English", langname)}</a>
- %endif
- %endfor
- %endif
-</%def>
-
-
-<%def name="html_tags(post)">
- %if post.tags:
- &nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
- %for tag in post.tags:
- <a class="tag" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
- %endfor
- %endif
-</%def>
-
-<%def name="html_pager(post)">
- <ul class="pager">
- %if post.prev_post:
- <li class="previous">
- <a href="${post.prev_post.permalink()}">&larr; ${messages("Previous post")}</a>
- </li>
- %endif
- %if post.next_post:
- <li class="next">
- <a href="${post.next_post.permalink()}">${messages("Next post")} &rarr;</a>
- </li>
- %endif
- </ul>
-</%def>
-
-<%def name="twitter_card_information(post)">
- %if twitter_card and twitter_card['use_twitter_cards']:
- <meta name="twitter:card" content="${twitter_card.get('card', 'summary')|h}">
- <meta name="og:url" content="${post.permalink(absolute=True)}">
- %if 'site:id' in twitter_card:
- <meta name="twitter:site:id" content="${twitter_card['site:id']}">
- %elif 'site' in twitter_card:
- <meta name="twitter:site" content="${twitter_card['site']}">
- %endif
- %if 'creator:id' in twitter_card:
- <meta name="twitter:creator:id" content="${twitter_card['creator:id']}">
- %elif 'creator' in twitter_card:
- <meta name="twitter:creator" content="${twitter_card['creator']}">
- %endif
- <meta name="og:title" content="${post.title()[:70]|h}">
- %if post.description():
- <meta name="og:description" content="${post.description()[:200]|h}">
- %else:
- <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}">
- %endif
- %endif
-</%def>
-
-<%def name="mathjax_script(post)">
- %if post.is_mathjax:
- <script src="/assets/js/mathjax.js" type="text/javascript"></script>
- %endif
-</%def>
diff --git a/nikola/data/themes/default/templates/story.tmpl b/nikola/data/themes/default/templates/story.tmpl
deleted file mode 100644
index c1c06d8..0000000
--- a/nikola/data/themes/default/templates/story.tmpl
+++ /dev/null
@@ -1,16 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="post.tmpl"/>
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%block name="extra_head">
-${helper.twitter_card_information(post)}
-</%block>
-<%block name="content">
-%if title:
- <h1>${title}</h1>
-%endif
- ${post.text()}
-%if enable_comments and not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
-%endif
-</%block>
diff --git a/nikola/data/themes/default/templates/tags.tmpl b/nikola/data/themes/default/templates/tags.tmpl
deleted file mode 100644
index 5727dc5..0000000
--- a/nikola/data/themes/default/templates/tags.tmpl
+++ /dev/null
@@ -1,12 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <!--Body content-->
- <h1>${title}</h1>
- <ul class="unstyled bricks">
- % for text, link in items:
- <li><a class="reference" href="${link}">${text}</a></li>
- % endfor
- </ul>
- <!--End of body content-->
-</%block>
diff --git a/nikola/data/themes/jinja-default/README b/nikola/data/themes/jinja-default/README
deleted file mode 100644
index 4b0bf04..0000000
--- a/nikola/data/themes/jinja-default/README
+++ /dev/null
@@ -1,3 +0,0 @@
-This theme is exactly the same as "default" but using Jinja2 templates.
-
-To try it, set THEME="jinja-default" in your 'conf.py'
diff --git a/nikola/data/themes/jinja-default/engine b/nikola/data/themes/jinja-default/engine
deleted file mode 100644
index 6f04b30..0000000
--- a/nikola/data/themes/jinja-default/engine
+++ /dev/null
@@ -1 +0,0 @@
-jinja
diff --git a/nikola/data/themes/jinja-default/parent b/nikola/data/themes/jinja-default/parent
deleted file mode 100644
index 4ad96d5..0000000
--- a/nikola/data/themes/jinja-default/parent
+++ /dev/null
@@ -1 +0,0 @@
-default
diff --git a/nikola/data/themes/jinja-default/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl
deleted file mode 100644
index c104b20..0000000
--- a/nikola/data/themes/jinja-default/templates/base.tmpl
+++ /dev/null
@@ -1,131 +0,0 @@
-<!DOCTYPE html>
-{{set_locale(lang)}}
-<html lang="{{lang}}">
-<head>
- <meta charset="utf-8">
- <meta name="description" content="{{description}}" >
- <meta name="author" content="{{blog_author}}">
- <title>{{title}} | {{blog_title}}</title>
- <!-- Le styles -->
- {% if use_bundles %}
- {% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- {% else %}
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- {% else %}
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
- {% endif %}
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
- {% if has_custom_css %}
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- {% endif %}
- {% endif %}
- <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
- <!--[if lt IE 9]>
- <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
- <![endif]-->
- {% if rss_link %}
- {{rss_link}}
- {% else %}
- {% for language in translations %}
- <link rel="alternate" type="application/rss+xml" title="RSS ({{language}})" href="{{_link("rss", None, lang)}}">
- {% endfor %}
- {% endif %}
- {% block extra_head %}
- {% endblock %}
- {{extra_head_data}}
-</head>
-<body>
- {% if add_this_buttons %}
- <script type="text/javascript">var addthis_config={"ui_language":"{{lang}}"};</script>
- {% endif %}
-<div class="container-fluid" id="container">
- <div class="row-fluid" id="titlerow">
- <div class="span12" id="titlecolumn">
- <!-- Banner-like substance !-->
- <div class="titlebox">
- <h1 id="blog-title">
- <a href="{{abs_link('/')}}" title="{{blog_title}}">{{blog_title}}</a>
- </h1>
- {% block belowtitle%}
- {% if translations|length > 1 %}
- <small>
- {{ messages("Also available in") }}:&nbsp;
- {% for langname in translations.keys() %}
- {% if langname != lang %}
- <a href="{{_link("index", None, langname)}}">{{messages("LANGUAGE", langname)}}</a>
- {% endif %}
- {% endfor %}
- </small>
- {% endif %}
- {% endblock %}
- <hr>
- </div>
- <!-- End of banner-like substance !-->
- <div class="row-fluid" id="contentrow">
- <div class="span10" id="contentcolumn">
- <!--Body content-->
- {% block content %}{% endblock %}
- <!--End of body content-->
- <hr>
- <small>{{content_footer}}</small>
- </div>
- <div class="span2" id="sidebar">
- <!--Sidebar content-->
- <ul class="unstyled">
- <li>{{license}}
- {% for url, text in sidebar_links[lang] %}
- <li><a href="{{url}}">{{text}}</a>
- {% endfor %}
- <li>{{search_form}}
- </ul>
- <!--End of sidebar content-->
- </div>
- {% if add_this_buttons %}
- <!-- social buttons -->
- <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
- <a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a>
- <li><a class="addthis_button_google_plusone_share"></a>
- <li><a class="addthis_button_linkedin"></a>
- <li><a class="addthis_button_twitter"></a>
- </ul>
- </div>
- <script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
- <!-- End of social buttons -->
- {% endif %}
- </div>
- </div>
- </div>
-</div>
- <!-- late load javascript -->
- {% if use_bundles %}
- {% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
- <script src="/assets/js/all.js" type="text/javascript"></script>
- {% else %}
- <script src="/assets/js/all-nocdn.js" type="text/javascript"></script>
- {% endif %}
- {% else %}
- {% if use_cdn %}
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
- <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
- {% else %}
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script>
- <script src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
- {% endif %}
- <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
- {% endif %}
- {{analytics}}
- <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script>
-</body>
diff --git a/nikola/data/themes/jinja-default/templates/gallery.tmpl b/nikola/data/themes/jinja-default/templates/gallery.tmpl
deleted file mode 100644
index 9b16df6..0000000
--- a/nikola/data/themes/jinja-default/templates/gallery.tmpl
+++ /dev/null
@@ -1,47 +0,0 @@
-{% extends "base.tmpl" %}
-{% block sourcelink %}{% endblock %}
-
-{% block content %}
- <ul class="breadcrumb">
- {% for link, crumb in crumbs %}
- <li><a href="{{link}}">/ {{crumb}}</a>
- {% endfor %}
- </ul>
- {% if text %}
- <p>
- {{ text }}
- </p>
- {% endif %}
- <ul>
- {% for folder in folders %}
- <li><a href="{{folder}}"><i class="icon-folder-open"></i>&nbsp;{{folder}}</a>
- {% endfor %}
- </ul>
- <ul class="thumbnails">
- {% for image in images %}
- <li><a href="{{image[0]}}" class="thumbnail image-reference" {{image[2]}}>
- <img src="{{image[1]}}" /></a>
- {% endfor %}
- </ul>
- {%if enable_comments %}
- {%if disqus_forum %}
- <div id="disqus_thread"></div>
- <script type="text/javascript">
- var disqus_shortname ="{{disqus_forum}}";
- var disqus_title={{title|tojson}};
- var disqus_identifier="{{permalink}}";
- var disqus_config = function () {
- this.language = "{{lang}}";
- };
- (function() {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- })();
- </script>
- <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
- {% endif %}
- {% endif %}
-
-{% endblock %}
-
diff --git a/nikola/data/themes/jinja-default/templates/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl
deleted file mode 100644
index 7d1aa00..0000000
--- a/nikola/data/themes/jinja-default/templates/index.tmpl
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
- {% for post in posts %}
- <div class="postbox">
- <h1><a href="{{post.permalink()}}">{{post.title()}}</a>
- <small>&nbsp;&nbsp;
- {{messages("Posted")}}: {{post.formatted_date(date_format)}}
- </small></h1>
- <hr>
- {{post.text(teaser_only=index_teasers)}}
- <p>
- {% if disqus_forum and not post.meta('nocomments')%}
- <a href="{{post.permalink()}}#disqus_thread" data-disqus-identifier="{{post.base_path}}">Comments</a>
- {% endif %}
- </div>
- {% endfor %}
- <div>
-<ul class="pager">
- {%if prevlink %}
- <li class="previous">
- <a href="{{prevlink}}">&larr; {{messages("Newer posts")}}</a>
- {% endif %}
- {% if nextlink %}
- <li class="next">
- <a href="{{nextlink}}">{{messages("Older posts")}} &rarr;</a>
- {% endif %}
-</ul>
-
- </div>
- <hr>
- {% if disqus_forum %}
- <script type="text/javascript"> var disqus_shortname = '{{disqus_forum}}'; (function () { var s = document.createElement('script'); s.async = true; s.type = 'text/javascript'; s.src = 'http://' + disqus_shortname + '.disqus.com/count.js'; (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s); }()); </script>
- {% endif %}
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/list.tmpl b/nikola/data/themes/jinja-default/templates/list.tmpl
deleted file mode 100644
index eb11fd0..0000000
--- a/nikola/data/themes/jinja-default/templates/list.tmpl
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
- <!--Body content-->
- <div class="postbox">
- <h1>{{title}}</h1>
- <ul class="unstyled">
- {% for text, link in items %}
- <li><a href="{{link}}">{{text}}</a>
- {% endfor %}
- </ul>
- </div>
- <!--End of body content-->
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/list_post.tmpl b/nikola/data/themes/jinja-default/templates/list_post.tmpl
deleted file mode 100644
index b4ac59e..0000000
--- a/nikola/data/themes/jinja-default/templates/list_post.tmpl
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
- <!--Body content-->
- <div class="postbox">
- <h1>{{title}}</h1>
- <ul class="unstyled">
- {% for post in posts %}
- <li><a href="{{post.permalink()}}">[{{post.formatted_date(date_format)}}] {{post.title()}}</a>
- {% endfor %}
- </ul>
- </div>
- <!--End of body content-->
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/listing.tmpl b/nikola/data/themes/jinja-default/templates/listing.tmpl
deleted file mode 100644
index 493624a..0000000
--- a/nikola/data/themes/jinja-default/templates/listing.tmpl
+++ /dev/null
@@ -1,9 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
-<ul class="breadcrumb">
- {% for link, crumb in crumbs %}
- <li><a href="{{link}}">/ {{crumb}}</a>
- {% endfor %}
-</ul>
-{{code}}
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl
deleted file mode 100644
index ab96682..0000000
--- a/nikola/data/themes/jinja-default/templates/post.tmpl
+++ /dev/null
@@ -1,61 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
- <div class="postbox">
- <h1><a href='{{permalink}}'>{{title}}</a></h1>
- {% if link %}
- <p><a href='{{link}}'>{{messages("Original site")}}</a></p>
- {% endif %}
- <hr>
- <small>
- {{messages("Posted")}}: {{post.formatted_date(date_format)}}&nbsp;&nbsp;|&nbsp;&nbsp;
-
- {% if translations|length > 1 %}
- {% for langname in translations.keys() %}
- {% if langname != lang and post.is_translation_available(langname) %}
- <a href="{{post.permalink(langname)}}">{{messages("Read in English", langname)}}</a>
- &nbsp;&nbsp;|&nbsp;&nbsp;
- {% endif %}
- {% endfor %}
- {% endif %}
- {% if not post.meta('password')
- <a href="{{post.meta('slug')+".txt"}}" id="sourcelink">{{messages("Source")}}</a>
- {% endif %}
- {% if post.tags %}
- &nbsp;&nbsp;|&nbsp;&nbsp;{{messages("More posts about")}}
- {% for tag in post.tags %}
- <a href="{{_link("tag", tag)}}"><span class="badge badge-info">{{tag}}</span></a>
- {% endfor %}
- {% endif %}
- </small>
- <hr>
- {{post.text()}}
- <ul class="pager">
- {%if post.prev_post %}
- <li class="previous">
- <a href="{{rel_link(permalink, post.prev_post.permalink())}}">&larr; {{messages("Previous post")}}</a>
- {% endif %}
- {%if post.next_post %}
- <li class="next">
- <a href="{{rel_link(permalink, post.next_post.permalink())}}">{{messages("Next post")}} &rarr;</a>
- {% endif %}
- </ul>
- {% if disqus_forum and not post.meta('nocomments')%}
- <div id="disqus_thread"></div>
- <script type="text/javascript">
- var disqus_shortname ="{{disqus_forum}}";
- var disqus_url="{{post.permalink(absolute=True)}}";
- var disqus_title={{post.title()|tojson }};
- var disqus_identifier="{{post.base_path}}";
- var disqus_config = function () {
- this.language = "{{lang}}";
- };
- (function() {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- })();
- </script>
- <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
- {% endif %}
- </div>
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/story.tmpl b/nikola/data/themes/jinja-default/templates/story.tmpl
deleted file mode 100644
index a4ad375..0000000
--- a/nikola/data/themes/jinja-default/templates/story.tmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "post.tmpl" %}
-{% block content %}
-{% if title %}
- <h1>{{title}}</h1>
-{% endif %}
- {{post.text()}}
-{%if enable_comments and disqus_forum and not post.meta('nocomments')%}
- <div id="disqus_thread"></div>
- <script type="text/javascript">
- var disqus_shortname ="{{disqus_forum}}";
- var disqus_url="{{post.permalink(absolute=True)}}";
- var disqus_title={{post.title()|tojson }};
- var disqus_identifier="{{post.base_path}}";
- var disqus_config = function () {
- this.language = "{{lang}}";
- };
- (function() {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- })();
-</script>
-<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
-{%endif%}
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/tag.tmpl b/nikola/data/themes/jinja-default/templates/tag.tmpl
deleted file mode 100644
index 77db27d..0000000
--- a/nikola/data/themes/jinja-default/templates/tag.tmpl
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends "list_post.tmpl"%}
-{%block extra_head %}
- {% for language in translations %}
- <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag {{tag}} ({{language}})" href="{{_link("tag_rss", tag, language)}}">
- {% endfor %}
-{% endblock %}
diff --git a/nikola/data/themes/jinja-default/templates/tags.tmpl b/nikola/data/themes/jinja-default/templates/tags.tmpl
deleted file mode 100644
index 0fa9d0f..0000000
--- a/nikola/data/themes/jinja-default/templates/tags.tmpl
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "base.tmpl" %}
-{% block content %}
- <div class="postbox">
- <!--Body content-->
- <h1>{{title}}</h1>
- <ul class="unstyled bricks">
- {% for text, link in items %}
- <li><a href="{{link}}">{{text}}</a></li>
- {% endfor %}
- </ul>
- <!--End of body content-->
- </div>
-{% endblock %}
diff --git a/nikola/data/themes/monospace/assets/css/theme.css b/nikola/data/themes/monospace/assets/css/theme.css
deleted file mode 100644
index b9c0cf2..0000000
--- a/nikola/data/themes/monospace/assets/css/theme.css
+++ /dev/null
@@ -1,14 +0,0 @@
-body { margin:0px; padding:20px 0px; text-align:center; font-family:Monospace; color:#585858; }
-.post { margin:0px 0px 30px 0px; padding:0px 0px 30px 0px; border-bottom:1px dotted #C8C8C8; }
-.meta { margin:10px; padding:15px; background:#EAEAEA; clear:both; }
-#footer { text-align:center; clear:both; margin:30px 0px 0px 0px; padding:30px 0px 0px 0px; border-top:1px dotted #C8C8C8; }
-#wrap { margin:0px auto; text-align:left; font-size: 13px; line-height: 1.4; }
-#container { float:right; }
-#sidebar { overflow:hidden; clear:left; text-align:right; width:250px; height:auto; padding:0px 15px 0px 0px; border-right:1px dotted #C8C8C8; }
-#sidebar li { list-style-type:none; }
-#sidebar > li { margin:20px 0px; }
-#sidebar h1 { border-bottom:1px dotted #C8C8C8; }
-#sidebar .description { display:block; width:100%; height:auto; margin:0px 0px 10px 0px; }
-h1, h2, h3, h4, h5, h6, h7 { margin:0px; text-transform:uppercase; }
-h4, h5, h6 { font-size:14px; }
-#blog-title { margin-top: 0; line-height:48px;}
diff --git a/nikola/data/themes/monospace/templates/base.tmpl b/nikola/data/themes/monospace/templates/base.tmpl
deleted file mode 100644
index 806795d..0000000
--- a/nikola/data/themes/monospace/templates/base.tmpl
+++ /dev/null
@@ -1,45 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace file="base_helper.tmpl" import="*"/>
-${set_locale(lang)}
-<!DOCTYPE html>
-<html lang="${lang}">
-<head>
- ${html_head()}
- <%block name="extra_head">
- </%block>
- ${extra_head_data}
-</head>
-<body class="home blog">
- %if add_this_buttons:
- <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script>
- % endif
- <div id="wrap" style="width:850px">
- <div id="container" style="width:560px">
- <%block name="content"></%block>
- </div>
- <div id="sidebar">
- <!--Sidebar content-->
- <h1 id="blog-title">
- <a href="${abs_link('/')}" title="${blog_title}">${blog_title}</a>
- </h1>
- <%block name="belowtitle">
- %if len(translations) > 1:
- <small>
- ${(messages("Also available in"))}:&nbsp;
- ${html_translations()}
- </small>
- %endif
- </%block>
- <ul class="unstyled">
- <li>${license}
- ${html_social()}
- ${html_sidebar_links()}
- <li>${search_form}
- </ul>
- </div>
- <div id="footer">
- ${content_footer}
- </div>
- </div>
- ${analytics}
-</body>
diff --git a/nikola/data/themes/monospace/templates/base_helper.tmpl b/nikola/data/themes/monospace/templates/base_helper.tmpl
deleted file mode 100644
index 4f3e45b..0000000
--- a/nikola/data/themes/monospace/templates/base_helper.tmpl
+++ /dev/null
@@ -1,82 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_head()">
- <meta charset="utf-8">
- <meta name="description" content="${description}" >
- <meta name="author" content="${blog_author}">
- <title>${title} | ${blog_title}</title>
- ${mathjax_config}
- %if use_bundles:
- %if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- %else:
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- %endif
- %else:
- %if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- %else:
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
- %endif
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
- %if has_custom_css:
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- %endif
- %endif
- <!--[if lt IE 9]>
- <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
- <![endif]-->
- %if rss_link:
- ${rss_link}
- %else:
- %for language in translations:
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
- %endfor
- %endif
- %if favicons:
- %for name, file, size in favicons:
- <link rel="${name}" href="${file}" sizes="${size}"/>
- %endfor
- %endif
-</%def>
-
-
-<%def name="html_social()">
-%if add_this_buttons:
- <!-- Social buttons -->
- <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
- <a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a>
- <li><a class="addthis_button_google_plusone_share"></a>
- <li><a class="addthis_button_linkedin"></a>
- <li><a class="addthis_button_twitter"></a>
- </ul>
- </div>
- <script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
- <!-- End of social buttons -->
-%endif
-</%def>
-
-
-<%def name="html_sidebar_links()">
- %for url, text in sidebar_links[lang]:
- % if rel_link(permalink, url) == "#":
- <li class="active"><a href="${url}">${text}</a>
- %else:
- <li><a href="${url}">${text}</a>
- %endif
- %endfor
-</%def>
-
-
-<%def name="html_translations()">
- %for langname in translations.keys():
- %if langname != lang:
- <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
- %endif
- %endfor
-</%def>
diff --git a/nikola/data/themes/monospace/templates/gallery.tmpl b/nikola/data/themes/monospace/templates/gallery.tmpl
deleted file mode 100644
index 3186cc8..0000000
--- a/nikola/data/themes/monospace/templates/gallery.tmpl
+++ /dev/null
@@ -1,31 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%block name="sourcelink"></%block>
-
-<%block name="content">
- <ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
- % endfor
- </ul>
- %if text:
- <p>
- ${text}
- </p>
- %endif
- <ul>
- % for folder in folders:
- <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
- % endfor
- </ul>
- <ul class="thumbnails">
- %for image in images:
- <li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}>
- <img src="${image[1]}" /></a></li>
- %endfor
- </ul>
-%if enable_comments:
- ${disqus.html_disqus(None, permalink, title)}
-%endif
-</%block>
diff --git a/nikola/data/themes/monospace/templates/index.tmpl b/nikola/data/themes/monospace/templates/index.tmpl
deleted file mode 100644
index 4a0c630..0000000
--- a/nikola/data/themes/monospace/templates/index.tmpl
+++ /dev/null
@@ -1,30 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="index_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- % for post in posts:
- <div class="postbox">
- <h1><a href="${post.permalink()}">${post.title()}</a></h1>
- <div class="meta" style="background-color: rgb(234, 234, 234); ">
- <span class="authordate">
- ${messages("Posted")}: ${post.formatted_date(date_format)}
- </span>
- <br>
- <span class="tags">Tags:&nbsp;
- %if post.tags:
- %for tag in post.tags:
- <a class="tag" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
- %endfor
- %endif
- </span>
- </div>
- ${post.text(teaser_only=index_teasers)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus_link(post.permalink()+"#disqus_thread", post.base_path)}
- % endif
- </div>
- % endfor
- ${helper.html_pager()}
- ${disqus.html_disqus_script()}
-</%block>
diff --git a/nikola/data/themes/monospace/templates/index_helper.tmpl b/nikola/data/themes/monospace/templates/index_helper.tmpl
deleted file mode 100644
index 1bb700c..0000000
--- a/nikola/data/themes/monospace/templates/index_helper.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_pager()">
-<div>
-<ul class="pager">
- %if prevlink:
- <li class="previous">
- <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
- </li>
- %endif
- %if nextlink:
- <li class="next">
- <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
- </li>
- %endif
-</ul>
-</div>
-</%def>
diff --git a/nikola/data/themes/monospace/templates/list.tmpl b/nikola/data/themes/monospace/templates/list.tmpl
deleted file mode 100644
index a60b508..0000000
--- a/nikola/data/themes/monospace/templates/list.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <!--Body content-->
- <div class="postbox">
- <h1>${title}</h1>
- <ul class="unstyled">
- % for text, link in items:
- <li><a href="${link}">${text}</a>
- % endfor
- </ul>
- </div>
- <!--End of body content-->
-</%block>
diff --git a/nikola/data/themes/monospace/templates/list_post.tmpl b/nikola/data/themes/monospace/templates/list_post.tmpl
deleted file mode 100644
index f0e159d..0000000
--- a/nikola/data/themes/monospace/templates/list_post.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <!--Body content-->
- <div class="postbox">
- <h1>${title}</h1>
- <ul class="unstyled">
- % for post in posts:
- <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a>
- % endfor
- </ul>
- </div>
- <!--End of body content-->
-</%block>
diff --git a/nikola/data/themes/monospace/templates/listing.tmpl b/nikola/data/themes/monospace/templates/listing.tmpl
deleted file mode 100644
index 596a704..0000000
--- a/nikola/data/themes/monospace/templates/listing.tmpl
+++ /dev/null
@@ -1,10 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
-<ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
- % endfor
-</ul>
-${code}
-</%block>
diff --git a/nikola/data/themes/monospace/templates/post.tmpl b/nikola/data/themes/monospace/templates/post.tmpl
deleted file mode 100644
index 0ec360d..0000000
--- a/nikola/data/themes/monospace/templates/post.tmpl
+++ /dev/null
@@ -1,34 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <div class="post">
- ${helper.html_title()}
- <div class="meta" style="background-color: rgb(234, 234, 234); ">
- <span class="authordate">
- ${messages("Posted")}: ${post.formatted_date(date_format)}
- % if not post.meta('password'):
- [<a href="${post.meta('slug')+'.txt'}" id="sourcelink">${messages("Source")}</a>]
- % endif
- </span>
- <br>
- %if post.tags:
- <span class="tags">${messages("Tags")}:&nbsp;
- %for tag in post.tags:
- <a class="tag" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
- %endfor
- </span>
- <br>
- %endif
- <span class="authordate">
- ${helper.html_translations(post)}
- </span>
- </div>
- ${post.text()}
- ${helper.html_pager(post)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
- % endif
- </div>
-</%block>
diff --git a/nikola/data/themes/monospace/templates/post_helper.tmpl b/nikola/data/themes/monospace/templates/post_helper.tmpl
deleted file mode 100644
index cce0ecf..0000000
--- a/nikola/data/themes/monospace/templates/post_helper.tmpl
+++ /dev/null
@@ -1,73 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_title()">
- <h1>${title}</h1>
- % if link:
- <p><a href='${link}'>${messages("Original site")}</a></p>
- % endif
-</%def>
-
-
-<%def name="html_translations(post)">
- %if len(translations) > 1:
- %for langname in translations.keys():
- %if langname != lang and post.is_translation_available(langname):
- &nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.permalink(langname)}">${messages("Read in English", langname)}</a>
- %endif
- %endfor
- %endif
-</%def>
-
-
-<%def name="html_tags(post)">
- %if post.tags:
- &nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
- %for tag in post.tags:
- <a class="tag" href="${_link('tag', tag)}"><span class="badge badge-info">${tag}</span></a>
- %endfor
- %endif
-</%def>
-
-<%def name="html_pager(post)">
- <ul class="pager">
- %if post.prev_post:
- <li class="previous">
- <a href="${post.prev_post.permalink()}">&larr; ${messages("Previous post")}</a>
- </li>
- %endif
- %if post.next_post:
- <li class="next">
- <a href="${post.next_post.permalink()}">${messages("Next post")} &rarr;</a>
- </li>
- %endif
- </ul>
-</%def>
-
-<%def name="twitter_card_information(post)">
- %if twitter_card and twitter_card['use_twitter_cards']:
- <meta name="twitter:card" content="${twitter_card.get('card', 'summary')|h}">
- <meta name="og:url" content="${post.permalink(absolute=True)}">
- %if 'site:id' in twitter_card:
- <meta name="twitter:site:id" content="${twitter_card['site:id']}">
- %elif 'site' in twitter_card:
- <meta name="twitter:site" content="${twitter_card['site']}">
- %endif
- %if 'creator:id' in twitter_card:
- <meta name="twitter:creator:id" content="${twitter_card['creator:id']}">
- %elif 'creator' in twitter_card:
- <meta name="twitter:creator" content="${twitter_card['creator']}">
- %endif
- <meta name="og:title" content="${post.title()[:70]|h}">
- %if post.description():
- <meta name="og:description" content="${post.description()[:200]|h}">
- %else:
- <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}">
- %endif
- %endif
-</%def>
-
-<%def name="mathjax_script(post)">
- %if post.is_mathjax:
- <script src="/assets/js/mathjax.js" type="text/javascript"></script>
- %endif
-</%def>
diff --git a/nikola/data/themes/monospace/templates/story.tmpl b/nikola/data/themes/monospace/templates/story.tmpl
deleted file mode 100644
index 21d0e2f..0000000
--- a/nikola/data/themes/monospace/templates/story.tmpl
+++ /dev/null
@@ -1,15 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="post.tmpl"/>
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%block name="extra_head">
-${helper.twitter_card_information(post)}
-</%block>
-<%block name="content">
-%if title:
- <h1>${title}</h1>
-%endif
- ${post.text()}
-%if enable_comments and not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
-%endif
-</%block>
diff --git a/nikola/data/themes/monospace/templates/tag.tmpl b/nikola/data/themes/monospace/templates/tag.tmpl
deleted file mode 100644
index 97aafeb..0000000
--- a/nikola/data/themes/monospace/templates/tag.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="list_post.tmpl"/>
-<%block name="extra_head">
- %for language in translations:
- <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag} (${language})" href="${_link("tag_rss", tag, language)}">
- %endfor
-</%block>
diff --git a/nikola/data/themes/monospace/templates/tags.tmpl b/nikola/data/themes/monospace/templates/tags.tmpl
deleted file mode 100644
index 369a3d5..0000000
--- a/nikola/data/themes/monospace/templates/tags.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <div class="postbox">
- <!--Body content-->
- <h1>${title}</h1>
- <ul class="unstyled">
- % for text, link in items:
- <li><a class="tag" href="${link}"><span class="badge badge-info">${text}</span></a>
- % endfor
- </ul>
- <!--End of body content-->
- </div>
-</%block>
diff --git a/nikola/data/themes/orphan/assets/css/rst.css b/nikola/data/themes/orphan/assets/css/rst.css
deleted file mode 120000
index 2e56146..0000000
--- a/nikola/data/themes/orphan/assets/css/rst.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../default/assets/css/rst.css \ No newline at end of file
diff --git a/nikola/data/themes/orphan/bundles b/nikola/data/themes/orphan/bundles
deleted file mode 100644
index aa35d9c..0000000
--- a/nikola/data/themes/orphan/bundles
+++ /dev/null
@@ -1 +0,0 @@
-assets/css/all.css=rst.css,code.css,theme.css
diff --git a/nikola/data/themes/orphan/templates/base.tmpl b/nikola/data/themes/orphan/templates/base.tmpl
deleted file mode 100644
index 2a62b58..0000000
--- a/nikola/data/themes/orphan/templates/base.tmpl
+++ /dev/null
@@ -1,37 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace file="base_helper.tmpl" import="*"/>
-${set_locale(lang)}
-<!DOCTYPE html>
-<html lang="${lang}">
-<head>
- ${html_head()}
- <%block name="extra_head">
- </%block>
- ${extra_head_data}
-</head>
-<body>
- %if add_this_buttons:
- <script type="text/javascript">var addthis_config={"ui_language":"${lang}"};</script>
- % endif
- <h1 id="blog-title">
- <a href="${abs_link('/')}" title="${blog_title}">${blog_title}</a>
- </h1>
- <%block name="belowtitle">
- %if len(translations) > 1:
- <small>
- ${(messages("Also available in"))}:&nbsp;
- ${html_translations()}
- </small>
- %endif
- </%block>
- <%block name="content"></%block>
- <small>${content_footer}</small>
- <!--Sidebar content-->
- <ul class="unstyled">
- <li>${license}
- ${html_social()}
- ${html_sidebar_links()}
- <li>${search_form}
- </ul>
- ${analytics}
-</body>
diff --git a/nikola/data/themes/orphan/templates/base_helper.tmpl b/nikola/data/themes/orphan/templates/base_helper.tmpl
deleted file mode 100644
index 4f3e45b..0000000
--- a/nikola/data/themes/orphan/templates/base_helper.tmpl
+++ /dev/null
@@ -1,82 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_head()">
- <meta charset="utf-8">
- <meta name="description" content="${description}" >
- <meta name="author" content="${blog_author}">
- <title>${title} | ${blog_title}</title>
- ${mathjax_config}
- %if use_bundles:
- %if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- %else:
- <link href="/assets/css/all-nocdn.css" rel="stylesheet" type="text/css">
- %endif
- %else:
- %if use_cdn:
- <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
- %else:
- <link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/bootstrap-responsive.min.css" rel="stylesheet" type="text/css">
- %endif
- <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
- <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
- <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
- %if has_custom_css:
- <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- %endif
- %endif
- <!--[if lt IE 9]>
- <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script>
- <![endif]-->
- %if rss_link:
- ${rss_link}
- %else:
- %for language in translations:
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
- %endfor
- %endif
- %if favicons:
- %for name, file, size in favicons:
- <link rel="${name}" href="${file}" sizes="${size}"/>
- %endfor
- %endif
-</%def>
-
-
-<%def name="html_social()">
-%if add_this_buttons:
- <!-- Social buttons -->
- <div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
- <a class="addthis_button_more">Share</a>
- <ul><li><a class="addthis_button_facebook"></a>
- <li><a class="addthis_button_google_plusone_share"></a>
- <li><a class="addthis_button_linkedin"></a>
- <li><a class="addthis_button_twitter"></a>
- </ul>
- </div>
- <script type="text/javascript" src="http://s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
- <!-- End of social buttons -->
-%endif
-</%def>
-
-
-<%def name="html_sidebar_links()">
- %for url, text in sidebar_links[lang]:
- % if rel_link(permalink, url) == "#":
- <li class="active"><a href="${url}">${text}</a>
- %else:
- <li><a href="${url}">${text}</a>
- %endif
- %endfor
-</%def>
-
-
-<%def name="html_translations()">
- %for langname in translations.keys():
- %if langname != lang:
- <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
- %endif
- %endfor
-</%def>
diff --git a/nikola/data/themes/orphan/templates/disqus_helper.tmpl b/nikola/data/themes/orphan/templates/disqus_helper.tmpl
deleted file mode 100644
index 4c60f85..0000000
--- a/nikola/data/themes/orphan/templates/disqus_helper.tmpl
+++ /dev/null
@@ -1,43 +0,0 @@
-## -*- coding: utf-8 -*-
-<%!
- import json
- translations = {
- 'es': 'es_ES',
- }
-%>
-<%def name="html_disqus(url, title, identifier)">
- %if disqus_forum:
- <div id="disqus_thread"></div>
- <script type="text/javascript">
- var disqus_shortname ="${disqus_forum}";
- %if url:
- var disqus_url="${url}";
- %endif
- var disqus_title=${json.dumps(title)};
- var disqus_identifier="${identifier}";
- var disqus_config = function () {
- this.language = "${translations.get(lang, lang)}";
- };
- (function() {
- var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
- dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- })();
- </script>
- <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
- %endif
-</%def>
-
-<%def name="html_disqus_link(link, identifier)">
- <p>
- %if disqus_forum:
- <a href="${link}" data-disqus-identifier="${identifier}">Comments</a>
- %endif
-</%def>
-
-
-<%def name="html_disqus_script()">
- %if disqus_forum:
- <script type="text/javascript">var disqus_shortname="${disqus_forum}";(function(){var a=document.createElement("script");a.async=true;a.type="text/javascript";a.src="http://"+disqus_shortname+".disqus.com/count.js";(document.getElementsByTagName("HEAD")[0]||document.getElementsByTagName("BODY")[0]).appendChild(a)}());</script>
- %endif
-</%def>
diff --git a/nikola/data/themes/orphan/templates/gallery.tmpl b/nikola/data/themes/orphan/templates/gallery.tmpl
deleted file mode 100644
index 3186cc8..0000000
--- a/nikola/data/themes/orphan/templates/gallery.tmpl
+++ /dev/null
@@ -1,31 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%block name="sourcelink"></%block>
-
-<%block name="content">
- <ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
- % endfor
- </ul>
- %if text:
- <p>
- ${text}
- </p>
- %endif
- <ul>
- % for folder in folders:
- <li><a href="${folder}"><i class="icon-folder-open"></i>&nbsp;${folder}</a></li>
- % endfor
- </ul>
- <ul class="thumbnails">
- %for image in images:
- <li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}>
- <img src="${image[1]}" /></a></li>
- %endfor
- </ul>
-%if enable_comments:
- ${disqus.html_disqus(None, permalink, title)}
-%endif
-</%block>
diff --git a/nikola/data/themes/orphan/templates/index.tmpl b/nikola/data/themes/orphan/templates/index.tmpl
deleted file mode 100644
index 59d391a..0000000
--- a/nikola/data/themes/orphan/templates/index.tmpl
+++ /dev/null
@@ -1,21 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="index_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- % for post in posts:
- <div class="postbox">
- <h1><a href="${post.permalink()}">${post.title()}</a>
- <small>&nbsp;&nbsp;
- ${messages("Posted")}: ${post.formatted_date(date_format)}
- </small></h1>
- <hr>
- ${post.text(teaser_only=index_teasers)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus_link(post.permalink()+"#disqus_thread", post.base_path)}
- % endif
- </div>
- % endfor
- ${helper.html_pager()}
- ${disqus.html_disqus_script()}
-</%block>
diff --git a/nikola/data/themes/orphan/templates/index_helper.tmpl b/nikola/data/themes/orphan/templates/index_helper.tmpl
deleted file mode 100644
index 1bb700c..0000000
--- a/nikola/data/themes/orphan/templates/index_helper.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-## -*- coding: utf-8 -*-
-<%def name="html_pager()">
-<div>
-<ul class="pager">
- %if prevlink:
- <li class="previous">
- <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
- </li>
- %endif
- %if nextlink:
- <li class="next">
- <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
- </li>
- %endif
-</ul>
-</div>
-</%def>
diff --git a/nikola/data/themes/orphan/templates/list.tmpl b/nikola/data/themes/orphan/templates/list.tmpl
deleted file mode 100644
index a60b508..0000000
--- a/nikola/data/themes/orphan/templates/list.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <!--Body content-->
- <div class="postbox">
- <h1>${title}</h1>
- <ul class="unstyled">
- % for text, link in items:
- <li><a href="${link}">${text}</a>
- % endfor
- </ul>
- </div>
- <!--End of body content-->
-</%block>
diff --git a/nikola/data/themes/orphan/templates/list_post.tmpl b/nikola/data/themes/orphan/templates/list_post.tmpl
deleted file mode 100644
index f0e159d..0000000
--- a/nikola/data/themes/orphan/templates/list_post.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <!--Body content-->
- <div class="postbox">
- <h1>${title}</h1>
- <ul class="unstyled">
- % for post in posts:
- <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a>
- % endfor
- </ul>
- </div>
- <!--End of body content-->
-</%block>
diff --git a/nikola/data/themes/orphan/templates/listing.tmpl b/nikola/data/themes/orphan/templates/listing.tmpl
deleted file mode 100644
index 596a704..0000000
--- a/nikola/data/themes/orphan/templates/listing.tmpl
+++ /dev/null
@@ -1,10 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
-<ul class="breadcrumb">
- % for link, crumb in crumbs:
- <li><a href="${link}">/ ${crumb}</a></li>
- % endfor
-</ul>
-${code}
-</%block>
diff --git a/nikola/data/themes/orphan/templates/post.tmpl b/nikola/data/themes/orphan/templates/post.tmpl
deleted file mode 100644
index 6f6529d..0000000
--- a/nikola/data/themes/orphan/templates/post.tmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <div class="postbox">
- ${helper.html_title()}
- <hr>
- <small>
- ${messages("Posted")}: ${post.formatted_date(date_format)}
- ${helper.html_translations(post)}
- &nbsp;&nbsp;|&nbsp;&nbsp;
- % if not post.meta('password'):
- <a href="${post.meta('slug')+'.txt'}" id="sourcelink">${messages("Source")}</a>
- % endif
- ${helper.html_tags(post)}
- </small>
- <hr>
- ${post.text()}
- ${helper.html_pager(post)}
- % if not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
- % endif
- </div>
-</%block>
diff --git a/nikola/data/themes/orphan/templates/tag.tmpl b/nikola/data/themes/orphan/templates/tag.tmpl
deleted file mode 100644
index 97aafeb..0000000
--- a/nikola/data/themes/orphan/templates/tag.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="list_post.tmpl"/>
-<%block name="extra_head">
- %for language in translations:
- <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag} (${language})" href="${_link("tag_rss", tag, language)}">
- %endfor
-</%block>
diff --git a/nikola/data/themes/orphan/templates/tags.tmpl b/nikola/data/themes/orphan/templates/tags.tmpl
deleted file mode 100644
index 369a3d5..0000000
--- a/nikola/data/themes/orphan/templates/tags.tmpl
+++ /dev/null
@@ -1,14 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="base.tmpl"/>
-<%block name="content">
- <div class="postbox">
- <!--Body content-->
- <h1>${title}</h1>
- <ul class="unstyled">
- % for text, link in items:
- <li><a class="tag" href="${link}"><span class="badge badge-info">${text}</span></a>
- % endfor
- </ul>
- <!--End of body content-->
- </div>
-</%block>
diff --git a/nikola/data/themes/site-planetoid/README b/nikola/data/themes/site-planetoid/README
deleted file mode 100644
index c148591..0000000
--- a/nikola/data/themes/site-planetoid/README
+++ /dev/null
@@ -1 +0,0 @@
-A version of the site theme for the use with the "planetoid" plugin.
diff --git a/nikola/data/themes/site-planetoid/parent b/nikola/data/themes/site-planetoid/parent
deleted file mode 100644
index 1320f90..0000000
--- a/nikola/data/themes/site-planetoid/parent
+++ /dev/null
@@ -1 +0,0 @@
-site
diff --git a/nikola/data/themes/site-planetoid/templates/index.tmpl b/nikola/data/themes/site-planetoid/templates/index.tmpl
deleted file mode 100644
index 29243e0..0000000
--- a/nikola/data/themes/site-planetoid/templates/index.tmpl
+++ /dev/null
@@ -1,16 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="index_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="content">
- % for post in posts:
- <div style="border: 2px solid darkgrey; margin-bottom: 12px; border-radius: 4px; padding:12px; overflow: auto;">
- <a href="${post.meta('link')}"><h1>${post.title(lang)}</a>
- <small>&nbsp;&nbsp;
- ${messages("Posted")}: <time class="published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
- </small></h1>
- ${post.text(lang)}
- </div>
- % endfor
- ${helper.html_pager()}
-</ul>
-</%block>
diff --git a/nikola/data/themes/site-planetoid/templates/post.tmpl b/nikola/data/themes/site-planetoid/templates/post.tmpl
deleted file mode 100644
index d60de78..0000000
--- a/nikola/data/themes/site-planetoid/templates/post.tmpl
+++ /dev/null
@@ -1,9 +0,0 @@
-## -*- coding: utf-8 -*-
-<html>
-<head>
-<meta http-equiv="Refresh" content="0;url=${post.meta('link')}">
-</head>
-<body>
-Redirecting you to <a href="${post.meta('link')}">the original location.</a>
-</body>
-</html>
diff --git a/nikola/data/themes/site-planetoid/templates/story.tmpl b/nikola/data/themes/site-planetoid/templates/story.tmpl
deleted file mode 100644
index 7712e71..0000000
--- a/nikola/data/themes/site-planetoid/templates/story.tmpl
+++ /dev/null
@@ -1,25 +0,0 @@
-## -*- coding: utf-8 -*-
-<%namespace name="helper" file="post_helper.tmpl"/>
-<%namespace name="disqus" file="disqus_helper.tmpl"/>
-<%inherit file="base.tmpl"/>
-<%block name="extra_head">
-${helper.twitter_card_information(post)}
-</%block>
-
-<%block name="content">
-%if title:
- <h1>${title}</h1>
-%endif
- ${post.text()}
-%if enable_comments and not post.meta('nocomments'):
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(), post.base_path)}
-%endif
-</%block>
-
-<%block name="sourcelink">
-% if not post.meta('password'):
- <li>
- <a href="${post.meta('slug')+post.source_ext()}" id="sourcelink">${messages("Source")}</a>
- </li>
-% endif
-</%block>
diff --git a/nikola/data/themes/site/engine b/nikola/data/themes/site/engine
deleted file mode 100644
index 2951cdd..0000000
--- a/nikola/data/themes/site/engine
+++ /dev/null
@@ -1 +0,0 @@
-mako
diff --git a/nikola/data/themes/site/parent b/nikola/data/themes/site/parent
deleted file mode 100644
index 4ad96d5..0000000
--- a/nikola/data/themes/site/parent
+++ /dev/null
@@ -1 +0,0 @@
-default
diff --git a/nikola/filters.py b/nikola/filters.py
index a3bff81..04703cb 100644
--- a/nikola/filters.py
+++ b/nikola/filters.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,11 +26,40 @@
"""Utility functions to help you run filters on files."""
+from functools import wraps
import os
import re
import shutil
import subprocess
import tempfile
+import shlex
+
+try:
+ import typogrify.filters as typo
+except ImportError:
+ typo = None
+
+
+def apply_to_file(f):
+ """Takes a function f that transforms a data argument, and returns
+ a function that takes a filename and applies f to the contents,
+ in place."""
+ @wraps(f)
+ def f_in_file(fname):
+ with open(fname, 'rb') as inf:
+ data = inf.read()
+ data = f(data)
+ with open(fname, 'wb+') as outf:
+ outf.write(data)
+
+ return f_in_file
+
+
+def list_replace(the_list, find, replacement):
+ "Replaces all occurrences of ``find`` with ``replacement`` in ``the_list``"
+ for i, v in enumerate(the_list):
+ if v == find:
+ the_list[i] = replacement
def runinplace(command, infile):
@@ -44,19 +75,30 @@ def runinplace(command, infile):
That will replace myfile.css with a minified version.
+ You can also supply command as a list.
"""
- tmpdir = tempfile.mkdtemp()
- tmpfname = os.path.join(tmpdir, os.path.basename(infile))
- command = command.replace('%1', "'%s'" % infile)
+ if not isinstance(command, list):
+ command = shlex.split(command)
+
+ tmpdir = None
+
+ if "%2" in command:
+ tmpdir = tempfile.mkdtemp(prefix="nikola")
+ tmpfname = os.path.join(tmpdir, os.path.basename(infile))
- needs_tmp = "%2" in command
- command = command.replace('%2', "'%s'" % tmpfname)
+ try:
+ list_replace(command, "%1", infile)
+ if tmpdir:
+ list_replace(command, "%2", tmpfname)
- subprocess.check_call(command, shell=True)
+ subprocess.check_call(command)
- if needs_tmp:
- shutil.move(tmpfname, infile)
+ if tmpdir:
+ shutil.move(tmpfname, infile)
+ finally:
+ if tmpdir:
+ shutil.rmtree(tmpdir)
def yui_compressor(infile):
@@ -79,11 +121,10 @@ def tidy(inplace):
return
# Tidy will give error exits, that we will ignore.
- output = subprocess.check_output("tidy -m -w 90 --indent no --quote-marks"
- "no --keep-time yes --tidy-mark no "
- "--force-output yes '{0}'; exit 0".format(
- inplace), stderr=subprocess.STDOUT,
- shell=True)
+ output = subprocess.check_output(
+ "tidy -m -w 90 --indent no --quote-marks"
+ "no --keep-time yes --tidy-mark no "
+ "--force-output yes '{0}'; exit 0".format(inplace), stderr=subprocess.STDOUT, shell=True)
for line in output.split("\n"):
if "Warning:" in line:
@@ -114,3 +155,17 @@ def tidy(inplace):
continue
else:
assert False, line
+
+
+@apply_to_file
+def typogrify(data):
+ global typogrify_filter
+ if typo is None:
+ raise Exception("To use the typogrify filter, you need to install typogrify.")
+ 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
diff --git a/nikola/main.py b/nikola/main.py
index 8263b7e..f00a4d2 100644
--- a/nikola/main.py
+++ b/nikola/main.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,8 +25,11 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function, unicode_literals
-import sys
from operator import attrgetter
+import os
+import shutil
+import sys
+import traceback
from doit.loader import generate_tasks
from doit.cmd_base import TaskLoader
@@ -33,22 +37,41 @@ 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 doit.cmd_clean import Clean as DoitClean
+from logbook import NullHandler
+from . import __version__
from .nikola import Nikola
-from .utils import _reload
+from .utils import _reload, sys_decode, LOGGER, STRICT_HANDLER
+
+
+config = {}
def main(args):
+ quiet = False
+ if len(args) > 0 and args[0] == 'build' and '--strict' in args:
+ LOGGER.notice('Running in strict mode')
+ STRICT_HANDLER.push_application()
+ if len(args) > 0 and args[0] == 'build' and '-q' in args or '--quiet' in args:
+ nullhandler = NullHandler()
+ nullhandler.push_application()
+ quiet = True
+ global config
sys.path.append('')
try:
import conf
_reload(conf)
config = conf.__dict__
- except ImportError:
+ except Exception:
+ if os.path.exists('conf.py'):
+ msg = traceback.format_exc(0).splitlines()[1]
+ LOGGER.error('In conf.py line {0}: {1}'.format(sys.exc_info()[2].tb_lineno, msg))
+ sys.exit(1)
config = {}
site = Nikola(**config)
- return DoitNikola(site).run(args)
+ return DoitNikola(site, quiet).run(args)
class Help(DoitHelp):
@@ -57,48 +80,92 @@ class Help(DoitHelp):
@staticmethod
def print_usage(cmds):
"""print nikola "usage" (basic help) instructions"""
- print("Nikola")
+ print("Nikola is a tool to create static websites and blogs. For full documentation and more information, please visit http://getnikola.com\n\n")
print("Available commands:")
for cmd in sorted(cmds.values(), key=attrgetter('name')):
- print(" nikola %s \t\t %s" % (cmd.name, cmd.doc_purpose))
+ print(" nikola %-*s %s" % (20, cmd.name, cmd.doc_purpose))
print("")
- print(" nikola help show help / reference")
- print(" nikola help <command> show command usage")
- print(" nikola help <task-name> show task usage")
+ print(" nikola help show help / reference")
+ print(" nikola help <command> show command usage")
+ print(" nikola help <task-name> show task usage")
class Build(DoitRun):
"""expose "run" command as "build" for backward compatibility"""
- pass
+ def __init__(self, *args, **kw):
+ opts = list(self.cmd_options)
+ opts.append(
+ {
+ 'name': 'strict',
+ 'long': 'strict',
+ 'default': False,
+ 'type': bool,
+ 'help': "Fail on things that would normally be warnings.",
+ }
+ )
+ opts.append(
+ {
+ 'name': 'quiet',
+ 'long': 'quiet',
+ 'short': 'q',
+ 'default': False,
+ 'type': bool,
+ 'help': "Run quietly.",
+ }
+ )
+ self.cmd_options = tuple(opts)
+ super(Build, self).__init__(*args, **kw)
+
+
+class Clean(DoitClean):
+ """A clean that removes cache/"""
+
+ def clean_tasks(self, tasks, dryrun):
+ 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)
class NikolaTaskLoader(TaskLoader):
"""custom task loader to get tasks from Nikola instead of dodo.py file"""
- def __init__(self, nikola):
+ def __init__(self, nikola, quiet=False):
self.nikola = nikola
+ self.quiet = quiet
def load_tasks(self, cmd, opt_values, pos_args):
- DOIT_CONFIG = {
- 'reporter': ExecutedOnlyReporter,
- 'default_tasks': ['render_site'],
- }
- tasks = generate_tasks('render_site', self.nikola.gen_tasks())
- return tasks, DOIT_CONFIG
+ if self.quiet:
+ DOIT_CONFIG = {
+ 'verbosity': 0,
+ 'reporter': 'zero',
+ }
+ else:
+ DOIT_CONFIG = {
+ 'reporter': ExecutedOnlyReporter,
+ }
+ DOIT_CONFIG['default_tasks'] = ['render_site', 'post_render']
+ tasks = generate_tasks(
+ 'render_site',
+ self.nikola.gen_tasks('render_site', "Task", 'Group of tasks to render the site.'))
+ latetasks = generate_tasks(
+ 'post_render',
+ self.nikola.gen_tasks('post_render', "LateTask", 'Group of tasks to be executes after site is rendered.'))
+ return tasks + latetasks, DOIT_CONFIG
class DoitNikola(DoitMain):
# overwite help command
- DOIT_CMDS = list(DoitMain.DOIT_CMDS) + [Help, Build]
+ DOIT_CMDS = list(DoitMain.DOIT_CMDS) + [Help, Build, Clean]
TASK_LOADER = NikolaTaskLoader
- def __init__(self, nikola):
+ def __init__(self, nikola, quiet=False):
self.nikola = nikola
- self.task_loader = self.TASK_LOADER(nikola)
+ self.task_loader = self.TASK_LOADER(nikola, quiet)
def get_commands(self):
# core doit commands
cmds = DoitMain.get_commands(self)
-
# load nikola commands
for name, cmd in self.nikola.commands.items():
cmds[name] = cmd
@@ -107,16 +174,23 @@ class DoitNikola(DoitMain):
def run(self, cmd_args):
sub_cmds = self.get_commands()
args = self.process_args(cmd_args)
+ args = [sys_decode(arg) for arg in args]
if len(args) == 0 or any(arg in ["--help", '-h'] for arg in args):
cmd_args = ['help']
args = ['help']
+ # Hide run because Nikola uses build
+ sub_cmds.pop('run')
if len(args) == 0 or args[0] not in sub_cmds.keys() or \
- args[0] in ('run', 'build'):
+ args[0] == 'build':
# Check for conf.py before launching run
if not self.nikola.configured:
- print("This command needs to run inside an "
- "existing Nikola site.")
+ LOGGER.error("This command needs to run inside an "
+ "existing Nikola site.")
return False
- super(DoitNikola, self).run(cmd_args)
+ return super(DoitNikola, self).run(cmd_args)
+
+ @staticmethod
+ def print_version():
+ print("Nikola version " + __version__)
diff --git a/nikola/nikola.py b/nikola/nikola.py
index 8660a0f..13c91a7 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,7 +28,6 @@ from __future__ import print_function, unicode_literals
from collections import defaultdict
from copy import copy
import glob
-import gzip
import locale
import os
import sys
@@ -35,29 +35,36 @@ try:
from urlparse import urlparse, urlsplit, urljoin
except ImportError:
from urllib.parse import urlparse, urlsplit, urljoin # NOQA
-import warnings
-import lxml.html
-from yapsy.PluginManager import PluginManager
-import pytz
+from blinker import signal
+try:
+ import pyphen
+except ImportError:
+ pyphen = None
-if os.getenv('DEBUG'):
- import logging
+import logging
+if os.getenv('NIKOLA_DEBUG'):
logging.basicConfig(level=logging.DEBUG)
else:
- import logging
logging.basicConfig(level=logging.ERROR)
+import lxml.html
+from yapsy.PluginManager import PluginManager
+
from .post import Post
from . import utils
from .plugin_categories import (
Command,
LateTask,
PageCompiler,
+ RestExtension,
Task,
+ TaskMultiplier,
TemplateSystem,
+ SignalHandler,
)
+
config_changed = utils.config_changed
__all__ = ['Nikola']
@@ -79,13 +86,26 @@ class Nikola(object):
def __init__(self, **config):
"""Setup proper environment for running tasks."""
+ # Register our own path handlers
+ self.path_handlers = {
+ 'slug': self.slug_path,
+ 'post_path': self.post_path,
+ }
+
+ self.strict = False
self.global_data = {}
+ self.posts = []
self.posts_per_year = defaultdict(list)
self.posts_per_month = defaultdict(list)
self.posts_per_tag = defaultdict(list)
+ self.posts_per_category = defaultdict(list)
+ self.post_per_file = {}
self.timeline = []
self.pages = []
self._scanned = False
+ self._template_system = None
+ self._THEMES = None
+ self.loghandlers = []
if not config:
self.configured = False
else:
@@ -94,99 +114,217 @@ class Nikola(object):
# This is the default config
self.config = {
'ADD_THIS_BUTTONS': True,
- 'ANALYTICS': '',
+ 'ANNOTATIONS': False,
'ARCHIVE_PATH': "",
'ARCHIVE_FILENAME': "archive.html",
+ 'BLOG_TITLE': 'Default Title',
+ 'BLOG_DESCRIPTION': 'Default Description',
+ 'BODY_END': "",
'CACHE_FOLDER': 'cache',
'CODE_COLOR_SCHEME': 'default',
+ 'COMMENT_SYSTEM': 'disqus',
'COMMENTS_IN_GALLERIES': False,
'COMMENTS_IN_STORIES': False,
+ 'COMPILERS': {
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown'),
+ "textile": ('.textile',),
+ "txt2tags": ('.t2t',),
+ "bbcode": ('.bb',),
+ "wiki": ('.wiki',),
+ "ipynb": ('.ipynb',),
+ "html": ('.html', '.htm')
+ },
'CONTENT_FOOTER': '',
+ 'COPY_SOURCES': True,
'CREATE_MONTHLY_ARCHIVE': False,
+ 'CREATE_SINGLE_ARCHIVE': False,
'DATE_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': [],
'DISABLED_PLUGINS': (),
- 'DISQUS_FORUM': 'nikolademo',
+ 'COMMENT_SYSTEM_ID': 'nikolademo',
'ENABLED_EXTRAS': (),
'EXTRA_HEAD_DATA': '',
'FAVICONS': {},
+ 'FEED_LENGTH': 10,
'FILE_METADATA_REGEXP': None,
+ 'ADDITIONAL_METADATA': {},
'FILES_FOLDERS': {'files': ''},
'FILTERS': {},
'GALLERY_PATH': 'galleries',
+ 'GALLERY_SORT_BY_DATE': True,
+ 'GZIP_COMMAND': None,
'GZIP_FILES': False,
- 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json'),
+ 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json', '.xml'),
+ 'HIDE_SOURCELINK': False,
'HIDE_UNTRANSLATED_POSTS': False,
+ 'HYPHENATE': False,
'INDEX_DISPLAY_POST_COUNT': 10,
+ 'INDEX_FILE': 'index.html',
'INDEX_TEASERS': False,
'INDEXES_TITLE': "",
'INDEXES_PAGES': "",
'INDEX_PATH': '',
+ 'IPYNB_CONFIG': {},
'LICENSE': '',
+ 'LINK_CHECK_WHITELIST': [],
'LISTINGS_FOLDER': 'listings',
+ 'NAVIGATION_LINKS': None,
+ 'MARKDOWN_EXTENSIONS': ['fenced_code', 'codehilite'],
'MAX_IMAGE_SIZE': 1280,
'MATHJAX_CONFIG': '',
'OLD_THEME_SUPPORT': True,
'OUTPUT_FOLDER': 'output',
- 'post_compilers': {
- "rest": ('.txt', '.rst'),
- "markdown": ('.md', '.mdown', '.markdown'),
- "textile": ('.textile',),
- "txt2tags": ('.t2t',),
- "bbcode": ('.bb',),
- "wiki": ('.wiki',),
- "ipynb": ('.ipynb',),
- "html": ('.html', '.htm')
- },
- 'POST_PAGES': (
- ("posts/*.txt", "posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "story.tmpl", False),
- ),
+ 'POSTS': (("posts/*.txt", "posts", "post.tmpl"),),
+ 'PAGES': (("stories/*.txt", "stories", "story.tmpl"),),
+ 'PRETTY_URLS': False,
+ 'FUTURE_IS_NOW': False,
+ 'READ_MORE_LINK': '<p class="more"><a href="{link}">{read_more}…</a></p>',
'REDIRECTIONS': [],
'RSS_LINK': None,
'RSS_PATH': '',
'RSS_TEASERS': True,
'SEARCH_FORM': '',
'SLUG_TAG_PATH': True,
+ 'SOCIAL_BUTTONS_CODE': SOCIAL_BUTTONS_CODE,
+ 'SITE_URL': 'http://getnikola.com/',
'STORY_INDEX': False,
- 'STRIP_INDEX_HTML': False,
+ 'STRIP_INDEXES': False,
+ 'SITEMAP_INCLUDE_FILELESS_DIRS': True,
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
- 'THEME': 'site',
- 'THEME_REVEAL_CONGIF_SUBTHEME': 'sky',
- 'THEME_REVEAL_CONGIF_TRANSITION': 'cube',
+ 'THEME': 'bootstrap',
+ 'THEME_REVEAL_CONFIG_SUBTHEME': 'sky',
+ 'THEME_REVEAL_CONFIG_TRANSITION': 'cube',
'THUMBNAIL_SIZE': 180,
'USE_BUNDLES': True,
'USE_CDN': False,
'USE_FILENAME_AS_TITLE': True,
'TIMEZONE': None,
+ 'DEPLOY_DRAFTS': True,
+ 'DEPLOY_FUTURE': False,
+ 'SCHEDULE_ALL': False,
+ 'SCHEDULE_RULE': '',
+ 'SCHEDULE_FORCE_TODAY': False,
+ 'LOGGING_HANDLERS': {'stderr': {'loglevel': 'WARNING', 'bubble': True}},
}
self.config.update(config)
- self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
- {self.config['DEFAULT_'
- 'LANG']: ''})
- self.THEMES = utils.get_theme_chain(self.config['THEME'])
+ # 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.')
+ self.config['HYPHENATE'] = False
+
+ # Deprecating post_compilers
+ # TODO: remove on v7
+ if 'post_compilers' in config:
+ utils.LOGGER.warn('The post_compilers option is deprecated, use COMPILERS instead.')
+ if 'COMPILERS' in config:
+ utils.LOGGER.warn('COMPILERS conflicts with post_compilers, ignoring post_compilers.')
+ else:
+ self.config['COMPILERS'] = config['post_compilers']
+
+ # Deprecating post_pages
+ # TODO: remove on v7
+ if 'post_pages' in config:
+ utils.LOGGER.warn('The post_pages option is deprecated, use POSTS and PAGES instead.')
+ if 'POSTS' in config or 'PAGES' in config:
+ utils.LOGGER.warn('POSTS and PAGES conflict with post_pages, ignoring post_pages.')
+ else:
+ self.config['POSTS'] = [item[:3] for item in config['post_pages'] if item[-1]]
+ self.config['PAGES'] = [item[:3] for item in config['post_pages'] if not item[-1]]
+ # FIXME: Internally, we still use post_pages because it's a pain to change it
+ self.config['post_pages'] = []
+ for i1, i2, i3 in self.config['POSTS']:
+ self.config['post_pages'].append([i1, i2, i3, True])
+ for i1, i2, i3 in self.config['PAGES']:
+ self.config['post_pages'].append([i1, i2, i3, False])
+
+ # Deprecating DISQUS_FORUM
+ # TODO: remove on v7
+ if 'DISQUS_FORUM' in config:
+ utils.LOGGER.warn('The DISQUS_FORUM option is deprecated, use COMMENT_SYSTEM_ID instead.')
+ if 'COMMENT_SYSTEM_ID' in config:
+ utils.LOGGER.warn('DISQUS_FORUM conflicts with COMMENT_SYSTEM_ID, ignoring DISQUS_FORUM.')
+ else:
+ self.config['COMMENT_SYSTEM_ID'] = config['DISQUS_FORUM']
+
+ # Deprecating the ANALYTICS option
+ # TODO: remove on v7
+ if 'ANALYTICS' in config:
+ utils.LOGGER.warn('The ANALYTICS option is deprecated, use BODY_END instead.')
+ if 'BODY_END' in config:
+ utils.LOGGER.warn('ANALYTICS conflicts with BODY_END, ignoring ANALYTICS.')
+ else:
+ self.config['BODY_END'] = config['ANALYTICS']
+
+ # Deprecating the SIDEBAR_LINKS option
+ # TODO: remove on v7
+ if 'SIDEBAR_LINKS' in config:
+ utils.LOGGER.warn('The SIDEBAR_LINKS option is deprecated, use NAVIGATION_LINKS instead.')
+ if 'NAVIGATION_LINKS' in config:
+ utils.LOGGER.warn('The SIDEBAR_LINKS conflicts with NAVIGATION_LINKS, ignoring SIDEBAR_LINKS.')
+ else:
+ self.config['NAVIGATION_LINKS'] = config['SIDEBAR_LINKS']
+ # Compatibility alias
+ self.config['SIDEBAR_LINKS'] = self.config['NAVIGATION_LINKS']
+
+ if self.config['NAVIGATION_LINKS'] in (None, {}):
+ self.config['NAVIGATION_LINKS'] = {self.config['DEFAULT_LANG']: ()}
+
+ # Deprecating the ADD_THIS_BUTTONS option
+ # TODO: remove on v7
+ if 'ADD_THIS_BUTTONS' in config:
+ utils.LOGGER.warn('The ADD_THIS_BUTTONS option is deprecated, use SOCIAL_BUTTONS_CODE instead.')
+ if not config['ADD_THIS_BUTTONS']:
+ utils.LOGGER.warn('Setting SOCIAL_BUTTONS_CODE to empty because ADD_THIS_BUTTONS is False.')
+ self.config['SOCIAL_BUTTONS_CODE'] = ''
+
+ # STRIP_INDEX_HTML config has been replaces with STRIP_INDEXES
+ # Port it if only the oldef form is there
+ # TODO: remove on v7
+ if 'STRIP_INDEX_HTML' in config and 'STRIP_INDEXES' not in config:
+ utils.LOGGER.warn('You should configure STRIP_INDEXES instead of STRIP_INDEX_HTML')
+ self.config['STRIP_INDEXES'] = config['STRIP_INDEX_HTML']
+
+ # PRETTY_URLS defaults to enabling STRIP_INDEXES unless explicitly disabled
+ if config.get('PRETTY_URLS', False) and 'STRIP_INDEXES' not in config:
+ self.config['STRIP_INDEXES'] = True
+
+ if config.get('COPY_SOURCES') and not self.config['HIDE_SOURCELINK']:
+ self.config['HIDE_SOURCELINK'] = True
- self.MESSAGES = utils.load_messages(self.THEMES,
- self.config['TRANSLATIONS'],
- self.config['DEFAULT_LANG'])
+ self.config['TRANSLATIONS'] = self.config.get('TRANSLATIONS',
+ {self.config['DEFAULT_LANG']: ''})
# SITE_URL is required, but if the deprecated BLOG_URL
# is available, use it and warn
+ # TODO: remove on v7
if 'SITE_URL' not in self.config:
if 'BLOG_URL' in self.config:
- print("WARNING: You should configure SITE_URL instead of BLOG_URL")
+ utils.LOGGER.warn('You should configure SITE_URL instead of BLOG_URL')
self.config['SITE_URL'] = self.config['BLOG_URL']
self.default_lang = self.config['DEFAULT_LANG']
self.translations = self.config['TRANSLATIONS']
+ locale_fallback, locale_default, locales = sanitized_locales(
+ self.config.get('LOCALE_FALLBACK', None),
+ self.config.get('LOCALE_DEFAULT', None),
+ self.config.get('LOCALES', {}),
+ self.translations) # NOQA
+ utils.LocaleBorg.initialize(locales, self.default_lang)
+
# 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.")
self.plugin_manager = PluginManager(categories_filter={
"Command": Command,
@@ -194,15 +332,35 @@ class Nikola(object):
"LateTask": LateTask,
"TemplateSystem": TemplateSystem,
"PageCompiler": PageCompiler,
+ "TaskMultiplier": TaskMultiplier,
+ "RestExtension": RestExtension,
+ "SignalHandler": SignalHandler,
})
self.plugin_manager.setPluginInfoExtension('plugin')
- self.plugin_manager.setPluginPlaces([
- str(os.path.join(os.path.dirname(__file__), 'plugins')),
- str(os.path.join(os.getcwd(), 'plugins')),
- ])
-
+ if sys.version_info[0] == 3:
+ places = [
+ os.path.join(os.path.dirname(__file__), 'plugins'),
+ os.path.join(os.getcwd(), 'plugins'),
+ ]
+ else:
+ places = [
+ os.path.join(os.path.dirname(__file__), utils.sys_encode('plugins')),
+ os.path.join(os.getcwd(), utils.sys_encode('plugins')),
+ ]
+ self.plugin_manager.setPluginPlaces(places)
self.plugin_manager.collectPlugins()
+ # Activate all required SignalHandler plugins
+ for plugin_info in self.plugin_manager.getPluginsOfCategory("SignalHandler"):
+ if plugin_info.name in self.config.get('DISABLED_PLUGINS'):
+ self.plugin_manager.removePluginFromCategory(plugin_info, "SignalHandler")
+ else:
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
+
+ # Emit signal for SignalHandlers which need to start running immediately.
+ signal('sighandlers_loaded').send(self)
+
self.commands = {}
# Activate all command plugins
for plugin_info in self.plugin_manager.getPluginsOfCategory("Command"):
@@ -228,86 +386,81 @@ class Nikola(object):
self.plugin_manager.activatePluginByName(plugin_info.name)
plugin_info.plugin_object.set_site(self)
- # set global_context for template rendering
- self.GLOBAL_CONTEXT = {
- }
+ # Activate all multiplier plugins
+ for plugin_info in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"):
+ if (plugin_info.name in self.config['DISABLED_PLUGINS']
+ or (plugin_info.name in self.EXTRA_PLUGINS and
+ plugin_info.name not in self.config['ENABLED_EXTRAS'])):
+ self.plugin_manager.removePluginFromCategory(plugin_info, task_type)
+ continue
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
+
+ # Activate all required compiler plugins
+ for plugin_info in self.plugin_manager.getPluginsOfCategory("PageCompiler"):
+ if plugin_info.name in self.config["COMPILERS"].keys():
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
- self.GLOBAL_CONTEXT['messages'] = self.MESSAGES
- self.GLOBAL_CONTEXT['_link'] = self.link
- self.GLOBAL_CONTEXT['set_locale'] = s_l
- 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_TAG_PATH'] = self.config[
- 'SLUG_TAG_PATH']
-
- self.GLOBAL_CONTEXT['add_this_buttons'] = self.config[
- 'ADD_THIS_BUTTONS']
- self.GLOBAL_CONTEXT['index_display_post_count'] = self.config[
+ # set global_context for template rendering
+ self._GLOBAL_CONTEXT = {}
+
+ self._GLOBAL_CONTEXT['_link'] = self.link
+ self._GLOBAL_CONTEXT['set_locale'] = utils.LocaleBorg().set_locale
+ 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_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['use_bundles'] = self.config['USE_BUNDLES']
- self.GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
- self.GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
- self.GLOBAL_CONTEXT['date_format'] = self.config.get(
+ self._GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES']
+ self._GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN")
+ self._GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS']
+ self._GLOBAL_CONTEXT['date_format'] = self.config.get(
'DATE_FORMAT', '%Y-%m-%d %H:%M')
- self.GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
- self.GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE')
-
- self.GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL'))
- self.GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
- self.GLOBAL_CONTEXT['analytics'] = self.config.get('ANALYTICS')
- self.GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS')
- self.GLOBAL_CONTEXT['license'] = self.config.get('LICENSE')
- self.GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM')
- self.GLOBAL_CONTEXT['disqus_forum'] = self.config.get('DISQUS_FORUM')
- self.GLOBAL_CONTEXT['mathjax_config'] = self.config.get(
+ self._GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR')
+ self._GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE')
+
+ # TODO: remove fallback in v7
+ self._GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL'))
+ self._GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION')
+ self._GLOBAL_CONTEXT['body_end'] = self.config.get('BODY_END')
+ # TODO: remove in v7
+ self._GLOBAL_CONTEXT['analytics'] = self.config.get('BODY_END')
+ # TODO: remove in v7
+ self._GLOBAL_CONTEXT['add_this_buttons'] = self.config.get('SOCIAL_BUTTONS_CODE')
+ self._GLOBAL_CONTEXT['social_buttons_code'] = self.config.get('SOCIAL_BUTTONS_CODE')
+ self._GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS')
+ self._GLOBAL_CONTEXT['license'] = self.config.get('LICENSE')
+ self._GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM')
+ self._GLOBAL_CONTEXT['comment_system'] = self.config.get('COMMENT_SYSTEM')
+ self._GLOBAL_CONTEXT['comment_system_id'] = self.config.get('COMMENT_SYSTEM_ID')
+ # TODO: remove in v7
+ self._GLOBAL_CONTEXT['disqus_forum'] = self.config.get('COMMENT_SYSTEM_ID')
+ self._GLOBAL_CONTEXT['mathjax_config'] = self.config.get(
'MATHJAX_CONFIG')
- self.GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONGIF_SUBTHEME')
- self.GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONGIF_TRANSITION')
- self.GLOBAL_CONTEXT['content_footer'] = self.config.get(
+ 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['rss_path'] = self.config.get('RSS_PATH')
- self.GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
+ self._GLOBAL_CONTEXT['rss_path'] = self.config.get('RSS_PATH')
+ self._GLOBAL_CONTEXT['rss_link'] = self.config.get('RSS_LINK')
- self.GLOBAL_CONTEXT['sidebar_links'] = utils.Functionary(list, self.config['DEFAULT_LANG'])
- for k, v in self.config.get('SIDEBAR_LINKS', {}).items():
- self.GLOBAL_CONTEXT['sidebar_links'][k] = v
+ self._GLOBAL_CONTEXT['navigation_links'] = utils.Functionary(list, self.config['DEFAULT_LANG'])
+ for k, v in self.config.get('NAVIGATION_LINKS', {}).items():
+ self._GLOBAL_CONTEXT['navigation_links'][k] = v
+ # TODO: remove on v7
+ # Compatibility alias
+ self._GLOBAL_CONTEXT['sidebar_links'] = self._GLOBAL_CONTEXT['navigation_links']
- self.GLOBAL_CONTEXT['twitter_card'] = self.config.get(
+ self._GLOBAL_CONTEXT['twitter_card'] = self.config.get(
'TWITTER_CARD', {})
- self.GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
+ self._GLOBAL_CONTEXT['hide_sourcelink'] = self.config.get(
+ 'HIDE_SOURCELINK')
+ self._GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
- self.GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
-
- # check if custom css exist and is not empty
- for files_path in list(self.config['FILES_FOLDERS'].keys()):
- custom_css_path = os.path.join(files_path, 'assets/css/custom.css')
- if self.file_exists(custom_css_path, not_empty=True):
- self.GLOBAL_CONTEXT['has_custom_css'] = True
- break
- else:
- self.GLOBAL_CONTEXT['has_custom_css'] = False
-
- # Load template plugin
- template_sys_name = utils.get_template_engine(self.THEMES)
- pi = self.plugin_manager.getPluginByName(
- template_sys_name, "TemplateSystem")
- if pi is None:
- sys.stderr.write("Error loading {0} template system "
- "plugin\n".format(template_sys_name))
- sys.exit(1)
- self.template_system = pi.plugin_object
- lookup_dirs = [os.path.join(utils.get_theme_path(name), "templates")
- for name in self.THEMES]
- self.template_system.set_directories(lookup_dirs,
- self.config['CACHE_FOLDER'])
-
- # Check consistency of USE_CDN and the current THEME (Issue #386)
- if self.config['USE_CDN']:
- bootstrap_path = utils.get_asset_path(os.path.join(
- 'assets', 'css', 'bootstrap.min.css'), self.THEMES)
- if bootstrap_path.split(os.sep)[-4] != 'site':
- warnings.warn('The USE_CDN option may be incompatible with your theme, because it uses a hosted version of bootstrap.')
+ self._GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
# Load compiler plugins
self.compilers = {}
@@ -316,11 +469,91 @@ class Nikola(object):
for plugin_info in self.plugin_manager.getPluginsOfCategory(
"PageCompiler"):
self.compilers[plugin_info.name] = \
- plugin_info.plugin_object.compile_html
+ plugin_info.plugin_object
+ signal('configured').send(self)
+
+ def _get_themes(self):
+ if self._THEMES is None:
+ # Check for old theme names (Issue #650) TODO: remove in v7
+ theme_replacements = {
+ 'site': 'bootstrap',
+ 'orphan': 'base',
+ 'default': 'oldfashioned',
+ }
+ if self.config['THEME'] in theme_replacements:
+ utils.LOGGER.warn('You are using the old theme "{0}", using "{1}" instead.'.format(
+ self.config['THEME'], theme_replacements[self.config['THEME']]))
+ self.config['THEME'] = theme_replacements[self.config['THEME']]
+ if self.config['THEME'] == 'oldfashioned':
+ utils.LOGGER.warn('''You may need to install the "oldfashioned" theme '''
+ '''from themes.nikola.ralsina.com.ar because it's not '''
+ '''shipped by default anymore.''')
+ utils.LOGGER.warn('Please change your THEME setting.')
+ try:
+ self._THEMES = utils.get_theme_chain(self.config['THEME'])
+ except Exception:
+ utils.LOGGER.warn('''Can't load theme "{0}", using 'bootstrap' instead.'''.format(self.config['THEME']))
+ self.config['THEME'] = 'bootstrap'
+ return self._get_themes()
+ # Check consistency of USE_CDN and the current THEME (Issue #386)
+ if self.config['USE_CDN']:
+ 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.')
+
+ return self._THEMES
+
+ THEMES = property(_get_themes)
+
+ def _get_messages(self):
+ return utils.load_messages(self.THEMES,
+ self.translations,
+ self.default_lang)
+
+ MESSAGES = property(_get_messages)
+
+ def _get_global_context(self):
+ """Initialize some parts of GLOBAL_CONTEXT only when it's queried."""
+ if 'messages' not in self._GLOBAL_CONTEXT:
+ self._GLOBAL_CONTEXT['messages'] = self.MESSAGES
+ if 'has_custom_css' not in self._GLOBAL_CONTEXT:
+ # check if custom css exist and is not empty
+ custom_css_path = utils.get_asset_path(
+ 'assets/css/custom.css',
+ self.THEMES,
+ self.config['FILES_FOLDERS']
+ )
+ if custom_css_path and self.file_exists(custom_css_path, not_empty=True):
+ self._GLOBAL_CONTEXT['has_custom_css'] = True
+ else:
+ self._GLOBAL_CONTEXT['has_custom_css'] = False
+
+ return self._GLOBAL_CONTEXT
+
+ GLOBAL_CONTEXT = property(_get_global_context)
+
+ def _get_template_system(self):
+ if self._template_system is None:
+ # Load template plugin
+ template_sys_name = utils.get_template_engine(self.THEMES)
+ pi = self.plugin_manager.getPluginByName(
+ template_sys_name, "TemplateSystem")
+ if pi is None:
+ sys.stderr.write("Error loading {0} template system "
+ "plugin\n".format(template_sys_name))
+ sys.exit(1)
+ self._template_system = pi.plugin_object
+ lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name), "templates")
+ for name in self.THEMES]
+ self._template_system.set_directories(lookup_dirs,
+ self.config['CACHE_FOLDER'])
+ return self._template_system
+
+ template_system = property(_get_template_system)
def get_compiler(self, source_name):
- """Get the correct compiler for a post from `conf.post_compilers`
-
+ """Get the correct compiler for a post from `conf.COMPILERS`
To make things easier for users, the mapping in conf.py is
compiler->[extensions], although this is less convenient for us. The
majority of this function is reversing that dictionary and error
@@ -332,18 +565,18 @@ class Nikola(object):
except KeyError:
# Find the correct compiler for this files extension
langs = [lang for lang, exts in
- list(self.config['post_compilers'].items())
+ list(self.config['COMPILERS'].items())
if ext in exts]
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 'post_compilers' in conf.py\n(The error is in"
+ "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("post_compilers in conf.py does not tell me how to "
+ exit("COMPILERS in conf.py does not tell me how to "
"handle '{0}' extensions.".format(ext))
lang = langs[0]
@@ -416,43 +649,34 @@ class Nikola(object):
return result
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ utils.makedirs(os.path.dirname(output_name))
doc = lxml.html.document_fromstring(data)
doc.rewrite_links(replacer)
data = b'<!DOCTYPE html>' + lxml.html.tostring(doc, encoding='utf8')
with open(output_name, "wb+") as post_file:
post_file.write(data)
- def current_lang(self): # FIXME: this is duplicated, turn into a mixin
- """Return the currently set locale, if it's one of the
- available translations, or default_lang."""
- lang = utils.LocaleBorg().current_lang
- if lang:
- if lang in self.translations:
- return lang
- lang = lang.split('_')[0]
- if lang in self.translations:
- return lang
- # whatever
- return self.default_lang
-
def path(self, kind, name, lang=None, is_link=False):
"""Build the path to a certain kind of page.
- kind is one of:
+ These are mostly defined by plugins by registering via
+ the register_path_handler method, except for slug and
+ post_path which are defined in this class' init method.
+
+ Here's some of the others, for historical reasons:
* tag_index (name is ignored)
* tag (and name is the tag name)
* tag_rss (name is the tag name)
+ * category (and name is the category name)
+ * category_rss (and name is the category name)
* archive (and name is the year, or None for the main archive index)
* index (name is the number in index-number)
* rss (name is ignored)
* gallery (name is the gallery name)
* listing (name is the source code file name)
- * post_path (name is 1st element in a post_pages tuple)
+ * post_path (name is 1st element in a POSTS/PAGES tuple)
+ * slug (name is the slug of a post or story)
The returned value is always a path relative to output, like
"categories/whatever.html"
@@ -465,64 +689,43 @@ class Nikola(object):
"""
if lang is None:
- lang = self.current_lang()
-
- path = []
-
- if kind == "tag_index":
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['TAG_PATH'], 'index.html'] if _f]
- elif kind == "tag":
- if self.config['SLUG_TAG_PATH']:
- name = utils.slugify(name)
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['TAG_PATH'], name + ".html"] if
- _f]
- elif kind == "tag_rss":
- if self.config['SLUG_TAG_PATH']:
- name = utils.slugify(name)
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['TAG_PATH'], name + ".xml"] if
- _f]
- elif kind == "index":
- if name not in [None, 0]:
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['INDEX_PATH'],
- 'index-{0}.html'.format(name)] if _f]
- else:
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['INDEX_PATH'], 'index.html']
- if _f]
- elif kind == "post_path":
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- os.path.dirname(name), "index.html"] if _f]
- elif kind == "rss":
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['RSS_PATH'], 'rss.xml'] if _f]
- elif kind == "archive":
- if name:
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['ARCHIVE_PATH'], name,
- 'index.html'] if _f]
- else:
- path = [_f for _f in [self.config['TRANSLATIONS'][lang],
- self.config['ARCHIVE_PATH'],
- self.config['ARCHIVE_FILENAME']] if _f]
- elif kind == "gallery":
- path = [_f for _f in [self.config['GALLERY_PATH'], name,
- 'index.html'] if _f]
- elif kind == "listing":
- path = [_f for _f in [self.config['LISTINGS_FOLDER'], name +
- '.html'] if _f]
+ lang = utils.LocaleBorg().current_lang
+
+ path = self.path_handlers[kind](name, lang)
+
if is_link:
link = '/' + ('/'.join(path))
- if self.config['STRIP_INDEX_HTML'] and link.endswith('/index.html'):
- return link[:-10]
+ 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):
+ """post_path path handler"""
+ return [_f for _f in [self.config['TRANSLATIONS'][lang],
+ os.path.dirname(name),
+ self.config['INDEX_FILE']] if _f]
+
+ def slug_path(self, name, lang):
+ """slug path handler"""
+ results = [p for p in self.timeline if p.meta('slug') == name]
+ if not results:
+ utils.LOGGER.warning("Can't resolve path request for slug: {0}".format(name))
+ 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]
+
+ def register_path_handler(self, kind, f):
+ if kind in self.path_handlers:
+ utils.LOGGER.warning('Conflicting path handlers for kind: {0}'.format(kind))
+ else:
+ self.path_handlers[kind] = f
+
def link(self, *args):
return self.path(*args, is_link=True)
@@ -564,12 +767,14 @@ class Nikola(object):
exists = os.stat(path).st_size > 0
return exists
- def gen_tasks(self):
+ def clean_task_paths(self, task):
+ """Normalize target paths in the task."""
+ targets = task.get('targets', None)
+ if targets is not None:
+ task['targets'] = [os.path.normpath(t) for t in targets]
+ return task
- def create_gzipped_copy(in_path, out_path):
- with gzip.GzipFile(out_path, 'wb+') as outf:
- with open(in_path, 'rb') as inf:
- outf.write(inf.read())
+ def gen_tasks(self, name, plugin_category, doc=''):
def flatten(task):
if isinstance(task, dict):
@@ -579,57 +784,24 @@ class Nikola(object):
for ft in flatten(t):
yield ft
- def add_gzipped_copies(task):
- if not self.config['GZIP_FILES']:
- return None
- if task.get('name') is None:
- return None
- gzip_task = {
- 'file_dep': [],
- 'targets': [],
- 'actions': [],
- 'basename': 'gzip',
- 'name': task.get('name') + '.gz',
- 'clean': True,
- }
- targets = task.get('targets', [])
- flag = False
- for target in targets:
- ext = os.path.splitext(target)[1]
- if (ext.lower() in self.config['GZIP_EXTENSIONS'] and
- target.startswith(self.config['OUTPUT_FOLDER'])):
- flag = True
- gzipped = target + '.gz'
- gzip_task['file_dep'].append(target)
- gzip_task['targets'].append(gzipped)
- gzip_task['actions'].append((create_gzipped_copy, (target, gzipped)))
- if not flag:
- return None
- return gzip_task
-
- if self.config['GZIP_FILES']:
- task_dep = ['gzip']
- else:
- task_dep = []
- for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"):
+ task_dep = []
+ for pluginInfo in self.plugin_manager.getPluginsOfCategory(plugin_category):
for task in flatten(pluginInfo.plugin_object.gen_tasks()):
- gztask = add_gzipped_copies(task)
- if gztask:
- yield gztask
- yield task
- if pluginInfo.plugin_object.is_default:
- task_dep.append(pluginInfo.plugin_object.name)
-
- for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"):
- for task in pluginInfo.plugin_object.gen_tasks():
- gztask = add_gzipped_copies(task)
- if gztask:
- yield gztask
+ assert 'basename' in task
+ task = self.clean_task_paths(task)
yield task
+ for multi in self.plugin_manager.getPluginsOfCategory("TaskMultiplier"):
+ flag = False
+ for task in multi.plugin_object.process(task, name):
+ flag = True
+ yield self.clean_task_paths(task)
+ if flag:
+ task_dep.append('{0}_{1}'.format(name, multi.plugin_object.name))
if pluginInfo.plugin_object.is_default:
task_dep.append(pluginInfo.plugin_object.name)
yield {
- 'name': b'all',
+ 'basename': name,
+ 'doc': doc,
'actions': None,
'clean': True,
'task_dep': task_dep
@@ -639,15 +811,12 @@ class Nikola(object):
"""Scan all the posts."""
if self._scanned:
return
-
- print("Scanning posts", end='')
- tzinfo = None
- if self.config['TIMEZONE'] is not None:
- tzinfo = pytz.timezone(self.config['TIMEZONE'])
- targets = set([])
+ seen = set([])
+ print("Scanning posts", end='', file=sys.stderr)
+ lower_case_tags = set([])
for wildcard, destination, template_name, use_in_feeds in \
self.config['post_pages']:
- print(".", end='')
+ print(".", end='', file=sys.stderr)
dirname = os.path.dirname(wildcard)
for dirpath, _, _ in os.walk(dirname):
dir_glob = os.path.join(dirpath, os.path.basename(wildcard))
@@ -663,45 +832,52 @@ class Nikola(object):
if orig_name in full_list:
continue
full_list.append(orig_name)
+ # We eliminate from the list the files inside any .ipynb folder
+ full_list = [p for p in full_list
+ if not any([x.startswith('.')
+ for x in p.split(os.sep)])]
for base_path in full_list:
+ if base_path in seen:
+ continue
+ else:
+ seen.add(base_path)
post = Post(
base_path,
- self.config['CACHE_FOLDER'],
+ self.config,
dest_dir,
use_in_feeds,
- self.config['TRANSLATIONS'],
- self.config['DEFAULT_LANG'],
- self.config['BASE_URL'],
self.MESSAGES,
template_name,
- self.config['FILE_METADATA_REGEXP'],
- self.config['STRIP_INDEX_HTML'],
- tzinfo,
- self.config['HIDE_UNTRANSLATED_POSTS'],
+ self.get_compiler(base_path).compile_html
)
- for lang, langpath in list(
- self.config['TRANSLATIONS'].items()):
- dest = (destination, langpath, dir_glob,
- post.meta[lang]['slug'])
- if dest in targets:
- raise Exception('Duplicated output path {0!r} '
- 'in post {1!r}'.format(
- post.meta[lang]['slug'],
- base_path))
- targets.add(dest)
- self.global_data[post.post_name] = post
+ self.global_data[post.source_path] = post
if post.use_in_feeds:
+ self.posts.append(post.source_path)
self.posts_per_year[
- str(post.date.year)].append(post.post_name)
+ str(post.date.year)].append(post.source_path)
self.posts_per_month[
- '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.post_name)
+ '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.source_path)
for tag in post.alltags:
- self.posts_per_tag[tag].append(post.post_name)
+ if tag.lower() in lower_case_tags:
+ if tag not in self.posts_per_tag:
+ # Tags that differ only in case
+ other_tag = [k for k in self.posts_per_tag.keys() if k.lower() == tag.lower()][0]
+ utils.LOGGER.error('You have cases that differ only in upper/lower case: {0} and {1}'.format(tag, other_tag))
+ utils.LOGGER.error('Tag {0} is used in: {1}'.format(tag, post.source_path))
+ utils.LOGGER.error('Tag {0} is used in: {1}'.format(other_tag, ', '.join(self.posts_per_tag[other_tag])))
+ sys.exit(1)
+ else:
+ lower_case_tags.add(tag.lower())
+ self.posts_per_tag[tag].append(post.source_path)
+ self.posts_per_category[post.meta('category')].append(post.source_path)
else:
self.pages.append(post)
if self.config['OLD_THEME_SUPPORT']:
post._add_old_metadata()
+ self.post_per_file[post.destination_path(lang=lang)] = post
+ self.post_per_file[post.destination_path(lang=lang, extension=post.source_ext())] = post
+
for name, post in list(self.global_data.items()):
self.timeline.append(post)
self.timeline.sort(key=lambda p: p.date)
@@ -712,13 +888,15 @@ class Nikola(object):
for i, p in enumerate(post_timeline[:-1]):
p.prev_post = post_timeline[i + 1]
self._scanned = True
- print("done!")
+ print("done!", file=sys.stderr)
def generic_page_renderer(self, lang, post, filters):
"""Render post fragments to final HTML pages."""
context = {}
deps = post.deps(lang) + \
self.template_system.template_deps(post.template_name)
+ deps.extend(utils.get_asset_path(x, self.THEMES) for x in ('bundles', 'parent', 'engine'))
+ deps = list(filter(None, deps))
context['post'] = post
context['lang'] = lang
context['title'] = post.title(lang)
@@ -729,8 +907,9 @@ class Nikola(object):
context['enable_comments'] = True
else:
context['enable_comments'] = self.config['COMMENTS_IN_STORIES']
+ extension = self.get_compiler(post.source_path).extension()
output_name = os.path.join(self.config['OUTPUT_FOLDER'],
- post.destination_path(lang))
+ post.destination_path(lang, extension))
deps_dict = copy(context)
deps_dict.pop('post')
if post.prev_post:
@@ -788,12 +967,192 @@ class Nikola(object):
return utils.apply_filters(task, filters)
-def s_l(lang):
- """A set_locale that uses utf8 encoding and returns ''."""
- utils.LocaleBorg().current_lang = lang
+def sanitized_locales(locale_fallback, locale_default, locales, translations):
+ """Sanitizes all locales availble into a nikola session
+
+ There will be one locale for each language in translations.
+
+ Locales for languages not in translations are ignored.
+
+ An explicit locale for a language can be specified in locales[language].
+
+ 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.
+
+ Explicit but invalid locales are replaced with the sanitized locale_fallback
+
+ 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
+
+ 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))
+ locales[lang] = locale_n
+
+ return locale_fallback, locale_default, locales
+
+
+def is_valid_locale(locale_n):
+ """True if locale_n is acceptable for locale.setlocale
+
+ for py2x compat locale_n should be of type str
+ """
+ try:
+ locale.setlocale(locale.LC_ALL, locale_n)
+ return True
+ except locale.Error:
+ return False
+
+
+def valid_locale_fallback(desired_locale=None):
+ """returns 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.utf8'), 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):
+ locale_n = str(_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):
+ # 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]) + '.utf8')
+ 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:
- locale.setlocale(locale.LC_ALL, (lang, "utf8"))
+ 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:
- print("WARNING: could not set locale to {0}."
- "This may cause some i18n features not to work.".format(lang))
- return ''
+ pass
+
+
+_windows_locale_guesses = {
+ # some languages may need that the appropiate Microsoft's Language Pack
+ # be instaled; the 'str' bit will be added in the guess function
+ "bg": "Bulgarian",
+ "ca": "Catalan",
+ "de": "German",
+ "el": "Greek",
+ "en": "English",
+ "eo": "Esperanto",
+ "es": "Spanish",
+ "fa": "Farsi", # persian
+ "fr": "French",
+ "hr": "Croatian",
+ "it": "Italian",
+ "jp": "Japanese",
+ "nl": "Dutch",
+ "pl": "Polish",
+ "pt_br": "Portuguese_Brazil",
+ "ru": "Russian",
+ "sl_si": "Slovenian",
+ "tr_tr": "Turkish",
+ "zh_cn": "Chinese_China", # Chinese (Simplified)
+}
+
+
+SOCIAL_BUTTONS_CODE = """
+<!-- Social buttons -->
+<div id="addthisbox" class="addthis_toolbox addthis_peekaboo_style addthis_default_style addthis_label_style addthis_32x32_style">
+<a class="addthis_button_more">Share</a>
+<ul><li><a class="addthis_button_facebook"></a>
+<li><a class="addthis_button_google_plusone_share"></a>
+<li><a class="addthis_button_linkedin"></a>
+<li><a class="addthis_button_twitter"></a>
+</ul>
+</div>
+<script type="text/javascript" src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-4f7088a56bb93798"></script>
+<!-- End of social buttons -->
+"""
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
index c4ca788..23ca287 100644
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,17 +24,24 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from __future__ import absolute_import
+
__all__ = [
'Command',
'LateTask',
'PageCompiler',
+ 'RestExtension',
'Task',
- 'TemplateSystem'
+ 'TaskMultiplier',
+ 'TemplateSystem',
+ 'SignalHandler'
]
from yapsy.IPlugin import IPlugin
from doit.cmd_base import Command as DoitCommand
+from .utils import LOGGER, first_line
+
class BasePlugin(IPlugin):
"""Base plugin class."""
@@ -63,7 +72,7 @@ class Command(BasePlugin, DoitCommand):
"""Check if the command can run in the current environment,
fail if needed, or call _execute."""
if self.needs_config and not self.site.configured:
- print("This command needs to run inside an existing Nikola site.")
+ LOGGER.error("This command needs to run inside an existing Nikola site.")
return False
self._execute(options, args)
@@ -96,7 +105,7 @@ DoitCommand.help = help
class BaseTask(BasePlugin):
- """PLugins of this type are task generators."""
+ """Plugins of this type are task generators."""
name = "dummy_task"
@@ -108,9 +117,17 @@ class BaseTask(BasePlugin):
"""Task generator."""
raise NotImplementedError()
+ def group_task(self):
+ """dict for group task"""
+ return {
+ 'basename': self.name,
+ 'name': None,
+ 'doc': first_line(self.__doc__),
+ }
+
class Task(BaseTask):
- """PLugins of this type are task generators."""
+ """Plugins of this type are task generators."""
class LateTask(BaseTask):
@@ -119,7 +136,7 @@ class LateTask(BaseTask):
name = "dummy_latetask"
-class TemplateSystem(object):
+class TemplateSystem(BasePlugin):
"""Plugins of this type wrap templating systems."""
name = "dummy templates"
@@ -132,7 +149,7 @@ class TemplateSystem(object):
"""Returns filenames which are dependencies for a template."""
raise NotImplementedError()
- def render_template(name, output_name, context):
+ def render_template(self, template_name, output_name, context):
"""Renders template to a file using context.
This must save the data to output_name *and* return it
@@ -140,8 +157,24 @@ class TemplateSystem(object):
"""
raise NotImplementedError()
+ def render_template_to_string(self, template, context):
+ """ Renders template to a string using context. """
+
+ raise NotImplementedError()
+
-class PageCompiler(object):
+class TaskMultiplier(BasePlugin):
+ """Plugins that take a task and return *more* tasks."""
+
+ name = "dummy multiplier"
+
+ def process(self, task):
+ """Examine task and create more tasks.
+ Returns extra tasks only."""
+ return []
+
+
+class PageCompiler(BasePlugin):
"""Plugins that compile text files into HTML."""
name = "dummy compiler"
@@ -154,10 +187,105 @@ class PageCompiler(object):
'description': '',
}
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=False):
"""Compile the source, save it on dest."""
raise NotImplementedError()
def create_post(self, path, onefile=False, **kw):
"""Create post file with optional metadata."""
raise NotImplementedError()
+
+ def extension(self):
+ """The preferred extension for the output of this compiler."""
+ return ".html"
+
+
+class RestExtension(BasePlugin):
+ name = "dummy_rest_extension"
+
+
+class SignalHandler(BasePlugin):
+ name = "dummy_signal_handler"
+
+
+class Importer(Command):
+ """Basic structure for importing data into Nikola.
+
+ The flow is:
+
+ read_data
+ preprocess_data
+ parse_data
+ generate_base_site
+ populate_context
+ create_config
+ filter_data
+ process_data
+
+ process_data can branch into:
+
+ import_story (may use import_file and save_post)
+ import_post (may use import_file and save_post)
+ import_attachment (may use import_file)
+
+ Finally:
+
+ write_urlmap
+ """
+
+ name = "dummy_importer"
+
+ def _execute(self, options={}, args=[]):
+ """Import the data into Nikola."""
+ raise NotImplementedError()
+
+ def generate_base_site(self, path):
+ """Create the base site."""
+ raise NotImplementedError()
+
+ def populate_context(self):
+ """Use data to fill context for configuration."""
+ raise NotImplementedError()
+
+ def create_config(self):
+ """Use the context to create configuration."""
+ raise NotImplementedError()
+
+ def read_data(self, source):
+ """Fetch data into self.data"""
+ raise NotImplementedError()
+
+ def preprocess_data(self):
+ """Modify data if needed."""
+ pass
+
+ def parse_data(self):
+ """Convert self.data into self.items"""
+ raise NotImplementedError()
+
+ def filter_data(self):
+ """Remove data that's not to be imported."""
+ pass
+
+ def process_data(self):
+ """Go through self.items and save them."""
+
+ def import_story(self):
+ """Create a story."""
+ raise NotImplementedError()
+
+ def import_post(self):
+ """Create a post."""
+ raise NotImplementedError()
+
+ def import_attachment(self):
+ """Create an attachment."""
+ raise NotImplementedError()
+
+ def import_file(self):
+ """Import a file."""
+ raise NotImplementedError()
+
+ def save_post(self):
+ """Save a post to disk."""
+ raise NotImplementedError()
diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py
index b1de7f1..139759b 100644
--- a/nikola/plugins/__init__.py
+++ b/nikola/plugins/__init__.py
@@ -1,3 +1,2 @@
+# -*- coding: utf-8 -*-
from __future__ import absolute_import
-
-from . import command_import_wordpress # NOQA
diff --git a/nikola/plugins/basic_import.py b/nikola/plugins/basic_import.py
new file mode 100644
index 0000000..e368fca
--- /dev/null
+++ b/nikola/plugins/basic_import.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals, print_function
+import codecs
+import csv
+import datetime
+import os
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse # NOQA
+
+from lxml import etree, html
+from mako.template import Template
+
+from nikola import utils
+
+links = {}
+
+
+class ImportMixin(object):
+ """Mixin with common used methods."""
+
+ name = "import_mixin"
+ needs_config = False
+ doc_usage = "[options] wordpress_export_file"
+ doc_purpose = "import a wordpress dump."
+ cmd_options = [
+ {
+ 'name': 'output_folder',
+ 'long': 'output-folder',
+ 'short': 'o',
+ 'default': 'new_site',
+ 'help': 'Location to write imported content.'
+ },
+ ]
+
+ def _execute(self, options={}, args=[]):
+ """Import a blog from an export into a Nikola site."""
+ raise NotImplementedError("Must be implemented by a subclass.")
+
+ @classmethod
+ def get_channel_from_file(cls, filename):
+ tree = etree.fromstring(cls.read_xml_file(filename))
+ channel = tree.find('channel')
+ return channel
+
+ @staticmethod
+ def configure_redirections(url_map):
+ redirections = []
+ for k, v in url_map.items():
+ if not k[-1] == '/':
+ k = k + '/'
+
+ # remove the initial "/" because src is a relative file path
+ src = (urlparse(k).path + 'index.html')[1:]
+ dst = (urlparse(v).path)
+ if src == 'index.html':
+ utils.LOGGER.warn("Can't do a redirect for: {0!r}".format(k))
+ else:
+ redirections.append((src, dst))
+
+ return redirections
+
+ def generate_base_site(self):
+ if not os.path.exists(self.output_folder):
+ os.system('nikola init ' + self.output_folder)
+ else:
+ self.import_into_existing_site = True
+ utils.LOGGER.notice('The folder {0} already exists - assuming that this is a '
+ 'already existing nikola site.'.format(self.output_folder))
+
+ filename = os.path.join(os.path.dirname(utils.__file__), 'conf.py.in')
+ # The 'strict_undefined=True' will give the missing symbol name if any,
+ # (ex: NameError: 'THEME' is not defined )
+ # for other errors from mako/runtime.py, you can add format_extensions=True ,
+ # then more info will be writen to *somefile* (most probably conf.py)
+ conf_template = Template(filename=filename, strict_undefined=True)
+
+ return conf_template
+
+ @staticmethod
+ def populate_context(channel):
+ raise NotImplementedError("Must be implemented by a subclass.")
+
+ @classmethod
+ def transform_content(cls, content):
+ return content
+
+ @classmethod
+ def write_content(cls, filename, content):
+ doc = html.document_fromstring(content)
+ doc.rewrite_links(replacer)
+
+ utils.makedirs(os.path.dirname(filename))
+ with open(filename, "wb+") as fd:
+ fd.write(html.tostring(doc, encoding='utf8'))
+
+ @staticmethod
+ def write_metadata(filename, title, slug, post_date, description, tags):
+ if not description:
+ description = ""
+
+ utils.makedirs(os.path.dirname(filename))
+ with codecs.open(filename, "w+", "utf8") as fd:
+ fd.write('{0}\n'.format(title))
+ fd.write('{0}\n'.format(slug))
+ fd.write('{0}\n'.format(post_date))
+ fd.write('{0}\n'.format(','.join(tags)))
+ fd.write('\n')
+ fd.write('{0}\n'.format(description))
+
+ @staticmethod
+ def write_urlmap_csv(output_file, url_map):
+ utils.makedirs(os.path.dirname(output_file))
+ with codecs.open(output_file, 'w+', 'utf8') as fd:
+ csv_writer = csv.writer(fd)
+ for item in url_map.items():
+ csv_writer.writerow(item)
+
+ def get_configuration_output_path(self):
+ if not self.import_into_existing_site:
+ filename = 'conf.py'
+ else:
+ filename = 'conf.py.{name}-{time}'.format(
+ time=datetime.datetime.now().strftime('%Y%m%d_%H%M%S'),
+ name=self.name)
+ config_output_path = os.path.join(self.output_folder, filename)
+ utils.LOGGER.notice('Configuration will be written to: {0}'.format(config_output_path))
+
+ return config_output_path
+
+ @staticmethod
+ def write_configuration(filename, rendered_template):
+ utils.makedirs(os.path.dirname(filename))
+ with codecs.open(filename, 'w+', 'utf8') as fd:
+ fd.write(rendered_template)
+
+
+def replacer(dst):
+ return links.get(dst, dst)
diff --git a/nikola/plugins/command/__init__.py b/nikola/plugins/command/__init__.py
new file mode 100644
index 0000000..9be4d63
--- /dev/null
+++ b/nikola/plugins/command/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 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.
diff --git a/nikola/plugins/command/auto.plugin b/nikola/plugins/command/auto.plugin
new file mode 100644
index 0000000..87939b2
--- /dev/null
+++ b/nikola/plugins/command/auto.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = auto
+Module = auto
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.2
+Website = http://getnikola.com
+Description = Automatically detect site changes, rebuild and optionally refresh a browser.
diff --git a/nikola/plugins/command/auto.py b/nikola/plugins/command/auto.py
new file mode 100644
index 0000000..cb726d9
--- /dev/null
+++ b/nikola/plugins/command/auto.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function, unicode_literals
+
+import codecs
+import json
+import os
+import subprocess
+
+from nikola.plugin_categories import Command
+from nikola.utils import req_missing
+
+GUARDFILE = """#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from livereload.task import Task
+import json
+import subprocess
+
+def f():
+ import subprocess
+ subprocess.call(("nikola", "build"))
+
+fdata = json.loads('''{0}''')
+
+for watch in fdata:
+ Task.add(watch, f)
+"""
+
+
+class Auto(Command):
+ """Start debugging console."""
+ name = "auto"
+ doc_purpose = "automatically detect site changes, rebuild and optionally refresh a browser"
+ cmd_options = [
+ {
+ 'name': 'browser',
+ 'short': 'b',
+ 'type': bool,
+ 'help': 'Start a web browser.',
+ 'default': False,
+ },
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Start the watcher."""
+ try:
+ from livereload.server import start
+ except ImportError:
+ req_missing(['livereload'], 'use the "auto" command')
+ return
+
+ # Run an initial build so we are uptodate
+ subprocess.call(("nikola", "build"))
+
+ port = options and options.get('port')
+
+ # Create a Guardfile
+ with codecs.open("Guardfile", "wb+", "utf8") as guardfile:
+ l = ["conf.py", "themes", "templates", self.site.config['GALLERY_PATH']]
+ for item in self.site.config['post_pages']:
+ l.append(os.path.dirname(item[0]))
+ for item in self.site.config['FILES_FOLDERS']:
+ l.append(os.path.dirname(item))
+ data = GUARDFILE.format(json.dumps(l))
+ guardfile.write(data)
+
+ out_folder = self.site.config['OUTPUT_FOLDER']
+
+ os.chmod("Guardfile", 0o755)
+
+ start(port, out_folder, options and options.get('browser'))
diff --git a/nikola/plugins/command_bootswatch_theme.plugin b/nikola/plugins/command/bootswatch_theme.plugin
index f75f734..7091310 100644
--- a/nikola/plugins/command_bootswatch_theme.plugin
+++ b/nikola/plugins/command/bootswatch_theme.plugin
@@ -1,10 +1,10 @@
[Core]
Name = bootswatch_theme
-Module = command_bootswatch_theme
+Module = bootswatch_theme
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Given a swatch name and a parent theme, creates a custom theme.
diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command/bootswatch_theme.py
index 8400c9f..eb27f94 100644
--- a/nikola/plugins/command_bootswatch_theme.py
+++ b/nikola/plugins/command/bootswatch_theme.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,15 +33,18 @@ except ImportError:
requests = None # NOQA
from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('bootswatch_theme', utils.STDERR_HANDLER)
class CommandBootswatchTheme(Command):
- """Given a swatch name and a parent theme, creates a custom theme."""
+ """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 and a parent theme, creates a custom"\
- " theme."
+ doc_purpose = "given a swatch name from bootswatch.com and a parent theme, creates a custom"\
+ " theme"
cmd_options = [
{
'name': 'name',
@@ -60,37 +65,39 @@ class CommandBootswatchTheme(Command):
'name': 'parent',
'short': 'p',
'long': 'parent',
- 'default': 'site',
- 'help': 'Parent theme name (default: site)',
+ '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."""
if requests is None:
- print('To use the install_theme command, you need to install the '
- '"requests" package.')
- return
+ utils.req_missing(['requests'], 'install Bootswatch themes')
name = options['name']
swatch = options['swatch']
parent = options['parent']
+ version = ''
+
+ # See if we need bootswatch for bootstrap v2 or v3
+ themes = utils.get_theme_chain(parent)
+ if 'bootstrap3' not in themes:
+ version = '2'
+ elif 'bootstrap' not in themes:
+ LOGGER.warn('"bootswatch_theme" only makes sense for themes that use bootstrap')
- print("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch,
- parent))
- try:
- os.makedirs(os.path.join('themes', name, 'assets', 'css'))
- except:
- pass
+ LOGGER.notice("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, parent))
+ utils.makedirs(os.path.join('themes', name, 'assets', 'css'))
for fname in ('bootstrap.min.css', 'bootstrap.css'):
- url = '/'.join(('http://bootswatch.com', swatch, fname))
- print("Downloading: ", url)
+ url = '/'.join(('http://bootswatch.com', version, swatch, fname))
+ LOGGER.notice("Downloading: " + url)
data = requests.get(url).text
with open(os.path.join('themes', name, 'assets', 'css', fname),
'wb+') as output:
- output.write(data)
+ output.write(data.encode('utf-8'))
with open(os.path.join('themes', name, 'parent'), 'wb+') as output:
- output.write(parent)
- print('Theme created. Change the THEME setting to "{0}" to use '
- 'it.'.format(name))
+ 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 d4dcd1c..8ceda5f 100644
--- a/nikola/plugins/command_check.plugin
+++ b/nikola/plugins/command/check.plugin
@@ -1,10 +1,10 @@
[Core]
Name = check
-Module = command_check
+Module = check
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Check the generated site
diff --git a/nikola/plugins/command/check.py b/nikola/plugins/command/check.py
new file mode 100644
index 0000000..5c7e49a
--- /dev/null
+++ b/nikola/plugins/command/check.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+import os
+import re
+import sys
+try:
+ from urllib import unquote
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import unquote, urlparse # NOQA
+
+import lxml.html
+
+from nikola.plugin_categories import Command
+from nikola.utils import get_logger
+
+
+class CommandCheck(Command):
+ """Check the generated site."""
+
+ name = "check"
+ logger = None
+
+ doc_usage = "-l [--find-sources] | -f"
+ doc_purpose = "check links and files in the generated site"
+ cmd_options = [
+ {
+ 'name': 'links',
+ 'short': 'l',
+ 'long': 'check-links',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Check for dangling links',
+ },
+ {
+ 'name': 'files',
+ 'short': 'f',
+ 'long': 'check-files',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Check for unknown files',
+ },
+ {
+ 'name': 'clean',
+ 'long': 'clean-files',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Remove all unknown files, use with caution',
+ },
+ {
+ 'name': 'find_sources',
+ 'long': 'find-sources',
+ 'type': bool,
+ 'default': False,
+ 'help': 'List possible source files for files with broken links.',
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Check the generated site."""
+
+ self.logger = get_logger('check', self.site.loghandlers)
+
+ if not options['links'] and not options['files'] and not options['clean']:
+ print(self.help())
+ return False
+ if options['links']:
+ failure = self.scan_links(options['find_sources'])
+ if options['files']:
+ failure = self.scan_files()
+ if options['clean']:
+ failure = self.clean_files()
+ if failure:
+ sys.exit(1)
+
+ existing_targets = set([])
+
+ def analyze(self, task, find_sources=False):
+ rv = False
+ self.whitelist = [re.compile(x) for x in self.site.config['LINK_CHECK_WHITELIST']]
+ try:
+ filename = task.split(":")[-1]
+ d = lxml.html.fromstring(open(filename).read())
+ for l in d.iterlinks():
+ target = l[0].attrib[l[1]]
+ if target == "#":
+ continue
+ parsed = urlparse(target)
+ if parsed.scheme or target.startswith('//'):
+ continue
+ if parsed.fragment:
+ target = target.split('#')[0]
+ target_filename = os.path.abspath(
+ os.path.join(os.path.dirname(filename), unquote(target)))
+ if any(re.match(x, target_filename) for x in self.whitelist):
+ continue
+ elif target_filename not in self.existing_targets:
+ if os.path.exists(target_filename):
+ self.existing_targets.add(target_filename)
+ else:
+ rv = True
+ self.logger.warn("Broken link in {0}: ".format(filename), target)
+ if find_sources:
+ self.logger.warn("Possible sources:")
+ self.logger.warn(os.popen('nikola list --deps ' + task, 'r').read())
+ self.logger.warn("===============================\n")
+ except Exception as exc:
+ self.logger.error("Error with:", filename, exc)
+ return rv
+
+ def scan_links(self, find_sources=False):
+ self.logger.notice("Checking Links:")
+ self.logger.notice("===============")
+ failure = False
+ for task in os.popen('nikola list --all', 'r').readlines():
+ task = task.strip()
+ if task.split(':')[0] in (
+ 'render_tags', 'render_archive',
+ 'render_galleries', 'render_indexes',
+ 'render_pages'
+ 'render_site') and '.html' in task:
+ if self.analyze(task, find_sources):
+ failure = True
+ if not failure:
+ self.logger.notice("All links checked.")
+ return failure
+
+ def scan_files(self):
+ failure = False
+ self.logger.notice("Checking Files:")
+ self.logger.notice("===============\n")
+ only_on_output, only_on_input = self.real_scan_files()
+
+ # Ignore folders
+ only_on_output = [p for p in only_on_output if not os.path.isdir(p)]
+ only_on_input = [p for p in only_on_input if not os.path.isdir(p)]
+
+ if only_on_output:
+ only_on_output.sort()
+ self.logger.warn("Files from unknown origins:")
+ for f in only_on_output:
+ self.logger.warn(f)
+ failure = True
+ if only_on_input:
+ only_on_input.sort()
+ self.logger.warn("Files not generated:")
+ for f in only_on_input:
+ self.logger.warn(f)
+ if not failure:
+ self.logger.notice("All files checked.")
+ return failure
+
+ def clean_files(self):
+ only_on_output, _ = self.real_scan_files()
+ for f in only_on_output:
+ os.unlink(f)
+ return True
+
+ def real_scan_files(self):
+ task_fnames = set([])
+ real_fnames = set([])
+ output_folder = self.site.config['OUTPUT_FOLDER']
+ # First check that all targets are generated in the right places
+ for task in os.popen('nikola list --all', 'r').readlines():
+ task = task.strip()
+ if output_folder in task and ':' in task:
+ fname = task.split(':', 1)[-1]
+ task_fnames.add(fname)
+ # And now check that there are no non-target files
+ for root, dirs, files in os.walk(output_folder):
+ for src_name in files:
+ fname = os.path.join(root, src_name)
+ real_fnames.add(fname)
+
+ only_on_output = list(real_fnames - task_fnames)
+
+ only_on_input = list(task_fnames - real_fnames)
+
+ return (only_on_output, only_on_input)
diff --git a/nikola/plugins/command_console.plugin b/nikola/plugins/command/console.plugin
index 003b994..a2be9ca 100644
--- a/nikola/plugins/command_console.plugin
+++ b/nikola/plugins/command/console.plugin
@@ -1,9 +1,9 @@
[Core]
Name = console
-Module = command_console
+Module = console
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Start a debugging python console
diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command/console.py
index f4d0295..fe17dfc 100644
--- a/nikola/plugins/command_console.py
+++ b/nikola/plugins/command/console.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,14 +28,19 @@ 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
+
+LOGGER = get_logger('console', STDERR_HANDLER)
class Console(Command):
"""Start debugging console."""
name = "console"
shells = ['ipython', 'bpython', 'plain']
- doc_purpose = "Start an interactive python console with access to your site and configuration."
+ doc_purpose = "Start an interactive Python (IPython->bpython->plain) console with access to your site and configuration"
+ header = "Nikola v" + __version__ + " -- {0} Console (conf = configuration, SITE = site engine)"
def ipython(self):
"""IPython shell."""
@@ -41,13 +48,12 @@ class Console(Command):
try:
import conf
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import IPython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
- IPython.embed(header='Nikola Console (conf = configuration, SITE '
- '= site engine)')
+ IPython.embed(header=self.header.format('IPython'))
def bpython(self):
"""bpython shell."""
@@ -55,14 +61,14 @@ class Console(Command):
try:
import conf
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import bpython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
- bpython.embed(banner='Nikola Console (conf = configuration, SITE '
- '= site engine)', locals_=gl)
+ bpython.embed(banner=self.header.format(
+ 'bpython (Slightly Deprecated)'), locals_=gl)
def plain(self):
"""Plain Python shell."""
@@ -73,7 +79,7 @@ class Console(Command):
SITE.scan_posts()
gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
except ImportError:
- print("No configuration found, cannot run the console.")
+ LOGGER.error("No configuration found, cannot run the console.")
else:
import code
try:
@@ -92,8 +98,7 @@ class Console(Command):
except NameError:
pass
- code.interact(local=gl, banner='Nikola Console (conf = '
- 'configuration, SITE = site engine)')
+ code.interact(local=gl, banner=self.header.format('Python'))
def _execute(self, options, args):
"""Start the console."""
diff --git a/nikola/plugins/command_deploy.plugin b/nikola/plugins/command/deploy.plugin
index c8776b5..10cc796 100644
--- a/nikola/plugins/command_deploy.plugin
+++ b/nikola/plugins/command/deploy.plugin
@@ -1,9 +1,9 @@
[Core]
Name = deploy
-Module = command_deploy
+Module = deploy
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Deploy the site
diff --git a/nikola/plugins/command/deploy.py b/nikola/plugins/command/deploy.py
new file mode 100644
index 0000000..efb909d
--- /dev/null
+++ b/nikola/plugins/command/deploy.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+from ast import literal_eval
+import codecs
+from datetime import datetime
+import os
+import sys
+import subprocess
+import time
+import pytz
+
+from blinker import signal
+
+from nikola.plugin_categories import Command
+from nikola.utils import remove_file, get_logger
+
+
+class Deploy(Command):
+ """Deploy site. """
+ name = "deploy"
+
+ doc_usage = ""
+ doc_purpose = "deploy the site"
+
+ logger = None
+
+ def _execute(self, command, args):
+ self.logger = get_logger('deploy', self.site.loghandlers)
+ # Get last successful deploy date
+ timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
+ 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\n")
+ time.sleep(5)
+
+ deploy_drafts = self.site.config.get('DEPLOY_DRAFTS', True)
+ deploy_future = self.site.config.get('DEPLOY_FUTURE', False)
+ if not (deploy_drafts and deploy_future):
+ # Remove drafts and future posts
+ out_dir = self.site.config['OUTPUT_FOLDER']
+ undeployed_posts = []
+ self.site.scan_posts()
+ for post in self.site.timeline:
+ if (not deploy_drafts and post.is_draft) or \
+ (not deploy_future and post.publish_later):
+ remove_file(os.path.join(out_dir, post.destination_path()))
+ remove_file(os.path.join(out_dir, post.source_path))
+ undeployed_posts.append(post)
+
+ for command in self.site.config['DEPLOY_COMMANDS']:
+ self.logger.notice("==> {0}".format(command))
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ self.logger.error('Failed deployment — command {0} '
+ 'returned {1}'.format(e.cmd, e.returncode))
+ sys.exit(e.returncode)
+
+ self.logger.notice("Successful deployment")
+ if self.site.config['TIMEZONE'] is not None:
+ tzinfo = pytz.timezone(self.site.config['TIMEZONE'])
+ else:
+ tzinfo = pytz.UTC
+ try:
+ with open(timestamp_path, 'rb') as inf:
+ last_deploy = literal_eval(inf.read().strip())
+ # this might ignore DST
+ last_deploy = last_deploy.replace(tzinfo=tzinfo)
+ clean = False
+ except Exception:
+ last_deploy = datetime(1970, 1, 1).replace(tzinfo=tzinfo)
+ clean = True
+
+ new_deploy = datetime.now()
+ self._emit_deploy_event(last_deploy, new_deploy, clean, undeployed_posts)
+
+ # Store timestamp of successful deployment
+ with codecs.open(timestamp_path, 'wb+', 'utf8') as outf:
+ outf.write(repr(new_deploy))
+
+ def _emit_deploy_event(self, last_deploy, new_deploy, clean=False, undeployed=None):
+ """ Emit events for all timeline entries newer than last deploy.
+
+ last_deploy: datetime
+ Time stamp of the last successful deployment.
+
+ new_deploy: datetime
+ Time stamp of the current deployment.
+
+ clean: bool
+ True when it appears like deploy is being run after a clean.
+
+ """
+
+ if undeployed is None:
+ undeployed = []
+
+ event = {
+ 'last_deploy': last_deploy,
+ 'new_deploy': new_deploy,
+ 'clean': clean,
+ 'undeployed': undeployed
+ }
+
+ deployed = [
+ entry for entry in self.site.timeline
+ if entry.date > last_deploy and entry not in undeployed
+ ]
+
+ event['deployed'] = deployed
+
+ if len(deployed) > 0 or len(undeployed) > 0:
+ signal('deployed').send(event)
diff --git a/nikola/plugins/command_import_blogger.plugin b/nikola/plugins/command/import_blogger.plugin
index b275a7f..91a7cb6 100644
--- a/nikola/plugins/command_import_blogger.plugin
+++ b/nikola/plugins/command/import_blogger.plugin
@@ -1,10 +1,10 @@
[Core]
Name = import_blogger
-Module = command_import_blogger
+Module = import_blogger
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Import a blogger site from a XML dump.
diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command/import_blogger.py
index ecc4676..53618b4 100644
--- a/nikola/plugins/command_import_blogger.py
+++ b/nikola/plugins/command/import_blogger.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,8 +25,6 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals, print_function
-import codecs
-import csv
import datetime
import os
import time
@@ -38,30 +38,23 @@ try:
import feedparser
except ImportError:
feedparser = None # NOQA
-from lxml import html
-from mako.template import Template
from nikola.plugin_categories import Command
from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin
-links = {}
+LOGGER = utils.get_logger('import_blogger', utils.STDERR_HANDLER)
-class CommandImportBlogger(Command):
+class CommandImportBlogger(Command, ImportMixin):
"""Import a blogger dump."""
name = "import_blogger"
needs_config = False
doc_usage = "[options] blogger_export_file"
- doc_purpose = "Import a blogger dump."
- cmd_options = [
- {
- 'name': 'output_folder',
- 'long': 'output-folder',
- 'short': 'o',
- 'default': 'new_site',
- 'help': 'Location to write imported content.'
- },
+ doc_purpose = "import a blogger dump"
+ cmd_options = ImportMixin.cmd_options + [
{
'name': 'exclude_drafts',
'long': 'no-drafts',
@@ -74,11 +67,9 @@ class CommandImportBlogger(Command):
def _execute(self, options, args):
"""Import a Blogger blog from an export file into a Nikola site."""
-
# Parse the data
if feedparser is None:
- print('To use the import_blogger command,'
- ' you have to install the "feedparser" package.')
+ req_missing(['feedparser'], 'import Blogger dumps')
return
if not args:
@@ -101,8 +92,11 @@ class CommandImportBlogger(Command):
self.write_urlmap_csv(
os.path.join(self.output_folder, 'url_map.csv'), self.url_map)
- self.write_configuration(self.get_configuration_output_path(
- ), conf_template.render(**self.context))
+ conf_out_path = self.get_configuration_output_path()
+ # if it tracebacks here, look a comment in
+ # basic_import.Import_Mixin.generate_base_site
+ conf_termplate_render = conf_template.render(**self.context)
+ self.write_configuration(conf_out_path, conf_termplate_render)
@classmethod
def get_channel_from_file(cls, filename):
@@ -111,82 +105,36 @@ class CommandImportBlogger(Command):
return feedparser.parse(filename)
@staticmethod
- def configure_redirections(url_map):
- redirections = []
- for k, v in url_map.items():
- # remove the initial "/" because src is a relative file path
- src = (urlparse(k).path + 'index.html')[1:]
- dst = (urlparse(v).path)
- if src == 'index.html':
- print("Can't do a redirect for: {0!r}".format(k))
- else:
- redirections.append((src, dst))
-
- return redirections
-
- def generate_base_site(self):
- if not os.path.exists(self.output_folder):
- os.system('nikola init ' + self.output_folder)
- else:
- self.import_into_existing_site = True
- print('The folder {0} already exists - assuming that this is a '
- 'already existing nikola site.'.format(self.output_folder))
-
- conf_template = Template(filename=os.path.join(
- os.path.dirname(utils.__file__), 'conf.py.in'))
-
- return conf_template
-
- @staticmethod
def populate_context(channel):
+ # may need changes when the template conf.py.in changes
context = {}
context['DEFAULT_LANG'] = 'en' # blogger doesn't include the language
# in the dump
context['BLOG_TITLE'] = channel.feed.title
context['BLOG_DESCRIPTION'] = '' # Missing in the dump
- context['SITE_URL'] = channel.feed.link.rstrip('/')
+ context['SITE_URL'] = channel.feed.link
context['BLOG_EMAIL'] = channel.feed.author_detail.email
context['BLOG_AUTHOR'] = channel.feed.author_detail.name
- context['POST_PAGES'] = '''(
- ("posts/*.html", "posts", "post.tmpl", True),
- ("stories/*.html", "stories", "story.tmpl", False),
- )'''
- context['POST_COMPILERS'] = '''{
- "rest": ('.txt', '.rst'),
- "markdown": ('.md', '.mdown', '.markdown', '.wp'),
- "html": ('.html', '.htm')
- }
- '''
+ context['POSTS'] = '''(
+ ("posts/*.txt", "posts", "post.tmpl"),
+ ("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.html", "posts", "post.tmpl"),
+ )'''
+ context['PAGES'] = '''(
+ ("articles/*.txt", "articles", "story.tmpl"),
+ ("articles/*.rst", "articles", "story.tmpl"),
+ )'''
+ context['COMPILERS'] = '''{
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown', '.wp'),
+ "html": ('.html', '.htm')
+ }
+ '''
+ context['THEME'] = 'bootstrap3'
return context
- @classmethod
- def transform_content(cls, content):
- # No transformations yet
- return content
-
- @classmethod
- def write_content(cls, filename, content):
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
-
- with open(filename, "wb+") as fd:
- fd.write(html.tostring(doc, encoding='utf8'))
-
- @staticmethod
- def write_metadata(filename, title, slug, post_date, description, tags):
- if not description:
- description = ""
-
- with codecs.open(filename, "w+", "utf8") as fd:
- fd.write('{0}\n'.format(title))
- fd.write('{0}\n'.format(slug))
- fd.write('{0}\n'.format(post_date))
- fd.write('{0}\n'.format(','.join(tags)))
- fd.write('\n')
- fd.write('{0}\n'.format(description))
-
def import_item(self, item, out_folder=None):
"""Takes an item from the feed and creates a post file."""
if out_folder is None:
@@ -201,8 +149,8 @@ class CommandImportBlogger(Command):
# blogger supports empty titles, which Nikola doesn't
if not title:
- print("Warning: Empty title in post with URL {0}. Using NO_TITLE "
- "as placeholder, please fix.".format(link))
+ LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE "
+ "as placeholder, please fix.".format(link))
title = "NO_TITLE"
if link_path.lower().endswith('.html'):
@@ -211,7 +159,7 @@ class CommandImportBlogger(Command):
slug = utils.slugify(link_path)
if not slug: # should never happen
- print("Error converting post:", title)
+ LOGGER.error("Error converting post:", title)
return
description = ''
@@ -239,7 +187,7 @@ class CommandImportBlogger(Command):
out_folder + '/' + slug + '.html'
if is_draft and self.exclude_drafts:
- print('Draft "{0}" will not be imported.'.format(title))
+ LOGGER.notice('Draft "{0}" will not be imported.'.format(title))
elif content.strip():
# If no content is found, no files are written.
content = self.transform_content(content)
@@ -251,8 +199,8 @@ class CommandImportBlogger(Command):
os.path.join(self.output_folder, out_folder, slug + '.html'),
content)
else:
- print('Not going to import "{0}" because it seems to contain'
- ' no content.'.format(title))
+ LOGGER.warn('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
def process_item(self, item):
post_type = item.tags[0].term
@@ -274,35 +222,8 @@ class CommandImportBlogger(Command):
# FIXME: not importing comments. Does blogger support "pages"?
pass
else:
- print("Unknown post_type:", post_type)
+ LOGGER.warn("Unknown post_type:", post_type)
def import_posts(self, channel):
for item in channel.entries:
self.process_item(item)
-
- @staticmethod
- def write_urlmap_csv(output_file, url_map):
- with codecs.open(output_file, 'w+', 'utf8') as fd:
- csv_writer = csv.writer(fd)
- for item in url_map.items():
- csv_writer.writerow(item)
-
- def get_configuration_output_path(self):
- if not self.import_into_existing_site:
- filename = 'conf.py'
- else:
- filename = 'conf.py.blogger_import-{0}'.format(
- datetime.datetime.now().strftime('%Y%m%d_%H%M%s'))
- config_output_path = os.path.join(self.output_folder, filename)
- print('Configuration will be written to: ' + config_output_path)
-
- return config_output_path
-
- @staticmethod
- def write_configuration(filename, rendered_template):
- with codecs.open(filename, 'w+', 'utf8') as fd:
- fd.write(rendered_template)
-
-
-def replacer(dst):
- return links.get(dst, dst)
diff --git a/nikola/plugins/command/import_feed.plugin b/nikola/plugins/command/import_feed.plugin
new file mode 100644
index 0000000..26e570a
--- /dev/null
+++ b/nikola/plugins/command/import_feed.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = import_feed
+Module = import_feed
+
+[Documentation]
+Author = Grzegorz Śliwiński
+Version = 0.1
+Website = http://www.fizyk.net.pl/
+Description = Import a blog posts from a RSS/Atom dump
+
diff --git a/nikola/plugins/command/import_feed.py b/nikola/plugins/command/import_feed.py
new file mode 100644
index 0000000..b25d9ec
--- /dev/null
+++ b/nikola/plugins/command/import_feed.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals, print_function
+import datetime
+import os
+import time
+
+try:
+ from urlparse import urlparse
+except ImportError:
+ from urllib.parse import urlparse # NOQA
+
+try:
+ import feedparser
+except ImportError:
+ feedparser = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin
+
+LOGGER = utils.get_logger('import_feed', utils.STDERR_HANDLER)
+
+
+class CommandImportFeed(Command, ImportMixin):
+ """Import a feed dump."""
+
+ name = "import_feed"
+ needs_config = False
+ doc_usage = "[options] feed_file"
+ doc_purpose = "import a RSS/Atom dump"
+ cmd_options = ImportMixin.cmd_options
+
+ def _execute(self, options, args):
+ '''
+ Import Atom/RSS feed
+ '''
+ if feedparser is None:
+ req_missing(['feedparser'], 'import feeds')
+ return
+
+ if not args:
+ print(self.help())
+ return
+
+ options['filename'] = args[0]
+ self.feed_export_file = options['filename']
+ self.output_folder = options['output_folder']
+ self.import_into_existing_site = False
+ self.url_map = {}
+ channel = self.get_channel_from_file(self.feed_export_file)
+ self.context = self.populate_context(channel)
+ conf_template = self.generate_base_site()
+ self.context['REDIRECTIONS'] = self.configure_redirections(
+ self.url_map)
+
+ self.import_posts(channel)
+
+ self.write_configuration(self.get_configuration_output_path(
+ ), conf_template.render(**self.context))
+
+ @classmethod
+ def get_channel_from_file(cls, filename):
+ return feedparser.parse(filename)
+
+ @staticmethod
+ def populate_context(channel):
+ context = {}
+ context['DEFAULT_LANG'] = channel.feed.title_detail.language \
+ if channel.feed.title_detail.language else 'en'
+ context['BLOG_TITLE'] = channel.feed.title
+
+ context['BLOG_DESCRIPTION'] = channel.feed.get('subtitle', '')
+ context['SITE_URL'] = channel.feed.get('link', '').rstrip('/')
+ context['BLOG_EMAIL'] = channel.feed.author_detail.get('email', '') if 'author_detail' in channel.feed else ''
+ context['BLOG_AUTHOR'] = channel.feed.author_detail.get('name', '') if 'author_detail' in channel.feed else ''
+
+ context['POST_PAGES'] = '''(
+ ("posts/*.html", "posts", "post.tmpl", True),
+ ("stories/*.html", "stories", "story.tmpl", False),
+ )'''
+ context['COMPILERS'] = '''{
+ "rest": ('.txt', '.rst'),
+ "markdown": ('.md', '.mdown', '.markdown', '.wp'),
+ "html": ('.html', '.htm')
+ }
+ '''
+
+ return context
+
+ def import_posts(self, channel):
+ for item in channel.entries:
+ self.process_item(item)
+
+ def process_item(self, item):
+ self.import_item(item, 'posts')
+
+ def import_item(self, item, out_folder=None):
+ """Takes an item from the feed and creates a post file."""
+ if out_folder is None:
+ out_folder = 'posts'
+
+ # link is something like http://foo.com/2012/09/01/hello-world/
+ # So, take the path, utils.slugify it, and that's our slug
+ link = item.link
+ link_path = urlparse(link).path
+
+ title = item.title
+
+ # blogger supports empty titles, which Nikola doesn't
+ if not title:
+ LOGGER.warn("Empty title in post with URL {0}. Using NO_TITLE "
+ "as placeholder, please fix.".format(link))
+ title = "NO_TITLE"
+
+ if link_path.lower().endswith('.html'):
+ link_path = link_path[:-5]
+
+ slug = utils.slugify(link_path)
+
+ if not slug: # should never happen
+ LOGGER.error("Error converting post:", title)
+ return
+
+ description = ''
+ post_date = datetime.datetime.fromtimestamp(time.mktime(
+ item.published_parsed))
+ if item.get('content'):
+ for candidate in item.get('content', []):
+ content = candidate.value
+ break
+ # FIXME: handle attachments
+ elif item.get('summary'):
+ content = item.get('summary')
+
+ tags = []
+ for tag in item.get('tags', []):
+ tags.append(tag.term)
+
+ if item.get('app_draft'):
+ tags.append('draft')
+ is_draft = True
+ else:
+ is_draft = False
+
+ self.url_map[link] = self.context['SITE_URL'] + '/' + \
+ out_folder + '/' + slug + '.html'
+
+ if is_draft and self.exclude_drafts:
+ LOGGER.notice('Draft "{0}" will not be imported.'.format(title))
+ elif content.strip():
+ # If no content is found, no files are written.
+ content = self.transform_content(content)
+
+ self.write_metadata(os.path.join(self.output_folder, out_folder,
+ slug + '.meta'),
+ title, slug, post_date, description, tags)
+ self.write_content(
+ os.path.join(self.output_folder, out_folder, slug + '.html'),
+ content)
+ else:
+ LOGGER.warn('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
+
+ @staticmethod
+ def write_metadata(filename, title, slug, post_date, description, tags):
+ ImportMixin.write_metadata(filename,
+ title,
+ slug,
+ post_date.strftime(r'%Y/%m/%d %H:%m:%S'),
+ description,
+ tags)
diff --git a/nikola/plugins/command_import_wordpress.plugin b/nikola/plugins/command/import_wordpress.plugin
index ff7cdca..fadc759 100644
--- a/nikola/plugins/command_import_wordpress.plugin
+++ b/nikola/plugins/command/import_wordpress.plugin
@@ -1,10 +1,10 @@
[Core]
Name = import_wordpress
-Module = command_import_wordpress
+Module = import_wordpress
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Import a wordpress site from a XML dump (requires markdown).
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command/import_wordpress.py
index b45fe78..4f32198 100644
--- a/nikola/plugins/command_import_wordpress.py
+++ b/nikola/plugins/command/import_wordpress.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,46 +25,43 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import unicode_literals, print_function
-import codecs
-import csv
-import datetime
import os
import re
+import sys
+from lxml import etree
try:
from urlparse import urlparse
+ from urllib import unquote
except ImportError:
- from urllib.parse import urlparse # NOQA
-
-from lxml import etree, html
-from mako.template import Template
+ from urllib.parse import urlparse, unquote # NOQA
try:
import requests
except ImportError:
requests = None # NOQA
+try:
+ import phpserialize
+except ImportError:
+ phpserialize = None # NOQA
+
from nikola.plugin_categories import Command
from nikola import utils
+from nikola.utils import req_missing
+from nikola.plugins.basic_import import ImportMixin, links
-links = {}
+LOGGER = utils.get_logger('import_wordpress', utils.STDERR_HANDLER)
-class CommandImportWordpress(Command):
- """Import a wordpress dump."""
+class CommandImportWordpress(Command, ImportMixin):
+ """Import a WordPress dump."""
name = "import_wordpress"
needs_config = False
doc_usage = "[options] wordpress_export_file"
- doc_purpose = "Import a wordpress dump."
- cmd_options = [
- {
- 'name': 'output_folder',
- 'long': 'output-folder',
- 'short': 'o',
- 'default': 'new_site',
- 'help': 'Location to write imported content.'
- },
+ doc_purpose = "import a WordPress dump"
+ cmd_options = ImportMixin.cmd_options + [
{
'name': 'exclude_drafts',
'long': 'no-drafts',
@@ -88,13 +87,7 @@ class CommandImportWordpress(Command):
]
def _execute(self, options={}, args=[]):
- """Import a Wordpress blog from an export file into a Nikola site."""
- # Parse the data
- if requests is None:
- print('To use the import_wordpress command,'
- ' you have to install the "requests" package.')
- return
-
+ """Import a WordPress blog from an export file into a Nikola site."""
if not args:
print(self.help())
return
@@ -106,17 +99,38 @@ class CommandImportWordpress(Command):
options['output_folder'] = args.pop(0)
if args:
- print('You specified additional arguments ({0}). Please consider '
- 'putting these arguments before the filename if you '
- 'are running into problems.'.format(args))
+ LOGGER.warn('You specified additional arguments ({0}). Please consider '
+ 'putting these arguments before the filename if you '
+ 'are running into problems.'.format(args))
+
+ self.import_into_existing_site = False
+ self.url_map = {}
+ self.timezone = None
self.wordpress_export_file = options['filename']
self.squash_newlines = options.get('squash_newlines', False)
- self.no_downloads = options.get('no_downloads', False)
self.output_folder = options.get('output_folder', 'new_site')
- self.import_into_existing_site = False
+
self.exclude_drafts = options.get('exclude_drafts', False)
- self.url_map = {}
+ self.no_downloads = options.get('no_downloads', False)
+
+ if not self.no_downloads:
+ def show_info_about_mising_module(modulename):
+ LOGGER.error(
+ 'To use the "{commandname}" command, you have to install '
+ 'the "{package}" package or supply the "--no-downloads" '
+ 'option.'.format(
+ commandname=self.name,
+ package=modulename)
+ )
+
+ if requests is None and phpserialize is None:
+ req_missing(['requests', 'phpserialize'], 'import WordPress dumps without --no-downloads')
+ elif requests is None:
+ req_missing(['requests'], 'import WordPress dumps without --no-downloads')
+ elif phpserialize is None:
+ req_missing(['phpserialize'], 'import WordPress dumps without --no-downloads')
+
channel = self.get_channel_from_file(self.wordpress_export_file)
self.context = self.populate_context(channel)
conf_template = self.generate_base_site()
@@ -130,6 +144,10 @@ class CommandImportWordpress(Command):
rendered_template = conf_template.render(**self.context)
rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ',
rendered_template)
+ if self.timezone:
+ rendered_template = re.sub('# TIMEZONE = \'Europe/Zurich\'',
+ 'TIMEZONE = \'' + self.timezone + '\'',
+ rendered_template)
self.write_configuration(self.get_configuration_output_path(),
rendered_template)
@@ -173,33 +191,6 @@ class CommandImportWordpress(Command):
return channel
@staticmethod
- def configure_redirections(url_map):
- redirections = []
- for k, v in url_map.items():
- # remove the initial "/" because src is a relative file path
- src = (urlparse(k).path + 'index.html')[1:]
- dst = (urlparse(v).path)
- if src == 'index.html':
- print("Can't do a redirect for: {0!r}".format(k))
- else:
- redirections.append((src, dst))
-
- return redirections
-
- def generate_base_site(self):
- if not os.path.exists(self.output_folder):
- os.system('nikola init ' + self.output_folder)
- else:
- self.import_into_existing_site = True
- print('The folder {0} already exists - assuming that this is a '
- 'already existing nikola site.'.format(self.output_folder))
-
- conf_template = Template(filename=os.path.join(
- os.path.dirname(utils.__file__), 'conf.py.in'))
-
- return conf_template
-
- @staticmethod
def populate_context(channel):
wordpress_namespace = channel.nsmap['wp']
@@ -212,8 +203,11 @@ class CommandImportWordpress(Command):
context['BASE_URL'] = get_text_tag(channel, 'link', '#')
if not context['BASE_URL']:
base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace))
- context['BASE_URL'] = get_text_tag(base_site_url, None, "http://foo.com")
+ context['BASE_URL'] = get_text_tag(base_site_url,
+ None,
+ "http://foo.com")
context['SITE_URL'] = context['BASE_URL']
+ context['THEME'] = 'bootstrap3'
author = channel.find('{{{0}}}author'.format(wordpress_namespace))
context['BLOG_EMAIL'] = get_text_tag(
@@ -224,11 +218,13 @@ class CommandImportWordpress(Command):
author,
'{{{0}}}author_display_name'.format(wordpress_namespace),
"Joe Example")
- context['POST_PAGES'] = '''(
- ("posts/*.wp", "posts", "post.tmpl", True),
- ("stories/*.wp", "stories", "story.tmpl", False),
+ context['POSTS'] = '''(
+ ("posts/*.wp", "posts", "post.tmpl"),
)'''
- context['POST_COMPILERS'] = '''{
+ context['PAGES'] = '''(
+ ("stories/*.wp", "stories", "story.tmpl"),
+ )'''
+ context['COMPILERS'] = '''{
"rest": ('.txt', '.rst'),
"markdown": ('.md', '.mdown', '.markdown', '.wp'),
"html": ('.html', '.htm')
@@ -245,8 +241,7 @@ class CommandImportWordpress(Command):
with open(dst_path, 'wb+') as fd:
fd.write(requests.get(url).content)
except requests.exceptions.ConnectionError as err:
- print("Downloading {0} to {1} failed: {2}".format(url, dst_path,
- err))
+ LOGGER.warn("Downloading {0} to {1} failed: {2}".format(url, dst_path, err))
def import_attachment(self, item, wordpress_namespace):
url = get_text_tag(
@@ -257,14 +252,67 @@ class CommandImportWordpress(Command):
dst_path = os.path.join(*([self.output_folder, 'files']
+ list(path.split('/'))))
dst_dir = os.path.dirname(dst_path)
- if not os.path.isdir(dst_dir):
- os.makedirs(dst_dir)
- print("Downloading {0} => {1}".format(url, dst_path))
+ utils.makedirs(dst_dir)
+ LOGGER.notice("Downloading {0} => {1}".format(url, dst_path))
self.download_url_content_to_file(url, dst_path)
dst_url = '/'.join(dst_path.split(os.sep)[2:])
links[link] = '/' + dst_url
links[url] = '/' + dst_url
+ self.download_additional_image_sizes(
+ item,
+ wordpress_namespace,
+ os.path.dirname(url)
+ )
+
+ def download_additional_image_sizes(self, item, wordpress_namespace, source_path):
+ if phpserialize is None:
+ return
+
+ additional_metadata = item.findall('{{{0}}}postmeta'.format(wordpress_namespace))
+
+ if additional_metadata is None:
+ return
+
+ for element in additional_metadata:
+ meta_key = element.find('{{{0}}}meta_key'.format(wordpress_namespace))
+ if meta_key is not None and meta_key.text == '_wp_attachment_metadata':
+ meta_value = element.find('{{{0}}}meta_value'.format(wordpress_namespace))
+
+ if meta_value is None:
+ continue
+
+ # Someone from Wordpress thought it was a good idea
+ # serialize PHP objects into that metadata field. Given
+ # that the export should give you the power to insert
+ # your blogging into another site or system its not.
+ # Why don't they just use JSON?
+ if sys.version_info[0] == 2:
+ metadata = phpserialize.loads(meta_value.text)
+ size_key = 'sizes'
+ file_key = 'file'
+ else:
+ metadata = phpserialize.loads(meta_value.text.encode('UTF-8'))
+ size_key = b'sizes'
+ file_key = b'file'
+
+ if not size_key in metadata:
+ continue
+
+ for filename in [metadata[size_key][size][file_key] for size in metadata[size_key]]:
+ url = '/'.join([source_path, filename.decode('utf-8')])
+
+ path = urlparse(url).path
+ dst_path = os.path.join(*([self.output_folder, 'files']
+ + list(path.split('/'))))
+ dst_dir = os.path.dirname(dst_path)
+ utils.makedirs(dst_dir)
+ LOGGER.notice("Downloading {0} => {1}".format(url, dst_path))
+ self.download_url_content_to_file(url, dst_path)
+ dst_url = '/'.join(dst_path.split(os.sep)[2:])
+ links[url] = '/' + dst_url
+ links[url] = '/' + dst_url
+
@staticmethod
def transform_sourcecode(content):
new_content = re.sub('\[sourcecode language="([^"]+)"\]',
@@ -293,27 +341,6 @@ class CommandImportWordpress(Command):
new_content = self.transform_multiple_newlines(new_content)
return new_content
- @classmethod
- def write_content(cls, filename, content):
- doc = html.document_fromstring(content)
- doc.rewrite_links(replacer)
-
- with open(filename, "wb+") as fd:
- fd.write(html.tostring(doc, encoding='utf8'))
-
- @staticmethod
- def write_metadata(filename, title, slug, post_date, description, tags):
- if not description:
- description = ""
-
- with codecs.open(filename, "w+", "utf8") as fd:
- fd.write('{0}\n'.format(title))
- fd.write('{0}\n'.format(slug))
- fd.write('{0}\n'.format(post_date))
- fd.write('{0}\n'.format(','.join(tags)))
- fd.write('\n')
- fd.write('{0}\n'.format(description))
-
def import_item(self, item, wordpress_namespace, out_folder=None):
"""Takes an item from the feed and creates a post file."""
if out_folder is None:
@@ -323,12 +350,13 @@ class CommandImportWordpress(Command):
# link is something like http://foo.com/2012/09/01/hello-world/
# So, take the path, utils.slugify it, and that's our slug
link = get_text_tag(item, 'link', None)
- path = urlparse(link).path
+ path = unquote(urlparse(link).path)
# In python 2, path is a str. slug requires a unicode
- # object. Luckily, paths are also ASCII
+ # object. According to wikipedia, unquoted strings will
+ # usually be UTF8
if isinstance(path, utils.bytes_str):
- path = path.decode('ASCII')
+ path = path.decode('utf8')
slug = utils.slugify(path)
if not slug: # it happens if the post has no "nice" URL
slug = get_text_tag(
@@ -337,12 +365,15 @@ class CommandImportWordpress(Command):
slug = get_text_tag(
item, '{{{0}}}post_id'.format(wordpress_namespace), None)
if not slug: # should never happen
- print("Error converting post:", title)
+ LOGGER.error("Error converting post:", title)
return
description = get_text_tag(item, 'description', '')
post_date = get_text_tag(
item, '{{{0}}}post_date'.format(wordpress_namespace), None)
+ dt = utils.to_datetime(post_date)
+ if dt.tzinfo and self.timezone is None:
+ self.timezone = utils.get_tzname(dt)
status = get_text_tag(
item, '{{{0}}}status'.format(wordpress_namespace), 'publish')
content = get_text_tag(
@@ -350,7 +381,7 @@ class CommandImportWordpress(Command):
tags = []
if status == 'trash':
- print('Trashed post "{0}" will not be imported.'.format(title))
+ LOGGER.warn('Trashed post "{0}" will not be imported.'.format(title))
return
elif status != 'publish':
tags.append('draft')
@@ -365,7 +396,7 @@ class CommandImportWordpress(Command):
tags.append(text)
if is_draft and self.exclude_drafts:
- print('Draft "{0}" will not be imported.'.format(title))
+ LOGGER.notice('Draft "{0}" will not be imported.'.format(title))
elif content.strip():
# If no content is found, no files are written.
self.url_map[link] = self.context['SITE_URL'] + '/' + \
@@ -380,8 +411,8 @@ class CommandImportWordpress(Command):
os.path.join(self.output_folder, out_folder, slug + '.wp'),
content)
else:
- print('Not going to import "{0}" because it seems to contain'
- ' no content.'.format(title))
+ LOGGER.warn('Not going to import "{0}" because it seems to contain'
+ ' no content.'.format(title))
def process_item(self, item):
# The namespace usually is something like:
@@ -401,33 +432,6 @@ class CommandImportWordpress(Command):
for item in channel.findall('item'):
self.process_item(item)
- @staticmethod
- def write_urlmap_csv(output_file, url_map):
- with codecs.open(output_file, 'w+', 'utf8') as fd:
- csv_writer = csv.writer(fd)
- for item in url_map.items():
- csv_writer.writerow(item)
-
- def get_configuration_output_path(self):
- if not self.import_into_existing_site:
- filename = 'conf.py'
- else:
- filename = 'conf.py.wordpress_import-{0}'.format(
- datetime.datetime.now().strftime('%Y%m%d_%H%M%s'))
- config_output_path = os.path.join(self.output_folder, filename)
- print('Configuration will be written to:', config_output_path)
-
- return config_output_path
-
- @staticmethod
- def write_configuration(filename, rendered_template):
- with codecs.open(filename, 'w+', 'utf8') as fd:
- fd.write(rendered_template)
-
-
-def replacer(dst):
- return links.get(dst, dst)
-
def get_text_tag(tag, name, default):
if tag is None:
diff --git a/nikola/plugins/command_init.plugin b/nikola/plugins/command/init.plugin
index f4adf4a..a539f51 100644
--- a/nikola/plugins/command_init.plugin
+++ b/nikola/plugins/command/init.plugin
@@ -1,9 +1,9 @@
[Core]
Name = init
-Module = command_init
+Module = init
[Documentation]
Author = Roberto Alsina
Version = 0.2
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create a new site.
diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command/init.py
index bc36266..1873ec4 100644
--- a/nikola/plugins/command_init.py
+++ b/nikola/plugins/command/init.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,6 +33,10 @@ from mako.template import Template
import nikola
from nikola.plugin_categories import Command
+from nikola.utils import get_logger, makedirs, STDERR_HANDLER
+from nikola.winutils import fix_git_symlinked
+
+LOGGER = get_logger('init', STDERR_HANDLER)
class CommandInit(Command):
@@ -40,7 +46,7 @@ class CommandInit(Command):
doc_usage = "[--demo] folder"
needs_config = False
- doc_purpose = """Create a Nikola site in the specified folder."""
+ doc_purpose = "create a Nikola site in the specified folder"
cmd_options = [
{
'name': 'demo',
@@ -54,25 +60,33 @@ class CommandInit(Command):
SAMPLE_CONF = {
'BLOG_AUTHOR': "Your Name",
'BLOG_TITLE': "Demo Site",
- 'SITE_URL': "http://nikola.ralsina.com.ar",
+ 'SITE_URL': "http://getnikola.com/",
'BLOG_EMAIL': "joe@demo.site",
'BLOG_DESCRIPTION': "This is a demo site for Nikola.",
'DEFAULT_LANG': "en",
+ 'THEME': 'bootstrap3',
- 'POST_PAGES': """(
- ("posts/*.txt", "posts", "post.tmpl", True),
- ("stories/*.txt", "stories", "story.tmpl", False),
+ 'POSTS': """(
+ ("posts/*.rst", "posts", "post.tmpl"),
+ ("posts/*.txt", "posts", "post.tmpl"),
)""",
-
- 'POST_COMPILERS': """{
- "rest": ('.txt', '.rst'),
+ 'PAGES': """(
+ ("stories/*.rst", "stories", "story.tmpl"),
+ ("stories/*.txt", "stories", "story.tmpl"),
+)""",
+ 'COMPILERS': """{
+ "rest": ('.rst', '.txt'),
"markdown": ('.md', '.mdown', '.markdown'),
"textile": ('.textile',),
"txt2tags": ('.t2t',),
"bbcode": ('.bb',),
"wiki": ('.wiki',),
"ipynb": ('.ipynb',),
- "html": ('.html', '.htm')
+ "html": ('.html', '.htm'),
+ # 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'),
}""",
'REDIRECTIONS': '[]',
}
@@ -82,6 +96,7 @@ class CommandInit(Command):
lib_path = cls.get_path_to_nikola_modules()
src = os.path.join(lib_path, 'data', 'samplesite')
shutil.copytree(src, target)
+ fix_git_symlinked(src, target)
@classmethod
def create_configuration(cls, target):
@@ -95,7 +110,7 @@ class CommandInit(Command):
@classmethod
def create_empty_site(cls, target):
for folder in ('files', 'galleries', 'listings', 'posts', 'stories'):
- os.makedirs(os.path.join(target, folder))
+ makedirs(os.path.join(target, folder))
@staticmethod
def get_path_to_nikola_modules():
@@ -112,11 +127,11 @@ class CommandInit(Command):
else:
if not options or not options.get('demo'):
self.create_empty_site(target)
- print('Created empty site at {0}.'.format(target))
+ LOGGER.notice('Created empty site at {0}.'.format(target))
else:
self.copy_sample_site(target)
- print("A new site with example data has been created at "
- "{0}.".format(target))
- print("See README.txt in that folder for more information.")
+ LOGGER.notice("A new site with example data has been created at "
+ "{0}.".format(target))
+ LOGGER.notice("See README.txt in that folder for more information.")
self.create_configuration(target)
diff --git a/nikola/plugins/command/install_plugin.plugin b/nikola/plugins/command/install_plugin.plugin
new file mode 100644
index 0000000..3dbabd8
--- /dev/null
+++ b/nikola/plugins/command/install_plugin.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = install_plugin
+Module = install_plugin
+
+[Documentation]
+Author = Roberto Alsina and Chris Warrick
+Version = 0.1
+Website = http://getnikola.com
+Description = Install a plugin into the current site.
+
diff --git a/nikola/plugins/command/install_plugin.py b/nikola/plugins/command/install_plugin.py
new file mode 100644
index 0000000..fdbd0b7
--- /dev/null
+++ b/nikola/plugins/command/install_plugin.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+import codecs
+import os
+import json
+import shutil
+import subprocess
+from io import BytesIO
+
+import pygments
+from pygments.lexers import PythonLexer
+from pygments.formatters import TerminalFormatter
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('install_plugin', utils.STDERR_HANDLER)
+
+
+# Stolen from textwrap in Python 3.3.2.
+def indent(text, prefix, predicate=None): # NOQA
+ """Adds 'prefix' to the beginning of selected lines in 'text'.
+
+ If 'predicate' is provided, 'prefix' will only be added to the lines
+ where 'predicate(line)' is True. If 'predicate' is not provided,
+ it will default to adding 'prefix' to all non-empty lines that do not
+ consist solely of whitespace characters.
+ """
+ if predicate is None:
+ def predicate(line):
+ return line.strip()
+
+ def prefixed_lines():
+ for line in text.splitlines(True):
+ yield (prefix + line if predicate(line) else line)
+ return ''.join(prefixed_lines())
+
+
+class CommandInstallPlugin(Command):
+ """Install a plugin."""
+
+ name = "install_plugin"
+ doc_usage = "[[-u] plugin_name] | [[-u] -l]"
+ doc_purpose = "install plugin into current site"
+ output_dir = 'plugins'
+ cmd_options = [
+ {
+ 'name': 'list',
+ 'short': 'l',
+ 'long': 'list',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Show list of available plugins.'
+ },
+ {
+ 'name': 'url',
+ 'short': 'u',
+ 'long': 'url',
+ 'type': str,
+ 'help': "URL for the plugin repository (default: "
+ "http://plugins.getnikola.com/v6/plugins.json)",
+ 'default': 'http://plugins.getnikola.com/v6/plugins.json'
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Install plugin into current site."""
+ if requests is None:
+ utils.req_missing(['requests'], 'install plugins')
+
+ listing = options['list']
+ url = options['url']
+ if args:
+ name = args[0]
+ else:
+ name = None
+
+ if name is None and not listing:
+ LOGGER.error("This command needs either a plugin name or the -l option.")
+ return False
+ data = requests.get(url).text
+ data = json.loads(data)
+ if listing:
+ print("Plugins:")
+ print("--------")
+ for plugin in sorted(data.keys()):
+ print(plugin)
+ return True
+ else:
+ self.do_install(name, data)
+
+ def do_install(self, name, data):
+ if name in data:
+ utils.makedirs(self.output_dir)
+ LOGGER.notice('Downloading: ' + data[name])
+ zip_file = BytesIO()
+ zip_file.write(requests.get(data[name]).content)
+ LOGGER.notice('Extracting: {0} into plugins'.format(name))
+ utils.extract_all(zip_file, 'plugins')
+ dest_path = os.path.join('plugins', name)
+ else:
+ try:
+ plugin_path = utils.get_plugin_path(name)
+ except:
+ LOGGER.error("Can't find plugin " + name)
+ return False
+
+ utils.makedirs(self.output_dir)
+ dest_path = os.path.join(self.output_dir, name)
+ if os.path.exists(dest_path):
+ LOGGER.error("{0} is already installed".format(name))
+ return False
+
+ LOGGER.notice('Copying {0} into plugins'.format(plugin_path))
+ shutil.copytree(plugin_path, dest_path)
+
+ reqpath = os.path.join(dest_path, 'requirements.txt')
+ print(reqpath)
+ if os.path.exists(reqpath):
+ LOGGER.notice('This plugin has Python dependencies.')
+ LOGGER.notice('Installing dependencies with pip...')
+ try:
+ subprocess.check_call(('pip', 'install', '-r', reqpath))
+ except subprocess.CalledProcessError:
+ LOGGER.error('Could not install the dependencies.')
+ print('Contents of the requirements.txt file:\n')
+ with codecs.open(reqpath, 'rb', 'utf-8') as fh:
+ print(indent(fh.read(), 4 * ' '))
+ print('You have to install those yourself or through a '
+ 'package manager.')
+ else:
+ LOGGER.notice('Dependency installation succeeded.')
+ reqnpypath = os.path.join(dest_path, 'requirements-nonpy.txt')
+ if os.path.exists(reqnpypath):
+ LOGGER.notice('This plugin has third-party '
+ 'dependencies you need to install '
+ 'manually.')
+ print('Contents of the requirements-nonpy.txt file:\n')
+ with codecs.open(reqnpypath, 'rb', 'utf-8') as fh:
+ for l in fh.readlines():
+ i, j = l.split('::')
+ print(indent(i.strip(), 4 * ' '))
+ print(indent(j.strip(), 8 * ' '))
+ print()
+
+ print('You have to install those yourself or through a package '
+ 'manager.')
+ confpypath = os.path.join(dest_path, 'conf.py.sample')
+ if os.path.exists(confpypath):
+ LOGGER.notice('This plugin has a sample config file.')
+ print('Contents of the conf.py.sample file:\n')
+ with codecs.open(confpypath, 'rb', 'utf-8') as fh:
+ print(indent(pygments.highlight(
+ fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' '))
+ return True
diff --git a/nikola/plugins/command_install_theme.plugin b/nikola/plugins/command/install_theme.plugin
index f010074..84b2623 100644
--- a/nikola/plugins/command_install_theme.plugin
+++ b/nikola/plugins/command/install_theme.plugin
@@ -1,10 +1,10 @@
[Core]
Name = install_theme
-Module = command_install_theme
+Module = install_theme
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Install a theme into the current site.
diff --git a/nikola/plugins/command/install_theme.py b/nikola/plugins/command/install_theme.py
new file mode 100644
index 0000000..a9d835a
--- /dev/null
+++ b/nikola/plugins/command/install_theme.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+import os
+import json
+import shutil
+import codecs
+from io import BytesIO
+
+import pygments
+from pygments.lexers import PythonLexer
+from pygments.formatters import TerminalFormatter
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+from nikola.plugin_categories import Command
+from nikola import utils
+
+LOGGER = utils.get_logger('install_theme', utils.STDERR_HANDLER)
+
+
+# Stolen from textwrap in Python 3.3.2.
+def indent(text, prefix, predicate=None): # NOQA
+ """Adds 'prefix' to the beginning of selected lines in 'text'.
+
+ If 'predicate' is provided, 'prefix' will only be added to the lines
+ where 'predicate(line)' is True. If 'predicate' is not provided,
+ it will default to adding 'prefix' to all non-empty lines that do not
+ consist solely of whitespace characters.
+ """
+ if predicate is None:
+ def predicate(line):
+ return line.strip()
+
+ def prefixed_lines():
+ for line in text.splitlines(True):
+ yield (prefix + line if predicate(line) else line)
+ return ''.join(prefixed_lines())
+
+
+class CommandInstallTheme(Command):
+ """Install a theme."""
+
+ 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: "
+ "http://themes.getnikola.com/v6/themes.json)",
+ 'default': 'http://themes.getnikola.com/v6/themes.json'
+ },
+ ]
+
+ def _execute(self, options, args):
+ """Install theme into current site."""
+ if requests is None:
+ utils.req_missing(['requests'], 'install themes')
+
+ listing = options['list']
+ url = options['url']
+ if args:
+ name = args[0]
+ else:
+ name = None
+
+ if name is None and not listing:
+ LOGGER.error("This command needs either a theme name or the -l option.")
+ return False
+ data = requests.get(url).text
+ data = json.loads(data)
+ if listing:
+ print("Themes:")
+ print("-------")
+ for theme in sorted(data.keys()):
+ print(theme)
+ return True
+ else:
+ self.do_install(name, data)
+ # See if the theme's parent is available. If not, install it
+ while True:
+ parent_name = utils.get_parent_theme_name(name)
+ if parent_name is None:
+ break
+ try:
+ utils.get_theme_path(parent_name)
+ break
+ except: # Not available
+ self.do_install(parent_name, data)
+ name = parent_name
+
+ def do_install(self, name, data):
+ if name in data:
+ utils.makedirs(self.output_dir)
+ LOGGER.notice('Downloading: ' + data[name])
+ zip_file = BytesIO()
+ zip_file.write(requests.get(data[name]).content)
+ LOGGER.notice('Extracting: {0} into themes'.format(name))
+ utils.extract_all(zip_file)
+ dest_path = os.path.join('themes', name)
+ else:
+ try:
+ theme_path = utils.get_theme_path(name)
+ except:
+ LOGGER.error("Can't find theme " + name)
+ return False
+
+ utils.makedirs(self.output_dir)
+ dest_path = os.path.join(self.output_dir, name)
+ if os.path.exists(dest_path):
+ LOGGER.error("{0} is already installed".format(name))
+ return False
+
+ LOGGER.notice('Copying {0} into themes'.format(theme_path))
+ shutil.copytree(theme_path, dest_path)
+ confpypath = os.path.join(dest_path, 'conf.py.sample')
+ if os.path.exists(confpypath):
+ LOGGER.notice('This plugin has a sample config file.')
+ print('Contents of the conf.py.sample file:\n')
+ with codecs.open(confpypath, 'rb', 'utf-8') as fh:
+ print(indent(pygments.highlight(
+ fh.read(), PythonLexer(), TerminalFormatter()), 4 * ' '))
+ return True
diff --git a/nikola/plugins/command/mincss.plugin b/nikola/plugins/command/mincss.plugin
new file mode 100644
index 0000000..d394d06
--- /dev/null
+++ b/nikola/plugins/command/mincss.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = mincss
+Module = mincss
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Apply mincss to the generated site
+
diff --git a/nikola/plugins/command/mincss.py b/nikola/plugins/command/mincss.py
new file mode 100644
index 0000000..5c9a7cb
--- /dev/null
+++ b/nikola/plugins/command/mincss.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function, unicode_literals
+import os
+import sys
+
+try:
+ from mincss.processor import Processor
+except ImportError:
+ Processor = None
+
+from nikola.plugin_categories import Command
+from nikola.utils import req_missing, get_logger, STDERR_HANDLER
+
+
+class CommandMincss(Command):
+ """Check the generated site."""
+ name = "mincss"
+
+ doc_usage = ""
+ doc_purpose = "apply mincss to the generated site"
+
+ logger = get_logger('mincss', STDERR_HANDLER)
+
+ def _execute(self, options, args):
+ """Apply mincss the generated site."""
+ output_folder = self.site.config['OUTPUT_FOLDER']
+ if Processor is None:
+ req_missing(['mincss'], 'use the "mincss" command')
+ return
+
+ p = Processor(preserve_remote_urls=False)
+ urls = []
+ css_files = {}
+ for root, dirs, files in os.walk(output_folder):
+ for f in files:
+ url = os.path.join(root, f)
+ if url.endswith('.css'):
+ fname = os.path.basename(url)
+ if fname in css_files:
+ self.logger.error("You have two CSS files with the same name and that confuses me.")
+ sys.exit(1)
+ css_files[fname] = url
+ if not f.endswith('.html'):
+ continue
+ urls.append(url)
+ p.process(*urls)
+ for inline in p.links:
+ fname = os.path.basename(inline.href)
+ with open(css_files[fname], 'wb+') as outf:
+ outf.write(inline.after)
diff --git a/nikola/plugins/command_new_post.plugin b/nikola/plugins/command/new_post.plugin
index 6d70aff..ec35c35 100644
--- a/nikola/plugins/command_new_post.plugin
+++ b/nikola/plugins/command/new_post.plugin
@@ -1,10 +1,10 @@
[Core]
Name = new_post
-Module = command_new_post
+Module = new_post
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create a new post.
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command/new_post.py
index 933a51a..ea0f3de 100644
--- a/nikola/plugins/command_new_post.py
+++ b/nikola/plugins/command/new_post.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,20 +30,24 @@ import datetime
import os
import sys
+from blinker import signal
+
from nikola.plugin_categories import Command
from nikola import utils
+LOGGER = utils.get_logger('new_post', utils.STDERR_HANDLER)
+
-def filter_post_pages(compiler, is_post, post_compilers, post_pages):
+def filter_post_pages(compiler, is_post, compilers, post_pages):
"""Given a compiler ("markdown", "rest"), and whether it's meant for
- a post or a page, and post_compilers, return the correct entry from
+ a post or a page, and compilers, return the correct entry from
post_pages."""
# First throw away all the post_pages with the wrong is_post
filtered = [entry for entry in post_pages if entry[3] == is_post]
# These are the extensions supported by the required format
- extensions = post_compilers[compiler]
+ extensions = compilers[compiler]
# Throw away the post_pages with the wrong extensions
filtered = [entry for entry in filtered if any([ext in entry[0] for ext in
@@ -51,13 +57,13 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages):
type_name = "post" if is_post else "page"
raise Exception("Can't find a way, using your configuration, to create "
"a {0} in format {1}. You may want to tweak "
- "post_compilers or post_pages in conf.py".format(
+ "COMPILERS or POSTS/PAGES in conf.py".format(
type_name, compiler))
return filtered[0]
-def get_default_compiler(is_post, post_compilers, post_pages):
- """Given post_compilers and post_pages, return a reasonable
+def get_default_compiler(is_post, compilers, post_pages):
+ """Given compilers and post_pages, return a reasonable
default compiler for this kind of post/page.
"""
@@ -67,19 +73,60 @@ def get_default_compiler(is_post, post_compilers, post_pages):
# Get extensions in filtered post_pages until one matches a compiler
for entry in filtered:
extension = os.path.splitext(entry[0])[-1]
- for compiler, extensions in post_compilers.items():
+ for compiler, extensions in compilers.items():
if extension in extensions:
return compiler
# No idea, back to default behaviour
return 'rest'
+def get_date(schedule=False, rule=None, last_date=None, force_today=False):
+ """Returns a date stamp, given a recurrence rule.
+
+ schedule - bool:
+ whether to use the recurrence rule or not
+
+ rule - str:
+ an iCal RRULE string that specifies the rule for scheduling posts
+
+ last_date - datetime:
+ timestamp of the last post
+
+ force_today - bool:
+ tries to schedule a post to today, if possible, even if the scheduled
+ time has already passed in the day.
+ """
+
+ date = now = datetime.datetime.now()
+ if schedule:
+ try:
+ from dateutil import rrule
+ except ImportError:
+ LOGGER.error('To use the --schedule switch of new_post, '
+ 'you have to install the "dateutil" package.')
+ rrule = None
+ if schedule and rrule and rule:
+ if last_date and last_date.tzinfo:
+ # strip tzinfo for comparisons
+ last_date = last_date.replace(tzinfo=None)
+ try:
+ rule_ = rrule.rrulestr(rule, dtstart=last_date)
+ except Exception:
+ LOGGER.error('Unable to parse rule string, using current time.')
+ else:
+ # Try to post today, instead of tomorrow, if no other post today.
+ if force_today:
+ now = now.replace(hour=0, minute=0, second=0, microsecond=0)
+ date = rule_.after(max(now, last_date or now), last_date is None)
+ return date.strftime('%Y/%m/%d %H:%M:%S')
+
+
class CommandNewPost(Command):
"""Create a new post."""
name = "new_post"
doc_usage = "[options] [path]"
- doc_purpose = "Create a new blog post or site page."
+ doc_purpose = "create a new blog post or site page"
cmd_options = [
{
'name': 'is_page',
@@ -126,12 +173,19 @@ class CommandNewPost(Command):
'default': '',
'help': 'Markup format for post, one of rest, markdown, wiki, '
'bbcode, html, textile, txt2tags',
- }
+ },
+ {
+ 'name': 'schedule',
+ 'short': 's',
+ 'type': bool,
+ 'default': False,
+ 'help': 'Schedule post based on recurrence rule'
+ },
+
]
def _execute(self, options, args):
"""Create a new post or page."""
-
compiler_names = [p.name for p in
self.site.plugin_manager.getPluginsOfCategory(
"PageCompiler")]
@@ -161,18 +215,18 @@ class CommandNewPost(Command):
if not post_format: # Issue #400
post_format = get_default_compiler(
is_post,
- self.site.config['post_compilers'],
+ self.site.config['COMPILERS'],
self.site.config['post_pages'])
if post_format not in compiler_names:
- print("ERROR: Unknown post format " + post_format)
+ LOGGER.error("Unknown post format " + post_format)
return
compiler_plugin = self.site.plugin_manager.getPluginByName(
post_format, "PageCompiler").plugin_object
# Guess where we should put this
entry = filter_post_pages(post_format, is_post,
- self.site.config['post_compilers'],
+ self.site.config['COMPILERS'],
self.site.config['post_pages'])
print("Creating New Post")
@@ -193,7 +247,14 @@ class CommandNewPost(Command):
if isinstance(path, utils.bytes_str):
path = path.decode(sys.stdin.encoding)
slug = utils.slugify(os.path.splitext(os.path.basename(path))[0])
- date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
+ # Calculate the date to use for the post
+ schedule = options['schedule'] or self.site.config['SCHEDULE_ALL']
+ rule = self.site.config['SCHEDULE_RULE']
+ force_today = self.site.config['SCHEDULE_FORCE_TODAY']
+ self.site.scan_posts()
+ timeline = self.site.timeline
+ last_date = None if not timeline else timeline[0].date
+ date = get_date(schedule, rule, last_date, force_today)
data = [title, slug, date, tags]
output_path = os.path.dirname(entry[0])
meta_path = os.path.join(output_path, slug + ".meta")
@@ -206,20 +267,25 @@ class CommandNewPost(Command):
if (not onefile and os.path.isfile(meta_path)) or \
os.path.isfile(txt_path):
- print("The title already exists!")
+ LOGGER.error("The title already exists!")
exit()
d_name = os.path.dirname(txt_path)
- if not os.path.exists(d_name):
- os.makedirs(d_name)
+ utils.makedirs(d_name)
+ metadata = self.site.config['ADDITIONAL_METADATA']
compiler_plugin.create_post(
txt_path, onefile, title=title,
- slug=slug, date=date, tags=tags)
+ slug=slug, date=date, tags=tags, **metadata)
+
+ event = dict(path=txt_path)
if not onefile: # write metadata file
with codecs.open(meta_path, "wb+", "utf8") as fd:
fd.write('\n'.join(data))
with codecs.open(txt_path, "wb+", "utf8") as fd:
fd.write("Write your post here.")
- print("Your post's metadata is at: ", meta_path)
- print("Your post's text is at: ", txt_path)
+ LOGGER.notice("Your post's metadata is at: {0}".format(meta_path))
+ event['meta_path'] = meta_path
+ LOGGER.notice("Your post's text is at: {0}".format(txt_path))
+
+ signal('new_post').send(self, **event)
diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command/planetoid.plugin
index 8636d49..e767f31 100644
--- a/nikola/plugins/command_planetoid.plugin
+++ b/nikola/plugins/command/planetoid.plugin
@@ -1,9 +1,9 @@
[Core]
Name = planetoid
-Module = command_planetoid
+Module = planetoid
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Maintain a planet-like site
diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command/planetoid/__init__.py
index 183dd51..369862b 100644
--- a/nikola/plugins/command_planetoid/__init__.py
+++ b/nikola/plugins/command/planetoid/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -33,7 +34,9 @@ import sys
from doit.tools import timeout
from nikola.plugin_categories import Command, Task
-from nikola.utils import config_changed
+from nikola.utils import config_changed, req_missing, get_logger, STDERR_HANDLER
+
+LOGGER = get_logger('planetoid', STDERR_HANDLER)
try:
import feedparser
@@ -75,10 +78,10 @@ class Planetoid(Command, Task):
def gen_tasks(self):
if peewee is None or sys.version_info[0] == 3:
if sys.version_info[0] == 3:
- message = 'Peewee is currently incompatible with Python 3.'
+ message = 'Peewee, a requirement of the "planetoid" command, is currently incompatible with Python 3.'
else:
- message = 'You need to install the \"peewee\" module.'
-
+ req_missing('peewee', 'use the "planetoid" command')
+ message = ''
yield {
'basename': self.name,
'name': '',
@@ -159,12 +162,12 @@ class Planetoid(Command, Task):
# TODO: log failure
return
if parsed.feed.get('title'):
- print(parsed.feed.title)
+ LOGGER.notice(parsed.feed.title)
else:
- print(feed.url)
+ LOGGER.notice(feed.url)
feed.etag = parsed.get('etag', 'foo')
modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6]
- print("==========>", modified)
+ LOGGER.notice("==========>", modified)
modified = datetime.datetime(*modified)
feed.last_modified = modified
feed.save()
@@ -173,15 +176,14 @@ class Planetoid(Command, Task):
# TODO log failure
return
for entry_data in parsed.entries:
- print("=========================================")
+ LOGGER.notice("=========================================")
date = entry_data.get('published_parsed', None)
if date is None:
date = entry_data.get('updated_parsed', None)
if date is None:
- print("Can't parse date from:")
- print(entry_data)
+ LOGGER.error("Can't parse date from:\n", entry_data)
return False
- print("DATE:===>", date)
+ LOGGER.notice("DATE:===>", date)
date = datetime.datetime(*(date[:6]))
title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título'))
content = entry_data.get('content', None)
@@ -193,9 +195,9 @@ class Planetoid(Command, Task):
content = entry_data.get('summary', 'Sin contenido')
guid = str(entry_data.get('guid', entry_data.link))
link = entry_data.link
- print(repr([date, title]))
+ LOGGER.notice(repr([date, title]))
e = list(Entry.select().where(Entry.guid == guid))
- print(
+ LOGGER.notice(
repr(dict(
date=date,
title=title,
diff --git a/nikola/plugins/command_serve.plugin b/nikola/plugins/command/serve.plugin
index 684935d..e663cc6 100644
--- a/nikola/plugins/command_serve.plugin
+++ b/nikola/plugins/command/serve.plugin
@@ -1,10 +1,10 @@
[Core]
Name = serve
-Module = command_serve
+Module = serve
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Start test server.
diff --git a/nikola/plugins/command/serve.py b/nikola/plugins/command/serve.py
new file mode 100644
index 0000000..07403d4
--- /dev/null
+++ b/nikola/plugins/command/serve.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function
+import os
+try:
+ from BaseHTTPServer import HTTPServer
+ from SimpleHTTPServer import SimpleHTTPRequestHandler
+except ImportError:
+ from http.server import HTTPServer # NOQA
+ from http.server import SimpleHTTPRequestHandler # NOQA
+
+from nikola.plugin_categories import Command
+from nikola.utils import get_logger
+
+
+class CommandBuild(Command):
+ """Start test server."""
+
+ name = "serve"
+ doc_usage = "[options]"
+ doc_purpose = "start the test webserver"
+ logger = None
+
+ cmd_options = (
+ {
+ 'name': 'port',
+ 'short': 'p',
+ 'long': 'port',
+ 'default': 8000,
+ 'type': int,
+ 'help': 'Port nummber (default: 8000)',
+ },
+ {
+ 'name': 'address',
+ 'short': 'a',
+ 'long': '--address',
+ 'type': str,
+ 'default': '127.0.0.1',
+ 'help': 'Address to bind (default: 127.0.0.1)',
+ },
+ )
+
+ def _execute(self, options, args):
+ """Start test server."""
+ self.logger = get_logger('serve', self.site.loghandlers)
+ out_dir = self.site.config['OUTPUT_FOLDER']
+ if not os.path.isdir(out_dir):
+ self.logger.error("Missing '{0}' folder?".format(out_dir))
+ else:
+ os.chdir(out_dir)
+ httpd = HTTPServer((options['address'], options['port']),
+ OurHTTPRequestHandler)
+ sa = httpd.socket.getsockname()
+ self.logger.notice("Serving HTTP on {0} port {1} ...".format(*sa))
+ httpd.serve_forever()
+
+
+class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
+ extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
+ extensions_map[""] = "text/plain"
+
+ # NOTICE: this is a patched version of send_head() to disable all sorts of
+ # caching. `nikola serve` is a development server, hence caching should
+ # not happen to have access to the newest resources.
+ #
+ # The original code was copy-pasted from Python 2.7. Python 3.3 contains
+ # the same code, missing the binary mode comment.
+ #
+ # 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.
+
+ This sends the response code and MIME headers.
+
+ Return value is either a file object (which has to be copied
+ to the outputfile by the caller unless the command was HEAD,
+ and must be closed by the caller under all circumstances), or
+ None, in which case the caller has nothing further to do.
+
+ """
+ path = self.translate_path(self.path)
+ f = None
+ if os.path.isdir(path):
+ if not self.path.endswith('/'):
+ # redirect browser - doing basically what apache does
+ self.send_response(301)
+ self.send_header("Location", self.path + "/")
+ # begin no-cache patch
+ # For redirects. With redirects, caching is even worse and can
+ # break more. Especially with 301 Moved Permanently redirects,
+ # like this one.
+ self.send_header("Cache-Control", "no-cache, no-store, "
+ "must-revalidate")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ # end no-cache patch
+ self.end_headers()
+ return None
+ for index in "index.html", "index.htm":
+ index = os.path.join(path, index)
+ if os.path.exists(index):
+ path = index
+ break
+ else:
+ return self.list_directory(path)
+ ctype = self.guess_type(path)
+ try:
+ # Always read in binary mode. Opening files in text mode may cause
+ # newline translations, making the actual size of the content
+ # transmitted *less* than the content-length!
+ f = open(path, 'rb')
+ except IOError:
+ self.send_error(404, "File not found")
+ return None
+ self.send_response(200)
+ self.send_header("Content-type", ctype)
+ fs = os.fstat(f.fileno())
+ self.send_header("Content-Length", str(fs[6]))
+ self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
+ # begin no-cache patch
+ # For standard requests.
+ self.send_header("Cache-Control", "no-cache, no-store, "
+ "must-revalidate")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ # end no-cache patch
+ self.end_headers()
+ return f
diff --git a/nikola/plugins/command/version.plugin b/nikola/plugins/command/version.plugin
new file mode 100644
index 0000000..3c1ae95
--- /dev/null
+++ b/nikola/plugins/command/version.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = version
+Module = version
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.2
+Website = http://getnikola.com
+Description = Show nikola version
diff --git a/nikola/plugins/compile_rest/dummy.py b/nikola/plugins/command/version.py
index 39543fd..65896e9 100644
--- a/nikola/plugins/compile_rest/dummy.py
+++ b/nikola/plugins/command/version.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,22 +24,21 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""A stupid codeblock replacement for neanderthals and users of Debian Sid."""
-
-from __future__ import unicode_literals
+from __future__ import print_function
-from docutils import nodes
-from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import Command
+from nikola import __version__
-CODE = '<pre>{0}</pre>'
+class CommandVersion(Command):
+ """Print the version."""
-class CodeBlock(Directive):
- required_arguments = 1
- has_content = True
+ name = "version"
- def run(self):
- """ Required by the Directive interface. Create docutils nodes """
- return [nodes.raw('', CODE.format('\n'.join(self.content)), format='html')]
+ doc_usage = ""
+ needs_config = False
+ doc_purpose = "print the Nikola version number"
-directives.register_directive('code', CodeBlock)
+ def _execute(self, options={}, args=None):
+ """Print the version number."""
+ print("Nikola version " + __version__)
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
deleted file mode 100644
index ea82703..0000000
--- a/nikola/plugins/command_check.py
+++ /dev/null
@@ -1,166 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function
-import os
-import sys
-try:
- from urllib import unquote
- from urlparse import urlparse
-except ImportError:
- from urllib.parse import unquote, urlparse # NOQA
-
-import lxml.html
-
-from nikola.plugin_categories import Command
-
-
-class CommandCheck(Command):
- """Check the generated site."""
-
- name = "check"
-
- doc_usage = "-l [--find-sources] | -f"
- doc_purpose = "Check links and files in the generated site."
- cmd_options = [
- {
- 'name': 'links',
- 'short': 'l',
- 'long': 'check-links',
- 'type': bool,
- 'default': False,
- 'help': 'Check for dangling links',
- },
- {
- 'name': 'files',
- 'short': 'f',
- 'long': 'check-files',
- 'type': bool,
- 'default': False,
- 'help': 'Check for unknown files',
- },
- {
- 'name': 'find_sources',
- 'long': 'find-sources',
- 'type': bool,
- 'default': False,
- 'help': 'List possible source files for files with broken links.',
- },
- ]
-
- def _execute(self, options, args):
- """Check the generated site."""
- if not options['links'] and not options['files']:
- print(self.help())
- return False
- if options['links']:
- failure = scan_links(options['find_sources'])
- if options['files']:
- failure = scan_files()
- if failure:
- sys.exit(1)
-
-existing_targets = set([])
-
-
-def analize(task, find_sources=False):
- rv = False
- try:
- filename = task.split(":")[-1]
- d = lxml.html.fromstring(open(filename).read())
- for l in d.iterlinks():
- target = l[0].attrib[l[1]]
- if target == "#":
- continue
- parsed = urlparse(target)
- if parsed.scheme:
- continue
- if parsed.fragment:
- target = target.split('#')[0]
- target_filename = os.path.abspath(
- os.path.join(os.path.dirname(filename), unquote(target)))
- if target_filename not in existing_targets:
- if os.path.exists(target_filename):
- existing_targets.add(target_filename)
- else:
- rv = True
- print("Broken link in {0}: ".format(filename), target)
- if find_sources:
- print("Possible sources:")
- print(os.popen('nikola list --deps ' + task,
- 'r').read())
- print("===============================\n")
-
- except Exception as exc:
- print("Error with:", filename, exc)
- return rv
-
-
-def scan_links(find_sources=False):
- print("Checking Links:\n===============\n")
- failure = False
- for task in os.popen('nikola list --all', 'r').readlines():
- task = task.strip()
- if task.split(':')[0] in ('render_tags', 'render_archive',
- 'render_galleries', 'render_indexes',
- 'render_pages'
- 'render_site') and '.html' in task:
- if analize(task, find_sources):
- failure = True
- return failure
-
-
-def scan_files():
- print("Checking Files:\n===============\n")
- task_fnames = set([])
- real_fnames = set([])
- # First check that all targets are generated in the right places
- failure = False
- for task in os.popen('nikola list --all', 'r').readlines():
- task = task.strip()
- if 'output' in task and ':' in task:
- fname = task.split(':')[-1]
- task_fnames.add(fname)
- # And now check that there are no non-target files
- for root, dirs, files in os.walk('output'):
- for src_name in files:
- fname = os.path.join(root, src_name)
- real_fnames.add(fname)
-
- only_on_output = list(real_fnames - task_fnames)
- if only_on_output:
- only_on_output.sort()
- print("\nFiles from unknown origins:\n")
- for f in only_on_output:
- print(f)
- failure = True
-
- only_on_input = list(task_fnames - real_fnames)
- if only_on_input:
- only_on_input.sort()
- print("\nFiles not generated:\n")
- for f in only_on_input:
- print(f)
-
- return failure
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
deleted file mode 100644
index 3277567..0000000
--- a/nikola/plugins/command_deploy.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function
-from ast import literal_eval
-import codecs
-from datetime import datetime
-import os
-import subprocess
-
-
-from nikola.plugin_categories import Command
-
-
-class Deploy(Command):
- """Deploy site. """
- name = "deploy"
-
- doc_usage = ""
- doc_purpose = "Deploy the site."
-
- def _execute(self, command, args):
- for command in self.site.config['DEPLOY_COMMANDS']:
-
- # Get last succesful deploy date
- timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
- try:
- with open(timestamp_path, 'rb') as inf:
- last_deploy = literal_eval(inf.read().strip())
- except Exception:
- last_deploy = datetime(1970, 1, 1) # NOQA
-
- print("==>", command)
- ret = subprocess.check_call(command, shell=True)
- if ret != 0: # failed deployment
- raise Exception("Failed deployment")
- print("Successful deployment")
- new_deploy = datetime.now()
- # Store timestamp of successful deployment
- with codecs.open(timestamp_path, 'wb+', 'utf8') as outf:
- outf.write(repr(new_deploy))
- # Here is where we would do things with whatever is
- # on self.site.timeline and is newer than
- # last_deploy
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
deleted file mode 100644
index 2a0a0cc..0000000
--- a/nikola/plugins/command_install_theme.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function
-import os
-import json
-from io import BytesIO
-
-try:
- import requests
-except ImportError:
- requests = None # NOQA
-
-from nikola.plugin_categories import Command
-from nikola import utils
-
-
-class CommandInstallTheme(Command):
- """Start test server."""
-
- name = "install_theme"
- doc_usage = "[[-u] theme_name] | [[-u] -l]"
- doc_purpose = "Install theme into current site."
- 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: "
- "http://nikola.ralsina.com.ar/themes/index.json)",
- 'default': 'http://nikola.ralsina.com.ar/themes/index.json'
- },
- ]
-
- def _execute(self, options, args):
- """Install theme into current site."""
- if requests is None:
- print('This command requires the requests package be installed.')
- return False
-
- listing = options['list']
- url = options['url']
- if args:
- name = args[0]
- else:
- name = None
-
- if name is None and not listing:
- print("This command needs either a theme name or the -l option.")
- return False
- data = requests.get(url).text
- data = json.loads(data)
- if listing:
- print("Themes:")
- print("-------")
- for theme in sorted(data.keys()):
- print(theme)
- return True
- else:
- if name in data:
- if os.path.isfile("themes"):
- raise IOError("'themes' isn't a directory!")
- elif not os.path.isdir("themes"):
- try:
- os.makedirs("themes")
- except:
- raise OSError("mkdir 'theme' error!")
- print('Downloading: ' + data[name])
- zip_file = BytesIO()
- zip_file.write(requests.get(data[name]).content)
- print('Extracting: {0} into themes'.format(name))
- utils.extract_all(zip_file)
- else:
- print("Can't find theme " + name)
- return False
diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py
deleted file mode 100644
index 64efe7d..0000000
--- a/nikola/plugins/command_serve.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function
-import os
-try:
- from BaseHTTPServer import HTTPServer
- from SimpleHTTPServer import SimpleHTTPRequestHandler
-except ImportError:
- from http.server import HTTPServer # NOQA
- from http.server import SimpleHTTPRequestHandler # NOQA
-
-from nikola.plugin_categories import Command
-
-
-class CommandBuild(Command):
- """Start test server."""
-
- name = "serve"
- doc_usage = "[options]"
- doc_purpose = "Start the test webserver."
-
- cmd_options = (
- {
- 'name': 'port',
- 'short': 'p',
- 'long': 'port',
- 'default': 8000,
- 'type': int,
- 'help': 'Port nummber (default: 8000)',
- },
- {
- 'name': 'address',
- 'short': 'a',
- 'long': '--address',
- 'type': str,
- 'default': '127.0.0.1',
- 'help': 'Address to bind (default: 127.0.0.1)',
- },
- )
-
- def _execute(self, options, args):
- """Start test server."""
- out_dir = self.site.config['OUTPUT_FOLDER']
- if not os.path.isdir(out_dir):
- print("Error: Missing '{0}' folder?".format(out_dir))
- else:
- os.chdir(out_dir)
- httpd = HTTPServer((options['address'], options['port']),
- OurHTTPRequestHandler)
- sa = httpd.socket.getsockname()
- print("Serving HTTP on", sa[0], "port", sa[1], "...")
- httpd.serve_forever()
-
-
-class OurHTTPRequestHandler(SimpleHTTPRequestHandler):
- extensions_map = dict(SimpleHTTPRequestHandler.extensions_map)
- extensions_map[""] = "text/plain"
diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/compile/__init__.py
diff --git a/nikola/plugins/compile/asciidoc.plugin b/nikola/plugins/compile/asciidoc.plugin
new file mode 100644
index 0000000..47c5608
--- /dev/null
+++ b/nikola/plugins/compile/asciidoc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = asciidoc
+Module = asciidoc
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile ASCIIDoc into HTML
+
diff --git a/nikola/plugins/compile/asciidoc.py b/nikola/plugins/compile/asciidoc.py
new file mode 100644
index 0000000..67dfe1a
--- /dev/null
+++ b/nikola/plugins/compile/asciidoc.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on asciidoc.
+
+You will need, of course, to install asciidoc
+
+"""
+
+import codecs
+import os
+import subprocess
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
+
+
+class CompileAsciiDoc(PageCompiler):
+ """Compile asciidoc into HTML."""
+
+ name = "asciidoc"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ try:
+ subprocess.check_call(('asciidoc', '-f', 'html', '-s', '-o', dest, source))
+ except OSError as e:
+ if e.strreror == 'No such file or directory':
+ req_missing(['asciidoc'], 'build this site (compile with asciidoc)', python=False)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write("/////////////////////////////////////////////\n")
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write("/////////////////////////////////////////////\n")
+ fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_bbcode.plugin b/nikola/plugins/compile/bbcode.plugin
index ec3ce2b..b3d9357 100644
--- a/nikola/plugins/compile_bbcode.plugin
+++ b/nikola/plugins/compile/bbcode.plugin
@@ -1,10 +1,10 @@
[Core]
Name = bbcode
-Module = compile_bbcode
+Module = bbcode
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile BBCode into HTML
diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile/bbcode.py
index f8022f3..e998417 100644
--- a/nikola/plugins/compile_bbcode.py
+++ b/nikola/plugins/compile/bbcode.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,7 @@
import codecs
import os
+import re
try:
import bbcode
@@ -33,9 +36,10 @@ except ImportError:
bbcode = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileTextile(PageCompiler):
+class CompileBbcode(PageCompiler):
"""Compile bbcode into HTML."""
name = "bbcode"
@@ -46,17 +50,15 @@ class CompileTextile(PageCompiler):
self.parser = bbcode.Parser()
self.parser.add_simple_formatter("note", "")
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if bbcode is None:
- raise Exception('To build this site, you need to install the '
- '"bbcode" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['bbcode'], 'build this site (compile BBCode)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = self.parser.format(data)
out_file.write(output)
@@ -64,9 +66,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('[note]<!--\n')
diff --git a/nikola/plugins/compile_html.plugin b/nikola/plugins/compile/html.plugin
index f6cdfbc..21dd338 100644
--- a/nikola/plugins/compile_html.plugin
+++ b/nikola/plugins/compile/html.plugin
@@ -1,10 +1,10 @@
[Core]
Name = html
-Module = compile_html
+Module = html
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile HTML into HTML (just copy)
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile/html.py
index 7551b33..a309960 100644
--- a/nikola/plugins/compile_html.py
+++ b/nikola/plugins/compile/html.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,13 +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 based on markdown."""
+"""Implementation of compile_html for HTML source files."""
import os
import shutil
import codecs
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs
class CompileHtml(PageCompiler):
@@ -36,24 +39,20 @@ class CompileHtml(PageCompiler):
name = "html"
- def compile_html(self, source, dest):
- try:
- os.makedirs(os.path.dirname(dest))
- except Exception:
- pass
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
shutil.copyfile(source, dest)
+ return True
def create_post(self, path, onefile=False, **kw):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
- for k, v in metadata.keys():
+ for k, v in metadata.items():
fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->\n\n')
fd.write("\n<p>Write your post here.</p>")
diff --git a/nikola/plugins/compile_ipynb.plugin b/nikola/plugins/compile/ipynb.plugin
index 51051e0..3d15bb0 100644
--- a/nikola/plugins/compile_ipynb.plugin
+++ b/nikola/plugins/compile/ipynb.plugin
@@ -1,10 +1,10 @@
[Core]
Name = ipynb
-Module = compile_ipynb
+Module = ipynb
[Documentation]
Author = Damián Avila
-Version = 0.1
+Version = 1.0
Website = http://www.oquanta.info
Description = Compile IPython notebooks into HTML
diff --git a/nikola/plugins/compile/ipynb/README.txt b/nikola/plugins/compile/ipynb/README.txt
new file mode 100644
index 0000000..0a7d6db
--- /dev/null
+++ b/nikola/plugins/compile/ipynb/README.txt
@@ -0,0 +1,44 @@
+To make this work...
+
+1- You can install the "jinja-site-ipython" theme using this command:
+
+$ nikola install_theme -n jinja-site-ipython
+
+(or xkcd-site-ipython, if you want xkcd styling)
+
+More info here about themes:
+http://getnikola.com/handbook.html#getting-more-themes
+
+OR
+
+You can to download the "jinja-site-ipython" theme from here:
+https://github.com/damianavila/jinja-site-ipython-theme-for-Nikola
+and copy the "site-ipython" folder inside the "themes" folder of your site.
+
+
+2- Then, just add:
+
+post_pages = (
+ ("posts/*.ipynb", "posts", "post.tmpl", True),
+ ("stories/*.ipynb", "stories", "story.tmpl", False),
+)
+
+and
+
+THEME = 'jinja-site-ipython' (or 'xkcd-site-ipython', if you want xkcd styling)
+
+to your conf.py.
+Finally... to use it:
+
+$nikola new_page -f ipynb
+
+**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
+create one metadata file and the corresponding naive IPython notebook.
+
+$nikola build
+
+And deploy the output folder... to see it locally: $nikola serve
+If you have any doubts, just ask: @damianavila
+
+Cheers.
+Damián
diff --git a/nikola/plugins/compile_ipynb/__init__.py b/nikola/plugins/compile/ipynb/__init__.py
index d38f6f2..7c318ca 100644
--- a/nikola/plugins/compile_ipynb/__init__.py
+++ b/nikola/plugins/compile/ipynb/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2013 Damian Avila.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013 Damian Avila.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,13 +31,15 @@ import codecs
import os
try:
- from .nbformat import current as nbformat
- from .nbconvert.converters import bloggerhtml as nbconverter
- bloggerhtml = True
+ from IPython.nbconvert.exporters import HTMLExporter
+ from IPython.nbformat import current as nbformat
+ from IPython.config import Config
+ flag = True
except ImportError:
- bloggerhtml = None
+ flag = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileIPynb(PageCompiler):
@@ -43,30 +47,26 @@ class CompileIPynb(PageCompiler):
name = "ipynb"
- def compile_html(self, source, dest):
- if bloggerhtml is None:
- raise Exception('To build this site, you also need '
- 'https://github.com/damianavila/com'
- 'pile_ipynb-for-Nikola.git.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- converter = nbconverter.ConverterBloggerHTML()
+ def compile_html(self, source, dest, is_two_file=True):
+ if flag is None:
+ req_missing(['ipython>=1.0.0'], 'build this site (compile ipynb)')
+ makedirs(os.path.dirname(dest))
+ HTMLExporter.default_template = 'basic'
+ c = Config(self.site.config['IPYNB_CONFIG'])
+ exportHtml = HTMLExporter(config=c)
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
- converter.nb = nbformat.reads_json(data)
- output = converter.convert()
- out_file.write(output)
+ nb = in_file.read()
+ nb_json = nbformat.reads_json(nb)
+ (body, resources) = exportHtml.from_notebook_node(nb_json)
+ out_file.write(body)
def create_post(self, path, onefile=False, **kw):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
meta_path = os.path.join(d_name, kw['slug'] + ".meta")
with codecs.open(meta_path, "wb+", "utf8") as fd:
if onefile:
@@ -78,7 +78,7 @@ class CompileIPynb(PageCompiler):
with codecs.open(path, "wb+", "utf8") as fd:
fd.write("""{
"metadata": {
- "name": "%s"
+ "name": ""
},
"nbformat": 3,
"nbformat_minor": 0,
@@ -97,4 +97,4 @@ class CompileIPynb(PageCompiler):
"metadata": {}
}
]
-}""" % kw['slug'])
+}""")
diff --git a/nikola/plugins/compile_markdown.plugin b/nikola/plugins/compile/markdown.plugin
index f3e119b..157579a 100644
--- a/nikola/plugins/compile_markdown.plugin
+++ b/nikola/plugins/compile/markdown.plugin
@@ -1,10 +1,10 @@
[Core]
Name = markdown
-Module = compile_markdown
+Module = markdown
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Markdown into HTML
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py
index ae700e6..b41c6b5 100644
--- a/nikola/plugins/compile_markdown/__init__.py
+++ b/nikola/plugins/compile/markdown/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -28,17 +30,18 @@ from __future__ import unicode_literals
import codecs
import os
+import re
try:
from markdown import markdown
- from nikola.plugins.compile_markdown.mdx_nikola import NikolaExtension
+ from nikola.plugins.compile.markdown.mdx_nikola import NikolaExtension
nikola_extension = NikolaExtension()
- from nikola.plugins.compile_markdown.mdx_gist import GistExtension
+ from nikola.plugins.compile.markdown.mdx_gist import GistExtension
gist_extension = GistExtension()
- from nikola.plugins.compile_markdown.mdx_podcast import PodcastExtension
+ from nikola.plugins.compile.markdown.mdx_podcast import PodcastExtension
podcast_extension = PodcastExtension()
except ImportError:
@@ -48,27 +51,26 @@ except ImportError:
podcast_extension = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileMarkdown(PageCompiler):
"""Compile markdown into HTML."""
name = "markdown"
+ extensions = [gist_extension, nikola_extension, podcast_extension]
+ site = None
- extensions = ['fenced_code', 'codehilite', gist_extension,
- nikola_extension, podcast_extension]
-
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if markdown is None:
- raise Exception('To build this site, you need to install the '
- '"markdown" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['markdown'], 'build this site (compile Markdown)')
+ makedirs(os.path.dirname(dest))
+ self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS")
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = markdown(data, self.extensions)
out_file.write(output)
@@ -76,9 +78,7 @@ class CompileMarkdown(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
diff --git a/nikola/plugins/compile_markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py
index 808e383..3c3bef9 100644
--- a/nikola/plugins/compile_markdown/mdx_gist.py
+++ b/nikola/plugins/compile/markdown/mdx_gist.py
@@ -21,12 +21,11 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
+# Warning: URL formats of "raw" gists are undocummented and subject to change.
+# See also: http://developer.github.com/v3/gists/
+#
# Inspired by "[Python] reStructuredText GitHub Gist directive"
# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
-
-from __future__ import print_function
-
-
'''
Extension to Python Markdown for Embedded Gists (gist.github.com)
@@ -83,13 +82,48 @@ Example using reStructuredText syntax:
</noscript>
</div>
</p>
+
+Error Case: non-existent Gist ID:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 0]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/0.js"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/0 --></noscript>
+ </div>
+ </p>
+
+Error Case: non-existent file:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 doesntexist.py]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script>
+ <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.github.com/raw/4747847/doesntexist.py --></noscript>
+ </div>
+ </p>
+
'''
-from __future__ import unicode_literals
-import warnings
+from __future__ import unicode_literals, print_function
from markdown.extensions import Extension
from markdown.inlinepatterns import Pattern
from markdown.util import AtomicString
from markdown.util import etree
+from nikola.utils import get_logger, req_missing, STDERR_HANDLER
+
+LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER)
try:
import requests
@@ -98,13 +132,21 @@ except ImportError:
GIST_JS_URL = "https://gist.github.com/{0}.js"
GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
-GIST_RAW_URL = "https://raw.github.com/gist/{0}"
-GIST_FILE_RAW_URL = "https://raw.github.com/gist/{0}/{1}"
+GIST_RAW_URL = "https://gist.github.com/raw/{0}"
+GIST_FILE_RAW_URL = "https://gist.github.com/raw/{0}/{1}"
-GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\]'
+GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\s*\]'
GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$'
+class GistFetchException(Exception):
+ '''Raised when attempt to fetch content of a Gist from github.com fails.'''
+ def __init__(self, url, status_code):
+ Exception.__init__(self)
+ self.message = 'Received a {0} response from Gist URL: {1}'.format(
+ status_code, url)
+
+
class GistPattern(Pattern):
""" InlinePattern for footnote markers in a document's body text. """
@@ -113,11 +155,21 @@ class GistPattern(Pattern):
def get_raw_gist_with_filename(self, gist_id, filename):
url = GIST_FILE_RAW_URL.format(gist_id, filename)
- return requests.get(url).text
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
def get_raw_gist(self, gist_id):
url = GIST_RAW_URL.format(gist_id)
- return requests.get(url).text
+ resp = requests.get(url)
+
+ if not resp.ok:
+ raise GistFetchException(url, resp.status_code)
+
+ return resp.text
def handleMatch(self, m):
gist_id = m.group('gist_id')
@@ -127,34 +179,32 @@ class GistPattern(Pattern):
gist_elem.set('class', 'gist')
script_elem = etree.SubElement(gist_elem, 'script')
- if gist_file:
- script_elem.set('src', GIST_FILE_JS_URL.format(
- gist_id, gist_file))
-
- else:
- script_elem.set('src', GIST_JS_URL.format(
- gist_id))
-
if requests:
- if gist_file:
- raw_gist = (self.get_raw_gist_with_filename(
- gist_id, gist_file))
- script_elem.set('src', GIST_FILE_JS_URL.format(
- gist_id, gist_file))
-
- else:
- raw_gist = (self.get_raw_gist(gist_id))
- script_elem.set('src', GIST_JS_URL.format(
- gist_id))
-
- # Insert source as <pre/> within <noscript>
noscript_elem = etree.SubElement(gist_elem, 'noscript')
- pre_elem = etree.SubElement(noscript_elem, 'pre')
- pre_elem.text = AtomicString(raw_gist)
+
+ try:
+ if gist_file:
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+ raw_gist = (self.get_raw_gist_with_filename(
+ gist_id, gist_file))
+
+ else:
+ script_elem.set('src', GIST_JS_URL.format(
+ gist_id))
+ raw_gist = (self.get_raw_gist(gist_id))
+
+ # Insert source as <pre/> within <noscript>
+ pre_elem = etree.SubElement(noscript_elem, 'pre')
+ pre_elem.text = AtomicString(raw_gist)
+
+ except GistFetchException as e:
+ LOGGER.warn(e.message)
+ warning_comment = etree.Comment(' WARNING: {0} '.format(e.message))
+ noscript_elem.append(warning_comment)
else:
- warnings.warn('"requests" package not installed. '
- 'Please install to add inline gist source.')
+ req_missing('requests', 'have inline gist source', optional=True)
return gist_elem
@@ -185,5 +235,7 @@ def makeExtension(configs=None):
if __name__ == '__main__':
import doctest
+
+ # Silence user warnings thrown by tests:
doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile_markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py
index f7a1959..b0ad2f7 100644
--- a/nikola/plugins/compile_markdown/mdx_nikola.py
+++ b/nikola/plugins/compile/markdown/mdx_nikola.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 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_markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py
index be8bb6b..be8bb6b 100644
--- a/nikola/plugins/compile_markdown/mdx_podcast.py
+++ b/nikola/plugins/compile/markdown/mdx_podcast.py
diff --git a/nikola/plugins/compile_misaka.plugin b/nikola/plugins/compile/misaka.plugin
index 1b9c8a8..fef6d71 100644
--- a/nikola/plugins/compile_misaka.plugin
+++ b/nikola/plugins/compile/misaka.plugin
@@ -1,6 +1,6 @@
[Core]
Name = misaka
-Module = compile_misaka
+Module = misaka
[Documentation]
Author = Chris Lee
diff --git a/nikola/plugins/compile_misaka/__init__.py b/nikola/plugins/compile/misaka.py
index a3f687e..3733a85 100644
--- a/nikola/plugins/compile_misaka/__init__.py
+++ b/nikola/plugins/compile/misaka.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
# Copyright (c) 2013 Chris Lee
# Permission is hereby granted, free of charge, to any
@@ -28,10 +30,10 @@ from __future__ import unicode_literals
import codecs
import os
+import re
try:
import misaka
-
except ImportError:
misaka = None # NOQA
nikola_extension = None
@@ -39,30 +41,29 @@ except ImportError:
podcast_extension = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileMarkdown(PageCompiler):
- """Compile markdown into HTML."""
+class CompileMisaka(PageCompiler):
+ """Compile Misaka into HTML."""
- name = "markdown"
+ name = "misaka"
def __init__(self, *args, **kwargs):
- super(CompileMarkdown, self).__init__(*args, **kwargs)
+ super(CompileMisaka, self).__init__(*args, **kwargs)
if misaka is not None:
self.ext = misaka.EXT_FENCED_CODE | misaka.EXT_STRIKETHROUGH | \
misaka.EXT_AUTOLINK | misaka.EXT_NO_INTRA_EMPHASIS
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if misaka is None:
- raise Exception('To build this site, you need to install the '
- '"misaka" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['misaka'], 'build this site (compile with misaka)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = misaka.html(data, extensions=self.ext)
out_file.write(output)
@@ -70,9 +71,7 @@ class CompileMarkdown(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin
new file mode 100644
index 0000000..157b694
--- /dev/null
+++ b/nikola/plugins/compile/pandoc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = pandoc
+Module = pandoc
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile markups into HTML using pandoc
+
diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py
new file mode 100644
index 0000000..3a2911f
--- /dev/null
+++ b/nikola/plugins/compile/pandoc.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on pandoc.
+
+You will need, of course, to install pandoc
+
+"""
+
+import codecs
+import os
+import subprocess
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import req_missing, makedirs
+
+
+class CompilePandoc(PageCompiler):
+ """Compile markups into HTML using pandoc."""
+
+ name = "pandoc"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ try:
+ subprocess.check_call(('pandoc', '-o', dest, source))
+ except OSError as e:
+ if e.strreror == 'No such file or directory':
+ req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin
new file mode 100644
index 0000000..ac25259
--- /dev/null
+++ b/nikola/plugins/compile/php.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = php
+Module = php
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Compile PHP into HTML (just copy and name the file .php)
+
diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py
new file mode 100644
index 0000000..44701c8
--- /dev/null
+++ b/nikola/plugins/compile/php.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html for HTML+php."""
+
+from __future__ import unicode_literals
+
+import os
+import shutil
+import codecs
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs
+
+
+class CompilePhp(PageCompiler):
+ """Compile PHP into PHP."""
+
+ name = "php"
+
+ def compile_html(self, source, dest, is_two_file=True):
+ makedirs(os.path.dirname(dest))
+ shutil.copyfile(source, dest)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ os.makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("\n<p>Write your post here.</p>")
+
+ def extension(self):
+ return ".php"
diff --git a/nikola/plugins/compile_rest.plugin b/nikola/plugins/compile/rest.plugin
index 67eb562..55e9c59 100644
--- a/nikola/plugins/compile_rest.plugin
+++ b/nikola/plugins/compile/rest.plugin
@@ -1,10 +1,10 @@
[Core]
Name = rest
-Module = compile_rest
+Module = rest
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile reSt into HTML
diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py
new file mode 100644
index 0000000..c71a5f8
--- /dev/null
+++ b/nikola/plugins/compile/rest/__init__.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+import codecs
+import os
+import re
+
+try:
+ import docutils.core
+ import docutils.nodes
+ import docutils.utils
+ import docutils.io
+ import docutils.readers.standalone
+ has_docutils = True
+except ImportError:
+ has_docutils = False
+
+from nikola.plugin_categories import PageCompiler
+from nikola.utils import get_logger, makedirs, req_missing
+
+
+class CompileRest(PageCompiler):
+ """Compile reSt into HTML."""
+
+ name = "rest"
+ logger = None
+
+ def compile_html(self, source, dest, is_two_file=True):
+ """Compile reSt into HTML."""
+
+ if not has_docutils:
+ req_missing(['docutils'], 'build this site (compile reStructuredText)')
+ makedirs(os.path.dirname(dest))
+ error_level = 100
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ add_ln = 0
+ if not is_two_file:
+ spl = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)
+ data = spl[-1]
+ if len(spl) != 1:
+ # If errors occur, this will be added to the line
+ # number reported by docutils so the line number
+ # matches the actual line number (off by 7 with default
+ # metadata, could be more or less depending on the post
+ # author).
+ add_ln = len(spl[0].splitlines()) + 1
+
+ output, error_level, deps = rst2html(
+ data, settings_overrides={
+ 'initial_header_level': 2,
+ 'record_dependencies': True,
+ 'stylesheet_path': None,
+ 'link_stylesheet': True,
+ 'syntax_highlight': 'short',
+ 'math_output': 'mathjax',
+ }, logger=self.logger, l_source=source, l_add_ln=add_ln)
+ out_file.write(output)
+ deps_path = dest + '.dep'
+ if deps.list:
+ with codecs.open(deps_path, "wb+", "utf8") as deps_file:
+ deps_file.write('\n'.join(deps.list))
+ else:
+ if os.path.isfile(deps_path):
+ os.unlink(deps_path)
+ if error_level < 3:
+ return True
+ else:
+ return False
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write("\nWrite your post here.")
+
+ def set_site(self, site):
+ for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"):
+ if (plugin_info.name in site.config['DISABLED_PLUGINS']
+ or (plugin_info.name in site.EXTRA_PLUGINS and
+ plugin_info.name not in site.config['ENABLED_EXTRAS'])):
+ site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension")
+ continue
+
+ site.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(site)
+ plugin_info.plugin_object.short_help = plugin_info.description
+
+ self.logger = get_logger('compile_rest', site.loghandlers)
+ return super(CompileRest, self).set_site(site)
+
+
+def get_observer(settings):
+ """Return an observer for the docutils Reporter."""
+ def observer(msg):
+ """Report docutils/rest messages to a Nikola user.
+
+ 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 |
+ +------+---------+------+----------+
+ """
+ errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6}
+ text = docutils.nodes.Element.astext(msg)
+ out = '[{source}:{line}] {text}'.format(source=settings['source'], line=msg['line'] + settings['add_ln'], text=text)
+ settings['logger'].log(errormap[msg['level']], out)
+
+ return observer
+
+
+class NikolaReader(docutils.readers.standalone.Reader):
+
+ 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))
+ return document
+
+
+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_source='', l_add_ln=0):
+ """
+ Set up & run a `Publisher`, and return a dictionary of document parts.
+ Dictionary keys are the names of parts, and values are Unicode strings;
+ encoding is up to the client. For programmatic use with string I/O.
+
+ For encoded string input, be sure to set the 'input_encoding' setting to
+ the desired encoding. Set it to 'unicode' for unencoded Unicode string
+ input. Here's how::
+
+ publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
+
+ 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()
+ # 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': l_source,
+ 'add_ln': l_add_ln}
+
+ pub = docutils.core.Publisher(reader, parser, writer, settings=settings,
+ source_class=source_class,
+ destination_class=docutils.io.StringOutput)
+ pub.set_components(None, parser_name, writer_name)
+ pub.process_programmatic_settings(
+ settings_spec, settings_overrides, config_section)
+ pub.set_source(source, source_path)
+ 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
diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin
new file mode 100644
index 0000000..3e27a25
--- /dev/null
+++ b/nikola/plugins/compile/rest/chart.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_chart
+Module = chart
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Chart directive based in PyGal
+
diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py
new file mode 100644
index 0000000..ee917b9
--- /dev/null
+++ b/nikola/plugins/compile/rest/chart.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from ast import literal_eval
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+try:
+ import pygal
+except ImportError:
+ pygal = None # NOQA
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_chart"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('chart', Chart)
+ return super(Plugin, self).set_site(site)
+
+
+class Chart(Directive):
+ """ Restructured text extension for inserting charts as SVG
+
+ Usage:
+ .. chart:: Bar
+ :title: 'Browser usage evolution (in %)'
+ :x_labels: ["2002", "2003", "2004", "2005", "2006", "2007"]
+
+ 'Firefox', [None, None, 0, 16.6, 25, 31]
+ 'Chrome', [None, None, None, None, None, None]
+ 'IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6]
+ 'Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4]
+ """
+
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "copy": directives.unchanged,
+ "css": directives.unchanged,
+ "disable_xml_declaration": directives.unchanged,
+ "dots_size": directives.unchanged,
+ "explicit_size": directives.unchanged,
+ "fill": directives.unchanged,
+ "font_sizes": directives.unchanged,
+ "height": directives.unchanged,
+ "human_readable": directives.unchanged,
+ "include_x_axis": directives.unchanged,
+ "interpolate": directives.unchanged,
+ "interpolation_parameters": directives.unchanged,
+ "interpolation_precision": directives.unchanged,
+ "js": directives.unchanged,
+ "label_font_size": directives.unchanged,
+ "legend_at_bottom": directives.unchanged,
+ "legend_box_size": directives.unchanged,
+ "legend_font_size": directives.unchanged,
+ "logarithmic": directives.unchanged,
+ "major_label_font_size": directives.unchanged,
+ "margin": directives.unchanged,
+ "no_data_font_size": directives.unchanged,
+ "no_data_text": directives.unchanged,
+ "no_prefix": directives.unchanged,
+ "order_min": directives.unchanged,
+ "pretty_print": directives.unchanged,
+ "print_values": directives.unchanged,
+ "print_zeroes": directives.unchanged,
+ "range": directives.unchanged,
+ "rounded_bars": directives.unchanged,
+ "show_dots": directives.unchanged,
+ "show_legend": directives.unchanged,
+ "show_minor_x_labels": directives.unchanged,
+ "show_y_labels": directives.unchanged,
+ "spacing": directives.unchanged,
+ "strict": directives.unchanged,
+ "stroke": directives.unchanged,
+ "style": directives.unchanged,
+ "title": directives.unchanged,
+ "title_font_size": directives.unchanged,
+ "to_dict": directives.unchanged,
+ "tooltip_border_radius": directives.unchanged,
+ "tooltip_font_size": directives.unchanged,
+ "truncate_label": directives.unchanged,
+ "truncate_legend": directives.unchanged,
+ "value_font_size": directives.unchanged,
+ "value_formatter": directives.unchanged,
+ "width": directives.unchanged,
+ "x_label_rotation": directives.unchanged,
+ "x_labels": directives.unchanged,
+ "x_labels_major": directives.unchanged,
+ "x_labels_major_count": directives.unchanged,
+ "x_labels_major_every": directives.unchanged,
+ "x_title": directives.unchanged,
+ "y_label_rotation": directives.unchanged,
+ "y_labels": directives.unchanged,
+ "y_title": directives.unchanged,
+ "zero": directives.unchanged,
+ }
+
+ def run(self):
+ if pygal is None:
+ msg = req_missing(['pygal'], 'use the Chart directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+ options = {}
+ if 'style' in self.options:
+ style_name = self.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 self.options.items():
+ options[k] = literal_eval(v)
+
+ chart = getattr(pygal, self.arguments[0])(style=style)
+ chart.config(**options)
+ for line in self.content:
+ label, series = literal_eval('({0})'.format(line))
+ chart.add(label, series)
+
+ return [nodes.raw('', chart.render().decode('utf8'), format='html')]
diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin
new file mode 100644
index 0000000..1984f52
--- /dev/null
+++ b/nikola/plugins/compile/rest/doc.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_doc
+Module = doc
+
+[Documentation]
+Author = Manuel Kaufmann
+Version = 0.1
+Website = http://getnikola.com
+Description = Role to link another page / post from the blog
+
diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py
new file mode 100644
index 0000000..915a7e1
--- /dev/null
+++ b/nikola/plugins/compile/rest/doc.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+from docutils import nodes
+from docutils.parsers.rst import roles
+
+from nikola.utils import split_explicit_title
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = 'rest_doc'
+
+ def set_site(self, site):
+ self.site = site
+ roles.register_canonical_role('doc', doc_role)
+ doc_role.site = site
+ return super(Plugin, self).set_site(site)
+
+
+def doc_role(name, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+
+ # split link's text and post's slug in role content
+ has_explicit_title, title, slug = split_explicit_title(text)
+
+ # check if the slug given is part of our blog posts/pages
+ twin_slugs = False
+ post = None
+ for p in doc_role.site.timeline:
+ if p.meta('slug') == slug:
+ if post is None:
+ post = p
+ else:
+ twin_slugs = True
+ break
+
+ try:
+ if post is None:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ '"{0}" slug doesn\'t exist.'.format(slug),
+ line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ if not has_explicit_title:
+ # use post's title as link's text
+ title = post.title()
+
+ permalink = post.permalink()
+ if twin_slugs:
+ msg = inliner.reporter.warning(
+ 'More than one post with the same slug. Using "{0}"'.format(permalink))
+
+ node = make_link_node(rawtext, title, permalink, options)
+ return [node], []
+
+
+def make_link_node(rawtext, text, url, options):
+ node = nodes.reference(rawtext, text, refuri=url, *options)
+ return node
diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin
new file mode 100644
index 0000000..8f498ec
--- /dev/null
+++ b/nikola/plugins/compile/rest/gist.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_gist
+Module = gist
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Gist directive
+
diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile/rest/gist.py
index 1506519..e09ed76 100644
--- a/nikola/plugins/compile_rest/gist_directive.py
+++ b/nikola/plugins/compile/rest/gist.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
# This file is public domain according to its author, Brian Hsu
from docutils.parsers.rst import Directive, directives
@@ -8,13 +9,32 @@ try:
except ImportError:
requests = None # NOQA
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_gist"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('gist', GitHubGist)
+ return super(Plugin, self).set_site(site)
+
class GitHubGist(Directive):
""" Embed GitHub Gist.
Usage:
+
.. gist:: GIST_ID
+ or
+
+ .. gist:: GIST_URL
+
+
"""
required_arguments = 1
@@ -24,33 +44,41 @@ class GitHubGist(Directive):
has_content = False
def get_raw_gist_with_filename(self, gistID, filename):
- url = '/'.join(("https://raw.github.com/gist", gistID, filename))
+ url = '/'.join(("https://gist.github.com/raw", gistID, filename))
return requests.get(url).text
def get_raw_gist(self, gistID):
- url = "https://raw.github.com/gist/{0}".format(gistID)
+ url = "https://gist.github.com/raw/{0}".format(gistID)
return requests.get(url).text
def run(self):
- if requests is None:
- print('To use the gist directive, you need to install the '
- '"requests" package.')
- return []
- gistID = self.arguments[0].strip()
+ if 'https://' in self.arguments[0]:
+ gistID = self.arguments[0].split('/')[-1].strip()
+ else:
+ gistID = self.arguments[0].strip()
embedHTML = ""
rawGist = ""
if 'file' in self.options:
filename = self.options['file']
- rawGist = (self.get_raw_gist_with_filename(gistID, filename))
+ if requests is not None:
+ rawGist = (self.get_raw_gist_with_filename(gistID, filename))
embedHTML = ('<script src="https://gist.github.com/{0}.js'
'?file={1}"></script>').format(gistID, filename)
else:
- rawGist = (self.get_raw_gist(gistID))
+ if requests is not None:
+ rawGist = (self.get_raw_gist(gistID))
embedHTML = ('<script src="https://gist.github.com/{0}.js">'
'</script>').format(gistID)
+ if requests is None:
+ reqnode = nodes.raw(
+ '', req_missing('requests', 'have inline gist source',
+ optional=True), format='html')
+ else:
+ reqnode = nodes.literal_block('', rawGist)
+
return [nodes.raw('', embedHTML, format='html'),
nodes.raw('', '<noscript>', format='html'),
- nodes.literal_block('', rawGist),
+ reqnode,
nodes.raw('', '</noscript>', format='html')]
diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin
new file mode 100644
index 0000000..4c9883e
--- /dev/null
+++ b/nikola/plugins/compile/rest/listing.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_listing
+Module = listing
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Extension for source listings
+
diff --git a/nikola/plugins/compile_rest/listing.py b/nikola/plugins/compile/rest/listing.py
index 1b816f5..31975bb 100644
--- a/nikola/plugins/compile_rest/listing.py
+++ b/nikola/plugins/compile/rest/listing.py
@@ -1,121 +1,109 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# 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.
-
-
-""" Define and register a listing directive using the existing CodeBlock """
-
-
-from __future__ import unicode_literals
-from codecs import open as codecs_open # for patching purposes
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
-
-from docutils import core
-from docutils.parsers.rst import directives
-try:
- from docutils.parsers.rst.directives.body import CodeBlock
-except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
- from dummy import CodeBlock # NOQA
-
-import os
-
-
-class Listing(CodeBlock):
- """ listing directive: create a CodeBlock from file
-
- Usage:
-
- .. listing:: nikola.py python
- :number-lines:
-
- """
- has_content = False
- required_arguments = 1
- optional_arguments = 1
-
- option_spec = {
- 'start-at': directives.unchanged,
- 'end-at': directives.unchanged,
- 'start-after': directives.unchanged,
- 'end-before': directives.unchanged,
- }
-
- def run(self):
- fname = self.arguments.pop(0)
- with codecs_open(os.path.join('listings', fname), 'rb+', 'utf8') as fileobject:
- self.content = fileobject.read().splitlines()
- self.trim_content()
- target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = (
- [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
- generated_nodes += self.get_code_from_file(fileobject)
- return generated_nodes
-
- def trim_content(self):
- """Cut the contents based in options."""
- start = 0
- end = len(self.content)
- if 'start-at' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-at'] in l:
- break
- else:
- start = 0
- elif 'start-before' in self.options:
- for start, l in enumerate(self.content):
- if self.options['start-before'] in l:
- if start > 0:
- start -= 1
- break
- else:
- start = 0
- if 'end-at' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-at'] in l:
- break
- else:
- end = len(self.content)
- elif 'end-before' in self.options:
- for end, l in enumerate(self.content):
- if self.options['end-before'] in l:
- end -= 1
- break
- else:
- end = len(self.content)
-
- self.content = self.content[start:end]
-
- def get_code_from_file(self, data):
- """ Create CodeBlock nodes from file object content """
- return super(Listing, self).run()
-
- def assert_has_content(self):
- """ Listing has no content, override check from superclass """
- pass
-
-
-directives.register_directive('listing', Listing)
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 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.
+
+
+""" Define and register a listing directive using the existing CodeBlock """
+
+
+from __future__ import unicode_literals
+from codecs import open as codecs_open # for patching purposes
+import os
+try:
+ from urlparse import urlunsplit
+except ImportError:
+ from urllib.parse import urlunsplit # NOQA
+
+from docutils import core
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst.directives.misc import Include
+try:
+ from docutils.parsers.rst.directives.body import CodeBlock
+except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
+ class CodeBlock(Directive):
+ required_arguments = 1
+ has_content = True
+ CODE = '<pre>{0}</pre>'
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ return [nodes.raw('', self.CODE.format('\n'.join(self.content)), format='html')]
+ directives.register_directive('code', CodeBlock)
+
+
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_listing"
+
+ def set_site(self, site):
+ self.site = site
+ # Even though listings don't use CodeBlock anymore, I am
+ # leaving these to make the code directive work with
+ # docutils < 0.9
+ directives.register_directive('code-block', CodeBlock)
+ directives.register_directive('sourcecode', CodeBlock)
+ directives.register_directive('listing', Listing)
+ return super(Plugin, self).set_site(site)
+
+
+class Listing(Include):
+ """ listing directive: create a highlighted block of code from a file in listings/
+
+ Usage:
+
+ .. listing:: nikola.py python
+ :number-lines:
+
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 1
+
+ def run(self):
+ fname = self.arguments.pop(0)
+ lang = self.arguments.pop(0)
+ fpath = os.path.join('listings', fname)
+ self.arguments.insert(0, fpath)
+ self.options['code'] = lang
+ with codecs_open(fpath, 'rb+', 'utf8') as fileobject:
+ self.content = fileobject.read().splitlines()
+ self.state.document.settings.record_dependencies.add(fpath)
+ target = urlunsplit(("link", 'listing', fname, '', ''))
+ generated_nodes = (
+ [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
+ generated_nodes += self.get_code_from_file(fileobject)
+ return generated_nodes
+
+ def get_code_from_file(self, data):
+ """ Create CodeBlock nodes from file object content """
+ return super(Listing, self).run()
+
+ def assert_has_content(self):
+ """ Listing has no content, override check from superclass """
+ pass
diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin
new file mode 100644
index 0000000..5f5276b
--- /dev/null
+++ b/nikola/plugins/compile/rest/media.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_media
+Module = media
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Directive to support oembed via micawber
+
diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py
new file mode 100644
index 0000000..d1930dd
--- /dev/null
+++ b/nikola/plugins/compile/rest/media.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+try:
+ import micawber
+except ImportError:
+ micawber = None # NOQA
+
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_media"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('media', Media)
+ return super(Plugin, self).set_site(site)
+
+
+class Media(Directive):
+ """ Restructured text extension for inserting any sort of media using micawber."""
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 999
+
+ def run(self):
+ if micawber is None:
+ msg = req_missing(['micawber'], 'use the media directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+
+ providers = micawber.bootstrap_basic()
+ return [nodes.raw('', micawber.parse_text(" ".join(self.arguments), providers), format='html')]
diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin
new file mode 100644
index 0000000..82450a0
--- /dev/null
+++ b/nikola/plugins/compile/rest/post_list.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = rest_post_list
+Module = post_list
+
+[Documentation]
+Author = Udo Spallek
+Version = 0.1
+Website = http://getnikola.com
+Description = Includes a list of posts with tag and slide based filters.
diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py
new file mode 100644
index 0000000..eae4016
--- /dev/null
+++ b/nikola/plugins/compile/rest/post_list.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2013 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.
+from __future__ import unicode_literals
+
+import uuid
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+from nikola import utils
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+ name = "rest_post_list"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('post-list', PostList)
+ PostList.site = site
+ return super(Plugin, self).set_site(site)
+
+
+class PostList(Directive):
+ """
+ Post List
+ =========
+ :Directive Arguments: None.
+ :Directive Options: lang, start, stop, reverse, tags, template, id
+ :Directive Content: None.
+
+ Provides a reStructuredText directive to create a list of posts.
+ The posts appearing in the list can be filtered by options.
+ *List slicing* is provided with the *start*, *stop* and *reverse* options.
+
+ 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.
+
+ ``tags`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``tags``.
+ Defaults to None.
+
+ ``slugs`` : string [, string...]
+ Filter posts to show only posts having at least one of the ``slugs``.
+ Defaults to None.
+
+ ``all`` : flag
+ Shows all posts and pages in the post list.
+ Defaults to show only posts with set *use_in_feeds*.
+
+ ``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.
+ """
+ option_spec = {
+ 'start': int,
+ 'stop': int,
+ 'reverse': directives.flag,
+ 'tags': directives.unchanged,
+ 'slugs': directives.unchanged,
+ 'all': directives.flag,
+ 'lang': directives.unchanged,
+ 'template': directives.path,
+ 'id': directives.unchanged,
+ }
+
+ def run(self):
+ start = self.options.get('start')
+ stop = self.options.get('stop')
+ reverse = self.options.get('reverse', False)
+ tags = self.options.get('tags')
+ tags = [t.strip().lower() for t in tags.split(',')] if tags else []
+ slugs = self.options.get('slugs')
+ slugs = [s.strip() for s in slugs.split(',')] if slugs else []
+ show_all = self.options.get('all', False)
+ lang = self.options.get('lang', utils.LocaleBorg().current_lang)
+ template = self.options.get('template', 'post_list_directive.tmpl')
+ post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex)
+
+ posts = []
+ step = -1 if reverse is None else None
+ if show_all is None:
+ timeline = [p for p in self.site.timeline]
+ else:
+ timeline = [p for p in self.site.timeline if p.use_in_feeds]
+
+ for post in timeline[start:stop:step]:
+ if tags:
+ cont = True
+ for tag in tags:
+ if tag in [t.lower() for t in post.tags]:
+ cont = False
+
+ if cont:
+ continue
+
+ if slugs:
+ cont = True
+ for slug in slugs:
+ if slug == post.meta('slug'):
+ cont = False
+
+ if cont:
+ continue
+
+ posts += [post]
+
+ if not posts:
+ return []
+
+ template_data = {
+ 'lang': lang,
+ 'posts': posts,
+ 'date_format': self.site.GLOBAL_CONTEXT.get('date_format'),
+ 'post_list_id': post_list_id,
+ }
+ output = self.site.template_system.render_template(
+ template, None, template_data)
+ return [nodes.raw('', output, format='html')]
diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin
new file mode 100644
index 0000000..cee4b06
--- /dev/null
+++ b/nikola/plugins/compile/rest/slides.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_slides
+Module = slides
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Slides directive
+
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile/rest/slides.py
index 57fb754..41c3314 100644
--- a/nikola/plugins/compile_rest/slides.py
+++ b/nikola/plugins/compile/rest/slides.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,9 +26,24 @@
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):
+
+ name = "rest_slides"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('slides', Slides)
+ Slides.site = site
+ return super(Plugin, self).set_site(site)
+
class Slides(Directive):
""" Restructured text extension for inserting slideshows."""
@@ -35,31 +52,16 @@ class Slides(Directive):
def run(self):
if len(self.content) == 0:
return
- output = []
- output.append("""
- <div id="myCarousel" class="carousel slide">
- <ol class="carousel-indicators">
- """)
- for i in range(len(self.content)):
- if i == 0:
- classname = 'class="active"'
- else:
- classname = ''
- output.append(' <li data-target="#myCarousel" data-slide-to="{0}" {1}></li>'.format(i, classname))
- output.append("""</ol>
- <div class="carousel-inner">
- """)
- for i, image in enumerate(self.content):
- if i == 0:
- classname = "item active"
- else:
- classname = "item"
- output.append("""<div class="{0}"><img src="{1}" alt="" style="margin: 0 auto 0 auto;"></div>""".format(classname, image))
- output.append("""</div>
- <a class="left carousel-control" href="#myCarousel" data-slide="prev">&lsaquo;</a>
- <a class="right carousel-control" href="#myCarousel" data-slide="next">&rsaquo;</a>
- </div>""")
- return [nodes.raw('', '\n'.join(output), format='html')]
+
+ output = self.site.template_system.render_template(
+ 'slides.tmpl',
+ None,
+ {
+ 'content': self.content,
+ 'carousel_id': 'slides_' + uuid.uuid4().hex,
+ }
+ )
+ 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
new file mode 100644
index 0000000..1d31a8f
--- /dev/null
+++ b/nikola/plugins/compile/rest/soundcloud.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = rest_soundcloud
+Module = soundcloud
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Soundcloud directive
+
diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py
index 6bdd4d5..6fb3e99 100644
--- a/nikola/plugins/compile_rest/soundcloud.py
+++ b/nikola/plugins/compile/rest/soundcloud.py
@@ -1,10 +1,23 @@
-# coding: utf8
+# -*- coding: utf-8 -*-
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_soundcloud"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('soundcloud', SoundCloud)
+ return super(Plugin, self).set_site(site)
+
+
CODE = ("""<iframe width="{width}" height="{height}"
scrolling="no" frameborder="no"
src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/"""
@@ -45,6 +58,3 @@ class SoundCloud(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('soundcloud', SoundCloud)
diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin
new file mode 100644
index 0000000..e0ff3f1
--- /dev/null
+++ b/nikola/plugins/compile/rest/vimeo.plugin
@@ -0,0 +1,7 @@
+[Core]
+Name = rest_vimeo
+Module = vimeo
+
+[Documentation]
+Description = Vimeo directive
+
diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py
index c1dc143..6d66648 100644
--- a/nikola/plugins/compile_rest/vimeo.py
+++ b/nikola/plugins/compile/rest/vimeo.py
@@ -1,5 +1,6 @@
-# coding: utf8
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -31,13 +32,21 @@ try:
import requests
except ImportError:
requests = None # NOQA
-try:
- import json # python 2.6 or higher
-except ImportError:
- try:
- import simplejson as json # NOQA
- except ImportError:
- json = None
+import json
+
+
+from nikola.plugin_categories import RestExtension
+from nikola.utils import req_missing
+
+
+class Plugin(RestExtension):
+
+ name = "rest_vimeo"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('vimeo', Vimeo)
+ return super(Plugin, self).set_site(site)
CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}"
@@ -77,18 +86,19 @@ class Vimeo(Directive):
'height': VIDEO_DEFAULT_HEIGHT,
}
if self.request_size:
- self.check_modules()
+ err = self.check_modules()
+ if err:
+ return err
self.set_video_size()
options.update(self.options)
return [nodes.raw('', CODE.format(**options), format='html')]
def check_modules(self):
+ msg = None
if requests is None:
- raise Exception("To use the Vimeo directive you need to install "
- "the requests module.")
- if json is None:
- raise Exception("To use the Vimeo directive you need python 2.6 "
- "or to install the simplejson module.")
+ msg = req_missing(['requests'], 'use the vimeo directive', optional=True)
+ return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')]
+ return None
def set_video_size(self):
# Only need to make a connection if width and height aren't provided
@@ -113,6 +123,3 @@ class Vimeo(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('vimeo', Vimeo)
diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin
new file mode 100644
index 0000000..01275be
--- /dev/null
+++ b/nikola/plugins/compile/rest/youtube.plugin
@@ -0,0 +1,8 @@
+[Core]
+Name = rest_youtube
+Module = youtube
+
+[Documentation]
+Version = 0.1
+Description = Youtube directive
+
diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile/rest/youtube.py
index 767be32..3d4bdd3 100644
--- a/nikola/plugins/compile_rest/youtube.py
+++ b/nikola/plugins/compile/rest/youtube.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,19 @@ from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+ name = "rest_youtube"
+
+ def set_site(self, site):
+ self.site = site
+ directives.register_directive('youtube', Youtube)
+ return super(Plugin, self).set_site(site)
+
+
CODE = """\
<iframe width="{width}"
height="{height}"
@@ -64,6 +79,3 @@ class Youtube(Directive):
raise self.warning("This directive does not accept content. The "
"'key=value' format for options is deprecated, "
"use ':key: value' instead")
-
-
-directives.register_directive('youtube', Youtube)
diff --git a/nikola/plugins/compile_textile.plugin b/nikola/plugins/compile/textile.plugin
index c13b3b1..6439b0f 100644
--- a/nikola/plugins/compile_textile.plugin
+++ b/nikola/plugins/compile/textile.plugin
@@ -1,10 +1,10 @@
[Core]
Name = textile
-Module = compile_textile
+Module = textile
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Textile into HTML
diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile/textile.py
index 85efd3f..b402329 100644
--- a/nikola/plugins/compile_textile.py
+++ b/nikola/plugins/compile/textile.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,6 +28,7 @@
import codecs
import os
+import re
try:
from textile import textile
@@ -33,6 +36,7 @@ except ImportError:
textile = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileTextile(PageCompiler):
@@ -40,17 +44,15 @@ class CompileTextile(PageCompiler):
name = "textile"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if textile is None:
- raise Exception('To build this site, you need to install the '
- '"textile" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['textile'], 'build this site (compile Textile)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
+ if not is_two_file:
+ data = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)[-1]
output = textile(data, head_offset=1)
out_file.write(output)
@@ -58,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<notextile> <!--\n')
diff --git a/nikola/plugins/compile_txt2tags.plugin b/nikola/plugins/compile/txt2tags.plugin
index 2c65da1..55eb0a0 100644
--- a/nikola/plugins/compile_txt2tags.plugin
+++ b/nikola/plugins/compile/txt2tags.plugin
@@ -1,10 +1,10 @@
[Core]
Name = txt2tags
-Module = compile_txt2tags
+Module = txt2tags
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile Txt2tags into HTML
diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile/txt2tags.py
index 001da6e..2f62f04 100644
--- a/nikola/plugins/compile_txt2tags.py
+++ b/nikola/plugins/compile/txt2tags.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -39,6 +41,7 @@ except ImportError:
txt2tags = None # NOQA
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
class CompileTextile(PageCompiler):
@@ -46,14 +49,10 @@ class CompileTextile(PageCompiler):
name = "txt2tags"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if txt2tags is None:
- raise Exception('To build this site, you need to install the '
- '"txt2tags" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['txt2tags'], 'build this site (compile txt2tags)')
+ makedirs(os.path.dirname(dest))
cmd = ["-t", "html", "--no-headers", "--outfile", dest, source]
txt2tags(cmd)
@@ -61,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write("\n'''\n<!--\n")
diff --git a/nikola/plugins/compile_wiki.plugin b/nikola/plugins/compile/wiki.plugin
index 65cd942..eee14a8 100644
--- a/nikola/plugins/compile_wiki.plugin
+++ b/nikola/plugins/compile/wiki.plugin
@@ -1,10 +1,10 @@
[Core]
Name = wiki
-Module = compile_wiki
+Module = wiki
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Compile WikiMarkup into HTML
diff --git a/nikola/plugins/compile_wiki.py b/nikola/plugins/compile/wiki.py
index fb9e010..b2c4afc 100644
--- a/nikola/plugins/compile_wiki.py
+++ b/nikola/plugins/compile/wiki.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,7 +24,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-"""Implementation of compile_html based on textile."""
+"""Implementation of compile_html based on CreoleWiki."""
import codecs
import os
@@ -35,21 +37,18 @@ except ImportError:
creole = None
from nikola.plugin_categories import PageCompiler
+from nikola.utils import makedirs, req_missing
-class CompileTextile(PageCompiler):
- """Compile textile into HTML."""
+class CompileWiki(PageCompiler):
+ """Compile CreoleWiki into HTML."""
name = "wiki"
- def compile_html(self, source, dest):
+ def compile_html(self, source, dest, is_two_file=True):
if creole is None:
- raise Exception('To build this site, you need to install the '
- '"creole" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
+ req_missing(['creole'], 'build this site (compile CreoleWiki)')
+ makedirs(os.path.dirname(dest))
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
@@ -61,9 +60,7 @@ class CompileTextile(PageCompiler):
metadata = {}
metadata.update(self.default_metadata)
metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
+ makedirs(os.path.dirname(path))
if onefile:
raise Exception('There are no comments in CreoleWiki markup, so '
'one-file format is not possible, use the -2 '
diff --git a/nikola/plugins/compile_ipynb/README.txt b/nikola/plugins/compile_ipynb/README.txt
deleted file mode 100644
index 2cfd45e..0000000
--- a/nikola/plugins/compile_ipynb/README.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-To make this work...
-
-1- First, you have to put this plugin in your_site/plugins/ folder.
-
-2- Then, you have to download the custom nbconvert from here: https://github.com/damianavila/compile_ipynb-for-Nikola.git
-and put it inside your_site/plugins/compile_ipynb/ folder
-
-3- Also, you have to use the site-ipython theme (or make a new one containing the ipython css, mathjax.js and the proper template).
-You can get it here: https://github.com/damianavila/site-ipython-theme-for-Nikola
-
-4- Finally, you have to put:
-
-post_pages = (
- ("posts/*.ipynb", "posts", "post.tmpl", True),
- ("stories/*.ipynb", "stories", "story.tmpl", False),
-)
-
-in your conf.py
-
-Then... to use it:
-
-$nikola new_page -f ipynb
-
-**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
-create one metadata file and the corresponding naive IPython notebook.
-
-$nikola build
-
-And deploy the output folder... to see it locally: $nikola serve
-
-If you have any doubts, just ask: @damianavila
-
-Cheers.
-
-Damián
diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py
deleted file mode 100644
index 3d41571..0000000
--- a/nikola/plugins/compile_rest/__init__.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import unicode_literals
-import codecs
-import os
-
-try:
- import docutils.core
- import docutils.io
- from docutils.parsers.rst import directives
-
- from .listing import Listing, CodeBlock
- directives.register_directive('code-block', CodeBlock)
- directives.register_directive('sourcecode', CodeBlock)
- directives.register_directive('listing', Listing)
- from .youtube import Youtube
- directives.register_directive('youtube', Youtube)
- from .vimeo import Vimeo
- directives.register_directive('vimeo', Vimeo)
- from .slides import Slides
- directives.register_directive('slides', Slides)
- from .gist_directive import GitHubGist
- directives.register_directive('gist', GitHubGist)
- from .soundcloud import SoundCloud
- directives.register_directive('soundcloud', SoundCloud)
- has_docutils = True
-except ImportError:
- has_docutils = False
-
-from nikola.plugin_categories import PageCompiler
-
-
-class CompileRest(PageCompiler):
- """Compile reSt into HTML."""
-
- name = "rest"
-
- def compile_html(self, source, dest):
- """Compile reSt into HTML."""
- if not has_docutils:
- raise Exception('To build this site, you need to install the '
- '"docutils" package.')
- try:
- os.makedirs(os.path.dirname(dest))
- except:
- pass
- error_level = 100
- with codecs.open(dest, "w+", "utf8") as out_file:
- with codecs.open(source, "r", "utf8") as in_file:
- data = in_file.read()
- output, error_level, deps = rst2html(
- data, settings_overrides={
- 'initial_header_level': 2,
- 'record_dependencies': True,
- 'stylesheet_path': None,
- 'link_stylesheet': True,
- 'syntax_highlight': 'short',
- })
- out_file.write(output)
- deps_path = dest + '.dep'
- if deps.list:
- with codecs.open(deps_path, "wb+", "utf8") as deps_file:
- deps_file.write('\n'.join(deps.list))
- else:
- if os.path.isfile(deps_path):
- os.unlink(deps_path)
- if error_level < 3:
- return True
- else:
- return False
-
- def create_post(self, path, onefile=False, **kw):
- metadata = {}
- metadata.update(self.default_metadata)
- metadata.update(kw)
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
- with codecs.open(path, "wb+", "utf8") as fd:
- if onefile:
- for k, v in metadata.items():
- fd.write('.. {0}: {1}\n'.format(k, v))
- fd.write("\nWrite your post here.")
-
-
-def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
- destination_path=None, reader=None, reader_name='standalone',
- 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):
- """
- Set up & run a `Publisher`, and return a dictionary of document parts.
- Dictionary keys are the names of parts, and values are Unicode strings;
- encoding is up to the client. For programmatic use with string I/O.
-
- For encoded string input, be sure to set the 'input_encoding' setting to
- the desired encoding. Set it to 'unicode' for unencoded Unicode string
- input. Here's how::
-
- publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
-
- Parameters: see `publish_programmatically`.
- """
- output, pub = docutils.core.publish_programmatically(
- source=source, source_path=source_path, source_class=source_class,
- destination_class=docutils.io.StringOutput,
- destination=None, destination_path=destination_path,
- reader=reader, reader_name=reader_name,
- parser=parser, parser_name=parser_name,
- writer=writer, writer_name=writer_name,
- settings=settings, settings_spec=settings_spec,
- settings_overrides=settings_overrides,
- config_section=config_section,
- enable_exit_status=enable_exit_status)
- return pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
diff --git a/nikola/plugins/loghandler/smtp.plugin b/nikola/plugins/loghandler/smtp.plugin
new file mode 100644
index 0000000..e914b3d
--- /dev/null
+++ b/nikola/plugins/loghandler/smtp.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = smtp
+Module = smtp
+
+[Documentation]
+Author = Daniel Devine
+Version = 0.1
+Website = http://getnikola.com
+Description = Log over smtp (email).
diff --git a/nikola/plugins/loghandler/smtp.py b/nikola/plugins/loghandler/smtp.py
new file mode 100644
index 0000000..deb8f4e
--- /dev/null
+++ b/nikola/plugins/loghandler/smtp.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Daniel Devine and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from nikola.plugin_categories import SignalHandler
+from blinker import signal
+import logbook
+
+
+class SmtpHandler(SignalHandler):
+ name = 'smtp'
+
+ def attach_handler(self, sender):
+ """Add the handler to a list of handlers that are attached when get_logger() is called.."""
+ smtpconf = self.site.config.get('LOGGING_HANDLERS').get('smtp')
+ if smtpconf:
+ smtpconf['format_string'] = '''\
+Subject: {record.level_name}: {record.channel}
+
+{record.message}
+'''
+ self.site.loghandlers.append(logbook.MailHandler(
+ smtpconf.pop('from_addr'),
+ smtpconf.pop('recipients'),
+ **smtpconf
+ ))
+
+ def set_site(self, site):
+ self.site = site
+
+ ready = signal('sighandlers_loaded')
+ ready.connect(self.attach_handler)
diff --git a/nikola/plugins/loghandler/stderr.plugin b/nikola/plugins/loghandler/stderr.plugin
new file mode 100644
index 0000000..211d2b4
--- /dev/null
+++ b/nikola/plugins/loghandler/stderr.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = stderr
+Module = stderr
+
+[Documentation]
+Author = Daniel Devine
+Version = 0.1
+Website = http://getnikola.com
+Description = Log to stderr, the default logger.
diff --git a/nikola/plugins/loghandler/stderr.py b/nikola/plugins/loghandler/stderr.py
new file mode 100644
index 0000000..71f1de5
--- /dev/null
+++ b/nikola/plugins/loghandler/stderr.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Daniel Devine and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from nikola.plugin_categories import SignalHandler
+from blinker import signal
+import logbook
+import os
+
+
+class StderrHandler(SignalHandler):
+ """Logs messages to stderr."""
+ name = 'stderr'
+
+ def attach_handler(self, sender):
+ """Attach the handler to the logger."""
+ conf = self.site.config.get('LOGGING_HANDLERS').get('stderr')
+ if conf or os.getenv('NIKOLA_DEBUG'):
+ self.site.loghandlers.append(logbook.StderrHandler(
+ level='DEBUG' if os.getenv('NIKOLA_DEBUG') else conf.get('loglevel', 'WARNING').upper(),
+ format_string=u'[{record.time:%Y-%m-%dT%H:%M:%SZ}] {record.level_name}: {record.channel}: {record.message}'
+ ))
+
+ def set_site(self, site):
+ self.site = site
+
+ ready = signal('sighandlers_loaded')
+ ready.connect(self.attach_handler)
diff --git a/nikola/plugins/task/__init__.py b/nikola/plugins/task/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/task/__init__.py
diff --git a/nikola/plugins/task_archive.plugin b/nikola/plugins/task/archive.plugin
index 23f93ed..448b115 100644
--- a/nikola/plugins/task_archive.plugin
+++ b/nikola/plugins/task/archive.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_archive
-Module = task_archive
+Module = archive
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's archive pages.
diff --git a/nikola/plugins/task_archive.py b/nikola/plugins/task/archive.py
index a67826f..3afbea1 100644
--- a/nikola/plugins/task_archive.py
+++ b/nikola/plugins/task/archive.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,10 +24,10 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import calendar
import os
-import sys
+# for tearDown with _reload we cannot use 'import from' to access LocaleBorg
+import nikola.utils
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -35,6 +37,10 @@ class Archive(Task):
name = "render_archive"
+ def set_site(self, site):
+ site.register_path_handler('archive', self.archive_path)
+ return super(Archive, self).set_site(site)
+
def gen_tasks(self):
kw = {
"messages": self.site.MESSAGES,
@@ -42,16 +48,28 @@ class Archive(Task):
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
"create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
+ "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'],
}
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']:
+ raise Exception('Cannot create monthly and single archives at the same time.')
for lang in kw["translations"]:
- for year, posts in self.site.posts_per_year.items():
+ archdata = self.site.posts_per_year
+ # A bit of a hack.
+ if kw['create_single_archive']:
+ archdata = {None: self.site.posts}
+
+ for year, posts in archdata.items():
output_name = os.path.join(
kw['output_folder'], self.site.path("archive", year, lang))
context = {}
context["lang"] = lang
- context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ if year:
+ context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ else:
+ context["title"] = kw["messages"][lang]["Archive"]
context["permalink"] = self.site.link("archive", year, lang)
if not kw["create_monthly_archive"]:
template_name = "list_post.tmpl"
@@ -62,8 +80,9 @@ class Archive(Task):
else: # Monthly archives, just list the months
months = set([m.split('/')[1] for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
months = sorted(list(months))
+ months.reverse()
template_name = "list.tmpl"
- context["items"] = [[get_month_name(int(month), lang), month] for month in months]
+ context["items"] = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), month] for month in months]
post_list = []
task = self.site.generic_post_list_renderer(
lang,
@@ -95,7 +114,7 @@ class Archive(Task):
context["permalink"] = self.site.link("archive", year, lang)
context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
- year=year, month=get_month_name(int(month), lang))
+ year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -109,39 +128,40 @@ class Archive(Task):
task['basename'] = self.name
yield task
- # And global "all your years" page
- years = list(self.site.posts_per_year.keys())
- years.sort(reverse=True)
- template_name = "list.tmpl"
- kw['years'] = years
- for lang in kw["translations"]:
- context = {}
- output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", None,
- lang))
- context["title"] = kw["messages"][lang]["Archive"]
- context["items"] = [(year, self.site.link("archive", year, lang))
- for year in years]
- context["permalink"] = self.site.link("archive", None, lang)
- task = self.site.generic_post_list_renderer(
- lang,
- [],
- output_name,
- template_name,
- kw['filters'],
- context,
- )
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
-
+ if not kw['create_single_archive']:
+ # And an "all your years" page for yearly and monthly archives
+ years = list(self.site.posts_per_year.keys())
+ years.sort(reverse=True)
+ template_name = "list.tmpl"
+ kw['years'] = years
+ for lang in kw["translations"]:
+ context = {}
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", None,
+ lang))
+ context["title"] = kw["messages"][lang]["Archive"]
+ context["items"] = [(year, self.site.link("archive", year, lang))
+ for year in years]
+ context["permalink"] = self.site.link("archive", None, lang)
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = self.name
+ yield task
-def get_month_name(month_no, locale):
- if sys.version_info[0] == 3: # Python 3
- with calendar.different_locale((locale, "UTF-8")):
- s = calendar.month_name[month_no]
- else: # Python 2
- with calendar.TimeEncoding((locale, "UTF-8")):
- s = calendar.month_name[month_no]
- return s
+ def archive_path(self, name, lang):
+ if name:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['ARCHIVE_PATH'], name,
+ self.site.config['INDEX_FILE']] if _f]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['ARCHIVE_PATH'],
+ self.site.config['ARCHIVE_FILENAME']] if _f]
diff --git a/nikola/plugins/task/build_less.plugin b/nikola/plugins/task/build_less.plugin
new file mode 100644
index 0000000..27ca8cd
--- /dev/null
+++ b/nikola/plugins/task/build_less.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = build_less
+Module = build_less
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Build CSS out of LESS sources
+
diff --git a/nikola/plugins/task/build_less.py b/nikola/plugins/task/build_less.py
new file mode 100644
index 0000000..8889cbe
--- /dev/null
+++ b/nikola/plugins/task/build_less.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+
+import codecs
+import glob
+import os
+import subprocess
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class BuildLess(Task):
+ """Generate CSS out of LESS sources."""
+
+ name = "build_less"
+ sources_folder = "less"
+ sources_ext = ".less"
+ compiler_name = "lessc"
+
+ def gen_tasks(self):
+ """Generate CSS out of LESS sources."""
+
+ kw = {
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'themes': self.site.THEMES,
+ }
+
+ # Find where in the theme chain we define the LESS targets
+ # There can be many *.less in the folder, but we only will build
+ # the ones listed in less/targets
+ targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES)
+ try:
+ with codecs.open(targets_path, "rb", "utf-8") as inf:
+ targets = [x.strip() for x in inf.readlines()]
+ except Exception:
+ targets = []
+
+ for theme_name in kw['themes']:
+ src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder)
+ for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)):
+ task['basename'] = 'prepare_less_sources'
+ yield task
+
+ # Build targets and write CSS files
+ base_path = utils.get_theme_path(self.site.THEMES[0])
+ dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css')
+ # Make everything depend on all sources, rough but enough
+ deps = glob.glob(os.path.join(
+ base_path,
+ self.sources_folder,
+ "*{0}".format(self.sources_ext)))
+
+ def compile_target(target, dst):
+ utils.makedirs(dst_dir)
+ src = os.path.join(kw['cache_folder'], self.sources_folder, target)
+ compiled = subprocess.check_output([self.compiler_name, src])
+ with open(dst, "wb+") as outf:
+ outf.write(compiled)
+
+ yield self.group_task()
+
+ for target in targets:
+ dst = os.path.join(dst_dir, target.replace(self.sources_ext, ".css"))
+ yield {
+ 'basename': self.name,
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': deps,
+ 'task_dep': ['prepare_less_sources'],
+ 'actions': ((compile_target, [target, dst]), ),
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True
+ }
diff --git a/nikola/plugins/task/build_sass.plugin b/nikola/plugins/task/build_sass.plugin
new file mode 100644
index 0000000..746c1df
--- /dev/null
+++ b/nikola/plugins/task/build_sass.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = build_sass
+Module = build_sass
+
+[Documentation]
+Author = Roberto Alsina, Chris “Kwpolska” Warrick
+Version = 0.1
+Website = http://getnikola.com
+Description = Build CSS out of Sass sources
diff --git a/nikola/plugins/task/build_sass.py b/nikola/plugins/task/build_sass.py
new file mode 100644
index 0000000..a5d22fb
--- /dev/null
+++ b/nikola/plugins/task/build_sass.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+
+import codecs
+import glob
+import os
+import subprocess
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class BuildSass(Task):
+ """Generate CSS out of Sass sources."""
+
+ name = "build_sass"
+ sources_folder = "sass"
+ sources_ext = (".sass", ".scss")
+ compiler_name = "sass"
+
+ def gen_tasks(self):
+ """Generate CSS out of Sass sources."""
+ self.logger = utils.get_logger('build_sass', self.site.loghandlers)
+
+ kw = {
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'themes': self.site.THEMES,
+ }
+
+ # Find where in the theme chain we define the Sass targets
+ # There can be many *.sass/*.scss in the folder, but we only
+ # will build the ones listed in sass/targets
+ targets_path = utils.get_asset_path(os.path.join(self.sources_folder, "targets"), self.site.THEMES)
+ try:
+ with codecs.open(targets_path, "rb", "utf-8") as inf:
+ targets = [x.strip() for x in inf.readlines()]
+ except Exception:
+ targets = []
+
+ for theme_name in kw['themes']:
+ src = os.path.join(utils.get_theme_path(theme_name), self.sources_folder)
+ for task in utils.copy_tree(src, os.path.join(kw['cache_folder'], self.sources_folder)):
+ task['basename'] = 'prepare_sass_sources'
+ yield task
+
+ # Build targets and write CSS files
+ base_path = utils.get_theme_path(self.site.THEMES[0])
+ dst_dir = os.path.join(self.site.config['OUTPUT_FOLDER'], 'assets', 'css')
+ # Make everything depend on all sources, rough but enough
+ deps = glob.glob(os.path.join(
+ base_path,
+ self.sources_folder,
+ *("*{0}".format(ext) for ext in self.sources_ext)))
+
+ def compile_target(target, dst):
+ utils.makedirs(dst_dir)
+ src = os.path.join(kw['cache_folder'], self.sources_folder, target)
+ compiled = subprocess.check_output([self.compiler_name, src])
+ with open(dst, "wb+") as outf:
+ outf.write(compiled)
+
+ yield self.group_task()
+
+ # We can have file conflicts. This is a way to prevent them.
+ # I orignally wanted to use sets and their cannot-have-duplicates
+ # magic, but I decided not to do this so we can show the user
+ # what files were problematic.
+ # If we didn’t do this, there would be a cryptic message from doit
+ # instead.
+ seennames = {}
+ for target in targets:
+ base = os.path.splitext(target)[0]
+ dst = os.path.join(dst_dir, base + ".css")
+
+ if base in seennames:
+ self.logger.error(
+ 'Duplicate filenames for SASS compiled files: {0} and '
+ '{1} (both compile to {2})'.format(
+ seennames[base], target, base + ".css"))
+ else:
+ seennames.update({base: target})
+
+ yield {
+ 'basename': self.name,
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': deps,
+ 'task_dep': ['prepare_sass_sources'],
+ 'actions': ((compile_target, [target, dst]), ),
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True
+ }
diff --git a/nikola/plugins/task_create_bundles.plugin b/nikola/plugins/task/bundles.plugin
index 5d4f6d3..e0b0a4d 100644
--- a/nikola/plugins/task_create_bundles.plugin
+++ b/nikola/plugins/task/bundles.plugin
@@ -1,10 +1,10 @@
[Core]
Name = create_bundles
-Module = task_create_bundles
+Module = bundles
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Theme bundles using WebAssets
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task/bundles.py
index 84ac0ab..488f96f 100644
--- a/nikola/plugins/task_create_bundles.py
+++ b/nikola/plugins/task/bundles.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -38,7 +40,7 @@ from nikola import utils
class BuildBundles(LateTask):
"""Bundle assets using WebAssets."""
- name = "build_bundles"
+ name = "create_bundles"
def set_site(self, site):
super(BuildBundles, self).set_site(site)
@@ -64,44 +66,38 @@ class BuildBundles(LateTask):
inputs = [i for i in inputs if os.path.isfile(
os.path.join(out_dir, i))]
cache_dir = os.path.join(kw['cache_folder'], 'webassets')
- if not os.path.isdir(cache_dir):
- os.makedirs(cache_dir)
+ utils.makedirs(cache_dir)
env = webassets.Environment(out_dir, os.path.dirname(output),
cache=cache_dir)
- bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
- env.register(output, bundle)
- # This generates the file
- env[output].urls()
+ if inputs:
+ bundle = webassets.Bundle(*inputs, output=os.path.basename(output))
+ env.register(output, bundle)
+ # This generates the file
+ env[output].urls()
+ else:
+ with open(os.path.join(out_dir, os.path.basename(output)), 'wb+'):
+ pass # Create empty file
- flag = False
+ yield self.group_task()
if (webassets is not None and self.site.config['USE_BUNDLES'] is not
False):
for name, files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
- file_dep = [utils.get_asset_path(
- os.path.join(dname, fname), kw['themes'],
- kw['files_folders'])
- for fname in files
- ]
- file_dep = filter(None, file_dep) # removes missing files
+ file_dep = [os.path.join(kw['output_folder'], dname, fname)
+ for fname in files]
+ file_dep = filter(os.path.isfile, file_dep) # removes missing files
task = {
- 'file_dep': file_dep,
+ 'file_dep': list(file_dep),
+ 'task_dep': ['copy_assets'],
'basename': str(self.name),
'name': str(output_path),
'actions': [(build_bundle, (name, files))],
'targets': [output_path],
- 'uptodate': [utils.config_changed(kw)]
+ 'uptodate': [utils.config_changed(kw)],
+ 'clean': True,
}
- flag = True
yield utils.apply_filters(task, kw['filters'])
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': self.name,
- 'uptodate': [True],
- 'name': 'None',
- 'actions': [],
- }
def get_theme_bundles(themes):
diff --git a/nikola/plugins/task_copy_assets.plugin b/nikola/plugins/task/copy_assets.plugin
index b11133f..28b9e32 100644
--- a/nikola/plugins/task_copy_assets.plugin
+++ b/nikola/plugins/task/copy_assets.plugin
@@ -1,10 +1,10 @@
[Core]
Name = copy_assets
-Module = task_copy_assets
+Module = copy_assets
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy theme assets into output.
diff --git a/nikola/plugins/task_copy_assets.py b/nikola/plugins/task/copy_assets.py
index 06d17e7..f3d85df 100644
--- a/nikola/plugins/task_copy_assets.py
+++ b/nikola/plugins/task/copy_assets.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -47,10 +49,12 @@ class CopyAssets(Task):
"filters": self.site.config['FILTERS'],
"code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
}
- flag = True
has_code_css = False
tasks = {}
code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
+
+ yield self.group_task()
+
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')
@@ -62,22 +66,14 @@ class CopyAssets(Task):
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw)]
task['basename'] = self.name
- flag = False
yield utils.apply_filters(task, kw['filters'])
- if flag:
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
if not has_code_css: # Generate it
def create_code_css():
from pygments.formatters import get_formatter_by_name
formatter = get_formatter_by_name('html', style=kw["code_color_scheme"])
+ utils.makedirs(os.path.dirname(code_css_path))
with codecs.open(code_css_path, 'wb+', 'utf8') as outf:
outf.write(formatter.get_style_defs('.code'))
outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}")
diff --git a/nikola/plugins/task_copy_files.plugin b/nikola/plugins/task/copy_files.plugin
index 0bfc5be..45c9e0d 100644
--- a/nikola/plugins/task_copy_files.plugin
+++ b/nikola/plugins/task/copy_files.plugin
@@ -1,10 +1,10 @@
[Core]
Name = copy_files
-Module = task_copy_files
+Module = copy_files
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy static files into the output.
diff --git a/nikola/plugins/task_copy_files.py b/nikola/plugins/task/copy_files.py
index feaf147..88e89eb 100644
--- a/nikola/plugins/task_copy_files.py
+++ b/nikola/plugins/task/copy_files.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -42,18 +44,12 @@ class CopyFiles(Task):
'filters': self.site.config['FILTERS'],
}
- flag = False
+ yield self.group_task()
for src in kw['files_folders']:
dst = kw['output_folder']
filters = kw['filters']
real_dst = os.path.join(dst, kw['files_folders'][src])
for task in utils.copy_tree(src, real_dst, link_cutoff=dst):
- flag = True
task['basename'] = self.name
task['uptodate'] = [utils.config_changed(kw)]
yield utils.apply_filters(task, filters)
- if not flag:
- yield {
- 'basename': self.name,
- 'actions': (),
- }
diff --git a/nikola/plugins/task_render_galleries.plugin b/nikola/plugins/task/galleries.plugin
index e0a86c0..8352151 100644
--- a/nikola/plugins/task_render_galleries.plugin
+++ b/nikola/plugins/task/galleries.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_galleries
-Module = task_render_galleries
+Module = galleries
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create image galleries automatically.
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
new file mode 100644
index 0000000..cf670e0
--- /dev/null
+++ b/nikola/plugins/task/galleries.py
@@ -0,0 +1,553 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+import codecs
+import datetime
+import glob
+import json
+import mimetypes
+import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
+
+Image = None
+try:
+ from PIL import Image, ExifTags # NOQA
+except ImportError:
+ try:
+ import Image as _Image
+ import ExifTags
+ Image = _Image
+ except ImportError:
+ pass
+import PyRSS2Gen as rss
+
+from nikola.plugin_categories import Task
+from nikola import utils
+from nikola.post import Post
+
+
+class Galleries(Task):
+ """Render image galleries."""
+
+ name = 'render_galleries'
+ dates = {}
+
+ def set_site(self, site):
+ site.register_path_handler('gallery', self.gallery_path)
+ site.register_path_handler('gallery_rss', self.gallery_rss_path)
+ return super(Galleries, self).set_site(site)
+
+ def gallery_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ self.site.config['INDEX_FILE']] if _f]
+
+ def gallery_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ 'rss.xml'] if _f]
+
+ def gen_tasks(self):
+ """Render image galleries."""
+
+ self.logger = utils.get_logger('render_galleries', self.site.loghandlers)
+ self.image_ext_list = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+ self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
+
+ self.kw = {
+ 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'default_lang': self.site.config['DEFAULT_LANG'],
+ 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_path': self.site.config['GALLERY_PATH'],
+ 'sort_by_date': self.site.config['GALLERY_SORT_BY_DATE'],
+ 'filters': self.site.config['FILTERS'],
+ 'translations': self.site.config['TRANSLATIONS'],
+ 'global_context': self.site.GLOBAL_CONTEXT,
+ "feed_length": self.site.config['FEED_LENGTH'],
+ }
+
+ yield self.group_task()
+
+ template_name = "gallery.tmpl"
+
+ # Find all galleries we need to process
+ self.find_galleries()
+
+ # Create all output folders
+ for task in self.create_galleries():
+ yield task
+
+ # For each gallery:
+ for gallery in self.gallery_list:
+
+ # Create subfolder list
+ folder_list = [x.split(os.sep)[-2] for x in
+ glob.glob(os.path.join(gallery, '*') + os.sep)]
+
+ # Parse index into a post (with translations)
+ post = self.parse_index(gallery)
+
+ # Create image list, filter exclusions
+ image_list = self.get_image_list(gallery)
+
+ # Sort as needed
+ # Sort by date
+ if self.kw['sort_by_date']:
+ image_list.sort(key=lambda a: self.image_date(a))
+ else: # Sort by name
+ image_list.sort()
+
+ # Create thumbnails and large images in destination
+ for image in image_list:
+ for task in self.create_target_images(image):
+ yield task
+
+ # Remove excluded images
+ for image in self.get_excluded_images(gallery):
+ for task in self.remove_excluded_image(image):
+ yield task
+
+ crumbs = utils.get_crumbs(gallery)
+
+ # Create index.html for each language
+ for lang in self.kw['translations']:
+ dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ dst = os.path.normpath(dst)
+
+ context = {}
+ context["lang"] = lang
+ if post:
+ context["title"] = post.title(lang)
+ else:
+ context["title"] = os.path.basename(gallery)
+ context["description"] = None
+
+ image_name_list = [os.path.basename(p) for p in image_list]
+
+ if self.kw['use_filename_as_title']:
+ img_titles = []
+ for fn in image_name_list:
+ name_without_ext = os.path.splitext(fn)[0]
+ img_titles.append(
+ 'id="{0}" alt="{1}" title="{2}"'.format(
+ name_without_ext,
+ name_without_ext,
+ utils.unslugify(name_without_ext)))
+ else:
+ img_titles = [''] * len(image_name_list)
+
+ thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list]
+ thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs]
+
+ ## TODO: in v7 remove images from context, use photo_array
+ context["images"] = list(zip(image_name_list, thumbs, img_titles))
+ context["folders"] = folder_list
+ context["crumbs"] = crumbs
+ context["permalink"] = self.site.link(
+ "gallery", os.path.basename(gallery), lang)
+ # FIXME: use kw
+ context["enable_comments"] = (
+ self.site.config["COMMENTS_IN_GALLERIES"])
+ context["thumbnail_size"] = self.kw["thumbnail_size"]
+
+ # FIXME: render post in a task
+ if post:
+ post.compile(lang)
+ context['text'] = post.text(lang)
+ else:
+ context['text'] = ''
+
+ file_dep = self.site.template_system.template_deps(
+ template_name) + image_list + thumbs
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': dst,
+ 'file_dep': file_dep,
+ 'targets': [dst],
+ 'actions': [
+ (self.render_gallery_index, (
+ template_name,
+ dst,
+ context,
+ image_list,
+ thumbs,
+ file_dep))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ 2: self.site.config["COMMENTS_IN_GALLERIES"],
+ 3: context,
+ })],
+ }, self.kw['filters'])
+
+ # RSS for the gallery
+ rss_dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery_rss",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ rss_dst = os.path.normpath(rss_dst)
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': rss_dst,
+ 'file_dep': file_dep,
+ 'targets': [rss_dst],
+ 'actions': [
+ (self.gallery_rss, (
+ image_list,
+ img_titles,
+ lang,
+ self.site.link(
+ "gallery_rss", os.path.basename(gallery), lang),
+ rss_dst,
+ context['title']
+ ))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ })],
+ }, self.kw['filters'])
+
+ def find_galleries(self):
+ """Find all galleries to be processed according to conf.py"""
+
+ self.gallery_list = []
+ for root, dirs, files in os.walk(self.kw['gallery_path']):
+ self.gallery_list.append(root)
+
+ def create_galleries(self):
+ """Given a list of galleries, create the output folders."""
+
+ # gallery_path is "gallery/foo/name"
+ for gallery_path in self.gallery_list:
+ gallery_name = os.path.relpath(gallery_path, self.kw['gallery_path'])
+ # have to use dirname because site.path returns .../index.html
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ output_gallery = os.path.normpath(output_gallery)
+ # Task to create gallery in output/
+ yield {
+ 'basename': self.name,
+ 'name': output_gallery,
+ 'actions': [(utils.makedirs, (output_gallery,))],
+ 'targets': [output_gallery],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }
+
+ def parse_index(self, gallery):
+ """Returns a Post object if there is an index.txt."""
+
+ index_path = os.path.join(gallery, "index.txt")
+ destination = os.path.join(
+ self.kw["output_folder"],
+ gallery)
+ if os.path.isfile(index_path):
+ post = Post(
+ index_path,
+ self.site.config,
+ destination,
+ False,
+ self.site.MESSAGES,
+ 'story.tmpl',
+ self.site.get_compiler(index_path).compile_html
+ )
+ else:
+ post = None
+ return post
+
+ def get_excluded_images(self, gallery_path):
+ exclude_path = os.path.join(gallery_path, "exclude.meta")
+
+ try:
+ f = open(exclude_path, 'r')
+ excluded_image_name_list = f.read().split()
+ except IOError:
+ excluded_image_name_list = []
+
+ excluded_image_list = ["{0}/{1}".format(gallery_path, i) for i in excluded_image_name_list]
+ return excluded_image_list
+
+ def get_image_list(self, gallery_path):
+
+ # Gather image_list contains "gallery/name/image_name.jpg"
+ image_list = []
+
+ for ext in self.image_ext_list:
+ image_list += glob.glob(gallery_path + '/*' + ext.lower()) +\
+ glob.glob(gallery_path + '/*' + ext.upper())
+
+ # Filter ignored images
+ excluded_image_list = self.get_excluded_images(gallery_path)
+ image_set = set(image_list) - set(excluded_image_list)
+ image_list = list(image_set)
+ return image_list
+
+ def create_target_images(self, img):
+ gallery_name = os.path.relpath(os.path.dirname(img), self.kw['gallery_path'])
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ # Do thumbnails and copy originals
+ # img is "galleries/name/image_name.jpg"
+ # img_name is "image_name.jpg"
+ # fname, ext are "image_name", ".jpg"
+ # thumb_path is
+ # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
+ img_name = os.path.basename(img)
+ fname, ext = os.path.splitext(img_name)
+ thumb_path = os.path.join(
+ output_gallery,
+ ".thumbnail".join([fname, ext]))
+ # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
+ 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']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['thumbnail_size']
+ })],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': orig_dest_path,
+ 'file_dep': [img],
+ 'targets': [orig_dest_path],
+ 'actions': [
+ (self.resize_image,
+ (img, orig_dest_path, self.kw['max_image_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['max_image_size']
+ })],
+ }, self.kw['filters'])
+
+ def remove_excluded_image(self, img):
+ # Remove excluded images
+ # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path
+ # and we should remove both the large and thumbnail *destination* paths
+
+ img = os.path.relpath(img, self.kw['gallery_path'])
+ output_folder = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", os.path.dirname(img))))
+ img_path = os.path.join(output_folder, os.path.basename(img))
+ fname, ext = os.path.splitext(img_path)
+ thumb_path = fname + '.thumbnail' + ext
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': thumb_path,
+ 'actions': [
+ (utils.remove_file, (thumb_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': img_path,
+ 'actions': [
+ (utils.remove_file, (img_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ def render_gallery_index(
+ self,
+ template_name,
+ output_name,
+ context,
+ img_list,
+ thumbs,
+ file_dep):
+ """Build the gallery index."""
+
+ # The photo array needs to be created here, because
+ # it relies on thumbnails already being created on
+ # output
+
+ def url_from_path(p):
+ url = '/'.join(os.path.relpath(p, os.path.dirname(output_name) + os.sep).split(os.sep))
+ return url
+
+ photo_array = []
+ for img, thumb in zip(img_list, thumbs):
+ im = Image.open(thumb)
+ w, h = im.size
+ title = ''
+ if self.kw['use_filename_as_title']:
+ title = utils.unslugify(os.path.splitext(img)[0])
+ # Thumbs are files in output, we need URLs
+ photo_array.append({
+ 'url': url_from_path(img),
+ 'url_thumb': url_from_path(thumb),
+ 'title': title,
+ 'size': {
+ 'w': w,
+ 'h': h
+ },
+ })
+ context['photo_array_json'] = json.dumps(photo_array)
+ context['photo_array'] = photo_array
+
+ self.site.render_template(template_name, output_name, context)
+
+ def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title):
+ """Create a RSS showing the latest images in the gallery.
+
+ This doesn't use generic_rss_renderer because it
+ doesn't involve Post objects.
+ """
+
+ def make_url(url):
+ return urljoin(self.site.config['BASE_URL'], url)
+
+ items = []
+ for img, full_title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]:
+ img_size = os.stat(
+ os.path.join(
+ self.site.config['OUTPUT_FOLDER'], img)).st_size
+ args = {
+ 'title': full_title.split('"')[-2],
+ 'link': make_url(img),
+ 'guid': rss.Guid(img, False),
+ 'pubDate': self.image_date(img),
+ 'enclosure': rss.Enclosure(
+ make_url(img),
+ img_size,
+ mimetypes.guess_type(img)[0]
+ ),
+ }
+ items.append(rss.RSSItem(**args))
+ rss_obj = utils.ExtendedRSS2(
+ title=title,
+ link=make_url(permalink),
+ description='',
+ lastBuildDate=datetime.datetime.now(),
+ items=items,
+ generator='nikola',
+ language=lang
+ )
+ rss_obj.self_url = make_url(permalink)
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
+ dst_dir = os.path.dirname(output_path)
+ utils.makedirs(dst_dir)
+ with codecs.open(output_path, "wb+", "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 resize_image(self, src, dst, max_size):
+ """Make a copy of the image in the requested size."""
+ if not Image:
+ utils.copy_file(src, dst)
+ return
+ im = Image.open(src)
+ w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+
+ # Panoramas get larger thumbnails because they look *awful*
+ if w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+
+ if decoded == 'Orientation':
+ if value == 3:
+ im = im.rotate(180)
+ elif value == 6:
+ im = im.rotate(270)
+ elif value == 8:
+ im = im.rotate(90)
+ break
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(dst)
+ except Exception:
+ self.logger.warn("Can't thumbnail {0}, using original image as thumbnail".format(src))
+ utils.copy_file(src, dst)
+ else: # Image is small
+ utils.copy_file(src, dst)
+
+ def image_date(self, src):
+ """Try to figure out the date of the image."""
+ if src not in self.dates:
+ try:
+ im = Image.open(src)
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+ if decoded == 'DateTimeOriginal':
+ try:
+ self.dates[src] = datetime.datetime.strptime(
+ value, r'%Y:%m:%d %H:%M:%S')
+ break
+ except ValueError: # Invalid EXIF date.
+ pass
+ if src not in self.dates:
+ self.dates[src] = datetime.datetime.fromtimestamp(
+ os.stat(src).st_mtime)
+ return self.dates[src]
diff --git a/nikola/plugins/task/gzip.plugin b/nikola/plugins/task/gzip.plugin
new file mode 100644
index 0000000..b68ea6f
--- /dev/null
+++ b/nikola/plugins/task/gzip.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = gzip
+Module = gzip
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://getnikola.com
+Description = Create gzipped copies of files
+
diff --git a/nikola/plugins/task/gzip.py b/nikola/plugins/task/gzip.py
new file mode 100644
index 0000000..738d52c
--- /dev/null
+++ b/nikola/plugins/task/gzip.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 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.
+
+"""Create gzipped copies of files."""
+
+import gzip
+import os
+import shlex
+import subprocess
+
+from nikola.plugin_categories import TaskMultiplier
+
+
+class GzipFiles(TaskMultiplier):
+ """If appropiate, create tasks to create gzipped versions of files."""
+
+ name = "gzip"
+ is_default = True
+
+ def process(self, task, prefix):
+ if not self.site.config['GZIP_FILES']:
+ return []
+ if task.get('name') is None:
+ return []
+ gzip_task = {
+ 'file_dep': [],
+ 'targets': [],
+ 'actions': [],
+ 'basename': '{0}_gzip'.format(prefix),
+ 'name': task.get('name').split(":", 1)[-1] + '.gz',
+ 'clean': True,
+ }
+ targets = task.get('targets', [])
+ flag = False
+ for target in targets:
+ ext = os.path.splitext(target)[1]
+ if (ext.lower() in self.site.config['GZIP_EXTENSIONS'] and
+ target.startswith(self.site.config['OUTPUT_FOLDER'])):
+ flag = True
+ gzipped = target + '.gz'
+ gzip_task['file_dep'].append(target)
+ gzip_task['targets'].append(gzipped)
+ gzip_task['actions'].append((create_gzipped_copy, (target, gzipped, self.site.config['GZIP_COMMAND'])))
+ if not flag:
+ return []
+ return [gzip_task]
+
+
+def create_gzipped_copy(in_path, out_path, command=None):
+ if command:
+ subprocess.check_call(shlex.split(command.format(filename=in_path)))
+ else:
+ with gzip.GzipFile(out_path, 'wb+') as outf:
+ with open(in_path, 'rb') as inf:
+ outf.write(inf.read())
diff --git a/nikola/plugins/task_indexes.plugin b/nikola/plugins/task/indexes.plugin
index 1536006..a18942c 100644
--- a/nikola/plugins/task_indexes.plugin
+++ b/nikola/plugins/task/indexes.plugin
@@ -1,10 +1,10 @@
[Core]
-Name = render_index
-Module = task_indexes
+Name = render_indexes
+Module = indexes
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's index pages.
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task/indexes.py
index aa5e648..0d20422 100644
--- a/nikola/plugins/task_indexes.py
+++ b/nikola/plugins/task/indexes.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -24,6 +26,7 @@
from __future__ import unicode_literals
import glob
+import itertools
import os
from nikola.plugin_categories import Task
@@ -35,8 +38,13 @@ class Indexes(Task):
name = "render_indexes"
+ def set_site(self, site):
+ site.register_path_handler('index', self.index_path)
+ return super(Indexes, self).set_site(site)
+
def gen_tasks(self):
self.site.scan_posts()
+ yield self.group_task()
kw = {
"translations": self.site.config['TRANSLATIONS'],
@@ -54,8 +62,6 @@ class Indexes(Task):
template_name = "index.tmpl"
posts = [x for x in self.site.timeline if x.use_in_feeds]
- if not posts:
- yield {'basename': 'render_indexes', 'actions': []}
for lang in kw["translations"]:
# Split in smaller lists
lists = []
@@ -63,31 +69,40 @@ class Indexes(Task):
filtered_posts = [x for x in posts if x.is_translation_available(lang)]
else:
filtered_posts = posts
+ lists.append(filtered_posts[:kw["index_display_post_count"]])
+ filtered_posts = filtered_posts[kw["index_display_post_count"]:]
while filtered_posts:
- lists.append(filtered_posts[:kw["index_display_post_count"]])
- filtered_posts = filtered_posts[kw["index_display_post_count"]:]
+ lists.append(filtered_posts[-kw["index_display_post_count"]:])
+ filtered_posts = filtered_posts[:-kw["index_display_post_count"]]
num_pages = len(lists)
for i, post_list in enumerate(lists):
context = {}
indexes_title = kw['indexes_title'] or kw['blog_title']
- if not i:
- context["title"] = indexes_title
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % i
else:
- if kw["indexes_pages"]:
- indexes_pages = kw["indexes_pages"] % i
- else:
- indexes_pages = " (" + \
- kw["messages"][lang]["old posts page %d"] % i + ")"
+ indexes_pages = " (" + \
+ kw["messages"][lang]["old posts page %d"] % i + ")"
+ if i > 0:
context["title"] = indexes_title + indexes_pages
+ else:
+ context["title"] = indexes_title
context["prevlink"] = None
context["nextlink"] = None
context['index_teasers'] = kw['index_teasers']
- if i > 1:
- context["prevlink"] = "index-{0}.html".format(i - 1)
- if i == 1:
- context["prevlink"] = "index.html"
- if i < num_pages - 1:
- context["nextlink"] = "index-{0}.html".format(i + 1)
+ if i == 0: # index.html page
+ context["prevlink"] = None
+ if num_pages > 1:
+ context["nextlink"] = "index-{0}.html".format(num_pages - 1)
+ else:
+ context["nextlink"] = None
+ else: # index-x.html pages
+ if i > 1:
+ context["nextlink"] = "index-{0}.html".format(i - 1)
+ if i < num_pages - 1:
+ context["prevlink"] = "index-{0}.html".format(i + 1)
+ elif i == num_pages - 1:
+ context["prevlink"] = "index.html"
context["permalink"] = self.site.link("index", i, lang)
output_name = os.path.join(
kw['output_folder'], self.site.path("index", i,
@@ -115,15 +130,15 @@ class Indexes(Task):
}
template_name = "list.tmpl"
for lang in kw["translations"]:
- for wildcard, dest, _, is_post in kw["post_pages"]:
- if is_post:
- continue
+ # Need to group by folder to avoid duplicated tasks (Issue #758)
+ for dirname, wildcards in itertools.groupby((w for w, d, x, i in kw["post_pages"] if not i), os.path.dirname):
context = {}
# vim/pyflakes thinks it's unused
# src_dir = os.path.dirname(wildcard)
- files = glob.glob(wildcard)
- post_list = [self.site.global_data[os.path.splitext(p)[0]] for
- p in files]
+ files = []
+ for wildcard in wildcards:
+ files += glob.glob(wildcard)
+ post_list = [self.site.global_data[p] for p in files]
output_name = os.path.join(kw["output_folder"],
self.site.path("post_path",
wildcard,
@@ -135,7 +150,18 @@ class Indexes(Task):
template_name,
kw['filters'],
context)
- task_cfg = {1: task['uptodate'][0].config, 2: kw}
- task['uptodate'] = [config_changed(task_cfg)]
- task['basename'] = self.name
- yield task
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = self.name
+ yield task
+
+ def index_path(self, name, lang):
+ if name not in [None, 0]:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['INDEX_PATH'],
+ 'index-{0}.html'.format(name)] if _f]
+ else:
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['INDEX_PATH'],
+ self.site.config['INDEX_FILE']]
+ if _f]
diff --git a/nikola/plugins/task_render_listings.plugin b/nikola/plugins/task/listings.plugin
index 1f897b9..c93184d 100644
--- a/nikola/plugins/task_render_listings.plugin
+++ b/nikola/plugins/task/listings.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_listings
-Module = task_render_listings
+Module = listings
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Render code listings into output
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task/listings.py
index 0cadfd3..ab62e74 100644
--- a/nikola/plugins/task_render_listings.py
+++ b/nikola/plugins/task/listings.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -39,12 +41,17 @@ class Listings(Task):
name = "render_listings"
+ def set_site(self, site):
+ site.register_path_handler('listing', self.listing_path)
+ return super(Listings, self).set_site(site)
+
def gen_tasks(self):
"""Render pretty code listings."""
kw = {
"default_lang": self.site.config["DEFAULT_LANG"],
"listings_folder": self.site.config["LISTINGS_FOLDER"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
+ "index_file": self.site.config["INDEX_FILE"],
}
# Things to ignore in listings
@@ -60,7 +67,7 @@ class Listings(Task):
code = highlight(fd.read(), lexer,
HtmlFormatter(cssclass='code',
linenos="table", nowrap=False,
- lineanchors=utils.slugify(f),
+ lineanchors=utils.slugify(in_name),
anchorlinenos=True))
title = os.path.basename(in_name)
else:
@@ -80,14 +87,15 @@ class Listings(Task):
}
self.site.render_template('listing.tmpl', out_name,
context)
- flag = True
+
+ yield self.group_task()
+
template_deps = self.site.template_system.template_deps('listing.tmpl')
for root, dirs, files in os.walk(kw['listings_folder']):
- flag = False
# Render all files
out_name = os.path.join(
kw['output_folder'],
- root, 'index.html'
+ root, kw['index_file']
)
yield {
'basename': self.name,
@@ -98,7 +106,7 @@ class Listings(Task):
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])],
+ self.site.GLOBAL_CONTEXT)],
'clean': True,
}
for f in files:
@@ -119,11 +127,10 @@ class Listings(Task):
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])],
+ self.site.GLOBAL_CONTEXT)],
'clean': True,
}
- if flag:
- yield {
- 'basename': self.name,
- 'actions': [],
- }
+
+ def listing_path(self, name, lang):
+ return [_f for _f in [self.site.config['LISTINGS_FOLDER'], name +
+ '.html'] if _f]
diff --git a/nikola/plugins/task_localsearch.plugin b/nikola/plugins/task/localsearch.plugin
index 33eb78b..86accb6 100644
--- a/nikola/plugins/task_localsearch.plugin
+++ b/nikola/plugins/task/localsearch.plugin
@@ -1,10 +1,10 @@
[Core]
Name = local_search
-Module = task_localsearch
+Module = localsearch
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create data files for local search via Tipue
diff --git a/nikola/plugins/task_localsearch/MIT-LICENSE.txt b/nikola/plugins/task/localsearch/MIT-LICENSE.txt
index f131068..f131068 100644
--- a/nikola/plugins/task_localsearch/MIT-LICENSE.txt
+++ b/nikola/plugins/task/localsearch/MIT-LICENSE.txt
diff --git a/nikola/plugins/task_localsearch/__init__.py b/nikola/plugins/task/localsearch/__init__.py
index db8610a..9162604 100644
--- a/nikola/plugins/task_localsearch/__init__.py
+++ b/nikola/plugins/task/localsearch/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,8 +29,10 @@ import codecs
import json
import os
+from doit.tools import result_dep
+
from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed, copy_tree
+from nikola.utils import config_changed, copy_tree, makedirs
# This is what we need to produce:
#var tipuesearch = {"pages": [
@@ -68,7 +72,7 @@ class Tipue(LateTask):
for lang in kw["translations"]:
for post in posts:
# Don't index drafts (Issue #387)
- if post.is_draft:
+ if post.is_draft or post.is_retired or post.publish_later:
continue
text = post.text(lang, strip_html=True)
text = text.replace('^', '')
@@ -80,10 +84,7 @@ class Tipue(LateTask):
data["loc"] = post.permalink(lang)
pages.append(data)
output = json.dumps({"pages": pages}, indent=2)
- try:
- os.makedirs(os.path.dirname(dst_path))
- except:
- pass
+ makedirs(os.path.dirname(dst_path))
with codecs.open(dst_path, "wb+", "utf8") as fd:
fd.write(output)
@@ -92,8 +93,11 @@ class Tipue(LateTask):
"name": dst_path,
"targets": [dst_path],
"actions": [(save_data, [])],
- 'uptodate': [config_changed(kw)]
+ 'uptodate': [config_changed(kw), result_dep('sitemap')]
}
+ # Note: The task should run everytime a new file is added or a
+ # file is changed. We cheat, and depend on the sitemap task,
+ # to run everytime a new file is added.
# Copy all the assets to the right places
asset_folder = os.path.join(os.path.dirname(__file__), "files")
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif
index 9c97738..9c97738 100644
--- a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif
+++ b/nikola/plugins/task/localsearch/files/assets/css/img/loader.gif
Binary files differ
diff --git a/nikola/plugins/task/localsearch/files/assets/css/img/search.png b/nikola/plugins/task/localsearch/files/assets/css/img/search.png
new file mode 100755
index 0000000..9ab0f2c
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/css/img/search.png
Binary files differ
diff --git a/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css
new file mode 100755
index 0000000..2230193
--- /dev/null
+++ b/nikola/plugins/task/localsearch/files/assets/css/tipuesearch.css
@@ -0,0 +1,159 @@
+
+/*
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+#tipue_search_input
+{
+ font: 12px/1.7 'open sans', sans-serif;
+ color: #333;
+ padding: 7px;
+ width: 150px;
+ border: 1px solid #e2e2e2;
+ border-radius: 0;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ box-shadow: none;
+ outline: 0;
+ margin: 0;
+}
+#tipue_search_input:focus
+{
+ border: 1px solid #ccc;
+}
+#tipue_search_button
+{
+ width: 70px;
+ height: 36px;
+ border: 0;
+ border-radius: 1px;
+ background: #5193fb url('img/search.png') no-repeat center;
+ outline: none;
+}
+#tipue_search_button:hover
+{
+ background-color: #4589fb;
+}
+
+#tipue_search_content
+{
+ clear: left;
+ max-width: 650px;
+ padding: 25px 0 13px 0;
+ margin: 0;
+}
+#tipue_search_loading
+{
+ padding-top: 60px;
+ background: #fff url('img/loader.gif') no-repeat left;
+}
+
+#tipue_search_warning_head
+{
+ font: 300 16px/1.6 'open sans', sans-serif;
+ color: #333;
+}
+#tipue_search_warning
+{
+ font: 12px/1.6 'open sans', sans-serif;
+ color: #333;
+ margin: 7px 0;
+}
+#tipue_search_warning a
+{
+ color: #3f72d8;
+ text-decoration: none;
+}
+#tipue_search_warning a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+#tipue_search_results_count
+{
+ font: 13px/1.6 'open sans', sans-serif;
+ color: #333;
+}
+.tipue_search_content_title
+{
+ font: 300 23px/1.6 'open sans', sans-serif;
+ margin-top: 31px;
+}
+.tipue_search_content_title a
+{
+ color: #3f72d8;
+ text-decoration: none;
+}
+.tipue_search_content_title a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+.tipue_search_content_text
+{
+ font: 12px/1.7 'open sans', sans-serif;
+ color: #333;
+ padding: 13px 0;
+}
+.tipue_search_content_loc
+{
+ font: 300 13px/1.7 'open sans', sans-serif;
+ overflow: auto;
+}
+.tipue_search_content_loc a
+{
+ color: #555;
+ text-decoration: none;
+}
+.tipue_search_content_loc a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+#tipue_search_foot
+{
+ margin: 51px 0 21px 0;
+}
+#tipue_search_foot_boxes
+{
+ padding: 0;
+ margin: 0;
+ font: 12px/1 'open sans', sans-serif;
+}
+#tipue_search_foot_boxes li
+{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+#tipue_search_foot_boxes li a
+{
+ padding: 7px 13px 8px 13px;
+ background-color: #f1f1f1;
+ border: 1px solid #dcdcdc;
+ border-radius: 1px;
+ color: #333;
+ margin-right: 7px;
+ text-decoration: none;
+ text-align: center;
+}
+#tipue_search_foot_boxes li.current
+{
+ padding: 7px 13px 8px 13px;
+ background: #fff;
+ border: 1px solid #dcdcdc;
+ border-radius: 1px;
+ color: #333;
+ margin-right: 7px;
+ text-align: center;
+}
+#tipue_search_foot_boxes li a:hover
+{
+ border: 1px solid #ccc;
+ background-color: #f3f3f3;
+}
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js
index 5c766ea..a9982cd 100644
--- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch.js
@@ -1,10 +1,10 @@
/*
-Tipue Search 2.1
+Tipue Search 3.0.1
Copyright (c) 2013 Tipue
Tipue Search is released under the MIT License
http://www.tipue.com/search
-*/
+*/
(function($) {
@@ -12,7 +12,7 @@ http://www.tipue.com/search
$.fn.tipuesearch = function(options) {
var set = $.extend( {
-
+
'show' : 7,
'newWindow' : false,
'showURL' : true,
@@ -24,9 +24,9 @@ http://www.tipue.com/search
'liveDescription' : '*',
'liveContent' : '*',
'contentLocation' : 'tipuesearch/tipuesearch_content.json'
-
+
}, options);
-
+
return this.each(function() {
var tipuesearch_in = {
@@ -47,7 +47,7 @@ http://www.tipue.com/search
cont = cont.replace(/\s+/g, ' ');
var desc = $(set.liveDescription, html).text();
desc = desc.replace(/\s+/g, ' ');
-
+
var t_1 = html.toLowerCase().indexOf('<title>');
var t_2 = html.toLowerCase().indexOf('</title>', t_1 + 7);
if (t_1 != -1 && t_2 != -1)
@@ -63,13 +63,13 @@ http://www.tipue.com/search
"title": tit,
"text": desc,
"tags": cont,
- "loc": tipuesearch_pages[i]
- });
+ "loc": tipuesearch_pages[i]
+ });
}
);
}
}
-
+
if (set.mode == 'json')
{
$.getJSON(set.contentLocation,
@@ -80,15 +80,15 @@ http://www.tipue.com/search
);
}
- if (set.mode == 'static' || set.mode == 'static-images')
+ if (set.mode == 'static')
{
tipuesearch_in = $.extend({}, tipuesearch);
- }
-
+ }
+
var tipue_search_w = '';
if (set.newWindow)
{
- tipue_search_w = ' target="_blank"';
+ tipue_search_w = ' target="_blank"';
}
function getURLP(name)
@@ -99,8 +99,8 @@ http://www.tipue.com/search
{
$('#tipue_search_input').val(getURLP('q'));
getTipueSearch(0, true);
- }
-
+ }
+
$('#tipue_search_button').click(function()
{
getTipueSearch(0, true);
@@ -120,7 +120,7 @@ http://www.tipue.com/search
var results = '';
var show_replace = false;
var show_stop = false;
-
+
var d = $('#tipue_search_input').val().toLowerCase();
d = $.trim(d);
var d_w = d.split(' ');
@@ -133,7 +133,7 @@ http://www.tipue.com/search
if (d_w[i] == tipuesearch_stop_words[f])
{
a_w = false;
- show_stop = true;
+ show_stop = true;
}
}
if (a_w)
@@ -143,7 +143,7 @@ http://www.tipue.com/search
}
d = $.trim(d);
d_w = d.split(' ');
-
+
if (d.length >= set.minimumLength)
{
if (replace)
@@ -161,8 +161,8 @@ http://www.tipue.com/search
}
}
d_w = d.split(' ');
- }
-
+ }
+
var d_t = d;
for (var i = 0; i < d_w.length; i++)
{
@@ -193,10 +193,10 @@ http://www.tipue.com/search
{
score -= (150000 - i);
}
-
+
if (set.highlightTerms)
{
- if (set.highlightEveryTerm)
+ if (set.highlightEveryTerm)
{
var patr = new RegExp('(' + d_w[f] + ')', 'gi');
}
@@ -204,33 +204,26 @@ http://www.tipue.com/search
{
var patr = new RegExp('(' + d_w[f] + ')', 'i');
}
- s_t = s_t.replace(patr, "<em>$1</em>");
+ s_t = s_t.replace(patr, "<b>$1</b>");
}
-
if (tipuesearch_in.pages[i].tags.search(pat) != -1)
{
score -= (100000 - i);
- }
+ }
+
}
if (score < 1000000000)
{
- if (set.mode == 'static-images')
- {
- found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc + '^' + tipuesearch_in.pages[i].image;
- }
- else
- {
- found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc;
- }
+ found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc;
}
- }
-
+ }
+
if (c != 0)
{
if (show_replace == 1)
{
out += '<div id="tipue_search_warning_head">Showing results for ' + d + '</div>';
- out += '<div id="tipue_search_warning">Show results for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
+ out += '<div id="tipue_search_warning">Search for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
}
if (c == 1)
{
@@ -241,7 +234,7 @@ http://www.tipue.com/search
c_c = c.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
out += '<div id="tipue_search_results_count">' + c_c + ' results</div>';
}
-
+
found.sort();
var l_o = 0;
for (var i = 0; i < found.length; i++)
@@ -250,16 +243,7 @@ http://www.tipue.com/search
if (l_o >= start && l_o < set.show + start)
{
out += '<div class="tipue_search_content_title"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[1] + '</a></div>';
-
- if (set.mode == 'static-images')
- {
- if (fo[4])
- {
- out += '<div class="tipue_search_content_image_box"><img class="tipue_search_content_image" id="' + fo[1] + '^' + fo[3] + '" ';
- out += 'src=' + fo[4] + '></div><div style="clear: both;"></div>';
- }
- }
-
+
var t = fo[2];
var t_d = '';
var t_w = t.split(' ');
@@ -271,7 +255,7 @@ http://www.tipue.com/search
{
for (var f = 0; f < set.descriptiveWords; f++)
{
- t_d += t_w[f] + ' ';
+ t_d += t_w[f] + ' ';
}
}
t_d = $.trim(t_d);
@@ -280,33 +264,38 @@ http://www.tipue.com/search
t_d += ' ...';
}
out += '<div class="tipue_search_content_text">' + t_d + '</div>';
-
+
if (set.showURL)
{
- out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[3] + '</a></div>';
+ t_url = fo[3];
+ if (t_url.length > 45)
+ {
+ t_url = fo[3].substr(0, 45) + ' ...';
+ }
+ out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + t_url + '</a></div>';
}
}
- l_o++;
+ l_o++;
}
-
+
if (c > set.show)
{
var pages = Math.ceil(c / set.show);
var page = (start / set.show);
out += '<div id="tipue_search_foot"><ul id="tipue_search_foot_boxes">';
-
+
if (start > 0)
{
- out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">&#171; Previous</a></li>';
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">Prev</a></li>';
}
-
- if (page <= 4)
+
+ if (page <= 2)
{
var p_b = pages;
- if (pages > 5)
+ if (pages > 3)
{
- p_b = 5;
- }
+ p_b = 3;
+ }
for (var f = 0; f < p_b; f++)
{
if (f == page)
@@ -321,10 +310,10 @@ http://www.tipue.com/search
}
else
{
- var p_b = pages + 4;
+ var p_b = page + 3;
if (p_b > pages)
{
- p_b = pages;
+ p_b = pages;
}
for (var f = page; f < p_b; f++)
{
@@ -336,27 +325,27 @@ http://www.tipue.com/search
{
out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
}
- }
+ }
}
-
+
if (page + 1 != pages)
{
- out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next &#187;</a></li>';
- }
-
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next</a></li>';
+ }
+
out += '</ul></div>';
- }
+ }
}
else
{
- out += '<div id="tipue_search_warning_head">Nothing found</div>';
+ out += '<div id="tipue_search_warning_head">Nothing found</div>';
}
}
else
{
if (show_stop)
{
- out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>';
+ out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>';
}
else
{
@@ -371,56 +360,25 @@ http://www.tipue.com/search
}
}
}
-
+
$('#tipue_search_content').html(out);
$('#tipue_search_content').slideDown(200);
-
+
$('#tipue_search_replaced').click(function()
{
getTipueSearch(0, false);
});
-
- $('.tipue_search_content_image').click(function()
- {
- var src_i = $(this).attr('src');
- var id_v = $(this).attr('id');
- var id_a = id_v.split('^');
-
- var l_c_i = '<img src="' + src_i + '"><div id="tipue_lightbox_content_title">' + id_a[0] + '</div>';
- l_c_i += '<a href="' + id_a[1] + '"' + tipue_search_w + '><div id="tipue_lightbox_content_link"></div></a>';
- l_c_i += '<a href="' + src_i + '"' + tipue_search_w + '><div id="tipue_lightbox_content_expand"></div></a><div style="clear: both;"></div>';
-
- if ($('#tipue_lightbox').length > 0)
- {
- $('#tipue_lightbox_content').html(l_c_i);
- $('#tipue_lightbox').fadeIn();
- }
- else
- {
- var tipue_lightbox = '<div id="tipue_lightbox">' + '<div id="tipue_lightbox_content">' + l_c_i + '</div></div>';
- $('body').append(tipue_lightbox);
- $('#tipue_lightbox').fadeIn();
- }
- });
- $('#tipue_lightbox').live('click', function()
- {
- $('#tipue_lightbox').hide();
- });
-
+
$('.tipue_search_foot_box').click(function()
{
var id_v = $(this).attr('id');
var id_a = id_v.split('_');
-
+
getTipueSearch(parseInt(id_a[0]), id_a[1]);
- });
- }
-
+ });
+ }
+
});
};
-
-})(jQuery);
-
-
-
+})(jQuery);
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
index 8989c3c..8493ec1 100644
--- a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
+++ b/nikola/plugins/task/localsearch/files/assets/js/tipuesearch_set.js
@@ -1,7 +1,7 @@
/*
-Tipue Search 2.0
-Copyright (c) 2012 Tipue
+Tipue Search 3.0.1
+Copyright (c) 2013 Tipue
Tipue Search is released under the MIT License
http://www.tipue.com/search
*/
@@ -19,10 +19,3 @@ var tipuesearch_stem = {"words": [
{"word": "javascript", stem: "script"},
{"word": "javascript", stem: "js"}
]};
-
-/*
-Include the following variable listing the pages on your site if you're using Live mode
-*/
-
-var tipuesearch_pages = ["http://foo.com/", "http://foo.com/about/", "http://foo.com/blog/", "http://foo.com/tos/"];
-
diff --git a/nikola/plugins/task_localsearch/files/tipue_search.html b/nikola/plugins/task/localsearch/files/tipue_search.html
index 789fbe5..789fbe5 100755
--- a/nikola/plugins/task_localsearch/files/tipue_search.html
+++ b/nikola/plugins/task/localsearch/files/tipue_search.html
diff --git a/nikola/plugins/task_mustache.plugin b/nikola/plugins/task/mustache.plugin
index 6103936..d6b487a 100644
--- a/nikola/plugins/task_mustache.plugin
+++ b/nikola/plugins/task/mustache.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_mustache
-Module = task_mustache
+Module = mustache
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generates the blog's index pages in json.
diff --git a/nikola/plugins/task_mustache/__init__.py b/nikola/plugins/task/mustache/__init__.py
index 7364979..c392e3b 100644
--- a/nikola/plugins/task_mustache/__init__.py
+++ b/nikola/plugins/task/mustache/__init__.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -29,7 +31,7 @@ import json
import os
from nikola.plugin_categories import Task
-from nikola.utils import config_changed, copy_file, unicode_str
+from nikola.utils import config_changed, copy_file, unicode_str, makedirs
class Mustache(Task):
@@ -103,25 +105,11 @@ class Mustache(Task):
post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']),
})
- # Disqus comments
- data["disqus_html"] = ('<div id="disqus_thread"></div> <script '
- 'type="text/javascript">var disqus_'
- 'shortname="%s";var disqus_url="%s";'
- '(function(){var a=document.createElement'
- '("script");a.type="text/javascript";'
- 'a.async=true;a.src="http://"+disqus_'
- 'shortname+".disqus.com/embed.js";('
- 'document.getElementsByTagName("head")'
- '[0]||document.getElementsByTagName("body")'
- '[0]).appendChild(a)})(); </script>'
- '<noscript>Please enable JavaScript to view'
- ' the <a href="http://disqus.com/'
- '?ref_noscript">comments powered by DISQUS.'
- '</a></noscript><a href="http://disqus.com"'
- 'class="dsq-brlink">comments powered by <sp'
- 'an class="logo-disqus">DISQUS</span></a>' %
- (self.site.config['DISQUS_FORUM'],
- post.permalink(absolute=True)))
+ # Comments
+ context = dict(post=post, lang=self.site.current_lang())
+ context.update(self.site.GLOBAL_CONTEXT)
+ data["comment_html"] = self.site.template_system.render_template(
+ 'mustache-comment-form.tmpl', None, context).strip()
# Post translations
translations = []
@@ -135,10 +123,7 @@ class Mustache(Task):
".html", ".json")})
data["translations"] = translations
- try:
- os.makedirs(os.path.dirname(path))
- except:
- pass
+ makedirs(os.path.dirname(path))
with codecs.open(path, 'wb+', 'utf8') as fd:
fd.write(json.dumps(data))
diff --git a/nikola/plugins/task_mustache/mustache-template.html b/nikola/plugins/task/mustache/mustache-template.html
index 7f2b34c..e9a0213 100644
--- a/nikola/plugins/task_mustache/mustache-template.html
+++ b/nikola/plugins/task/mustache/mustache-template.html
@@ -5,7 +5,7 @@
<hr>
<h2>{{title}}</h2>
Posted on: {{date}}</br>
- {{#tags?}} More posts about:
+ {{#tags?}} More posts about:
{{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}}
</br>
{{/tags?}}
@@ -14,13 +14,13 @@
{{{text}}}
<ul class="pager">
{{#prev}}
- <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li>
+ <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li>
{{/prev}}
{{#next}}
<li class="next"><a href="javascript:load_data('{{next}}')">{{message_Next post}}</a></li>
{{/next}}
</ul>
- {{{disqus_html}}}
+ {{{comment_html}}}
</div>
<div class="footerbox">
{{{CONTENT_FOOTER}}}
diff --git a/nikola/plugins/task_mustache/mustache.html b/nikola/plugins/task/mustache/mustache.html
index 5dbebef..7ff6312 100644
--- a/nikola/plugins/task_mustache/mustache.html
+++ b/nikola/plugins/task/mustache/mustache.html
@@ -4,14 +4,12 @@
<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/slides.css" rel="stylesheet" type="text/css"/>
<link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
<link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script>
- <script src="https://raw.github.com/jonnyreeves/jquery-Mustache/master/src/jquery.mustache.js"></script>
- <script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script>
+ <script src="/assets/js/jquery-1.10.2.min.js" type="text/javascript"></script>
+ <script src="//cdn.jsdelivr.net/jquery.mustache/0.2.7/jquery.mustache.js"></script>
+ <script src="//cdn.jsdelivr.net/mustache.js/0.7.2/mustache.js"></script>
<script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
- <script src="/assets/js/slides.min.jquery.js" type="text/javascript"></script>
<script type="text/javascript">
function load_data(dataurl) {
jQuery.getJSON(dataurl, function(data) {
@@ -29,7 +27,7 @@ $.Mustache.load('/mustache-template.html')
load_data('{{first_post_data}}');
};
})
-});
+});
</script>
</head>
<body style="padding-top: 0;">
diff --git a/nikola/plugins/task_render_pages.plugin b/nikola/plugins/task/pages.plugin
index e2a358c..67212d2 100644
--- a/nikola/plugins/task_render_pages.plugin
+++ b/nikola/plugins/task/pages.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_pages
-Module = task_render_pages
+Module = pages
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create pages in the output.
diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task/pages.py
index 1883d7b..eb5b49e 100644
--- a/nikola/plugins/task_render_pages.py
+++ b/nikola/plugins/task/pages.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -41,7 +43,7 @@ class RenderPages(Task):
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
- flag = False
+ yield self.group_task()
for lang in kw["translations"]:
for post in self.site.timeline:
if kw["hide_untranslated_posts"] and not post.is_translation_available(lang):
@@ -53,12 +55,4 @@ class RenderPages(Task):
2: kw})]
task['basename'] = self.name
task['task_dep'] = ['render_posts']
- flag = True
yield task
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
diff --git a/nikola/plugins/task_render_posts.plugin b/nikola/plugins/task/posts.plugin
index 0d19ea9..e1a42fd 100644
--- a/nikola/plugins/task_render_posts.plugin
+++ b/nikola/plugins/task/posts.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_posts
-Module = task_render_posts
+Module = posts
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create HTML fragments out of posts.
diff --git a/nikola/plugins/task/posts.py b/nikola/plugins/task/posts.py
new file mode 100644
index 0000000..18d61b8
--- /dev/null
+++ b/nikola/plugins/task/posts.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from copy import copy
+import nikola.post
+
+from nikola.plugin_categories import Task
+from nikola import utils
+
+
+class RenderPosts(Task):
+ """Build HTML fragments from metadata and text."""
+
+ name = "render_posts"
+
+ def gen_tasks(self):
+ """Build HTML fragments from metadata and text."""
+ self.site.scan_posts()
+ kw = {
+ "translations": self.site.config["TRANSLATIONS"],
+ "timeline": self.site.timeline,
+ "default_lang": self.site.config["DEFAULT_LANG"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ }
+
+ nikola.post.READ_MORE_LINK = self.site.config['READ_MORE_LINK']
+ yield self.group_task()
+
+ for lang in kw["translations"]:
+ deps_dict = copy(kw)
+ deps_dict.pop('timeline')
+ for post in kw['timeline']:
+ dest = post.translated_base_path(lang)
+ task = {
+ 'basename': self.name,
+ 'name': dest,
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [dest],
+ 'actions': [(post.compile, (lang, ))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(deps_dict)],
+ }
+ yield task
diff --git a/nikola/plugins/task_redirect.plugin b/nikola/plugins/task/redirect.plugin
index 285720b..826f3d8 100644
--- a/nikola/plugins/task_redirect.plugin
+++ b/nikola/plugins/task/redirect.plugin
@@ -1,10 +1,10 @@
[Core]
Name = redirect
-Module = task_redirect
+Module = redirect
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Create redirect pages.
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task/redirect.py
index 2503bb7..ade878a 100644
--- a/nikola/plugins/task_redirect.py
+++ b/nikola/plugins/task/redirect.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -30,7 +32,7 @@ from nikola import utils
class Redirect(Task):
- """Copy theme assets into output."""
+ """Generate redirections"""
name = "redirect"
@@ -42,17 +44,8 @@ class Redirect(Task):
'output_folder': self.site.config['OUTPUT_FOLDER'],
}
- if not kw['redirections']:
- # If there are no redirections, still needs to create a
- # dummy action so dependencies don't fail
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
- else:
+ yield self.group_task()
+ if kw['redirections']:
for src, dst in kw["redirections"]:
src_path = os.path.join(kw["output_folder"], src)
yield {
@@ -66,10 +59,7 @@ class Redirect(Task):
def create_redirect(src, dst):
- try:
- os.makedirs(os.path.dirname(src))
- except:
- pass
+ utils.makedirs(os.path.dirname(src))
with codecs.open(src, "wb+", "utf8") as fd:
fd.write('<!DOCTYPE html><head><title>Redirecting...</title>'
'<meta http-equiv="refresh" content="0; '
diff --git a/nikola/plugins/task_render_rss.plugin b/nikola/plugins/task/rss.plugin
index 20caf15..7206a43 100644
--- a/nikola/plugins/task_render_rss.plugin
+++ b/nikola/plugins/task/rss.plugin
@@ -1,10 +1,10 @@
[Core]
-Name = render_rss
-Module = task_render_rss
+Name = generate_rss
+Module = rss
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generate RSS feeds.
diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task/rss.py
index 3000e47..bcca4da 100644
--- a/nikola/plugins/task_render_rss.py
+++ b/nikola/plugins/task/rss.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -22,16 +24,25 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from __future__ import unicode_literals, print_function
import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
from nikola import utils
from nikola.plugin_categories import Task
-class RenderRSS(Task):
+class GenerateRSS(Task):
"""Generate RSS feeds."""
- name = "render_rss"
+ name = "generate_rss"
+
+ def set_site(self, site):
+ site.register_path_handler('rss', self.rss_path)
+ return super(GenerateRSS, self).set_site(site)
def gen_tasks(self):
"""Generate RSS feeds."""
@@ -44,8 +55,10 @@ class RenderRSS(Task):
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "feed_length": self.site.config['FEED_LENGTH'],
}
self.site.scan_posts()
+ yield self.group_task()
for lang in kw["translations"]:
output_name = os.path.join(kw['output_folder'],
self.site.path("rss", None, lang))
@@ -57,16 +70,22 @@ class RenderRSS(Task):
posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
for post in posts:
deps += post.deps(lang)
+
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link("rss", None, lang).lstrip('/'))
yield {
- 'basename': 'render_rss',
+ 'basename': 'generate_rss',
'name': os.path.normpath(output_name),
'file_dep': deps,
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
(lang, kw["blog_title"], kw["site_url"],
kw["blog_description"], posts, output_name,
- kw["rss_teasers"]))],
+ kw["rss_teasers"], kw['feed_length'], feed_url))],
'task_dep': ['render_posts'],
'clean': True,
'uptodate': [utils.config_changed(kw)],
}
+
+ def rss_path(self, name, lang):
+ 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_sitemap.plugin b/nikola/plugins/task/sitemap.plugin
index f6b01d7..2cd8195 100644
--- a/nikola/plugins/task_sitemap.plugin
+++ b/nikola/plugins/task/sitemap.plugin
@@ -1,10 +1,10 @@
[Core]
Name = sitemap
-Module = task_sitemap
+Module = sitemap
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Generate google sitemap.
diff --git a/nikola/plugins/task/sitemap/__init__.py b/nikola/plugins/task/sitemap/__init__.py
new file mode 100644
index 0000000..f34bc0a
--- /dev/null
+++ b/nikola/plugins/task/sitemap/__init__.py
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function, absolute_import, unicode_literals
+import codecs
+import datetime
+import os
+try:
+ from urlparse import urljoin, urlparse
+except ImportError:
+ from urllib.parse import urljoin, urlparse # NOQA
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed
+
+
+header = """<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
+"""
+
+url_format = """ <url>
+ <loc>{0}</loc>
+ <lastmod>{1}</lastmod>
+ </url>
+"""
+
+get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
+
+
+def get_base_path(base):
+ """returns the path of a base URL if it contains one.
+
+ >>> get_base_path('http://some.site') == '/'
+ True
+ >>> get_base_path('http://some.site/') == '/'
+ True
+ >>> get_base_path('http://some.site/some/sub-path') == '/some/sub-path/'
+ True
+ >>> get_base_path('http://some.site/some/sub-path/') == '/some/sub-path/'
+ True
+ """
+ # first parse the base_url for some path
+ base_parsed = urlparse(base)
+
+ if not base_parsed.path:
+ sub_path = ''
+ else:
+ sub_path = base_parsed.path
+ if sub_path.endswith('/'):
+ return sub_path
+ else:
+ return sub_path + '/'
+
+
+class Sitemap(LateTask):
+ """Generate google sitemap."""
+
+ name = "sitemap"
+
+ def gen_tasks(self):
+ """Generate Google sitemap."""
+ kw = {
+ "base_url": self.site.config["BASE_URL"],
+ "site_url": self.site.config["SITE_URL"],
+ "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', ['.html', '.htm', '.xml'])
+ }
+ output_path = kw['output_folder']
+ sitemap_path = os.path.join(output_path, "sitemap.xml")
+ base_path = get_base_path(kw['base_url'])
+ locs = {}
+
+ output = kw['output_folder']
+ base_url = kw['base_url']
+ mapped_exts = kw['mapped_extensions']
+
+ def scan_locs():
+ for root, dirs, files in os.walk(output):
+ if not dirs and not files and not kw['sitemap_include_fileless_dirs']:
+ continue # Totally empty, not on sitemap
+ path = os.path.relpath(root, output)
+ # ignore the current directory.
+ path = (path.replace(os.sep, '/') + '/').replace('./', '')
+ lastmod = get_lastmod(root)
+ loc = urljoin(base_url, base_path + path)
+ if kw['index_file'] in files and kw['strip_indexes']: # ignore folders when not stripping urls
+ locs[loc] = url_format.format(loc, lastmod)
+ for fname in files:
+ if kw['strip_indexes'] and fname == kw['index_file']:
+ 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)
+ if path.endswith(kw['index_file']) and kw['strip_indexes']:
+ # ignore index files when stripping urls
+ continue
+ if path.endswith('.html') or path.endswith('.htm'):
+ if not u'<!doctype html' in codecs.open(real_path, 'r', 'utf8').read(1024).lower():
+ # ignores "html" files without doctype
+ # alexa-verify, google-site-verification, etc.
+ continue
+ if path.endswith('.xml'):
+ if not u'<rss' in codecs.open(real_path, 'r', 'utf8').read(512):
+ # ignores all XML files except those presumed to be RSS
+ continue
+ post = self.site.post_per_file.get(path)
+ if post and (post.is_draft or post.is_retired or post.publish_later):
+ continue
+ path = path.replace(os.sep, '/')
+ lastmod = get_lastmod(real_path)
+ loc = urljoin(base_url, base_path + path)
+ locs[loc] = url_format.format(loc, lastmod)
+
+ def write_sitemap():
+ # Have to rescan, because files may have been added between
+ # task dep scanning and task execution
+ scan_locs()
+ with codecs.open(sitemap_path, 'wb+', 'utf8') as outf:
+ outf.write(header)
+ for k in sorted(locs.keys()):
+ outf.write(locs[k])
+ outf.write("</urlset>")
+ # Other tasks can depend on this output, instead of having
+ # to scan locations.
+ return {'locations': list(locs.keys())}
+
+ scan_locs()
+ yield self.group_task()
+ task = {
+ "basename": "sitemap",
+ "name": sitemap_path,
+ "targets": [sitemap_path],
+ "actions": [(write_sitemap,)],
+ "uptodate": [config_changed({1: kw, 2: locs})],
+ "clean": True,
+ "task_dep": ["render_site"],
+ }
+ yield task
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
diff --git a/nikola/plugins/task_render_sources.plugin b/nikola/plugins/task/sources.plugin
index 5b59598..6224e48 100644
--- a/nikola/plugins/task_render_sources.plugin
+++ b/nikola/plugins/task/sources.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_sources
-Module = task_render_sources
+Module = sources
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Copy page sources into the output.
diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task/sources.py
index 392345c..672f354 100644
--- a/nikola/plugins/task_render_sources.py
+++ b/nikola/plugins/task/sources.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -50,35 +52,30 @@ class Sources(Task):
}
self.site.scan_posts()
- flag = False
- for lang in kw["translations"]:
- for post in self.site.timeline:
- if post.meta('password'):
- continue
- output_name = os.path.join(
- kw['output_folder'], post.destination_path(
- lang, post.source_ext()))
- source = post.source_path
- if source.endswith('.html'):
- continue
- if lang != kw["default_lang"]:
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
- if os.path.isfile(source):
- yield {
- 'basename': 'render_sources',
- 'name': os.path.normpath(output_name),
- 'file_dep': [source],
- 'targets': [output_name],
- 'actions': [(utils.copy_file, (source, output_name))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- if flag is False: # No page rendered, yield a dummy task
- yield {
- 'basename': 'render_sources',
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
+ yield self.group_task()
+ if self.site.config['COPY_SOURCES']:
+ for lang in kw["translations"]:
+ for post in self.site.timeline:
+ if post.meta('password'):
+ continue
+ output_name = os.path.join(
+ kw['output_folder'], post.destination_path(
+ lang, post.source_ext()))
+ source = post.source_path
+ dest_ext = self.site.get_compiler(post.source_path).extension()
+ if dest_ext == post.source_ext():
+ continue
+ if lang != kw["default_lang"]:
+ source_lang = source + '.' + lang
+ if os.path.exists(source_lang):
+ source = source_lang
+ if os.path.isfile(source):
+ yield {
+ 'basename': 'render_sources',
+ 'name': os.path.normpath(output_name),
+ 'file_dep': [source],
+ 'targets': [output_name],
+ 'actions': [(utils.copy_file, (source, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
diff --git a/nikola/plugins/task_render_tags.plugin b/nikola/plugins/task/tags.plugin
index b826e87..f01e0f8 100644
--- a/nikola/plugins/task_render_tags.plugin
+++ b/nikola/plugins/task/tags.plugin
@@ -1,10 +1,10 @@
[Core]
Name = render_tags
-Module = task_render_tags
+Module = tags
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Render the tag pages and feeds.
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task/tags.py
index 58a7ff3..299dca4 100644
--- a/nikola/plugins/task_render_tags.py
+++ b/nikola/plugins/task/tags.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,16 +28,28 @@ from __future__ import unicode_literals
import codecs
import json
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
class RenderTags(Task):
- """Render the tag pages and feeds."""
+ """Render the tag/category pages and feeds."""
name = "render_tags"
+ def set_site(self, site):
+ site.register_path_handler('tag_index', self.tag_index_path)
+ site.register_path_handler('tag', self.tag_path)
+ site.register_path_handler('tag_rss', self.tag_rss_path)
+ site.register_path_handler('category', self.category_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."""
@@ -43,7 +57,6 @@ class RenderTags(Task):
"translations": self.site.config["TRANSLATIONS"],
"blog_title": self.site.config["BLOG_TITLE"],
"site_url": self.site.config["SITE_URL"],
- "blog_description": self.site.config["BLOG_DESCRIPTION"],
"messages": self.site.MESSAGES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
@@ -53,17 +66,21 @@ class RenderTags(Task):
"index_teasers": self.site.config['INDEX_TEASERS'],
"rss_teasers": self.site.config["RSS_TEASERS"],
"hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "feed_length": self.site.config['FEED_LENGTH'],
}
self.site.scan_posts()
+ yield self.group_task()
yield self.list_tags_page(kw)
- if not self.site.posts_per_tag:
- yield {'basename': str(self.name), 'actions': []}
+ if not self.site.posts_per_tag and not self.site.posts_per_category:
return
- for tag, posts in list(self.site.posts_per_tag.items()):
+ 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):
post_list = [self.site.global_data[post] for post in posts]
post_list.sort(key=lambda a: a.date)
post_list.reverse()
@@ -72,13 +89,23 @@ class RenderTags(Task):
filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
else:
filtered_posts = post_list
- rss_post_list = [p.post_name for p in filtered_posts]
- yield self.tag_rss(tag, lang, rss_post_list, kw)
+ rss_post_list = [p.source_path for p in filtered_posts]
+ yield self.tag_rss(tag, lang, rss_post_list, kw, is_category)
# Render HTML
if kw['tag_pages_are_indexes']:
- yield self.tag_page_as_index(tag, lang, filtered_posts, kw)
+ 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)
+ 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 tag, posts in cat_list:
+ if tag == '': # This is uncategorized posts
+ continue
+ for task in render_lists(tag, posts, True):
+ yield task
# Tag cloud json file
tag_cloud_data = {}
@@ -89,10 +116,7 @@ class RenderTags(Task):
'assets', 'js', 'tag_cloud_data.json')
def write_tag_data(data):
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ utils.makedirs(os.path.dirname(output_name))
with codecs.open(output_name, 'wb+', 'utf8') as fd:
fd.write(json.dumps(data))
@@ -107,21 +131,37 @@ class RenderTags(Task):
yield task
def list_tags_page(self, kw):
- """a global "all your tags" page for each language"""
+ """a global "all your tags/categories" page for each language"""
tags = list(self.site.posts_per_tag.keys())
+ categories = list(self.site.posts_per_category.keys())
# We want our tags to be sorted case insensitive
tags.sort(key=lambda a: a.lower())
+ categories.sort(key=lambda a: a.lower())
+ if categories != ['']:
+ has_categories = True
+ else:
+ has_categories = False
template_name = "tags.tmpl"
kw['tags'] = tags
+ kw['categories'] = categories
for lang in kw["translations"]:
output_name = os.path.join(
kw['output_folder'], self.site.path('tag_index', None, lang))
output_name = output_name
context = {}
- context["title"] = kw["messages"][lang]["Tags"]
+ if has_categories:
+ context["title"] = kw["messages"][lang]["Tags and Categories"]
+ else:
+ context["title"] = kw["messages"][lang]["Tags"]
context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
in tags]
+ if has_categories:
+ context["cat_items"] = [(tag, self.site.link("category", tag, lang)) for tag
+ in categories]
+ else:
+ context["cat_items"] = None
context["permalink"] = self.site.link("tag_index", None, lang)
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
[],
@@ -132,15 +172,18 @@ class RenderTags(Task):
)
task_cfg = {1: task['uptodate'][0].config, 2: kw}
task['uptodate'] = [utils.config_changed(task_cfg)]
+ task['basename'] = str(self.name)
yield task
- def tag_page_as_index(self, tag, lang, post_list, kw):
+ def tag_page_as_index(self, tag, lang, post_list, kw, is_category):
"""render a sort of index page collection using only this
tag's posts."""
+ kind = "category" if is_category else "tag"
+
def page_name(tagname, i, lang):
"""Given tag, n, returns a page name."""
- name = self.site.path("tag", tag, lang)
+ name = self.site.path(kind, tag, lang)
if i:
name = name.replace('.html', '-{0}.html'.format(i))
return name
@@ -159,7 +202,7 @@ class RenderTags(Task):
rss_link = ("""<link rel="alternate" type="application/rss+xml" """
"""type="application/rss+xml" title="RSS for tag """
"""{0} ({1})" href="{2}">""".format(
- tag, lang, self.site.link("tag_rss", tag, lang)))
+ tag, lang, self.site.link(kind + "_rss", tag, lang)))
context['rss_link'] = rss_link
output_name = os.path.join(kw['output_folder'],
page_name(tag, i, lang))
@@ -177,8 +220,9 @@ class RenderTags(Task):
if i < num_pages - 1:
context["nextlink"] = os.path.basename(
page_name(tag, i + 1, lang))
- context["permalink"] = self.site.link("tag", tag, lang)
+ context["permalink"] = self.site.link(kind, tag, lang)
context["tag"] = tag
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -190,19 +234,23 @@ class RenderTags(Task):
task_cfg = {1: task['uptodate'][0].config, 2: kw}
task['uptodate'] = [utils.config_changed(task_cfg)]
task['basename'] = str(self.name)
+
yield task
- def tag_page_as_list(self, tag, lang, post_list, kw):
+ def tag_page_as_list(self, tag, lang, post_list, kw, is_category):
"""We render a single flat link list with this tag's posts"""
+ kind = "category" if is_category else "tag"
template_name = "tag.tmpl"
output_name = os.path.join(kw['output_folder'], self.site.path(
- "tag", tag, lang))
+ kind, tag, lang))
context = {}
context["lang"] = lang
context["title"] = kw["messages"][lang]["Posts about %s"] % tag
context["posts"] = post_list
- context["permalink"] = self.site.link("tag", tag, lang)
+ context["permalink"] = self.site.link(kind, tag, lang)
context["tag"] = tag
+ context["kind"] = kind
+ context["description"] = None
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -216,11 +264,14 @@ class RenderTags(Task):
task['basename'] = str(self.name)
yield task
- def tag_rss(self, tag, lang, posts, kw):
+ def tag_rss(self, tag, lang, posts, kw, is_category):
"""RSS for a single tag / language"""
+ kind = "category" if is_category else "tag"
#Render RSS
- output_name = os.path.join(kw['output_folder'],
- self.site.path("tag_rss", tag, lang))
+ output_name = os.path.normpath(
+ os.path.join(kw['output_folder'],
+ self.site.path(kind + "_rss", tag, lang)))
+ feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", tag, lang).lstrip('/'))
deps = []
post_list = [self.site.global_data[post] for post in posts if
self.site.global_data[post].use_in_feeds]
@@ -235,9 +286,39 @@ class RenderTags(Task):
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
(lang, "{0} ({1})".format(kw["blog_title"], tag),
- kw["site_url"], kw["blog_description"], post_list,
- output_name, kw["rss_teasers"]))],
+ kw["site_url"], None, post_list,
+ output_name, kw["rss_teasers"], kw['feed_length'], feed_url))],
'clean': True,
'uptodate': [utils.config_changed(kw)],
'task_dep': ['render_posts'],
}
+
+ def slugify_name(self, name):
+ if self.site.config['SLUG_TAG_PATH']:
+ name = utils.slugify(name)
+ return name
+
+ def tag_index_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'],
+ self.site.config['INDEX_FILE']] if _f]
+
+ def tag_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_name(name) + ".html"] if
+ _f]
+
+ def tag_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], self.slugify_name(name) + ".xml"] if
+ _f]
+
+ def category_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".html"] if
+ _f]
+
+ def category_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['TAG_PATH'], "cat_" + self.slugify_name(name) + ".xml"] if
+ _f]
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
deleted file mode 100755
index 21bb7b0..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/link.png b/nikola/plugins/task_localsearch/files/assets/css/img/link.png
deleted file mode 100755
index d4e51c5..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/link.png
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
deleted file mode 100644
index 644bd17..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
+++ /dev/null
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
deleted file mode 100755
index 96dadf0..0000000
--- a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
+++ /dev/null
@@ -1,232 +0,0 @@
-
-/*
-Tipue Search 2.1
-Copyright (c) 2013 Tipue
-Tipue Search is released under the MIT License
-http://www.tipue.com/search
-*/
-
-
-em
-{
- font: inherit;
- font-weight: 400;
-}
-#tipue_search_input
-{
-}
-#tipue_search_input:focus
-{
- border-color: #c3c3c3;
- box-shadow: 0 0 3px rgba(0,0,0,.2);
-}
-#tipue_search_button
-{
- width: 60px;
- height: 33px;
- margin-top: 1px;
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- background: #f1f1f1 url('img/search.gif') no-repeat center;
- outline: none;
-}
-#tipue_search_button:hover
-{
- border: 1px solid #c3c3c3;
- -moz-box-shadow: 1px 1px 2px #e3e3e3;
- -webkit-box-shadow: 1px 1px 2px #e3e3e3;
- box-shadow: 1px 1px 2px #e3e3e3;
-}
-
-#tipue_search_content
-{
- clear: left;
- max-width: 650px;
- padding: 25px 0 13px 0;
- margin: 0;
-}
-#tipue_search_loading
-{
- padding-top: 60px;
- background: #fff url('img/loader.gif') no-repeat left;
-}
-
-#tipue_search_warning_head
-{
- font: 14px/1.5 'open sans', sans-serif;
- color: #333;
-}
-#tipue_search_warning
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
- margin: 13px 0;
-}
-#tipue_search_warning a
-{
- color: #36c;
- text-decoration: none;
-}
-#tipue_search_warning a:hover
-{
- color: #111;
-}
-
-#tipue_search_results_count
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
-}
-
-.tipue_search_content_title
-{
- font: 300 19px/1.5 'open sans', sans-serif;
- margin-top: 31px;
-}
-.tipue_search_content_title a
-{
- color: #36c;
- text-decoration: none;
-}
-.tipue_search_content_title a:hover
-{
- color: #333;
-}
-
-.tipue_search_content_image_box
-{
- float: left;
- border: 1px solid #f3f3f3;
- padding: 13px;
- margin: 21px 0 7px 0;
-}
-.tipue_search_content_image
-{
- max-width: 110px;
- height: auto;
- outline: none;
- cursor: pointer;
-}
-#tipue_lightbox
-{
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(255, 255, 255, .9);
-}
-#tipue_lightbox_content
-{
- margin: 37px auto;
- background-color: #fff;
- padding: 30px;
- border: 1px solid #ccc;
- width: 250px;
- text-align: center;
- box-shadow: 0 1px 2px #eee;
-}
-#tipue_lightbox img
-{
- max-width: 200px;
- height: auto;
-}
-#tipue_lightbox_content_title
-{
- font: 300 14px/1.7 lato, sans-serif;
- color: #111;
- padding: 17px 25px 0 25px;
- width: 200px;
-}
-#tipue_lightbox_content_link
-{
- float: left;
- width: 30px;
- height: 30px;
- margin-top: 17px;
- background: #fff url('img/link.png') no-repeat center;
-}
-#tipue_lightbox_content_expand
-{
- float: left;
- width: 30px;
- height: 30px;
- margin: 17px 0 0 3px;
- background: #fff url('img/expand.png') no-repeat center;
-}
-#tipue_lightbox_content_link:hover, #tipue_lightbox_content_expand:hover
-{
- background-color: #f3f3f3;
-}
-
-.tipue_search_content_text
-{
- font: 300 14px/1.7 lato, sans-serif;
- color: #111;
- padding: 13px 0;
-}
-.tipue_search_content_loc
-{
- font: 300 14px/1.5 lato, sans-serif;
- color: #111;
-}
-.tipue_search_content_loc a
-{
- color: #555;
- text-decoration: none;
-}
-.tipue_search_content_loc a:hover
-{
- padding-bottom: 1px;
- border-bottom: 1px solid #ccc;
-}
-
-#tipue_search_foot
-{
- margin: 47px 0 31px 0;
-}
-#tipue_search_foot_boxes
-{
- padding: 0;
- margin: 0;
- font: 12px/1 'open sans', sans-serif;
-}
-#tipue_search_foot_boxes li
-{
- list-style: none;
- margin: 0;
- padding: 0;
- display: inline;
-}
-#tipue_search_foot_boxes li a
-{
- padding: 7px 10px 8px 10px;
- background-color: #f5f5f5;
- background: -webkit-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -moz-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -ms-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: -o-linear-gradient(top, #f7f7f7, #f1f1f1);
- background: linear-gradient(top, #f7f7f7, #f1f1f1);
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- color: #333;
- margin-right: 7px;
- text-decoration: none;
- text-align: center;
-}
-#tipue_search_foot_boxes li.current
-{
- padding: 7px 10px 8px 10px;
- background: #fff;
- border: 1px solid #dcdcdc;
- border-radius: 2px;
- color: #333;
- margin-right: 7px;
- text-align: center;
-}
-#tipue_search_foot_boxes li a:hover
-{
- border: 1px solid #c3c3c3;
- box-shadow: 1px 1px 2px #e3e3e3;
-}
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
deleted file mode 100644
index d4e4a3a..0000000
--- a/nikola/plugins/task_render_galleries.py
+++ /dev/null
@@ -1,338 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import unicode_literals
-import codecs
-import datetime
-import glob
-import hashlib
-import os
-
-Image = None
-try:
- import Image as _Image
- import ExifTags
- Image = _Image
-except ImportError:
- try:
- from PIL import Image, ExifTags # NOQA
- except ImportError:
- pass
-
-
-from nikola.plugin_categories import Task
-from nikola import utils
-
-
-class Galleries(Task):
- """Copy theme assets into output."""
-
- name = str("render_galleries")
- dates = {}
-
- def gen_tasks(self):
- """Render image galleries."""
-
- kw = {
- 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
- 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
- 'output_folder': self.site.config['OUTPUT_FOLDER'],
- 'cache_folder': self.site.config['CACHE_FOLDER'],
- 'default_lang': self.site.config['DEFAULT_LANG'],
- 'blog_description': self.site.config['BLOG_DESCRIPTION'],
- 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
- 'gallery_path': self.site.config['GALLERY_PATH']
- }
-
- # FIXME: lots of work is done even when images don't change,
- # which should be moved into the task.
-
- template_name = "gallery.tmpl"
-
- gallery_list = []
- for root, dirs, files in os.walk(kw['gallery_path']):
- gallery_list.append(root)
- if not gallery_list:
- yield {
- 'basename': str('render_galleries'),
- 'actions': [],
- }
- return
-
- # gallery_path is "gallery/name"
- for gallery_path in gallery_list:
- # gallery_name is "name"
- splitted = gallery_path.split(os.sep)[1:]
- if not splitted:
- gallery_name = ''
- else:
- gallery_name = os.path.join(*splitted)
- # output_gallery is "output/GALLERY_PATH/name"
- output_gallery = os.path.dirname(os.path.join(
- kw["output_folder"], self.site.path("gallery", gallery_name,
- None)))
- output_name = os.path.join(output_gallery, "index.html")
- if not os.path.isdir(output_gallery):
- yield {
- 'basename': str('render_galleries'),
- 'name': output_gallery,
- 'actions': [(os.makedirs, (output_gallery,))],
- 'targets': [output_gallery],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- # image_list contains "gallery/name/image_name.jpg"
- image_list = glob.glob(gallery_path + "/*jpg") +\
- glob.glob(gallery_path + "/*JPG") +\
- glob.glob(gallery_path + "/*PNG") +\
- glob.glob(gallery_path + "/*png")
-
- # Filter ignore images
- try:
- def add_gallery_path(index):
- return "{0}/{1}".format(gallery_path, index)
-
- exclude_path = os.path.join(gallery_path, "exclude.meta")
- try:
- f = open(exclude_path, 'r')
- excluded_image_name_list = f.read().split()
- except IOError:
- excluded_image_name_list = []
-
- excluded_image_list = list(map(add_gallery_path,
- excluded_image_name_list))
- image_set = set(image_list) - set(excluded_image_list)
- image_list = list(image_set)
- except IOError:
- pass
-
- # List of sub-galleries
- folder_list = [x.split(os.sep)[-2] for x in
- glob.glob(os.path.join(gallery_path, '*') + os.sep)]
-
- crumbs = utils.get_crumbs(gallery_path)
-
- image_list = [x for x in image_list if "thumbnail" not in x]
- # Sort by date
- image_list.sort(key=lambda a: self.image_date(a))
- image_name_list = [os.path.basename(x) for x in image_list]
- thumbs = []
- # Do thumbnails and copy originals
- for img, img_name in list(zip(image_list, image_name_list)):
- # img is "galleries/name/image_name.jpg"
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- # thumb_path is
- # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
- thumb_path = os.path.join(output_gallery,
- ".thumbnail".join([fname, ext]))
- # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
- orig_dest_path = os.path.join(output_gallery, img_name)
- thumbs.append(os.path.basename(thumb_path))
- yield {
- 'basename': str('render_galleries'),
- 'name': thumb_path,
- 'file_dep': [img],
- 'targets': [thumb_path],
- 'actions': [
- (self.resize_image,
- (img, thumb_path, kw['thumbnail_size']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- yield {
- 'basename': str('render_galleries'),
- 'name': orig_dest_path,
- 'file_dep': [img],
- 'targets': [orig_dest_path],
- 'actions': [
- (self.resize_image,
- (img, orig_dest_path, kw['max_image_size']))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- # Remove excluded images
- if excluded_image_name_list:
- for img, img_name in zip(excluded_image_list,
- excluded_image_name_list):
- # img_name is "image_name.jpg"
- # fname, ext are "image_name", ".jpg"
- fname, ext = os.path.splitext(img_name)
- excluded_thumb_dest_path = os.path.join(
- output_gallery, ".thumbnail".join([fname, ext]))
- excluded_dest_path = os.path.join(output_gallery, img_name)
- yield {
- 'basename': str('render_galleries_clean'),
- 'name': excluded_thumb_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_thumb_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_thumb_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
- yield {
- 'basename': str('render_galleries_clean'),
- 'name': excluded_dest_path,
- 'file_dep': [exclude_path],
- #'targets': [excluded_dest_path],
- 'actions': [
- (utils.remove_file, (excluded_dest_path,))
- ],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- context = {}
- context["lang"] = kw["default_lang"]
- context["title"] = os.path.basename(gallery_path)
- context["description"] = kw["blog_description"]
- if kw['use_filename_as_title']:
- img_titles = ['id="{0}" alt="{1}" title="{2}"'.format(
- fn[:-4], fn[:-4], utils.unslugify(fn[:-4])) for fn
- in image_name_list]
- else:
- img_titles = [''] * len(image_name_list)
- context["images"] = list(zip(image_name_list, thumbs, img_titles))
- context["folders"] = folder_list
- context["crumbs"] = crumbs
- context["permalink"] = self.site.link(
- "gallery", gallery_name, None)
- context["enable_comments"] = (
- self.site.config["COMMENTS_IN_GALLERIES"])
-
- # Use galleries/name/index.txt to generate a blurb for
- # the gallery, if it exists
- index_path = os.path.join(gallery_path, "index.txt")
- cache_dir = os.path.join(kw["cache_folder"], 'galleries')
- if not os.path.isdir(cache_dir):
- os.makedirs(cache_dir)
- index_dst_path = os.path.join(
- cache_dir,
- str(hashlib.sha224(index_path.encode('utf-8')).hexdigest() +
- '.html'))
- if os.path.exists(index_path):
- compile_html = self.site.get_compiler(index_path)
- yield {
- 'basename': str('render_galleries'),
- 'name': index_dst_path,
- 'file_dep': [index_path],
- 'targets': [index_dst_path],
- 'actions': [(compile_html, [index_path, index_dst_path])],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
-
- file_dep = self.site.template_system.template_deps(
- template_name) + image_list
-
- def render_gallery(output_name, context, index_dst_path):
- if os.path.exists(index_dst_path):
- with codecs.open(index_dst_path, "rb", "utf8") as fd:
- context['text'] = fd.read()
- file_dep.append(index_dst_path)
- else:
- context['text'] = ''
- self.site.render_template(template_name, output_name, context)
-
- yield {
- 'basename': str('render_galleries'),
- 'name': output_name,
- 'file_dep': file_dep,
- 'targets': [output_name],
- 'actions': [(render_gallery, (output_name, context,
- index_dst_path))],
- 'clean': True,
- 'uptodate': [utils.config_changed({
- 1: kw,
- 2: self.site.config['GLOBAL_CONTEXT'],
- 3: self.site.config["COMMENTS_IN_GALLERIES"],
- })],
- }
-
- def resize_image(self, src, dst, max_size):
- """Make a copy of the image in the requested size."""
- if not Image:
- utils.copy_file(src, dst)
- return
- im = Image.open(src)
- w, h = im.size
- if w > max_size or h > max_size:
- size = max_size, max_size
- try:
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
-
- if decoded == 'Orientation':
- if value == 3:
- im = im.rotate(180)
- elif value == 6:
- im = im.rotate(270)
- elif value == 8:
- im = im.rotate(90)
-
- break
-
- try:
- im.thumbnail(size, Image.ANTIALIAS)
- except Exception:
- # TODO: inform the user, but do not fail
- pass
- else:
- im.save(dst)
-
- else:
- utils.copy_file(src, dst)
-
- def image_date(self, src):
- """Try to figure out the date of the image."""
- if src not in self.dates:
- try:
- im = Image.open(src)
- exif = im._getexif()
- except Exception:
- exif = None
- if exif is not None:
- for tag, value in list(exif.items()):
- decoded = ExifTags.TAGS.get(tag, tag)
- if decoded == 'DateTimeOriginal':
- try:
- self.dates[src] = datetime.datetime.strptime(
- value, r'%Y:%m:%d %H:%M:%S')
- break
- except ValueError: # Invalid EXIF date.
- pass
- if src not in self.dates:
- self.dates[src] = datetime.datetime.fromtimestamp(
- os.stat(src).st_mtime)
- return self.dates[src]
diff --git a/nikola/plugins/task_render_posts.py b/nikola/plugins/task_render_posts.py
deleted file mode 100644
index 4be68bf..0000000
--- a/nikola/plugins/task_render_posts.py
+++ /dev/null
@@ -1,140 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from copy import copy
-import codecs
-import string
-
-from nikola.plugin_categories import Task
-from nikola import utils, rc4
-
-
-def wrap_encrypt(path, password):
- """Wrap a post with encryption."""
- with codecs.open(path, 'rb+', 'utf8') as inf:
- data = inf.read() + "<!--tail-->"
- data = CRYPT.substitute(data=rc4.rc4(password, data))
- with codecs.open(path, 'wb+', 'utf8') as outf:
- outf.write(data)
-
-
-class RenderPosts(Task):
- """Build HTML fragments from metadata and text."""
-
- name = "render_posts"
-
- def gen_tasks(self):
- """Build HTML fragments from metadata and text."""
- self.site.scan_posts()
- kw = {
- "translations": self.site.config["TRANSLATIONS"],
- "timeline": self.site.timeline,
- "default_lang": self.site.config["DEFAULT_LANG"],
- "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
- }
-
- flag = False
- for lang in kw["translations"]:
- deps_dict = copy(kw)
- deps_dict.pop('timeline')
- for post in kw['timeline']:
- source = post.source_path
- dest = post.base_path
- if not post.is_translation_available(lang) and kw["hide_untranslated_posts"]:
- continue
- else:
- source = post.translated_source_path(lang)
- if lang != post.default_lang:
- dest = dest + '.' + lang
- flag = True
- task = {
- 'basename': self.name,
- 'name': dest,
- 'file_dep': post.fragment_deps(lang),
- 'targets': [dest],
- 'actions': [(self.site.get_compiler(post.source_path),
- [source, dest])],
- 'clean': True,
- 'uptodate': [utils.config_changed(deps_dict)],
- }
- if post.meta('password'):
- task['actions'].append((wrap_encrypt, (dest, post.meta('password'))))
- yield task
- if flag is False: # Return a dummy task
- yield {
- 'basename': self.name,
- 'name': 'None',
- 'uptodate': [True],
- 'actions': [],
- }
-
-
-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/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
deleted file mode 100644
index 044e0e3..0000000
--- a/nikola/plugins/task_sitemap/__init__.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
-
-# Permission is hereby granted, free of charge, to any
-# person obtaining a copy of this software and associated
-# documentation files (the "Software"), to deal in the
-# Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the
-# Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice
-# shall be included in all copies or substantial portions of
-# the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
-# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-from __future__ import print_function, absolute_import, unicode_literals
-import codecs
-import datetime
-import os
-try:
- from urlparse import urljoin
-except ImportError:
- from urllib.parse import urljoin # NOQA
-
-from nikola.plugin_categories import LateTask
-from nikola.utils import config_changed
-
-
-header = """<?xml version="1.0" encoding="UTF-8"?>
-<urlset
- xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
- http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
-"""
-
-url_format = """ <url>
- <loc>{0}</loc>
- <lastmod>{1}</lastmod>
- <priority>0.5000</priority>
- </url>
-"""
-
-get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
-
-
-class Sitemap(LateTask):
- """Generate google sitemap."""
-
- name = "sitemap"
-
- def gen_tasks(self):
- """Generate Google sitemap."""
- kw = {
- "base_url": self.site.config["BASE_URL"],
- "site_url": self.site.config["SITE_URL"],
- "output_folder": self.site.config["OUTPUT_FOLDER"],
- "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm'])
- }
- output_path = kw['output_folder']
- sitemap_path = os.path.join(output_path, "sitemap.xml")
-
- def sitemap():
- with codecs.open(sitemap_path, 'wb+', 'utf8') as outf:
- output = kw['output_folder']
- base_url = kw['base_url']
- mapped_exts = kw['mapped_extensions']
- outf.write(header)
- locs = {}
- for root, dirs, files in os.walk(output):
- path = os.path.relpath(root, output)
- path = path.replace(os.sep, '/') + '/'
- lastmod = get_lastmod(root)
- loc = urljoin(base_url, path)
- locs[loc] = url_format.format(loc, lastmod)
- for fname in files:
- if os.path.splitext(fname)[-1] in mapped_exts:
- real_path = os.path.join(root, fname)
- path = os.path.relpath(real_path, output)
- path = path.replace(os.sep, '/')
- lastmod = get_lastmod(real_path)
- loc = urljoin(base_url, path)
- locs[loc] = url_format.format(loc, lastmod)
-
- for k in sorted(locs.keys()):
- outf.write(locs[k])
- outf.write("</urlset>")
-
- yield {
- "basename": "sitemap",
- "name": sitemap_path,
- "targets": [sitemap_path],
- "actions": [(sitemap,)],
- "uptodate": [config_changed(kw)],
- "clean": True,
- }
diff --git a/nikola/plugins/template/__init__.py b/nikola/plugins/template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/nikola/plugins/template/__init__.py
diff --git a/nikola/plugins/template_jinja.plugin b/nikola/plugins/template/jinja.plugin
index 01e6d8c..53b0fec 100644
--- a/nikola/plugins/template_jinja.plugin
+++ b/nikola/plugins/template/jinja.plugin
@@ -1,9 +1,9 @@
[Core]
Name = jinja
-Module = template_jinja
+Module = jinja
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Support for Jinja2 templates.
diff --git a/nikola/plugins/template_jinja.py b/nikola/plugins/template/jinja.py
index b6d762b..a40a476 100644
--- a/nikola/plugins/template_jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -26,12 +28,15 @@
import os
import json
+from collections import deque
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
class JinjaTemplates(TemplateSystem):
@@ -39,6 +44,7 @@ class JinjaTemplates(TemplateSystem):
name = "jinja"
lookup = None
+ dependency_cache = {}
def __init__(self):
""" initialize Jinja2 wrapper with extended set of filters"""
@@ -46,31 +52,51 @@ class JinjaTemplates(TemplateSystem):
return
self.lookup = jinja2.Environment()
self.lookup.filters['tojson'] = json.dumps
+ self.lookup.globals['enumerate'] = enumerate
def set_directories(self, directories, cache_folder):
"""Createa template lookup."""
if jinja2 is None:
- raise Exception('To use this theme you need to install the '
- '"Jinja2" package.')
+ req_missing(['jinja2'], 'use this theme')
self.lookup.loader = jinja2.FileSystemLoader(directories,
encoding='utf-8')
def render_template(self, template_name, output_name, context):
"""Render the template into output_name using context."""
if jinja2 is None:
- raise Exception('To use this theme you need to install the '
- '"Jinja2" package.')
+ req_missing(['jinja2'], 'use this theme')
template = self.lookup.get_template(template_name)
output = template.render(**context)
if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ makedirs(os.path.dirname(output_name))
with open(output_name, 'w+') as output:
output.write(output.encode('utf8'))
return output
+ def render_template_to_string(self, template, context):
+ """ Render template to a string using context. """
+
+ return jinja2.Template(template).render(**context)
+
def template_deps(self, template_name):
- # FIXME: unimplemented
- return []
+ # Cache the lists of dependencies for each template name.
+ if self.dependency_cache.get(template_name) is None:
+ # Use a breadth-first search to find all templates this one
+ # depends on.
+ queue = deque([template_name])
+ visited_templates = set([template_name])
+ deps = []
+ while len(queue) > 0:
+ curr = queue.popleft()
+ source, filename = self.lookup.loader.get_source(self.lookup,
+ curr)[:2]
+ deps.append(filename)
+ ast = self.lookup.parse(source)
+ dep_names = meta.find_referenced_templates(ast)
+ for dep_name in dep_names:
+ if (dep_name not in visited_templates
+ and dep_name is not None):
+ visited_templates.add(dep_name)
+ queue.append(dep_name)
+ self.dependency_cache[template_name] = deps
+ return self.dependency_cache[template_name]
diff --git a/nikola/plugins/template_mako.plugin b/nikola/plugins/template/mako.plugin
index 3fdc354..71f2c71 100644
--- a/nikola/plugins/template_mako.plugin
+++ b/nikola/plugins/template/mako.plugin
@@ -1,9 +1,9 @@
[Core]
Name = mako
-Module = template_mako
+Module = mako
[Documentation]
Author = Roberto Alsina
Version = 0.1
-Website = http://nikola.ralsina.com.ar
+Website = http://getnikola.com
Description = Support for Mako templates.
diff --git a/nikola/plugins/template_mako.py b/nikola/plugins/template/mako.py
index 6ec6698..ae51cac 100644
--- a/nikola/plugins/template_mako.py
+++ b/nikola/plugins/template/mako.py
@@ -1,4 +1,6 @@
-# Copyright (c) 2012 Roberto Alsina y otros.
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,14 +25,21 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Mako template handlers"""
-
+from __future__ import unicode_literals, print_function, absolute_import
import os
import shutil
+import sys
+import tempfile
from mako import util, lexer
from mako.lookup import TemplateLookup
+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
+
+LOGGER = get_logger('mako', STDERR_HANDLER)
class MakoTemplates(TemplateSystem):
@@ -55,8 +64,16 @@ class MakoTemplates(TemplateSystem):
return deps
def set_directories(self, directories, cache_folder):
- """Createa template lookup."""
+ """Create a template lookup."""
cache_dir = os.path.join(cache_folder, '.mako.tmp')
+ # Workaround for a Mako bug, Issue #825
+ if sys.version_info[0] == 2:
+ 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.lookup = TemplateLookup(
@@ -66,21 +83,23 @@ class MakoTemplates(TemplateSystem):
def render_template(self, template_name, output_name, context):
"""Render the template into output_name using context."""
-
+ context['striphtml'] = striphtml
template = self.lookup.get_template(template_name)
data = template.render_unicode(**context)
if output_name is not None:
- try:
- os.makedirs(os.path.dirname(output_name))
- except:
- pass
+ makedirs(os.path.dirname(output_name))
with open(output_name, 'w+') as output:
output.write(data)
return data
+ def render_template_to_string(self, template, context):
+ """ Render template to a string using context. """
+
+ return Template(template).render(**context)
+
def template_deps(self, template_name):
"""Returns filenames which are dependencies for a template."""
- # We can cache here because depedencies should
+ # We can cache here because dependencies should
# not change between runs
if self.cache.get(template_name, None) is None:
template = self.lookup.get_template(template_name)
@@ -90,3 +109,7 @@ class MakoTemplates(TemplateSystem):
deps += self.template_deps(fname)
self.cache[template_name] = tuple(deps)
return list(self.cache[template_name])
+
+
+def striphtml(text):
+ return Markup(text).striptags()
diff --git a/nikola/post.py b/nikola/post.py
index ac97c73..a41901d 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -23,21 +24,43 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-from __future__ import unicode_literals, print_function
+from __future__ import unicode_literals, print_function, absolute_import
import codecs
from collections import defaultdict
+import datetime
import os
import re
import string
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
import lxml.html
-
-from .utils import to_datetime, slugify, bytes_str, Functionary, LocaleBorg
+try:
+ import pyphen
+except ImportError:
+ pyphen = None
+import pytz
+
+# for tearDown with _reload we cannot use 'from import' to get forLocaleBorg
+import nikola.utils
+from .utils import (
+ bytes_str,
+ current_time,
+ Functionary,
+ LOGGER,
+ slugify,
+ to_datetime,
+ unicode_str,
+)
+from .rc4 import rc4
__all__ = ['Post']
TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE)
+READ_MORE_LINK = '<p class="more"><a href="{link}">{read_more}…</a></p>'
class Post(object):
@@ -45,60 +68,90 @@ class Post(object):
"""Represents a blog post or web page."""
def __init__(
- self, source_path, cache_folder, destination, use_in_feeds,
- translations, default_lang, base_url, messages, template_name,
- file_metadata_regexp=None, strip_index_html=False, tzinfo=None,
- skip_untranslated=False,
+ self,
+ source_path,
+ config,
+ destination,
+ use_in_feeds,
+ messages,
+ template_name,
+ compiler
):
"""Initialize post.
- The source path is the .txt post file. From it we calculate
+ 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.
"""
+ self.compiler = compiler
+ self.config = config
+ tzinfo = None
+ if self.config['TIMEZONE'] is not None:
+ tzinfo = pytz.timezone(self.config['TIMEZONE'])
+ 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.base_url = base_url
+ self.base_url = self.config['BASE_URL']
self.is_draft = False
+ self.is_retired = False
self.is_mathjax = False
- self.strip_index_html = strip_index_html
+ self.strip_indexes = self.config['STRIP_INDEXES']
+ self.index_file = self.config['INDEX_FILE']
+ self.pretty_urls = self.config['PRETTY_URLS']
self.source_path = source_path # posts/blah.txt
self.post_name = os.path.splitext(source_path)[0] # posts/blah
+ # 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 = os.path.join(cache_folder, self.post_name + ".html")
+ self._base_path = self.base_path.replace('\\', '/')
self.metadata_path = self.post_name + ".meta" # posts/blah.meta
self.folder = destination
- self.translations = translations
- self.default_lang = default_lang
+ self.translations = self.config['TRANSLATIONS']
+ self.default_lang = self.config['DEFAULT_LANG']
self.messages = messages
- self.skip_untranslated = skip_untranslated
+ self.skip_untranslated = self.config['HIDE_UNTRANSLATED_POSTS']
self._template_name = template_name
+ self.is_two_file = True
+ self.hyphenate = self.config['HYPHENATE']
+ self._reading_time = None
- default_metadata = get_meta(self, file_metadata_regexp)
+ default_metadata = get_meta(self, self.config['FILE_METADATA_REGEXP'])
self.meta = Functionary(lambda: None, self.default_lang)
- self.meta[default_lang] = default_metadata
+ self.meta[self.default_lang] = default_metadata
# Load internationalized metadata
- for lang in translations:
- if lang != default_lang:
+ for lang in self.translations:
+ if lang != self.default_lang:
if os.path.isfile(self.source_path + "." + lang):
self.translated_to.add(lang)
meta = defaultdict(lambda: '')
meta.update(default_metadata)
- meta.update(get_meta(self, file_metadata_regexp, lang))
+ meta.update(get_meta(self, self.config['FILE_METADATA_REGEXP'], lang))
self.meta[lang] = meta
elif os.path.isfile(self.source_path):
- self.translated_to.add(default_lang)
+ self.translated_to.add(self.default_lang)
- if not self.is_translation_available(default_lang):
+ if not self.is_translation_available(self.default_lang):
# Special case! (Issue #373)
# Fill default_metadata with stuff from the other languages
for lang in sorted(self.translated_to):
default_metadata.update(self.meta[lang])
+ if 'date' not in default_metadata and not use_in_feeds:
+ # For stories we don't *really* need a date
+ default_metadata['date'] = datetime.datetime.utcfromtimestamp(
+ os.stat(self.source_path).st_ctime)
+
+ if tzinfo:
+ default_metadata['date'] = default_metadata['date'].replace(
+ tzinfo=pytz.UTC).astimezone(tzinfo)
+
if 'title' not in default_metadata or 'slug' not in default_metadata \
or 'date' not in default_metadata:
raise OSError("You must set a title (found '{0}'), a slug (found "
@@ -109,7 +162,9 @@ class Post(object):
source_path))
# If timezone is set, build localized datetime.
- self.date = to_datetime(self.meta[default_lang]['date'], tzinfo)
+ self.date = to_datetime(self.meta[self.default_lang]['date'], tzinfo)
+
+ self.publish_later = False if self.current_time is None else self.date >= self.current_time
is_draft = False
is_retired = False
@@ -123,14 +178,27 @@ class Post(object):
if 'retired' in self._tags[lang]:
is_retired = True
self._tags[lang].remove('retired')
+ if 'private' in self._tags[lang]:
+ is_retired = True
+ self._tags[lang].remove('private')
# While draft comes from the tags, it's not really a tag
self.is_draft = is_draft
- self.use_in_feeds = use_in_feeds and not is_draft and not is_retired
+ self.is_retired = is_retired
+ self.use_in_feeds = use_in_feeds and not is_draft and not is_retired \
+ and not self.publish_later
# If mathjax is a tag, then enable mathjax rendering support
self.is_mathjax = 'mathjax' in self.tags
+ 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
+ else:
+ return False
+
@property
def alltags(self):
"""This is ALL the tags for this post."""
@@ -141,7 +209,7 @@ class Post(object):
@property
def tags(self):
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
if lang in self._tags:
return self._tags[lang]
elif self.default_lang in self._tags:
@@ -151,7 +219,7 @@ class Post(object):
@property
def prev_post(self):
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
rv = self._prev_post
while self.skip_untranslated:
if rv is None:
@@ -167,7 +235,7 @@ class Post(object):
@property
def next_post(self):
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
rv = self._next_post
while self.skip_untranslated:
if rv is None:
@@ -202,33 +270,20 @@ class Post(object):
fmt_date = fmt_date.decode('utf8')
return fmt_date
- def current_lang(self):
- """Return the currently set locale, if it's one of the
- available translations, or default_lang."""
- lang = LocaleBorg().current_lang
- if lang:
- if lang in self.translations:
- return lang
- lang = lang.split('_')[0]
- if lang in self.translations:
- return lang
- # whatever
- return self.default_lang
-
def title(self, lang=None):
"""Return localized title.
- If lang is not specified, it will use the currently set locale,
- because templates set it.
+ If lang is not specified, it defaults to the current language from
+ templates, as set in LocaleBorg.
"""
if lang is None:
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
return self.meta[lang]['title']
def description(self, lang=None):
"""Return localized description."""
if lang is None:
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
return self.meta[lang]['description']
def deps(self, lang):
@@ -241,6 +296,32 @@ class Post(object):
deps += self.fragment_deps(lang)
return deps
+ def compile(self, lang):
+ """Generate the cache/ file with the compiled post."""
+
+ def wrap_encrypt(path, password):
+ """Wrap a post with encryption."""
+ with codecs.open(path, 'rb+', 'utf8') as inf:
+ data = inf.read() + "<!--tail-->"
+ data = CRYPT.substitute(data=rc4(password, data))
+ with codecs.open(path, 'wb+', 'utf8') as outf:
+ outf.write(data)
+
+ self.READ_MORE_LINK = self.config['READ_MORE_LINK']
+ dest = self.translated_base_path(lang)
+ if not self.is_translation_available(lang) and self.config['HIDE_UNTRANSLATED_POSTS']:
+ return
+ else:
+ self.compiler(
+ self.translated_source_path(lang),
+ dest,
+ self.is_two_file),
+ if self.meta('password'):
+ wrap_encrypt(dest, self.meta('password'))
+ if self.publish_later:
+ LOGGER.notice('{0} is scheduled to be published in the future ({1})'.format(
+ self.source_path, self.date))
+
def fragment_deps(self, lang):
"""Return a list of dependencies to build this post's fragment."""
deps = []
@@ -274,6 +355,13 @@ class Post(object):
else:
return '.'.join((self.source_path, sorted(self.translated_to)[0]))
+ def translated_base_path(self, lang):
+ """Return path to the translation's base_path file."""
+ if lang == self.default_lang:
+ return self.base_path
+ else:
+ return '.'.join((self.base_path, lang))
+
def _translated_file_path(self, lang):
"""Return path to the translation's file, or to the original."""
if lang in self.translated_to:
@@ -286,60 +374,125 @@ class Post(object):
else:
return '.'.join((self.base_path, sorted(self.translated_to)[0]))
- def text(self, lang=None, teaser_only=False, strip_html=False):
- """Read the post file for that language and return its contents."""
+ def text(self, lang=None, teaser_only=False, strip_html=False, really_absolute=False):
+ """Read the post file for that language and return its contents.
+
+ teaser_only=True breaks at the teaser marker and returns only the teaser.
+ strip_html=True removes HTML tags
+ lang=None uses the last used to set locale
+
+ All links in the returned HTML will be relative.
+ The HTML returned is a bare fragment, not a full document.
+ """
if lang is None:
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
file_name = self._translated_file_path(lang)
with codecs.open(file_name, "r", "utf8") as post_file:
data = post_file.read().strip()
-
try:
- document = lxml.html.document_fromstring(data)
+ document = lxml.html.fragment_fromstring(data, "body")
except lxml.etree.ParserError as e:
# if we don't catch this, it breaks later (Issue #374)
if str(e) == "Document is empty":
return ""
# let other errors raise
raise(e)
- document.make_links_absolute(self.permalink(lang=lang))
+ base_url = self.permalink(lang=lang, absolute=really_absolute)
+ document.make_links_absolute(base_url)
+
+ if self.hyphenate:
+ hyphenate(document, lang)
+
data = lxml.html.tostring(document, encoding='unicode')
+ # data here is a full HTML doc, including HTML and BODY tags
+ # which is not ideal (Issue #464)
+ try:
+ body = document.body
+ data = (body.text or '') + ''.join(
+ [lxml.html.tostring(child, encoding='unicode')
+ for child in body.iterchildren()])
+ except IndexError: # No body there, it happens sometimes
+ pass
+
if teaser_only:
teaser = TEASER_REGEXP.split(data)[0]
if teaser != data:
- teaser_str = self.messages[lang]["Read more"] + '...'
- teaser += '<p><a href="{0}">{1}</a></p>'.format(
- self.permalink(lang), teaser_str)
+ if not strip_html:
+ if TEASER_REGEXP.search(data).groups()[-1]:
+ teaser += '<p class="more"><a href="{0}">{1}</a></p>'.format(
+ self.permalink(lang, absolute=really_absolute),
+ TEASER_REGEXP.search(data).groups()[-1])
+ else:
+ teaser += READ_MORE_LINK.format(
+ link=self.permalink(lang, absolute=really_absolute),
+ read_more=self.messages[lang]["Read more"])
# This closes all open tags and sanitizes the broken HTML
document = lxml.html.fromstring(teaser)
data = lxml.html.tostring(document, encoding='unicode')
if data and strip_html:
- content = lxml.html.fromstring(data)
- data = content.text_content().strip() # No whitespace wanted.
+ try:
+ # 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:
+ data = ""
return data
- def destination_path(self, lang, extension='.html'):
- path = os.path.join(self.translations[lang],
- self.folder, self.meta[lang]['slug'] + extension)
+ @property
+ def reading_time(self):
+ """Reading time based on length of text.
+ """
+ if self._reading_time is None:
+ text = self.text(strip_html=True)
+ words_per_minute = 180
+ words = len(text.split())
+ self._reading_time = int(round(words / words_per_minute)) or 1
+ return self._reading_time
+
+ def source_link(self, lang=None):
+ """Return absolute link to the post's source."""
+ return "/" + self.destination_path(
+ lang=lang,
+ extension=self.source_ext(),
+ sep='/')
+
+ def destination_path(self, lang=None, extension='.html', sep=os.sep):
+ """Destination path for this post, relative to output/.
+
+ If lang is not specified, it's the current language.
+ Extension is used in the path if specified.
+ """
+ if lang is None:
+ lang = nikola.utils.LocaleBorg().current_lang
+ if self._has_pretty_url(lang):
+ path = os.path.join(self.translations[lang],
+ self.folder, self.meta[lang]['slug'], 'index' + extension)
+ else:
+ path = os.path.join(self.translations[lang],
+ self.folder, self.meta[lang]['slug'] + extension)
+ if sep != os.sep:
+ path = path.replace(os.sep, sep)
return path
def permalink(self, lang=None, absolute=False, extension='.html'):
if lang is None:
- lang = self.current_lang()
+ lang = nikola.utils.LocaleBorg().current_lang
pieces = self.translations[lang].split(os.sep)
pieces += self.folder.split(os.sep)
- pieces += [self.meta[lang]['slug'] + extension]
+ if self._has_pretty_url(lang):
+ pieces += [self.meta[lang]['slug'], 'index' + extension]
+ else:
+ pieces += [self.meta[lang]['slug'] + extension]
pieces = [_f for _f in pieces if _f and _f != '.']
+ link = '/' + '/'.join(pieces)
if absolute:
- pieces = [self.base_url] + pieces
- else:
- pieces = [""] + pieces
- link = "/".join(pieces)
- if self.strip_index_html and link.endswith('/index.html'):
- return link[:-10]
+ link = urljoin(self.base_url, link)
+ index_len = len(self.index_file)
+ if self.strip_indexes and link[-(1 + index_len):] == '/' + self.index_file:
+ return link[:-index_len]
else:
return link
@@ -391,6 +544,8 @@ def get_metadata_from_file(source_path, lang=None):
with codecs.open(source_path, "r", "utf8") as meta_file:
meta_data = [x.strip() for x in meta_file.readlines()]
return _get_metadata_from_file(meta_data)
+ except (UnicodeDecodeError, UnicodeEncodeError):
+ raise ValueError('Error reading {0}: Nikola only supports UTF-8 files'.format(source_path))
except Exception: # The file may not exist, for multilingual sites
return {}
@@ -407,8 +562,10 @@ def _get_metadata_from_file(meta_data):
'FooBar'
>>> str(g([".. title: FooBar"])["title"])
'FooBar'
- >>> 'title' in g(["",".. title: FooBar"])
+ >>> 'title' in g(["","",".. title: FooBar"])
False
+ >>> 'title' in g(["",".. title: FooBar"]) # for #520
+ True
"""
meta = {}
@@ -420,7 +577,11 @@ def _get_metadata_from_file(meta_data):
string.punctuation)))
for i, line in enumerate(meta_data):
- if not line:
+ # txt2tags requires an empty line at the beginning
+ # and since we are here because it's a 1-file post
+ # let's be flexible on what we accept, so, skip empty
+ # first lines.
+ if not line and i > 0:
break
if 'title' not in meta:
match = re_meta(line, 'title')
@@ -469,6 +630,12 @@ def get_metadata_from_meta_file(path, lang=None):
meta['description'] = description
return meta
+
+ 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, lang=None)
else:
return {}
@@ -487,6 +654,7 @@ def get_meta(post, file_metadata_regexp=None, lang=None):
if meta:
return meta
+ post.is_two_file = False
if file_metadata_regexp is not None:
meta.update(_get_metadata_from_filename_by_regex(post.source_path,
@@ -499,8 +667,8 @@ def get_meta(post, file_metadata_regexp=None, lang=None):
if 'slug' not in meta:
# If no slug is found in the metadata use the filename
- meta['slug'] = slugify(os.path.splitext(
- os.path.basename(post.source_path))[0])
+ meta['slug'] = slugify(unicode_str(os.path.splitext(
+ os.path.basename(post.source_path))[0]))
if 'title' not in meta:
# If no title is found, use the filename without extension
@@ -508,3 +676,84 @@ def get_meta(post, file_metadata_regexp=None, lang=None):
os.path.basename(post.source_path))[0]
return meta
+
+
+def hyphenate(dom, lang):
+ if pyphen is not None:
+ hyphenator = pyphen.Pyphen(lang=lang)
+ for tag in ('p', 'li', 'span'):
+ for node in dom.xpath("//%s[not(parent::pre)]" % tag):
+ insert_hyphens(node, hyphenator)
+ return dom
+
+
+def insert_hyphens(node, hyphenator):
+ textattrs = ('text', 'tail')
+ if isinstance(node, lxml.etree._Entity):
+ # HTML entities have no .text
+ textattrs = ('tail',)
+ for attr in textattrs:
+ text = getattr(node, attr)
+ if not text:
+ continue
+ new_data = ' '.join([hyphenator.inserted(w, hyphen='\u00AD')
+ for w in text.split(' ')])
+ # Spaces are trimmed, we have to add them manually back
+ if text[0].isspace():
+ new_data = ' ' + new_data
+ if text[-1].isspace():
+ new_data += ' '
+ setattr(node, attr, new_data)
+
+ 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
index 6e63474..fe2ab17 100644
--- a/nikola/rc4.py
+++ b/nikola/rc4.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
"""
Copyright (C) 2012 Bo Zhu http://about.bozhu.me
diff --git a/nikola/utils.py b/nikola/utils.py
index 423ded8..0a615f8 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Copyright © 2012-2013 Roberto Alsina and others.
# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
@@ -27,9 +28,11 @@
from __future__ import print_function, unicode_literals
from collections import defaultdict, Callable
+import calendar
import datetime
import hashlib
import locale
+import logging
import os
import re
import codecs
@@ -43,8 +46,65 @@ try:
except ImportError:
pass
+import logbook
+from logbook.more import ExceptionHandler
import pytz
+
+class ApplicationWarning(Exception):
+ pass
+
+
+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 = [logbook.StderrHandler(
+ level=logbook.NOTICE if not os.getenv('NIKOLA_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')
+
+if os.getenv('NIKOLA_DEBUG'):
+ logging.basicConfig(level=logging.DEBUG)
+else:
+ logging.basicConfig(level=logging.WARNING)
+
+
+def req_missing(names, purpose, python=True, optional=False):
+ """Log that we are missing some requirements."""
+ if not (isinstance(names, tuple) or isinstance(names, list) or isinstance(names, set)):
+ names = (names,)
+ if python:
+ whatarethey_s = 'Python package'
+ whatarethey_p = 'Python packages'
+ else:
+ whatarethey_s = whatarethey_p = 'software'
+ if len(names) == 1:
+ msg = 'In order to {0}, you must install the "{1}" {2}.'.format(
+ purpose, names[0], whatarethey_s)
+ else:
+ most = '", "'.join(names[:-1])
+ pnames = most + '" and "' + names[-1]
+ msg = 'In order to {0}, you must install the "{1}" {2}.'.format(
+ purpose, pnames, whatarethey_p)
+
+ if optional:
+ LOGGER.warn(msg)
+ else:
+ LOGGER.error(msg)
+ raise Exception('Missing dependencies: {0}'.format(', '.join(names)))
+
+ return msg
+
if sys.version_info[0] == 3:
# Python 3
bytes_str = bytes
@@ -65,8 +125,35 @@ import PyRSS2Gen as rss
__all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree',
'generic_rss_renderer', 'copy_file', 'slugify', 'unslugify',
'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs',
- 'get_asset_path', '_reload', 'unicode_str', 'bytes_str',
- 'unichr', 'Functionary', 'LocaleBorg']
+ 'get_tzname', 'get_asset_path', '_reload', 'unicode_str', 'bytes_str',
+ 'unichr', 'Functionary', 'LocaleBorg', 'sys_encode', 'sys_decode',
+ 'makedirs', 'get_parent_theme_name', 'ExtendedRSS2']
+
+
+ENCODING = sys.getfilesystemencoding() or sys.stdin.encoding
+
+
+def sys_encode(thing):
+ """Return bytes encoded in the system's encoding."""
+ if isinstance(thing, unicode_str):
+ return thing.encode(ENCODING)
+ return thing
+
+
+def sys_decode(thing):
+ """Returns unicode."""
+ if isinstance(thing, bytes_str):
+ return thing.decode(ENCODING)
+ return thing
+
+
+def makedirs(path):
+ """Create a folder."""
+ if not path or os.path.isdir(path):
+ return
+ if os.path.exists(path):
+ raise OSError('Path {0} already exists and is not a folder.')
+ os.makedirs(path)
class Functionary(defaultdict):
@@ -77,24 +164,12 @@ class Functionary(defaultdict):
super(Functionary, self).__init__(default)
self.default_lang = default_lang
- def current_lang(self):
- """Guess the current language from locale or default."""
- lang = locale.getlocale()[0]
- if lang:
- if lang in self.keys():
- return lang
- lang = lang.split('_')[0]
- if lang in self.keys():
- return lang
- # whatever
- return self.default_lang
-
def __call__(self, key, lang=None):
"""When called as a function, take an optional lang
and return self[lang][key]."""
if lang is None:
- lang = self.current_lang()
+ lang = LocaleBorg().current_lang
return self[lang][key]
@@ -114,7 +189,7 @@ class config_changed(tools.config_changed):
if isinstance(self.config, str):
return self.config
elif isinstance(self.config, dict):
- data = json.dumps(self.config, cls=CustomEncoder)
+ data = json.dumps(self.config, cls=CustomEncoder, sort_keys=True)
if isinstance(data, str): # pragma: no cover # python3
byte_data = data.encode("utf-8")
else:
@@ -155,19 +230,20 @@ def get_template_engine(themes):
return 'mako'
+def get_parent_theme_name(theme_name):
+ parent_path = os.path.join(get_theme_path(theme_name), 'parent')
+ if os.path.isfile(parent_path):
+ with open(parent_path) as fd:
+ return fd.readlines()[0].strip()
+ return None
+
+
def get_theme_chain(theme):
"""Create the full theme inheritance chain."""
themes = [theme]
- def get_parent(theme_name):
- parent_path = os.path.join(get_theme_path(theme_name), 'parent')
- if os.path.isfile(parent_path):
- with open(parent_path) as fd:
- return fd.readlines()[0].strip()
- return None
-
while True:
- parent = get_parent(themes[-1])
+ parent = get_parent_theme_name(themes[-1])
# Avoid silly loops
if parent is None or parent in themes:
break
@@ -175,6 +251,9 @@ def get_theme_chain(theme):
return themes
+warned = []
+
+
def load_messages(themes, translations, default_lang):
""" Load theme's messages into context.
@@ -182,11 +261,10 @@ def load_messages(themes, translations, default_lang):
and "younger" themes have priority.
"""
messages = Functionary(dict, default_lang)
- warned = []
oldpath = sys.path[:]
for theme_name in themes[::-1]:
msg_folder = os.path.join(get_theme_path(theme_name), 'messages')
- default_folder = os.path.join(get_theme_path('default'), 'messages')
+ default_folder = os.path.join(get_theme_path('base'), 'messages')
sys.path.insert(0, default_folder)
sys.path.insert(0, msg_folder)
english = __import__('messages_en')
@@ -195,12 +273,11 @@ def load_messages(themes, translations, default_lang):
translation = __import__('messages_' + lang)
reload(translation)
if sorted(translation.MESSAGES.keys()) !=\
- sorted(english.MESSAGES.keys()) and \
+ sorted(english.MESSAGES.keys()) and \
lang not in warned:
- # FIXME: get real logging in place
- print("Warning: Incomplete translation for language "
- "'{0}'.".format(lang))
warned.append(lang)
+ LOGGER.warn("Incomplete translation for language "
+ "'{0}'.".format(lang))
messages[lang].update(english.MESSAGES)
messages[lang].update(translation.MESSAGES)
del(translation)
@@ -230,8 +307,7 @@ def copy_tree(src, dst, link_cutoff=None):
if set(root_parts) & ignore:
continue
dst_dir = os.path.join(dst, *root_parts[base_len:])
- if not os.path.isdir(dst_dir):
- os.makedirs(dst_dir)
+ makedirs(dst_dir)
for src_name in files:
if src_name == '.DS_Store':
continue
@@ -247,31 +323,37 @@ def copy_tree(src, dst, link_cutoff=None):
def generic_rss_renderer(lang, title, link, description, timeline, output_path,
- rss_teasers):
+ rss_teasers, feed_length=10, feed_url=None):
"""Takes all necessary data, and renders a RSS feed in output_path."""
items = []
- for post in timeline[:10]:
+ for post in timeline[:feed_length]:
args = {
'title': post.title(lang),
'link': post.permalink(lang, absolute=True),
- 'description': post.text(lang, teaser_only=rss_teasers),
+ 'description': post.text(lang, teaser_only=rss_teasers, really_absolute=True),
'guid': post.permalink(lang, absolute=True),
# PyRSS2Gen's pubDate is GMT time.
'pubDate': (post.date if post.date.tzinfo is None else
post.date.astimezone(pytz.timezone('UTC'))),
+ 'categories': post._tags.get(lang, []),
+ 'author': post.meta('author'),
}
- items.append(rss.RSSItem(**args))
- rss_obj = rss.RSS2(
+
+ items.append(ExtendedItem(**args))
+ rss_obj = ExtendedRSS2(
title=title,
link=link,
description=description,
lastBuildDate=datetime.datetime.now(),
items=items,
generator='nikola',
+ language=lang
)
+ rss_obj.self_url = feed_url
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
+ rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/"
dst_dir = os.path.dirname(output_path)
- if not os.path.isdir(dst_dir):
- os.makedirs(dst_dir)
+ makedirs(dst_dir)
with codecs.open(output_path, "wb+", "utf-8") as rss_file:
data = rss_obj.to_xml(encoding='utf-8')
if isinstance(data, bytes_str):
@@ -281,8 +363,7 @@ def generic_rss_renderer(lang, title, link, description, timeline, output_path,
def copy_file(source, dest, cutoff=None):
dst_dir = os.path.dirname(dest)
- if not os.path.isdir(dst_dir):
- os.makedirs(dst_dir)
+ makedirs(dst_dir)
if os.path.islink(source):
link_target = os.path.relpath(
os.path.normpath(os.path.join(dst_dir, os.readlink(source))))
@@ -343,7 +424,7 @@ def unslugify(value):
"""
Given a slug string (as a filename), return a human readable string
"""
- value = re.sub('^[0-9]', '', value)
+ value = re.sub('^[0-9]+', '', value)
value = re.sub('([_\-\.])', ' ', value)
value = value.strip().capitalize()
return value
@@ -356,9 +437,10 @@ class UnsafeZipException(Exception):
pass
-def extract_all(zipfile):
+def extract_all(zipfile, path='themes'):
pwd = os.getcwd()
- os.chdir('themes')
+ makedirs(path)
+ os.chdir(path)
with zip(zipfile) as z:
namelist = z.namelist()
for f in namelist:
@@ -367,12 +449,7 @@ def extract_all(zipfile):
'not safe to expand.')
for f in namelist:
if f.endswith('/'):
- if not os.path.isdir(f):
- try:
- os.makedirs(f)
- except:
- raise OSError("Failed making {0} directory "
- "tree!".format(f))
+ makedirs(f)
else:
z.extract(f)
os.chdir(pwd)
@@ -408,12 +485,39 @@ def to_datetime(value, tzinfo=None):
try:
from dateutil import parser
dt = parser.parse(value)
- if tzinfo is None:
+ if tzinfo is None or dt.tzinfo:
return dt
return tzinfo.localize(dt)
except ImportError:
raise ValueError('Unrecognized date/time: {0!r}, try installing dateutil...'.format(value))
- raise ValueError('Unrecognized date/time: {0!r}'.format(value))
+ except Exception:
+ raise ValueError('Unrecognized date/time: {0!r}'.format(value))
+
+
+def get_tzname(dt):
+ """
+ Give a datetime value, find the name of the timezone
+ """
+ try:
+ from dateutil import tz
+ except ImportError:
+ raise ValueError('Unrecognized date/time: {0!r}, try installing dateutil...'.format(dt))
+
+ tzoffset = dt.strftime('%z')
+ for name in pytz.common_timezones:
+ timezone = tz.gettz(name)
+ now = dt.now(timezone)
+ offset = now.strftime('%z')
+ if offset == tzoffset:
+ return name
+ raise ValueError('Unrecognized date/time: {0!r}'.format(dt))
+
+
+def current_time(tzinfo=None):
+ dt = datetime.datetime.now()
+ if tzinfo is not None:
+ dt = tzinfo.localize(dt)
+ return dt
def apply_filters(task, filters):
@@ -435,7 +539,7 @@ def apply_filters(task, filters):
else:
assert False, key
- for target in task['targets']:
+ for target in task.get('targets', []):
ext = os.path.splitext(target)[-1].lower()
filter_ = filter_matches(ext)
if filter_:
@@ -495,25 +599,26 @@ def get_crumbs(path, is_file=False):
def get_asset_path(path, themes, files_folders={'files': ''}):
- """Checks which theme provides the path with the given asset,
- and returns the "real" path to the asset, relative to the
- current directory.
+ """
+ .. versionchanged:: 6.1.0
+
+ Checks which theme provides the path with the given asset,
+ and returns the "real", absolute path to the asset.
If the asset is not provided by a theme, then it will be checked for
in the FILES_FOLDERS
- >>> print(get_asset_path('assets/css/rst.css', ['site', 'default']))
- nikola/data/themes/default/assets/css/rst.css
+ >>> print(get_asset_path('assets/css/rst.css', ['bootstrap', 'base'])) # doctest: +SKIP
+ [...]/nikola/data/themes/base/assets/css/rst.css
- >>> print(get_asset_path('assets/css/theme.css', ['site', 'default']))
- nikola/data/themes/site/assets/css/theme.css
+ >>> print(get_asset_path('assets/css/theme.css', ['bootstrap', 'base'])) # doctest: +SKIP
+ [...]/nikola/data/themes/bootstrap/assets/css/theme.css
- >>> print(get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''}))
- nikola/nikola.py
+ >>> print(get_asset_path('nikola.py', ['bootstrap', 'base'], {'nikola': ''})) # doctest: +SKIP
+ [...]/nikola/nikola.py
- >>> print(get_asset_path('nikola/nikola.py', ['site', 'default'],
- ... {'nikola':'nikola'}))
- nikola/nikola.py
+ >>> print(get_asset_path('nikola/nikola.py', ['bootstrap', 'base'], {'nikola':'nikola'})) # doctest: +SKIP
+ [...]/nikola/nikola.py
"""
for theme_name in themes:
@@ -522,23 +627,168 @@ def get_asset_path(path, themes, files_folders={'files': ''}):
path
)
if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
+ return candidate
for src, rel_dst in files_folders.items():
- candidate = os.path.join(
- src,
- os.path.relpath(path, rel_dst)
- )
+ candidate = os.path.abspath(os.path.join(src, path))
if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
+ return candidate
# whatever!
return None
-class LocaleBorg:
- __shared_state = {
- 'current_lang': None
- }
+class LocaleBorg(object):
+ """
+ Provides locale related services and autoritative current_lang,
+ where current_lang is the last lang for which the locale was set.
+
+ current_lang is meant to be set only by LocaleBorg.set_locale
+
+ 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.
+
+ 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
+
+ Usage:
+ # early in cmd or test execution
+ LocaleBorg.initialize(...)
+
+ # any time later
+ 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
+
+ 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
+ That used to break calendar, but now seems is not the case, with month at least
+ """
+ @classmethod
+ def initialize(cls, locales, initial_lang):
+ """
+ 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"
+ """
+ assert initial_lang is not None and initial_lang in locales
+ cls.reset()
+ cls.locales = locales
+
+ # 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.__shared_state['current_lang'] = initial_lang
+ cls.initialized = True
+
+ @classmethod
+ def reset(cls):
+ """used in testing to not leak state between tests"""
+ cls.locales = {}
+ cls.encodings = {}
+ cls.__shared_state = {'current_lang': None}
+ cls.initialized = False
def __init__(self):
+ if not self.initialized:
+ raise Exception("Attempt to use LocaleBorg before initialization")
self.__dict__ = self.__shared_state
+
+ def set_locale(self, lang):
+ """Sets the locale for language lang, returns ''
+
+ 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]
+ """
+ # 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
+ locale_n = self.locales[lang]
+ self.__shared_state['current_lang'] = lang
+ locale.setlocale(locale.LC_ALL, locale_n)
+ return ''
+
+ def get_month_name(self, month_no, lang):
+ """returns localized month name in an unicode string"""
+ if sys.version_info[0] == 3: # Python 3
+ with calendar.different_locale(self.locales[lang]):
+ s = calendar.month_name[month_no]
+ # for py3 s is unicode
+ else: # Python 2
+ with calendar.TimeEncoding(self.locales[lang]):
+ s = calendar.month_name[month_no]
+ s = s.decode(self.encodings[lang])
+ # paranoid about calendar ending in the wrong locale (windows)
+ self.set_locale(self.current_lang)
+ return s
+
+
+class ExtendedRSS2(rss.RSS2):
+ def publish_extensions(self, handler):
+ if self.self_url:
+ handler.startElement("atom:link", {
+ 'href': self.self_url,
+ 'rel': "self",
+ 'type': "application/rss+xml"
+ })
+ handler.endElement("atom:link")
+
+
+class ExtendedItem(rss.RSSItem):
+
+ def __init__(self, **kw):
+ author = kw.pop('author')
+ if author and '@' in author[1:]: # Yes, this is a silly way to validate an email
+ kw['author'] = author
+ self.creator = None
+ else:
+ self.creator = author
+ # It's an old style class
+ return rss.RSSItem.__init__(self, **kw)
+
+ def publish_extensions(self, handler):
+ if self.creator:
+ handler.startElement("dc:creator", {})
+ handler.characters(self.creator)
+ handler.endElement("dc:creator")
+
+
+# \x00 means the "<" was backslash-escaped
+explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
+
+
+def split_explicit_title(text):
+ """Split role content into title and target, if given.
+
+ From Sphinx's "sphinx/util/nodes.py"
+ """
+ match = explicit_title_re.match(text)
+ if match:
+ return True, match.group(1), match.group(2)
+ return False, text, text
+
+
+def first_line(doc):
+ """extract first non-blank line from text, to extract docstring title"""
+ if doc is not None:
+ for line in doc.splitlines():
+ striped = line.strip()
+ if striped:
+ return striped
+ return ''
diff --git a/nikola/winutils.py b/nikola/winutils.py
new file mode 100644
index 0000000..291c99d
--- /dev/null
+++ b/nikola/winutils.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 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.
+
+"""windows utilities to workaround problems with symlinks in a git clone"""
+
+import os
+import shutil
+import sys
+# don't add imports outside stdlib, will be imported in setup.py
+
+
+def should_fix_git_symlinked():
+ """True if git symlinls markers should be filled with the real content"""
+ if sys.platform == 'win32':
+ path = (os.path.dirname(__file__) +
+ r'\data\samplesite\stories\theming.rst')
+ try:
+ if os.path.getsize(path) < 200:
+ return True
+ except Exception:
+ pass
+ return False
+
+
+def fix_git_symlinked(src, dst):
+ """fix git symlinked files in windows that had been copied from src to dst
+
+ Most (all?) of git implementations in windows store a symlink pointing
+ into the repo as a text file, the text being the relative path to the
+ file with the real content.
+
+ So, in a clone of nikola in windows the symlinked files will have the
+ wrong content.
+
+ The linux usage pattern for those files is 'copy to some dir, then use',
+ so we inspect after the copy and rewrite the wrong contents.
+
+ The goals are:
+ support running nikola from a clone without installing and without
+ making dirty the WC.
+
+ support install from the WC.
+
+ if possible and needed, support running the test suite without making
+ dirty the WC.
+ """
+ # if running from WC there should be a 'doc' dir sibling to nikola package
+ if not should_fix_git_symlinked():
+ return
+ # probabbly in a WC, so symlinks should be fixed
+ for root, dirs, files in os.walk(dst):
+ for name in files:
+ filename = os.path.join(root, name)
+
+ # detect if symlinked
+ try:
+ if not (2 < os.path.getsize(filename) < 500):
+ continue
+ # which encoding uses a git symlink marker ? betting on default
+ with open(filename, 'r') as f:
+ text = f.read()
+ if text[0] != '.':
+ # de facto hint to skip binary files and exclude.meta
+ continue
+ except Exception:
+ # probably encoding: content binary or encoding not defalt,
+ # also in py2.6 it can be path encoding
+ continue
+ dst_dir_relpath = os.path.dirname(os.path.relpath(filename, dst))
+ path = os.path.normpath(os.path.join(src, dst_dir_relpath, text))
+ if not os.path.exists(path):
+ continue
+ # most probably it is a git symlinked file
+
+ # copy original content to filename
+ shutil.copy(path, filename)
diff --git a/requirements-full.txt b/requirements-full.txt
new file mode 100644
index 0000000..0740c23
--- /dev/null
+++ b/requirements-full.txt
@@ -0,0 +1,15 @@
+-r requirements.txt
+mock>=1.0.0
+requests>=1.0
+markdown
+Jinja2>=2.7
+bbcode
+https://github.com/lepture/python-livereload/archive/master.zip
+pyphen
+python-dateutil
+micawber
+pygal
+typogrify
+phpserialize
+webassets
+ipython>=1.0.0
diff --git a/requirements-tests.txt b/requirements-tests.txt
new file mode 100644
index 0000000..5c94a5c
--- /dev/null
+++ b/requirements-tests.txt
@@ -0,0 +1,6 @@
+-r requirements-full.txt
+coverage
+nose
+flake8
+freezegun
+python-coveralls
diff --git a/requirements.txt b/requirements.txt
index 03451ef..6df3a0e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,12 @@
-mock>=1.0.0
-requests>=1.0
-markdown
-Jinja2
-bbcode
+doit>=0.23.0
+pygments
+pillow>=2.0.0
+docutils
+mako>=0.6
+unidecode
+lxml
+yapsy==1.10.2-pythons2n3
+PyRSS2Gen
+pytz
+logbook
+blinker
diff --git a/scripts/import_po.py b/scripts/import_po.py
index 6514391..89e1089 100644..100755
--- a/scripts/import_po.py
+++ b/scripts/import_po.py
@@ -12,7 +12,7 @@ os.system("tx pull -a")
trans_files = glob(os.path.join('translations', 'nikola.messages', '*.po'))
for fname in trans_files:
lang = os.path.splitext(os.path.basename(fname))[0].lower()
- outf = os.path.join('nikola', 'data', 'themes', 'default',
+ outf = os.path.join('nikola', 'data', 'themes', 'base',
'messages', 'messages_{0}.py'.format(lang))
po = polib.pofile(fname)
lines = """# -*- encoding:utf-8 -*-
diff --git a/scripts/nikola.bat b/scripts/nikola.bat
index 0aad2d4..0aad2d4 100644..100755
--- a/scripts/nikola.bat
+++ b/scripts/nikola.bat
diff --git a/scripts/set_version.py b/scripts/set_version.py
new file mode 100755
index 0000000..e34c7ae
--- /dev/null
+++ b/scripts/set_version.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""Script to set the version number wherever it's needed before a release."""
+
+from __future__ import unicode_literals, print_function
+import codecs
+import os
+import re
+import sys
+import glob
+
+
+def sed_like_thing(pattern, repl, path):
+ """Like re.sub but applies to a file instead of a string."""
+
+ with codecs.open(path, 'rb', 'utf8') as inf:
+ data = inf.read()
+
+ data = re.sub(pattern, repl, data)
+
+ with codecs.open(path, 'wb+', 'utf8') as outf:
+ outf.write(data)
+
+if __name__ == "__main__":
+ print("New version number: ", end="")
+ sys.stdout.flush()
+ version = sys.stdin.readline().strip()
+
+ for doc in glob.glob(os.path.join("docs/*.txt")):
+ sed_like_thing(":Version: .*", ":Version: {0}".format(version), doc)
+
+ sed_like_thing("version='.+'", "version='{0}'".format(version), 'setup.py')
+ sed_like_thing('__version__ = ".*"', '__version__ = "{0}"'.format(version), os.path.join('nikola', '__init__.py'))
+ sed_like_thing('New in Master', 'New in {0}'.format(version), 'CHANGES.txt')
+ os.system("help2man -h help -N --version-string='{0}' nikola > {1}".format(version, os.path.join('docs', 'man', 'nikola.1')))
diff --git a/setup.py b/setup.py
index 9fbe258..3cf70eb 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,8 @@
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
-from __future__ import print_function
+# Don't use __future__ in this script, it breaks buildout
+# from __future__ import print_function
import os
import subprocess
import sys
@@ -29,18 +30,8 @@ except ImportError:
from distutils.command.install import install
from distutils.util import convert_path # NOQA
-dependencies = [
- 'doit>=0.20.0',
- 'pygments',
- 'pillow',
- 'docutils',
- 'mako>=0.6',
- 'unidecode',
- 'lxml',
- 'yapsy',
- 'PyRSS2Gen',
- 'pytz',
-]
+with open('requirements.txt', 'r') as fh:
+ dependencies = [l.strip() for l in fh]
########### platform specific stuff #############
import platform
@@ -51,15 +42,16 @@ scripts = ['scripts/nikola']
if platform_system == "Windows":
scripts.append('scripts/nikola.bat')
+if sys.version_info[0] == 2 and sys.version_info[1] < 6:
+ raise Exception('Python 2 version < 2.6 is not supported')
+elif sys.version_info[0] == 3 and sys.version_info[1] < 3:
+ raise Exception('Python 3 version < 3.3 is not supported')
+
##################################################
if sys.version_info[0] == 2:
# in Python 3 this becomes a builtin, for Python 2 we need the backport
dependencies.append('configparser')
-elif sys.version_info[0] == 3:
- # Pillow introduced support for Python 3 with 2.0.0
- dependencies.remove('pillow')
- dependencies.append('pillow>=2.0.0')
# Provided as an attribute, so you can append to these instead
# of replicating them:
@@ -84,6 +76,44 @@ def copy_messages():
shutil.copytree(original_messages_directory, theme_messages_directory)
+def copy_symlinked_for_windows():
+ """replaces the symlinked files with a copy of the original content.
+
+ In windows (msysgit), a symlink is converted to a text file with a
+ path to the file it points to. If not corrected, installing from a git
+ clone will end with some files with bad content
+
+ After install the WC will be dirty (symlink markers rewroted with real
+ content)
+ """
+
+ # essentially nikola.utils.should_fix_git_symlinked inlined, to not
+ # fiddle with sys.path / import unless really needed
+ if sys.platform != 'win32':
+ return
+ path = (os.path.dirname(__file__) +
+ r'nikola\data\samplesite\stories\theming.rst')
+ try:
+ if os.path.getsize(path) < 200:
+ pass
+ else:
+ return
+ except Exception:
+ return
+
+ # apply the fix
+ localdir = os.path.dirname(__file__)
+ dst = os.path.join(localdir, 'nikola', 'data', 'samplesite')
+ src = dst
+ oldpath = sys.path[:]
+ sys.path.insert(0, os.path.join(localdir, 'nikola'))
+ winutils = __import__('winutils')
+ winutils.fix_git_symlinked(src, dst)
+ sys.path = oldpath
+ del sys.modules['winutils']
+ print('WARNING: your working copy is now dirty by changes in samplesite')
+
+
def install_manpages(root, prefix):
try:
man_pages = [
@@ -113,6 +143,7 @@ def install_manpages(root, prefix):
class nikola_install(install):
def run(self):
+ copy_symlinked_for_windows()
install.run(self)
install_manpages(self.root, self.prefix)
@@ -196,24 +227,49 @@ def find_package_data(
out.setdefault(package, []).append(prefix + name)
return out
+
setup(name='Nikola',
- version='5.4.4',
- description='Static blog/website generator',
+ version='6.2.1',
+ description='A modular, fast, simple, static website generator',
+ long_description=open('README.rst').read(),
author='Roberto Alsina and others',
author_email='ralsina@netmanagers.com.ar',
- url='http://nikola.ralsina.com.ar/',
+ url='http://getnikola.com',
packages=['nikola',
'nikola.plugins',
- 'nikola.plugins.command_planetoid',
- 'nikola.plugins.compile_ipynb',
- 'nikola.plugins.compile_markdown',
- 'nikola.plugins.compile_misaka',
- 'nikola.plugins.compile_rest',
- 'nikola.plugins.task_localsearch',
- 'nikola.plugins.task_mustache',
- 'nikola.plugins.task_sitemap',
+ 'nikola.plugins.command',
+ 'nikola.plugins.command.planetoid',
+ 'nikola.plugins.compile',
+ 'nikola.plugins.compile.ipynb',
+ 'nikola.plugins.compile.markdown',
+ 'nikola.plugins.compile.rest',
+ 'nikola.plugins.task',
+ 'nikola.plugins.task.localsearch',
+ 'nikola.plugins.task.mustache',
+ 'nikola.plugins.task.sitemap',
+ 'nikola.plugins.template',
],
+ license='MIT',
+ keywords='website, static',
scripts=scripts,
+ classifiers=('Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Environment :: Plugins',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: End Users/Desktop',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: MacOS',
+ 'Operating System :: Microsoft :: Windows',
+ 'Operating System :: OS Independent',
+ 'Operating System :: POSIX',
+ 'Operating System :: Unix',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3.3',
+ 'Topic :: Internet',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Text Processing :: Markup'),
install_requires=dependencies,
package_data=find_package_data(),
cmdclass={'install': nikola_install},
diff --git a/tests/README.rst b/tests/README.rst
new file mode 100644
index 0000000..2b3afb8
--- /dev/null
+++ b/tests/README.rst
@@ -0,0 +1,92 @@
+.. title: The Nikola Test Suite
+.. slug: tests
+.. date: 2012/03/30 23:00
+
+The Nikola Test Suite
+=====================
+
+Nikola, like many software projects, has a test suite. There are over 100
+tests.
+
+Tests (in alphabetical order)
+-----------------------------
+
+* ``test_command_import_wordpress`` tests the WordPress importer for
+ Nikola.
+* ``test_command_init`` checks whether new sites are created properly via the
+ ``init`` command.
+* ``test_compile_markdown`` exercises the Markdown compiler plugin of Nikola.
+* ``test_integration`` are used to validate that sites actually build.
+* ``test_locale`` tests the locale support of Nikola.
+* ``test_plugin_importing`` checks three basic plugins to know whether they
+ get imported properly.
+* ``test_rss_feeds`` asserts that RSS created by Nikola is sane.
+* ``test_rst_compiler`` exercises the reStructuredText compiler plugin of
+ Nikola.
+* ``test_scheduling`` performs tests on post scheduling rules.
+* ``test_utils`` test various Nikola utilities.
+
+Requirements to run the tests
+-----------------------------
+
+You need:
+
+* ``pip install -r requirements-tests.txt``
+* a few minutes’ time
+* appropriate locale settings
+
+How to set the locale for Nikola tests?
+---------------------------------------
+
+For testing nikola needs to specify two languages, each one with a supported locale. By default, the test suite uses ``en`` and ``es`` as languages, and their respective default locale for them.
+
+You can set the language - locale pairs by exporting two shell variables, like in::
+
+ export NIKOLA_LOCALE_DEFAULT=en,en_US.utf8
+ export NIKOLA_LOCALE_OTHER=es,es_ES.utf8
+
+In Windows that would be::
+
+ set NIKOLA_LOCALE_DEFAULT=en,English
+ set NIKOLA_LOCALE_OTHER=es,Spanish
+
+Replace the part before the comma with a Nikola translation selector (see ``nikola/conf.py.in`` for details), and the part after the comma with an *installed* glibc locale.
+
+To check if the desired locale is supported in your host you can, in a python console::
+
+ import locale
+ locale.setlocale(locale.LC_ALL, 'locale_name')
+ # by example, 'en_US.utf8' (posix) 'English' (windows)
+ # if it does not traceback, then python can use that locale
+
+Alternatively, if you have some disk space to spare, you can install
+the two default locales. Here is how to do that in Ubuntu::
+
+ sudo apt-get install language-pack-en language-pack-es
+
+
+How to execute the tests
+------------------------
+
+The command to execute tests is::
+
+ nosetests --with-coverage --cover-package=nikola --with-doctest --doctest-options=+NORMALIZE_WHITESPACE --logging-filter=-yapsy
+
+However, this command may change at any given moment. Check the
+``/.travis.yml`` file to get the current command.
+
+In Windows you want to drop the doctests parts, they fail over trivial differences in OS details.
+
+It is also recommended to run ``nikola help`` to see if Nikola actually
+works.
+
+If you are committing code, make sure to run ``flake8 --ignore=E501 .`` to see if you comply with the PEP 8 style guide and do not have basic code mistakes (we ignore the 79-characters-per-line rule).
+
+In windows ignore the two flake8 diagnostics about messages_sl_si.py , they are artifacts of (symlinks + git + windows).
+
+
+Travis CI
+---------
+
+We also run our tests on `Travis CI <https://travis-ci.org/>`_.
+You can check the `current build status <https://travis-ci.org/getnikola/nikola>`_ there.
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/base.py b/tests/base.py
index 92576c7..00f7486 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -6,12 +6,21 @@
""" Base class for Nikola test cases """
-__all__ = ["BaseTestCase"]
-
+__all__ = ["BaseTestCase", "cd", "LocaleSupportInTesting"]
+from contextlib import contextmanager
+import locale
+import os
import sys
import unittest
+import logbook
+
+# Make logbook shutup
+import nikola.utils
+
+nikola.utils.LOGGER.handlers.append(logbook.TestHandler())
+
if sys.version_info < (2, 7):
@@ -56,3 +65,99 @@ if sys.version_info < (2, 7):
else:
BaseTestCase = unittest.TestCase
+
+
+@contextmanager
+def cd(path):
+ old_dir = os.getcwd()
+ os.chdir(path)
+ yield
+ os.chdir(old_dir)
+
+
+class LocaleSupportInTesting(object):
+ """
+ Nikola needs two pairs of valid (language, locale_n) to test multilingual sites.
+
+ As languages of interest and installed OS support varies from host to host
+ we allow to specify two such pairs.
+
+ A valid pair complies
+ 'languaje' one of the names of nikola translations ('en', 'es', ...)
+ 'locale_n' is a string that python accepts to set a locale, like in
+ import locale
+ locale.setlocale(locale.LC_ALL, str(locale_n))
+
+ You specify the custom pairs to use with two environment variables
+ NIKOLA_LOCALE_DEFAULT (lang and locale to use as nikola's DEFAULT_LANG)
+ NIKOLA_LOCALE_OTHER
+
+ The value of the pair is lang (as in keys of Nikola's TRANSLATIONS), followed
+ by coma, followed by the locale.
+ """
+
+ @classmethod
+ def initialize(cls):
+ """Determines and diagnoses the two (lang, locale) pairs to use in testing
+
+ While it only needs to run once at the beginning of the testing session,
+ calling multiple times is fine.
+ """
+ if hasattr(cls, 'langlocales'):
+ return
+ defaults = {
+ 'linux': {
+ # non-windows defaults, must be two locales suported by .travis.yml
+ 'default': ("en", str("en_US.utf8")),
+ 'other': ("es", str("es_ES.utf8")),
+ },
+ 'windows': {
+ # windows defaults
+ 'default': ("en", str("English")),
+ 'other': ("es", str("Spanish")),
+ },
+ }
+ os_id = 'windows' if sys.platform == 'win32' else 'linux'
+ langlocales = {}
+ for suffix in ['other', 'default']:
+ try:
+ envar = 'NIKOLA_LOCALE_' + suffix.upper()
+ s = os.environ[envar]
+ parts = s.split(',')
+ lang = parts[0].strip()
+ try:
+ locale_n = str(parts[1].strip())
+ locale.setlocale(locale.LC_ALL, locale_n)
+ except Exception:
+ msg = ("Environment variable {0} fails to specify a valid <lang>,<locale>." +
+ "Check your syntax, check that python supports that locale in your host.")
+ nikola.utils.LOGGER.error(msg.format(envar))
+ sys.exit(1)
+ except KeyError:
+ lang, locale_n = defaults[os_id][suffix]
+ langlocales[suffix] = (lang, locale_n)
+ if (langlocales['default'][0] == langlocales['other'][0] or
+ langlocales['default'][1] == langlocales['other'][1]): # NOQA
+ # the mix of defaults and enviro is not good
+ msg = ('Locales for testing should differ in lang and locale, else ' +
+ 'the test would we weak. Check your environment settings for ' +
+ 'NIKOLA_LOCALE_DEFAULT and NIKOLA_LOCALE_OTHER')
+ nikola.utils.LOGGER.error(msg)
+ setattr(cls, 'langlocales', langlocales)
+
+ @classmethod
+ def initialize_locales_for_testing(cls, variant):
+ """initializes nikola.utils.LocaleBorg"""
+ if not hasattr(cls, 'langlocales'):
+ cls.initialize()
+ default_lang = cls.langlocales['default'][0]
+ locales = {}
+ locales[default_lang] = cls.langlocales['default'][1]
+ if variant == 'unilingual':
+ pass
+ elif variant == 'bilingual':
+ locales[cls.langlocales['other'][0]] = cls.langlocales['other'][1]
+ else:
+ raise ValueError('Unknown locale variant')
+ nikola.utils.LocaleBorg.reset()
+ nikola.utils.LocaleBorg.initialize(locales, default_lang)
diff --git a/tests/data/translated_titles/conf.py b/tests/data/translated_titles/conf.py
index 69c7bc7..b445ba9 100644
--- a/tests/data/translated_titles/conf.py
+++ b/tests/data/translated_titles/conf.py
@@ -1,4 +1,3 @@
-
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import time
@@ -72,7 +71,7 @@ SIDEBAR_LINKS = {
# The wildcard is used to generate a list of reSt source files
# (whatever/thing.txt).
# That fragment must have an associated metadata file (whatever/thing.meta),
-# and opcionally translated files (example for spanish, with code "es"):
+# and optionally translated files (example for spanish, with code "es"):
# whatever/thing.txt.es and whatever/thing.meta.es
#
# From those files, a set of HTML fragment files will be generated:
@@ -106,7 +105,7 @@ post_pages = (
# 'rest' is reStructuredText
# 'markdown' is MarkDown
# 'html' assumes the file is html and just copies it
-post_compilers = {
+COMPILERS = {
"rest": ('.txt', '.rst'),
"markdown": ('.md', '.mdown', '.markdown'),
"textile": ('.textile',),
@@ -340,10 +339,14 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# external resources.
# USE_CDN = False
-# Google analytics or whatever else you use. Added to the bottom of <body>
+# Google analytics script or whatever else you use. Added to the bottom of <body>
# in the default template (base.tmpl).
# ANALYTICS = ""
+# HTML snippet that will be added at the bottom of body of <body>
+# in the default template (base.tmpl).
+# SOCIAL_BUTTONS_CODE = ""
+
# The possibility to extract metadata from the filename by using a
# regular expression.
# To make it work you need to name parts of your regular expression.
diff --git a/tests/import_wordpress_and_build_workflow.py b/tests/import_wordpress_and_build_workflow.py
index 90cb6a8..bc04a1f 100644
--- a/tests/import_wordpress_and_build_workflow.py
+++ b/tests/import_wordpress_and_build_workflow.py
@@ -26,12 +26,13 @@ def main(import_directory=None):
test_directory = os.path.dirname(__file__)
package_directory = os.path.abspath(os.path.join(test_directory, '..'))
- os.system('echo "y" | pip uninstall Nikola')
+ os.system('pip uninstall -y Nikola')
os.system('pip install %s' % package_directory)
os.system('nikola')
import_file = os.path.join(test_directory, 'wordpress_export_example.xml')
os.system(
- 'nikola import_wordpress -f %s -o %s' % (import_file, import_directory))
+ 'nikola import_wordpress -o {folder} {file}'.format(file=import_file,
+ folder=import_directory))
assert os.path.exists(
import_directory), "The directory %s should be existing."
diff --git a/tests/test_command_import_wordpress.py b/tests/test_command_import_wordpress.py
index 3be2ad9..f215705 100644
--- a/tests/test_command_import_wordpress.py
+++ b/tests/test_command_import_wordpress.py
@@ -1,15 +1,19 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
-from context import nikola
+from .context import nikola
import os
import unittest
import mock
+import nikola.plugins.command.import_wordpress
+from .base import BaseTestCase
-class BasicCommandImportWordpress(unittest.TestCase):
+
+class BasicCommandImportWordpress(BaseTestCase):
def setUp(self):
- self.import_command = nikola.plugins.command_import_wordpress.CommandImportWordpress()
+ self.module = nikola.plugins.command.import_wordpress
+ self.import_command = self.module.CommandImportWordpress()
self.import_filename = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'wordpress_export_example.xml'))
@@ -51,11 +55,11 @@ class CommandImportWordpressRunTest(BasicCommandImportWordpress):
site_generation_patch = mock.patch('os.system', self.site_generation)
data_import_patch = mock.patch(
- 'nikola.plugins.command_import_wordpress.CommandImportWordpress.import_posts', self.data_import)
+ 'nikola.plugins.command.import_wordpress.CommandImportWordpress.import_posts', self.data_import)
write_urlmap_patch = mock.patch(
- 'nikola.plugins.command_import_wordpress.CommandImportWordpress.write_urlmap_csv', self.write_urlmap)
+ 'nikola.plugins.command.import_wordpress.CommandImportWordpress.write_urlmap_csv', self.write_urlmap)
write_configuration_patch = mock.patch(
- 'nikola.plugins.command_import_wordpress.CommandImportWordpress.write_configuration', self.write_configuration)
+ 'nikola.plugins.command.import_wordpress.CommandImportWordpress.write_configuration', self.write_configuration)
self.patches = [site_generation_patch, data_import_patch,
write_urlmap_patch, write_configuration_patch]
@@ -117,7 +121,7 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
self.import_filename)
context = self.import_command.populate_context(channel)
- for required_key in ('POST_PAGES', 'POST_COMPILERS'):
+ for required_key in ('POSTS', 'PAGES', 'COMPILERS'):
self.assertTrue(required_key in context)
self.assertEqual('de', context['DEFAULT_LANG'])
@@ -133,33 +137,37 @@ class CommandImportWordpressTest(BasicCommandImportWordpress):
self.import_filename)
self.import_command.context = self.import_command.populate_context(
channel)
- self.import_command.url_map = {} # For testing we use an empty one.
self.import_command.output_folder = 'new_site'
self.import_command.squash_newlines = True
self.import_command.no_downloads = False
+ # Ensuring clean results
+ self.import_command.url_map = {}
+ self.module.links = {}
+
write_metadata = mock.MagicMock()
write_content = mock.MagicMock()
download_mock = mock.MagicMock()
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.write_content', write_content):
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.write_metadata', write_metadata):
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.download_url_content_to_file', download_mock):
- with mock.patch('nikola.plugins.command_import_wordpress.os.makedirs'):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.write_content', write_content):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.write_metadata', write_metadata):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.download_url_content_to_file', download_mock):
+ with mock.patch('nikola.plugins.command.import_wordpress.os.makedirs'):
self.import_command.import_posts(channel)
self.assertTrue(download_mock.called)
+ qpath = 'new_site/files/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png'
download_mock.assert_any_call(
'http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png',
- 'new_site/files/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png')
+ qpath.replace('/', os.sep))
self.assertTrue(write_metadata.called)
write_metadata.assert_any_call(
- 'new_site/stories/kontakt.meta', 'Kontakt',
+ 'new_site/stories/kontakt.meta'.replace('/', os.sep), 'Kontakt',
'kontakt', '2009-07-16 20:20:32', None, [])
self.assertTrue(write_content.called)
- write_content.assert_any_call('new_site/posts/200704hoert.wp',
+ write_content.assert_any_call('new_site/posts/200704hoert.wp'.replace('/', os.sep),
"""An image.
<img class="size-full wp-image-16" title="caption test" src="http://some.blog/wp-content/uploads/2009/07/caption_test.jpg" alt="caption test" width="739" height="517" />
@@ -179,11 +187,11 @@ The end.
""")
write_content.assert_any_call(
- 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp',
+ 'new_site/posts/200807arzt-und-pfusch-s-i-c-k.wp'.replace('/', os.sep),
'''<img class="size-full wp-image-10 alignright" title="Arzt+Pfusch - S.I.C.K." src="http://some.blog/wp-content/uploads/2008/07/arzt_und_pfusch-sick-cover.png" alt="Arzt+Pfusch - S.I.C.K." width="210" height="209" />Arzt+Pfusch - S.I.C.K.Gerade bin ich \xfcber das Album <em>S.I.C.K</em> von <a title="Arzt+Pfusch" href="http://www.arztpfusch.com/" target="_blank">Arzt+Pfusch</a> gestolpert, welches Arzt+Pfusch zum Download f\xfcr lau anbieten. Das Album steht unter einer Creative Commons <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/de/">BY-NC-ND</a>-Lizenz.
Die Ladung <em>noisebmstupidevildustrial</em> gibts als MP3s mit <a href="http://www.archive.org/download/dmp005/dmp005_64kb_mp3.zip">64kbps</a> und <a href="http://www.archive.org/download/dmp005/dmp005_vbr_mp3.zip">VBR</a>, als Ogg Vorbis und als FLAC (letztere <a href="http://www.archive.org/details/dmp005">hier</a>). <a href="http://www.archive.org/download/dmp005/dmp005-artwork.zip">Artwork</a> und <a href="http://www.archive.org/download/dmp005/dmp005-lyrics.txt">Lyrics</a> gibts nochmal einzeln zum Download.''')
write_content.assert_any_call(
- 'new_site/stories/kontakt.wp', """<h1>Datenschutz</h1>
+ 'new_site/stories/kontakt.wp'.replace('/', os.sep), """<h1>Datenschutz</h1>
Ich erhebe und speichere automatisch in meine Server Log Files Informationen, die dein Browser an mich \xfcbermittelt. Dies sind:
<ul>
@@ -209,15 +217,36 @@ Diese Daten sind f\xfcr mich nicht bestimmten Personen zuordenbar. Eine Zusammen
self.import_command.url_map['http://some.blog/kontakt/'],
'http://some.blog/stories/kontakt.html')
+ image_thumbnails = [
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-64x64.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-300x175.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-36x36.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-24x24.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-48x48.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-96x96.png',
+ 'http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-150x150.png'
+ ]
+
+ for link in image_thumbnails:
+ self.assertTrue(
+ link in self.module.links,
+ 'No link to "{0}" found in {map}.'.format(
+ link,
+ map=self.module.links
+ )
+ )
+
def test_transforming_content(self):
"""Applying markup conversions to content."""
transform_sourcecode = mock.MagicMock()
transform_caption = mock.MagicMock()
transform_newlines = mock.MagicMock()
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_sourcecode', transform_sourcecode):
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_caption', transform_caption):
- with mock.patch('nikola.plugins.command_import_wordpress.CommandImportWordpress.transform_multiple_newlines', transform_newlines):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_sourcecode', transform_sourcecode):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_caption', transform_caption):
+ with mock.patch('nikola.plugins.command.import_wordpress.CommandImportWordpress.transform_multiple_newlines', transform_newlines):
self.import_command.transform_content("random content")
self.assertTrue(transform_sourcecode.called)
@@ -327,7 +356,7 @@ newlines.
config_path_with_timestamp = self.import_command.get_configuration_output_path(
)
self.assertNotEqual(default_config_path, config_path_with_timestamp)
- self.assertTrue('wordpress_import' in config_path_with_timestamp)
+ self.assertTrue(self.import_command.name in config_path_with_timestamp)
def test_write_content_does_not_detroy_text(self):
content = b"""<h1>Installation</h1>
@@ -338,7 +367,7 @@ There are many plugins.
<h2>Violations</h2>
You can use the <a title="Jenkins Plugin: Violations" href="https://wiki.jenkins-ci.org/display/JENKINS/Violations">Violations</a> plugin."""
open_mock = mock.mock_open()
- with mock.patch('nikola.plugins.command_import_wordpress.open', open_mock, create=True):
+ with mock.patch('nikola.plugins.basic_import.open', open_mock, create=True):
self.import_command.write_content('some_file', content)
open_mock.assert_called_once_with('some_file', 'wb+')
@@ -346,5 +375,20 @@ You can use the <a title="Jenkins Plugin: Violations" href="https://wiki.jenkins
call_context.write.assert_called_once_with(
content.join([b'<html><body>', b'</body></html>']))
+ def test_configure_redirections(self):
+ """
+ Testing the configuration of the redirections.
+
+ We need to make sure that we have valid sources and target links.
+ """
+ url_map = {
+ '/somewhere/else': 'http://foo.bar/posts/somewhereelse.html'
+ }
+
+ redirections = self.import_command.configure_redirections(url_map)
+
+ self.assertEqual(1, len(redirections))
+ self.assertTrue(('somewhere/else/index.html', '/posts/somewhereelse.html') in redirections)
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_command_init.py b/tests/test_command_init.py
index 1904fa1..a9ec208 100644
--- a/tests/test_command_init.py
+++ b/tests/test_command_init.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
-from context import nikola
+from .context import nikola
import unittest
import mock
@@ -12,18 +12,18 @@ class CommandInitCallTest(unittest.TestCase):
self.create_configuration = mock.MagicMock()
self.create_empty_site = mock.MagicMock()
copy_sample_site_patch = mock.patch(
- 'nikola.plugins.command_init.CommandInit.copy_sample_site', self.copy_sample_site)
+ 'nikola.plugins.command.init.CommandInit.copy_sample_site', self.copy_sample_site)
create_configuration_patch = mock.patch(
- 'nikola.plugins.command_init.CommandInit.create_configuration', self.create_configuration)
+ 'nikola.plugins.command.init.CommandInit.create_configuration', self.create_configuration)
create_empty_site_patch = mock.patch(
- 'nikola.plugins.command_init.CommandInit.create_empty_site', self.create_empty_site)
+ 'nikola.plugins.command.init.CommandInit.create_empty_site', self.create_empty_site)
self.patches = [copy_sample_site_patch,
create_configuration_patch, create_empty_site_patch]
for patch in self.patches:
patch.start()
- self.init_commad = nikola.plugins.command_init.CommandInit()
+ self.init_commad = nikola.plugins.command.init.CommandInit()
def tearDown(self):
for patch in self.patches:
diff --git a/tests/test_compile_markdown.py b/tests/test_compile_markdown.py
index a1f8591..a8252d8 100644
--- a/tests/test_compile_markdown.py
+++ b/tests/test_compile_markdown.py
@@ -6,7 +6,14 @@ import tempfile
import unittest
from os import path
-from nikola.plugins.compile_markdown import CompileMarkdown
+from nikola.plugins.compile.markdown import CompileMarkdown
+
+
+class FakeSite(object):
+ config = {
+ "MARKDOWN_EXTENSIONS": ['fenced_code', 'codehilite'],
+ "LOGGING_HANDLERS": {'stderr': {'loglevel': 'WARNING', 'bubble': True}}
+ }
class CompileMarkdownTests(unittest.TestCase):
@@ -16,6 +23,7 @@ class CompileMarkdownTests(unittest.TestCase):
self.output_path = path.join(self.tmp_dir, 'output.html')
self.compiler = CompileMarkdown()
+ self.compiler.set_site(FakeSite())
def compile(self, input_string):
with codecs.open(self.input_path, "w+", "utf8") as input_file:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 802dcc7..fdb2494 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,44 +1,40 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, print_function
+from __future__ import unicode_literals, print_function, absolute_import
import codecs
-from contextlib import contextmanager
import locale
import os
import shutil
-import subprocess # NOQA
+import subprocess
+import sys
import tempfile
import unittest
import lxml.html
from nose.plugins.skip import SkipTest
-from context import nikola
from nikola import main
+import nikola
+import nikola.plugins.command
+import nikola.plugins.command.init
+from .base import BaseTestCase, cd
-@contextmanager
-def cd(path):
- old_dir = os.getcwd()
- os.chdir(path)
- yield
- os.chdir(old_dir)
-
-class EmptyBuildTest(unittest.TestCase):
+class EmptyBuildTest(BaseTestCase):
"""Basic integration testcase."""
dataname = None
@classmethod
- def setUpClass(self):
+ def setUpClass(cls):
"""Setup a demo site."""
- self.tmpdir = tempfile.mkdtemp()
- self.target_dir = os.path.join(self.tmpdir, "target")
- self.init_command = nikola.plugins.command_init.CommandInit()
- self.fill_site()
- self.patch_site()
- self.build()
+ cls.tmpdir = tempfile.mkdtemp()
+ cls.target_dir = os.path.join(cls.tmpdir, "target")
+ cls.init_command = nikola.plugins.command.init.CommandInit()
+ cls.fill_site()
+ cls.patch_site()
+ cls.build()
@classmethod
def fill_site(self):
@@ -69,7 +65,13 @@ class EmptyBuildTest(unittest.TestCase):
@classmethod
def tearDownClass(self):
"""Remove the demo site."""
- shutil.rmtree(self.tmpdir)
+ # ignore_errors=True for windows by issue #782
+ shutil.rmtree(self.tmpdir, ignore_errors=(sys.platform == 'win32'))
+ # Fixes Issue #438
+ try:
+ del sys.modules['conf']
+ except KeyError:
+ pass
def test_build(self):
"""Ensure the build did something."""
@@ -91,9 +93,82 @@ class DemoBuildTest(EmptyBuildTest):
outf.write(
".. title: foobar\n"
".. slug: foobar\n"
- ".. date: 2013/03/06 19:08:15\n"
+ ".. date: 2013-03-06 19:08:15\n"
+ )
+
+ def test_index_in_sitemap(self):
+ sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml")
+ sitemap_data = codecs.open(sitemap_path, "r", "utf8").read()
+ self.assertTrue('<loc>http://getnikola.com/index.html</loc>' in sitemap_data)
+
+ def test_avoid_double_slash_in_rss(self):
+ rss_path = os.path.join(self.target_dir, "output", "rss.xml")
+ rss_data = codecs.open(rss_path, "r", "utf8").read()
+ self.assertFalse('http://getnikola.com//' in rss_data)
+
+
+class RepeatedPostsSetting(DemoBuildTest):
+ """Duplicate POSTS, should not read each post twice, which causes conflicts."""
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "ab", "utf8") as outf:
+ outf.write('\nPOSTS = (("posts/*.txt", "posts", "post.tmpl"),("posts/*.txt", "posts", "post.tmpl"))\n')
+
+
+class FuturePostTest(EmptyBuildTest):
+ """Test a site with future posts."""
+
+ @classmethod
+ def fill_site(self):
+ import datetime
+ from nikola.utils import current_time
+ self.init_command.copy_sample_site(self.target_dir)
+ self.init_command.create_configuration(self.target_dir)
+
+ # Change COMMENT_SYSTEM_ID to not wait for 5 seconds
+ with codecs.open(os.path.join(self.target_dir, 'conf.py'), "ab+", "utf8") as outf:
+ outf.write('\nCOMMENT_SYSTEM_ID = "nikolatest"\n')
+
+ with codecs.open(os.path.join(self.target_dir, 'posts', 'empty1.txt'), "wb+", "utf8") as outf:
+ outf.write(
+ ".. title: foo\n"
+ ".. slug: foo\n"
+ ".. date: %s\n" % (current_time() + datetime.timedelta(-1)).strftime('%Y-%m-%d %H:%M:%S')
)
+ with codecs.open(os.path.join(self.target_dir, 'posts', 'empty2.txt'), "wb+", "utf8") as outf:
+ outf.write(
+ ".. title: bar\n"
+ ".. slug: bar\n"
+ ".. date: %s\n" % (current_time() + datetime.timedelta(1)).strftime('%Y-%m-%d %H:%M:%S')
+ )
+
+ def test_future_post(self):
+ """ Ensure that the future post is not present in the index and sitemap."""
+ index_path = os.path.join(self.target_dir, "output", "index.html")
+ sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml")
+ foo_path = os.path.join(self.target_dir, "output", "posts", "foo.html")
+ bar_path = os.path.join(self.target_dir, "output", "posts", "bar.html")
+ self.assertTrue(os.path.isfile(index_path))
+ self.assertTrue(os.path.isfile(foo_path))
+ self.assertTrue(os.path.isfile(bar_path))
+ index_data = codecs.open(index_path, "r", "utf8").read()
+ sitemap_data = codecs.open(sitemap_path, "r", "utf8").read()
+ self.assertTrue('foo.html' in index_data)
+ self.assertFalse('bar.html' in index_data)
+ self.assertTrue('foo.html' in sitemap_data)
+ self.assertFalse('bar.html' in sitemap_data)
+
+ # Run deploy command to see if future post is deleted
+ with cd(self.target_dir):
+ main.main(["deploy"])
+
+ self.assertTrue(os.path.isfile(index_path))
+ self.assertTrue(os.path.isfile(foo_path))
+ self.assertFalse(os.path.isfile(bar_path))
+
class TranslatedBuildTest(EmptyBuildTest):
"""Test a site with translated content."""
@@ -132,8 +207,8 @@ class RelativeLinkTest(DemoBuildTest):
conf_path = os.path.join(self.target_dir, "conf.py")
with codecs.open(conf_path, "rb", "utf-8") as inf:
data = inf.read()
- data = data.replace('SITE_URL = "http://nikola.ralsina.com.ar"',
- 'SITE_URL = "http://nikola.ralsina.com.ar/foo/bar/"')
+ data = data.replace('SITE_URL = "http://getnikola.com/"',
+ 'SITE_URL = "http://getnikola.com/foo/bar/"')
with codecs.open(conf_path, "wb+", "utf8") as outf:
outf.write(data)
@@ -151,36 +226,51 @@ class RelativeLinkTest(DemoBuildTest):
# But I also need to be sure it is there!
self.assertTrue(flag)
+ def test_index_in_sitemap(self):
+ """Test that the correct path is in sitemap, and not the wrong one."""
+ sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml")
+ sitemap_data = codecs.open(sitemap_path, "r", "utf8").read()
+ self.assertFalse('<loc>http://getnikola.com/</loc>' in sitemap_data)
+ self.assertTrue('<loc>http://getnikola.com/foo/bar/index.html</loc>' in sitemap_data)
+
-#class TestCheck(DemoBuildTest):
- #"""The demo build should pass 'nikola check'"""
+if sys.version_info[0] == 2 and sys.version_info[1] < 7:
+ check_output = subprocess.check_call
+else:
+ check_output = subprocess.check_output
- #def test_check_links(self):
- #with cd(self.target_dir):
- #r = subprocess.call("nikola check -l", shell=True)
- #self.assertEqual(r, 0)
- #def test_check_files(self):
- #with cd(self.target_dir):
- #r = subprocess.call("nikola check -f", shell=True)
- #self.assertEqual(r, 0)
+class TestCheck(DemoBuildTest):
+ """The demo build should pass 'nikola check'"""
+ def test_check_links(self):
+ with cd(self.target_dir):
+ check_output("nikola check -l", shell=True, stderr=subprocess.STDOUT)
-#class TestCheckFailure(DemoBuildTest):
- #"""The demo build should pass 'nikola check'"""
+ def test_check_files(self):
+ with cd(self.target_dir):
+ check_output("nikola check -f", shell=True, stderr=subprocess.STDOUT)
- #def test_check_links_fail(self):
- #with cd(self.target_dir):
- #os.unlink(os.path.join("output", "archive.html"))
- #rv = subprocess.call("nikola check -l", shell=True)
- #self.assertEqual(rv, 1)
- #def test_check_files_fail(self):
- #with cd(self.target_dir):
- #with codecs.open(os.path.join("output", "foobar"), "wb+", "utf8") as outf:
- #outf.write("foo")
- #rv = subprocess.call("nikola check -f", shell=True)
- #self.assertEqual(rv, 1)
+class TestCheckFailure(DemoBuildTest):
+ """The demo build should pass 'nikola check'"""
+
+ def test_check_links_fail(self):
+ with cd(self.target_dir):
+ os.unlink(os.path.join("output", "archive.html"))
+ self.assertRaises(
+ subprocess.CalledProcessError,
+ check_output, "nikola check -f", shell=True
+ )
+
+ def test_check_files_fail(self):
+ with cd(self.target_dir):
+ with codecs.open(os.path.join("output", "foobar"), "wb+", "utf8") as outf:
+ outf.write("foo")
+ self.assertRaises(
+ subprocess.CalledProcessError,
+ check_output, "nikola check -f", shell=True
+ )
class RelativeLinkTest2(DemoBuildTest):
@@ -192,8 +282,10 @@ class RelativeLinkTest2(DemoBuildTest):
conf_path = os.path.join(self.target_dir, "conf.py")
with codecs.open(conf_path, "rb", "utf-8") as inf:
data = inf.read()
- data = data.replace('("stories/*.txt", "stories", "story.tmpl", False),',
- '("stories/*.txt", "", "story.tmpl", False),')
+ data = data.replace('("stories/*.txt", "stories", "story.tmpl"),',
+ '("stories/*.txt", "", "story.tmpl"),')
+ data = data.replace('("stories/*.rst", "stories", "story.tmpl"),',
+ '("stories/*.rst", "", "story.tmpl"),')
data = data.replace('# INDEX_PATH = ""',
'INDEX_PATH = "blog"')
with codecs.open(conf_path, "wb+", "utf8") as outf:
@@ -202,8 +294,6 @@ class RelativeLinkTest2(DemoBuildTest):
def test_relative_links(self):
"""Check that the links in a story are correct"""
- conf_path = os.path.join(self.target_dir, "conf.py")
- data = open(conf_path).read()
test_path = os.path.join(self.target_dir, "output", "about-nikola.html")
flag = False
with open(test_path, "rb") as inf:
@@ -215,3 +305,34 @@ class RelativeLinkTest2(DemoBuildTest):
flag = True
# But I also need to be sure it is there!
self.assertTrue(flag)
+
+ def test_index_in_sitemap(self):
+ """Test that the correct path is in sitemap, and not the wrong one."""
+ sitemap_path = os.path.join(self.target_dir, "output", "sitemap.xml")
+ sitemap_data = codecs.open(sitemap_path, "r", "utf8").read()
+ self.assertFalse('<loc>http://getnikola.com/</loc>' in sitemap_data)
+ self.assertTrue('<loc>http://getnikola.com/blog/index.html</loc>' in sitemap_data)
+
+
+class MonthlyArchiveTest(DemoBuildTest):
+ """Check that the monthly archives build and are correct."""
+
+ @classmethod
+ def patch_site(self):
+ """Set the SITE_URL to have a path"""
+ conf_path = os.path.join(self.target_dir, "conf.py")
+ with codecs.open(conf_path, "rb", "utf-8") as inf:
+ data = inf.read()
+ data = data.replace('# CREATE_MONTHLY_ARCHIVE = False',
+ 'CREATE_MONTHLY_ARCHIVE = True')
+ with codecs.open(conf_path, "wb+", "utf8") as outf:
+ outf.write(data)
+ outf.flush()
+
+ def test_monthly_archive(self):
+ """See that it builds"""
+ self.assertTrue(os.path.isfile(os.path.join(self.tmpdir, 'target', 'output', '2012', '03', 'index.html')))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_locale.py b/tests/test_locale.py
new file mode 100644
index 0000000..93df2ae
--- /dev/null
+++ b/tests/test_locale.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import sys
+
+# needed if @unittest.expectedFailure is used
+try:
+ import unittest2 as unittest
+except:
+ import unittest
+
+import nikola.nikola
+import nikola.utils
+from .base import LocaleSupportInTesting
+
+LocaleSupportInTesting.initialize_locales_for_testing('bilingual')
+lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
+lang_22, loc_22 = LocaleSupportInTesting.langlocales['other']
+
+
+# these are candidates to hardcoded locales, using str() for py2x setlocale
+loc_C = str('C')
+loc_Cutf8 = str('C.utf8')
+
+if sys.platform != 'win32':
+ nikola.nikola.workaround_empty_LC_ALL_posix()
+
+
+class TestHarcodedFallbacks(unittest.TestCase):
+ def test_hardcoded_fallbacks_work(self):
+ # keep in sync with nikola.valid_locale_fallback
+ if sys.platform == 'win32':
+ self.assertTrue(nikola.nikola.is_valid_locale(str('English')))
+ self.assertTrue(nikola.nikola.is_valid_locale(str('C')))
+ else:
+ # the 1st is desired in Travis, not a problem if fails in user host
+ self.assertTrue(nikola.nikola.is_valid_locale(str('en_US.utf8')))
+ # this is supposed to be always valid, and we need an universal
+ # fallback. Failure is not a problem in user host if he / she
+ # sets a valid (in his host) locale_fallback.
+ self.assertTrue(nikola.nikola.is_valid_locale(str('C')))
+
+
+class TestConfigLocale(unittest.TestCase):
+
+ def test_implicit_fallback(self):
+ locale_fallback = None
+ sanitized_fallback = nikola.nikola.valid_locale_fallback(
+ desired_locale=locale_fallback)
+ self.assertTrue(nikola.nikola.is_valid_locale(sanitized_fallback))
+
+ def test_explicit_good_fallback(self):
+ locale_fallback = loc_22
+ sanitized_fallback = nikola.nikola.valid_locale_fallback(
+ desired_locale=locale_fallback)
+ self.assertEquals(sanitized_fallback, locale_fallback)
+
+ def test_explicit_bad_fallback(self):
+ locale_fallback = str('xyz')
+ sanitized_fallback = nikola.nikola.valid_locale_fallback(
+ desired_locale=locale_fallback)
+ self.assertTrue(nikola.nikola.is_valid_locale(sanitized_fallback))
+
+ def test_explicit_good_default(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ loc_11,
+ {},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertEquals(fallback, locale_fallback)
+ self.assertEquals(default, locale_default)
+
+ def test_explicit_bad_default(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ str('xyz'),
+ {},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertEquals(fallback, locale_fallback)
+ self.assertEquals(default, fallback)
+
+ def test_extra_locales_deleted(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ None,
+ {'@z': loc_22},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertTrue('@z' not in locales)
+
+ def test_explicit_good_locale_retained(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ loc_22,
+ {lang_11: loc_11},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertEquals(locales[lang_11], str(LOCALES[lang_11]))
+
+ def test_explicit_bad_locale_replaced_with_fallback(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ loc_11,
+ {lang_11: str('xyz')},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertEquals(locales['en'], locale_fallback)
+
+ def test_impicit_locale_when_default_locale_defined(self):
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_11,
+ loc_22,
+ {},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ self.assertEquals(locales['en'], locale_default)
+
+ def test_impicit_locale_when_default_locale_is_not_defined(self):
+ # legacy mode, compat v6.0.4 : guess locale from lang
+ locale_fallback, locale_default, LOCALES, translations = (
+ loc_22,
+ None,
+ {},
+ {lang_11: ''},
+ )
+ fallback, default, locales = nikola.nikola.sanitized_locales(
+ locale_fallback,
+ locale_default,
+ LOCALES,
+ translations)
+ if sys.platform == 'win32':
+ guess_locale_for_lang = nikola.nikola.guess_locale_from_lang_windows
+ else:
+ guess_locale_for_lang = nikola.nikola.guess_locale_from_lang_posix
+
+ self.assertEquals(locales[lang_11], guess_locale_for_lang(lang_11))
+
+
+class TestCalendarRelated(unittest.TestCase):
+ def test_type_of_month_name(self):
+ """validate assumption calendar month name is of type str
+
+ Yes, both in windows and linuxTravis, py 26, 27, 33
+ """
+ import calendar
+ if sys.version_info[0] == 3: # Python 3
+ with calendar.different_locale(loc_11):
+ s = calendar.month_name[1]
+ else: # Python 2
+ with calendar.TimeEncoding(loc_11):
+ s = calendar.month_name[1]
+ self.assertTrue(type(s) == str)
+
+
+class TestLocaleBorg(unittest.TestCase):
+ def test_initial_lang(self):
+ lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
+ lang_22, loc_22 = LocaleSupportInTesting.langlocales['other']
+
+ locales = {lang_11: loc_11, lang_22: loc_22}
+ initial_lang = lang_22
+ nikola.utils.LocaleBorg.initialize(locales, initial_lang)
+ self.assertEquals(initial_lang, nikola.utils.LocaleBorg().current_lang)
+
+ def test_remembers_last_lang(self):
+ lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
+ lang_22, loc_22 = LocaleSupportInTesting.langlocales['other']
+
+ locales = {lang_11: loc_11, lang_22: loc_22}
+ initial_lang = lang_22
+ nikola.utils.LocaleBorg.initialize(locales, initial_lang)
+
+ nikola.utils.LocaleBorg().set_locale(lang_11)
+ self.assertTrue(nikola.utils.LocaleBorg().current_lang, lang_11)
+
+ def test_services_ensure_initialization(self):
+ nikola.utils.LocaleBorg.reset()
+ self.assertRaises(Exception, nikola.utils.LocaleBorg)
+
+ def test_services_reject_dumb_wrong_call(self):
+ lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
+ nikola.utils.LocaleBorg.reset()
+ self.assertRaises(Exception, nikola.utils.LocaleBorg.set_locale, lang_11)
+ self.assertRaises(Exception, getattr, nikola.utils.LocaleBorg, 'current_lang')
+
+ def test_set_locale_raises_on_invalid_lang(self):
+ lang_11, loc_11 = LocaleSupportInTesting.langlocales['default']
+ lang_22, loc_22 = LocaleSupportInTesting.langlocales['other']
+
+ locales = {lang_11: loc_11, lang_22: loc_22}
+ initial_lang = lang_22
+ nikola.utils.LocaleBorg.initialize(locales, initial_lang)
+ self.assertRaises(KeyError, nikola.utils.LocaleBorg().set_locale, '@z')
+
+
+class TestTestPreconditions(unittest.TestCase):
+ """If this fails the other test in this module are mostly nonsense, and
+ probably same for tests of multilingual features.
+
+ Failure probably means the OS support for the failing locale is not
+ instaled or environmet variables NIKOLA_LOCALE_DEFAULT or
+ NIKOLA_LOCALE_OTHER with bad values.
+ """
+ def test_langlocale_default_availability(self):
+ msg = "META ERROR: The pair lang, locale : {0} {1} is invalid"
+ self.assertTrue(nikola.nikola.is_valid_locale(loc_11), msg.format(lang_11, loc_11))
+
+ def test_langlocale_other_availability(self):
+ msg = "META ERROR: The pair lang, locale : {0} {1} is invalid"
+ self.assertTrue(nikola.nikola.is_valid_locale(loc_22), msg.format(lang_22, loc_22))
diff --git a/tests/test_plugin_importing.py b/tests/test_plugin_importing.py
index 677dde8..5009f88 100644
--- a/tests/test_plugin_importing.py
+++ b/tests/test_plugin_importing.py
@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import
-from context import nikola # NOQA
+from .context import nikola # NOQA
import unittest
class ImportPluginsTest(unittest.TestCase):
def test_importing_command_import_wordpress(self):
- import nikola.plugins.command_import_wordpress # NOQA
+ import nikola.plugins.command.import_wordpress # NOQA
def test_importing_compile_rest(self):
- import nikola.plugins.compile_rest # NOQA
+ import nikola.plugins.compile.rest # NOQA
def test_importing_plugin_compile_markdown(self):
- import nikola.plugins.compile_markdown # NOQA
+ import nikola.plugins.compile.markdown # NOQA
diff --git a/tests/test_rss_feeds.py b/tests/test_rss_feeds.py
index 5b9b981..169e1e7 100644
--- a/tests/test_rss_feeds.py
+++ b/tests/test_rss_feeds.py
@@ -1,25 +1,37 @@
# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-import unittest
+from __future__ import unicode_literals, absolute_import
+from collections import defaultdict
+from io import StringIO
import os
import re
-from io import StringIO
+import unittest
import mock
-from context import nikola
+from .context import nikola # NOQA
from lxml import etree
+from .base import LocaleSupportInTesting
+
+
+fake_conf = defaultdict(str)
+fake_conf['TIMEZONE'] = None
+fake_conf['DEFAULT_LANG'] = 'en'
+fake_conf['TRANSLATIONS'] = {'en': ''}
+fake_conf['BASE_URL'] = 'http://some.blog/'
class RSSFeedTest(unittest.TestCase):
def setUp(self):
+ LocaleSupportInTesting.initialize_locales_for_testing('unilingual')
+
self.blog_url = "http://some.blog"
with mock.patch('nikola.post.get_meta',
mock.Mock(return_value=({'title': 'post title',
'slug': 'awesome_article',
'date': '2012-10-01 22:41',
+ 'author': None,
'tags': 'tags', 'link':
'link', 'description':
'description'}))):
@@ -29,14 +41,12 @@ class RSSFeedTest(unittest.TestCase):
mock.Mock(return_value='some long text')):
example_post = nikola.nikola.Post('source.file',
- 'cache',
+ fake_conf,
'blog_folder',
True,
{'en': ''},
- 'en',
- self.blog_url,
- 'unused message.',
- 'post.tmpl')
+ 'post.tmpl',
+ lambda *a: None)
opener_mock = mock.mock_open()
diff --git a/tests/test_rst_extensions.py b/tests/test_rst_compiler.py
index 845d6a7..c5db8dd 100644
--- a/tests/test_rst_extensions.py
+++ b/tests/test_rst_compiler.py
@@ -25,36 +25,134 @@ always unquoted.
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
+import codecs
try:
from io import StringIO
except ImportError:
from StringIO import StringIO # NOQA
+import os
+import sys
+import tempfile
-from docutils.core import publish_parts
+import docutils
from lxml import html
-import mock
-
+from nose.plugins.skip import SkipTest
import unittest
-import nikola.plugins.compile_rest
-from nikola.utils import _reload
-from base import BaseTestCase
+from yapsy.PluginManager import PluginManager
+
+from nikola import utils
+import nikola.plugins.compile.rest
+from nikola.plugins.compile.rest import gist
+from nikola.plugins.compile.rest import vimeo
+import nikola.plugins.compile.rest.listing
+from nikola.plugins.compile.rest.doc import Plugin as DocPlugin
+from nikola.utils import _reload, STDERR_HANDLER
+from nikola.plugin_categories import (
+ Command,
+ Task,
+ LateTask,
+ TemplateSystem,
+ PageCompiler,
+ TaskMultiplier,
+ RestExtension,
+)
+from .base import BaseTestCase
+
+
+class FakePost(object):
+
+ def __init__(self, title, slug):
+ self._title = title
+ self._slug = slug
+ self._meta = {'slug': slug}
+
+ def title(self):
+ return self._title
+
+ def meta(self, key):
+ return self._meta[key]
+
+ def permalink(self):
+ return '/posts/' + self._slug
+
+
+class FakeSite(object):
+ def __init__(self):
+ self.template_system = self
+ self.config = {
+ 'DISABLED_PLUGINS': [],
+ 'EXTRA_PLUGINS': [],
+ 'DEFAULT_LANG': 'en',
+ }
+ self.EXTRA_PLUGINS = self.config['EXTRA_PLUGINS']
+ self.plugin_manager = PluginManager(categories_filter={
+ "Command": Command,
+ "Task": Task,
+ "LateTask": LateTask,
+ "TemplateSystem": TemplateSystem,
+ "PageCompiler": PageCompiler,
+ "TaskMultiplier": TaskMultiplier,
+ "RestExtension": RestExtension,
+ })
+ self.loghandlers = [STDERR_HANDLER]
+ self.plugin_manager.setPluginInfoExtension('plugin')
+ if sys.version_info[0] == 3:
+ places = [
+ os.path.join(os.path.dirname(utils.__file__), 'plugins'),
+ ]
+ else:
+ places = [
+ os.path.join(os.path.dirname(utils.__file__), utils.sys_encode('plugins')),
+ ]
+ self.plugin_manager.setPluginPlaces(places)
+ self.plugin_manager.collectPlugins()
+
+ self.timeline = [
+ FakePost(title='Fake post',
+ slug='fake-post')
+ ]
+
+ def render_template(self, name, _, context):
+ return('<img src="IMG.jpg">')
class ReSTExtensionTestCase(BaseTestCase):
""" Base class for testing ReST extensions """
- sample = None
+ sample = 'foo'
+ deps = None
def setUp(self):
+ self.compiler = nikola.plugins.compile.rest.CompileRest()
+ self.compiler.set_site(FakeSite())
+ return super(ReSTExtensionTestCase, self).setUp()
+
+ def basic_test(self):
""" Parse cls.sample into a HTML document tree """
- super(ReSTExtensionTestCase, self).setUp()
self.setHtmlFromRst(self.sample)
def setHtmlFromRst(self, rst):
""" Create html output from rst string """
- self.html = publish_parts(rst, writer_name="html")["body"]
+ tmpdir = tempfile.mkdtemp()
+ inf = os.path.join(tmpdir, 'inf')
+ outf = os.path.join(tmpdir, 'outf')
+ depf = os.path.join(tmpdir, 'outf.dep')
+ with codecs.open(inf, 'wb+', 'utf8') as f:
+ f.write(rst)
+ self.html = self.compiler.compile_html(inf, outf)
+ with codecs.open(outf, 'r', 'utf8') as f:
+ self.html = f.read()
+ os.unlink(inf)
+ os.unlink(outf)
+ if os.path.isfile(depf):
+ with codecs.open(depf, 'r', 'utf8') as f:
+ self.assertEqual(self.deps, f.read())
+ os.unlink(depf)
+ else:
+ self.assertEqual(self.deps, None)
+ os.rmdir(tmpdir)
self.html_doc = html.parse(StringIO(self.html))
def assertHTMLContains(self, element, attributes=None, text=None):
@@ -65,7 +163,7 @@ class ReSTExtensionTestCase(BaseTestCase):
try:
tag = next(self.html_doc.iter(element))
except StopIteration:
- raise Exception("<{}> not in {}".format(element, self.html))
+ raise Exception("<{0}> not in {1}".format(element, self.html))
else:
if attributes:
arg_attrs = set(attributes.items())
@@ -81,29 +179,42 @@ class ReSTExtensionTestCaseTestCase(ReSTExtensionTestCase):
sample = '.. raw:: html\n\n <iframe src="foo" height="bar">spam</iframe>'
def test_test(self):
+ self.basic_test()
self.assertHTMLContains("iframe", attributes={"src": "foo"},
text="spam")
self.assertRaises(Exception, self.assertHTMLContains, "eggs", {})
+class MathTestCase(ReSTExtensionTestCase):
+ sample = ':math:`e^{ix} = \cos x + i\sin x`'
+
+ def test_mathjax(self):
+ """ Test that math is outputting MathJax."""
+ self.basic_test()
+ self.assertHTMLContains("span", attributes={"class": "math"},
+ text="\(e^{ix} = \cos x + i\sin x\)")
+
+
class GistTestCase(ReSTExtensionTestCase):
""" Test GitHubGist.
We will replace get_raw_gist() and get_raw_gist_with_filename()
monkeypatching the GitHubGist class for avoiding network dependency
"""
- gist_type = nikola.plugins.compile_rest.GitHubGist
+ gist_type = gist.GitHubGist
sample = '.. gist:: fake_id\n :file: spam.py'
sample_without_filename = '.. gist:: fake_id2'
def setUp(self):
""" Patch GitHubGist for avoiding network dependency """
+ super(GistTestCase, self).setUp()
self.gist_type.get_raw_gist_with_filename = lambda *_: 'raw_gist_file'
self.gist_type.get_raw_gist = lambda *_: "raw_gist"
- _reload(nikola.plugins.compile_rest)
+ _reload(nikola.plugins.compile.rest)
def test_gist(self):
""" Test the gist directive with filename """
+ raise SkipTest
self.setHtmlFromRst(self.sample)
output = 'https://gist.github.com/fake_id.js?file=spam.py'
self.assertHTMLContains("script", attributes={"src": output})
@@ -111,6 +222,7 @@ class GistTestCase(ReSTExtensionTestCase):
def test_gist_without_filename(self):
""" Test the gist directive without filename """
+ raise SkipTest
self.setHtmlFromRst(self.sample_without_filename)
output = 'https://gist.github.com/fake_id2.js'
self.assertHTMLContains("script", attributes={"src": output})
@@ -126,6 +238,7 @@ class GistIntegrationTestCase(ReSTExtensionTestCase):
def test_gist_integration(self):
""" Fetch contents of the gist from GH and render in a noscript tag """
+ self.basic_test()
text = ('Be alone, that is the secret of invention: be alone, that is'
' when ideas are born. -- Nikola Tesla')
self.assertHTMLContains('pre', text=text)
@@ -138,6 +251,7 @@ class SlidesTestCase(ReSTExtensionTestCase):
def test_slides(self):
""" Test the slides js generation and img tag creation """
+ self.basic_test()
self.assertHTMLContains("img", attributes={"src": "IMG.jpg"})
@@ -148,6 +262,7 @@ class SoundCloudTestCase(ReSTExtensionTestCase):
def test_soundcloud(self):
""" Test SoundCloud iframe tag generation """
+ self.basic_test()
self.assertHTMLContains("iframe",
attributes={"src": ("https://w.soundcloud.com"
"/player/?url=http://"
@@ -166,12 +281,13 @@ class VimeoTestCase(ReSTExtensionTestCase):
def setUp(self):
""" Disable query of the vimeo api over the wire """
- nikola.plugins.compile_rest.Vimeo.request_size = False
+ vimeo.Vimeo.request_size = False
super(VimeoTestCase, self).setUp()
- _reload(nikola.plugins.compile_rest)
+ _reload(nikola.plugins.compile.rest)
def test_vimeo(self):
""" Test Vimeo iframe tag generation """
+ self.basic_test()
self.assertHTMLContains("iframe",
attributes={"src": ("http://player.vimeo.com/"
"video/VID"),
@@ -185,6 +301,7 @@ class YoutubeTestCase(ReSTExtensionTestCase):
def test_youtube(self):
""" Test Youtube iframe tag generation """
+ self.basic_test()
self.assertHTMLContains("iframe",
attributes={"src": ("http://www.youtube.com/"
"embed/YID?rel=0&hd=1&"
@@ -195,28 +312,57 @@ class YoutubeTestCase(ReSTExtensionTestCase):
class ListingTestCase(ReSTExtensionTestCase):
""" Listing test case and CodeBlock alias tests """
- sample = '.. listing:: nikola.py python'
+ deps = None
+ sample1 = '.. listing:: nikola.py python\n\n'
sample2 = '.. code-block:: python\n\n import antigravity'
sample3 = '.. sourcecode:: python\n\n import antigravity'
- opener_mock = mock.mock_open(read_data="import antigravity\n")
- opener_mock.return_value.readlines.return_value = "import antigravity\n"
-
- def setUp(self):
- """ Inject a mock open function for not generating a test site """
- self.f = StringIO("import antigravity\n")
- #_reload(nikola.plugins.compile_rest)
-
- def test_listing(self):
- """ Test that we can render a file object contents without errors """
- with mock.patch("nikola.plugins.compile_rest.listing.codecs_open", self.opener_mock, create=True):
- self.setHtmlFromRst(self.sample)
+ #def test_listing(self):
+ ##""" Test that we can render a file object contents without errors """
+ ##with cd(os.path.dirname(__file__)):
+ #self.deps = 'listings/nikola.py'
+ #self.setHtmlFromRst(self.sample1)
def test_codeblock_alias(self):
""" Test CodeBlock aliases """
- with mock.patch("nikola.plugins.compile_rest.listing.codecs_open", self.opener_mock, create=True):
- self.setHtmlFromRst(self.sample2)
- self.setHtmlFromRst(self.sample3)
+ self.deps = None
+ self.setHtmlFromRst(self.sample2)
+ self.setHtmlFromRst(self.sample3)
+
+
+class DocTestCase(ReSTExtensionTestCase):
+ """ Ref role test case """
+
+ sample = 'Sample for testing my :doc:`doesnt-exist-post`'
+ sample1 = 'Sample for testing my :doc:`fake-post`'
+ sample2 = 'Sample for testing my :doc:`titled post <fake-post>`'
+
+ def setUp(self):
+ # Initialize plugin, register role
+ self.plugin = DocPlugin()
+ self.plugin.set_site(FakeSite())
+ # Hack to fix leaked state from integration tests
+ try:
+ f = docutils.parsers.rst.roles.role('doc', None, None, None)[0]
+ f.site = FakeSite()
+ except AttributeError:
+ pass
+ return super(DocTestCase, self).setUp()
+
+ def test_doc_doesnt_exist(self):
+ self.assertRaises(Exception, self.assertHTMLContains, 'anything', {})
+
+ def test_doc(self):
+ self.setHtmlFromRst(self.sample1)
+ self.assertHTMLContains('a',
+ text='Fake post',
+ attributes={'href': '/posts/fake-post'})
+
+ def test_doc_titled(self):
+ self.setHtmlFromRst(self.sample2)
+ self.assertHTMLContains('a',
+ text='titled post',
+ attributes={'href': '/posts/fake-post'})
if __name__ == "__main__":
diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py
new file mode 100644
index 0000000..264d1c5
--- /dev/null
+++ b/tests/test_scheduling.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals, absolute_import
+from .base import BaseTestCase
+import datetime
+from nose.plugins.skip import SkipTest
+try:
+ from freezegun import freeze_time
+ _freeze_time = True
+except ImportError:
+ _freeze_time = False
+ freeze_time = lambda x: lambda y: y
+
+FMT = '%Y/%m/%d %H:%M:%S'
+NOW = '2013/08/22 10:00:00' # Thursday
+TODAY = datetime.datetime.strptime(NOW, FMT)
+RULE_TH = 'RRULE:FREQ=WEEKLY;BYDAY=TH'
+RULE_FR = 'RRULE:FREQ=WEEKLY;BYDAY=FR'
+
+
+class TestScheduling(BaseTestCase):
+
+ @classmethod
+ def setUp(self):
+ if not _freeze_time:
+ raise SkipTest('freezegun not installed')
+
+ @freeze_time(NOW)
+ def test_get_date(self):
+ from nikola.plugins.command.new_post import get_date
+
+ #### NOW does not match rule #########################################
+ ## No last date
+ expected = TODAY.replace(day=23).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_FR))
+ self.assertEqual(expected, get_date(True, RULE_FR, force_today=True))
+
+ ## Last date in the past; doesn't match rule
+ date = TODAY.replace(hour=7)
+ expected = TODAY.replace(day=23, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_FR, date))
+ self.assertEqual(expected, get_date(True, RULE_FR, date, True))
+
+ ## Last date in the future; doesn't match rule
+ date = TODAY.replace(day=24, hour=7)
+ expected = TODAY.replace(day=30, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_FR, date))
+ self.assertEqual(expected, get_date(True, RULE_FR, date, True))
+
+ ## Last date in the past; matches rule
+ date = TODAY.replace(day=16, hour=8)
+ expected = TODAY.replace(day=23, hour=8).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_FR, date))
+ self.assertEqual(expected, get_date(True, RULE_FR, date, True))
+
+ ## Last date in the future; matches rule
+ date = TODAY.replace(day=23, hour=18)
+ expected = TODAY.replace(day=30, hour=18).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_FR, date))
+ self.assertEqual(expected, get_date(True, RULE_FR, date, True))
+
+ #### NOW matches rule ################################################
+ ## Not scheduling should return NOW
+ self.assertEqual(NOW, get_date(False, RULE_TH))
+ ## No last date
+ self.assertEqual(NOW, get_date(True, RULE_TH))
+ self.assertEqual(NOW, get_date(True, RULE_TH, force_today=True))
+
+ ## Last date in the past; doesn't match rule
+ ### Corresponding time has already passed, today
+ date = TODAY.replace(day=21, hour=7)
+ expected = TODAY.replace(day=29, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ expected = TODAY.replace(day=22, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+ ### Corresponding time has not passed today
+ date = TODAY.replace(day=21, hour=18)
+ expected = TODAY.replace(day=22, hour=18).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+
+ ## Last date in the future; doesn't match rule
+ ### Corresponding time has already passed, today
+ date = TODAY.replace(day=24, hour=7)
+ expected = TODAY.replace(day=29, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+ ### Corresponding time has not passed today
+ date = TODAY.replace(day=24, hour=18)
+ expected = TODAY.replace(day=29, hour=18).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+
+ ## Last date in the past; matches rule
+ ### Corresponding time has already passed, today
+ date = TODAY.replace(day=15, hour=7)
+ expected = TODAY.replace(day=29, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ expected = TODAY.replace(day=22, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+ ### Corresponding time has already passed, today; rule specifies HOUR
+ date = TODAY.replace(day=15, hour=7)
+ expected = TODAY.replace(day=29, hour=9).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH + ';BYHOUR=9', date))
+ expected = TODAY.replace(day=22, hour=9).strftime(FMT)
+ self.assertEqual(expected,
+ get_date(True, RULE_TH + ';BYHOUR=9', date, True))
+ ### Corresponding time has not passed today
+ date = TODAY.replace(day=15, hour=18)
+ expected = TODAY.replace(day=22, hour=18).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+
+ ## Last date in the future; matches rule
+ ### Corresponding time has already passed, today
+ date = TODAY.replace(day=29, hour=7)
+ expected = TODAY.replace(day=5, month=9, hour=7).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ ### Corresponding time has not passed today
+ date = TODAY.replace(day=22, hour=18)
+ expected = TODAY.replace(day=29, hour=18).strftime(FMT)
+ self.assertEqual(expected, get_date(True, RULE_TH, date))
+ self.assertEqual(expected, get_date(True, RULE_TH, date, True))
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
diff --git a/tests/wordpress_export_example.xml b/tests/wordpress_export_example.xml
index e697a5b..5fd0a90 100644
--- a/tests/wordpress_export_example.xml
+++ b/tests/wordpress_export_example.xml
@@ -230,5 +230,62 @@ A listing with another listing inside.
</wp:postmeta>
</item>
-</channel>
+ <item>
+ <title>Screenshot - 2012-12-19</title>
+ <link>http://some.blog/2012/12/wintermodus/2012-12-19-1355925145_1024x600_scrot/</link>
+ <pubDate>Wed, 19 Dec 2012 13:53:19 +0000</pubDate>
+ <dc:creator>Niko</dc:creator>
+ <guid isPermaLink="false">http://some.blog/wp-content/uploads/2012/12/2012-12-19-355925145_1024x600_scrot.png</guid>
+ <description></description>
+ <content:encoded><![CDATA[]]></content:encoded>
+ <excerpt:encoded><![CDATA[]]></excerpt:encoded>
+ <wp:post_id>2271</wp:post_id>
+ <wp:post_date>2012-12-19 14:53:19</wp:post_date>
+ <wp:post_date_gmt>2012-12-19 13:53:19</wp:post_date_gmt>
+ <wp:comment_status>open</wp:comment_status>
+ <wp:ping_status>open</wp:ping_status>
+ <wp:post_name>2012-12-19-1355925145_1024x600_scrot</wp:post_name>
+ <wp:status>inherit</wp:status>
+ <wp:post_parent>2270</wp:post_parent>
+ <wp:menu_order>0</wp:menu_order>
+ <wp:post_type>attachment</wp:post_type>
+ <wp:post_password></wp:post_password>
+ <wp:is_sticky>0</wp:is_sticky>
+ <wp:attachment_url>http://some.blog/wp-content/uploads/2012/12/2012-12-19-355925145_1024x600_scrot.png</wp:attachment_url>
+ <wp:postmeta>
+ <wp:meta_key>_wp_attached_file</wp:meta_key>
+ <wp:meta_value><![CDATA[2012/12/2012-12-19-1355925145_1024x600_scrot.png]]></wp:meta_value>
+ </wp:postmeta>
+ <wp:postmeta>
+ <wp:meta_key>_wp_attachment_metadata</wp:meta_key>
+ <wp:meta_value><![CDATA[a:5:{s:5:"width";i:1024;s:6:"height";i:600;s:4:"file";s:48:"2012/12/2012-12-19-1355925145_1024x600_scrot.png";s:5:"sizes";a:9:{s:9:"thumbnail";a:4:{s:4:"file";s:48:"2012-12-19-1355925145_1024x600_scrot-150x150.png";s:5:"width";i:150;s:6:"height";i:150;s:9:"mime-type";s:9:"image/png";}s:6:"medium";a:4:{s:4:"file";s:48:"2012-12-19-1355925145_1024x600_scrot-300x175.png";s:5:"width";i:300;s:6:"height";i:175;s:9:"mime-type";s:9:"image/png";}s:12:"mosaic-thumb";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-96x96.png";s:5:"width";i:96;s:6:"height";i:96;s:9:"mime-type";s:9:"image/png";}s:13:"gallery-thumb";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-96x96.png";s:5:"width";i:96;s:6:"height";i:96;s:9:"mime-type";s:9:"image/png";}s:9:"widget-24";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-24x24.png";s:5:"width";i:24;s:6:"height";i:24;s:9:"mime-type";s:9:"image/png";}s:9:"widget-32";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-36x36.png";s:5:"width";i:36;s:6:"height";i:36;s:9:"mime-type";s:9:"image/png";}s:9:"widget-48";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-48x48.png";s:5:"width";i:48;s:6:"height";i:48;s:9:"mime-type";s:9:"image/png";}s:9:"widget-64";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-64x64.png";s:5:"width";i:64;s:6:"height";i:64;s:9:"mime-type";s:9:"image/png";}s:9:"widget-96";a:4:{s:4:"file";s:46:"2012-12-19-1355925145_1024x600_scrot-96x96.png";s:5:"width";i:96;s:6:"height";i:96;s:9:"mime-type";s:9:"image/png";}}s:10:"image_meta";a:10:{s:8:"aperture";i:0;s:6:"credit";s:0:"";s:6:"camera";s:0:"";s:7:"caption";s:0:"";s:17:"created_timestamp";i:0;s:9:"copyright";s:0:"";s:12:"focal_length";i:0;s:3:"iso";i:0;s:13:"shutter_speed";i:0;s:5:"title";s:0:"";}}]]></wp:meta_value>
+ </wp:postmeta>
+ </item>
+
+ <item>
+ <title>Image Link Rewriting</title>
+ <link>http://some.blog/2012/12/wintermodus/</link>
+ <pubDate>Wed, 19 Dec 2012 13:55:10 +0000</pubDate>
+ <dc:creator>Niko</dc:creator>
+ <guid isPermaLink="false">http://some.blog/?p=2270</guid>
+ <description></description>
+ <content:encoded><![CDATA[Some image upload. The links to this and the src of the img-tag should be rewritten correctly.
+
+ <a href="http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot.png"><img class="aligncenter size-medium wp-image-2271" alt="Netbook Screenshot - 2012-12-19" src="http://some.blog/wp-content/uploads/2012/12/2012-12-19-1355925145_1024x600_scrot-300x175.ng" width="300" height="175" /></a>]]></content:encoded>
+ <excerpt:encoded><![CDATA[]]></excerpt:encoded>
+ <wp:post_id>2270</wp:post_id>
+ <wp:post_date>2012-12-19 14:55:10</wp:post_date>
+ <wp:post_date_gmt>2012-12-19 13:55:10</wp:post_date_gmt>
+ <wp:comment_status>open</wp:comment_status>
+ <wp:ping_status>open</wp:ping_status>
+ <wp:post_name>image-link-rewriting</wp:post_name>
+ <wp:status>publish</wp:status>
+ <wp:post_parent>0</wp:post_parent>
+ <wp:menu_order>0</wp:menu_order>
+ <wp:post_type>post</wp:post_type>
+ <wp:post_password></wp:post_password>
+ <wp:is_sticky>0</wp:is_sticky>
+ <category domain="category" nicename="linux"><![CDATA[Linux]]></category>
+ </item>
+ </channel>
</rss>
diff --git a/translations/nikola.messages/bg.po b/translations/nikola.messages/bg.po
new file mode 100644
index 0000000..25f02cd
--- /dev/null
+++ b/translations/nikola.messages/bg.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# ivoarch <ivkuzev@gmail.com>, 2013
+# ivoarch <ivkuzev@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Bulgarian (http://www.transifex.com/projects/p/nikola/language/bg/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "More posts about"
+msgstr "Още публикации относно "
+
+msgid "LANGUAGE"
+msgstr "Български"
+
+msgid "Tags"
+msgstr "Тагове"
+
+msgid "Categories"
+msgstr "Категории"
+
+msgid "Tags and Categories"
+msgstr "Тагове и Категории"
+
+msgid "Read more"
+msgstr "Прочети още"
+
+msgid "Posts about %s"
+msgstr "Публикации относно %s"
+
+msgid "Next post"
+msgstr "Следваща публикация"
+
+msgid "old posts page %d"
+msgstr "стари публикации страница %d"
+
+msgid "Source"
+msgstr "Source"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Прочетете на български"
+
+msgid "Posts for year %s"
+msgstr "Публикации за %s година"
+
+msgid "Newer posts"
+msgstr "Нови публикации"
+
+msgid "Previous post"
+msgstr "Предишна публикация"
+
+msgid "Also available in"
+msgstr "Също достъпно в"
+
+msgid "Original site"
+msgstr "Оригиналния сайт"
+
+msgid "Older posts"
+msgstr "Стари публикации"
+
+msgid "Archive"
+msgstr "Архив"
+
+msgid "Posted"
+msgstr "Публиковано"
+
+msgid "Posts for {month} {year}"
+msgstr "Публикации за {month} {year}"
diff --git a/translations/nikola.messages/ca.po b/translations/nikola.messages/ca.po
index f983fda..de98c52 100644
--- a/translations/nikola.messages/ca.po
+++ b/translations/nikola.messages/ca.po
@@ -4,14 +4,15 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 18:50+0000\n"
-"Last-Translator: ralsina <ralsina@netmanagers.com.ar>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Catalan (http://www.transifex.com/projects/p/nikola/language/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -28,6 +29,12 @@ msgstr "Català"
msgid "Tags"
msgstr "Etiquetes"
+msgid "Categories"
+msgstr ""
+
+msgid "Tags and Categories"
+msgstr ""
+
msgid "Read more"
msgstr "Llegeix-ne més"
@@ -43,6 +50,8 @@ msgstr "entrades antigues pàgina %d"
msgid "Source"
msgstr "Codi"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Llegeix-ho en català"
diff --git a/translations/nikola.messages/de.po b/translations/nikola.messages/de.po
index 97712de..d10b470 100644
--- a/translations/nikola.messages/de.po
+++ b/translations/nikola.messages/de.po
@@ -4,15 +4,22 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
-# <wenso@gmx.de>, 2013.
+# Kwpolska <kwpolska@gmail.com>, 2013
+# Kwpolska <kwpolska@gmail.com>, 2013
+# Kwpolska <kwpolska@gmail.com>, 2013
+# Niko <wenso@gmx.de>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# udono <udono@gmx.net>, 2013
+# udono <udono@gmx.net>, 2013
+# Niko <wenso@gmx.de>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-10 06:15+0000\n"
-"Last-Translator: Niko <wenso@gmx.de>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: German (http://www.transifex.com/projects/p/nikola/language/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +36,12 @@ msgstr "Deutsch"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Kategorien"
+
+msgid "Tags and Categories"
+msgstr "Tags und Kategorien"
+
msgid "Read more"
msgstr "Weiterlesen"
@@ -44,6 +57,8 @@ msgstr "Vorherige Einträge %d"
msgid "Source"
msgstr "Source"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Auf Deutsch lesen"
@@ -72,4 +87,4 @@ msgid "Posted"
msgstr "Veröffentlicht"
msgid "Posts for {month} {year}"
-msgstr "Einträge für {month} {year}"
+msgstr "Einträge aus {month} {year}"
diff --git a/translations/nikola.messages/el.po b/translations/nikola.messages/el.po
index a4077b9..04547e1 100644
--- a/translations/nikola.messages/el.po
+++ b/translations/nikola.messages/el.po
@@ -4,15 +4,17 @@
#
# Translators:
# Translators:
-# Panagiotis Mavrogiorgos <otinanai90@gmail.com>, 2013.
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# pmav99 <otinanai90@gmail.com>, 2013
+# pmav99 <otinanai90@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 18:50+0000\n"
-"Last-Translator: ralsina <ralsina@netmanagers.com.ar>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Greek (http://www.transifex.com/projects/p/nikola/language/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +31,12 @@ msgstr "Ελληνικά"
msgid "Tags"
msgstr "Ετικέτες"
+msgid "Categories"
+msgstr "Κατηγορίες"
+
+msgid "Tags and Categories"
+msgstr ""
+
msgid "Read more"
msgstr "Διαβάστε περισσότερα"
@@ -44,6 +52,8 @@ msgstr "σελίδα παλαιότερων αναρτήσεων %d"
msgid "Source"
msgstr "Πηγαίος κώδικας"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Διαβάστε στα Ελληνικά"
@@ -72,4 +82,4 @@ msgid "Posted"
msgstr "Αναρτήθηκε"
msgid "Posts for {month} {year}"
-msgstr ""
+msgstr "Αναρτήσεις για τον {month} του {year}"
diff --git a/translations/nikola.messages/en.po b/translations/nikola.messages/en.po
index 4fa7200..3a03df3 100644
--- a/translations/nikola.messages/en.po
+++ b/translations/nikola.messages/en.po
@@ -26,6 +26,12 @@ msgstr "English"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Categories"
+
+msgid "Tags and Categories"
+msgstr "Tags and Categories"
+
msgid "Read more"
msgstr "Read more"
@@ -41,6 +47,7 @@ msgstr "old posts page %d"
msgid "Source"
msgstr "Source"
+#. Here your translation should not say English but the name of your language instead
msgid "Read in English"
msgstr "Read in English"
@@ -70,3 +77,9 @@ msgstr "Posted"
msgid "Posts for {month} {year}"
msgstr "Posts for {month} {year}"
+
+msgid "No title"
+msgstr ""
+
+msgid "No content"
+msgstr ""
diff --git a/translations/nikola.messages/eo.po b/translations/nikola.messages/eo.po
new file mode 100644
index 0000000..d21b63b
--- /dev/null
+++ b/translations/nikola.messages/eo.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# David Paleino <d.paleino@gmail.com>, 2013
+# David Paleino <d.paleino@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Esperanto (http://www.transifex.com/projects/p/nikola/language/eo/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: eo\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "More posts about"
+msgstr "Pli artikoloj pri..."
+
+msgid "LANGUAGE"
+msgstr "Anglalingve"
+
+msgid "Tags"
+msgstr "Etikedoj"
+
+msgid "Categories"
+msgstr "Kategorioj"
+
+msgid "Tags and Categories"
+msgstr "Etikedoj kaj Kategorioj"
+
+msgid "Read more"
+msgstr "Legu plu"
+
+msgid "Posts about %s"
+msgstr "Artikoloj pri %s"
+
+msgid "Next post"
+msgstr "Venonta artikolo"
+
+msgid "old posts page %d"
+msgstr "paĝo de malnovaj artikoloj %d"
+
+msgid "Source"
+msgstr "Fonto"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Legu ĝin en Esperanto"
+
+msgid "Posts for year %s"
+msgstr "Artikoloj de la jaro %s"
+
+msgid "Newer posts"
+msgstr "Pli novaj artikoloj"
+
+msgid "Previous post"
+msgstr "Antaŭa artikolo"
+
+msgid "Also available in"
+msgstr "Ankaŭ disponebla en"
+
+msgid "Original site"
+msgstr "Originala interretejo"
+
+msgid "Older posts"
+msgstr "Pli malnovaj artikoloj"
+
+msgid "Archive"
+msgstr "Arĥivo"
+
+msgid "Posted"
+msgstr "Skribita"
+
+msgid "Posts for {month} {year}"
+msgstr "Artikoloj skribitaj en {month} {year}"
diff --git a/translations/nikola.messages/es.po b/translations/nikola.messages/es.po
index 5d34152..76a6925 100644
--- a/translations/nikola.messages/es.po
+++ b/translations/nikola.messages/es.po
@@ -4,14 +4,15 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 18:52+0000\n"
-"Last-Translator: ralsina <ralsina@netmanagers.com.ar>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Spanish (http://www.transifex.com/projects/p/nikola/language/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -28,6 +29,12 @@ msgstr "Español"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Categorías"
+
+msgid "Tags and Categories"
+msgstr "Tags y Categorías"
+
msgid "Read more"
msgstr "Leer más"
@@ -43,6 +50,8 @@ msgstr "posts antiguos página %d"
msgid "Source"
msgstr "Código"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Leer en español"
diff --git a/translations/nikola.messages/fa.po b/translations/nikola.messages/fa.po
new file mode 100644
index 0000000..7b0bc84
--- /dev/null
+++ b/translations/nikola.messages/fa.po
@@ -0,0 +1,85 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# Shahinism <ishahinism@gmail.com>, 2013
+# sazary <soroosh@azary.ir>, 2013
+# Shahinism <ishahinism@gmail.com>, 2013
+# sazary <soroosh@azary.ir>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Persian (http://www.transifex.com/projects/p/nikola/language/fa/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fa\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgid "More posts about"
+msgstr "ارسال‌های بیشتر دربارهٔ"
+
+msgid "LANGUAGE"
+msgstr "فارسی"
+
+msgid "Tags"
+msgstr "برچسب‌ها"
+
+msgid "Categories"
+msgstr "دسته‌ها"
+
+msgid "Tags and Categories"
+msgstr "برچسب‌ها و دسته‌ها"
+
+msgid "Read more"
+msgstr "بیشتر بخوانید"
+
+msgid "Posts about %s"
+msgstr "ارسال‌ها دربارهٔ %s"
+
+msgid "Next post"
+msgstr "ارسال بعدی"
+
+msgid "old posts page %d"
+msgstr "صفحهٔ ارسال‌های قدیمی %d"
+
+msgid "Source"
+msgstr "منبع"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "به فارسی بخوانید"
+
+msgid "Posts for year %s"
+msgstr "ارسال‌ها برای سال %s"
+
+msgid "Newer posts"
+msgstr "ارسال‌های جدید‌تر"
+
+msgid "Previous post"
+msgstr "ارسال پیشین"
+
+msgid "Also available in"
+msgstr "همچنین قابل دسترس از"
+
+msgid "Original site"
+msgstr "سایت اصلی"
+
+msgid "Older posts"
+msgstr "پست‌های قدیمی‌تر"
+
+msgid "Archive"
+msgstr "آرشیو"
+
+msgid "Posted"
+msgstr "ارسال شده"
+
+msgid "Posts for {month} {year}"
+msgstr "ارسال برای {month} {year}"
diff --git a/translations/nikola.messages/fi.po b/translations/nikola.messages/fi.po
new file mode 100644
index 0000000..4949cf2
--- /dev/null
+++ b/translations/nikola.messages/fi.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# Eero Kari <eero.kari@gmail.com>, 2013
+# Eero Kari <eero.kari@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/nikola/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "More posts about"
+msgstr "Lisää postauksia aiheesta"
+
+msgid "LANGUAGE"
+msgstr "Suomi"
+
+msgid "Tags"
+msgstr "Tagit"
+
+msgid "Categories"
+msgstr "Kategoriat"
+
+msgid "Tags and Categories"
+msgstr "Tagit ja kategoriat"
+
+msgid "Read more"
+msgstr "Lue lisää"
+
+msgid "Posts about %s"
+msgstr "Postauksia aiheesta %s"
+
+msgid "Next post"
+msgstr "Seuraava postaus"
+
+msgid "old posts page %d"
+msgstr "vanhojen postauksien sivu %d"
+
+msgid "Source"
+msgstr "Lähde"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Lue suomeksi"
+
+msgid "Posts for year %s"
+msgstr "Postauksia vuodelta %s"
+
+msgid "Newer posts"
+msgstr "Uudempia postauksia"
+
+msgid "Previous post"
+msgstr "Vanhempia postauksia"
+
+msgid "Also available in"
+msgstr "Saatavilla myös"
+
+msgid "Original site"
+msgstr "Alkuperäinen sivusto"
+
+msgid "Older posts"
+msgstr "Vanhempia postauksia"
+
+msgid "Archive"
+msgstr "Arkisto"
+
+msgid "Posted"
+msgstr "Postattu"
+
+msgid "Posts for {month} {year}"
+msgstr "Postauksia ajalle {month} {year}"
diff --git a/translations/nikola.messages/fr.po b/translations/nikola.messages/fr.po
index aeb6ec5..35edf36 100644
--- a/translations/nikola.messages/fr.po
+++ b/translations/nikola.messages/fr.po
@@ -4,15 +4,17 @@
#
# Translators:
# Translators:
-# Pablo SEMINARIO <pabluk@gmail.com>, 2013.
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# pabluk <pablo@seminar.io>, 2013
+# pabluk <pablo@seminar.io>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-13 12:52+0000\n"
-"Last-Translator: pabluk <pabluk@gmail.com>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: French (http://www.transifex.com/projects/p/nikola/language/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +31,12 @@ msgstr "Français"
msgid "Tags"
msgstr "Étiquettes"
+msgid "Categories"
+msgstr "Catégories"
+
+msgid "Tags and Categories"
+msgstr "Étiquettes et catégories"
+
msgid "Read more"
msgstr "Lire la suite"
@@ -44,6 +52,8 @@ msgstr "anciens billets page %d"
msgid "Source"
msgstr "Source"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Lire en français"
diff --git a/translations/nikola.messages/hr.po b/translations/nikola.messages/hr.po
new file mode 100644
index 0000000..ccdfe61
--- /dev/null
+++ b/translations/nikola.messages/hr.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# tty, 2013
+# tty, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/nikola/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+msgid "More posts about"
+msgstr "Više postova o"
+
+msgid "LANGUAGE"
+msgstr "hrvatski"
+
+msgid "Tags"
+msgstr "Tagovi"
+
+msgid "Categories"
+msgstr ""
+
+msgid "Tags and Categories"
+msgstr ""
+
+msgid "Read more"
+msgstr "Čitaj dalje"
+
+msgid "Posts about %s"
+msgstr "Postovi o %s"
+
+msgid "Next post"
+msgstr "Sljedeći post"
+
+msgid "old posts page %d"
+msgstr "stari postovi stranice %d"
+
+msgid "Source"
+msgstr "Izvor"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Čitaj na hrvatskom"
+
+msgid "Posts for year %s"
+msgstr "Postovi za godinu %s"
+
+msgid "Newer posts"
+msgstr "Noviji postovi"
+
+msgid "Previous post"
+msgstr "Prethodni post"
+
+msgid "Also available in"
+msgstr "Također dostupno i u"
+
+msgid "Original site"
+msgstr "Izvorna stranica"
+
+msgid "Older posts"
+msgstr "Stariji postovi"
+
+msgid "Archive"
+msgstr "Arhiva"
+
+msgid "Posted"
+msgstr "Objavljeno"
+
+msgid "Posts for {month} {year}"
+msgstr "Postovi za {month} {year}"
diff --git a/translations/nikola.messages/it.po b/translations/nikola.messages/it.po
index 9b86f15..cd24846 100644
--- a/translations/nikola.messages/it.po
+++ b/translations/nikola.messages/it.po
@@ -4,14 +4,16 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# Alessandro Pisa <alessandro.pisa@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 18:50+0000\n"
-"Last-Translator: ralsina <ralsina@netmanagers.com.ar>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Italian (http://www.transifex.com/projects/p/nikola/language/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -20,7 +22,7 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "More posts about"
-msgstr "Altri articoli s"
+msgstr "Altri articoli collegati"
msgid "LANGUAGE"
msgstr "Italiano"
@@ -28,6 +30,12 @@ msgstr "Italiano"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Categorie"
+
+msgid "Tags and Categories"
+msgstr "Tags e Categorie"
+
msgid "Read more"
msgstr "Espandi"
@@ -41,8 +49,10 @@ msgid "old posts page %d"
msgstr "pagina dei vecchi articoli %d"
msgid "Source"
-msgstr "Source"
+msgstr "Sorgente"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Leggi in italiano"
@@ -62,7 +72,7 @@ msgid "Original site"
msgstr "Sito originale"
msgid "Older posts"
-msgstr "Articoli vecchi"
+msgstr "Articoli precedenti"
msgid "Archive"
msgstr "Archivio"
@@ -71,4 +81,4 @@ msgid "Posted"
msgstr "Pubblicato"
msgid "Posts for {month} {year}"
-msgstr ""
+msgstr "Articoli per {month} {year}"
diff --git a/translations/nikola.messages/ja.po b/translations/nikola.messages/ja.po
index c439ebe..f95eb24 100644
--- a/translations/nikola.messages/ja.po
+++ b/translations/nikola.messages/ja.po
@@ -4,15 +4,17 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
-# Yasuhiko Shiga <4wordextinguisher@gmail.com>, 2013.
+# quoth <4wordextinguisher@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# quoth <4wordextinguisher@gmail.com>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-14 03:31+0000\n"
-"Last-Translator: quoth <4wordextinguisher@gmail.com>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Japanese (http://www.transifex.com/projects/p/nikola/language/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +31,12 @@ msgstr "日本語"
msgid "Tags"
msgstr "タグ"
+msgid "Categories"
+msgstr ""
+
+msgid "Tags and Categories"
+msgstr ""
+
msgid "Read more"
msgstr "続きを読む"
@@ -44,6 +52,8 @@ msgstr "前の記事 %dページ目"
msgid "Source"
msgstr "ソース"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "日本語で読む"
diff --git a/translations/nikola.messages/nl.po b/translations/nikola.messages/nl.po
new file mode 100644
index 0000000..82291ca
--- /dev/null
+++ b/translations/nikola.messages/nl.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# Joes <joes@staalkemade.net>, 2013
+# Joes <joes@staalkemade.net>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/nikola/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "More posts about"
+msgstr "Meer berichten over"
+
+msgid "LANGUAGE"
+msgstr "Nederlands"
+
+msgid "Tags"
+msgstr "Tags"
+
+msgid "Categories"
+msgstr "Categorieën"
+
+msgid "Tags and Categories"
+msgstr "Tags en Categorieën"
+
+msgid "Read more"
+msgstr "Lees verder"
+
+msgid "Posts about %s"
+msgstr "Berichten over %s"
+
+msgid "Next post"
+msgstr "Volgend bericht"
+
+msgid "old posts page %d"
+msgstr "oude berichten pagina %d"
+
+msgid "Source"
+msgstr "Bron"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Lees in het Nederlands"
+
+msgid "Posts for year %s"
+msgstr "Berichten voor het jaar %s"
+
+msgid "Newer posts"
+msgstr "Nieuwere berichten"
+
+msgid "Previous post"
+msgstr "Vorig bericht"
+
+msgid "Also available in"
+msgstr "Ook beschikbaar in"
+
+msgid "Original site"
+msgstr "Originele site"
+
+msgid "Older posts"
+msgstr "Oudere berichten"
+
+msgid "Archive"
+msgstr "Archief"
+
+msgid "Posted"
+msgstr "Geplaatst"
+
+msgid "Posts for {month} {year}"
+msgstr "Berichten voor {month} {year}"
diff --git a/translations/nikola.messages/pl.po b/translations/nikola.messages/pl.po
index bf796b1..5b073c4 100644
--- a/translations/nikola.messages/pl.po
+++ b/translations/nikola.messages/pl.po
@@ -4,14 +4,16 @@
#
# Translators:
# Translators:
-# <kwpolska@gmail.com>, 2013.
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# Kwpolska <kwpolska@gmail.com>, 2013
+# Kwpolska <kwpolska@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 19:00+0000\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Polish (http://www.transifex.com/projects/p/nikola/language/pl/)\n"
"MIME-Version: 1.0\n"
@@ -29,6 +31,12 @@ msgstr "polski"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Kategorie"
+
+msgid "Tags and Categories"
+msgstr "Tagi i Kategorie"
+
msgid "Read more"
msgstr "Czytaj więcej"
@@ -44,6 +52,8 @@ msgstr "stare posty, strona %d"
msgid "Source"
msgstr "Źródło"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Czytaj po polsku"
diff --git a/translations/nikola.messages/pt_BR.po b/translations/nikola.messages/pt_BR.po
index 37e7a45..f2b1556 100644
--- a/translations/nikola.messages/pt_BR.po
+++ b/translations/nikola.messages/pt_BR.po
@@ -4,15 +4,19 @@
#
# Translators:
# Translators:
-# eduardo <schettino72@gmail.com>, 2013.
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# schettino72 <schettino72@gmail.com>, 2013
+# Kwpolska <kwpolska@gmail.com>, 2013
+# Kwpolska <kwpolska@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# schettino72 <schettino72@gmail.com>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-13 14:29+0000\n"
-"Last-Translator: schettino72 <schettino72@gmail.com>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/nikola/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +33,12 @@ msgstr "Português"
msgid "Tags"
msgstr "Tags"
+msgid "Categories"
+msgstr "Categorias"
+
+msgid "Tags and Categories"
+msgstr "Tags e Categorias"
+
msgid "Read more"
msgstr "Leia mais"
@@ -44,6 +54,8 @@ msgstr "Posts antigos página %d"
msgid "Source"
msgstr "Código"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Ler em português"
diff --git a/translations/nikola.messages/ru.po b/translations/nikola.messages/ru.po
index eb9fdc8..981b085 100644
--- a/translations/nikola.messages/ru.po
+++ b/translations/nikola.messages/ru.po
@@ -4,14 +4,17 @@
#
# Translators:
# Translators:
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# m.arnold <lwarxx@gmail.com>, 2013
+# m.arnold <lwarxx@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-06 18:50+0000\n"
-"Last-Translator: ralsina <ralsina@netmanagers.com.ar>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Russian (http://www.transifex.com/projects/p/nikola/language/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -28,21 +31,29 @@ msgstr "Русский"
msgid "Tags"
msgstr "Тэги"
+msgid "Categories"
+msgstr "Категории"
+
+msgid "Tags and Categories"
+msgstr "Тэги и категории"
+
msgid "Read more"
-msgstr "Продолжить чтение"
+msgstr "Читать далее"
msgid "Posts about %s"
-msgstr "Записи с тэгом %s:"
+msgstr "Записи о %s"
msgid "Next post"
msgstr "Следующая запись"
msgid "old posts page %d"
-msgstr "страница со старыми записями %d"
+msgstr "%d страница со старыми записями"
msgid "Source"
-msgstr "Source"
+msgstr "Источник"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "Прочесть по-русски"
@@ -56,7 +67,7 @@ msgid "Previous post"
msgstr "Предыдущая запись"
msgid "Also available in"
-msgstr "Также доступно в"
+msgstr "Также доступно на"
msgid "Original site"
msgstr "Оригинальный сайт"
@@ -71,4 +82,4 @@ msgid "Posted"
msgstr "Опубликовано"
msgid "Posts for {month} {year}"
-msgstr ""
+msgstr "Записи за {month} {year}"
diff --git a/translations/nikola.messages/sl.po b/translations/nikola.messages/sl.po
new file mode 100644
index 0000000..19fb3b1
--- /dev/null
+++ b/translations/nikola.messages/sl.po
@@ -0,0 +1,83 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# Venčeslav Vezjak <vezjakv@gmail.com>, 2013
+# Venčeslav Vezjak <vezjakv@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Slovenian (http://www.transifex.com/projects/p/nikola/language/sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sl\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
+
+msgid "More posts about"
+msgstr "Več objav o"
+
+msgid "LANGUAGE"
+msgstr "Slovenščina"
+
+msgid "Tags"
+msgstr "Značke"
+
+msgid "Categories"
+msgstr "Kategorije"
+
+msgid "Tags and Categories"
+msgstr "Značke in kategorije"
+
+msgid "Read more"
+msgstr "Več o tem"
+
+msgid "Posts about %s"
+msgstr "Objave o %s"
+
+msgid "Next post"
+msgstr "Naslednja objava"
+
+msgid "old posts page %d"
+msgstr "stare objave, stran %d"
+
+msgid "Source"
+msgstr "Izvor"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Beri v slovenščini"
+
+msgid "Posts for year %s"
+msgstr "Objave za leto %s"
+
+msgid "Newer posts"
+msgstr "Novejše objave"
+
+msgid "Previous post"
+msgstr "Prejšnja objava"
+
+msgid "Also available in"
+msgstr "Na voljo tudi v"
+
+msgid "Original site"
+msgstr "Izvorna spletna stran"
+
+msgid "Older posts"
+msgstr "Starejše objave"
+
+msgid "Archive"
+msgstr "Arhiv"
+
+msgid "Posted"
+msgstr "Objavljeno"
+
+msgid "Posts for {month} {year}"
+msgstr "Objave za {month} {year}"
diff --git a/translations/nikola.messages/tr_TR.po b/translations/nikola.messages/tr_TR.po
new file mode 100644
index 0000000..adab203
--- /dev/null
+++ b/translations/nikola.messages/tr_TR.po
@@ -0,0 +1,84 @@
+# Messages in Nikola
+# Copyright (C) 2012-2013
+# This file is distributed under the same license as the Nikola package.
+#
+# Translators:
+# Translators:
+# Batuhan Büyükgüzel <bbuyukguzel@gmail.com>, 2013
+# Caner BAŞARAN <basaran.caner@gmail.com>, 2013
+# Caner BAŞARAN <basaran.caner@gmail.com>, 2013
+msgid ""
+msgstr ""
+"Project-Id-Version: Nikola\n"
+"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
+"POT-Creation-Date: 2013-03-15 12:24-0300\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/nikola/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+msgid "More posts about"
+msgstr "ilgili diğer yazılar"
+
+msgid "LANGUAGE"
+msgstr "Türkçe"
+
+msgid "Tags"
+msgstr "Etiketler"
+
+msgid "Categories"
+msgstr "Kategoriler"
+
+msgid "Tags and Categories"
+msgstr "Etiketler ve Kategoriler"
+
+msgid "Read more"
+msgstr "Devamını oku"
+
+msgid "Posts about %s"
+msgstr "%s ile ilgili yazılar"
+
+msgid "Next post"
+msgstr "Sonraki yazı"
+
+msgid "old posts page %d"
+msgstr "eski yazılar sayfa %d"
+
+msgid "Source"
+msgstr "Kaynak"
+
+#. Here your translation should not say ENglish but the name of your language
+#. instead
+msgid "Read in English"
+msgstr "Türkçe olarak oku"
+
+msgid "Posts for year %s"
+msgstr "%s yılındaki yazılar"
+
+msgid "Newer posts"
+msgstr "Daha yeni yazılar"
+
+msgid "Previous post"
+msgstr "Önceki yazı"
+
+msgid "Also available in"
+msgstr "Şu dilde de mevcut:"
+
+msgid "Original site"
+msgstr "Orjinal web sayfası"
+
+msgid "Older posts"
+msgstr "Daha eski yazılar"
+
+msgid "Archive"
+msgstr "Arşiv"
+
+msgid "Posted"
+msgstr "Yayın tarihi"
+
+msgid "Posts for {month} {year}"
+msgstr "{month} {year} göre yazılar"
diff --git a/translations/nikola.messages/zh_CN.po b/translations/nikola.messages/zh_CN.po
index da56182..f96b7a0 100644
--- a/translations/nikola.messages/zh_CN.po
+++ b/translations/nikola.messages/zh_CN.po
@@ -4,15 +4,17 @@
#
# Translators:
# Translators:
-# Kade For <kadefor@gmail.com>, 2013.
-# Roberto Alsina <ralsina@netmanagers.com.ar>, 2013.
+# kadefor <kadefor@gmail.com>, 2013
+# kadefor <kadefor@gmail.com>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
+# ralsina <ralsina@netmanagers.com.ar>, 2013
msgid ""
msgstr ""
"Project-Id-Version: Nikola\n"
"Report-Msgid-Bugs-To: ralsina@netmanagers.com.ar\n"
"POT-Creation-Date: 2013-03-15 12:24-0300\n"
-"PO-Revision-Date: 2013-04-14 08:24+0000\n"
-"Last-Translator: kadefor <kadefor@gmail.com>\n"
+"PO-Revision-Date: 2013-10-25 09:00+0000\n"
+"Last-Translator: Kwpolska <kwpolska@gmail.com>\n"
"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/nikola/language/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -29,6 +31,12 @@ msgstr "简体中文"
msgid "Tags"
msgstr "标签"
+msgid "Categories"
+msgstr ""
+
+msgid "Tags and Categories"
+msgstr ""
+
msgid "Read more"
msgstr "更多"
@@ -44,6 +52,8 @@ msgstr "旧文章页 %d"
msgid "Source"
msgstr "源代码"
+#. Here your translation should not say ENglish but the name of your language
+#. instead
msgid "Read in English"
msgstr "中文版"