aboutsummaryrefslogtreecommitdiffstats
path: root/nikola
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-05-30 17:41:06 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-05-30 17:41:06 -0300
commit0c4dfdec5b55b6064dccc38bbfb0a7c0699c895a (patch)
treea6707225ccc559f7edf50ddd3fdc7fc85145c921 /nikola
parent8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (diff)
Imported Upstream version 5.4.4
Diffstat (limited to 'nikola')
-rw-r--r--nikola/conf.py.in43
-rw-r--r--nikola/data/themes/default/assets/css/code.css62
-rw-r--r--nikola/data/themes/default/assets/css/slides.css11
-rw-r--r--nikola/data/themes/default/assets/css/theme.css11
-rwxr-xr-xnikola/data/themes/default/assets/js/slides.jquery.js555
-rw-r--r--nikola/data/themes/default/bundles8
-rw-r--r--nikola/data/themes/default/messages/messages_ca.py23
-rw-r--r--nikola/data/themes/default/messages/messages_de.py25
-rw-r--r--nikola/data/themes/default/messages/messages_el.py23
-rw-r--r--nikola/data/themes/default/messages/messages_en.py21
-rw-r--r--nikola/data/themes/default/messages/messages_es.py23
-rw-r--r--nikola/data/themes/default/messages/messages_fr.py24
-rw-r--r--nikola/data/themes/default/messages/messages_it.py24
-rw-r--r--nikola/data/themes/default/messages/messages_ja.py23
-rw-r--r--nikola/data/themes/default/messages/messages_pl.py25
-rw-r--r--nikola/data/themes/default/messages/messages_pt_br.py (renamed from nikola/data/themes/default/messages/messages_pt-br.py)21
-rw-r--r--nikola/data/themes/default/messages/messages_ru.py23
-rw-r--r--nikola/data/themes/default/messages/messages_zh_cn.py (renamed from nikola/data/themes/default/messages/messages_zh-cn.py)45
-rw-r--r--nikola/data/themes/default/templates/base.tmpl6
-rw-r--r--nikola/data/themes/default/templates/base_helper.tmpl14
-rw-r--r--nikola/data/themes/default/templates/disqus_helper.tmpl7
-rw-r--r--nikola/data/themes/default/templates/index.tmpl10
-rw-r--r--nikola/data/themes/default/templates/index_helper.tmpl4
-rw-r--r--nikola/data/themes/default/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/default/templates/post.tmpl12
-rw-r--r--nikola/data/themes/default/templates/post_helper.tmpl26
-rw-r--r--nikola/data/themes/default/templates/story.tmpl10
-rw-r--r--nikola/data/themes/default/templates/tag.tmpl27
-rw-r--r--nikola/data/themes/default/templates/tags.tmpl18
-rw-r--r--nikola/data/themes/jinja-default/templates/base.tmpl10
-rw-r--r--nikola/data/themes/jinja-default/templates/index.tmpl12
-rw-r--r--nikola/data/themes/jinja-default/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/jinja-default/templates/post.tmpl27
-rw-r--r--nikola/data/themes/jinja-default/templates/story.tmpl21
-rw-r--r--nikola/data/themes/jinja-default/templates/tag.tmpl2
-rw-r--r--nikola/data/themes/jinja-default/templates/tags.tmpl4
-rw-r--r--nikola/data/themes/monospace/assets/css/code.css62
-rw-r--r--nikola/data/themes/monospace/bundles1
-rw-r--r--nikola/data/themes/monospace/templates/base.tmpl6
-rw-r--r--nikola/data/themes/monospace/templates/base_helper.tmpl36
-rw-r--r--nikola/data/themes/monospace/templates/disqus_helper.tmpl7
-rw-r--r--nikola/data/themes/monospace/templates/index.tmpl12
-rw-r--r--nikola/data/themes/monospace/templates/index_helper.tmpl4
-rw-r--r--nikola/data/themes/monospace/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/monospace/templates/post.tmpl15
-rw-r--r--nikola/data/themes/monospace/templates/post_helper.tmpl42
-rw-r--r--nikola/data/themes/monospace/templates/story.tmpl10
-rw-r--r--nikola/data/themes/monospace/templates/tag.tmpl2
l---------nikola/data/themes/orphan/assets/css/code.css1
-rw-r--r--nikola/data/themes/orphan/templates/base.tmpl4
-rw-r--r--nikola/data/themes/orphan/templates/base_helper.tmpl36
-rw-r--r--nikola/data/themes/orphan/templates/disqus_helper.tmpl7
-rw-r--r--nikola/data/themes/orphan/templates/index.tmpl10
-rw-r--r--nikola/data/themes/orphan/templates/index_helper.tmpl4
-rw-r--r--nikola/data/themes/orphan/templates/list_post.tmpl2
-rw-r--r--nikola/data/themes/orphan/templates/post.tmpl12
-rw-r--r--nikola/data/themes/orphan/templates/post_helper.tmpl44
-rw-r--r--nikola/data/themes/orphan/templates/story.tmpl10
-rw-r--r--nikola/data/themes/orphan/templates/tag.tmpl2
-rw-r--r--nikola/data/themes/site-planetoid/README1
-rw-r--r--nikola/data/themes/site-planetoid/engine1
-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/assets/css/theme.css14
-rw-r--r--nikola/data/themes/site/templates/base.tmpl12
-rw-r--r--nikola/main.py7
-rw-r--r--nikola/nikola.py157
-rw-r--r--nikola/plugin_categories.py11
-rw-r--r--nikola/plugins/command_check.py21
-rw-r--r--nikola/plugins/command_console.py76
-rw-r--r--nikola/plugins/command_deploy.py26
-rw-r--r--nikola/plugins/command_import_blogger.py17
-rw-r--r--nikola/plugins/command_import_wordpress.py32
-rw-r--r--nikola/plugins/command_install_theme.py4
-rw-r--r--nikola/plugins/command_new_post.py36
-rw-r--r--nikola/plugins/command_planetoid.plugin9
-rw-r--r--nikola/plugins/command_planetoid/__init__.py287
-rw-r--r--nikola/plugins/compile_bbcode.py16
-rw-r--r--nikola/plugins/compile_html.py14
-rw-r--r--nikola/plugins/compile_ipynb.plugin10
-rw-r--r--nikola/plugins/compile_ipynb/README.txt35
-rw-r--r--nikola/plugins/compile_ipynb/__init__.py100
-rw-r--r--nikola/plugins/compile_markdown/__init__.py51
-rw-r--r--nikola/plugins/compile_markdown/mdx_gist.py189
-rw-r--r--nikola/plugins/compile_markdown/mdx_nikola.py56
-rw-r--r--nikola/plugins/compile_markdown/mdx_podcast.py87
-rw-r--r--nikola/plugins/compile_misaka.plugin10
-rw-r--r--nikola/plugins/compile_misaka/__init__.py82
-rw-r--r--nikola/plugins/compile_rest/__init__.py81
-rw-r--r--nikola/plugins/compile_rest/dummy.py44
-rw-r--r--nikola/plugins/compile_rest/gist_directive.py2
-rw-r--r--nikola/plugins/compile_rest/listing.py121
-rw-r--r--nikola/plugins/compile_rest/pygments_code_block_directive.py424
-rw-r--r--nikola/plugins/compile_rest/slides.py79
-rw-r--r--nikola/plugins/compile_rest/soundcloud.py62
-rw-r--r--nikola/plugins/compile_rest/vimeo.py114
-rw-r--r--nikola/plugins/compile_rest/youtube.py57
-rw-r--r--nikola/plugins/compile_textile.py14
-rw-r--r--nikola/plugins/compile_txt2tags.py14
-rw-r--r--nikola/plugins/compile_wiki.py12
-rw-r--r--nikola/plugins/task_archive.py66
-rw-r--r--nikola/plugins/task_copy_assets.py25
-rw-r--r--nikola/plugins/task_create_bundles.py46
-rw-r--r--nikola/plugins/task_indexes.py35
-rw-r--r--nikola/plugins/task_localsearch.plugin10
-rw-r--r--nikola/plugins/task_localsearch/MIT-LICENSE.txt20
-rw-r--r--nikola/plugins/task_localsearch/__init__.py102
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/expand.pngbin0 -> 424 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/link.pngbin0 -> 463 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/loader.gifbin0 -> 4178 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/search.gifbin0 -> 208 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/tipuesearch.css232
-rw-r--r--nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js426
-rw-r--r--nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js28
-rwxr-xr-xnikola/plugins/task_localsearch/files/tipue_search.html31
-rw-r--r--nikola/plugins/task_mustache.plugin10
-rw-r--r--nikola/plugins/task_mustache/__init__.py197
-rw-r--r--nikola/plugins/task_mustache/mustache-template.html29
-rw-r--r--nikola/plugins/task_mustache/mustache.html36
-rw-r--r--nikola/plugins/task_redirect.py2
-rw-r--r--nikola/plugins/task_render_galleries.py29
-rw-r--r--nikola/plugins/task_render_listings.py12
-rw-r--r--nikola/plugins/task_render_pages.py5
-rw-r--r--nikola/plugins/task_render_posts.py84
-rw-r--r--nikola/plugins/task_render_rss.py11
-rw-r--r--nikola/plugins/task_render_sources.py21
-rw-r--r--nikola/plugins/task_render_tags.py20
-rw-r--r--nikola/plugins/task_sitemap/__init__.py94
-rw-r--r--nikola/plugins/task_sitemap/sitemap_gen.py2137
-rw-r--r--nikola/post.py312
-rw-r--r--nikola/rc4.py76
-rw-r--r--nikola/utils.py149
134 files changed, 4077 insertions, 4139 deletions
diff --git a/nikola/conf.py.in b/nikola/conf.py.in
index 7d75295..c2bf8ff 100644
--- a/nikola/conf.py.in
+++ b/nikola/conf.py.in
@@ -17,7 +17,7 @@ BLOG_TITLE = "${BLOG_TITLE}"
SITE_URL = "${SITE_URL}"
# This is the URL where nikola's output will be deployed.
# If not set, defaults to SITE_URL
-# BASE_URL = "${SITE_URL}
+# BASE_URL = "${SITE_URL}"
BLOG_EMAIL = "${BLOG_EMAIL}"
BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}"
@@ -47,7 +47,7 @@ DEFAULT_LANG = "${DEFAULT_LANG}"
# The format is {"translationcode" : "path/to/translation" }
# the path will be used as a prefix for the generated pages location
TRANSLATIONS = {
- "${DEFAULT_LANG}": "",
+ DEFAULT_LANG: "",
# Example for another language:
# "es": "./es",
}
@@ -58,6 +58,7 @@ SIDEBAR_LINKS = {
DEFAULT_LANG: (
('/archive.html', 'Archives'),
('/categories/index.html', 'Tags'),
+ ('/rss.xml', 'RSS'),
),
}
@@ -109,6 +110,12 @@ post_compilers = ${POST_COMPILERS}
# Set to False for two-file posts, with separate metadata.
# ONE_FILE_POSTS = True
+# If this is set to True, then posts that are not translated to a language
+# LANG will not be visible at all in the pages in that language.
+# If set to False, the DEFAULT_LANG version will be displayed for
+# untranslated posts.
+# HIDE_UNTRANSLATED_POSTS = False
+
# Paths for different autogenerated bits. These are combined with the
# translation paths.
@@ -124,11 +131,16 @@ post_compilers = ${POST_COMPILERS}
# Final location is output / TRANSLATION[lang] / INDEX_PATH / index-*.html
# INDEX_PATH = ""
+
+# Create per-month archives instead of per-year
+# CREATE_MONTHLY_ARCHIVE = False
# Final locations for the archives are:
# output / TRANSLATION[lang] / ARCHIVE_PATH / ARCHIVE_FILENAME
# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / index.html
+# output / TRANSLATION[lang] / ARCHIVE_PATH / YEAR / MONTH / index.html
# ARCHIVE_PATH = ""
# ARCHIVE_FILENAME = "archive.html"
+
# Final locations are:
# output / TRANSLATION[lang] / RSS_PATH / rss.xml
# RSS_PATH = ""
@@ -208,11 +220,17 @@ post_compilers = ${POST_COMPILERS}
# 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
-# Name of the theme to use. Themes are located in themes/theme_name
+# Name of the theme to use.
# THEME = 'site'
+# 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
+
# 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_CONGIF_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
@@ -261,6 +279,11 @@ 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.
+# http://mysite/foo/bar/index.html => http://mysite/foo/bar/
+# Default = False
+# STRIP_INDEX_HTML = False
+
# Do you want a add a Mathjax config file?
# MATHJAX_CONFIG = ""
@@ -328,6 +351,9 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# external resources.
# USE_CDN = False
+# Extra things you want in the pages HEAD tag. This will be added right
+# before </HEAD>
+# EXTRA_HEAD_DATA = ""
# Google analytics or whatever else you use. Added to the bottom of <body>
# in the default template (base.tmpl).
# ANALYTICS = ""
@@ -379,6 +405,15 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL,
# Plugins you don't want to use. Be careful :-)
# DISABLED_PLUGINS = ["render_galleries"]
+# Experimental plugins - use at your own risk.
+# They probably need some manual adjustments - please see their respective readme.
+# ENABLED_EXTRAS = [
+# 'planetoid',
+# 'ipynb',
+# 'localsearch',
+# 'mustache',
+# ]
+
# 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/themes/default/assets/css/code.css b/nikola/data/themes/default/assets/css/code.css
deleted file mode 100644
index b1d7ace..0000000
--- a/nikola/data/themes/default/assets/css/code.css
+++ /dev/null
@@ -1,62 +0,0 @@
-pre { word-break: pre; white-space: pre; word-wrap: pre; overflow: auto; max-width: 100%;}
-td.linenos { vertical-align: top; width: 4em;}
-div.code > pre, .code
-{ background: #f8f8f8; white-space: pre;}
-.code .c { color: #008800; font-style: italic } /* Comment */
-.code .err { border: 1px solid #FF0000 } /* Error */
-.code .k { color: #AA22FF; font-weight: bold } /* Keyword */
-.code .o { color: #666666 } /* Operator */
-.code .cm { color: #008800; font-style: italic } /* Comment.Multiline */
-.code .cp { color: #008800 } /* Comment.Preproc */
-.code .c1 { color: #008800; font-style: italic } /* Comment.Single */
-.code .cs { color: #008800; font-weight: bold } /* Comment.Special */
-.code .gd { color: #A00000 } /* Generic.Deleted */
-.code .ge { font-style: italic } /* Generic.Emph */
-.code .gr { color: #FF0000 } /* Generic.Error */
-.code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.code .gi { color: #00A000 } /* Generic.Inserted */
-.code .go { color: #808080 } /* Generic.Output */
-.code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
-.code .gs { font-weight: bold } /* Generic.Strong */
-.code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.code .gt { color: #0040D0 } /* Generic.Traceback */
-.code .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
-.code .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
-.code .kp { color: #AA22FF } /* Keyword.Pseudo */
-.code .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
-.code .kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */
-.code .m { color: #666666 } /* Literal.Number */
-.code .s { color: #BB4444 } /* Literal.String */
-.code .na { color: #BB4444 } /* Name.Attribute */
-.code .nb { color: #AA22FF } /* Name.Builtin */
-.code .nc { color: #0000FF } /* Name.Class */
-.code .no { color: #880000 } /* Name.Constant */
-.code .nd { color: #AA22FF } /* Name.Decorator */
-.code .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
-.code .nf { color: #00A000 } /* Name.Function */
-.code .nl { color: #A0A000 } /* Name.Label */
-.code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
-.code .nt { color: #008000; font-weight: bold } /* Name.Tag */
-.code .nv { color: #B8860B } /* Name.Variable */
-.code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.code .mf { color: #666666 } /* Literal.Number.Float */
-.code .mh { color: #666666 } /* Literal.Number.Hex */
-.code .mi { color: #666666 } /* Literal.Number.Integer */
-.code .mo { color: #666666 } /* Literal.Number.Oct */
-.code .sb { color: #BB4444 } /* Literal.String.Backtick */
-.code .sc { color: #BB4444 } /* Literal.String.Char */
-.code .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
-.code .s2 { color: #BB4444 } /* Literal.String.Double */
-.code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
-.code .sh { color: #BB4444 } /* Literal.String.Heredoc */
-.code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
-.code .sx { color: #008000 } /* Literal.String.Other */
-.code .sr { color: #BB6688 } /* Literal.String.Regex */
-.code .s1 { color: #BB4444 } /* Literal.String.Single */
-.code .ss { color: #B8860B } /* Literal.String.Symbol */
-.code .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
-.code .vc { color: #B8860B } /* Name.Variable.Class */
-.code .vg { color: #B8860B } /* Name.Variable.Global */
-.code .vi { color: #B8860B } /* Name.Variable.Instance */
-.code .il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/nikola/data/themes/default/assets/css/slides.css b/nikola/data/themes/default/assets/css/slides.css
deleted file mode 100644
index 272c83e..0000000
--- a/nikola/data/themes/default/assets/css/slides.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.slides_container {
- display: block;
- margin-left: auto;
- margin-right: auto;
- max-width: 80%;
- width: 400px;
- height: 300px;
-}
-.slide-current {
- font-weight: bold;
-}
diff --git a/nikola/data/themes/default/assets/css/theme.css b/nikola/data/themes/default/assets/css/theme.css
index 0523ce9..08a71f3 100644
--- a/nikola/data/themes/default/assets/css/theme.css
+++ b/nikola/data/themes/default/assets/css/theme.css
@@ -60,3 +60,14 @@ blockquote p, blockquote {
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/js/slides.jquery.js b/nikola/data/themes/default/assets/js/slides.jquery.js
deleted file mode 100755
index f2e09c8..0000000
--- a/nikola/data/themes/default/assets/js/slides.jquery.js
+++ /dev/null
@@ -1,555 +0,0 @@
-/*
-* Slides, A Slideshow Plugin for jQuery
-* Intructions: http://slidesjs.com
-* By: Nathan Searles, http://nathansearles.com
-* Version: 1.1.9
-* Updated: September 5th, 2011
-*
-* 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($){
- $.fn.slides = function( option ) {
- // override defaults with specified option
- option = $.extend( {}, $.fn.slides.option, option );
-
- return this.each(function(){
- // wrap slides in control container, make sure slides are block level
- $('.' + option.container, $(this)).children().wrapAll('<div class="slides_control"/>');
-
- var elem = $(this),
- control = $('.slides_control',elem),
- total = control.children().size(),
- width = control.children().outerWidth(),
- height = control.children().outerHeight(),
- start = option.start - 1,
- effect = option.effect.indexOf(',') < 0 ? option.effect : option.effect.replace(' ', '').split(',')[0],
- paginationEffect = option.effect.indexOf(',') < 0 ? effect : option.effect.replace(' ', '').split(',')[1],
- next = 0, prev = 0, number = 0, current = 0, loaded, active, clicked, position, direction, imageParent, pauseTimeout, playInterval;
-
- // is there only one slide?
- if (total < 2) {
- // Fade in .slides_container
- $('.' + option.container, $(this)).fadeIn(option.fadeSpeed, option.fadeEasing, function(){
- // let the script know everything is loaded
- loaded = true;
- // call the loaded funciton
- option.slidesLoaded();
- });
- // Hide the next/previous buttons
- $('.' + option.next + ', .' + option.prev).fadeOut(0);
- return false;
- }
-
- // animate slides
- function animate(direction, effect, clicked) {
- if (!active && loaded) {
- active = true;
- // start of animation
- option.animationStart(current + 1);
- switch(direction) {
- case 'next':
- // change current slide to previous
- prev = current;
- // get next from current + 1
- next = current + 1;
- // if last slide, set next to first slide
- next = total === next ? 0 : next;
- // set position of next slide to right of previous
- position = width*2;
- // distance to slide based on width of slides
- direction = -width*2;
- // store new current slide
- current = next;
- break;
- case 'prev':
- // change current slide to previous
- prev = current;
- // get next from current - 1
- next = current - 1;
- // if first slide, set next to last slide
- next = next === -1 ? total-1 : next;
- // set position of next slide to left of previous
- position = 0;
- // distance to slide based on width of slides
- direction = 0;
- // store new current slide
- current = next;
- break;
- case 'pagination':
- // get next from pagination item clicked, convert to number
- next = parseInt(clicked,10);
- // get previous from pagination item with class of current
- prev = $('.' + option.paginationClass + ' li.'+ option.currentClass +' a', elem).attr('href').match('[^#/]+$');
- // if next is greater then previous set position of next slide to right of previous
- if (next > prev) {
- position = width*2;
- direction = -width*2;
- } else {
- // if next is less then previous set position of next slide to left of previous
- position = 0;
- direction = 0;
- }
- // store new current slide
- current = next;
- break;
- }
-
- // fade animation
- if (effect === 'fade') {
- // fade animation with crossfade
- if (option.crossfade) {
- // put hidden next above current
- control.children(':eq('+ next +')', elem).css({
- zIndex: 10
- // fade in next
- }).fadeIn(option.fadeSpeed, option.fadeEasing, function(){
- if (option.autoHeight) {
- // animate container to height of next
- control.animate({
- height: control.children(':eq('+ next +')', elem).outerHeight()
- }, option.autoHeightSpeed, function(){
- // hide previous
- control.children(':eq('+ prev +')', elem).css({
- display: 'none',
- zIndex: 0
- });
- // reset z index
- control.children(':eq('+ next +')', elem).css({
- zIndex: 0
- });
- // end of animation
- option.animationComplete(next + 1);
- active = false;
- });
- } else {
- // hide previous
- control.children(':eq('+ prev +')', elem).css({
- display: 'none',
- zIndex: 0
- });
- // reset zindex
- control.children(':eq('+ next +')', elem).css({
- zIndex: 0
- });
- // end of animation
- option.animationComplete(next + 1);
- active = false;
- }
- });
- } else {
- // fade animation with no crossfade
- control.children(':eq('+ prev +')', elem).fadeOut(option.fadeSpeed, option.fadeEasing, function(){
- // animate to new height
- if (option.autoHeight) {
- control.animate({
- // animate container to height of next
- height: control.children(':eq('+ next +')', elem).outerHeight()
- }, option.autoHeightSpeed,
- // fade in next slide
- function(){
- control.children(':eq('+ next +')', elem).fadeIn(option.fadeSpeed, option.fadeEasing);
- });
- } else {
- // if fixed height
- control.children(':eq('+ next +')', elem).fadeIn(option.fadeSpeed, option.fadeEasing, function(){
- // fix font rendering in ie, lame
- if($.browser.msie) {
- $(this).get(0).style.removeAttribute('filter');
- }
- });
- }
- // end of animation
- option.animationComplete(next + 1);
- active = false;
- });
- }
- // slide animation
- } else {
- // move next slide to right of previous
- control.children(':eq('+ next +')').css({
- left: position,
- display: 'block'
- });
- // animate to new height
- if (option.autoHeight) {
- control.animate({
- left: direction,
- height: control.children(':eq('+ next +')').outerHeight()
- },option.slideSpeed, option.slideEasing, function(){
- control.css({
- left: -width
- });
- control.children(':eq('+ next +')').css({
- left: width,
- zIndex: 5
- });
- // reset previous slide
- control.children(':eq('+ prev +')').css({
- left: width,
- display: 'none',
- zIndex: 0
- });
- // end of animation
- option.animationComplete(next + 1);
- active = false;
- });
- // if fixed height
- } else {
- // animate control
- control.animate({
- left: direction
- },option.slideSpeed, option.slideEasing, function(){
- // after animation reset control position
- control.css({
- left: -width
- });
- // reset and show next
- control.children(':eq('+ next +')').css({
- left: width,
- zIndex: 5
- });
- // reset previous slide
- control.children(':eq('+ prev +')').css({
- left: width,
- display: 'none',
- zIndex: 0
- });
- // end of animation
- option.animationComplete(next + 1);
- active = false;
- });
- }
- }
- // set current state for pagination
- if (option.pagination) {
- // remove current class from all
- $('.'+ option.paginationClass +' li.' + option.currentClass, elem).removeClass(option.currentClass);
- // add current class to next
- $('.' + option.paginationClass + ' li:eq('+ next +')', elem).addClass(option.currentClass);
- }
- }
- } // end animate function
-
- function stop() {
- // clear interval from stored id
- clearInterval(elem.data('interval'));
- }
-
- function pause() {
- if (option.pause) {
- // clear timeout and interval
- clearTimeout(elem.data('pause'));
- clearInterval(elem.data('interval'));
- // pause slide show for option.pause amount
- pauseTimeout = setTimeout(function() {
- // clear pause timeout
- clearTimeout(elem.data('pause'));
- // start play interval after pause
- playInterval = setInterval( function(){
- animate("next", effect);
- },option.play);
- // store play interval
- elem.data('interval',playInterval);
- },option.pause);
- // store pause interval
- elem.data('pause',pauseTimeout);
- } else {
- // if no pause, just stop
- stop();
- }
- }
-
- // 2 or more slides required
- if (total < 2) {
- return;
- }
-
- // error corection for start slide
- if (start < 0) {
- start = 0;
- }
-
- if (start > total) {
- start = total - 1;
- }
-
- // change current based on start option number
- if (option.start) {
- current = start;
- }
-
- // randomizes slide order
- if (option.randomize) {
- control.randomize();
- }
-
- // make sure overflow is hidden, width is set
- $('.' + option.container, elem).css({
- overflow: 'hidden',
- // fix for ie
- position: 'relative'
- });
-
- // set css for slides
- control.children().css({
- position: 'absolute',
- top: 0,
- left: control.children().outerWidth(),
- zIndex: 0,
- display: 'none'
- });
-
- // set css for control div
- control.css({
- position: 'relative',
- // size of control 3 x slide width
- width: (width * 3),
- // set height to slide height
- height: height,
- // center control to slide
- left: -width
- });
-
- // show slides
- $('.' + option.container, elem).css({
- display: 'block'
- });
-
- // if autoHeight true, get and set height of first slide
- if (option.autoHeight) {
- control.children().css({
- height: 'auto'
- });
- control.animate({
- height: control.children(':eq('+ start +')').outerHeight()
- },option.autoHeightSpeed);
- }
-
- // checks if image is loaded
- if (option.preload && control.find('img:eq(' + start + ')').length) {
- // adds preload image
- $('.' + option.container, elem).css({
- background: 'url(' + option.preloadImage + ') no-repeat 50% 50%'
- });
-
- // gets image src, with cache buster
- var img = control.find('img:eq(' + start + ')').attr('src') + '?' + (new Date()).getTime();
-
- // check if the image has a parent
- if ($('img', elem).parent().attr('class') != 'slides_control') {
- // If image has parent, get tag name
- imageParent = control.children(':eq(0)')[0].tagName.toLowerCase();
- } else {
- // Image doesn't have parent, use image tag name
- imageParent = control.find('img:eq(' + start + ')');
- }
-
- // checks if image is loaded
- control.find('img:eq(' + start + ')').attr('src', img).load(function() {
- // once image is fully loaded, fade in
- control.find(imageParent + ':eq(' + start + ')').fadeIn(option.fadeSpeed, option.fadeEasing, function(){
- $(this).css({
- zIndex: 5
- });
- // removes preload image
- $('.' + option.container, elem).css({
- background: ''
- });
- // let the script know everything is loaded
- loaded = true;
- // call the loaded funciton
- option.slidesLoaded();
- });
- });
- } else {
- // if no preloader fade in start slide
- control.children(':eq(' + start + ')').fadeIn(option.fadeSpeed, option.fadeEasing, function(){
- // let the script know everything is loaded
- loaded = true;
- // call the loaded funciton
- option.slidesLoaded();
- });
- }
-
- // click slide for next
- if (option.bigTarget) {
- // set cursor to pointer
- control.children().css({
- cursor: 'pointer'
- });
- // click handler
- control.children().click(function(){
- // animate to next on slide click
- animate('next', effect);
- return false;
- });
- }
-
- // pause on mouseover
- if (option.hoverPause && option.play) {
- control.bind('mouseover',function(){
- // on mouse over stop
- stop();
- });
- control.bind('mouseleave',function(){
- // on mouse leave start pause timeout
- pause();
- });
- }
-
- // generate next/prev buttons
- if (option.generateNextPrev) {
- $('.' + option.container, elem).after('<a href="#" class="'+ option.prev +'">Prev</a>');
- $('.' + option.prev, elem).after('<a href="#" class="'+ option.next +'">Next</a>');
- }
-
- // next button
- $('.' + option.next ,elem).click(function(e){
- e.preventDefault();
- if (option.play) {
- pause();
- }
- animate('next', effect);
- });
-
- // previous button
- $('.' + option.prev, elem).click(function(e){
- e.preventDefault();
- if (option.play) {
- pause();
- }
- animate('prev', effect);
- });
-
- // generate pagination
- if (option.generatePagination) {
- // create unordered list
- if (option.prependPagination) {
- elem.prepend('<ul class='+ option.paginationClass +'></ul>');
- } else {
- elem.append('<ul class='+ option.paginationClass +'></ul>');
- }
- // for each slide create a list item and link
- control.children().each(function(){
- $('.' + option.paginationClass, elem).append('<li><a href="#'+ number +'">'+ (number+1) +'</a></li>');
- number++;
- });
- } else {
- // if pagination exists, add href w/ value of item number to links
- $('.' + option.paginationClass + ' li a', elem).each(function(){
- $(this).attr('href', '#' + number);
- number++;
- });
- }
-
- // add current class to start slide pagination
- $('.' + option.paginationClass + ' li:eq('+ start +')', elem).addClass(option.currentClass);
-
- // click handling
- $('.' + option.paginationClass + ' li a', elem ).click(function(){
- // pause slideshow
- if (option.play) {
- pause();
- }
- // get clicked, pass to animate function
- clicked = $(this).attr('href').match('[^#/]+$');
- // if current slide equals clicked, don't do anything
- if (current != clicked) {
- animate('pagination', paginationEffect, clicked);
- }
- return false;
- });
-
- // click handling
- $('a.link', elem).click(function(){
- // pause slideshow
- if (option.play) {
- pause();
- }
- // get clicked, pass to animate function
- clicked = $(this).attr('href').match('[^#/]+$') - 1;
- // if current slide equals clicked, don't do anything
- if (current != clicked) {
- animate('pagination', paginationEffect, clicked);
- }
- return false;
- });
-
- if (option.play) {
- // set interval
- playInterval = setInterval(function() {
- animate('next', effect);
- }, option.play);
- // store interval id
- elem.data('interval',playInterval);
- }
- });
- };
-
- // default options
- $.fn.slides.option = {
- preload: false, // boolean, Set true to preload images in an image based slideshow
- preloadImage: '/img/loading.gif', // string, Name and location of loading image for preloader. Default is "/img/loading.gif"
- container: 'slides_container', // string, Class name for slides container. Default is "slides_container"
- generateNextPrev: false, // boolean, Auto generate next/prev buttons
- next: 'next', // string, Class name for next button
- prev: 'prev', // string, Class name for previous button
- pagination: true, // boolean, If you're not using pagination you can set to false, but don't have to
- generatePagination: true, // boolean, Auto generate pagination
- prependPagination: false, // boolean, prepend pagination
- paginationClass: 'pagination', // string, Class name for pagination
- currentClass: 'current', // string, Class name for current class
- fadeSpeed: 350, // number, Set the speed of the fading animation in milliseconds
- fadeEasing: '', // string, must load jQuery's easing plugin before http://gsgd.co.uk/sandbox/jquery/easing/
- slideSpeed: 350, // number, Set the speed of the sliding animation in milliseconds
- slideEasing: '', // string, must load jQuery's easing plugin before http://gsgd.co.uk/sandbox/jquery/easing/
- start: 1, // number, Set the speed of the sliding animation in milliseconds
- effect: 'slide', // string, '[next/prev], [pagination]', e.g. 'slide, fade' or simply 'fade' for both
- crossfade: false, // boolean, Crossfade images in a image based slideshow
- randomize: false, // boolean, Set to true to randomize slides
- play: 0, // number, Autoplay slideshow, a positive number will set to true and be the time between slide animation in milliseconds
- pause: 0, // number, Pause slideshow on click of next/prev or pagination. A positive number will set to true and be the time of pause in milliseconds
- hoverPause: false, // boolean, Set to true and hovering over slideshow will pause it
- autoHeight: false, // boolean, Set to true to auto adjust height
- autoHeightSpeed: 350, // number, Set auto height animation time in milliseconds
- bigTarget: false, // boolean, Set to true and the whole slide will link to next slide on click
- animationStart: function(){}, // Function called at the start of animation
- animationComplete: function(){}, // Function called at the completion of animation
- slidesLoaded: function() {} // Function is called when slides is fully loaded
- };
-
- // Randomize slide order on load
- $.fn.randomize = function(callback) {
- function randomizeOrder() { return(Math.round(Math.random())-0.5); }
- return($(this).each(function() {
- var $this = $(this);
- var $children = $this.children();
- var childCount = $children.length;
- if (childCount > 1) {
- $children.hide();
- var indices = [];
- for (i=0;i<childCount;i++) { indices[indices.length] = i; }
- indices = indices.sort(randomizeOrder);
- $.each(indices,function(j,k) {
- var $child = $children.eq(k);
- var $clone = $child.clone(true);
- $clone.show().appendTo($this);
- if (callback !== undefined) {
- callback($child, $clone);
- }
- $child.remove();
- });
- }
- }));
- };
-})(jQuery); \ No newline at end of file
diff --git a/nikola/data/themes/default/bundles b/nikola/data/themes/default/bundles
index 35af9c0..2b81ea8 100644
--- a/nikola/data/themes/default/bundles
+++ b/nikola/data/themes/default/bundles
@@ -1,4 +1,4 @@
-assets/css/all-nocdn.css=bootstrap.css,bootstrap-responsive.css,rst.css,code.css,colorbox.css,slides.css,theme.css,custom.css
-assets/css/all.css=rst.css,code.css,colorbox.css,slides.css,theme.css,custom.css
-assets/js/all-nocdn.js=jquery-1.7.2.min.js,bootstrap.min.js,jquery.colorbox-min.js,slides.min.jquery.js
-assets/js/all.js=jquery.colorbox-min.js,slides.min.jquery.js
+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.js=jquery.colorbox-min.js
diff --git a/nikola/data/themes/default/messages/messages_ca.py b/nikola/data/themes/default/messages/messages_ca.py
index 8e7186f..a709f1b 100644
--- a/nikola/data/themes/default/messages/messages_ca.py
+++ b/nikola/data/themes/default/messages/messages_ca.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Català",
- "Posts for year %s": "Entrades de l'any %s",
- "Archive": "Arxiu",
- "Posts about %s": "Entrades sobre %s",
- "Tags": "Etiquetes",
"Also available in": "També disponibles en",
+ "Archive": "Arxiu",
+ "LANGUAGE": "Català",
"More posts about": "Més entrades sobre",
- "Posted": "Publicat",
- "Original site": "Lloc original",
- "Read in English": "Llegeix-ho en català",
- "Older posts": "Entrades anteriors",
"Newer posts": "Entrades posteriors",
- "Previous post": "Entrada anterior",
"Next post": "Entrada següent",
- "old posts page %d": "entrades antigues pàgina %d",
+ "Older posts": "Entrades anteriors",
+ "Original site": "Lloc original",
+ "Posted": "Publicat",
+ "Posts about %s": "Entrades sobre %s",
+ "Posts for year %s": "Entrades de l'any %s",
+ "Posts for {month} {year}": "",
+ "Previous post": "Entrada anterior",
+ "Read in English": "Llegeix-ho en català",
"Read more": "Llegeix-ne més",
"Source": "Codi",
+ "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/default/messages/messages_de.py
index 5da3b2b..57c784f 100644
--- a/nikola/data/themes/default/messages/messages_de.py
+++ b/nikola/data/themes/default/messages/messages_de.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Deutsch",
- "Posts for year %s": "Einträge aus dem Jahr %s",
- "Archive": "Archiv",
- "Posts about %s": "Einträge über %s",
- "Tags": "Tags",
"Also available in": "Auch verfügbar in",
+ "Archive": "Archiv",
+ "LANGUAGE": "Deutsch",
"More posts about": "Weitere Einträge über",
- "Posted": "Veröffentlicht",
- "Original site": "Original-Seite",
- "Read in English": "Auf Deutsch lesen",
- "Older posts": "Ältere Einträge",
"Newer posts": "Neuere Einträge",
- "Previous post": "Vorheriger Eintrag",
"Next post": "Nächster Eintrag",
- "Source": "Source",
+ "Older posts": "Ältere Einträge",
+ "Original site": "Original-Seite",
+ "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}",
+ "Previous post": "Vorheriger Eintrag",
+ "Read in English": "Auf Deutsch lesen",
"Read more": "Weiterlesen",
- "old posts page %d": "Vorherige Einträge %d"
+ "Source": "Source",
+ "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/default/messages/messages_el.py
new file mode 100644
index 0000000..a00f88f
--- /dev/null
+++ b/nikola/data/themes/default/messages/messages_el.py
@@ -0,0 +1,23 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "Διαθέσιμο και στα",
+ "Archive": "Αρχείο",
+ "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}": "",
+ "Previous post": "Προηγούμενη ανάρτηση",
+ "Read in English": "Διαβάστε στα Ελληνικά",
+ "Read more": "Διαβάστε περισσότερα",
+ "Source": "Πηγαίος κώδικας",
+ "Tags": "Ετικέτες",
+ "old posts page %d": "σελίδα παλαιότερων αναρτήσεων %d",
+}
diff --git a/nikola/data/themes/default/messages/messages_en.py b/nikola/data/themes/default/messages/messages_en.py
index 9fc77ef..982b654 100644
--- a/nikola/data/themes/default/messages/messages_en.py
+++ b/nikola/data/themes/default/messages/messages_en.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "English",
- "Posts for year %s": "Posts for year %s",
- "Archive": "Archive",
- "Posts about %s": "Posts about %s",
- "Tags": "Tags",
"Also available in": "Also available in",
+ "Archive": "Archive",
+ "LANGUAGE": "English",
"More posts about": "More posts about",
- "Posted": "Posted",
- "Original site": "Original site",
- "Read in English": "Read in English",
"Newer posts": "Newer posts",
+ "Next post": "Next post",
"Older posts": "Older posts",
+ "Original site": "Original site",
+ "Posted": "Posted",
+ "Posts about %s": "Posts about %s",
+ "Posts for year %s": "Posts for year %s",
+ "Posts for {month} {year}": "Posts for {month} {year}",
"Previous post": "Previous post",
- "Next post": "Next post",
- "old posts page %d": "old posts page %d",
+ "Read in English": "Read in English",
"Read more": "Read more",
"Source": "Source",
+ "Tags": "Tags",
+ "old posts page %d": "old posts page %d",
}
diff --git a/nikola/data/themes/default/messages/messages_es.py b/nikola/data/themes/default/messages/messages_es.py
index f17f058..4b73d47 100644
--- a/nikola/data/themes/default/messages/messages_es.py
+++ b/nikola/data/themes/default/messages/messages_es.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Español",
- "Posts for year %s": "Posts del año %s",
- "Archive": "Archivo",
- "Posts about %s": "Posts sobre %s",
- "Tags": "Tags",
"Also available in": "También disponible en",
+ "Archive": "Archivo",
+ "LANGUAGE": "Español",
"More posts about": "Más posts sobre",
- "Posted": "Publicado",
- "Original site": "Sitio original",
- "Read in English": "Leer en español",
- "Older posts": "Posts anteriores",
"Newer posts": "Posts posteriores",
- "Previous post": "Post anterior",
"Next post": "Siguiente post",
- "old posts page %d": "posts antiguos página %d",
+ "Older posts": "Posts anteriores",
+ "Original site": "Sitio original",
+ "Posted": "Publicado",
+ "Posts about %s": "Posts sobre %s",
+ "Posts for year %s": "Posts del año %s",
+ "Posts for {month} {year}": "Posts de {month} {year}",
+ "Previous post": "Post anterior",
+ "Read in English": "Leer en español",
"Read more": "Leer más",
"Source": "Código",
+ "Tags": "Tags",
+ "old posts page %d": "posts antiguos página %d",
}
diff --git a/nikola/data/themes/default/messages/messages_fr.py b/nikola/data/themes/default/messages/messages_fr.py
index 74eecb8..94e9f46 100644
--- a/nikola/data/themes/default/messages/messages_fr.py
+++ b/nikola/data/themes/default/messages/messages_fr.py
@@ -2,20 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Français",
- "Posts for year %s": "Billets de l'année %s",
- "Archive": "Archives",
- "Posts about %s": "Billets sur %s",
- "Tags": "Étiquettes",
"Also available in": "Disponible aussi en",
+ "Archive": "Archives",
+ "LANGUAGE": "Français",
"More posts about": "Plus de billets sur",
- "Posted": "Publié",
- "Original site": "Site d'origine",
- "Read in English": "Lire en français",
"Newer posts": "Billets récents",
+ "Next post": "Billet suivant",
"Older posts": "Anciens billets",
- "Previous post": "Previous post",
- "Next post": "Next post",
- "Read more": "Read more",
+ "Original site": "Site d'origine",
+ "Posted": "Publié",
+ "Posts about %s": "Billets sur %s",
+ "Posts for year %s": "Billets de l'année %s",
+ "Posts for {month} {year}": "Billets de {month} {year}",
+ "Previous post": "Billet précédent",
+ "Read in English": "Lire en français",
+ "Read more": "Lire la suite",
"Source": "Source",
+ "Tags": "Étiquettes",
+ "old posts page %d": "anciens billets page %d",
}
diff --git a/nikola/data/themes/default/messages/messages_it.py b/nikola/data/themes/default/messages/messages_it.py
index 42f4709..c41a20c 100644
--- a/nikola/data/themes/default/messages/messages_it.py
+++ b/nikola/data/themes/default/messages/messages_it.py
@@ -1,23 +1,23 @@
-# vim: set fileencoding=utf-8 :
+# -*- encoding:utf-8 -*-
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Italiano",
- "Posts for year %s": "Articoli per l'anno %s",
- "Archive": "Archivio",
- "Posts about %s": "Articoli su %s",
- "Tags": "Tags",
"Also available in": "Anche disponibile in",
+ "Archive": "Archivio",
+ "LANGUAGE": "Italiano",
"More posts about": "Altri articoli s",
- "Posted": "Pubblicato",
- "Original site": "Sito originale",
- "Read in English": "Leggi in italiano",
"Newer posts": "Articoli recenti",
- "Older posts": "Articoli più vecchi",
+ "Next post": "Articolo successivo",
"Older posts": "Articoli vecchi",
+ "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}": "",
"Previous post": "Articolo precedente",
- "Next post": "Articolo successivo",
- "old posts page %d": "pagina dei vecchi articoli %d",
+ "Read in English": "Leggi in italiano",
"Read more": "Espandi",
"Source": "Source",
+ "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/default/messages/messages_ja.py
new file mode 100644
index 0000000..8dd1521
--- /dev/null
+++ b/nikola/data/themes/default/messages/messages_ja.py
@@ -0,0 +1,23 @@
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "他の言語で読む",
+ "Archive": "過去の記事",
+ "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}": "{year}年{month}月の記事",
+ "Previous post": "前の記事",
+ "Read in English": "日本語で読む",
+ "Read more": "続きを読む",
+ "Source": "ソース",
+ "Tags": "タグ",
+ "old posts page %d": "前の記事 %dページ目",
+}
diff --git a/nikola/data/themes/default/messages/messages_pl.py b/nikola/data/themes/default/messages/messages_pl.py
index 7172ebc..876b5f2 100644
--- a/nikola/data/themes/default/messages/messages_pl.py
+++ b/nikola/data/themes/default/messages/messages_pl.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "polski",
- "Posts for year %s": "Posty z roku %s",
- "Archive": "Archiwum",
- "Posts about %s": "Posty o %s",
- "Tags": "Tags",
"Also available in": "Również dostępny w",
+ "Archive": "Archiwum",
+ "LANGUAGE": "polski",
"More posts about": "Więcej postów o",
- "Posted": "Opublikowany",
- "Original site": "Oryginalna strona",
- "Read in English": "Czytaj po polsku",
- "Older posts": "Starsze posty",
"Newer posts": "Nowsze posty",
- "Previous post": "Poprzedni post",
"Next post": "Następny post",
- "Source": "Źródło",
+ "Older posts": "Starsze posty",
+ "Original site": "Oryginalna strona",
+ "Posted": "Opublikowany",
+ "Posts about %s": "Posty o %s",
+ "Posts for year %s": "Posty z roku %s",
+ "Posts for {month} {year}": "Posty z {month} {year}",
+ "Previous post": "Poprzedni post",
+ "Read in English": "Czytaj po polsku",
"Read more": "Czytaj więcej",
- "old posts page %d": "stare posty, strona %d"
+ "Source": "Źródło",
+ "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/default/messages/messages_pt_br.py
index 183c577..12dafb5 100644
--- a/nikola/data/themes/default/messages/messages_pt-br.py
+++ b/nikola/data/themes/default/messages/messages_pt_br.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Português",
- "Posts for year %s": "Posts do ano %s",
- "Archive": "Arquivo",
- "Posts about %s": "Posts sobre %s",
- "Tags": "Tags",
"Also available in": "Também disponível em",
+ "Archive": "Arquivo",
+ "LANGUAGE": "Português",
"More posts about": "Mais posts sobre",
- "Posted": "Publicado",
- "Original site": "Site original",
- "Read in English": "Ler em português",
"Newer posts": "Posts mais recentes",
+ "Next post": "Próximo post",
"Older posts": "Posts mais antigos",
+ "Original site": "Site original",
+ "Posted": "Publicado",
+ "Posts about %s": "Posts sobre %s",
+ "Posts for year %s": "Posts do ano %s",
+ "Posts for {month} {year}": "Posts de {month} {year}",
"Previous post": "Post anterior",
- "Next post": "Próximo post",
- "old posts page %d": "Posts antigos página %d",
+ "Read in English": "Ler em português",
"Read more": "Leia mais",
"Source": "Código",
+ "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/default/messages/messages_ru.py
index 8a209ec..1d7e41c 100644
--- a/nikola/data/themes/default/messages/messages_ru.py
+++ b/nikola/data/themes/default/messages/messages_ru.py
@@ -2,21 +2,22 @@
from __future__ import unicode_literals
MESSAGES = {
- "LANGUAGE": "Русский",
- "Posts for year %s": "Записи за %s год",
- "Archive": "Архив",
- "Posts about %s": "Записи с тэгом %s:",
- "Tags": "Тэги",
"Also available in": "Также доступно в",
+ "Archive": "Архив",
+ "LANGUAGE": "Русский",
"More posts about": "Больше записей о",
- "Posted": "Опубликовано",
- "Original site": "Оригинальный сайт",
- "Read in English": "Прочесть по-русски",
- "Older posts": "Старые записи",
"Newer posts": "Новые записи",
- "Previous post": "Предыдущая запись",
"Next post": "Следующая запись",
- "old posts page %d": "страница со старыми записями %d",
+ "Older posts": "Старые записи",
+ "Original site": "Оригинальный сайт",
+ "Posted": "Опубликовано",
+ "Posts about %s": "Записи с тэгом %s:",
+ "Posts for year %s": "Записи за %s год",
+ "Posts for {month} {year}": "",
+ "Previous post": "Предыдущая запись",
+ "Read in English": "Прочесть по-русски",
"Read more": "Продолжить чтение",
"Source": "Source",
+ "Tags": "Тэги",
+ "old posts page %d": "страница со старыми записями %d",
}
diff --git a/nikola/data/themes/default/messages/messages_zh-cn.py b/nikola/data/themes/default/messages/messages_zh_cn.py
index 2f4b64e..aa69a96 100644
--- a/nikola/data/themes/default/messages/messages_zh-cn.py
+++ b/nikola/data/themes/default/messages/messages_zh_cn.py
@@ -1,22 +1,23 @@
-# -*- 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": "中文版",
- "Older posts": "旧一篇",
- "Newer posts": "新一篇",
- "Previous post": "前一篇",
- "Next post": "后一篇",
- "old posts page %d": "旧文章页 %d",
- "Read more": "更多",
- "Source": "源代码",
-}
+# -*- encoding:utf-8 -*-
+from __future__ import unicode_literals
+
+MESSAGES = {
+ "Also available in": "其他语言版本",
+ "Archive": "文章存档",
+ "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}": "{year}年{month}月文章",
+ "Previous post": "前一篇",
+ "Read in English": "中文版",
+ "Read more": "更多",
+ "Source": "源代码",
+ "Tags": "标签",
+ "old posts page %d": "旧文章页 %d",
+}
diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl
index c0935a2..72a4077 100644
--- a/nikola/data/themes/default/templates/base.tmpl
+++ b/nikola/data/themes/default/templates/base.tmpl
@@ -1,11 +1,13 @@
## -*- 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:
@@ -22,7 +24,7 @@
<%block name="belowtitle">
%if len(translations) > 1:
<small>
- ${(messages[lang][u"Also available in"])}:&nbsp;
+ ${(messages("Also available in"))}:&nbsp;
${html_translations()}
</small>
%endif
@@ -52,7 +54,7 @@
</div>
</div>
</div>
- ${analytics}
${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/base_helper.tmpl b/nikola/data/themes/default/templates/base_helper.tmpl
index eb22905..a833c51 100644
--- a/nikola/data/themes/default/templates/base_helper.tmpl
+++ b/nikola/data/themes/default/templates/base_helper.tmpl
@@ -22,7 +22,6 @@
<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"/>
%if has_custom_css:
<link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
@@ -34,9 +33,13 @@
%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
+ %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:
@@ -63,7 +66,6 @@
<script src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
%endif
<script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
- <script src="/assets/js/slides.min.jquery.js" type="text/javascript"></script>
%endif
</%def>
@@ -98,7 +100,7 @@
<%def name="html_translations()">
%for langname in translations.keys():
%if langname != lang:
- <a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a>
+ <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
%endif
%endfor
</%def>
diff --git a/nikola/data/themes/default/templates/disqus_helper.tmpl b/nikola/data/themes/default/templates/disqus_helper.tmpl
index 674e20e..4c60f85 100644
--- a/nikola/data/themes/default/templates/disqus_helper.tmpl
+++ b/nikola/data/themes/default/templates/disqus_helper.tmpl
@@ -1,6 +1,9 @@
## -*- coding: utf-8 -*-
<%!
import json
+ translations = {
+ 'es': 'es_ES',
+ }
%>
<%def name="html_disqus(url, title, identifier)">
%if disqus_forum:
@@ -12,8 +15,8 @@
%endif
var disqus_title=${json.dumps(title)};
var disqus_identifier="${identifier}";
- var disqus_config = function () {
- this.language = "${lang}";
+ var disqus_config = function () {
+ this.language = "${translations.get(lang, lang)}";
};
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl
index 4f66867..b49e764 100644
--- a/nikola/data/themes/default/templates/index.tmpl
+++ b/nikola/data/themes/default/templates/index.tmpl
@@ -5,13 +5,15 @@
<%block name="content">
% for post in posts:
<div class="postbox">
- <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a>
+ <h1><a href="${post.permalink()}">${post.title()}</a>
<small>&nbsp;&nbsp;
- ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time>
+ ${messages("Posted")}: <time class="published" datetime="${post.date.isoformat()}">${post.formatted_date(date_format)}</time>
</small></h1>
<hr>
- ${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()}
diff --git a/nikola/data/themes/default/templates/index_helper.tmpl b/nikola/data/themes/default/templates/index_helper.tmpl
index 151b4d2..7859972 100644
--- a/nikola/data/themes/default/templates/index_helper.tmpl
+++ b/nikola/data/themes/default/templates/index_helper.tmpl
@@ -4,11 +4,11 @@
<ul class="pager">
%if prevlink:
<li class="previous">
- <a href="${prevlink}">&larr; ${messages[lang]["Newer posts"]}</a>
+ <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
%endif
%if nextlink:
<li class="next">
- <a href="${nextlink}">${messages[lang]["Older posts"]} &rarr;</a>
+ <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
%endif
</ul>
</div>
diff --git a/nikola/data/themes/default/templates/list_post.tmpl b/nikola/data/themes/default/templates/list_post.tmpl
index 1a1cdee..f0e159d 100644
--- a/nikola/data/themes/default/templates/list_post.tmpl
+++ b/nikola/data/themes/default/templates/list_post.tmpl
@@ -6,7 +6,7 @@
<h1>${title}</h1>
<ul class="unstyled">
% for post in posts:
- <li><a href="${post.permalink(lang)}">[${post.date.strftime(date_format)}] ${post.title(lang)}</a>
+ <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a>
% endfor
</ul>
</div>
diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl
index 22d8a58..f9e24d2 100644
--- a/nikola/data/themes/default/templates/post.tmpl
+++ b/nikola/data/themes/default/templates/post.tmpl
@@ -10,20 +10,24 @@ ${helper.twitter_card_information(post)}
${helper.html_title()}
<hr>
<small>
- ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time>
+ ${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(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
${helper.mathjax_script(post)}
</div>
</%block>
<%block name="sourcelink">
+% if not post.meta('password'):
<li>
- <a href="${post.pagenames[lang]+post.source_ext()}" id="sourcelink">${messages[lang]["Source"]}</a>
+ <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
index 911a831..cce0ecf 100644
--- a/nikola/data/themes/default/templates/post_helper.tmpl
+++ b/nikola/data/themes/default/templates/post_helper.tmpl
@@ -2,7 +2,7 @@
<%def name="html_title()">
<h1>${title}</h1>
% if link:
- <p><a href='${link}'>${messages[lang]["Original site"]}</a></p>
+ <p><a href='${link}'>${messages("Original site")}</a></p>
% endif
</%def>
@@ -12,7 +12,7 @@
%for langname in translations.keys():
%if langname != lang and post.is_translation_available(langname):
&nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.permalink(langname)}">${messages[langname]["Read in English"]}</a>
+ <a href="${post.permalink(langname)}">${messages("Read in English", langname)}</a>
%endif
%endfor
%endif
@@ -21,9 +21,9 @@
<%def name="html_tags(post)">
%if post.tags:
- &nbsp;&nbsp;|&nbsp;&nbsp;${messages[lang]["More posts about"]}
+ &nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
%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 class="badge badge-info">${tag}</span></a>
%endfor
%endif
</%def>
@@ -32,19 +32,21 @@
<ul class="pager">
%if post.prev_post:
<li class="previous">
- <a href="${post.prev_post.permalink(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
+ <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(lang)}">${messages[lang]["Next post"]} &rarr;</a>
+ <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')}">
- <meta name="og:url" content="${post.permalink(lang, absolute=True)}">
+ <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:
@@ -55,11 +57,11 @@
%elif 'creator' in twitter_card:
<meta name="twitter:creator" content="${twitter_card['creator']}">
%endif
- <meta name="og:title" content="${post.title(lang)[:70]}">
- %if post.description(lang):
- <meta name="og:description" content="${post.description(lang)[:200]}">
+ <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(lang, strip_html=True)[:200]}">
+ <meta name="og:description" content="${post.text(strip_html=True)[:200]|h}">
%endif
%endif
</%def>
diff --git a/nikola/data/themes/default/templates/story.tmpl b/nikola/data/themes/default/templates/story.tmpl
index d5c2f44..c1c06d8 100644
--- a/nikola/data/themes/default/templates/story.tmpl
+++ b/nikola/data/themes/default/templates/story.tmpl
@@ -1,12 +1,16 @@
## -*- 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(lang)}
-%if enable_comments:
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
+ ${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/tag.tmpl b/nikola/data/themes/default/templates/tag.tmpl
index 7c89ad1..7fb43c0 100644
--- a/nikola/data/themes/default/templates/tag.tmpl
+++ b/nikola/data/themes/default/templates/tag.tmpl
@@ -1,7 +1,32 @@
## -*- coding: utf-8 -*-
<%inherit file="list_post.tmpl"/>
<%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, lang)}">
+ <link rel="alternate" type="application/rss+xml" type="application/rss+xml" title="RSS for tag ${tag} (${language})" href="${_link("tag_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)}">
+ %endif
+</%block>
+
+<%block name="content">
+ <!--Body content-->
+ <div class="postbox">
+ <h1>${title}</h1>
+ %if len(translations) > 1:
+ %for language in translations:
+ <a href="${_link("tag_rss", tag, language)}">RSS (${language})</a>&nbsp;
+ %endfor
+ %else:
+ <a href="${_link("tag_rss", tag)}">RSS</a>
+ %endif
+ <br>
+ <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/default/templates/tags.tmpl b/nikola/data/themes/default/templates/tags.tmpl
index 369a3d5..5727dc5 100644
--- a/nikola/data/themes/default/templates/tags.tmpl
+++ b/nikola/data/themes/default/templates/tags.tmpl
@@ -1,14 +1,12 @@
## -*- 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>
+ <!--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/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl
index 97cddff..c104b20 100644
--- a/nikola/data/themes/jinja-default/templates/base.tmpl
+++ b/nikola/data/themes/jinja-default/templates/base.tmpl
@@ -1,4 +1,5 @@
<!DOCTYPE html>
+{{set_locale(lang)}}
<html lang="{{lang}}">
<head>
<meta charset="utf-8">
@@ -23,7 +24,6 @@
<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"/>
{% if has_custom_css %}
<link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
@@ -42,6 +42,7 @@
{% endif %}
{% block extra_head %}
{% endblock %}
+ {{extra_head_data}}
</head>
<body>
{% if add_this_buttons %}
@@ -58,10 +59,10 @@
{% block belowtitle%}
{% if translations|length > 1 %}
<small>
- {{ messages[lang]["Also available in"] }}:&nbsp;
+ {{ messages("Also available in") }}:&nbsp;
{% for langname in translations.keys() %}
{% if langname != lang %}
- <a href="{{_link("index", None, langname)}}">{{messages[langname]["LANGUAGE"]}}</a>
+ <a href="{{_link("index", None, langname)}}">{{messages("LANGUAGE", langname)}}</a>
{% endif %}
{% endfor %}
</small>
@@ -106,7 +107,6 @@
</div>
</div>
</div>
- {{analytics}}
<!-- late load javascript -->
{% if use_bundles %}
{% if use_cdn %}
@@ -125,7 +125,7 @@
<script src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
{% endif %}
<script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
- <script src="/assets/js/slides.min.jquery.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/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl
index ab0392c..7d1aa00 100644
--- a/nikola/data/themes/jinja-default/templates/index.tmpl
+++ b/nikola/data/themes/jinja-default/templates/index.tmpl
@@ -2,14 +2,14 @@
{% block content %}
{% for post in posts %}
<div class="postbox">
- <h1><a href="{{post.permalink(lang)}}">{{post.title(lang)}}</a>
+ <h1><a href="{{post.permalink()}}">{{post.title()}}</a>
<small>&nbsp;&nbsp;
- {{messages[lang]["Posted"]}}: {{post.date.strftime(date_format)}}
+ {{messages("Posted")}}: {{post.formatted_date(date_format)}}
</small></h1>
<hr>
- {{post.text(lang, index_teasers)}}
+ {{post.text(teaser_only=index_teasers)}}
<p>
- {% if disqus_forum %}
+ {% if disqus_forum and not post.meta('nocomments')%}
<a href="{{post.permalink()}}#disqus_thread" data-disqus-identifier="{{post.base_path}}">Comments</a>
{% endif %}
</div>
@@ -18,11 +18,11 @@
<ul class="pager">
{%if prevlink %}
<li class="previous">
- <a href="{{prevlink}}">&larr; {{messages[lang]["Newer posts"]}}</a>
+ <a href="{{prevlink}}">&larr; {{messages("Newer posts")}}</a>
{% endif %}
{% if nextlink %}
<li class="next">
- <a href="{{nextlink}}">{{messages[lang]["Older posts"]}} &rarr;</a>
+ <a href="{{nextlink}}">{{messages("Older posts")}} &rarr;</a>
{% endif %}
</ul>
diff --git a/nikola/data/themes/jinja-default/templates/list_post.tmpl b/nikola/data/themes/jinja-default/templates/list_post.tmpl
index 7723214..b4ac59e 100644
--- a/nikola/data/themes/jinja-default/templates/list_post.tmpl
+++ b/nikola/data/themes/jinja-default/templates/list_post.tmpl
@@ -5,7 +5,7 @@
<h1>{{title}}</h1>
<ul class="unstyled">
{% for post in posts %}
- <li><a href="{{post.permalink(lang)}}">[{{post.date.strftime(date_format)}}] {{post.title(lang)}}</a>
+ <li><a href="{{post.permalink()}}">[{{post.formatted_date(date_format)}}] {{post.title()}}</a>
{% endfor %}
</ul>
</div>
diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl
index d14e973..ab96682 100644
--- a/nikola/data/themes/jinja-default/templates/post.tmpl
+++ b/nikola/data/themes/jinja-default/templates/post.tmpl
@@ -3,49 +3,50 @@
<div class="postbox">
<h1><a href='{{permalink}}'>{{title}}</a></h1>
{% if link %}
- <p><a href='{{link}}'>{{messages[lang]["Original site"]}}</a></p>
+ <p><a href='{{link}}'>{{messages("Original site")}}</a></p>
{% endif %}
<hr>
<small>
- {{messages[lang]["Posted"]}}: {{post.date.strftime(date_format)}}&nbsp;&nbsp;|&nbsp;&nbsp;
+ {{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[langname]["Read in English"]}}</a>
+ <a href="{{post.permalink(langname)}}">{{messages("Read in English", langname)}}</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
{% endif %}
{% endfor %}
{% endif %}
-
- <a href="{{post.pagenames[lang]+".txt"}}" id="sourcelink">{{messages[lang]["Source"]}}</a>
+ {% 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[lang]["More posts about"]}}
+ &nbsp;&nbsp;|&nbsp;&nbsp;{{messages("More posts about")}}
{% for tag in post.tags %}
- <a href="{{_link("tag", tag, lang)}}"><span class="badge badge-info">{{tag}}</span></a>
+ <a href="{{_link("tag", tag)}}"><span class="badge badge-info">{{tag}}</span></a>
{% endfor %}
{% endif %}
</small>
<hr>
- {{post.text(lang)}}
+ {{post.text()}}
<ul class="pager">
{%if post.prev_post %}
<li class="previous">
- <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">&larr; {{messages[lang]["Previous post"]}}</a>
+ <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(lang))}}">{{messages[lang]["Next post"]}} &rarr;</a>
+ <a href="{{rel_link(permalink, post.next_post.permalink())}}">{{messages("Next post")}} &rarr;</a>
{% endif %}
</ul>
- {% if disqus_forum %}
+ {% 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(lang)|tojson }};
+ var disqus_title={{post.title()|tojson }};
var disqus_identifier="{{post.base_path}}";
- var disqus_config = function () {
+ var disqus_config = function () {
this.language = "{{lang}}";
};
(function() {
diff --git a/nikola/data/themes/jinja-default/templates/story.tmpl b/nikola/data/themes/jinja-default/templates/story.tmpl
index ccaac91..a4ad375 100644
--- a/nikola/data/themes/jinja-default/templates/story.tmpl
+++ b/nikola/data/themes/jinja-default/templates/story.tmpl
@@ -3,8 +3,23 @@
{% if title %}
<h1>{{title}}</h1>
{% endif %}
- {{post.text(lang)}}
-{%if enable_comments %}
- {{disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}}
+ {{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
index 42720fd..77db27d 100644
--- a/nikola/data/themes/jinja-default/templates/tag.tmpl
+++ b/nikola/data/themes/jinja-default/templates/tag.tmpl
@@ -1,6 +1,6 @@
{% 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, lang)}}">
+ <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
index 3eae88d..0fa9d0f 100644
--- a/nikola/data/themes/jinja-default/templates/tags.tmpl
+++ b/nikola/data/themes/jinja-default/templates/tags.tmpl
@@ -3,9 +3,9 @@
<div class="postbox">
<!--Body content-->
<h1>{{title}}</h1>
- <ul class="unstyled">
+ <ul class="unstyled bricks">
{% for text, link in items %}
- <li><a href="{{link}}"><span class="badge badge-info">{{text}}</span></a>
+ <li><a href="{{link}}">{{text}}</a></li>
{% endfor %}
</ul>
<!--End of body content-->
diff --git a/nikola/data/themes/monospace/assets/css/code.css b/nikola/data/themes/monospace/assets/css/code.css
deleted file mode 100644
index b1d7ace..0000000
--- a/nikola/data/themes/monospace/assets/css/code.css
+++ /dev/null
@@ -1,62 +0,0 @@
-pre { word-break: pre; white-space: pre; word-wrap: pre; overflow: auto; max-width: 100%;}
-td.linenos { vertical-align: top; width: 4em;}
-div.code > pre, .code
-{ background: #f8f8f8; white-space: pre;}
-.code .c { color: #008800; font-style: italic } /* Comment */
-.code .err { border: 1px solid #FF0000 } /* Error */
-.code .k { color: #AA22FF; font-weight: bold } /* Keyword */
-.code .o { color: #666666 } /* Operator */
-.code .cm { color: #008800; font-style: italic } /* Comment.Multiline */
-.code .cp { color: #008800 } /* Comment.Preproc */
-.code .c1 { color: #008800; font-style: italic } /* Comment.Single */
-.code .cs { color: #008800; font-weight: bold } /* Comment.Special */
-.code .gd { color: #A00000 } /* Generic.Deleted */
-.code .ge { font-style: italic } /* Generic.Emph */
-.code .gr { color: #FF0000 } /* Generic.Error */
-.code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.code .gi { color: #00A000 } /* Generic.Inserted */
-.code .go { color: #808080 } /* Generic.Output */
-.code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
-.code .gs { font-weight: bold } /* Generic.Strong */
-.code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.code .gt { color: #0040D0 } /* Generic.Traceback */
-.code .kc { color: #AA22FF; font-weight: bold } /* Keyword.Constant */
-.code .kd { color: #AA22FF; font-weight: bold } /* Keyword.Declaration */
-.code .kp { color: #AA22FF } /* Keyword.Pseudo */
-.code .kr { color: #AA22FF; font-weight: bold } /* Keyword.Reserved */
-.code .kt { color: #AA22FF; font-weight: bold } /* Keyword.Type */
-.code .m { color: #666666 } /* Literal.Number */
-.code .s { color: #BB4444 } /* Literal.String */
-.code .na { color: #BB4444 } /* Name.Attribute */
-.code .nb { color: #AA22FF } /* Name.Builtin */
-.code .nc { color: #0000FF } /* Name.Class */
-.code .no { color: #880000 } /* Name.Constant */
-.code .nd { color: #AA22FF } /* Name.Decorator */
-.code .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
-.code .nf { color: #00A000 } /* Name.Function */
-.code .nl { color: #A0A000 } /* Name.Label */
-.code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
-.code .nt { color: #008000; font-weight: bold } /* Name.Tag */
-.code .nv { color: #B8860B } /* Name.Variable */
-.code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
-.code .mf { color: #666666 } /* Literal.Number.Float */
-.code .mh { color: #666666 } /* Literal.Number.Hex */
-.code .mi { color: #666666 } /* Literal.Number.Integer */
-.code .mo { color: #666666 } /* Literal.Number.Oct */
-.code .sb { color: #BB4444 } /* Literal.String.Backtick */
-.code .sc { color: #BB4444 } /* Literal.String.Char */
-.code .sd { color: #BB4444; font-style: italic } /* Literal.String.Doc */
-.code .s2 { color: #BB4444 } /* Literal.String.Double */
-.code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
-.code .sh { color: #BB4444 } /* Literal.String.Heredoc */
-.code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
-.code .sx { color: #008000 } /* Literal.String.Other */
-.code .sr { color: #BB6688 } /* Literal.String.Regex */
-.code .s1 { color: #BB4444 } /* Literal.String.Single */
-.code .ss { color: #B8860B } /* Literal.String.Symbol */
-.code .bp { color: #AA22FF } /* Name.Builtin.Pseudo */
-.code .vc { color: #B8860B } /* Name.Variable.Class */
-.code .vg { color: #B8860B } /* Name.Variable.Global */
-.code .vi { color: #B8860B } /* Name.Variable.Instance */
-.code .il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/nikola/data/themes/monospace/bundles b/nikola/data/themes/monospace/bundles
index aa35d9c..4760181 100644
--- a/nikola/data/themes/monospace/bundles
+++ b/nikola/data/themes/monospace/bundles
@@ -1 +1,2 @@
assets/css/all.css=rst.css,code.css,theme.css
+assets/css/all-nocdn.css=rst.css,code.css,theme.css
diff --git a/nikola/data/themes/monospace/templates/base.tmpl b/nikola/data/themes/monospace/templates/base.tmpl
index 9eecbd4..806795d 100644
--- a/nikola/data/themes/monospace/templates/base.tmpl
+++ b/nikola/data/themes/monospace/templates/base.tmpl
@@ -1,11 +1,13 @@
## -*- 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:
@@ -23,7 +25,7 @@
<%block name="belowtitle">
%if len(translations) > 1:
<small>
- ${(messages[lang][u"Also available in"])}:&nbsp;
+ ${(messages("Also available in"))}:&nbsp;
${html_translations()}
</small>
%endif
@@ -38,6 +40,6 @@
<div id="footer">
${content_footer}
</div>
- </div>
+ </div>
${analytics}
</body>
diff --git a/nikola/data/themes/monospace/templates/base_helper.tmpl b/nikola/data/themes/monospace/templates/base_helper.tmpl
index aba8dff..4f3e45b 100644
--- a/nikola/data/themes/monospace/templates/base_helper.tmpl
+++ b/nikola/data/themes/monospace/templates/base_helper.tmpl
@@ -4,27 +4,29 @@
<meta name="description" content="${description}" >
<meta name="author" content="${blog_author}">
<title>${title} | ${blog_title}</title>
- <!-- Le styles -->
+ ${mathjax_config}
%if use_bundles:
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/all.js" type="text/javascript"></script>
+ %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:
- <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css">
+ %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/slides.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
- <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></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 src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
%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]-->
@@ -32,7 +34,7 @@
${rss_link}
%else:
%for language in translations:
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, lang)}">
+ <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
%endfor
%endif
%if favicons:
@@ -48,10 +50,10 @@
<!-- 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>
- <li><a class="addthis_button_google_plusone_share"></a></li>
- <li><a class="addthis_button_linkedin"></a></li>
- <li><a class="addthis_button_twitter"></a></li>
+ <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>
@@ -74,7 +76,7 @@
<%def name="html_translations()">
%for langname in translations.keys():
%if langname != lang:
- <a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a>
+ <a href="${_link("index", None, langname)}">${messages("LANGUAGE", langname)}</a>
%endif
%endfor
</%def>
diff --git a/nikola/data/themes/monospace/templates/disqus_helper.tmpl b/nikola/data/themes/monospace/templates/disqus_helper.tmpl
index 674e20e..4c60f85 100644
--- a/nikola/data/themes/monospace/templates/disqus_helper.tmpl
+++ b/nikola/data/themes/monospace/templates/disqus_helper.tmpl
@@ -1,6 +1,9 @@
## -*- coding: utf-8 -*-
<%!
import json
+ translations = {
+ 'es': 'es_ES',
+ }
%>
<%def name="html_disqus(url, title, identifier)">
%if disqus_forum:
@@ -12,8 +15,8 @@
%endif
var disqus_title=${json.dumps(title)};
var disqus_identifier="${identifier}";
- var disqus_config = function () {
- this.language = "${lang}";
+ var disqus_config = function () {
+ this.language = "${translations.get(lang, lang)}";
};
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
diff --git a/nikola/data/themes/monospace/templates/index.tmpl b/nikola/data/themes/monospace/templates/index.tmpl
index ee57d26..4a0c630 100644
--- a/nikola/data/themes/monospace/templates/index.tmpl
+++ b/nikola/data/themes/monospace/templates/index.tmpl
@@ -5,22 +5,24 @@
<%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 class="badge badge-info">${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()}
diff --git a/nikola/data/themes/monospace/templates/index_helper.tmpl b/nikola/data/themes/monospace/templates/index_helper.tmpl
index 114a730..1bb700c 100644
--- a/nikola/data/themes/monospace/templates/index_helper.tmpl
+++ b/nikola/data/themes/monospace/templates/index_helper.tmpl
@@ -4,12 +4,12 @@
<ul class="pager">
%if prevlink:
<li class="previous">
- <a href="${prevlink}">&larr; ${messages[lang]["Newer posts"]}</a>
+ <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
</li>
%endif
%if nextlink:
<li class="next">
- <a href="${nextlink}">${messages[lang]["Older posts"]} &rarr;</a>
+ <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
</li>
%endif
</ul>
diff --git a/nikola/data/themes/monospace/templates/list_post.tmpl b/nikola/data/themes/monospace/templates/list_post.tmpl
index 1a1cdee..f0e159d 100644
--- a/nikola/data/themes/monospace/templates/list_post.tmpl
+++ b/nikola/data/themes/monospace/templates/list_post.tmpl
@@ -6,7 +6,7 @@
<h1>${title}</h1>
<ul class="unstyled">
% for post in posts:
- <li><a href="${post.permalink(lang)}">[${post.date.strftime(date_format)}] ${post.title(lang)}</a>
+ <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a>
% endfor
</ul>
</div>
diff --git a/nikola/data/themes/monospace/templates/post.tmpl b/nikola/data/themes/monospace/templates/post.tmpl
index 2ba27f1..0ec360d 100644
--- a/nikola/data/themes/monospace/templates/post.tmpl
+++ b/nikola/data/themes/monospace/templates/post.tmpl
@@ -7,13 +7,16 @@
${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'}" id="sourcelink">${messages[lang]["Source"]}</a>]
+ ${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[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 class="badge badge-info">${tag}</span></a>
%endfor
</span>
<br>
@@ -22,8 +25,10 @@
${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>
diff --git a/nikola/data/themes/monospace/templates/post_helper.tmpl b/nikola/data/themes/monospace/templates/post_helper.tmpl
index 8651c65..cce0ecf 100644
--- a/nikola/data/themes/monospace/templates/post_helper.tmpl
+++ b/nikola/data/themes/monospace/templates/post_helper.tmpl
@@ -2,7 +2,7 @@
<%def name="html_title()">
<h1>${title}</h1>
% if link:
- <p><a href='${link}'>${messages[lang]["Original site"]}</a></p>
+ <p><a href='${link}'>${messages("Original site")}</a></p>
% endif
</%def>
@@ -12,7 +12,7 @@
%for langname in translations.keys():
%if langname != lang and post.is_translation_available(langname):
&nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.permalink(langname)}">${messages[langname]["Read in English"]}</a>
+ <a href="${post.permalink(langname)}">${messages("Read in English", langname)}</a>
%endif
%endfor
%endif
@@ -21,25 +21,53 @@
<%def name="html_tags(post)">
%if post.tags:
- &nbsp;&nbsp;|&nbsp;&nbsp;${messages[lang]["More posts about"]}
+ &nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
%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 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(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
+ <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(lang)}">${messages[lang]["Next post"]} &rarr;</a>
+ <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
index 30d263b..21d0e2f 100644
--- a/nikola/data/themes/monospace/templates/story.tmpl
+++ b/nikola/data/themes/monospace/templates/story.tmpl
@@ -1,11 +1,15 @@
## -*- 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(lang)}
-%if enable_comments:
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
+ ${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
index 7c89ad1..97aafeb 100644
--- a/nikola/data/themes/monospace/templates/tag.tmpl
+++ b/nikola/data/themes/monospace/templates/tag.tmpl
@@ -2,6 +2,6 @@
<%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, lang)}">
+ <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/assets/css/code.css b/nikola/data/themes/orphan/assets/css/code.css
deleted file mode 120000
index 6b2b872..0000000
--- a/nikola/data/themes/orphan/assets/css/code.css
+++ /dev/null
@@ -1 +0,0 @@
-../../../default/assets/css/code.css \ No newline at end of file
diff --git a/nikola/data/themes/orphan/templates/base.tmpl b/nikola/data/themes/orphan/templates/base.tmpl
index 39e2b9d..2a62b58 100644
--- a/nikola/data/themes/orphan/templates/base.tmpl
+++ b/nikola/data/themes/orphan/templates/base.tmpl
@@ -1,11 +1,13 @@
## -*- 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:
@@ -17,7 +19,7 @@
<%block name="belowtitle">
%if len(translations) > 1:
<small>
- ${(messages[lang][u"Also available in"])}:&nbsp;
+ ${(messages("Also available in"))}:&nbsp;
${html_translations()}
</small>
%endif
diff --git a/nikola/data/themes/orphan/templates/base_helper.tmpl b/nikola/data/themes/orphan/templates/base_helper.tmpl
index aba8dff..4f3e45b 100644
--- a/nikola/data/themes/orphan/templates/base_helper.tmpl
+++ b/nikola/data/themes/orphan/templates/base_helper.tmpl
@@ -4,27 +4,29 @@
<meta name="description" content="${description}" >
<meta name="author" content="${blog_author}">
<title>${title} | ${blog_title}</title>
- <!-- Le styles -->
+ ${mathjax_config}
%if use_bundles:
- <link href="/assets/css/all.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/all.js" type="text/javascript"></script>
+ %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:
- <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css">
+ %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/slides.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
- <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css">
- <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></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 src="/assets/js/bootstrap.min.js" type="text/javascript"></script>
%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]-->
@@ -32,7 +34,7 @@
${rss_link}
%else:
%for language in translations:
- <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, lang)}">
+ <link rel="alternate" type="application/rss+xml" title="RSS (${language})" href="${_link('rss', None, language)}">
%endfor
%endif
%if favicons:
@@ -48,10 +50,10 @@
<!-- 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>
- <li><a class="addthis_button_google_plusone_share"></a></li>
- <li><a class="addthis_button_linkedin"></a></li>
- <li><a class="addthis_button_twitter"></a></li>
+ <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>
@@ -74,7 +76,7 @@
<%def name="html_translations()">
%for langname in translations.keys():
%if langname != lang:
- <a href="${_link("index", None, langname)}">${messages[langname]["LANGUAGE"]}</a>
+ <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
index 674e20e..4c60f85 100644
--- a/nikola/data/themes/orphan/templates/disqus_helper.tmpl
+++ b/nikola/data/themes/orphan/templates/disqus_helper.tmpl
@@ -1,6 +1,9 @@
## -*- coding: utf-8 -*-
<%!
import json
+ translations = {
+ 'es': 'es_ES',
+ }
%>
<%def name="html_disqus(url, title, identifier)">
%if disqus_forum:
@@ -12,8 +15,8 @@
%endif
var disqus_title=${json.dumps(title)};
var disqus_identifier="${identifier}";
- var disqus_config = function () {
- this.language = "${lang}";
+ var disqus_config = function () {
+ this.language = "${translations.get(lang, lang)}";
};
(function() {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
diff --git a/nikola/data/themes/orphan/templates/index.tmpl b/nikola/data/themes/orphan/templates/index.tmpl
index 1a436e2..59d391a 100644
--- a/nikola/data/themes/orphan/templates/index.tmpl
+++ b/nikola/data/themes/orphan/templates/index.tmpl
@@ -5,13 +5,15 @@
<%block name="content">
% for post in posts:
<div class="postbox">
- <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a>
+ <h1><a href="${post.permalink()}">${post.title()}</a>
<small>&nbsp;&nbsp;
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
+ ${messages("Posted")}: ${post.formatted_date(date_format)}
</small></h1>
<hr>
- ${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()}
diff --git a/nikola/data/themes/orphan/templates/index_helper.tmpl b/nikola/data/themes/orphan/templates/index_helper.tmpl
index 114a730..1bb700c 100644
--- a/nikola/data/themes/orphan/templates/index_helper.tmpl
+++ b/nikola/data/themes/orphan/templates/index_helper.tmpl
@@ -4,12 +4,12 @@
<ul class="pager">
%if prevlink:
<li class="previous">
- <a href="${prevlink}">&larr; ${messages[lang]["Newer posts"]}</a>
+ <a href="${prevlink}">&larr; ${messages("Newer posts")}</a>
</li>
%endif
%if nextlink:
<li class="next">
- <a href="${nextlink}">${messages[lang]["Older posts"]} &rarr;</a>
+ <a href="${nextlink}">${messages("Older posts")} &rarr;</a>
</li>
%endif
</ul>
diff --git a/nikola/data/themes/orphan/templates/list_post.tmpl b/nikola/data/themes/orphan/templates/list_post.tmpl
index 1a1cdee..f0e159d 100644
--- a/nikola/data/themes/orphan/templates/list_post.tmpl
+++ b/nikola/data/themes/orphan/templates/list_post.tmpl
@@ -6,7 +6,7 @@
<h1>${title}</h1>
<ul class="unstyled">
% for post in posts:
- <li><a href="${post.permalink(lang)}">[${post.date.strftime(date_format)}] ${post.title(lang)}</a>
+ <li><a href="${post.permalink()}">[${post.formatted_date(date_format)}] ${post.title()}</a>
% endfor
</ul>
</div>
diff --git a/nikola/data/themes/orphan/templates/post.tmpl b/nikola/data/themes/orphan/templates/post.tmpl
index 672d4f6..6f6529d 100644
--- a/nikola/data/themes/orphan/templates/post.tmpl
+++ b/nikola/data/themes/orphan/templates/post.tmpl
@@ -7,15 +7,19 @@
${helper.html_title()}
<hr>
<small>
- ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)}
+ ${messages("Posted")}: ${post.formatted_date(date_format)}
${helper.html_translations(post)}
&nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.pagenames[lang]+'.txt'}" id="sourcelink">${messages[lang]["Source"]}</a>
+ % 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(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>
diff --git a/nikola/data/themes/orphan/templates/post_helper.tmpl b/nikola/data/themes/orphan/templates/post_helper.tmpl
index a3dc75f..cce0ecf 100644
--- a/nikola/data/themes/orphan/templates/post_helper.tmpl
+++ b/nikola/data/themes/orphan/templates/post_helper.tmpl
@@ -2,7 +2,7 @@
<%def name="html_title()">
<h1>${title}</h1>
% if link:
- <p><a href='${link}'>${messages[lang]["Original site"]}</a></p>
+ <p><a href='${link}'>${messages("Original site")}</a></p>
% endif
</%def>
@@ -10,9 +10,9 @@
<%def name="html_translations(post)">
%if len(translations) > 1:
%for langname in translations.keys():
- %if langname != lang and post.is_translation_available(langname):
+ %if langname != lang and post.is_translation_available(langname):
&nbsp;&nbsp;|&nbsp;&nbsp;
- <a href="${post.permalink(langname)}">${messages[langname]["Read in English"]}</a>
+ <a href="${post.permalink(langname)}">${messages("Read in English", langname)}</a>
%endif
%endfor
%endif
@@ -21,25 +21,53 @@
<%def name="html_tags(post)">
%if post.tags:
- &nbsp;&nbsp;|&nbsp;&nbsp;${messages[lang]["More posts about"]}
+ &nbsp;&nbsp;|&nbsp;&nbsp;${messages("More posts about")}
%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 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(lang)}">&larr; ${messages[lang]["Previous post"]}</a>
+ <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(lang)}">${messages[lang]["Next post"]} &rarr;</a>
+ <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/orphan/templates/story.tmpl b/nikola/data/themes/orphan/templates/story.tmpl
index 30d263b..21d0e2f 100644
--- a/nikola/data/themes/orphan/templates/story.tmpl
+++ b/nikola/data/themes/orphan/templates/story.tmpl
@@ -1,11 +1,15 @@
## -*- 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(lang)}
-%if enable_comments:
- ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)}
+ ${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/orphan/templates/tag.tmpl b/nikola/data/themes/orphan/templates/tag.tmpl
index 7c89ad1..97aafeb 100644
--- a/nikola/data/themes/orphan/templates/tag.tmpl
+++ b/nikola/data/themes/orphan/templates/tag.tmpl
@@ -2,6 +2,6 @@
<%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, lang)}">
+ <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/site-planetoid/README b/nikola/data/themes/site-planetoid/README
new file mode 100644
index 0000000..c148591
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/README
@@ -0,0 +1 @@
+A version of the site theme for the use with the "planetoid" plugin.
diff --git a/nikola/data/themes/site-planetoid/engine b/nikola/data/themes/site-planetoid/engine
new file mode 100644
index 0000000..2951cdd
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/engine
@@ -0,0 +1 @@
+mako
diff --git a/nikola/data/themes/site-planetoid/parent b/nikola/data/themes/site-planetoid/parent
new file mode 100644
index 0000000..1320f90
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/parent
@@ -0,0 +1 @@
+site
diff --git a/nikola/data/themes/site-planetoid/templates/index.tmpl b/nikola/data/themes/site-planetoid/templates/index.tmpl
new file mode 100644
index 0000000..29243e0
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/templates/index.tmpl
@@ -0,0 +1,16 @@
+## -*- 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
new file mode 100644
index 0000000..d60de78
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/templates/post.tmpl
@@ -0,0 +1,9 @@
+## -*- 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
new file mode 100644
index 0000000..7712e71
--- /dev/null
+++ b/nikola/data/themes/site-planetoid/templates/story.tmpl
@@ -0,0 +1,25 @@
+## -*- 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/assets/css/theme.css b/nikola/data/themes/site/assets/css/theme.css
index aa0ee4a..24072ac 100644
--- a/nikola/data/themes/site/assets/css/theme.css
+++ b/nikola/data/themes/site/assets/css/theme.css
@@ -64,3 +64,17 @@ blockquote p, blockquote {
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;
+}
+
+h1, h2, h3, h4, h5, h6, h7 {
+ margin-top: -40px;
+ padding-top: 40px;
+}
diff --git a/nikola/data/themes/site/templates/base.tmpl b/nikola/data/themes/site/templates/base.tmpl
index 416d04b..4efd0ad 100644
--- a/nikola/data/themes/site/templates/base.tmpl
+++ b/nikola/data/themes/site/templates/base.tmpl
@@ -1,5 +1,6 @@
## -*- coding: utf-8 -*-
<%namespace file="base_helper.tmpl" import="*"/>
+${set_locale(lang)}
<!DOCTYPE html>
<html lang="${lang}">
<head>
@@ -7,20 +8,21 @@
${html_head()}
<%block name="extra_head">
</%block>
+ ${extra_head_data}
</head>
<body>
<!-- Menubar -->
<div class="navbar navbar-fixed-top">
<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>
+
<a class="brand" href="${abs_link('/')}">
${blog_title}
</a>
@@ -52,14 +54,14 @@
<div class="span8">
<%block name="content"></%block>
</div>
- </div>
+ </div>
<!--End of body content-->
</div>
<div class="footerbox">
${content_footer}
</div>
${html_social()}
-${analytics}
${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/main.py b/nikola/main.py
index b390387..8263b7e 100644
--- a/nikola/main.py
+++ b/nikola/main.py
@@ -35,16 +35,13 @@ from doit.cmd_help import Help as DoitHelp
from doit.cmd_run import Run as DoitRun
from .nikola import Nikola
+from .utils import _reload
def main(args):
sys.path.append('')
try:
import conf
- if sys.version_info[0] > 2:
- from imp import reload as _reload
- else:
- _reload = reload # NOQA
_reload(conf)
config = conf.__dict__
except ImportError:
@@ -111,7 +108,7 @@ class DoitNikola(DoitMain):
sub_cmds = self.get_commands()
args = self.process_args(cmd_args)
- if len(args) == 0 or args == ["--help"]:
+ if len(args) == 0 or any(arg in ["--help", '-h'] for arg in args):
cmd_args = ['help']
args = ['help']
diff --git a/nikola/nikola.py b/nikola/nikola.py
index a1506e7..8660a0f 100644
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -28,12 +28,14 @@ from collections import defaultdict
from copy import copy
import glob
import gzip
+import locale
import os
import sys
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
@@ -67,12 +69,19 @@ class Nikola(object):
Takes a site config as argument on creation.
"""
+ EXTRA_PLUGINS = [
+ 'planetoid',
+ 'ipynb',
+ 'local_search',
+ 'render_mustache',
+ ]
def __init__(self, **config):
"""Setup proper environment for running tasks."""
self.global_data = {}
self.posts_per_year = defaultdict(list)
+ self.posts_per_month = defaultdict(list)
self.posts_per_tag = defaultdict(list)
self.timeline = []
self.pages = []
@@ -83,21 +92,24 @@ class Nikola(object):
self.configured = True
# This is the default config
- # TODO: fill it
self.config = {
'ADD_THIS_BUTTONS': True,
'ANALYTICS': '',
'ARCHIVE_PATH': "",
'ARCHIVE_FILENAME': "archive.html",
'CACHE_FOLDER': 'cache',
+ 'CODE_COLOR_SCHEME': 'default',
'COMMENTS_IN_GALLERIES': False,
'COMMENTS_IN_STORIES': False,
'CONTENT_FOOTER': '',
+ 'CREATE_MONTHLY_ARCHIVE': False,
'DATE_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_LANG': "en",
'DEPLOY_COMMANDS': [],
'DISABLED_PLUGINS': (),
'DISQUS_FORUM': 'nikolademo',
+ 'ENABLED_EXTRAS': (),
+ 'EXTRA_HEAD_DATA': '',
'FAVICONS': {},
'FILE_METADATA_REGEXP': None,
'FILES_FOLDERS': {'files': ''},
@@ -105,6 +117,7 @@ class Nikola(object):
'GALLERY_PATH': 'galleries',
'GZIP_FILES': False,
'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json'),
+ 'HIDE_UNTRANSLATED_POSTS': False,
'INDEX_DISPLAY_POST_COUNT': 10,
'INDEX_TEASERS': False,
'INDEXES_TITLE': "",
@@ -114,6 +127,7 @@ class Nikola(object):
'LISTINGS_FOLDER': 'listings',
'MAX_IMAGE_SIZE': 1280,
'MATHJAX_CONFIG': '',
+ 'OLD_THEME_SUPPORT': True,
'OUTPUT_FOLDER': 'output',
'post_compilers': {
"rest": ('.txt', '.rst'),
@@ -136,6 +150,7 @@ class Nikola(object):
'SEARCH_FORM': '',
'SLUG_TAG_PATH': True,
'STORY_INDEX': False,
+ 'STRIP_INDEX_HTML': False,
'TAG_PATH': 'categories',
'TAG_PAGES_ARE_INDEXES': False,
'THEME': 'site',
@@ -156,16 +171,19 @@ class Nikola(object):
self.THEMES = utils.get_theme_chain(self.config['THEME'])
self.MESSAGES = utils.load_messages(self.THEMES,
- self.config['TRANSLATIONS'])
+ self.config['TRANSLATIONS'],
+ self.config['DEFAULT_LANG'])
# SITE_URL is required, but if the deprecated BLOG_URL
# is available, use it and warn
if 'SITE_URL' not in self.config:
if 'BLOG_URL' in self.config:
print("WARNING: You should configure SITE_URL instead of BLOG_URL")
- print("See docs at FIXME put URL")
self.config['SITE_URL'] = self.config['BLOG_URL']
+ self.default_lang = self.config['DEFAULT_LANG']
+ self.translations = self.config['TRANSLATIONS']
+
# BASE_URL defaults to SITE_URL
if 'BASE_URL' not in self.config:
self.config['BASE_URL'] = self.config.get('SITE_URL')
@@ -187,23 +205,28 @@ class Nikola(object):
self.commands = {}
# Activate all command plugins
- for pluginInfo in self.plugin_manager.getPluginsOfCategory("Command"):
- if pluginInfo.name in self.config['DISABLED_PLUGINS']:
- self.plugin_manager.removePluginFromCategory(pluginInfo, "Command")
+ for plugin_info in self.plugin_manager.getPluginsOfCategory("Command"):
+ 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, "Command")
continue
- self.plugin_manager.activatePluginByName(pluginInfo.name)
- pluginInfo.plugin_object.set_site(self)
- pluginInfo.plugin_object.short_help = pluginInfo.description
- self.commands[pluginInfo.name] = pluginInfo.plugin_object
+
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
+ plugin_info.plugin_object.short_help = plugin_info.description
+ self.commands[plugin_info.name] = plugin_info.plugin_object
# Activate all task plugins
for task_type in ["Task", "LateTask"]:
- for pluginInfo in self.plugin_manager.getPluginsOfCategory(task_type):
- if pluginInfo.name in self.config['DISABLED_PLUGINS']:
- self.plugin_manager.removePluginFromCategory(pluginInfo, task_type)
+ for plugin_info in self.plugin_manager.getPluginsOfCategory(task_type):
+ 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(pluginInfo.name)
- pluginInfo.plugin_object.set_site(self)
+ self.plugin_manager.activatePluginByName(plugin_info.name)
+ plugin_info.plugin_object.set_site(self)
# set global_context for template rendering
self.GLOBAL_CONTEXT = {
@@ -211,6 +234,7 @@ class Nikola(object):
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
@@ -244,9 +268,14 @@ class Nikola(object):
'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['sidebar_links'] = self.config.get('SIDEBAR_LINKS')
+
+ 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['twitter_card'] = self.config.get(
'TWITTER_CARD', {})
+ self.GLOBAL_CONTEXT['extra_head_data'] = self.config.get('EXTRA_HEAD_DATA')
self.GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {}))
@@ -273,14 +302,21 @@ class Nikola(object):
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.')
+
# Load compiler plugins
self.compilers = {}
self.inverse_compilers = {}
- for pluginInfo in self.plugin_manager.getPluginsOfCategory(
+ for plugin_info in self.plugin_manager.getPluginsOfCategory(
"PageCompiler"):
- self.compilers[pluginInfo.name] = \
- pluginInfo.plugin_object.compile_html
+ self.compilers[plugin_info.name] = \
+ plugin_info.plugin_object.compile_html
def get_compiler(self, source_name):
"""Get the correct compiler for a post from `conf.post_compilers`
@@ -324,11 +360,9 @@ class Nikola(object):
data = self.template_system.render_template(
template_name, None, local_context)
- assert isinstance(output_name, bytes)
assert output_name.startswith(
- self.config["OUTPUT_FOLDER"].encode('utf8'))
- url_part = output_name.decode('utf8')[len(self.config["OUTPUT_FOLDER"])
- + 1:]
+ self.config["OUTPUT_FOLDER"])
+ url_part = output_name[len(self.config["OUTPUT_FOLDER"]) + 1:]
# Treat our site as if output/ is "/" and then make all URLs relative,
# making the site "relocatable"
@@ -392,7 +426,20 @@ class Nikola(object):
with open(output_name, "wb+") as post_file:
post_file.write(data)
- def path(self, kind, name, lang, is_link=False):
+ 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:
@@ -417,6 +464,9 @@ class Nikola(object):
(ex: "archive\\index.html")
"""
+ if lang is None:
+ lang = self.current_lang()
+
path = []
if kind == "tag_index":
@@ -465,7 +515,11 @@ class Nikola(object):
path = [_f for _f in [self.config['LISTINGS_FOLDER'], name +
'.html'] if _f]
if is_link:
- return '/' + ('/'.join(path))
+ link = '/' + ('/'.join(path))
+ if self.config['STRIP_INDEX_HTML'] and link.endswith('/index.html'):
+ return link[:-10]
+ else:
+ return link
else:
return os.path.join(*path)
@@ -528,12 +582,14 @@ class Nikola(object):
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', 'unknown'),
+ 'name': task.get('name') + '.gz',
'clean': True,
}
targets = task.get('targets', [])
@@ -597,7 +653,18 @@ class Nikola(object):
dir_glob = os.path.join(dirpath, os.path.basename(wildcard))
dest_dir = os.path.normpath(os.path.join(destination,
os.path.relpath(dirpath, dirname)))
- for base_path in glob.glob(dir_glob):
+ full_list = glob.glob(dir_glob)
+ # Now let's look for things that are not in default_lang
+ for lang in self.config['TRANSLATIONS'].keys():
+ lang_glob = dir_glob + "." + lang
+ translated_list = glob.glob(lang_glob)
+ for fname in translated_list:
+ orig_name = os.path.splitext(fname)[0]
+ if orig_name in full_list:
+ continue
+ full_list.append(orig_name)
+
+ for base_path in full_list:
post = Post(
base_path,
self.config['CACHE_FOLDER'],
@@ -609,26 +676,32 @@ class Nikola(object):
self.MESSAGES,
template_name,
self.config['FILE_METADATA_REGEXP'],
+ self.config['STRIP_INDEX_HTML'],
tzinfo,
+ self.config['HIDE_UNTRANSLATED_POSTS'],
)
for lang, langpath in list(
self.config['TRANSLATIONS'].items()):
dest = (destination, langpath, dir_glob,
- post.pagenames[lang])
+ post.meta[lang]['slug'])
if dest in targets:
raise Exception('Duplicated output path {0!r} '
'in post {1!r}'.format(
- post.pagenames[lang],
+ post.meta[lang]['slug'],
base_path))
targets.add(dest)
self.global_data[post.post_name] = post
if post.use_in_feeds:
self.posts_per_year[
str(post.date.year)].append(post.post_name)
- for tag in post.tags:
+ self.posts_per_month[
+ '{0}/{1:02d}'.format(post.date.year, post.date.month)].append(post.post_name)
+ for tag in post.alltags:
self.posts_per_tag[tag].append(post.post_name)
else:
self.pages.append(post)
+ if self.config['OLD_THEME_SUPPORT']:
+ post._add_old_metadata()
for name, post in list(self.global_data.items()):
self.timeline.append(post)
self.timeline.sort(key=lambda p: p.date)
@@ -657,7 +730,7 @@ class Nikola(object):
else:
context['enable_comments'] = self.config['COMMENTS_IN_STORIES']
output_name = os.path.join(self.config['OUTPUT_FOLDER'],
- post.destination_path(lang)).encode('utf8')
+ post.destination_path(lang))
deps_dict = copy(context)
deps_dict.pop('post')
if post.prev_post:
@@ -668,9 +741,11 @@ class Nikola(object):
deps_dict['TRANSLATIONS'] = self.config['TRANSLATIONS']
deps_dict['global'] = self.GLOBAL_CONTEXT
deps_dict['comments'] = context['enable_comments']
+ if post:
+ deps_dict['post_translations'] = post.translated_to
task = {
- 'name': output_name,
+ 'name': os.path.normpath(output_name),
'file_dep': deps,
'targets': [output_name],
'actions': [(self.render_template, [post.template_name,
@@ -685,9 +760,6 @@ class Nikola(object):
template_name, filters, extra_context):
"""Renders pages with lists of posts."""
- # This is a name on disk, has to be bytes
- assert isinstance(output_name, bytes)
-
deps = self.template_system.template_deps(template_name)
for post in posts:
deps += post.deps(lang)
@@ -700,11 +772,11 @@ class Nikola(object):
context["nextlink"] = None
context.update(extra_context)
deps_context = copy(context)
- deps_context["posts"] = [(p.titles[lang], p.permalink(lang)) for p in
+ deps_context["posts"] = [(p.meta[lang]['title'], p.permalink(lang)) for p in
posts]
deps_context["global"] = self.GLOBAL_CONTEXT
task = {
- 'name': output_name,
+ 'name': os.path.normpath(output_name),
'targets': [output_name],
'file_dep': deps,
'actions': [(self.render_template, [template_name, output_name,
@@ -714,3 +786,14 @@ 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
+ try:
+ locale.setlocale(locale.LC_ALL, (lang, "utf8"))
+ except Exception:
+ print("WARNING: could not set locale to {0}."
+ "This may cause some i18n features not to work.".format(lang))
+ return ''
diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py
index cff9b65..c4ca788 100644
--- a/nikola/plugin_categories.py
+++ b/nikola/plugin_categories.py
@@ -145,12 +145,19 @@ class PageCompiler(object):
"""Plugins that compile text files into HTML."""
name = "dummy compiler"
+ default_metadata = {
+ 'title': '',
+ 'slug': '',
+ 'date': '',
+ 'tags': '',
+ 'link': '',
+ 'description': '',
+ }
def compile_html(self, source, dest):
"""Compile the source, save it on dest."""
raise NotImplementedError()
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
"""Create post file with optional metadata."""
raise NotImplementedError()
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
index a396f63..ea82703 100644
--- a/nikola/plugins/command_check.py
+++ b/nikola/plugins/command_check.py
@@ -24,6 +24,7 @@
from __future__ import print_function
import os
+import sys
try:
from urllib import unquote
from urlparse import urlparse
@@ -74,14 +75,17 @@ class CommandCheck(Command):
print(self.help())
return False
if options['links']:
- scan_links(options['find_sources'])
+ failure = scan_links(options['find_sources'])
if options['files']:
- scan_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())
@@ -100,6 +104,7 @@ def analize(task, find_sources=False):
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:")
@@ -109,17 +114,21 @@ def analize(task, find_sources=False):
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_pages'
'render_site') and '.html' in task:
- analize(task, find_sources)
+ if analize(task, find_sources):
+ failure = True
+ return failure
def scan_files():
@@ -127,6 +136,7 @@ def scan_files():
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:
@@ -144,6 +154,7 @@ def scan_files():
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:
@@ -151,3 +162,5 @@ def scan_files():
print("\nFiles not generated:\n")
for f in only_on_input:
print(f)
+
+ return failure
diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command_console.py
index 4af759f..f4d0295 100644
--- a/nikola/plugins/command_console.py
+++ b/nikola/plugins/command_console.py
@@ -29,35 +29,77 @@ import os
from nikola.plugin_categories import Command
-class Deploy(Command):
+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."
- def _execute(self, options, args):
- """Start the console."""
+ def ipython(self):
+ """IPython shell."""
from nikola import Nikola
try:
import conf
+ except ImportError:
+ print("No configuration found, cannot run the console.")
+ else:
+ import IPython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
- print("You can now access your configuration as conf and your "
- "site engine as SITE.")
+ IPython.embed(header='Nikola Console (conf = configuration, SITE '
+ '= site engine)')
+
+ def bpython(self):
+ """bpython shell."""
+ from nikola import Nikola
+ try:
+ import conf
except ImportError:
- print("No configuration found.")
- import code
+ print("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)
+
+ def plain(self):
+ """Plain Python shell."""
+ from nikola import Nikola
try:
- import readline
+ import conf
+ SITE = Nikola(**conf.__dict__)
+ SITE.scan_posts()
+ gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
except ImportError:
- pass
+ print("No configuration found, cannot run the console.")
else:
- import rlcompleter
- readline.set_completer(rlcompleter.Completer(globals()).complete)
- readline.parse_and_bind("tab:complete")
+ import code
+ try:
+ import readline
+ except ImportError:
+ pass
+ else:
+ import rlcompleter
+ readline.set_completer(rlcompleter.Completer(gl).complete)
+ readline.parse_and_bind("tab:complete")
+
+ pythonrc = os.environ.get("PYTHONSTARTUP")
+ if pythonrc and os.path.isfile(pythonrc):
+ try:
+ execfile(pythonrc) # NOQA
+ except NameError:
+ pass
+
+ code.interact(local=gl, banner='Nikola Console (conf = '
+ 'configuration, SITE = site engine)')
- pythonrc = os.environ.get("PYTHONSTARTUP")
- if pythonrc and os.path.isfile(pythonrc):
+ def _execute(self, options, args):
+ """Start the console."""
+ for shell in self.shells:
try:
- execfile(pythonrc) # NOQA
- except NameError:
+ return getattr(self, shell)()
+ except ImportError:
pass
- code.interact(local=globals())
+ raise ImportError
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
index ffa86ab..3277567 100644
--- a/nikola/plugins/command_deploy.py
+++ b/nikola/plugins/command_deploy.py
@@ -23,7 +23,12 @@
# 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
@@ -37,5 +42,24 @@ class Deploy(Command):
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)
- os.system(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_import_blogger.py b/nikola/plugins/command_import_blogger.py
index 35a702e..ecc4676 100644
--- a/nikola/plugins/command_import_blogger.py
+++ b/nikola/plugins/command_import_blogger.py
@@ -73,7 +73,7 @@ class CommandImportBlogger(Command):
]
def _execute(self, options, args):
- """Import a Wordpress blog from an export file into a Nikola site."""
+ """Import a Blogger blog from an export file into a Nikola site."""
# Parse the data
if feedparser is None:
@@ -126,7 +126,7 @@ class CommandImportBlogger(Command):
def generate_base_site(self):
if not os.path.exists(self.output_folder):
- os.system('nikola init --empty ' + 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 '
@@ -176,9 +176,16 @@ class CommandImportBlogger(Command):
@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('\n'.join((title, slug, post_date, ','.join(tags), '',
- description)))
+ 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."""
@@ -284,7 +291,7 @@ class CommandImportBlogger(Command):
if not self.import_into_existing_site:
filename = 'conf.py'
else:
- filename = 'conf.py.wordpress_import-{0}'.format(
+ 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)
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py
index e7ecca0..b45fe78 100644
--- a/nikola/plugins/command_import_wordpress.py
+++ b/nikola/plugins/command_import_wordpress.py
@@ -90,7 +90,6 @@ class CommandImportWordpress(Command):
def _execute(self, options={}, args=[]):
"""Import a Wordpress blog from an export file into a Nikola site."""
# Parse the data
- print(options, args)
if requests is None:
print('To use the import_wordpress command,'
' you have to install the "requests" package.')
@@ -100,10 +99,16 @@ class CommandImportWordpress(Command):
print(self.help())
return
- options['filename'] = args[0]
+ options['filename'] = args.pop(0)
- if len(args) > 1:
- options['output_folder'] = args[1]
+ if args and ('output_folder' not in args or
+ options['output_folder'] == 'new_site'):
+ 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))
self.wordpress_export_file = options['filename']
self.squash_newlines = options.get('squash_newlines', False)
@@ -204,8 +209,12 @@ class CommandImportWordpress(Command):
'PUT TITLE HERE')
context['BLOG_DESCRIPTION'] = get_text_tag(
channel, 'description', 'PUT DESCRIPTION HERE')
- context['SITE_URL'] = get_text_tag(channel, 'link', '#')
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['SITE_URL'] = context['BASE_URL']
+
author = channel.find('{{{0}}}author'.format(wordpress_namespace))
context['BLOG_EMAIL'] = get_text_tag(
author,
@@ -314,7 +323,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)
- slug = utils.slugify(urlparse(link).path)
+ path = urlparse(link).path
+
+ # In python 2, path is a str. slug requires a unicode
+ # object. Luckily, paths are also ASCII
+ if isinstance(path, utils.bytes_str):
+ path = path.decode('ASCII')
+ slug = utils.slugify(path)
if not slug: # it happens if the post has no "nice" URL
slug = get_text_tag(
item, '{{{0}}}post_name'.format(wordpress_namespace), None)
@@ -334,7 +349,10 @@ class CommandImportWordpress(Command):
item, '{http://purl.org/rss/1.0/modules/content/}encoded', '')
tags = []
- if status != 'publish':
+ if status == 'trash':
+ print('Trashed post "{0}" will not be imported.'.format(title))
+ return
+ elif status != 'publish':
tags.append('draft')
is_draft = True
else:
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
index 04a2cce..2a0a0cc 100644
--- a/nikola/plugins/command_install_theme.py
+++ b/nikola/plugins/command_install_theme.py
@@ -64,6 +64,10 @@ class CommandInstallTheme(Command):
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:
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py
index a823da3..933a51a 100644
--- a/nikola/plugins/command_new_post.py
+++ b/nikola/plugins/command_new_post.py
@@ -49,13 +49,31 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages):
if not filtered:
type_name = "post" if is_post else "page"
- raise Exception("Can't find a way, using your configuration, to create"
+ 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(
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
+ default compiler for this kind of post/page.
+ """
+
+ # First throw away all the post_pages with the wrong is_post
+ filtered = [entry for entry in post_pages if entry[3] == is_post]
+
+ # 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():
+ if extension in extensions:
+ return compiler
+ # No idea, back to default behaviour
+ return 'rest'
+
+
class CommandNewPost(Command):
"""Create a new post."""
@@ -105,7 +123,7 @@ class CommandNewPost(Command):
'short': 'f',
'long': 'format',
'type': str,
- 'default': 'rest',
+ 'default': '',
'help': 'Markup format for post, one of rest, markdown, wiki, '
'bbcode, html, textile, txt2tags',
}
@@ -140,6 +158,12 @@ class CommandNewPost(Command):
post_format = options['post_format']
+ if not post_format: # Issue #400
+ post_format = get_default_compiler(
+ is_post,
+ self.site.config['post_compilers'],
+ self.site.config['post_pages'])
+
if post_format not in compiler_names:
print("ERROR: Unknown post format " + post_format)
return
@@ -160,12 +184,14 @@ class CommandNewPost(Command):
title = sys.stdin.readline()
else:
print("Title:", title)
- if isinstance(title, bytes):
+ if isinstance(title, utils.bytes_str):
title = title.decode(sys.stdin.encoding)
title = title.strip()
if not path:
slug = utils.slugify(title)
else:
+ 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')
data = [title, slug, date, tags]
@@ -186,7 +212,9 @@ class CommandNewPost(Command):
d_name = os.path.dirname(txt_path)
if not os.path.exists(d_name):
os.makedirs(d_name)
- compiler_plugin.create_post(txt_path, onefile, title, slug, date, tags)
+ compiler_plugin.create_post(
+ txt_path, onefile, title=title,
+ slug=slug, date=date, tags=tags)
if not onefile: # write metadata file
with codecs.open(meta_path, "wb+", "utf8") as fd:
diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command_planetoid.plugin
new file mode 100644
index 0000000..8636d49
--- /dev/null
+++ b/nikola/plugins/command_planetoid.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = planetoid
+Module = command_planetoid
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Maintain a planet-like site
diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command_planetoid/__init__.py
new file mode 100644
index 0000000..183dd51
--- /dev/null
+++ b/nikola/plugins/command_planetoid/__init__.py
@@ -0,0 +1,287 @@
+# -*- 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.
+
+from __future__ import print_function, unicode_literals
+import codecs
+import datetime
+import hashlib
+from optparse import OptionParser
+import os
+import sys
+
+from doit.tools import timeout
+from nikola.plugin_categories import Command, Task
+from nikola.utils import config_changed
+
+try:
+ import feedparser
+except ImportError:
+ feedparser = None # NOQA
+
+try:
+ import peewee
+except ImportError:
+ peewee = None
+
+
+if peewee is not None:
+ class Feed(peewee.Model):
+ name = peewee.CharField()
+ url = peewee.CharField(max_length=200)
+ last_status = peewee.CharField(null=True)
+ etag = peewee.CharField(max_length=200)
+ last_modified = peewee.DateTimeField()
+
+ class Entry(peewee.Model):
+ date = peewee.DateTimeField()
+ feed = peewee.ForeignKeyField(Feed)
+ content = peewee.TextField(max_length=20000)
+ link = peewee.CharField(max_length=200)
+ title = peewee.CharField(max_length=200)
+ guid = peewee.CharField(max_length=200)
+
+
+class Planetoid(Command, Task):
+ """Maintain a planet-like thing."""
+ name = "planetoid"
+
+ def init_db(self):
+ # setup database
+ Feed.create_table(fail_silently=True)
+ Entry.create_table(fail_silently=True)
+
+ 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.'
+ else:
+ message = 'You need to install the \"peewee\" module.'
+
+ yield {
+ 'basename': self.name,
+ 'name': '',
+ 'verbosity': 2,
+ 'actions': ['echo "%s"' % message]
+ }
+ else:
+ self.init_db()
+ self.load_feeds()
+ for task in self.task_update_feeds():
+ yield task
+ for task in self.task_generate_posts():
+ yield task
+ yield {
+ 'basename': self.name,
+ 'name': '',
+ 'actions': [],
+ 'file_dep': ['feeds'],
+ 'task_dep': [
+ self.name + "_fetch_feed",
+ self.name + "_generate_posts",
+ ]
+ }
+
+ def run(self, *args):
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ (options, args) = parser.parse_args(list(args))
+
+ def load_feeds(self):
+ "Read the feeds file, add it to the database."
+ feeds = []
+ feed = name = None
+ for line in codecs.open('feeds', 'r', 'utf-8'):
+ line = line.strip()
+ if line.startswith("#"):
+ continue
+ elif line.startswith('http'):
+ feed = line
+ elif line:
+ name = line
+ if feed and name:
+ feeds.append([feed, name])
+ feed = name = None
+
+ def add_feed(name, url):
+ f = Feed.create(
+ name=name,
+ url=url,
+ etag='foo',
+ last_modified=datetime.datetime(1970, 1, 1),
+ )
+ f.save()
+
+ def update_feed_url(feed, url):
+ feed.url = url
+ feed.save()
+
+ for feed, name in feeds:
+ f = Feed.select().where(Feed.name == name)
+ if not list(f):
+ add_feed(name, feed)
+ elif list(f)[0].url != feed:
+ update_feed_url(list(f)[0], feed)
+
+ def task_update_feeds(self):
+ """Download feed contents, add entries to the database."""
+ def update_feed(feed):
+ modified = feed.last_modified.timetuple()
+ etag = feed.etag
+ try:
+ parsed = feedparser.parse(
+ feed.url,
+ etag=etag,
+ modified=modified
+ )
+ feed.last_status = str(parsed.status)
+ except: # Probably a timeout
+ # TODO: log failure
+ return
+ if parsed.feed.get('title'):
+ print(parsed.feed.title)
+ else:
+ print(feed.url)
+ feed.etag = parsed.get('etag', 'foo')
+ modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6]
+ print("==========>", modified)
+ modified = datetime.datetime(*modified)
+ feed.last_modified = modified
+ feed.save()
+ # No point in adding items from missinfg feeds
+ if parsed.status > 400:
+ # TODO log failure
+ return
+ for entry_data in parsed.entries:
+ print("=========================================")
+ 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)
+ return False
+ print("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)
+ if content:
+ content = content[0].value
+ if not content:
+ content = entry_data.get('description', None)
+ if not content:
+ content = entry_data.get('summary', 'Sin contenido')
+ guid = str(entry_data.get('guid', entry_data.link))
+ link = entry_data.link
+ print(repr([date, title]))
+ e = list(Entry.select().where(Entry.guid == guid))
+ print(
+ repr(dict(
+ date=date,
+ title=title,
+ content=content,
+ guid=guid,
+ feed=feed,
+ link=link,
+ ))
+ )
+ if not e:
+ entry = Entry.create(
+ date=date,
+ title=title,
+ content=content,
+ guid=guid,
+ feed=feed,
+ link=link,
+ )
+ else:
+ entry = e[0]
+ entry.date = date
+ entry.title = title
+ entry.content = content
+ entry.link = link
+ entry.save()
+ flag = False
+ for feed in Feed.select():
+ flag = True
+ task = {
+ 'basename': self.name + "_fetch_feed",
+ 'name': str(feed.url),
+ 'actions': [(update_feed, (feed, ))],
+ 'uptodate': [timeout(datetime.timedelta(minutes=
+ self.site.config.get('PLANETOID_REFRESH', 60)))],
+ }
+ yield task
+ if not flag:
+ yield {
+ 'basename': self.name + "_fetch_feed",
+ 'name': '',
+ 'actions': [],
+ }
+
+ def task_generate_posts(self):
+ """Generate post files for the blog entries."""
+ def gen_id(entry):
+ h = hashlib.md5()
+ h.update(entry.feed.name.encode('utf8'))
+ h.update(entry.guid)
+ return h.hexdigest()
+
+ def generate_post(entry):
+ unique_id = gen_id(entry)
+ meta_path = os.path.join('posts', unique_id + '.meta')
+ post_path = os.path.join('posts', unique_id + '.txt')
+ with codecs.open(meta_path, 'wb+', 'utf8') as fd:
+ fd.write('%s\n' % entry.title.replace('\n', ' '))
+ fd.write('%s\n' % unique_id)
+ fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M'))
+ fd.write('\n')
+ fd.write('%s\n' % entry.link)
+ with codecs.open(post_path, 'wb+', 'utf8') as fd:
+ fd.write('.. raw:: html\n\n')
+ content = entry.content
+ if not content:
+ content = 'Sin contenido'
+ for line in content.splitlines():
+ fd.write(' %s\n' % line)
+
+ if not os.path.isdir('posts'):
+ os.mkdir('posts')
+ flag = False
+ for entry in Entry.select().order_by(Entry.date.desc()):
+ flag = True
+ entry_id = gen_id(entry)
+ yield {
+ 'basename': self.name + "_generate_posts",
+ 'targets': [os.path.join('posts', entry_id + '.meta'), os.path.join('posts', entry_id + '.txt')],
+ 'name': entry_id,
+ 'actions': [(generate_post, (entry,))],
+ 'uptodate': [config_changed({1: entry})],
+ 'task_dep': [self.name + "_fetch_feed"],
+ }
+ if not flag:
+ yield {
+ 'basename': self.name + "_generate_posts",
+ 'name': '',
+ 'actions': [],
+ }
diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile_bbcode.py
index 26de727..f8022f3 100644
--- a/nikola/plugins/compile_bbcode.py
+++ b/nikola/plugins/compile_bbcode.py
@@ -60,19 +60,17 @@ class CompileTextile(PageCompiler):
output = self.parser.format(data)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ 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:
fd.write('[note]<!--\n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->[/note]\n\n')
- fd.write("\nWrite your post here.")
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile_html.py
index 6c1c381..7551b33 100644
--- a/nikola/plugins/compile_html.py
+++ b/nikola/plugins/compile_html.py
@@ -43,19 +43,17 @@ class CompileHtml(PageCompiler):
pass
shutil.copyfile(source, dest)
- def create_post(self, path, onefile=False, title="", slug="",
- date="", tags=""):
+ 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:
fd.write('<!-- \n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.keys():
+ 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
new file mode 100644
index 0000000..51051e0
--- /dev/null
+++ b/nikola/plugins/compile_ipynb.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = ipynb
+Module = compile_ipynb
+
+[Documentation]
+Author = Damián Avila
+Version = 0.1
+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..2cfd45e
--- /dev/null
+++ b/nikola/plugins/compile_ipynb/README.txt
@@ -0,0 +1,35 @@
+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_ipynb/__init__.py b/nikola/plugins/compile_ipynb/__init__.py
new file mode 100644
index 0000000..d38f6f2
--- /dev/null
+++ b/nikola/plugins/compile_ipynb/__init__.py
@@ -0,0 +1,100 @@
+# Copyright (c) 2013 Damian Avila.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on nbconvert."""
+
+from __future__ import unicode_literals, print_function
+import codecs
+import os
+
+try:
+ from .nbformat import current as nbformat
+ from .nbconvert.converters import bloggerhtml as nbconverter
+ bloggerhtml = True
+except ImportError:
+ bloggerhtml = None
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileIPynb(PageCompiler):
+ """Compile IPynb into HTML."""
+
+ 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()
+ 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)
+
+ 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))
+ meta_path = os.path.join(d_name, kw['slug'] + ".meta")
+ with codecs.open(meta_path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('%s\n' % kw['title'])
+ fd.write('%s\n' % kw['slug'])
+ fd.write('%s\n' % kw['date'])
+ fd.write('%s\n' % kw['tags'])
+ print("Your post's metadata is at: ", meta_path)
+ with codecs.open(path, "wb+", "utf8") as fd:
+ fd.write("""{
+ "metadata": {
+ "name": "%s"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+ {
+ "cells": [
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [],
+ "language": "python",
+ "metadata": {},
+ "outputs": []
+ }
+ ],
+ "metadata": {}
+ }
+ ]
+}""" % kw['slug'])
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile_markdown/__init__.py
index 7aa03a9..ae700e6 100644
--- a/nikola/plugins/compile_markdown/__init__.py
+++ b/nikola/plugins/compile_markdown/__init__.py
@@ -24,14 +24,28 @@
"""Implementation of compile_html based on markdown."""
+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
+ nikola_extension = NikolaExtension()
+
+ from nikola.plugins.compile_markdown.mdx_gist import GistExtension
+ gist_extension = GistExtension()
+
+ from nikola.plugins.compile_markdown.mdx_podcast import PodcastExtension
+ podcast_extension = PodcastExtension()
+
except ImportError:
markdown = None # NOQA
+ nikola_extension = None
+ gist_extension = None
+ podcast_extension = None
from nikola.plugin_categories import PageCompiler
@@ -41,6 +55,9 @@ class CompileMarkdown(PageCompiler):
name = "markdown"
+ extensions = ['fenced_code', 'codehilite', gist_extension,
+ nikola_extension, podcast_extension]
+
def compile_html(self, source, dest):
if markdown is None:
raise Exception('To build this site, you need to install the '
@@ -52,30 +69,20 @@ class CompileMarkdown(PageCompiler):
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
- output = markdown(data, ['fenced_code', 'codehilite'])
- # h1 is reserved for the title so increment all header levels
- for n in reversed(range(1, 9)):
- output = re.sub('<h{0}>'.format(n), '<h{0}>'.format(n + 1),
- output)
- output = re.sub('</h{0}>'.format(n), '</h{0}>'.format(n + 1),
- output)
- # python-markdown's highlighter uses the class 'codehilite' to wrap
- # code, # instead of the standard 'code'. None of the standard
- # pygments stylesheets use this class, so swap it to be 'code'
- output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
- r'\1code\2', output)
+ output = markdown(data, self.extensions)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ 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:
fd.write('<!-- \n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->\n\n')
- fd.write("\nWrite your post here.")
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile_markdown/mdx_gist.py b/nikola/plugins/compile_markdown/mdx_gist.py
new file mode 100644
index 0000000..808e383
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_gist.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt.
+#
+# 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.
+#
+# 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)
+
+Basic Example:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example with filename:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 zen.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=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example using reStructuredText syntax:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... .. gist:: 4747847 zen.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=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+'''
+from __future__ import unicode_literals
+import warnings
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import AtomicString
+from markdown.util import etree
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+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_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\]'
+GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$'
+
+
+class GistPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, 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
+
+ def get_raw_gist(self, gist_id):
+ url = GIST_RAW_URL.format(gist_id)
+ return requests.get(url).text
+
+ def handleMatch(self, m):
+ gist_id = m.group('gist_id')
+ gist_file = m.group('filename')
+
+ gist_elem = etree.Element('div')
+ 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)
+
+ else:
+ warnings.warn('"requests" package not installed. '
+ 'Please install to add inline gist source.')
+
+ return gist_elem
+
+
+class GistExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs())
+ gist_md_pattern.md = md
+ md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong")
+
+ gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs())
+ gist_rst_pattern.md = md
+ md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist")
+
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return GistExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+ 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
new file mode 100644
index 0000000..f7a1959
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_nikola.py
@@ -0,0 +1,56 @@
+# 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.
+
+"""Markdown Extension for Nikola-specific post-processing"""
+from __future__ import unicode_literals
+import re
+from markdown.postprocessors import Postprocessor
+from markdown.extensions import Extension
+
+
+class NikolaPostProcessor(Postprocessor):
+ def run(self, text):
+ output = text
+ # h1 is reserved for the title so increment all header levels
+ for n in reversed(range(1, 9)):
+ output = re.sub('<h%i>' % n, '<h%i>' % (n + 1), output)
+ output = re.sub('</h%i>' % n, '</h%i>' % (n + 1), output)
+
+ # python-markdown's highlighter uses the class 'codehilite' to wrap
+ # code, instead of the standard 'code'. None of the standard
+ # pygments stylesheets use this class, so swap it to be 'code'
+ output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
+ r'\1code\2', output)
+ return output
+
+
+class NikolaExtension(Extension):
+ def extendMarkdown(self, md, md_globals):
+ pp = NikolaPostProcessor()
+ md.postprocessors.add('nikola_post_processor', pp, '_end')
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return NikolaExtension(configs)
diff --git a/nikola/plugins/compile_markdown/mdx_podcast.py b/nikola/plugins/compile_markdown/mdx_podcast.py
new file mode 100644
index 0000000..be8bb6b
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_podcast.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt, Roberto Alsina
+#
+# 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.
+#
+# Inspired by "[Python] reStructuredText GitHub Podcast directive"
+# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
+
+from __future__ import print_function, unicode_literals
+
+
+'''
+Extension to Python Markdown for Embedded Audio
+
+Basic Example:
+
+>>> import markdown
+>>> text = """[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]"""
+>>> html = markdown.markdown(text, [PodcastExtension()])
+>>> print(html)
+<p><audio src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3"></audio></p>
+'''
+
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import etree
+
+PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]'
+
+
+class PodcastPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, pattern)
+
+ def handleMatch(self, m):
+ url = m.group('url').strip()
+ audio_elem = etree.Element('audio')
+ audio_elem.set('controls', '')
+ source_elem = etree.SubElement(audio_elem, 'source')
+ source_elem.set('src', url)
+ source_elem.set('type', 'audio/mpeg')
+ return audio_elem
+
+
+class PodcastExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs())
+ podcast_md_pattern.md = md
+ md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong")
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return PodcastExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
+ doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile_misaka.plugin b/nikola/plugins/compile_misaka.plugin
new file mode 100644
index 0000000..1b9c8a8
--- /dev/null
+++ b/nikola/plugins/compile_misaka.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = misaka
+Module = compile_misaka
+
+[Documentation]
+Author = Chris Lee
+Version = 0.1
+Website = http://c133.org/
+Description = Compile Markdown into HTML with Mikasa instead of python-markdown
+
diff --git a/nikola/plugins/compile_misaka/__init__.py b/nikola/plugins/compile_misaka/__init__.py
new file mode 100644
index 0000000..a3f687e
--- /dev/null
+++ b/nikola/plugins/compile_misaka/__init__.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2013 Chris Lee
+
+# 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 misaka."""
+
+from __future__ import unicode_literals
+
+import codecs
+import os
+
+try:
+ import misaka
+
+except ImportError:
+ misaka = None # NOQA
+ nikola_extension = None
+ gist_extension = None
+ podcast_extension = None
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileMarkdown(PageCompiler):
+ """Compile markdown into HTML."""
+
+ name = "markdown"
+
+ def __init__(self, *args, **kwargs):
+ super(CompileMarkdown, 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):
+ 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
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ output = misaka.html(data, extensions=self.ext)
+ out_file.write(output)
+
+ 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:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py
index b0a0c00..3d41571 100644
--- a/nikola/plugins/compile_rest/__init__.py
+++ b/nikola/plugins/compile_rest/__init__.py
@@ -26,26 +26,28 @@ from __future__ import unicode_literals
import codecs
import os
-import docutils.core
-import docutils.io
-from docutils.parsers.rst import directives
-
-from .pygments_code_block_directive import (
- code_block_directive,
- listings_directive)
-directives.register_directive('code-block', code_block_directive)
-directives.register_directive('listing', listings_directive)
-
-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)
+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
@@ -57,6 +59,9 @@ class CompileRest(PageCompiler):
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:
@@ -65,24 +70,38 @@ class CompileRest(PageCompiler):
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 = rst2html(
- data, settings_overrides={'initial_header_level': 2})
+ 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, title="", slug="", date="",
- tags=""):
+ 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:
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n\n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write("\nWrite your post here.")
@@ -116,4 +135,4 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
settings_overrides=settings_overrides,
config_section=config_section,
enable_exit_status=enable_exit_status)
- return pub.writer.parts['fragment'], pub.document.reporter.max_level
+ return pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
diff --git a/nikola/plugins/compile_rest/dummy.py b/nikola/plugins/compile_rest/dummy.py
new file mode 100644
index 0000000..39543fd
--- /dev/null
+++ b/nikola/plugins/compile_rest/dummy.py
@@ -0,0 +1,44 @@
+# -*- 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.
+
+"""A stupid codeblock replacement for neanderthals and users of Debian Sid."""
+
+from __future__ import unicode_literals
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+CODE = '<pre>{0}</pre>'
+
+
+class CodeBlock(Directive):
+ required_arguments = 1
+ has_content = True
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ return [nodes.raw('', CODE.format('\n'.join(self.content)), format='html')]
+
+directives.register_directive('code', CodeBlock)
diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile_rest/gist_directive.py
index 0ea8f23..1506519 100644
--- a/nikola/plugins/compile_rest/gist_directive.py
+++ b/nikola/plugins/compile_rest/gist_directive.py
@@ -28,7 +28,7 @@ class GitHubGist(Directive):
return requests.get(url).text
def get_raw_gist(self, gistID):
- url = "https://raw.github.com/gist/{0}/".format(gistID)
+ url = "https://raw.github.com/gist/{0}".format(gistID)
return requests.get(url).text
def run(self):
diff --git a/nikola/plugins/compile_rest/listing.py b/nikola/plugins/compile_rest/listing.py
new file mode 100644
index 0000000..1b816f5
--- /dev/null
+++ b/nikola/plugins/compile_rest/listing.py
@@ -0,0 +1,121 @@
+# -*- 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)
diff --git a/nikola/plugins/compile_rest/pygments_code_block_directive.py b/nikola/plugins/compile_rest/pygments_code_block_directive.py
deleted file mode 100644
index 79bada2..0000000
--- a/nikola/plugins/compile_rest/pygments_code_block_directive.py
+++ /dev/null
@@ -1,424 +0,0 @@
-# -*- coding: utf-8 -*-
-#$Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
-#$Revision: 2443 $
-
-# :Author: a Pygments author|contributor; Felix Wiemann; Guenter Milde
-# :Date: $Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
-# :Copyright: This module has been placed in the public domain.
-#
-# This is a merge of `Using Pygments in ReST documents`_ from the pygments_
-# documentation, and a `proof of concept`_ by Felix Wiemann.
-#
-# ========== ===========================================================
-# 2007-06-01 Removed redundancy from class values.
-# 2007-06-04 Merge of successive tokens of same type
-# (code taken from pygments.formatters.others).
-# 2007-06-05 Separate docutils formatter script
-# Use pygments' CSS class names (like the html formatter)
-# allowing the use of pygments-produced style sheets.
-# 2007-06-07 Merge in the formatting of the parsed tokens
-# (misnamed as docutils_formatter) as class DocutilsInterface
-# 2007-06-08 Failsave implementation (fallback to a standard literal block
-# if pygments not found)
-# ========== ===========================================================
-#
-# ::
-
-"""Define and register a code-block directive using pygments"""
-
-from __future__ import unicode_literals
-
-# Requirements
-# ------------
-# ::
-
-import codecs
-from copy import copy
-import os
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
-
-from docutils import nodes, core
-from docutils.parsers.rst import directives
-
-pygments = None
-try:
- import pygments
- from pygments.lexers import get_lexer_by_name
- from pygments.formatters.html import _get_ttype_class
-except ImportError:
- pass
-
-
-# Customisation
-# -------------
-#
-# Do not insert inline nodes for the following tokens.
-# (You could add e.g. Token.Punctuation like ``['', 'p']``.) ::
-
-unstyled_tokens = ['']
-
-
-# DocutilsInterface
-# -----------------
-#
-# This interface class combines code from
-# pygments.formatters.html and pygments.formatters.others.
-#
-# It does not require anything of docutils and could also become a part of
-# pygments::
-
-class DocutilsInterface(object):
- """Parse `code` string and yield "classified" tokens.
-
- Arguments
-
- code -- string of source code to parse
- language -- formal language the code is written in.
-
- Merge subsequent tokens of the same token-type.
-
- Yields the tokens as ``(ttype_class, value)`` tuples,
- where ttype_class is taken from pygments.token.STANDARD_TYPES and
- corresponds to the class argument used in pygments html output.
-
- """
-
- def __init__(self, code, language, custom_args={}):
- self.code = code
- self.language = language
- self.custom_args = custom_args
-
- def lex(self):
- """Get lexer for language (use text as fallback)"""
- try:
- if self.language and str(self.language).lower() != 'none':
- lexer = get_lexer_by_name(self.language.lower(),
- **self.custom_args)
- else:
- lexer = get_lexer_by_name('text', **self.custom_args)
- except ValueError:
- # what happens if pygment isn't present ?
- lexer = get_lexer_by_name('text')
- return pygments.lex(self.code, lexer)
-
- def join(self, tokens):
- """join subsequent tokens of same token-type
- """
- tokens = iter(tokens)
- (lasttype, lastval) = next(tokens)
- for ttype, value in tokens:
- if ttype is lasttype:
- lastval += value
- else:
- yield(lasttype, lastval)
- (lasttype, lastval) = (ttype, value)
- yield(lasttype, lastval)
-
- def __iter__(self):
- """parse code string and yield "clasified" tokens
- """
- try:
- tokens = self.lex()
- except IOError:
- yield ('', self.code)
- return
-
- for ttype, value in self.join(tokens):
- yield (_get_ttype_class(ttype), value)
-
-
-# code_block_directive
-# --------------------
-# ::
-
-def code_block_directive(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- """Parse and classify content of a code_block."""
- if 'include' in options:
- try:
- if 'encoding' in options:
- encoding = options['encoding']
- else:
- encoding = 'utf-8'
- content = codecs.open(
- options['include'], 'r', encoding).read().rstrip()
- except (IOError, UnicodeError): # no file or problem reading it
- content = ''
- line_offset = 0
- if content:
- # here we define the start-at and end-at options
- # so that limit is included in extraction
- # this is different than the start-after directive of docutils
- # (docutils/parsers/rst/directives/misc.py L73+)
- # which excludes the beginning
- # the reason is we want to be able to define a start-at like
- # def mymethod(self)
- # and have such a definition included
-
- after_text = options.get('start-at', None)
- if after_text:
- # skip content in include_text before
- # *and NOT incl.* a matching text
- after_index = content.find(after_text)
- if after_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "start-at" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['start-at']))
- # patch mmueller start
- # Move the after_index to the beginning of the line with the
- # match.
- for char in content[after_index:0:-1]:
- # codecs always opens binary. This works with '\n',
- # '\r' and '\r\n'. We are going backwards, so
- # '\n' is found first in '\r\n'.
- # Going with .splitlines() seems more appropriate
- # but needs a few more changes.
- if char == '\n' or char == '\r':
- break
- after_index -= 1
- # patch mmueller end
-
- content = content[after_index:]
- line_offset = len(content[:after_index].splitlines())
-
- after_text = options.get('start-after', None)
- if after_text:
- # skip content in include_text before
- # *and incl.* a matching text
- after_index = content.find(after_text)
- if after_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "start-after" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['start-after']))
- line_offset = len(content[:after_index +
- len(after_text)].splitlines())
- content = content[after_index + len(after_text):]
-
- # same changes here for the same reason
- before_text = options.get('end-at', None)
- if before_text:
- # skip content in include_text after
- # *and incl.* a matching text
- before_index = content.find(before_text)
- if before_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "end-at" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['end-at']))
- content = content[:before_index + len(before_text)]
-
- before_text = options.get('end-before', None)
- if before_text:
- # skip content in include_text after
- # *and NOT incl.* a matching text
- before_index = content.find(before_text)
- if before_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "end-before" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['end-before']))
- content = content[:before_index]
-
- else:
- content = '\n'.join(content)
-
- if 'tabsize' in options:
- tabw = options['tabsize']
- else:
- tabw = int(options.get('tab-width', 8))
-
- content = content.replace('\t', ' ' * tabw)
-
- withln = "linenos" in options
- if not "linenos_offset" in options:
- line_offset = 0
-
- language = arguments[0]
- # create a literal block element and set class argument
- code_block = nodes.literal_block(classes=["code", language])
-
- if withln:
- lineno = 1 + line_offset
- total_lines = content.count('\n') + 1 + line_offset
- lnwidth = len(str(total_lines))
- fstr = "\n%{0}d ".format(lnwidth)
- code_block += nodes.inline(fstr[1:].format(lineno),
- fstr[1:].format(lineno),
- classes=['linenumber'])
-
- # parse content with pygments and add to code_block element
- content = content.rstrip()
- if pygments is None:
- code_block += nodes.Text(content, content)
- else:
- # The [:-1] is because pygments adds a trailing \n which looks bad
- l = list(DocutilsInterface(content, language, options))
- if l[-1] == ('', '\n'):
- l = l[:-1]
- # We strip last element for the same reason (trailing \n looks bad)
- if l:
- l[-1] = (l[-1][0], l[-1][1].rstrip())
- for cls, value in l:
- if withln and "\n" in value:
- # Split on the "\n"s
- values = value.split("\n")
- # The first piece, pass as-is
- code_block += nodes.Text(values[0], values[0])
- # On the second and later pieces, insert \n and linenos
- linenos = list(range(lineno, lineno + len(values)))
- for chunk, ln in zip(values, linenos)[1:]:
- if ln <= total_lines:
- code_block += nodes.inline(fstr.format(ln),
- fstr.format(ln),
- classes=['linenumber'])
- code_block += nodes.Text(chunk, chunk)
- lineno += len(values) - 1
-
- elif cls in unstyled_tokens:
- # insert as Text to decrease the verbosity of the output.
- code_block += nodes.Text(value, value)
- else:
- code_block += nodes.inline(value, value, classes=[cls])
-
- return [code_block]
-
-# Custom argument validators
-# --------------------------
-# ::
-#
-# Move to separated module??
-
-
-def string_list(argument):
- """
- Converts a space- or comma-separated list of values into a python list
- of strings.
- (Directive option conversion function)
- Based in positive_int_list of docutils.parsers.rst.directives
- """
- if ',' in argument:
- entries = argument.split(',')
- else:
- entries = argument.split()
- return entries
-
-
-def string_bool(argument):
- """
- Converts True, true, False, False in python boolean values
- """
- if argument is None:
- msg = 'argument required but none supplied; choose "True" or "False"'
- raise ValueError(msg)
-
- elif argument.lower() == 'true':
- return True
- elif argument.lower() == 'false':
- return False
- else:
- raise ValueError('"{0}" unknown; choose from "True" or "False"'.format(
- argument))
-
-
-def csharp_unicodelevel(argument):
- return directives.choice(argument, ('none', 'basic', 'full'))
-
-
-def lhs_litstyle(argument):
- return directives.choice(argument, ('bird', 'latex'))
-
-
-def raw_compress(argument):
- return directives.choice(argument, ('gz', 'bz2'))
-
-
-def listings_directive(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- fname = arguments[0]
- options['include'] = os.path.join('listings', fname)
- target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = [core.publish_doctree('`{0} <{1}>`_'.format(fname,
- target))[0]]
- generated_nodes += code_block_directive(name, [arguments[1]], options,
- content, lineno, content_offset,
- block_text, state, state_machine)
- return generated_nodes
-
-code_block_directive.arguments = (1, 0, 1)
-listings_directive.arguments = (2, 0, 1)
-code_block_directive.content = 1
-listings_directive.content = 1
-code_block_directive.options = {'include': directives.unchanged_required,
- 'start-at': directives.unchanged_required,
- 'end-at': directives.unchanged_required,
- 'start-after': directives.unchanged_required,
- 'end-before': directives.unchanged_required,
- 'linenos': directives.unchanged,
- 'linenos_offset': directives.unchanged,
- 'tab-width': directives.unchanged,
- # generic
- 'stripnl': string_bool,
- 'stripall': string_bool,
- 'ensurenl': string_bool,
- 'tabsize': directives.positive_int,
- 'encoding': directives.encoding,
- # Lua
- 'func_name_hightlighting': string_bool,
- 'disabled_modules': string_list,
- # Python Console
- 'python3': string_bool,
- # Delphi
- 'turbopascal': string_bool,
- 'delphi': string_bool,
- 'freepascal': string_bool,
- 'units': string_list,
- # Modula2
- 'pim': string_bool,
- 'iso': string_bool,
- 'objm2': string_bool,
- 'gm2ext': string_bool,
- # CSharp
- 'unicodelevel': csharp_unicodelevel,
- # Literate haskell
- 'litstyle': lhs_litstyle,
- # Raw
- 'compress': raw_compress,
- # Rst
- 'handlecodeblocks': string_bool,
- # Php
- 'startinline': string_bool,
- 'funcnamehighlighting': string_bool,
- 'disabledmodules': string_list,
- }
-
-listings_directive.options = copy(code_block_directive.options)
-listings_directive.options.pop('include')
-
-# .. _doctutils: http://docutils.sf.net/
-# .. _pygments: http://pygments.org/
-# .. _Using Pygments in ReST documents: http://pygments.org/docs/rstdirective/
-# .. _proof of concept:
-# http://article.gmane.org/gmane.text.docutils.user/3689
-#
-# Test output
-# -----------
-#
-# If called from the command line, call the docutils publisher to render the
-# input::
-
-if __name__ == '__main__':
- from docutils.core import publish_cmdline, default_description
- from docutils.parsers.rst import directives
- directives.register_directive('code-block', code_block_directive)
- description = "code-block directive test output" + default_description
- try:
- import locale
- locale.setlocale(locale.LC_ALL, '')
- except Exception:
- pass
- publish_cmdline(writer_name='html', description=description)
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile_rest/slides.py
index f9901f5..57fb754 100644
--- a/nikola/plugins/compile_rest/slides.py
+++ b/nikola/plugins/compile_rest/slides.py
@@ -22,71 +22,44 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import json
+from __future__ import unicode_literals
from docutils import nodes
from docutils.parsers.rst import Directive, directives
-class slides(Directive):
+class Slides(Directive):
""" Restructured text extension for inserting slideshows."""
has_content = True
- option_spec = {
- "preload": directives.flag,
- "preloadImage": directives.uri,
- "container": directives.unchanged,
- "generateNextPrev": directives.flag,
- "next": directives.unchanged,
- "prev": directives.unchanged,
- "pagination": directives.flag,
- "generatePagination": directives.flag,
- "paginationClass": directives.unchanged,
- "currentClass": directives.unchanged,
- "fadeSpeed": directives.positive_int,
- "fadeEasing": directives.unchanged,
- "slideSpeed": directives.positive_int,
- "slideEasing": directives.unchanged,
- "start": directives.positive_int,
- "effect": directives.unchanged,
- "crossfade": directives.flag,
- "randomize": directives.flag,
- "play": directives.positive_int,
- "pause": directives.positive_int,
- "hoverPause": directives.flag,
- "autoHeight": directives.flag,
- "autoHeightSpeed": directives.positive_int,
- "bigTarget": directives.flag,
- "animationStart": directives.unchanged,
- "animationComplete": directives.unchanged,
- }
def run(self):
if len(self.content) == 0:
return
- for opt in ("preload", "generateNextPrev", "pagination",
- "generatePagination", "crossfade", "randomize",
- "hoverPause", "autoHeight", "bigTarget"):
- if opt in self.options:
- self.options[opt] = True
- options = {
- "autoHeight": True,
- "bigTarget": True,
- "paginationClass": "pager",
- "currentClass": "slide-current"
- }
- options.update(self.options)
- options = json.dumps(options)
output = []
- output.append('<script> $(function(){ $("#slides").slides(' + options +
- '); });'
- '</script>')
- output.append('<div id="slides" class="slides"><div '
- 'class="slides_container">')
- for image in self.content:
- output.append("""<div><img src="{0}"></div>""".format(image))
- output.append("""</div></div>""")
-
+ 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')]
-directives.register_directive('slides', slides)
+directives.register_directive('slides', Slides)
diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile_rest/soundcloud.py
index d47bebf..6bdd4d5 100644
--- a/nikola/plugins/compile_rest/soundcloud.py
+++ b/nikola/plugins/compile_rest/soundcloud.py
@@ -1,5 +1,9 @@
+# coding: utf8
+
+
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive, directives
+
CODE = ("""<iframe width="{width}" height="{height}"
scrolling="no" frameborder="no"
@@ -8,25 +12,39 @@ src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/"""
</iframe>""")
-def soundcloud(name, args, options, content, lineno,
- contentOffset, blockText, state, stateMachine):
- """ Restructured text extension for inserting SoundCloud embedded music """
- string_vars = {
- 'sid': content[0],
- 'width': 600,
- 'height': 160,
- 'extra': ''
+class SoundCloud(Directive):
+ """ Restructured text extension for inserting SoundCloud embedded music
+
+ Usage:
+ .. soundcloud:: <sound id>
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ 'width': directives.positive_int,
+ 'height': directives.positive_int,
}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
-
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-
-soundcloud.content = True
-directives.register_directive('soundcloud', soundcloud)
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ self.check_content()
+ options = {
+ 'sid': self.arguments[0],
+ 'width': 600,
+ 'height': 160,
+ }
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_content(self):
+ """ Emit a deprecation warning if there is content """
+ if self.content:
+ 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.py b/nikola/plugins/compile_rest/vimeo.py
index 34f2a50..c1dc143 100644
--- a/nikola/plugins/compile_rest/vimeo.py
+++ b/nikola/plugins/compile_rest/vimeo.py
@@ -1,3 +1,4 @@
+# coding: utf8
# Copyright (c) 2012 Roberto Alsina y otros.
# Permission is hereby granted, free of charge, to any
@@ -22,8 +23,9 @@
# 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 directives
+from docutils.parsers.rst import Directive, directives
try:
import requests
@@ -37,6 +39,7 @@ except ImportError:
except ImportError:
json = None
+
CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}"
width="{width}" height="{height}"
frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen>
@@ -47,46 +50,69 @@ VIDEO_DEFAULT_HEIGHT = 500
VIDEO_DEFAULT_WIDTH = 281
-def vimeo(name, args, options, content, lineno, contentOffset, blockText,
- state, stateMachine):
- """ Restructured text extension for inserting vimeo embedded videos """
- 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.")
- if len(content) == 0:
- return
-
- string_vars = {'vimeo_id': content[0]}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
-
- # Only need to make a connection if width and height aren't provided
- if 'height' not in string_vars or 'width' not in string_vars:
- string_vars['height'] = VIDEO_DEFAULT_HEIGHT
- string_vars['width'] = VIDEO_DEFAULT_WIDTH
-
- if json: # we can attempt to retrieve video attributes from vimeo
- try:
- url = ('http://vimeo.com/api/v2/video/{vimeo_id}'
- '.json'.format(**string_vars))
- data = requests.get(url).text
- video_attributes = json.loads(data)
- string_vars['height'] = video_attributes['height']
- string_vars['width'] = video_attributes['width']
- except Exception:
- # fall back to the defaults
- pass
-
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-
-vimeo.content = True
-directives.register_directive('vimeo', vimeo)
+class Vimeo(Directive):
+ """ Restructured text extension for inserting vimeo embedded videos
+
+ Usage:
+ .. vimeo:: 20241459
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "width": directives.positive_int,
+ "height": directives.positive_int,
+ }
+
+ # set to False for not querying the vimeo api for size
+ request_size = True
+
+ def run(self):
+ self.check_content()
+ options = {
+ 'vimeo_id': self.arguments[0],
+ 'width': VIDEO_DEFAULT_WIDTH,
+ 'height': VIDEO_DEFAULT_HEIGHT,
+ }
+ if self.request_size:
+ self.check_modules()
+ self.set_video_size()
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_modules(self):
+ 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.")
+
+ def set_video_size(self):
+ # Only need to make a connection if width and height aren't provided
+ if 'height' not in self.options or 'width' not in self.options:
+ self.options['height'] = VIDEO_DEFAULT_HEIGHT
+ self.options['width'] = VIDEO_DEFAULT_WIDTH
+
+ if json: # we can attempt to retrieve video attributes from vimeo
+ try:
+ url = ('http://vimeo.com/api/v2/video/{0}'
+ '.json'.format(self.arguments[0]))
+ data = requests.get(url).text
+ video_attributes = json.loads(data)[0]
+ self.options['height'] = video_attributes['height']
+ self.options['width'] = video_attributes['width']
+ except Exception:
+ # fall back to the defaults
+ pass
+
+ def check_content(self):
+ if self.content:
+ 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.py b/nikola/plugins/compile_rest/youtube.py
index 30ac000..767be32 100644
--- a/nikola/plugins/compile_rest/youtube.py
+++ b/nikola/plugins/compile_rest/youtube.py
@@ -23,7 +23,8 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive, directives
+
CODE = """\
<iframe width="{width}"
@@ -32,25 +33,37 @@ src="http://www.youtube.com/embed/{yid}?rel=0&amp;hd=1&amp;wmode=transparent"
></iframe>"""
-def youtube(name, args, options, content, lineno,
- contentOffset, blockText, state, stateMachine):
- """ Restructured text extension for inserting youtube embedded videos """
- if len(content) == 0:
- return
- string_vars = {
- 'yid': content[0],
- 'width': 425,
- 'height': 344,
- 'extra': ''
+class Youtube(Directive):
+ """ Restructured text extension for inserting youtube embedded videos
+
+ Usage:
+ .. youtube:: lyViVmaBQDg
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "width": directives.positive_int,
+ "height": directives.positive_int,
}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-youtube.content = True
-directives.register_directive('youtube', youtube)
+
+ def run(self):
+ self.check_content()
+ options = {
+ 'yid': self.arguments[0],
+ 'width': 425,
+ 'height': 344,
+ }
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_content(self):
+ if self.content:
+ 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.py b/nikola/plugins/compile_textile.py
index 3ca370d..85efd3f 100644
--- a/nikola/plugins/compile_textile.py
+++ b/nikola/plugins/compile_textile.py
@@ -54,19 +54,17 @@ class CompileTextile(PageCompiler):
output = textile(data, head_offset=1)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ 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:
fd.write('<notextile> <!--\n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('--></notextile>\n\n')
fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile_txt2tags.py
index 90372bd..001da6e 100644
--- a/nikola/plugins/compile_txt2tags.py
+++ b/nikola/plugins/compile_txt2tags.py
@@ -57,19 +57,17 @@ class CompileTextile(PageCompiler):
cmd = ["-t", "html", "--no-headers", "--outfile", dest, source]
txt2tags(cmd)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ 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:
fd.write("\n'''\n<!--\n")
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write("-->\n'''\n")
fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_wiki.py b/nikola/plugins/compile_wiki.py
index 1215506..fb9e010 100644
--- a/nikola/plugins/compile_wiki.py
+++ b/nikola/plugins/compile_wiki.py
@@ -57,14 +57,16 @@ class CompileTextile(PageCompiler):
output = HtmlEmitter(document).emit()
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ 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))
if onefile:
raise Exception('There are no comments in CreoleWiki markup, so '
'one-file format is not possible, use the -2 '
'option.')
- 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:
fd.write("Write your post here.")
diff --git a/nikola/plugins/task_archive.py b/nikola/plugins/task_archive.py
index f91a10e..a67826f 100644
--- a/nikola/plugins/task_archive.py
+++ b/nikola/plugins/task_archive.py
@@ -22,7 +22,9 @@
# 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
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -39,16 +41,51 @@ class Archive(Task):
"translations": self.site.config['TRANSLATIONS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
}
self.site.scan_posts()
# TODO add next/prev links for years
- template_name = "list_post.tmpl"
- # TODO: posts_per_year is global, kill it
- for year, posts in list(self.site.posts_per_year.items()):
- for lang in kw["translations"]:
+ for lang in kw["translations"]:
+ for year, posts in self.site.posts_per_year.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
+ context["permalink"] = self.site.link("archive", year, lang)
+ if not kw["create_monthly_archive"]:
+ template_name = "list_post.tmpl"
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(key=lambda a: a.date)
+ post_list.reverse()
+ context["posts"] = post_list
+ 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))
+ template_name = "list.tmpl"
+ context["items"] = [[get_month_name(int(month), lang), month] for month in months]
+ post_list = []
+ 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_monthly_archive"]:
+ continue # Just to avoid nesting the other loop in this if
+ template_name = "list_post.tmpl"
+ for yearmonth, posts in self.site.posts_per_month.items():
output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", year,
- lang)).encode('utf8')
+ kw['output_folder'], self.site.path("archive", yearmonth,
+ lang))
+ year, month = yearmonth.split('/')
post_list = [self.site.global_data[post] for post in posts]
post_list.sort(key=lambda a: a.date)
post_list.reverse()
@@ -56,8 +93,9 @@ class Archive(Task):
context["lang"] = lang
context["posts"] = post_list
context["permalink"] = self.site.link("archive", year, lang)
- context["title"] = kw["messages"][lang]["Posts for year %s"]\
- % year
+
+ context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
+ year=year, month=get_month_name(int(month), lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -80,7 +118,7 @@ class Archive(Task):
context = {}
output_name = os.path.join(
kw['output_folder'], self.site.path("archive", None,
- lang)).encode('utf8')
+ lang))
context["title"] = kw["messages"][lang]["Archive"]
context["items"] = [(year, self.site.link("archive", year, lang))
for year in years]
@@ -97,3 +135,13 @@ class Archive(Task):
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
diff --git a/nikola/plugins/task_copy_assets.py b/nikola/plugins/task_copy_assets.py
index 39fef5a..06d17e7 100644
--- a/nikola/plugins/task_copy_assets.py
+++ b/nikola/plugins/task_copy_assets.py
@@ -22,6 +22,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import codecs
import os
from nikola.plugin_categories import Task
@@ -44,15 +45,20 @@ class CopyAssets(Task):
"themes": self.site.THEMES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
"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')
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
if task['name'] in tasks:
continue
+ if task['targets'][0] == code_css_path:
+ has_code_css = True
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw)]
task['basename'] = self.name
@@ -66,3 +72,22 @@ class CopyAssets(Task):
'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"])
+ 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;}")
+
+ task = {
+ 'basename': self.name,
+ 'name': code_css_path,
+ 'targets': [code_css_path],
+ 'uptodate': [utils.config_changed(kw)],
+ 'actions': [(create_code_css, [])],
+ 'clean': True,
+ }
+ yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task_create_bundles.py
index ad670e1..84ac0ab 100644
--- a/nikola/plugins/task_create_bundles.py
+++ b/nikola/plugins/task_create_bundles.py
@@ -22,6 +22,8 @@
# 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 os
try:
@@ -53,6 +55,7 @@ class BuildBundles(LateTask):
'theme_bundles': get_theme_bundles(self.site.THEMES),
'themes': self.site.THEMES,
'files_folders': self.site.config['FILES_FOLDERS'],
+ 'code_color_scheme': self.site.config['CODE_COLOR_SCHEME'],
}
def build_bundle(output, inputs):
@@ -76,7 +79,7 @@ class BuildBundles(LateTask):
for name, files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
- file_dep = [get_asset_path(
+ file_dep = [utils.get_asset_path(
os.path.join(dname, fname), kw['themes'],
kw['files_folders'])
for fname in files
@@ -101,47 +104,6 @@ class BuildBundles(LateTask):
}
-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.
-
- If the asset is not provided by a theme, then it will be checked for
- in the FILES_FOLDERS
-
- >>> get_asset_path('assets/css/rst.css', ['site', 'default'])
- 'nikola/data/themes/default/assets/css/rst.css'
-
- >>> get_asset_path('assets/css/theme.css', ['site', 'default'])
- 'nikola/data/themes/site/assets/css/theme.css'
-
- >>> get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''})
- 'nikola/nikola.py'
-
- >>> get_asset_path('nikola/nikola.py', ['site', 'default'],
- ... {'nikola':'nikola'})
- 'nikola/nikola.py'
-
- """
- for theme_name in themes:
- candidate = os.path.join(
- utils.get_theme_path(theme_name),
- path
- )
- if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
- for src, rel_dst in files_folders.items():
- candidate = os.path.join(
- src,
- os.path.relpath(path, rel_dst)
- )
- if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
-
- # whatever!
- return None
-
-
def get_theme_bundles(themes):
"""Given a theme chain, return the bundle definitions."""
bundles = {}
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task_indexes.py
index 7baf660..aa5e648 100644
--- a/nikola/plugins/task_indexes.py
+++ b/nikola/plugins/task_indexes.py
@@ -46,31 +46,35 @@ class Indexes(Task):
"index_teasers": self.site.config['INDEX_TEASERS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "indexes_title": self.site.config['INDEXES_TITLE'],
+ "indexes_pages": self.site.config['INDEXES_PAGES'],
+ "blog_title": self.site.config["BLOG_TITLE"],
}
template_name = "index.tmpl"
- # TODO: timeline is global, get rid of it
posts = [x for x in self.site.timeline if x.use_in_feeds]
- # Split in smaller lists
- lists = []
- while posts:
- lists.append(posts[:kw["index_display_post_count"]])
- posts = posts[kw["index_display_post_count"]:]
- num_pages = len(lists)
- if not lists:
+ if not posts:
yield {'basename': 'render_indexes', 'actions': []}
for lang in kw["translations"]:
+ # Split in smaller lists
+ lists = []
+ if kw["hide_untranslated_posts"]:
+ filtered_posts = [x for x in posts if x.is_translation_available(lang)]
+ else:
+ filtered_posts = posts
+ while filtered_posts:
+ lists.append(filtered_posts[:kw["index_display_post_count"]])
+ filtered_posts = filtered_posts[kw["index_display_post_count"]:]
+ num_pages = len(lists)
for i, post_list in enumerate(lists):
context = {}
- if self.site.config.get("INDEXES_TITLE", ""):
- indexes_title = self.site.config['INDEXES_TITLE']
- else:
- indexes_title = self.site.config["BLOG_TITLE"]
+ indexes_title = kw['indexes_title'] or kw['blog_title']
if not i:
context["title"] = indexes_title
else:
- if self.site.config.get("INDEXES_PAGES", ""):
- indexes_pages = self.site.config["INDEXES_PAGES"] % i
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % i
else:
indexes_pages = " (" + \
kw["messages"][lang]["old posts page %d"] % i + ")"
@@ -87,7 +91,7 @@ class Indexes(Task):
context["permalink"] = self.site.link("index", i, lang)
output_name = os.path.join(
kw['output_folder'], self.site.path("index", i,
- lang)).encode('utf8')
+ lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -103,7 +107,6 @@ class Indexes(Task):
if not self.site.config["STORY_INDEX"]:
return
- # TODO: do story indexes as described in #232
kw = {
"translations": self.site.config['TRANSLATIONS'],
"post_pages": self.site.config["post_pages"],
diff --git a/nikola/plugins/task_localsearch.plugin b/nikola/plugins/task_localsearch.plugin
new file mode 100644
index 0000000..33eb78b
--- /dev/null
+++ b/nikola/plugins/task_localsearch.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = local_search
+Module = task_localsearch
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+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
new file mode 100644
index 0000000..f131068
--- /dev/null
+++ b/nikola/plugins/task_localsearch/MIT-LICENSE.txt
@@ -0,0 +1,20 @@
+Tipue Search Copyright (c) 2012 Tipue
+
+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/task_localsearch/__init__.py b/nikola/plugins/task_localsearch/__init__.py
new file mode 100644
index 0000000..db8610a
--- /dev/null
+++ b/nikola/plugins/task_localsearch/__init__.py
@@ -0,0 +1,102 @@
+# 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 json
+import os
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed, copy_tree
+
+# This is what we need to produce:
+#var tipuesearch = {"pages": [
+ #{"title": "Tipue Search, a jQuery site search engine", "text": "Tipue
+ #Search is a site search engine jQuery plugin. It's free for both commercial and
+ #non-commercial use and released under the MIT License. Tipue Search includes
+ #features such as word stemming and word replacement.", "tags": "JavaScript",
+ #"loc": "http://www.tipue.com/search"},
+ #{"title": "Tipue Search demo", "text": "Tipue Search demo. Tipue Search is
+ #a site search engine jQuery plugin.", "tags": "JavaScript", "loc":
+ #"http://www.tipue.com/search/demo"},
+ #{"title": "About Tipue", "text": "Tipue is a small web development/design
+ #studio based in North London. We've been around for over a decade.", "tags": "",
+ #"loc": "http://www.tipue.com/about"}
+#]};
+
+
+class Tipue(LateTask):
+ """Render the blog posts as JSON data."""
+
+ name = "local_search"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ }
+
+ posts = self.site.timeline[:]
+ dst_path = os.path.join(kw["output_folder"], "assets", "js",
+ "tipuesearch_content.json")
+
+ def save_data():
+ pages = []
+ for lang in kw["translations"]:
+ for post in posts:
+ # Don't index drafts (Issue #387)
+ if post.is_draft:
+ continue
+ text = post.text(lang, strip_html=True)
+ text = text.replace('^', '')
+
+ data = {}
+ data["title"] = post.title(lang)
+ data["text"] = text
+ data["tags"] = ",".join(post.tags)
+ 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
+ with codecs.open(dst_path, "wb+", "utf8") as fd:
+ fd.write(output)
+
+ yield {
+ "basename": str(self.name),
+ "name": dst_path,
+ "targets": [dst_path],
+ "actions": [(save_data, [])],
+ 'uptodate': [config_changed(kw)]
+ }
+
+ # Copy all the assets to the right places
+ asset_folder = os.path.join(os.path.dirname(__file__), "files")
+ for task in copy_tree(asset_folder, kw["output_folder"]):
+ task["basename"] = str(self.name)
+ yield task
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
new file mode 100755
index 0000000..21bb7b0
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
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
new file mode 100755
index 0000000..d4e51c5
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/link.png
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif
new file mode 100644
index 0000000..9c97738
--- /dev/null
+++ 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.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
new file mode 100644
index 0000000..644bd17
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
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..96dadf0
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
@@ -0,0 +1,232 @@
+
+/*
+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_localsearch/files/assets/js/tipuesearch.js b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
new file mode 100644
index 0000000..5c766ea
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
@@ -0,0 +1,426 @@
+
+/*
+Tipue Search 2.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+(function($) {
+
+ $.fn.tipuesearch = function(options) {
+
+ var set = $.extend( {
+
+ 'show' : 7,
+ 'newWindow' : false,
+ 'showURL' : true,
+ 'minimumLength' : 3,
+ 'descriptiveWords' : 25,
+ 'highlightTerms' : true,
+ 'highlightEveryTerm' : false,
+ 'mode' : 'static',
+ 'liveDescription' : '*',
+ 'liveContent' : '*',
+ 'contentLocation' : 'tipuesearch/tipuesearch_content.json'
+
+ }, options);
+
+ return this.each(function() {
+
+ var tipuesearch_in = {
+ pages: []
+ };
+ $.ajaxSetup({
+ async: false
+ });
+
+ if (set.mode == 'live')
+ {
+ for (var i = 0; i < tipuesearch_pages.length; i++)
+ {
+ $.get(tipuesearch_pages[i], '',
+ function (html)
+ {
+ var cont = $(set.liveContent, html).text();
+ 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)
+ {
+ var tit = html.slice(t_1 + 7, t_2);
+ }
+ else
+ {
+ var tit = 'No title';
+ }
+
+ tipuesearch_in.pages.push({
+ "title": tit,
+ "text": desc,
+ "tags": cont,
+ "loc": tipuesearch_pages[i]
+ });
+ }
+ );
+ }
+ }
+
+ if (set.mode == 'json')
+ {
+ $.getJSON(set.contentLocation,
+ function(json)
+ {
+ tipuesearch_in = $.extend({}, json);
+ }
+ );
+ }
+
+ if (set.mode == 'static' || set.mode == 'static-images')
+ {
+ tipuesearch_in = $.extend({}, tipuesearch);
+ }
+
+ var tipue_search_w = '';
+ if (set.newWindow)
+ {
+ tipue_search_w = ' target="_blank"';
+ }
+
+ function getURLP(name)
+ {
+ return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null;
+ }
+ if (getURLP('q'))
+ {
+ $('#tipue_search_input').val(getURLP('q'));
+ getTipueSearch(0, true);
+ }
+
+ $('#tipue_search_button').click(function()
+ {
+ getTipueSearch(0, true);
+ });
+ $(this).keyup(function(event)
+ {
+ if(event.keyCode == '13')
+ {
+ getTipueSearch(0, true);
+ }
+ });
+
+ function getTipueSearch(start, replace)
+ {
+ $('#tipue_search_content').hide();
+ var out = '';
+ 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(' ');
+ d = '';
+ for (var i = 0; i < d_w.length; i++)
+ {
+ var a_w = true;
+ for (var f = 0; f < tipuesearch_stop_words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stop_words[f])
+ {
+ a_w = false;
+ show_stop = true;
+ }
+ }
+ if (a_w)
+ {
+ d = d + ' ' + d_w[i];
+ }
+ }
+ d = $.trim(d);
+ d_w = d.split(' ');
+
+ if (d.length >= set.minimumLength)
+ {
+ if (replace)
+ {
+ var d_r = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_replace.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_replace.words[f].word)
+ {
+ d = d.replace(d_w[i], tipuesearch_replace.words[f].replace_with);
+ show_replace = true;
+ }
+ }
+ }
+ d_w = d.split(' ');
+ }
+
+ var d_t = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_stem.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stem.words[f].word)
+ {
+ d_t = d_t + ' ' + tipuesearch_stem.words[f].stem;
+ }
+ }
+ }
+ d_w = d_t.split(' ');
+
+ var c = 0;
+ found = new Array();
+ for (var i = 0; i < tipuesearch_in.pages.length; i++)
+ {
+ var score = 1000000000;
+ var s_t = tipuesearch_in.pages[i].text;
+ for (var f = 0; f < d_w.length; f++)
+ {
+ var pat = new RegExp(d_w[f], 'i');
+ if (tipuesearch_in.pages[i].title.search(pat) != -1)
+ {
+ score -= (200000 - i);
+ }
+ if (tipuesearch_in.pages[i].text.search(pat) != -1)
+ {
+ score -= (150000 - i);
+ }
+
+ if (set.highlightTerms)
+ {
+ if (set.highlightEveryTerm)
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'gi');
+ }
+ else
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'i');
+ }
+ s_t = s_t.replace(patr, "<em>$1</em>");
+ }
+
+ 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;
+ }
+ }
+ }
+
+ 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>';
+ }
+ if (c == 1)
+ {
+ out += '<div id="tipue_search_results_count">1 result</div>';
+ }
+ else
+ {
+ 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++)
+ {
+ var fo = found[i].split('^');
+ 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(' ');
+ if (t_w.length < set.descriptiveWords)
+ {
+ t_d = t;
+ }
+ else
+ {
+ for (var f = 0; f < set.descriptiveWords; f++)
+ {
+ t_d += t_w[f] + ' ';
+ }
+ }
+ t_d = $.trim(t_d);
+ if (t_d.charAt(t_d.length - 1) != '.')
+ {
+ 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>';
+ }
+ }
+ 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>';
+ }
+
+ if (page <= 4)
+ {
+ var p_b = pages;
+ if (pages > 5)
+ {
+ p_b = 5;
+ }
+ for (var f = 0; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
+ }
+ }
+ }
+ else
+ {
+ var p_b = pages + 4;
+ if (p_b > pages)
+ {
+ p_b = pages;
+ }
+ for (var f = page; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ 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 += '</ul></div>';
+ }
+ }
+ else
+ {
+ 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>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning_head">Search too short</div>';
+ if (set.minimumLength == 1)
+ {
+ out += '<div id="tipue_search_warning">Should be one character or more</div>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning">Should be ' + set.minimumLength + ' characters or more</div>';
+ }
+ }
+ }
+
+ $('#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);
+
+
+
+
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
new file mode 100644
index 0000000..8989c3c
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
@@ -0,0 +1,28 @@
+
+/*
+Tipue Search 2.0
+Copyright (c) 2012 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+var tipuesearch_stop_words = ["and", "be", "by", "do", "for", "he", "how", "if", "is", "it", "my", "not", "of", "or", "the", "to", "up", "what", "when"];
+
+var tipuesearch_replace = {"words": [
+ {"word": "tipua", replace_with: "tipue"},
+ {"word": "javscript", replace_with: "javascript"}
+]};
+
+var tipuesearch_stem = {"words": [
+ {"word": "e-mail", stem: "email"},
+ {"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
new file mode 100755
index 0000000..789fbe5
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/tipue_search.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+<head>
+<title>Tipue Search</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+
+<link rel="stylesheet" type="text/css" href="assets/css/tipuesearch.css">
+<script type="text/javascript" src="assets/js/tipuesearch_set.js"></script>
+<script type="text/javascript" src="assets/js/tipuesearch.js"></script>
+
+</head>
+<body>
+
+<div style="float: left;"><input type="text" id="tipue_search_input"></div>
+<div style="float: left; margin-left: 13px;"><input type="button" id="tipue_search_button"></div>
+<div id="tipue_search_content"><div id="tipue_search_loading"></div></div>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#tipue_search_input').tipuesearch({
+ 'mode': 'json',
+ 'contentLocation': 'assets/js/tipuesearch_content.json'
+ });
+});
+</script>
+</body>
+</html>
diff --git a/nikola/plugins/task_mustache.plugin b/nikola/plugins/task_mustache.plugin
new file mode 100644
index 0000000..6103936
--- /dev/null
+++ b/nikola/plugins/task_mustache.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_mustache
+Module = task_mustache
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+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
new file mode 100644
index 0000000..7364979
--- /dev/null
+++ b/nikola/plugins/task_mustache/__init__.py
@@ -0,0 +1,197 @@
+# 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 json
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed, copy_file, unicode_str
+
+
+class Mustache(Task):
+ """Render the blog posts as JSON data."""
+
+ name = "render_mustache"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "messages": self.site.MESSAGES,
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "blog_title": self.site.config['BLOG_TITLE'],
+ "content_footer": self.site.config['CONTENT_FOOTER'],
+ }
+
+ # TODO: timeline is global, get rid of it
+ posts = [x for x in self.site.timeline if x.use_in_feeds]
+ if not posts:
+ yield {
+ 'basename': 'render_mustache',
+ 'actions': [],
+ }
+ return
+
+ def write_file(path, post, lang):
+
+ # Prev/Next links
+ prev_link = False
+ if post.prev_post:
+ prev_link = post.prev_post.permalink(lang).replace(".html",
+ ".json")
+ next_link = False
+ if post.next_post:
+ next_link = post.next_post.permalink(lang).replace(".html",
+ ".json")
+ data = {}
+
+ # Configuration
+ for k, v in self.site.config.items():
+ if isinstance(v, (str, unicode_str)): # NOQA
+ data[k] = v
+
+ # Tag data
+ tags = []
+ for tag in post.tags:
+ tags.append({'name': tag, 'link': self.site.link("tag", tag,
+ lang)})
+ data.update({
+ "tags": tags,
+ "tags?": True if tags else False,
+ })
+
+ # Template strings
+ for k, v in kw["messages"][lang].items():
+ data["message_" + k] = v
+
+ # Post data
+ data.update({
+ "title": post.title(lang),
+ "text": post.text(lang),
+ "prev": prev_link,
+ "next": next_link,
+ "date":
+ 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)))
+
+ # Post translations
+ translations = []
+ for langname in kw["translations"]:
+ if langname == lang:
+ continue
+ translations.append({'name':
+ kw["messages"][langname]["Read in English"],
+ 'link': "javascript:load_data('%s');"
+ % post.permalink(langname).replace(
+ ".html", ".json")})
+ data["translations"] = translations
+
+ try:
+ os.makedirs(os.path.dirname(path))
+ except:
+ pass
+ with codecs.open(path, 'wb+', 'utf8') as fd:
+ fd.write(json.dumps(data))
+
+ for lang in kw["translations"]:
+ for i, post in enumerate(posts):
+ out_path = post.destination_path(lang, ".json")
+ out_file = os.path.join(kw['output_folder'], out_path)
+ task = {
+ 'basename': 'render_mustache',
+ 'name': out_file,
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [out_file],
+ 'actions': [(write_file, (out_file, post, lang))],
+ 'task_dep': ['render_posts'],
+ 'uptodate': [config_changed({
+ 1: post.text(lang),
+ 2: post.prev_post,
+ 3: post.next_post,
+ 4: post.title(lang),
+ })]
+ }
+ yield task
+
+ if posts:
+ first_post_data = posts[0].permalink(
+ self.site.config["DEFAULT_LANG"]).replace(".html", ".json")
+
+ # Copy mustache template
+ src = os.path.join(os.path.dirname(__file__), 'mustache-template.html')
+ dst = os.path.join(kw['output_folder'], 'mustache-template.html')
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'actions': [(copy_file, (src, dst))],
+ }
+
+ # Copy mustache.html with the right starting file in it
+ src = os.path.join(os.path.dirname(__file__), 'mustache.html')
+ dst = os.path.join(kw['output_folder'], 'mustache.html')
+
+ def copy_mustache():
+ with codecs.open(src, 'rb', 'utf8') as in_file:
+ with codecs.open(dst, 'wb+', 'utf8') as out_file:
+ data = in_file.read().replace('{{first_post_data}}',
+ first_post_data)
+ out_file.write(data)
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'uptodate': [config_changed({1: first_post_data})],
+ 'actions': [(copy_mustache, [])],
+ }
diff --git a/nikola/plugins/task_mustache/mustache-template.html b/nikola/plugins/task_mustache/mustache-template.html
new file mode 100644
index 0000000..7f2b34c
--- /dev/null
+++ b/nikola/plugins/task_mustache/mustache-template.html
@@ -0,0 +1,29 @@
+<script id="view" type="text/html">
+<div class="container" id="container">
+ <div class="postbox">
+ <h1>{{BLOG_TITLE}}</h1>
+ <hr>
+ <h2>{{title}}</h2>
+ Posted on: {{date}}</br>
+ {{#tags?}} More posts about:
+ {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}}
+ </br>
+ {{/tags?}}
+ {{#translations}}<a href={{link}}>{{name}}</a>{{/translations}}&nbsp;</br>
+ <hr>
+ {{{text}}}
+ <ul class="pager">
+ {{#prev}}
+ <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}}}
+ </div>
+ <div class="footerbox">
+ {{{CONTENT_FOOTER}}}
+ </div>
+</div>
+</script>
diff --git a/nikola/plugins/task_mustache/mustache.html b/nikola/plugins/task_mustache/mustache.html
new file mode 100644
index 0000000..5dbebef
--- /dev/null
+++ b/nikola/plugins/task_mustache/mustache.html
@@ -0,0 +1,36 @@
+<head>
+ <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css">
+ <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.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) {
+ $('body').mustache('view', data, { method: 'html' });
+ window.location.hash = '#' + dataurl;
+ })
+};
+$(document).ready(function() {
+$.Mustache.load('/mustache-template.html')
+ .done(function () {
+ if (window.location.hash != '') {
+ load_data(window.location.hash.slice(1));
+ }
+ else {
+ load_data('{{first_post_data}}');
+ };
+ })
+});
+</script>
+</head>
+<body style="padding-top: 0;">
+</body>
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task_redirect.py
index e440c30..2503bb7 100644
--- a/nikola/plugins/task_redirect.py
+++ b/nikola/plugins/task_redirect.py
@@ -57,7 +57,7 @@ class Redirect(Task):
src_path = os.path.join(kw["output_folder"], src)
yield {
'basename': self.name,
- 'name': src_path.encode('utf8'),
+ 'name': src_path,
'targets': [src_path],
'actions': [(create_redirect, (src_path, dst))],
'clean': True,
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
index 0880e3e..d4e4a3a 100644
--- a/nikola/plugins/task_render_galleries.py
+++ b/nikola/plugins/task_render_galleries.py
@@ -62,6 +62,7 @@ class Galleries(Task):
'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,
@@ -70,7 +71,7 @@ class Galleries(Task):
template_name = "gallery.tmpl"
gallery_list = []
- for root, dirs, files in os.walk('galleries'):
+ for root, dirs, files in os.walk(kw['gallery_path']):
gallery_list.append(root)
if not gallery_list:
yield {
@@ -95,7 +96,7 @@ class Galleries(Task):
if not os.path.isdir(output_gallery):
yield {
'basename': str('render_galleries'),
- 'name': str(output_gallery),
+ 'name': output_gallery,
'actions': [(os.makedirs, (output_gallery,))],
'targets': [output_gallery],
'clean': True,
@@ -152,7 +153,7 @@ class Galleries(Task):
thumbs.append(os.path.basename(thumb_path))
yield {
'basename': str('render_galleries'),
- 'name': thumb_path.encode('utf8'),
+ 'name': thumb_path,
'file_dep': [img],
'targets': [thumb_path],
'actions': [
@@ -164,7 +165,7 @@ class Galleries(Task):
}
yield {
'basename': str('render_galleries'),
- 'name': orig_dest_path.encode('utf8'),
+ 'name': orig_dest_path,
'file_dep': [img],
'targets': [orig_dest_path],
'actions': [
@@ -187,7 +188,7 @@ class Galleries(Task):
excluded_dest_path = os.path.join(output_gallery, img_name)
yield {
'basename': str('render_galleries_clean'),
- 'name': excluded_thumb_dest_path.encode('utf8'),
+ 'name': excluded_thumb_dest_path,
'file_dep': [exclude_path],
#'targets': [excluded_thumb_dest_path],
'actions': [
@@ -198,7 +199,7 @@ class Galleries(Task):
}
yield {
'basename': str('render_galleries_clean'),
- 'name': excluded_dest_path.encode('utf8'),
+ 'name': excluded_dest_path,
'file_dep': [exclude_path],
#'targets': [excluded_dest_path],
'actions': [
@@ -240,7 +241,7 @@ class Galleries(Task):
compile_html = self.site.get_compiler(index_path)
yield {
'basename': str('render_galleries'),
- 'name': index_dst_path.encode('utf-8'),
+ 'name': index_dst_path,
'file_dep': [index_path],
'targets': [index_dst_path],
'actions': [(compile_html, [index_path, index_dst_path])],
@@ -258,12 +259,11 @@ class Galleries(Task):
file_dep.append(index_dst_path)
else:
context['text'] = ''
- self.site.render_template(template_name, output_name.encode(
- 'utf8'), context)
+ self.site.render_template(template_name, output_name, context)
yield {
'basename': str('render_galleries'),
- 'name': output_name.encode('utf8'),
+ 'name': output_name,
'file_dep': file_dep,
'targets': [output_name],
'actions': [(render_gallery, (output_name, context,
@@ -303,8 +303,13 @@ class Galleries(Task):
break
- im.thumbnail(size, Image.ANTIALIAS)
- im.save(dst)
+ 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)
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task_render_listings.py
index b115a2f..0cadfd3 100644
--- a/nikola/plugins/task_render_listings.py
+++ b/nikola/plugins/task_render_listings.py
@@ -78,7 +78,7 @@ class Listings(Task):
'files': files,
'description': title,
}
- self.site.render_template('listing.tmpl', out_name.encode('utf8'),
+ self.site.render_template('listing.tmpl', out_name,
context)
flag = True
template_deps = self.site.template_system.template_deps('listing.tmpl')
@@ -91,14 +91,15 @@ class Listings(Task):
)
yield {
'basename': self.name,
- 'name': out_name.encode('utf8'),
+ 'name': out_name,
'file_dep': template_deps,
'targets': [out_name],
'actions': [(render_listing, [None, out_name, dirs, files])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])]
+ self.site.config['GLOBAL_CONTEXT'])],
+ 'clean': True,
}
for f in files:
ext = os.path.splitext(f)[-1]
@@ -111,14 +112,15 @@ class Listings(Task):
f) + '.html'
yield {
'basename': self.name,
- 'name': out_name.encode('utf8'),
+ 'name': out_name,
'file_dep': template_deps + [in_name],
'targets': [out_name],
'actions': [(render_listing, [in_name, out_name])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])]
+ self.site.config['GLOBAL_CONTEXT'])],
+ 'clean': True,
}
if flag:
yield {
diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task_render_pages.py
index 0145579..1883d7b 100644
--- a/nikola/plugins/task_render_pages.py
+++ b/nikola/plugins/task_render_pages.py
@@ -22,6 +22,7 @@
# 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
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -37,17 +38,21 @@ class RenderPages(Task):
"post_pages": self.site.config["post_pages"],
"translations": self.site.config["TRANSLATIONS"],
"filters": self.site.config["FILTERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
flag = False
for lang in kw["translations"]:
for post in self.site.timeline:
+ if kw["hide_untranslated_posts"] and not post.is_translation_available(lang):
+ continue
for task in self.site.generic_page_renderer(lang, post,
kw["filters"]):
task['uptodate'] = [config_changed({
1: task['uptodate'][0].config,
2: kw})]
task['basename'] = self.name
+ task['task_dep'] = ['render_posts']
flag = True
yield task
if flag is False: # No page rendered, yield a dummy task
diff --git a/nikola/plugins/task_render_posts.py b/nikola/plugins/task_render_posts.py
index a4d5578..4be68bf 100644
--- a/nikola/plugins/task_render_posts.py
+++ b/nikola/plugins/task_render_posts.py
@@ -23,10 +23,20 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from copy import copy
-import os
+import codecs
+import string
from nikola.plugin_categories import Task
-from nikola import utils
+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):
@@ -41,25 +51,26 @@ class RenderPosts(Task):
"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"]:
- # TODO: timeline is global, get rid of it
deps_dict = copy(kw)
deps_dict.pop('timeline')
for post in kw['timeline']:
source = post.source_path
dest = post.base_path
- if lang != kw["default_lang"]:
- dest += '.' + lang
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
+ 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
- yield {
+ task = {
'basename': self.name,
- 'name': dest.encode('utf-8'),
+ 'name': dest,
'file_dep': post.fragment_deps(lang),
'targets': [dest],
'actions': [(self.site.get_compiler(post.source_path),
@@ -67,6 +78,9 @@ class RenderPosts(Task):
'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,
@@ -74,3 +88,53 @@ class RenderPosts(Task):
'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_render_rss.py b/nikola/plugins/task_render_rss.py
index 9ce1d63..3000e47 100644
--- a/nikola/plugins/task_render_rss.py
+++ b/nikola/plugins/task_render_rss.py
@@ -43,25 +43,30 @@ class RenderRSS(Task):
"blog_description": self.site.config["BLOG_DESCRIPTION"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
- # TODO: timeline is global, kill it
for lang in kw["translations"]:
output_name = os.path.join(kw['output_folder'],
self.site.path("rss", None, lang))
deps = []
- posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
+ if kw["hide_untranslated_posts"]:
+ posts = [x for x in self.site.timeline if x.use_in_feeds
+ and x.is_translation_available(lang)][:10]
+ else:
+ posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
for post in posts:
deps += post.deps(lang)
yield {
'basename': 'render_rss',
- 'name': output_name.encode('utf8'),
+ '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"]))],
+ 'task_dep': ['render_posts'],
'clean': True,
'uptodate': [utils.config_changed(kw)],
}
diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task_render_sources.py
index 529e68e..392345c 100644
--- a/nikola/plugins/task_render_sources.py
+++ b/nikola/plugins/task_render_sources.py
@@ -53,6 +53,8 @@ class Sources(Task):
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()))
@@ -63,15 +65,16 @@ class Sources(Task):
source_lang = source + '.' + lang
if os.path.exists(source_lang):
source = source_lang
- yield {
- 'basename': 'render_sources',
- 'name': output_name.encode('utf8'),
- 'file_dep': [source],
- 'targets': [output_name],
- 'actions': [(utils.copy_file, (source, output_name))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
+ 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',
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task_render_tags.py
index 744f0cb..58a7ff3 100644
--- a/nikola/plugins/task_render_tags.py
+++ b/nikola/plugins/task_render_tags.py
@@ -52,6 +52,7 @@ class RenderTags(Task):
self.site.config['INDEX_DISPLAY_POST_COUNT'],
"index_teasers": self.site.config['INDEX_TEASERS'],
"rss_teasers": self.site.config["RSS_TEASERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
@@ -67,13 +68,17 @@ class RenderTags(Task):
post_list.sort(key=lambda a: a.date)
post_list.reverse()
for lang in kw["translations"]:
- yield self.tag_rss(tag, lang, posts, kw)
-
+ if kw["hide_untranslated_posts"]:
+ 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)
# Render HTML
if kw['tag_pages_are_indexes']:
- yield self.tag_page_as_index(tag, lang, post_list, kw)
+ yield self.tag_page_as_index(tag, lang, filtered_posts, kw)
else:
- yield self.tag_page_as_list(tag, lang, post_list, kw)
+ yield self.tag_page_as_list(tag, lang, filtered_posts, kw)
# Tag cloud json file
tag_cloud_data = {}
@@ -98,6 +103,7 @@ class RenderTags(Task):
task['uptodate'] = [utils.config_changed(tag_cloud_data)]
task['targets'] = [output_name]
task['actions'] = [(write_tag_data, [tag_cloud_data])]
+ task['clean'] = True
yield task
def list_tags_page(self, kw):
@@ -110,7 +116,7 @@ class RenderTags(Task):
for lang in kw["translations"]:
output_name = os.path.join(
kw['output_folder'], self.site.path('tag_index', None, lang))
- output_name = output_name.encode('utf8')
+ output_name = output_name
context = {}
context["title"] = kw["messages"][lang]["Tags"]
context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
@@ -157,7 +163,6 @@ class RenderTags(Task):
context['rss_link'] = rss_link
output_name = os.path.join(kw['output_folder'],
page_name(tag, i, lang))
- output_name = output_name.encode('utf8')
context["title"] = kw["messages"][lang][
"Posts about %s"] % tag
context["prevlink"] = None
@@ -192,7 +197,6 @@ class RenderTags(Task):
template_name = "tag.tmpl"
output_name = os.path.join(kw['output_folder'], self.site.path(
"tag", tag, lang))
- output_name = output_name.encode('utf8')
context = {}
context["lang"] = lang
context["title"] = kw["messages"][lang]["Posts about %s"] % tag
@@ -217,7 +221,6 @@ class RenderTags(Task):
#Render RSS
output_name = os.path.join(kw['output_folder'],
self.site.path("tag_rss", tag, lang))
- output_name = output_name.encode('utf8')
deps = []
post_list = [self.site.global_data[post] for post in posts if
self.site.global_data[post].use_in_feeds]
@@ -236,4 +239,5 @@ class RenderTags(Task):
output_name, kw["rss_teasers"]))],
'clean': True,
'uptodate': [utils.config_changed(kw)],
+ 'task_dep': ['render_posts'],
}
diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
index 9d89070..044e0e3 100644
--- a/nikola/plugins/task_sitemap/__init__.py
+++ b/nikola/plugins/task_sitemap/__init__.py
@@ -22,72 +22,82 @@
# 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
+from __future__ import print_function, absolute_import, unicode_literals
+import codecs
+import datetime
import os
-import sys
-import tempfile
+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
-from nikola.plugins.task_sitemap import sitemap_gen
+
+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):
- """Copy theme assets into output."""
+ """Generate google sitemap."""
name = "sitemap"
def gen_tasks(self):
- if sys.version_info[0] == 3:
- print("sitemap generation is not available for python 3")
- yield {
- 'basename': 'sitemap',
- 'name': 'sitemap',
- 'actions': [],
- }
- return
"""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 = os.path.abspath(kw['output_folder'])
- sitemap_path = os.path.join(output_path, "sitemap.xml.gz")
+ output_path = kw['output_folder']
+ sitemap_path = os.path.join(output_path, "sitemap.xml")
def sitemap():
- # Generate config
- config_data = """<?xml version="1.0" encoding="UTF-8"?>
- <site
- base_url="{0}"
- store_into="{1}"
- verbose="1" >
- <directory path="{2}" url="{3}" />
- <filter action="drop" type="wildcard" pattern="*~" />
- <filter action="drop" type="regexp" pattern="/\.[^/]*" />
- </site>""".format(kw["site_url"], sitemap_path, output_path,
- kw["base_url"])
- config_file = tempfile.NamedTemporaryFile(delete=False)
- config_file.write(config_data.encode('utf8'))
- config_file.close()
+ 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)
- # Generate sitemap
- sitemap = sitemap_gen.CreateSitemapFromFile(config_file.name, True)
- if not sitemap:
- sitemap_gen.output.Log('Configuration file errors -- exiting.',
- 0)
- else:
- sitemap.Generate()
- sitemap_gen.output.Log('Number of errors: {0}'.format(
- sitemap_gen.output.num_errors), 1)
- sitemap_gen.output.Log('Number of warnings: {0}'.format(
- sitemap_gen.output.num_warns), 1)
- os.unlink(config_file.name)
+ for k in sorted(locs.keys()):
+ outf.write(locs[k])
+ outf.write("</urlset>")
yield {
"basename": "sitemap",
- "name": os.path.join(kw['output_folder'], "sitemap.xml.gz"),
+ "name": sitemap_path,
"targets": [sitemap_path],
"actions": [(sitemap,)],
"uptodate": [config_changed(kw)],
diff --git a/nikola/plugins/task_sitemap/sitemap_gen.py b/nikola/plugins/task_sitemap/sitemap_gen.py
deleted file mode 100644
index 898325a..0000000
--- a/nikola/plugins/task_sitemap/sitemap_gen.py
+++ /dev/null
@@ -1,2137 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (c) 2004, 2005 Google Inc.
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in
-# the documentation and/or other materials provided with the
-# distribution.
-#
-# * Neither the name of Google nor the names of its contributors may
-# be used to endorse or promote products derived from this software
-# without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-#
-# The sitemap_gen.py script is written in Python 2.2 and released to
-# the open source community for continuous improvements under the BSD
-# 2.0 new license, which can be found at:
-#
-# http://www.opensource.org/licenses/bsd-license.php
-#
-from __future__ import print_function
-
-__usage__ = \
- """A simple script to automatically produce sitemaps for a webserver,
-in the Google Sitemap Protocol (GSP).
-
-Usage: python sitemap_gen.py --config=config.xml [--help] [--testing]
- --config=config.xml, specifies config file location
- --help, displays usage message
- --testing, specified when user is experimenting
-"""
-
-import fnmatch
-import glob
-import gzip
-import os
-import re
-import stat
-import sys
-import time
-import urllib
-import xml.sax
-
-try:
- import md5
-except ImportError:
- md5 = None # NOQA
- import hashlib
-
-try:
- from urlparse import urlsplit, urlunsplit, urljoin
-except ImportError:
- from urllib.parse import urlsplit, urlunsplit, urljoin # NOQA
-
-try:
- from urllib import quote as urllib_quote
- from urllib import FancyURLopener
- from urllib import urlopen
-except ImportError:
- from urllib.parse import quote as urllib_quote # NOQA
- from urllib.request import FancyURLopener # NOQA
- from urllib.request import urlopen # NOQA
-
-
-if sys.version_info[0] == 3:
- # Python 3
- bytes_str = bytes
- unicode_str = str
- unichr = chr
-else:
- bytes_str = str
- unicode_str = unicode # NOQA
-
-# Text encodings
-ENC_ASCII = 'ASCII'
-ENC_UTF8 = 'UTF-8'
-ENC_IDNA = 'IDNA'
-ENC_ASCII_LIST = ['ASCII', 'US-ASCII', 'US', 'IBM367', 'CP367', 'ISO646-US'
- 'ISO_646.IRV:1991', 'ISO-IR-6', 'ANSI_X3.4-1968',
- 'ANSI_X3.4-1986', 'CPASCII']
-ENC_DEFAULT_LIST = ['ISO-8859-1', 'ISO-8859-2', 'ISO-8859-5']
-
-# Available Sitemap types
-SITEMAP_TYPES = ['web', 'mobile', 'news']
-
-# General Sitemap tags
-GENERAL_SITEMAP_TAGS = ['loc', 'changefreq', 'priority', 'lastmod']
-
-# News specific tags
-NEWS_SPECIFIC_TAGS = ['keywords', 'publication_date', 'stock_tickers']
-
-# News Sitemap tags
-NEWS_SITEMAP_TAGS = GENERAL_SITEMAP_TAGS + NEWS_SPECIFIC_TAGS
-
-# Maximum number of urls in each sitemap, before next Sitemap is created
-MAXURLS_PER_SITEMAP = 50000
-
-# Suffix on a Sitemap index file
-SITEINDEX_SUFFIX = '_index.xml'
-
-# Regular expressions tried for extracting URLs from access logs.
-ACCESSLOG_CLF_PATTERN = re.compile(
- r'.+\s+"([^\s]+)\s+([^\s]+)\s+HTTP/\d+\.\d+"\s+200\s+.*'
-)
-
-# Match patterns for lastmod attributes
-DATE_PATTERNS = list(map(re.compile, [
- r'^\d\d\d\d$',
- r'^\d\d\d\d-\d\d$',
- r'^\d\d\d\d-\d\d-\d\d$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\dZ$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d[+-]\d\d:\d\d$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?[+-]\d\d:\d\d$',
-]))
-
-# Match patterns for changefreq attributes
-CHANGEFREQ_PATTERNS = [
- 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'
-]
-
-# XML formats
-GENERAL_SITEINDEX_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<sitemapindex\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'siteindex.xsd">\n'
-
-NEWS_SITEINDEX_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<sitemapindex\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'siteindex.xsd">\n'
-
-SITEINDEX_FOOTER = '</sitemapindex>\n'
-SITEINDEX_ENTRY = \
- ' <sitemap>\n' \
- ' <loc>%(loc)s</loc>\n' \
- ' <lastmod>%(lastmod)s</lastmod>\n' \
- ' </sitemap>\n'
-GENERAL_SITEMAP_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<urlset\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'sitemap.xsd">\n'
-
-NEWS_SITEMAP_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<urlset\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'sitemap.xsd">\n'
-
-SITEMAP_FOOTER = '</urlset>\n'
-SITEURL_XML_PREFIX = ' <url>\n'
-SITEURL_XML_SUFFIX = ' </url>\n'
-
-NEWS_TAG_XML_PREFIX = ' <news:news>\n'
-NEWS_TAG_XML_SUFFIX = ' </news:news>\n'
-
-# Search engines to notify with the updated sitemaps
-#
-# This list is very non-obvious in what's going on. Here's the gist:
-# Each item in the list is a 6-tuple of items. The first 5 are "almost"
-# the same as the input arguments to urlparse.urlunsplit():
-# 0 - schema
-# 1 - netloc
-# 2 - path
-# 3 - query <-- EXCEPTION: specify a query map rather than a string
-# 4 - fragment
-# Additionally, add item 5:
-# 5 - query attribute that should be set to the new Sitemap URL
-# Clear as mud, I know.
-NOTIFICATION_SITES = [
- ('http', 'www.google.com', 'webmasters/sitemaps/ping', {}, '', 'sitemap'),
-]
-
-
-def get_hash(text):
- if md5 is not None:
- return md5.new(text).digest()
- else:
- m = hashlib.md5()
- m.update(text.encode('utf8'))
- return m.digest()
-
-
-class Error(Exception):
- """
- Base exception class. In this module we tend not to use our own exception
- types for very much, but they come in very handy on XML parsing with SAX.
- """
- pass
-# end class Error
-
-
-class SchemaError(Error):
- """Failure to process an XML file according to the schema we know."""
- pass
-# end class SchemeError
-
-
-class Encoder:
- """
- Manages wide-character/narrow-character conversions for just about all
- text that flows into or out of the script.
-
- You should always use this class for string coercion, as opposed to
- letting Python handle coercions automatically. Reason: Python
- usually assumes ASCII (7-bit) as a default narrow character encoding,
- which is not the kind of data we generally deal with.
-
- General high-level methodologies used in sitemap_gen:
-
- [PATHS]
- File system paths may be wide or narrow, depending on platform.
- This works fine, just be aware of it and be very careful to not
- mix them. That is, if you have to pass several file path arguments
- into a library call, make sure they are all narrow or all wide.
- This class has MaybeNarrowPath() which should be called on every
- file system path you deal with.
-
- [URLS]
- URL locations are stored in Narrow form, already escaped. This has the
- benefit of keeping escaping and encoding as close as possible to the format
- we read them in. The downside is we may end up with URLs that have
- intermingled encodings -- the root path may be encoded in one way
- while the filename is encoded in another. This is obviously wrong, but
- it should hopefully be an issue hit by very few users. The workaround
- from the user level (assuming they notice) is to specify a default_encoding
- parameter in their config file.
-
- [OTHER]
- Other text, such as attributes of the URL class, configuration options,
- etc, are generally stored in Unicode for simplicity.
- """
-
- def __init__(self):
- self._user = None # User-specified default encoding
- self._learned = [] # Learned default encodings
- self._widefiles = False # File system can be wide
-
- # Can the file system be Unicode?
- try:
- self._widefiles = os.path.supports_unicode_filenames
- except AttributeError:
- try:
- self._widefiles = sys.getwindowsversion(
- ) == os.VER_PLATFORM_WIN32_NT
- except AttributeError:
- pass
-
- # Try to guess a working default
- try:
- encoding = sys.getfilesystemencoding()
- if encoding and not (encoding.upper() in ENC_ASCII_LIST):
- self._learned = [encoding]
- except AttributeError:
- pass
-
- if not self._learned:
- encoding = sys.getdefaultencoding()
- if encoding and not (encoding.upper() in ENC_ASCII_LIST):
- self._learned = [encoding]
-
- # If we had no guesses, start with some European defaults
- if not self._learned:
- self._learned = ENC_DEFAULT_LIST
- # end def __init__
-
- def SetUserEncoding(self, encoding):
- self._user = encoding
- # end def SetUserEncoding
-
- def NarrowText(self, text, encoding):
- """ Narrow a piece of arbitrary text """
- if isinstance(text, bytes_str):
- return text
-
- # Try the passed in preference
- if encoding:
- try:
- result = text.encode(encoding)
- if not encoding in self._learned:
- self._learned.append(encoding)
- return result
- except UnicodeError:
- pass
- except LookupError:
- output.Warn('Unknown encoding: %s' % encoding)
-
- # Try the user preference
- if self._user:
- try:
- return text.encode(self._user)
- except UnicodeError:
- pass
- except LookupError:
- temp = self._user
- self._user = None
- output.Warn('Unknown default_encoding: %s' % temp)
-
- # Look through learned defaults, knock any failing ones out of the list
- while self._learned:
- try:
- return text.encode(self._learned[0])
- except:
- del self._learned[0]
-
- # When all other defaults are exhausted, use UTF-8
- try:
- return text.encode(ENC_UTF8)
- except UnicodeError:
- pass
-
- # Something is seriously wrong if we get to here
- return text.encode(ENC_ASCII, 'ignore')
- # end def NarrowText
-
- def MaybeNarrowPath(self, text):
- """ Paths may be allowed to stay wide """
- if self._widefiles:
- return text
- return self.NarrowText(text, None)
- # end def MaybeNarrowPath
-
- def WidenText(self, text, encoding):
- """ Widen a piece of arbitrary text """
- if not isinstance(text, bytes_str):
- return text
-
- # Try the passed in preference
- if encoding:
- try:
- result = unicode_str(text, encoding)
- if not encoding in self._learned:
- self._learned.append(encoding)
- return result
- except UnicodeError:
- pass
- except LookupError:
- output.Warn('Unknown encoding: %s' % encoding)
-
- # Try the user preference
- if self._user:
- try:
- return unicode_str(text, self._user)
- except UnicodeError:
- pass
- except LookupError:
- temp = self._user
- self._user = None
- output.Warn('Unknown default_encoding: %s' % temp)
-
- # Look through learned defaults, knock any failing ones out of the list
- while self._learned:
- try:
- return unicode_str(text, self._learned[0])
- except:
- del self._learned[0]
-
- # When all other defaults are exhausted, use UTF-8
- try:
- return unicode_str(text, ENC_UTF8)
- except UnicodeError:
- pass
-
- # Getting here means it wasn't UTF-8 and we had no working default.
- # We really don't have anything "right" we can do anymore.
- output.Warn('Unrecognized encoding in text: %s' % text)
- if not self._user:
- output.Warn('You may need to set a default_encoding in your '
- 'configuration file.')
- return text.decode(ENC_ASCII, 'ignore')
- # end def WidenText
-# end class Encoder
-encoder = Encoder()
-
-
-class Output:
- """
- Exposes logging functionality, and tracks how many errors
- we have thus output.
-
- Logging levels should be used as thus:
- Fatal -- extremely sparingly
- Error -- config errors, entire blocks of user 'intention' lost
- Warn -- individual URLs lost
- Log(,0) -- Un-suppressable text that's not an error
- Log(,1) -- touched files, major actions
- Log(,2) -- parsing notes, filtered or duplicated URLs
- Log(,3) -- each accepted URL
- """
-
- def __init__(self):
- self.num_errors = 0 # Count of errors
- self.num_warns = 0 # Count of warnings
-
- self._errors_shown = {} # Shown errors
- self._warns_shown = {} # Shown warnings
- self._verbose = 0 # Level of verbosity
- # end def __init__
-
- def Log(self, text, level):
- """ Output a blurb of diagnostic text, if the verbose level allows it """
- if text:
- text = encoder.NarrowText(text, None)
- if self._verbose >= level:
- print(text)
- # end def Log
-
- def Warn(self, text):
- """ Output and count a warning. Suppress duplicate warnings. """
- if text:
- text = encoder.NarrowText(text, None)
- hash = get_hash(text)
- if not hash in self._warns_shown:
- self._warns_shown[hash] = 1
- print('[WARNING] ' + text)
- else:
- self.Log('(suppressed) [WARNING] ' + text, 3)
- self.num_warns = self.num_warns + 1
- # end def Warn
-
- def Error(self, text):
- """ Output and count an error. Suppress duplicate errors. """
- if text:
- text = encoder.NarrowText(text, None)
- hash = get_hash(text)
- if not hash in self._errors_shown:
- self._errors_shown[hash] = 1
- print('[ERROR] ' + text)
- else:
- self.Log('(suppressed) [ERROR] ' + text, 3)
- self.num_errors = self.num_errors + 1
- # end def Error
-
- def Fatal(self, text):
- """ Output an error and terminate the program. """
- if text:
- text = encoder.NarrowText(text, None)
- print('[FATAL] ' + text)
- else:
- print('Fatal error.')
- sys.exit(1)
- # end def Fatal
-
- def SetVerbose(self, level):
- """ Sets the verbose level. """
- try:
- if not isinstance(level, int):
- level = int(level)
- if (level >= 0) and (level <= 3):
- self._verbose = level
- return
- except ValueError:
- pass
- self.Error(
- 'Verbose level (%s) must be between 0 and 3 inclusive.' % level)
- # end def SetVerbose
-# end class Output
-output = Output()
-
-
-class URL(object):
- """ URL is a smart structure grouping together the properties we
- care about for a single web reference. """
- __slots__ = 'loc', 'lastmod', 'changefreq', 'priority'
-
- def __init__(self):
- self.loc = None # URL -- in Narrow characters
- self.lastmod = None # ISO8601 timestamp of last modify
- self.changefreq = None # Text term for update frequency
- self.priority = None # Float between 0 and 1 (inc)
- # end def __init__
-
- def __cmp__(self, other):
- if self.loc < other.loc:
- return -1
- if self.loc > other.loc:
- return 1
- return 0
- # end def __cmp__
-
- def TrySetAttribute(self, attribute, value):
- """ Attempt to set the attribute to the value, with a pretty try
- block around it. """
- if attribute == 'loc':
- self.loc = self.Canonicalize(value)
- else:
- try:
- setattr(self, attribute, value)
- except AttributeError:
- output.Warn('Unknown URL attribute: %s' % attribute)
- # end def TrySetAttribute
-
- def IsAbsolute(loc):
- """ Decide if the URL is absolute or not """
- if not loc:
- return False
- narrow = encoder.NarrowText(loc, None)
- (scheme, netloc, path, query, frag) = urlsplit(narrow)
- if (not scheme) or (not netloc):
- return False
- return True
- # end def IsAbsolute
- IsAbsolute = staticmethod(IsAbsolute)
-
- def Canonicalize(loc):
- """ Do encoding and canonicalization on a URL string """
- if not loc:
- return loc
-
- # Let the encoder try to narrow it
- narrow = encoder.NarrowText(loc, None)
-
- # Escape components individually
- (scheme, netloc, path, query, frag) = urlsplit(narrow)
- unr = '-._~'
- sub = '!$&\'()*+,;='
- netloc = urllib_quote(netloc, unr + sub + '%:@/[]')
- path = urllib_quote(path, unr + sub + '%:@/')
- query = urllib_quote(query, unr + sub + '%:@/?')
- frag = urllib_quote(frag, unr + sub + '%:@/?')
-
- # Try built-in IDNA encoding on the netloc
- try:
- (ignore, widenetloc, ignore, ignore, ignore) = urlsplit(loc)
- for c in widenetloc:
- if c >= unichr(128):
- netloc = widenetloc.encode(ENC_IDNA)
- netloc = urllib_quote(netloc, unr + sub + '%:@/[]')
- break
- except UnicodeError:
- # urlsplit must have failed, based on implementation differences in the
- # library. There is not much we can do here, except ignore it.
- pass
- except LookupError:
- output.Warn('An International Domain Name (IDN) is being used, but this '
- 'version of Python does not have support for IDNA encoding. '
- ' (IDNA support was introduced in Python 2.3) The encoding '
- 'we have used instead is wrong and will probably not yield '
- 'valid URLs.')
- bad_netloc = False
- if '%' in netloc:
- bad_netloc = True
-
- # Put it all back together
- narrow = urlunsplit((scheme, netloc, path, query, frag))
-
- # I let '%' through. Fix any that aren't pre-existing escapes.
- HEXDIG = '0123456789abcdefABCDEF'
- list = narrow.split('%')
- narrow = list[0]
- del list[0]
- for item in list:
- if (len(item) >= 2) and (item[0] in HEXDIG) and (item[1] in HEXDIG):
- narrow = narrow + '%' + item
- else:
- narrow = narrow + '%25' + item
-
- # Issue a warning if this is a bad URL
- if bad_netloc:
- output.Warn('Invalid characters in the host or domain portion of a URL: '
- + narrow)
-
- return narrow
- # end def Canonicalize
- Canonicalize = staticmethod(Canonicalize)
-
- def VerifyDate(self, date, metatag):
- """Verify the date format is valid"""
- match = False
- if date:
- date = date.upper()
- for pattern in DATE_PATTERNS:
- match = pattern.match(date)
- if match:
- return True
- if not match:
- output.Warn('The value for %s does not appear to be in ISO8601 '
- 'format on URL: %s' % (metatag, self.loc))
- return False
- # end of VerifyDate
-
- def Validate(self, base_url, allow_fragment):
- """ Verify the data in this URL is well-formed, and override if not. """
- assert isinstance(base_url, bytes_str)
-
- # Test (and normalize) the ref
- if not self.loc:
- output.Warn('Empty URL')
- return False
- if allow_fragment:
- self.loc = urljoin(base_url, self.loc)
- if not self.loc.startswith(base_url):
- output.Warn('Discarded URL for not starting with the base_url: %s' %
- self.loc)
- self.loc = None
- return False
-
- # Test the lastmod
- if self.lastmod:
- if not self.VerifyDate(self.lastmod, "lastmod"):
- self.lastmod = None
-
- # Test the changefreq
- if self.changefreq:
- match = False
- self.changefreq = self.changefreq.lower()
- for pattern in CHANGEFREQ_PATTERNS:
- if self.changefreq == pattern:
- match = True
- break
- if not match:
- output.Warn('Changefreq "%s" is not a valid change frequency on URL '
- ': %s' % (self.changefreq, self.loc))
- self.changefreq = None
-
- # Test the priority
- if self.priority:
- priority = -1.0
- try:
- priority = float(self.priority)
- except ValueError:
- pass
- if (priority < 0.0) or (priority > 1.0):
- output.Warn('Priority "%s" is not a number between 0 and 1 inclusive '
- 'on URL: %s' % (self.priority, self.loc))
- self.priority = None
-
- return True
- # end def Validate
-
- def MakeHash(self):
- """ Provides a uniform way of hashing URLs """
- if not self.loc:
- return None
- if self.loc.endswith('/'):
- return get_hash(self.loc[:-1])
- return get_hash(self.loc)
- # end def MakeHash
-
- def Log(self, prefix='URL', level=3):
- """ Dump the contents, empty or not, to the log. """
- out = prefix + ':'
-
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if not value:
- value = ''
- out = out + (' %s=[%s]' % (attribute, value))
-
- output.Log('%s' % encoder.NarrowText(out, None), level)
- # end def Log
-
- def WriteXML(self, file):
- """ Dump non-empty contents to the output file, in XML format. """
- if not self.loc:
- return
- out = SITEURL_XML_PREFIX
-
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if value:
- if isinstance(value, unicode_str):
- value = encoder.NarrowText(value, None)
- elif not isinstance(value, bytes_str):
- value = str(value)
- value = xml.sax.saxutils.escape(value)
- out = out + (' <%s>%s</%s>\n' % (attribute, value, attribute))
-
- out = out + SITEURL_XML_SUFFIX
- file.write(out)
- # end def WriteXML
-# end class URL
-
-
-class NewsURL(URL):
- """ NewsURL is a subclass of URL with News-Sitemap specific properties. """
- __slots__ = 'loc', 'lastmod', 'changefreq', 'priority', 'publication_date', \
- 'keywords', 'stock_tickers'
-
- def __init__(self):
- URL.__init__(self)
- self.publication_date = None # ISO8601 timestamp of publication date
- self.keywords = None # Text keywords
- self.stock_tickers = None # Text stock
- # end def __init__
-
- def Validate(self, base_url, allow_fragment):
- """ Verify the data in this News URL is well-formed, and override if not. """
- assert isinstance(base_url, bytes_str)
-
- if not URL.Validate(self, base_url, allow_fragment):
- return False
-
- if not URL.VerifyDate(self, self.publication_date, "publication_date"):
- self.publication_date = None
-
- return True
- # end def Validate
-
- def WriteXML(self, file):
- """ Dump non-empty contents to the output file, in XML format. """
- if not self.loc:
- return
- out = SITEURL_XML_PREFIX
-
- # printed_news_tag indicates if news-specific metatags are present
- printed_news_tag = False
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if value:
- if isinstance(value, unicode_str):
- value = encoder.NarrowText(value, None)
- elif not isinstance(value, bytes_str):
- value = str(value)
- value = xml.sax.saxutils.escape(value)
- if attribute in NEWS_SPECIFIC_TAGS:
- if not printed_news_tag:
- printed_news_tag = True
- out = out + NEWS_TAG_XML_PREFIX
- out = out + (' <news:%s>%s</news:%s>\n' %
- (attribute, value, attribute))
- else:
- out = out + (' <%s>%s</%s>\n' % (
- attribute, value, attribute))
-
- if printed_news_tag:
- out = out + NEWS_TAG_XML_SUFFIX
- out = out + SITEURL_XML_SUFFIX
- file.write(out)
- # end def WriteXML
-# end class NewsURL
-
-
-class Filter:
- """
- A filter on the stream of URLs we find. A filter is, in essence,
- a wildcard applied to the stream. You can think of this as an
- operator that returns a tri-state when given a URL:
-
- True -- this URL is to be included in the sitemap
- None -- this URL is undecided
- False -- this URL is to be dropped from the sitemap
- """
-
- def __init__(self, attributes):
- self._wildcard = None # Pattern for wildcard match
- self._regexp = None # Pattern for regexp match
- self._pass = False # "Drop" filter vs. "Pass" filter
-
- if not ValidateAttributes('FILTER', attributes,
- ('pattern', 'type', 'action')):
- return
-
- # Check error count on the way in
- num_errors = output.num_errors
-
- # Fetch the attributes
- pattern = attributes.get('pattern')
- type = attributes.get('type', 'wildcard')
- action = attributes.get('action', 'drop')
- if type:
- type = type.lower()
- if action:
- action = action.lower()
-
- # Verify the attributes
- if not pattern:
- output.Error('On a filter you must specify a "pattern" to match')
- elif (not type) or ((type != 'wildcard') and (type != 'regexp')):
- output.Error('On a filter you must specify either \'type="wildcard"\' '
- 'or \'type="regexp"\'')
- elif (action != 'pass') and (action != 'drop'):
- output.Error('If you specify a filter action, it must be either '
- '\'action="pass"\' or \'action="drop"\'')
-
- # Set the rule
- if action == 'drop':
- self._pass = False
- elif action == 'pass':
- self._pass = True
-
- if type == 'wildcard':
- self._wildcard = pattern
- elif type == 'regexp':
- try:
- self._regexp = re.compile(pattern)
- except re.error:
- output.Error('Bad regular expression: %s' % pattern)
-
- # Log the final results iff we didn't add any errors
- if num_errors == output.num_errors:
- output.Log('Filter: %s any URL that matches %s "%s"' %
- (action, type, pattern), 2)
- # end def __init__
-
- def Apply(self, url):
- """ Process the URL, as above. """
- if (not url) or (not url.loc):
- return None
-
- if self._wildcard:
- if fnmatch.fnmatchcase(url.loc, self._wildcard):
- return self._pass
- return None
-
- if self._regexp:
- if self._regexp.search(url.loc):
- return self._pass
- return None
-
- assert False # unreachable
- # end def Apply
-# end class Filter
-
-
-class InputURL:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a single URL, manually specified in the config file.
- """
-
- def __init__(self, attributes):
- self._url = None # The lonely URL
-
- if not ValidateAttributes('URL', attributes,
- ('href', 'lastmod', 'changefreq', 'priority')):
- return
-
- url = URL()
- for attr in attributes.keys():
- if attr == 'href':
- url.TrySetAttribute('loc', attributes[attr])
- else:
- url.TrySetAttribute(attr, attributes[attr])
-
- if not url.loc:
- output.Error('Url entries must have an href attribute.')
- return
-
- self._url = url
- output.Log('Input: From URL "%s"' % self._url.loc, 2)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
- if self._url:
- consumer(self._url, True)
- # end def ProduceURLs
-# end class InputURL
-
-
-class InputURLList:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a text file with a list of URLs
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
-
- if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From URLLIST "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Urllist entries must have a "path" attribute.')
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'URLLIST')
- if not file:
- return
-
- # Iterate lines
- linenum = 0
- for line in file.readlines():
- linenum = linenum + 1
-
- # Strip comments and empty lines
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
- if (not line) or line[0] == '#':
- continue
-
- # Split the line on space
- url = URL()
- cols = line.split(' ')
- for i in range(0, len(cols)):
- cols[i] = cols[i].strip()
- url.TrySetAttribute('loc', cols[0])
-
- # Extract attributes from the other columns
- for i in range(1, len(cols)):
- if cols[i]:
- try:
- (attr_name, attr_val) = cols[i].split('=', 1)
- url.TrySetAttribute(attr_name, attr_val)
- except ValueError:
- output.Warn('Line %d: Unable to parse attribute: %s' %
- (linenum, cols[i]))
-
- # Pass it on
- consumer(url, False)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputURLList
-
-
-class InputNewsURLList:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a text file with a list of News URLs and their metadata
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
- self._tag_order = [] # Order of URL metadata
-
- if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding', 'tag_order')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- self._tag_order = attributes.get('tag_order')
-
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From URLLIST "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Urllist entries must have a "path" attribute.')
-
- # parse tag_order into an array
- # tag_order_ascii created for more readable logging
- tag_order_ascii = []
- if self._tag_order:
- self._tag_order = self._tag_order.split(",")
- for i in range(0, len(self._tag_order)):
- element = self._tag_order[i].strip().lower()
- self._tag_order[i] = element
- tag_order_ascii.append(element.encode('ascii'))
- output.Log(
- 'Input: From URLLIST tag order is "%s"' % tag_order_ascii, 0)
- else:
- output.Error('News Urllist configuration file must contain tag_order '
- 'to define Sitemap metatags.')
-
- # verify all tag_order inputs are valid
- tag_order_dict = {}
- for tag in self._tag_order:
- tag_order_dict[tag] = ""
- if not ValidateAttributes('URLLIST', tag_order_dict,
- NEWS_SITEMAP_TAGS):
- return
-
- # loc tag must be present
- loc_tag = False
- for tag in self._tag_order:
- if tag == 'loc':
- loc_tag = True
- break
- if not loc_tag:
- output.Error('News Urllist tag_order in configuration file '
- 'does not contain "loc" value: %s' % tag_order_ascii)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'URLLIST')
- if not file:
- return
-
- # Iterate lines
- linenum = 0
- for line in file.readlines():
- linenum = linenum + 1
-
- # Strip comments and empty lines
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
- if (not line) or line[0] == '#':
- continue
-
- # Split the line on tabs
- url = NewsURL()
- cols = line.split('\t')
- for i in range(0, len(cols)):
- cols[i] = cols[i].strip()
-
- for i in range(0, len(cols)):
- if cols[i]:
- attr_value = cols[i]
- if i < len(self._tag_order):
- attr_name = self._tag_order[i]
- try:
- url.TrySetAttribute(attr_name, attr_value)
- except ValueError:
- output.Warn('Line %d: Unable to parse attribute: %s' %
- (linenum, cols[i]))
-
- # Pass it on
- consumer(url, False)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputNewsURLList
-
-
-class InputDirectory:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a directory that acts as base for walking the filesystem.
- """
-
- def __init__(self, attributes, base_url):
- self._path = None # The directory
- self._url = None # The URL equivalent
- self._default_file = None
- self._remove_empty_directories = False
-
- if not ValidateAttributes('DIRECTORY', attributes, ('path', 'url',
- 'default_file', 'remove_empty_directories')):
- return
-
- # Prep the path -- it MUST end in a sep
- path = attributes.get('path')
- if not path:
- output.Error('Directory entries must have both "path" and "url" '
- 'attributes')
- return
- path = encoder.MaybeNarrowPath(path)
- if not path.endswith(os.sep):
- path = path + os.sep
- if not os.path.isdir(path):
- output.Error('Can not locate directory: %s' % path)
- return
-
- # Prep the URL -- it MUST end in a sep
- url = attributes.get('url')
- if not url:
- output.Error('Directory entries must have both "path" and "url" '
- 'attributes')
- return
- url = URL.Canonicalize(url)
- if not url.endswith('/'):
- url = url + '/'
- if not url.startswith(base_url):
- url = urljoin(base_url, url)
- if not url.startswith(base_url):
- output.Error('The directory URL "%s" is not relative to the '
- 'base_url: %s' % (url, base_url))
- return
-
- # Prep the default file -- it MUST be just a filename
- file = attributes.get('default_file')
- if file:
- file = encoder.MaybeNarrowPath(file)
- if os.sep in file:
- output.Error('The default_file "%s" can not include path information.'
- % file)
- file = None
-
- # Prep the remove_empty_directories -- default is false
- remove_empty_directories = attributes.get('remove_empty_directories')
- if remove_empty_directories:
- if (remove_empty_directories == '1') or \
- (remove_empty_directories.lower() == 'true'):
- remove_empty_directories = True
- elif (remove_empty_directories == '0') or \
- (remove_empty_directories.lower() == 'false'):
- remove_empty_directories = False
- # otherwise the user set a non-default value
- else:
- output.Error('Configuration file remove_empty_directories '
- 'value is not recognized. Value must be true or false.')
- return
- else:
- remove_empty_directories = False
-
- self._path = path
- self._url = url
- self._default_file = file
- self._remove_empty_directories = remove_empty_directories
-
- if file:
- output.Log('Input: From DIRECTORY "%s" (%s) with default file "%s"'
- % (path, url, file), 2)
- else:
- output.Log('Input: From DIRECTORY "%s" (%s) with no default file'
- % (path, url), 2)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
- if not self._path:
- return
-
- root_path = self._path
- root_URL = self._url
- root_file = self._default_file
- remove_empty_directories = self._remove_empty_directories
-
- def HasReadPermissions(path):
- """ Verifies a given path has read permissions. """
- stat_info = os.stat(path)
- mode = stat_info[stat.ST_MODE]
- if mode & stat.S_IREAD:
- return True
- else:
- return None
-
- def PerFile(dirpath, name):
- """
- Called once per file.
- Note that 'name' will occasionally be None -- for a directory itself
- """
- # Pull a timestamp
- url = URL()
- isdir = False
- try:
- if name:
- path = os.path.join(dirpath, name)
- else:
- path = dirpath
- isdir = os.path.isdir(path)
- time = None
- if isdir and root_file:
- file = os.path.join(path, root_file)
- try:
- time = os.stat(file)[stat.ST_MTIME]
- except OSError:
- pass
- if not time:
- time = os.stat(path)[stat.ST_MTIME]
- url.lastmod = TimestampISO8601(time)
- except OSError:
- pass
- except ValueError:
- pass
-
- # Build a URL
- middle = dirpath[len(root_path):]
- if os.sep != '/':
- middle = middle.replace(os.sep, '/')
- if middle:
- middle = middle + '/'
- if name:
- middle = middle + name
- if isdir:
- middle = middle + '/'
- url.TrySetAttribute(
- 'loc', root_URL + encoder.WidenText(middle, None))
-
- # Suppress default files. (All the way down here so we can log
- # it.)
- if name and (root_file == name):
- url.Log(prefix='IGNORED (default file)', level=2)
- return
-
- # Suppress directories when remove_empty_directories="true"
- try:
- if isdir:
- if HasReadPermissions(path):
- if remove_empty_directories == 'true' and \
- len(os.listdir(path)) == 0:
- output.Log(
- 'IGNORED empty directory %s' % str(path), level=1)
- return
- elif path == self._path:
- output.Error('IGNORED configuration file directory input %s due '
- 'to file permissions' % self._path)
- else:
- output.Log('IGNORED files within directory %s due to file '
- 'permissions' % str(path), level=0)
- except OSError:
- pass
- except ValueError:
- pass
-
- consumer(url, False)
- # end def PerFile
-
- def PerDirectory(ignore, dirpath, namelist):
- """
- Called once per directory with a list of all the contained files/dirs.
- """
- ignore = ignore # Avoid warnings of an unused parameter
-
- if not dirpath.startswith(root_path):
- output.Warn('Unable to decide what the root path is for directory: '
- '%s' % dirpath)
- return
-
- for name in namelist:
- PerFile(dirpath, name)
- # end def PerDirectory
-
- output.Log('Walking DIRECTORY "%s"' % self._path, 1)
- PerFile(self._path, None)
- os.path.walk(self._path, PerDirectory, None)
- # end def ProduceURLs
-# end class InputDirectory
-
-
-class InputAccessLog:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles access logs. It's non-trivial in that we want to
- auto-detect log files in the Common Logfile Format (as used by Apache,
- for instance) and the Extended Log File Format (as used by IIS, for
- instance).
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
- self._is_elf = False # Extended Log File Format?
- self._is_clf = False # Common Logfile Format?
- self._elf_status = -1 # ELF field: '200'
- self._elf_method = -1 # ELF field: 'HEAD'
- self._elf_uri = -1 # ELF field: '/foo?bar=1'
- self._elf_urifrag1 = -1 # ELF field: '/foo'
- self._elf_urifrag2 = -1 # ELF field: 'bar=1'
-
- if not ValidateAttributes('ACCESSLOG', attributes, ('path', 'encoding')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From ACCESSLOG "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Accesslog entries must have a "path" attribute.')
- # end def __init__
-
- def RecognizeELFLine(self, line):
- """ Recognize the Fields directive that heads an ELF file """
- if not line.startswith('#Fields:'):
- return False
- fields = line.split(' ')
- del fields[0]
- for i in range(0, len(fields)):
- field = fields[i].strip()
- if field == 'sc-status':
- self._elf_status = i
- elif field == 'cs-method':
- self._elf_method = i
- elif field == 'cs-uri':
- self._elf_uri = i
- elif field == 'cs-uri-stem':
- self._elf_urifrag1 = i
- elif field == 'cs-uri-query':
- self._elf_urifrag2 = i
- output.Log('Recognized an Extended Log File Format file.', 2)
- return True
- # end def RecognizeELFLine
-
- def GetELFLine(self, line):
- """ Fetch the requested URL from an ELF line """
- fields = line.split(' ')
- count = len(fields)
-
- # Verify status was Ok
- if self._elf_status >= 0:
- if self._elf_status >= count:
- return None
- if not fields[self._elf_status].strip() == '200':
- return None
-
- # Verify method was HEAD or GET
- if self._elf_method >= 0:
- if self._elf_method >= count:
- return None
- if not fields[self._elf_method].strip() in ('HEAD', 'GET'):
- return None
-
- # Pull the full URL if we can
- if self._elf_uri >= 0:
- if self._elf_uri >= count:
- return None
- url = fields[self._elf_uri].strip()
- if url != '-':
- return url
-
- # Put together a fragmentary URL
- if self._elf_urifrag1 >= 0:
- if self._elf_urifrag1 >= count or self._elf_urifrag2 >= count:
- return None
- urlfrag1 = fields[self._elf_urifrag1].strip()
- urlfrag2 = None
- if self._elf_urifrag2 >= 0:
- urlfrag2 = fields[self._elf_urifrag2]
- if urlfrag1 and (urlfrag1 != '-'):
- if urlfrag2 and (urlfrag2 != '-'):
- urlfrag1 = urlfrag1 + '?' + urlfrag2
- return urlfrag1
-
- return None
- # end def GetELFLine
-
- def RecognizeCLFLine(self, line):
- """ Try to tokenize a logfile line according to CLF pattern and see if
- it works. """
- match = ACCESSLOG_CLF_PATTERN.match(line)
- recognize = match and (match.group(1) in ('HEAD', 'GET'))
- if recognize:
- output.Log('Recognized a Common Logfile Format file.', 2)
- return recognize
- # end def RecognizeCLFLine
-
- def GetCLFLine(self, line):
- """ Fetch the requested URL from a CLF line """
- match = ACCESSLOG_CLF_PATTERN.match(line)
- if match:
- request = match.group(1)
- if request in ('HEAD', 'GET'):
- return match.group(2)
- return None
- # end def GetCLFLine
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'ACCESSLOG')
- if not file:
- return
-
- # Iterate lines
- for line in file.readlines():
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
-
- # If we don't know the format yet, try them both
- if (not self._is_clf) and (not self._is_elf):
- self._is_elf = self.RecognizeELFLine(line)
- self._is_clf = self.RecognizeCLFLine(line)
-
- # Digest the line
- match = None
- if self._is_elf:
- match = self.GetELFLine(line)
- elif self._is_clf:
- match = self.GetCLFLine(line)
- if not match:
- continue
-
- # Pass it on
- url = URL()
- url.TrySetAttribute('loc', match)
- consumer(url, True)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputAccessLog
-
-
-class FilePathGenerator:
- """
- This class generates filenames in a series, upon request.
- You can request any iteration number at any time, you don't
- have to go in order.
-
- Example of iterations for '/path/foo.xml.gz':
- 0 --> /path/foo.xml.gz
- 1 --> /path/foo1.xml.gz
- 2 --> /path/foo2.xml.gz
- _index.xml --> /path/foo_index.xml
- """
-
- def __init__(self):
- self.is_gzip = False # Is this a GZIP file?
-
- self._path = None # '/path/'
- self._prefix = None # 'foo'
- self._suffix = None # '.xml.gz'
- # end def __init__
-
- def Preload(self, path):
- """ Splits up a path into forms ready for recombination. """
- path = encoder.MaybeNarrowPath(path)
-
- # Get down to a base name
- path = os.path.normpath(path)
- base = os.path.basename(path).lower()
- if not base:
- output.Error('Couldn\'t parse the file path: %s' % path)
- return False
- lenbase = len(base)
-
- # Recognize extension
- lensuffix = 0
- compare_suffix = ['.xml', '.xml.gz', '.gz']
- for suffix in compare_suffix:
- if base.endswith(suffix):
- lensuffix = len(suffix)
- break
- if not lensuffix:
- output.Error('The path "%s" doesn\'t end in a supported file '
- 'extension.' % path)
- return False
- self.is_gzip = suffix.endswith('.gz')
-
- # Split the original path
- lenpath = len(path)
- self._path = path[:lenpath - lenbase]
- self._prefix = path[lenpath - lenbase:lenpath - lensuffix]
- self._suffix = path[lenpath - lensuffix:]
-
- return True
- # end def Preload
-
- def GeneratePath(self, instance):
- """ Generates the iterations, as described above. """
- prefix = self._path + self._prefix
- if isinstance(instance, int):
- if instance:
- return '%s%d%s' % (prefix, instance, self._suffix)
- return prefix + self._suffix
- return prefix + instance
- # end def GeneratePath
-
- def GenerateURL(self, instance, root_url):
- """ Generates iterations, but as a URL instead of a path. """
- prefix = root_url + self._prefix
- retval = None
- if isinstance(instance, int):
- if instance:
- retval = '%s%d%s' % (prefix, instance, self._suffix)
- else:
- retval = prefix + self._suffix
- else:
- retval = prefix + instance
- return URL.Canonicalize(retval)
- # end def GenerateURL
-
- def GenerateWildURL(self, root_url):
- """ Generates a wildcard that should match all our iterations """
- prefix = URL.Canonicalize(root_url + self._prefix)
- temp = URL.Canonicalize(prefix + self._suffix)
- suffix = temp[len(prefix):]
- return prefix + '*' + suffix
- # end def GenerateURL
-# end class FilePathGenerator
-
-
-class PerURLStatistics:
- """ Keep track of some simple per-URL statistics, like file extension. """
-
- def __init__(self):
- self._extensions = {} # Count of extension instances
- # end def __init__
-
- def Consume(self, url):
- """ Log some stats for the URL. At the moment, that means extension. """
- if url and url.loc:
- (scheme, netloc, path, query, frag) = urlsplit(url.loc)
- if not path:
- return
-
- # Recognize directories
- if path.endswith('/'):
- if '/' in self._extensions:
- self._extensions['/'] = self._extensions['/'] + 1
- else:
- self._extensions['/'] = 1
- return
-
- # Strip to a filename
- i = path.rfind('/')
- if i >= 0:
- assert i < len(path)
- path = path[i:]
-
- # Find extension
- i = path.rfind('.')
- if i > 0:
- assert i < len(path)
- ext = path[i:].lower()
- if ext in self._extensions:
- self._extensions[ext] = self._extensions[ext] + 1
- else:
- self._extensions[ext] = 1
- else:
- if '(no extension)' in self._extensions:
- self._extensions['(no extension)'] = self._extensions[
- '(no extension)'] + 1
- else:
- self._extensions['(no extension)'] = 1
- # end def Consume
-
- def Log(self):
- """ Dump out stats to the output. """
- if len(self._extensions):
- output.Log('Count of file extensions on URLs:', 1)
- set = sorted(self._extensions.keys())
- for ext in set:
- output.Log(' %7d %s' % (self._extensions[ext], ext), 1)
- # end def Log
-
-
-class Sitemap(xml.sax.handler.ContentHandler):
- """
- This is the big workhorse class that processes your inputs and spits
- out sitemap files. It is built as a SAX handler for set up purposes.
- That is, it processes an XML stream to bring itself up.
- """
-
- def __init__(self, suppress_notify):
- xml.sax.handler.ContentHandler.__init__(self)
- self._filters = [] # Filter objects
- self._inputs = [] # Input objects
- self._urls = {} # Maps URLs to count of dups
- self._set = [] # Current set of URLs
- self._filegen = None # Path generator for output files
- self._wildurl1 = None # Sitemap URLs to filter out
- self._wildurl2 = None # Sitemap URLs to filter out
- self._sitemaps = 0 # Number of output files
- # We init _dup_max to 2 so the default priority is 0.5 instead of 1.0
- self._dup_max = 2 # Max number of duplicate URLs
- self._stat = PerURLStatistics() # Some simple stats
- self._in_site = False # SAX: are we in a Site node?
- self._in_Site_ever = False # SAX: were we ever in a Site?
-
- self._default_enc = None # Best encoding to try on URLs
- self._base_url = None # Prefix to all valid URLs
- self._store_into = None # Output filepath
- self._sitemap_type = None # Sitemap type (web, mobile or news)
- self._suppress = suppress_notify # Suppress notify of servers
- # end def __init__
-
- def ValidateBasicConfig(self):
- """ Verifies (and cleans up) the basic user-configurable options. """
- all_good = True
-
- if self._default_enc:
- encoder.SetUserEncoding(self._default_enc)
-
- # Canonicalize the base_url
- if all_good and not self._base_url:
- output.Error('A site needs a "base_url" attribute.')
- all_good = False
- if all_good and not URL.IsAbsolute(self._base_url):
- output.Error('The "base_url" must be absolute, not relative: %s' %
- self._base_url)
- all_good = False
- if all_good:
- self._base_url = URL.Canonicalize(self._base_url)
- if not self._base_url.endswith('/'):
- self._base_url = self._base_url + '/'
- output.Log('BaseURL is set to: %s' % self._base_url, 2)
-
- # Load store_into into a generator
- if all_good:
- if self._store_into:
- self._filegen = FilePathGenerator()
- if not self._filegen.Preload(self._store_into):
- all_good = False
- else:
- output.Error('A site needs a "store_into" attribute.')
- all_good = False
-
- # Ask the generator for patterns on what its output will look like
- if all_good:
- self._wildurl1 = self._filegen.GenerateWildURL(self._base_url)
- self._wildurl2 = self._filegen.GenerateURL(SITEINDEX_SUFFIX,
- self._base_url)
-
- # Unify various forms of False
- if all_good:
- if self._suppress:
- if (isinstance(self._suppress, bytes_str)) or (isinstance(self._suppress, unicode_str)):
- if (self._suppress == '0') or (self._suppress.lower() == 'false'):
- self._suppress = False
-
- # Clean up the sitemap_type
- if all_good:
- match = False
- # If sitemap_type is not specified, default to web sitemap
- if not self._sitemap_type:
- self._sitemap_type = 'web'
- else:
- self._sitemap_type = self._sitemap_type.lower()
- for pattern in SITEMAP_TYPES:
- if self._sitemap_type == pattern:
- match = True
- break
- if not match:
- output.Error('The "sitemap_type" value must be "web", "mobile" '
- 'or "news": %s' % self._sitemap_type)
- all_good = False
- output.Log('The Sitemap type is %s Sitemap.' %
- self._sitemap_type.upper(), 0)
-
- # Done
- if not all_good:
- output.Log('See "example_config.xml" for more information.', 0)
- return all_good
- # end def ValidateBasicConfig
-
- def Generate(self):
- """ Run over all the Inputs and ask them to Produce """
- # Run the inputs
- for input in self._inputs:
- input.ProduceURLs(self.ConsumeURL)
-
- # Do last flushes
- if len(self._set):
- self.FlushSet()
- if not self._sitemaps:
- output.Warn('No URLs were recorded, writing an empty sitemap.')
- self.FlushSet()
-
- # Write an index as needed
- if self._sitemaps > 1:
- self.WriteIndex()
-
- # Notify
- self.NotifySearch()
-
- # Dump stats
- self._stat.Log()
- # end def Generate
-
- def ConsumeURL(self, url, allow_fragment):
- """
- All per-URL processing comes together here, regardless of Input.
- Here we run filters, remove duplicates, spill to disk as needed, etc.
-
- """
- if not url:
- return
-
- # Validate
- if not url.Validate(self._base_url, allow_fragment):
- return
-
- # Run filters
- accept = None
- for filter in self._filters:
- accept = filter.Apply(url)
- if accept is not None:
- break
- if not (accept or (accept is None)):
- url.Log(prefix='FILTERED', level=2)
- return
-
- # Ignore our out output URLs
- if fnmatch.fnmatchcase(url.loc, self._wildurl1) or fnmatch.fnmatchcase(
- url.loc, self._wildurl2):
- url.Log(prefix='IGNORED (output file)', level=2)
- return
-
- # Note the sighting
- hash = url.MakeHash()
- if hash in self._urls:
- dup = self._urls[hash]
- if dup > 0:
- dup = dup + 1
- self._urls[hash] = dup
- if self._dup_max < dup:
- self._dup_max = dup
- url.Log(prefix='DUPLICATE')
- return
-
- # Acceptance -- add to set
- self._urls[hash] = 1
- self._set.append(url)
- self._stat.Consume(url)
- url.Log()
-
- # Flush the set if needed
- if len(self._set) >= MAXURLS_PER_SITEMAP:
- self.FlushSet()
- # end def ConsumeURL
-
- def FlushSet(self):
- """
- Flush the current set of URLs to the output. This is a little
- slow because we like to sort them all and normalize the priorities
- before dumping.
- """
-
- # Determine what Sitemap header to use (News or General)
- if self._sitemap_type == 'news':
- sitemap_header = NEWS_SITEMAP_HEADER
- else:
- sitemap_header = GENERAL_SITEMAP_HEADER
-
- # Sort and normalize
- output.Log('Sorting and normalizing collected URLs.', 1)
- self._set.sort()
- for url in self._set:
- hash = url.MakeHash()
- dup = self._urls[hash]
- if dup > 0:
- self._urls[hash] = -1
- if not url.priority:
- url.priority = '%.4f' % (float(dup) / float(self._dup_max))
-
- # Get the filename we're going to write to
- filename = self._filegen.GeneratePath(self._sitemaps)
- if not filename:
- output.Fatal('Unexpected: Couldn\'t generate output filename.')
- self._sitemaps = self._sitemaps + 1
- output.Log('Writing Sitemap file "%s" with %d URLs' %
- (filename, len(self._set)), 1)
-
- # Write to it
- frame = None
- file = None
-
- try:
- if self._filegen.is_gzip:
- basename = os.path.basename(filename)
- frame = open(filename, 'wb')
- file = gzip.GzipFile(
- fileobj=frame, filename=basename, mode='wt')
- else:
- file = open(filename, 'wt')
-
- file.write(sitemap_header)
- for url in self._set:
- url.WriteXML(file)
- file.write(SITEMAP_FOOTER)
-
- file.close()
- if frame:
- frame.close()
-
- frame = None
- file = None
- except IOError:
- output.Fatal('Couldn\'t write out to file: %s' % filename)
- os.chmod(filename, 0o0644)
-
- # Flush
- self._set = []
- # end def FlushSet
-
- def WriteIndex(self):
- """ Write the master index of all Sitemap files """
- # Make a filename
- filename = self._filegen.GeneratePath(SITEINDEX_SUFFIX)
- if not filename:
- output.Fatal(
- 'Unexpected: Couldn\'t generate output index filename.')
- output.Log('Writing index file "%s" with %d Sitemaps' %
- (filename, self._sitemaps), 1)
-
- # Determine what Sitemap index header to use (News or General)
- if self._sitemap_type == 'news':
- sitemap_index_header = NEWS_SITEMAP_HEADER
- else:
- sitemap_index_header = GENERAL_SITEMAP_HEADER
-
- # Make a lastmod time
- lastmod = TimestampISO8601(time.time())
-
- # Write to it
- try:
- fd = open(filename, 'wt')
- fd.write(sitemap_index_header)
-
- for mapnumber in range(0, self._sitemaps):
- # Write the entry
- mapurl = self._filegen.GenerateURL(mapnumber, self._base_url)
- mapattributes = {'loc': mapurl, 'lastmod': lastmod}
- fd.write(SITEINDEX_ENTRY % mapattributes)
-
- fd.write(SITEINDEX_FOOTER)
-
- fd.close()
- fd = None
- except IOError:
- output.Fatal('Couldn\'t write out to file: %s' % filename)
- os.chmod(filename, 0o0644)
- # end def WriteIndex
-
- def NotifySearch(self):
- """ Send notification of the new Sitemap(s) to the search engines. """
- if self._suppress:
- output.Log('Search engine notification is suppressed.', 1)
- return
-
- output.Log('Notifying search engines.', 1)
-
- # Override the urllib's opener class with one that doesn't ignore 404s
- class ExceptionURLopener(FancyURLopener):
- def http_error_default(self, url, fp, errcode, errmsg, headers):
- output.Log('HTTP error %d: %s' % (errcode, errmsg), 2)
- raise IOError
- # end def http_error_default
- # end class ExceptionURLOpener
- if sys.version_info[0] == 3:
- old_opener = urllib.request._urlopener
- urllib.request._urlopener = ExceptionURLopener()
- else:
- old_opener = urllib._urlopener
- urllib._urlopener = ExceptionURLopener()
-
- # Build the URL we want to send in
- if self._sitemaps > 1:
- url = self._filegen.GenerateURL(SITEINDEX_SUFFIX, self._base_url)
- else:
- url = self._filegen.GenerateURL(0, self._base_url)
-
- # Test if we can hit it ourselves
- try:
- u = urlopen(url)
- u.close()
- except IOError:
- output.Error('When attempting to access our generated Sitemap at the '
- 'following URL:\n %s\n we failed to read it. Please '
- 'verify the store_into path you specified in\n'
- ' your configuration file is web-accessable. Consult '
- 'the FAQ for more\n information.' % url)
- output.Warn('Proceeding to notify with an unverifyable URL.')
-
- # Cycle through notifications
- # To understand this, see the comment near the NOTIFICATION_SITES
- # comment
- for ping in NOTIFICATION_SITES:
- query_map = ping[3]
- query_attr = ping[5]
- query_map[query_attr] = url
- query = urllib.urlencode(query_map)
- notify = urlunsplit((ping[0], ping[1], ping[2], query, ping[4]))
-
- # Send the notification
- output.Log('Notifying: %s' % ping[1], 0)
- output.Log('Notification URL: %s' % notify, 2)
- try:
- u = urlopen(notify)
- u.read()
- u.close()
- except IOError:
- output.Warn('Cannot contact: %s' % ping[1])
-
- if old_opener:
- if sys.version_info[0] == 3:
- urllib.request._urlopener = old_opener
- else:
- urllib._urlopener = old_opener
- # end def NotifySearch
-
- def startElement(self, tag, attributes):
- """ SAX processing, called per node in the config stream. """
- if tag == 'site':
- if self._in_site:
- output.Error('Can not nest Site entries in the configuration.')
- else:
- self._in_site = True
-
- if not ValidateAttributes('SITE', attributes,
- ('verbose', 'default_encoding', 'base_url', 'store_into',
- 'suppress_search_engine_notify', 'sitemap_type')):
- return
-
- verbose = attributes.get('verbose', 0)
- if verbose:
- output.SetVerbose(verbose)
-
- self._default_enc = attributes.get('default_encoding')
- self._base_url = attributes.get('base_url')
- self._store_into = attributes.get('store_into')
- self._sitemap_type = attributes.get('sitemap_type')
- if not self._suppress:
- self._suppress = attributes.get(
- 'suppress_search_engine_notify',
- False)
- self.ValidateBasicConfig()
- elif tag == 'filter':
- self._filters.append(Filter(attributes))
-
- elif tag == 'url':
- print(type(attributes))
- self._inputs.append(InputURL(attributes))
-
- elif tag == 'urllist':
- for attributeset in ExpandPathAttribute(attributes, 'path'):
- if self._sitemap_type == 'news':
- self._inputs.append(InputNewsURLList(attributeset))
- else:
- self._inputs.append(InputURLList(attributeset))
-
- elif tag == 'directory':
- self._inputs.append(InputDirectory(attributes, self._base_url))
-
- elif tag == 'accesslog':
- for attributeset in ExpandPathAttribute(attributes, 'path'):
- self._inputs.append(InputAccessLog(attributeset))
- else:
- output.Error('Unrecognized tag in the configuration: %s' % tag)
- # end def startElement
-
- def endElement(self, tag):
- """ SAX processing, called per node in the config stream. """
- if tag == 'site':
- assert self._in_site
- self._in_site = False
- self._in_site_ever = True
- # end def endElement
-
- def endDocument(self):
- """ End of SAX, verify we can proceed. """
- if not self._in_site_ever:
- output.Error('The configuration must specify a "site" element.')
- else:
- if not self._inputs:
- output.Warn('There were no inputs to generate a sitemap from.')
- # end def endDocument
-# end class Sitemap
-
-
-def ValidateAttributes(tag, attributes, goodattributes):
- """ Makes sure 'attributes' does not contain any attribute not
- listed in 'goodattributes' """
- all_good = True
- for attr in attributes.keys():
- if not attr in goodattributes:
- output.Error('Unknown %s attribute: %s' % (tag, attr))
- all_good = False
- return all_good
-# end def ValidateAttributes
-
-
-def ExpandPathAttribute(src, attrib):
- """ Given a dictionary of attributes, return a list of dictionaries
- with all the same attributes except for the one named attrib.
- That one, we treat as a file path and expand into all its possible
- variations. """
- # Do the path expansion. On any error, just return the source dictionary.
- path = src.get(attrib)
- if not path:
- return [src]
- path = encoder.MaybeNarrowPath(path)
- pathlist = glob.glob(path)
- if not pathlist:
- return [src]
-
- # If this isn't actually a dictionary, make it one
- if not isinstance(src, dict):
- tmp = {}
- for key in src.keys():
- tmp[key] = src[key]
- src = tmp
- # Create N new dictionaries
- retval = []
- for path in pathlist:
- dst = src.copy()
- dst[attrib] = path
- retval.append(dst)
-
- return retval
-# end def ExpandPathAttribute
-
-
-def OpenFileForRead(path, logtext):
- """ Opens a text file, be it GZip or plain """
-
- frame = None
- file = None
-
- if not path:
- return (frame, file)
-
- try:
- if path.endswith('.gz'):
- frame = open(path, 'rb')
- file = gzip.GzipFile(fileobj=frame, mode='rt')
- else:
- file = open(path, 'rt')
-
- if logtext:
- output.Log('Opened %s file: %s' % (logtext, path), 1)
- else:
- output.Log('Opened file: %s' % path, 1)
- except IOError:
- output.Error('Can not open file: %s' % path)
-
- return (frame, file)
-# end def OpenFileForRead
-
-
-def TimestampISO8601(t):
- """Seconds since epoch (1970-01-01) --> ISO 8601 time string."""
- return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(t))
-# end def TimestampISO8601
-
-
-def CreateSitemapFromFile(configpath, suppress_notify):
- """ Sets up a new Sitemap object from the specified configuration file. """
-
- # Remember error count on the way in
- num_errors = output.num_errors
-
- # Rev up SAX to parse the config
- sitemap = Sitemap(suppress_notify)
- try:
- output.Log('Reading configuration file: %s' % configpath, 0)
- xml.sax.parse(configpath, sitemap)
- except IOError:
- output.Error('Cannot read configuration file: %s' % configpath)
- except xml.sax._exceptions.SAXParseException as e:
- output.Error('XML error in the config file (line %d, column %d): %s' %
- (e._linenum, e._colnum, e.getMessage()))
- except xml.sax._exceptions.SAXReaderNotAvailable:
- output.Error('Some installs of Python 2.2 did not include complete support'
- ' for XML.\n Please try upgrading your version of Python'
- ' and re-running the script.')
-
- # If we added any errors, return no sitemap
- if num_errors == output.num_errors:
- return sitemap
- return None
-# end def CreateSitemapFromFile
-
-
-def ProcessCommandFlags(args):
- """
- Parse command line flags per specified usage, pick off key, value pairs
- All flags of type "--key=value" will be processed as __flags[key] = value,
- "--option" will be processed as __flags[option] = option
- """
-
- flags = {}
- rkeyval = '--(?P<key>\S*)[=](?P<value>\S*)' # --key=val
- roption = '--(?P<option>\S*)' # --key
- r = '(' + rkeyval + ')|(' + roption + ')'
- rc = re.compile(r)
- for a in args:
- try:
- rcg = rc.search(a).groupdict()
- if 'key' in rcg:
- flags[rcg['key']] = rcg['value']
- if 'option' in rcg:
- flags[rcg['option']] = rcg['option']
- except AttributeError:
- return None
- return flags
-# end def ProcessCommandFlags
-
-
-#
-# __main__
-#
-
-if __name__ == '__main__':
- flags = ProcessCommandFlags(sys.argv[1:])
- if not flags or not 'config' in flags or 'help' in flags:
- output.Log(__usage__, 0)
- else:
- suppress_notify = 'testing' in flags
- sitemap = CreateSitemapFromFile(flags['config'], suppress_notify)
- if not sitemap:
- output.Log('Configuration file errors -- exiting.', 0)
- else:
- sitemap.Generate()
- output.Log('Number of errors: %d' % output.num_errors, 1)
- output.Log('Number of warnings: %d' % output.num_warns, 1)
diff --git a/nikola/post.py b/nikola/post.py
index 5060583..ac97c73 100644
--- a/nikola/post.py
+++ b/nikola/post.py
@@ -26,15 +26,14 @@
from __future__ import unicode_literals, print_function
import codecs
+from collections import defaultdict
import os
import re
-import sys
import string
-import unidecode
import lxml.html
-from .utils import to_datetime, slugify
+from .utils import to_datetime, slugify, bytes_str, Functionary, LocaleBorg
__all__ = ['Post']
@@ -48,7 +47,8 @@ class Post(object):
def __init__(
self, source_path, cache_folder, destination, use_in_feeds,
translations, default_lang, base_url, messages, template_name,
- file_metadata_regexp=None, tzinfo=None
+ file_metadata_regexp=None, strip_index_html=False, tzinfo=None,
+ skip_untranslated=False,
):
"""Initialize post.
@@ -56,14 +56,13 @@ class Post(object):
the meta file, as well as any translations available, and
the .html fragment file path.
"""
- self.translated_to = set([default_lang])
- self.tags = ''
- self.date = None
- self.prev_post = None
- self.next_post = None
+ self.translated_to = set([])
+ self._prev_post = None
+ self._next_post = None
self.base_url = base_url
self.is_draft = False
self.is_mathjax = False
+ self.strip_index_html = strip_index_html
self.source_path = source_path # posts/blah.txt
self.post_name = os.path.splitext(source_path)[0] # posts/blah
# cache/posts/blah.html
@@ -73,72 +72,170 @@ class Post(object):
self.translations = translations
self.default_lang = default_lang
self.messages = messages
- self.template_name = template_name
- self.meta = get_meta(self, file_metadata_regexp)
+ self.skip_untranslated = skip_untranslated
+ self._template_name = template_name
- default_title = self.meta.get('title', '')
- default_pagename = self.meta.get('slug', '')
- default_description = self.meta.get('description', '')
+ default_metadata = get_meta(self, file_metadata_regexp)
- for k, v in self.meta.items():
- if k not in ['title', 'slug', 'description']:
- if sys.version_info[0] == 2:
- setattr(self, unidecode.unidecode(unicode(k)), v) # NOQA
- else:
- setattr(self, k, v)
+ self.meta = Functionary(lambda: None, self.default_lang)
+ self.meta[default_lang] = default_metadata
- if not default_title or not default_pagename or not self.date:
+ # Load internationalized metadata
+ for lang in translations:
+ if lang != 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))
+ self.meta[lang] = meta
+ elif os.path.isfile(self.source_path):
+ self.translated_to.add(default_lang)
+
+ if not self.is_translation_available(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 '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 "
"'{1}') and a date (found '{2}')! [in file "
- "{3}]".format(default_title, default_pagename,
- self.date, source_path))
+ "{3}]".format(default_metadata.get('title', None),
+ default_metadata.get('slug', None),
+ default_metadata.get('date', None),
+ source_path))
# If timezone is set, build localized datetime.
- self.date = to_datetime(self.date, tzinfo)
- self.tags = [x.strip() for x in self.tags.split(',')]
- self.tags = [_f for _f in self.tags if _f]
+ self.date = to_datetime(self.meta[default_lang]['date'], tzinfo)
+
+ is_draft = False
+ is_retired = False
+ self._tags = {}
+ for lang in self.translated_to:
+ self._tags[lang] = [x.strip() for x in self.meta[lang]['tags'].split(',')]
+ self._tags[lang] = [t for t in self._tags[lang] if t]
+ if 'draft' in self._tags[lang]:
+ is_draft = True
+ self._tags[lang].remove('draft')
+ if 'retired' in self._tags[lang]:
+ is_retired = True
+ self._tags[lang].remove('retired')
# While draft comes from the tags, it's not really a tag
- self.use_in_feeds = use_in_feeds and "draft" not in self.tags
- self.is_draft = 'draft' in self.tags
- self.tags = [t for t in self.tags if t != 'draft']
+ self.is_draft = is_draft
+ self.use_in_feeds = use_in_feeds and not is_draft and not is_retired
# If mathjax is a tag, then enable mathjax rendering support
self.is_mathjax = 'mathjax' in self.tags
+ @property
+ def alltags(self):
+ """This is ALL the tags for this post."""
+ tags = []
+ for l in self._tags:
+ tags.extend(self._tags[l])
+ return list(set(tags))
+
+ @property
+ def tags(self):
+ lang = self.current_lang()
+ if lang in self._tags:
+ return self._tags[lang]
+ elif self.default_lang in self._tags:
+ return self._tags[self.default_lang]
+ else:
+ return []
+
+ @property
+ def prev_post(self):
+ lang = self.current_lang()
+ rv = self._prev_post
+ while self.skip_untranslated:
+ if rv is None:
+ break
+ if rv.is_translation_available(lang):
+ break
+ rv = rv._prev_post
+ return rv
+
+ @prev_post.setter # NOQA
+ def prev_post(self, v):
+ self._prev_post = v
+
+ @property
+ def next_post(self):
+ lang = self.current_lang()
+ rv = self._next_post
+ while self.skip_untranslated:
+ if rv is None:
+ break
+ if rv.is_translation_available(lang):
+ break
+ rv = rv._next_post
+ return rv
+
+ @next_post.setter # NOQA
+ def next_post(self, v):
+ self._next_post = v
+
+ @property
+ def template_name(self):
+ return self.meta('template') or self._template_name
+
+ def _add_old_metadata(self):
+ # Compatibility for themes up to Nikola 5.4.1
+ # TODO: remove before Nikola 6
self.pagenames = {}
self.titles = {}
- self.descriptions = {}
-
- # Load internationalized metadata
- for lang in translations:
- if lang == default_lang:
- self.titles[lang] = default_title
- self.pagenames[lang] = default_pagename
- self.descriptions[lang] = default_description
- else:
- if os.path.isfile(self.source_path + "." + lang):
- self.translated_to.add(lang)
-
- meta = self.meta.copy()
- meta.update(get_meta(self, file_metadata_regexp, lang))
-
- # FIXME this only gets three pieces of metadata from the i18n files
- self.titles[lang] = meta.get('title', default_title)
- self.pagenames[lang] = meta.get('slug', default_pagename)
- self.descriptions[lang] = meta.get('description', default_description)
-
- def title(self, lang):
- """Return localized title."""
- return self.titles[lang]
+ for lang in self.translations:
+ self.pagenames[lang] = self.meta[lang]['slug']
+ self.titles[lang] = self.meta[lang]['title']
+
+ def formatted_date(self, date_format):
+ """Return the formatted date, as unicode."""
+ fmt_date = self.date.strftime(date_format)
+ # Issue #383, this changes from py2 to py3
+ if isinstance(fmt_date, bytes_str):
+ fmt_date = fmt_date.decode('utf8')
+ return fmt_date
+
+ def 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 None:
+ lang = self.current_lang()
+ return self.meta[lang]['title']
- def description(self, lang):
+ def description(self, lang=None):
"""Return localized description."""
- return self.descriptions[lang]
+ if lang is None:
+ lang = self.current_lang()
+ return self.meta[lang]['description']
def deps(self, lang):
"""Return a list of dependencies to build this post's page."""
- deps = [self.base_path]
+ deps = []
+ if self.default_lang in self.translated_to:
+ deps.append(self.base_path)
if lang != self.default_lang:
deps += [self.base_path + "." + lang]
deps += self.fragment_deps(lang)
@@ -146,9 +243,15 @@ class Post(object):
def fragment_deps(self, lang):
"""Return a list of dependencies to build this post's fragment."""
- deps = [self.source_path]
+ deps = []
+ if self.default_lang in self.translated_to:
+ deps.append(self.source_path)
if os.path.isfile(self.metadata_path):
deps.append(self.metadata_path)
+ dep_path = self.base_path + '.dep'
+ if os.path.isfile(dep_path):
+ with codecs.open(dep_path, 'rb+', 'utf8') as depf:
+ deps.extend([l.strip() for l in depf.readlines()])
if lang != self.default_lang:
lang_deps = list(filter(os.path.exists, [x + "." + lang for x in
deps]))
@@ -159,67 +262,86 @@ class Post(object):
"""Return true if the translation actually exists."""
return lang in self.translated_to
+ def translated_source_path(self, lang):
+ """Return path to the translation's source file."""
+ if lang in self.translated_to:
+ if lang == self.default_lang:
+ return self.source_path
+ else:
+ return '.'.join((self.source_path, lang))
+ elif lang != self.default_lang:
+ return self.source_path
+ else:
+ return '.'.join((self.source_path, sorted(self.translated_to)[0]))
+
def _translated_file_path(self, lang):
"""Return path to the translation's file, or to the original."""
- file_name = self.base_path
- if lang != self.default_lang:
- file_name_lang = '.'.join((file_name, lang))
- if os.path.exists(file_name_lang):
- file_name = file_name_lang
- return file_name
+ if lang in self.translated_to:
+ if lang == self.default_lang:
+ return self.base_path
+ else:
+ return '.'.join((self.base_path, lang))
+ elif lang != self.default_lang:
+ return self.base_path
+ else:
+ return '.'.join((self.base_path, sorted(self.translated_to)[0]))
- def text(self, lang, teaser_only=False, strip_html=False):
- """Read the post file for that language and return its contents"""
- file_name = self._translated_file_path(lang)
+ def text(self, lang=None, teaser_only=False, strip_html=False):
+ """Read the post file for that language and return its contents."""
+ if lang is None:
+ lang = self.current_lang()
+ file_name = self._translated_file_path(lang)
with codecs.open(file_name, "r", "utf8") as post_file:
- data = post_file.read()
-
- if data:
- data = lxml.html.make_links_absolute(data, self.permalink(lang=lang))
- if data and teaser_only:
- e = lxml.html.fromstring(data)
- teaser = []
- teaser_str = self.messages[lang]["Read more"] + '...'
- flag = False
- for elem in e:
- elem_string = lxml.html.tostring(elem).decode('utf8')
- match = TEASER_REGEXP.match(elem_string)
- if match:
- flag = True
- if match.group(2):
- teaser_str = match.group(2)
- break
- teaser.append(elem_string)
- if flag:
- teaser.append('<p><a href="{0}">{1}</a></p>'.format(
- self.permalink(lang), teaser_str))
- data = ''.join(teaser)
+ data = post_file.read().strip()
+
+ try:
+ document = lxml.html.document_fromstring(data)
+ 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))
+ data = lxml.html.tostring(document, encoding='unicode')
+ 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)
+ # 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.
-
return data
def destination_path(self, lang, extension='.html'):
path = os.path.join(self.translations[lang],
- self.folder, self.pagenames[lang] + extension)
+ self.folder, self.meta[lang]['slug'] + extension)
return path
def permalink(self, lang=None, absolute=False, extension='.html'):
if lang is None:
- lang = self.default_lang
- pieces = list(os.path.split(self.translations[lang]))
- pieces += list(os.path.split(self.folder))
- pieces += [self.pagenames[lang] + extension]
+ lang = self.current_lang()
+
+ pieces = self.translations[lang].split(os.sep)
+ pieces += self.folder.split(os.sep)
+ pieces += [self.meta[lang]['slug'] + extension]
pieces = [_f for _f in pieces if _f and _f != '.']
if absolute:
pieces = [self.base_url] + pieces
else:
pieces = [""] + pieces
link = "/".join(pieces)
- return link
+ if self.strip_index_html and link.endswith('/index.html'):
+ return link[:-10]
+ else:
+ return link
def source_ext(self):
return os.path.splitext(self.source_path)[1]
@@ -359,7 +481,7 @@ def get_meta(post, file_metadata_regexp=None, lang=None):
If any metadata is then found inside the file the metadata from the
file will override previous findings.
"""
- meta = {}
+ meta = defaultdict(lambda: '')
meta.update(get_metadata_from_meta_file(post.metadata_path, lang))
diff --git a/nikola/rc4.py b/nikola/rc4.py
new file mode 100644
index 0000000..6e63474
--- /dev/null
+++ b/nikola/rc4.py
@@ -0,0 +1,76 @@
+"""
+ Copyright (C) 2012 Bo Zhu http://about.bozhu.me
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+"""
+
+import base64
+import sys
+
+
+def KSA(key):
+ keylength = len(key)
+
+ S = list(range(256))
+
+ j = 0
+ for i in range(256):
+ j = (j + S[i] + key[i % keylength]) % 256
+ S[i], S[j] = S[j], S[i] # swap
+
+ return S
+
+
+def PRGA(S):
+ i = 0
+ j = 0
+ while True:
+ i = (i + 1) % 256
+ j = (j + S[i]) % 256
+ S[i], S[j] = S[j], S[i] # swap
+
+ K = S[(S[i] + S[j]) % 256]
+ yield K
+
+
+def RC4(key):
+ S = KSA(key)
+ return PRGA(S)
+
+
+def rc4(key, string):
+ """Encrypt things.
+ >>> print(rc4("Key", "Plaintext"))
+ u/MW6NlArwrT
+ """
+
+ string.encode('utf8')
+ key.encode('utf8')
+
+ def convert_key(s):
+ return [ord(c) for c in s]
+ key = convert_key(key)
+ keystream = RC4(key)
+ r = b''
+ for c in string:
+ if sys.version_info[0] == 3:
+ r += bytes([ord(c) ^ next(keystream)])
+ else:
+ r += chr(ord(c) ^ next(keystream))
+ return base64.b64encode(r).replace(b'\n', b'').decode('ascii')
diff --git a/nikola/utils.py b/nikola/utils.py
index 5589d68..423ded8 100644
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -25,10 +25,11 @@
"""Utility functions."""
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
from collections import defaultdict, Callable
import datetime
import hashlib
+import locale
import os
import re
import codecs
@@ -49,9 +50,12 @@ if sys.version_info[0] == 3:
bytes_str = bytes
unicode_str = str
unichr = chr
+ from imp import reload as _reload
else:
bytes_str = str
unicode_str = unicode # NOQA
+ _reload = reload # NOQA
+ unichr = unichr
from doit import tools
from unidecode import unidecode
@@ -60,7 +64,38 @@ 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']
+ 'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs',
+ 'get_asset_path', '_reload', 'unicode_str', 'bytes_str',
+ 'unichr', 'Functionary', 'LocaleBorg']
+
+
+class Functionary(defaultdict):
+
+ """Class that looks like a function, but is a defaultdict."""
+
+ def __init__(self, default, default_lang):
+ 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()
+ return self[lang][key]
class CustomEncoder(json.JSONEncoder):
@@ -140,13 +175,13 @@ def get_theme_chain(theme):
return themes
-def load_messages(themes, translations):
+def load_messages(themes, translations, default_lang):
""" Load theme's messages into context.
All the messages from parent themes are loaded,
and "younger" themes have priority.
"""
- messages = defaultdict(dict)
+ messages = Functionary(dict, default_lang)
warned = []
oldpath = sys.path[:]
for theme_name in themes[::-1]:
@@ -285,16 +320,18 @@ def slugify(value):
From Django's "django/template/defaultfilters.py".
- >>> slugify('\xe1\xe9\xed.\xf3\xfa')
- 'aeiou'
+ >>> print(slugify('\xe1\xe9\xed.\xf3\xfa'))
+ aeiou
- >>> slugify('foo/bar')
- 'foobar'
+ >>> print(slugify('foo/bar'))
+ foobar
- >>> slugify('foo bar')
- 'foo-bar'
+ >>> print(slugify('foo bar'))
+ foo-bar
"""
+ if not isinstance(value, unicode_str):
+ raise ValueError("Not a unicode object: {0}".format(value))
value = unidecode(value)
# WARNING: this may not be python2/3 equivalent
# value = unicode(_slugify_strip_re.sub('', value).strip().lower())
@@ -367,6 +404,15 @@ def to_datetime(value, tzinfo=None):
return tzinfo.localize(dt)
except ValueError:
pass
+ # So, let's try dateutil
+ try:
+ from dateutil import parser
+ dt = parser.parse(value)
+ if tzinfo is None:
+ 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))
@@ -383,7 +429,7 @@ def apply_filters(task, filters):
if isinstance(key, (tuple, list)):
if ext in key:
return value
- elif isinstance(key, (str, bytes)):
+ elif isinstance(key, (bytes_str, unicode_str)):
if ext == key:
return value
else:
@@ -408,14 +454,29 @@ def apply_filters(task, filters):
def get_crumbs(path, is_file=False):
"""Create proper links for a crumb bar.
- >>> get_crumbs('galleries')
- [['#', 'galleries']]
-
- >>> get_crumbs(os.path.join('galleries','demo'))
- [['..', 'galleries'], ['#', 'demo']]
-
- >>> get_crumbs(os.path.join('listings','foo','bar'), is_file=True)
- [['..', 'listings'], ['.', 'foo'], ['#', 'bar']]
+ >>> crumbs = get_crumbs('galleries')
+ >>> len(crumbs)
+ 1
+ >>> print('|'.join(crumbs[0]))
+ #|galleries
+
+ >>> crumbs = get_crumbs(os.path.join('galleries','demo'))
+ >>> len(crumbs)
+ 2
+ >>> print('|'.join(crumbs[0]))
+ ..|galleries
+ >>> print('|'.join(crumbs[1]))
+ #|demo
+
+ >>> crumbs = get_crumbs(os.path.join('listings','foo','bar'), is_file=True)
+ >>> len(crumbs)
+ 3
+ >>> print('|'.join(crumbs[0]))
+ ..|listings
+ >>> print('|'.join(crumbs[1]))
+ .|foo
+ >>> print('|'.join(crumbs[2]))
+ #|bar
"""
crumbs = path.split(os.sep)
@@ -431,3 +492,53 @@ def get_crumbs(path, is_file=False):
_path = '/'.join(['..'] * i) or '#'
_crumbs.append([_path, crumb])
return list(reversed(_crumbs))
+
+
+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.
+
+ 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/theme.css', ['site', 'default']))
+ nikola/data/themes/site/assets/css/theme.css
+
+ >>> print(get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''}))
+ nikola/nikola.py
+
+ >>> print(get_asset_path('nikola/nikola.py', ['site', 'default'],
+ ... {'nikola':'nikola'}))
+ nikola/nikola.py
+
+ """
+ for theme_name in themes:
+ candidate = os.path.join(
+ get_theme_path(theme_name),
+ path
+ )
+ if os.path.isfile(candidate):
+ return os.path.relpath(candidate, os.getcwd())
+ for src, rel_dst in files_folders.items():
+ candidate = os.path.join(
+ src,
+ os.path.relpath(path, rel_dst)
+ )
+ if os.path.isfile(candidate):
+ return os.path.relpath(candidate, os.getcwd())
+
+ # whatever!
+ return None
+
+
+class LocaleBorg:
+ __shared_state = {
+ 'current_lang': None
+ }
+
+ def __init__(self):
+ self.__dict__ = self.__shared_state