diff options
| author | 2013-05-30 17:41:06 -0300 | |
|---|---|---|
| committer | 2013-05-30 17:41:06 -0300 | |
| commit | 0c4dfdec5b55b6064dccc38bbfb0a7c0699c895a (patch) | |
| tree | a6707225ccc559f7edf50ddd3fdc7fc85145c921 /nikola | |
| parent | 8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (diff) | |
Imported Upstream version 5.4.4
Diffstat (limited to 'nikola')
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"])}: + ${(messages("Also available in"))}: ${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> - ${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}">← ${messages[lang]["Newer posts"]}</a> + <a href="${prevlink}">← ${messages("Newer posts")}</a> %endif %if nextlink: <li class="next"> - <a href="${nextlink}">${messages[lang]["Older posts"]} →</a> + <a href="${nextlink}">${messages("Older posts")} →</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): | - <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: - | ${messages[lang]["More posts about"]} + | ${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)}">← ${messages[lang]["Previous post"]}</a> + <a href="${post.prev_post.permalink()}">← ${messages("Previous post")}</a> + </li> %endif %if post.next_post: <li class="next"> - <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} →</a> + <a href="${post.next_post.permalink()}">${messages("Next post")} →</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> + %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"] }}: + {{ messages("Also available in") }}: {% 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> - {{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}}">← {{messages[lang]["Newer posts"]}}</a> + <a href="{{prevlink}}">← {{messages("Newer posts")}}</a> {% endif %} {% if nextlink %} <li class="next"> - <a href="{{nextlink}}">{{messages[lang]["Older posts"]}} →</a> + <a href="{{nextlink}}">{{messages("Older posts")}} →</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)}} | + {{messages("Posted")}}: {{post.formatted_date(date_format)}} | {% 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> | {% 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 %} - | {{messages[lang]["More posts about"]}} + | {{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))}}">← {{messages[lang]["Previous post"]}}</a> + <a href="{{rel_link(permalink, post.prev_post.permalink())}}">← {{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"]}} →</a> + <a href="{{rel_link(permalink, post.next_post.permalink())}}">{{messages("Next post")}} →</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"])}: + ${(messages("Also available in"))}: ${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: %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}">← ${messages[lang]["Newer posts"]}</a> + <a href="${prevlink}">← ${messages("Newer posts")}</a> </li> %endif %if nextlink: <li class="next"> - <a href="${nextlink}">${messages[lang]["Older posts"]} →</a> + <a href="${nextlink}">${messages("Older posts")} →</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"]}: + <span class="tags">${messages("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 </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): | - <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: - | ${messages[lang]["More posts about"]} + | ${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)}">← ${messages[lang]["Previous post"]}</a> + <a href="${post.prev_post.permalink()}">← ${messages("Previous post")}</a> </li> %endif %if post.next_post: <li class="next"> - <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} →</a> + <a href="${post.next_post.permalink()}">${messages("Next post")} →</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"])}: + ${(messages("Also available in"))}: ${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> - ${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}">← ${messages[lang]["Newer posts"]}</a> + <a href="${prevlink}">← ${messages("Newer posts")}</a> </li> %endif %if nextlink: <li class="next"> - <a href="${nextlink}">${messages[lang]["Older posts"]} →</a> + <a href="${nextlink}">${messages("Older posts")} →</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)} | - <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): | - <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: - | ${messages[lang]["More posts about"]} + | ${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)}">← ${messages[lang]["Previous post"]}</a> + <a href="${post.prev_post.permalink()}">← ${messages("Previous post")}</a> </li> %endif %if post.next_post: <li class="next"> - <a href="${post.next_post.permalink(lang)}">${messages[lang]["Next post"]} →</a> + <a href="${post.next_post.permalink()}">${messages("Next post")} →</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> + ${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">‹</a> + <a class="right carousel-control" href="#myCarousel" data-slide="next">›</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&hd=1&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 Binary files differnew file mode 100755 index 0000000..21bb7b0 --- /dev/null +++ b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/link.png b/nikola/plugins/task_localsearch/files/assets/css/img/link.png Binary files differnew file mode 100755 index 0000000..d4e51c5 --- /dev/null +++ b/nikola/plugins/task_localsearch/files/assets/css/img/link.png diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif Binary files differnew file mode 100644 index 0000000..9c97738 --- /dev/null +++ b/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif Binary files differnew file mode 100644 index 0000000..644bd17 --- /dev/null +++ b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif 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 + '">« 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 »</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}} </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 |
