diff options
| author | 2013-03-13 20:58:39 -0300 | |
|---|---|---|
| committer | 2013-03-13 20:58:39 -0300 | |
| commit | 8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (patch) | |
| tree | 0895935489e4920d18824f7fb3a0d799649a27c3 /nikola | |
| parent | 878ba1152ebc64a4a2609d23c9e400a6111db642 (diff) | |
Imported Upstream version 5.4.2upstream/5.4.2
Diffstat (limited to 'nikola')
67 files changed, 1548 insertions, 902 deletions
diff --git a/nikola/conf.py.in b/nikola/conf.py.in index 40e1996..7d75295 100644 --- a/nikola/conf.py.in +++ b/nikola/conf.py.in @@ -2,7 +2,6 @@ <%text> # -*- coding: utf-8 -*- from __future__ import unicode_literals -import os import time ############################################## @@ -13,7 +12,12 @@ import time # Data about this site BLOG_AUTHOR = "${BLOG_AUTHOR}" BLOG_TITLE = "${BLOG_TITLE}" -BLOG_URL = "${BLOG_URL}" +# This is the main URL for your site. It will be used +# in a prominent link +SITE_URL = "${SITE_URL}" +# This is the URL where nikola's output will be deployed. +# If not set, defaults to SITE_URL +# BASE_URL = "${SITE_URL} BLOG_EMAIL = "${BLOG_EMAIL}" BLOG_DESCRIPTION = "${BLOG_DESCRIPTION}" @@ -46,7 +50,7 @@ TRANSLATIONS = { "${DEFAULT_LANG}": "", # Example for another language: # "es": "./es", - } +} # Links for the sidebar / navigation bar. # You should provide a key-value pair for each used language. @@ -101,6 +105,10 @@ post_pages = ${POST_PAGES} # 'html' assumes the file is html and just copies it post_compilers = ${POST_COMPILERS} +# Create by default posts in one file format? +# Set to False for two-file posts, with separate metadata. +# ONE_FILE_POSTS = True + # Paths for different autogenerated bits. These are combined with the # translation paths. @@ -176,6 +184,11 @@ post_compilers = ${POST_COMPILERS} # ".jpg": ["jpegoptim --strip-all -m75 -v %s"], # } +# Create a gzipped copy of each generated file. Cheap server-side optimization. +# GZIP_FILES = False +# File extensions that will be compressed +# GZIP_EXTENSIONS = ('.txt', '.htm', '.html', '.css', '.js', '.json') + # ############################################################################# # Image Gallery Options # ############################################################################# @@ -198,6 +211,12 @@ post_compilers = ${POST_COMPILERS} # Name of the theme to use. Themes are located in themes/theme_name # THEME = 'site' +# 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 + +# 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 + # date format used to display post dates. (str used by datetime.datetime.strftime) # DATE_FORMAT = '%Y-%m-%d %H:%M' @@ -242,6 +261,25 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # Enable comments on picture gallery pages? # COMMENTS_IN_GALLERIES = False +# Do you want a add a Mathjax config file? +# MATHJAX_CONFIG = "" + +# If you are using the compile-ipynb plugin, just add this one: +#MATHJAX_CONFIG = """ +#<script type="text/x-mathjax-config"> +#MathJax.Hub.Config({ +# tex2jax: { +# inlineMath: [ ['$','$'], ["\\\(","\\\)"] ], +# displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ] +# }, +# displayAlign: 'left', // Change this to 'center' to center equations. +# "HTML-CSS": { +# styles: {'.MathJax_Display': {"margin": 0}} +# } +#}); +#</script> +#""" + # Enable Addthis social buttons? # Defaults to true # ADD_THIS_BUTTONS = True @@ -309,6 +347,39 @@ CONTENT_FOOTER = CONTENT_FOOTER.format(email=BLOG_EMAIL, # '(?P<date>\d{4}-\d{2}-\d{2})-(?P<slug>.*)-(?P<title>.*)\.md' # FILE_METADATA_REGEXP = None +# Nikola supports Twitter Card summaries / Open Graph. +# Twitter cards make it possible for you to attach media to Tweets +# that link to your content. +# +# IMPORTANT: +# Please note, that you need to opt-in for using Twitter Cards! +# To do this please visit https://dev.twitter.com/form/participate-twitter-cards +# +# Uncomment and modify to following lines to match your accounts. +# Specifying the id for either 'site' or 'creator' will be preferred +# over the cleartext username. Specifying an ID is not necessary. +# Displaying images is currently not supported. +# TWITTER_CARD = { +# # 'use_twitter_cards': True, # enable Twitter Cards / Open Graph +# # 'site': '@website', # twitter nick for the website +# # 'site:id': 123456, # Same as site, but the website's Twitter user ID instead. +# # 'creator': '@username', # Username for the content creator / author. +# # 'creator:id': 654321, # Same as creator, but the Twitter user's ID. +# } + + +# If you want to use formatted post time in W3C-DTF Format(ex. 2012-03-30T23:00:00+02:00), +# set timzone if you want a localized posted date. +# +# TIMEZONE = 'Europe/Zurich' + +# If webassets is installed, bundle JS and CSS to make site loading faster +# USE_BUNDLES = True + +# Plugins you don't want to use. Be careful :-) +# DISABLED_PLUGINS = ["render_galleries"] + # Put in global_context things you want available on all your templates. # It can be anything, data, functions, modules, etc. + GLOBAL_CONTEXT = {} diff --git a/nikola/console.py b/nikola/console.py deleted file mode 100644 index fae0ecd..0000000 --- a/nikola/console.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import print_function, unicode_literals - -from nikola import Nikola -try: - import conf - SITE = Nikola(**conf.__dict__) - SITE.scan_posts() - print("You can now access your configuration as conf and your site engine " - "as SITE") -except ImportError: - print("No configuration found.") diff --git a/nikola/data/samplesite/files/images/biohazard.png b/nikola/data/samplesite/files/images/biohazard.png Binary files differnew file mode 100644 index 0000000..ae4629d --- /dev/null +++ b/nikola/data/samplesite/files/images/biohazard.png diff --git a/nikola/data/samplesite/listings/hello.py b/nikola/data/samplesite/listings/hello.py index 695c212..885acde 100644 --- a/nikola/data/samplesite/listings/hello.py +++ b/nikola/data/samplesite/listings/hello.py @@ -4,7 +4,8 @@ import sys def hello(name='world'): - print "hello", name + greeting = "hello " + name + print(greeting) if __name__ == "__main__": hello(*sys.argv[1:]) diff --git a/nikola/data/samplesite/stories/creating-a-theme.meta b/nikola/data/samplesite/stories/creating-a-theme.meta new file mode 100644 index 0000000..fe9b69a --- /dev/null +++ b/nikola/data/samplesite/stories/creating-a-theme.meta @@ -0,0 +1,3 @@ +Creating a Theme +creating-a-theme +2012/03/13 12:00 diff --git a/nikola/data/samplesite/stories/creating-a-theme.txt b/nikola/data/samplesite/stories/creating-a-theme.txt new file mode 120000 index 0000000..108a192 --- /dev/null +++ b/nikola/data/samplesite/stories/creating-a-theme.txt @@ -0,0 +1 @@ +../../../../docs/creating-a-theme.txt
\ No newline at end of file diff --git a/nikola/data/samplesite/stories/quickref.txt b/nikola/data/samplesite/stories/quickref.txt index 13ebc9b..9a06d74 100644 --- a/nikola/data/samplesite/stories/quickref.txt +++ b/nikola/data/samplesite/stories/quickref.txt @@ -55,7 +55,7 @@ <p>Links that look like "(<a href="#details">details</a>)" point into the HTML version of the full <a - href="../../ref/rst/restructuredtext.html">reStructuredText + href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html">reStructuredText specification</a> document. These are relative links; if they don't work, please use the <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html" @@ -65,7 +65,7 @@ <h2><a href="#contents" name="inline-markup" class="backref" >Inline Markup</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#inline-markup">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup">details</a>) <p>Inline markup allows words and phrases within text to have character styles (like italics and boldface) and functionality @@ -198,7 +198,7 @@ >Escaping with Backslashes</a></h2> <p>(<a - href="../../ref/rst/restructuredtext.html#escaping-mechanism">details</a>) + href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#escaping-mechanism">details</a>) <p>reStructuredText uses backslashes ("\") to override the special meaning given to markup characters and get the literal characters @@ -246,7 +246,7 @@ <h2><a href="#contents" name="section-structure" class="backref" >Section Structure</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#sections">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#sections">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -292,7 +292,7 @@ <h2><a href="#contents" name="paragraphs" class="backref" >Paragraphs</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#paragraphs">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#paragraphs">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -320,7 +320,7 @@ <h2><a href="#contents" name="bullet-lists" class="backref" >Bullet Lists</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#bullet-lists">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#bullet-lists">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -358,7 +358,7 @@ <h2><a href="#contents" name="enumerated-lists" class="backref" >Enumerated Lists</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#enumerated-lists">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#enumerated-lists">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -396,7 +396,7 @@ <h2><a href="#contents" name="definition-lists" class="backref" >Definition Lists</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#definition-lists">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#definition-lists">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -437,7 +437,7 @@ <h2><a href="#contents" name="field-lists" class="backref" >Field Lists</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#field-lists">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#field-lists">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -476,7 +476,7 @@ <h2><a href="#contents" name="option-lists" class="backref" >Option Lists</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#option-lists">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#option-lists">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -524,7 +524,7 @@ <h2><a href="#contents" name="literal-blocks" class="backref" >Literal Blocks</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#literal-blocks">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#literal-blocks">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -613,7 +613,7 @@ <h2><a href="#contents" name="line-blocks" class="backref" >Line Blocks</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#line-blocks">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#line-blocks">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -655,7 +655,7 @@ <h2><a href="#contents" name="block-quotes" class="backref" >Block Quotes</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#block-quotes">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#block-quotes">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -687,7 +687,7 @@ <h2><a href="#contents" name="doctest-blocks" class="backref" >Doctest Blocks</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#doctest-blocks">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#doctest-blocks">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -723,7 +723,7 @@ <h2><a href="#contents" name="tables" class="backref" >Tables</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#tables">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#tables">details</a>) <p>There are two syntaxes for tables in reStructuredText. Grid tables are complete but cumbersome to create. Simple tables are @@ -840,7 +840,7 @@ <h2><a href="#contents" name="transitions" class="backref" >Transitions</a></h2> - <p>(<a href="../../ref/rst/restructuredtext.html#transitions">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#transitions">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -890,7 +890,7 @@ <h3><a href="#contents" name="footnotes" class="backref" >Footnotes</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#footnotes">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#footnotes">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -983,7 +983,7 @@ <h3><a href="#contents" name="citations" class="backref" >Citations</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#citations">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#citations">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -1036,7 +1036,7 @@ <h3><a href="#contents" name="hyperlink-targets" class="backref" >Hyperlink Targets</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#hyperlink-targets">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#hyperlink-targets">details</a>) <h4><a href="#contents" name="external-hyperlink-targets" class="backref" >External Hyperlink Targets</a></h4> @@ -1081,11 +1081,11 @@ printed documents, where the link needs to be presented explicitly, for example as a footnote. You can force usage of the call-out form by using the - "<a href="../../ref/rst/directives.html#target-notes">target-notes</a>" + "<a href="http://docutils.sourceforge.net/docs/ref/rst/directives.html#target-notes">target-notes</a>" directive. <p>reStructuredText also provides for <b>embedded URIs</b> (<a - href="../../ref/rst/restructuredtext.html#embedded-uris">details</a>), + href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#embedded-uris">details</a>), a convenience at the expense of readability. A hyperlink reference may directly embed a target URI inline, within angle brackets. The following is exactly equivalent to the example above: @@ -1149,7 +1149,7 @@ <h4><a href="#contents" name="indirect-hyperlink-targets" class="backref" >Indirect Hyperlink Targets</a></h4> - <p>(<a href="../../ref/rst/restructuredtext.html#indirect-hyperlink-targets">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#indirect-hyperlink-targets">details</a>) <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> <thead> @@ -1189,7 +1189,7 @@ <h4><a href="#contents" name="implicit-hyperlink-targets" class="backref" >Implicit Hyperlink Targets</a></h4> - <p>(<a href="../../ref/rst/restructuredtext.html#implicit-hyperlink-targets">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#implicit-hyperlink-targets">details</a>) <p>Section titles, footnotes, and citations automatically generate hyperlink targets (the title text or footnote/citation label is @@ -1217,12 +1217,12 @@ <h3><a href="#contents" name="directives" class="backref" >Directives</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#directives">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#directives">details</a>) <p>Directives are a general-purpose extension mechanism, a way of adding support for new constructs without adding new syntax. For a description of all standard directives, see <a - href="../../ref/rst/directives.html" >reStructuredText + href="http://docutils.sourceforge.net/docs/ref/rst/directives.html" >reStructuredText Directives</a>. <p><table border="1" width="100%" bgcolor="#ffffcc" cellpadding="3"> @@ -1235,17 +1235,17 @@ <tr valign="top"> <td><samp>For instance:</samp> - <p><samp>.. image:: images/ball1.gif</samp> + <p><samp>.. image:: images/biohazard.png</samp> <td> For instance: - <p><img src="images/ball1.gif" alt="ball1"> + <p><img src="/images/biohazard.png" alt="ball1"> </table> <h3><a href="#contents" name="substitution-references-and-definitions" class="backref" >Substitution References and Definitions</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#substitution-definitions">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#substitution-definitions">details</a>) <p>Substitutions are like inline directives, allowing graphics and arbitrary constructs within text. @@ -1268,7 +1268,7 @@ <td> - <p>The <img src="images/biohazard.png" align="bottom" alt="biohazard"> symbol + <p>The <img src="/images/biohazard.png" align="bottom" alt="biohazard"> symbol must be used on containers used to dispose of medical waste. </table> @@ -1276,7 +1276,7 @@ <h3><a href="#contents" name="comments" class="backref" >Comments</a></h3> - <p>(<a href="../../ref/rst/restructuredtext.html#comments">details</a>) + <p>(<a href="http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#comments">details</a>) <p>Any text which begins with an explicit markup start but doesn't use the syntax of any of the constructs above, is a comment. diff --git a/nikola/data/samplesite/stories/quickstart.txt b/nikola/data/samplesite/stories/quickstart.txt index a74d8fa..1a0f330 100644 --- a/nikola/data/samplesite/stories/quickstart.txt +++ b/nikola/data/samplesite/stories/quickstart.txt @@ -96,7 +96,7 @@ __ quickref.html#escaping middle of a word won't be recognized. See the `markup spec`__ for full details. -__ ../../ref/rst/restructuredtext.html#inline-markup +__ http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#inline-markup Lists @@ -362,18 +362,18 @@ __ quickref.html#directives To include an image in your document, you use the the ``image`` directive__. For example:: - .. image:: images/biohazard.png + .. image:: /images/biohazard.png results in: -.. image:: images/biohazard.png +.. image:: /images/biohazard.png -The ``images/biohazard.png`` part indicates the filename of the image +The ``/images/biohazard.png`` part indicates the filename of the image you wish to appear in the document. There's no restriction placed on the image (format, size etc). If the image is to appear in HTML and you wish to supply additional information, you may:: - .. image:: images/biohazard.png + .. image:: /images/biohazard.png :height: 100 :width: 200 :scale: 50 @@ -381,8 +381,8 @@ you wish to supply additional information, you may:: See the full `image directive documentation`__ for more info. -__ ../../ref/rst/directives.html -__ ../../ref/rst/directives.html#images +__ http://docutils.sourceforge.net/docs/ref/rst/directives.html +__ http://docutils.sourceforge.net/docs/ref/rst/directives.html#images What Next? @@ -401,6 +401,6 @@ list. http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html. .. _reStructuredText Markup Specification: - ../../ref/rst/restructuredtext.html -.. _Docutils-users: ../mailing-lists.html#docutils-users + http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html +.. _Docutils-users: http://docutils.sourceforge.net/docs/user/mailing-lists.html#docutils-users .. _Docutils project web site: http://docutils.sourceforge.net/ diff --git a/nikola/data/themes/default/assets/css/theme.css b/nikola/data/themes/default/assets/css/theme.css index 6f3d4cb..0523ce9 100644 --- a/nikola/data/themes/default/assets/css/theme.css +++ b/nikola/data/themes/default/assets/css/theme.css @@ -49,9 +49,14 @@ div.figure > a > img { margin-right: auto; } -div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { +div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { /* Issue 277 */ border: 1px solid #aaa; border-radius: 5px; } +blockquote p, blockquote { + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} diff --git a/nikola/data/themes/default/assets/js/mathjax.js b/nikola/data/themes/default/assets/js/mathjax.js new file mode 100644 index 0000000..2f4e773 --- /dev/null +++ b/nikola/data/themes/default/assets/js/mathjax.js @@ -0,0 +1,12 @@ +// We wait for the onload function to load MathJax after the page is completely loaded. +// MathJax is loaded 1 unit of time after the page is ready. +// This hack prevent problems when you use social button from addthis. +// +window.onload = function () { + setTimeout(function () { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"; + document.getElementsByTagName("body")[0].appendChild(script); + },1) +}
\ No newline at end of file diff --git a/nikola/data/themes/default/bundles b/nikola/data/themes/default/bundles index 10f44e7..35af9c0 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=bootstrap.min.js,jquery-1.7.2.min.js,jquery.colorbox-min.js,slides.min.jquery.js +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 diff --git a/nikola/data/themes/default/templates/base.tmpl b/nikola/data/themes/default/templates/base.tmpl index 7af8497..c0935a2 100644 --- a/nikola/data/themes/default/templates/base.tmpl +++ b/nikola/data/themes/default/templates/base.tmpl @@ -48,6 +48,10 @@ </ul> <!--End of sidebar content--> </div> + </div> + </div> + </div> +</div> ${analytics} ${late_load_js()} <script type="text/javascript">jQuery("a.image-reference").colorbox({rel:"gal",maxWidth:"80%",maxHeight:"80%",scalePhotos:true});</script> diff --git a/nikola/data/themes/default/templates/base_helper.tmpl b/nikola/data/themes/default/templates/base_helper.tmpl index 51969c9..eb22905 100644 --- a/nikola/data/themes/default/templates/base_helper.tmpl +++ b/nikola/data/themes/default/templates/base_helper.tmpl @@ -4,7 +4,7 @@ <meta name="description" content="${description}" > <meta name="author" content="${blog_author}"> <title>${title} | ${blog_title}</title> - <!-- Le styles --> + ${mathjax_config} %if use_bundles: %if use_cdn: <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet"> @@ -28,7 +28,6 @@ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css"> %endif %endif - <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js" type="text/javascript"></script> <![endif]--> @@ -36,7 +35,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: @@ -73,10 +72,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> diff --git a/nikola/data/themes/default/templates/gallery.tmpl b/nikola/data/themes/default/templates/gallery.tmpl index 3186cc8..09c25cc 100644 --- a/nikola/data/themes/default/templates/gallery.tmpl +++ b/nikola/data/themes/default/templates/gallery.tmpl @@ -6,7 +6,7 @@ <%block name="content"> <ul class="breadcrumb"> % for link, crumb in crumbs: - <li><a href="${link}">/ ${crumb}</a></li> + <li><a href="${link}">/ ${crumb}</a> % endfor </ul> %if text: @@ -16,13 +16,13 @@ %endif <ul> % for folder in folders: - <li><a href="${folder}"><i class="icon-folder-open"></i> ${folder}</a></li> + <li><a href="${folder}"><i class="icon-folder-open"></i> ${folder}</a> % endfor </ul> <ul class="thumbnails"> %for image in images: <li><a href="${image[0]}" class="thumbnail image-reference" ${image[2]}> - <img src="${image[1]}" /></a></li> + <img src="${image[1]}" /></a> %endfor </ul> %if enable_comments: diff --git a/nikola/data/themes/default/templates/index.tmpl b/nikola/data/themes/default/templates/index.tmpl index 1a436e2..4f66867 100644 --- a/nikola/data/themes/default/templates/index.tmpl +++ b/nikola/data/themes/default/templates/index.tmpl @@ -7,7 +7,7 @@ <div class="postbox"> <h1><a href="${post.permalink(lang)}">${post.title(lang)}</a> <small> - ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)} + ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time> </small></h1> <hr> ${post.text(lang, index_teasers)} @@ -16,4 +16,5 @@ % endfor ${helper.html_pager()} ${disqus.html_disqus_script()} + ${helper.mathjax_script(post)} </%block> diff --git a/nikola/data/themes/default/templates/index_helper.tmpl b/nikola/data/themes/default/templates/index_helper.tmpl index 114a730..151b4d2 100644 --- a/nikola/data/themes/default/templates/index_helper.tmpl +++ b/nikola/data/themes/default/templates/index_helper.tmpl @@ -5,13 +5,17 @@ %if prevlink: <li class="previous"> <a href="${prevlink}">← ${messages[lang]["Newer posts"]}</a> - </li> %endif %if nextlink: <li class="next"> <a href="${nextlink}">${messages[lang]["Older posts"]} →</a> - </li> %endif </ul> </div> </%def> + +<%def name="mathjax_script(post)"> + %if any(post.is_mathjax for post in posts): + <script src="/assets/js/mathjax.js" type="text/javascript"></script> + %endif +</%def> diff --git a/nikola/data/themes/default/templates/listing.tmpl b/nikola/data/themes/default/templates/listing.tmpl index 596a704..f279af0 100644 --- a/nikola/data/themes/default/templates/listing.tmpl +++ b/nikola/data/themes/default/templates/listing.tmpl @@ -3,8 +3,18 @@ <%block name="content"> <ul class="breadcrumb"> % for link, crumb in crumbs: - <li><a href="${link}">/ ${crumb}</a></li> + <li><a href="${link}">/${crumb}</a> % endfor </ul> -${code} +<ul class="unstyled"> +% for name in folders: + <li><a href="${name}"><i class="icon-folder-open"></i> ${name}</a> +% endfor +% for name in files: + <li><a href="${name}.html"><i class="icon-file"></i> ${name}</a> +% endfor +</ul> +% if code: + ${code} +% endif </%block> diff --git a/nikola/data/themes/default/templates/post.tmpl b/nikola/data/themes/default/templates/post.tmpl index 672d4f6..22d8a58 100644 --- a/nikola/data/themes/default/templates/post.tmpl +++ b/nikola/data/themes/default/templates/post.tmpl @@ -2,20 +2,28 @@ <%namespace name="helper" file="post_helper.tmpl"/> <%namespace name="disqus" file="disqus_helper.tmpl"/> <%inherit file="base.tmpl"/> +<%block name="extra_head"> +${helper.twitter_card_information(post)} +</%block> <%block name="content"> <div class="postbox"> ${helper.html_title()} <hr> <small> - ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)} + ${messages[lang]["Posted"]}: <time class="published" datetime="${post.date.isoformat()}">${post.date.strftime(date_format)}</time> ${helper.html_translations(post)} - | - <a href="${post.pagenames[lang]+'.txt'}" id="sourcelink">${messages[lang]["Source"]}</a> ${helper.html_tags(post)} </small> <hr> ${post.text(lang)} ${helper.html_pager(post)} ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)} + ${helper.mathjax_script(post)} </div> </%block> + +<%block name="sourcelink"> + <li> + <a href="${post.pagenames[lang]+post.source_ext()}" id="sourcelink">${messages[lang]["Source"]}</a> + </li> +</%block> diff --git a/nikola/data/themes/default/templates/post_helper.tmpl b/nikola/data/themes/default/templates/post_helper.tmpl index ab08359..911a831 100644 --- a/nikola/data/themes/default/templates/post_helper.tmpl +++ b/nikola/data/themes/default/templates/post_helper.tmpl @@ -33,12 +33,39 @@ %if post.prev_post: <li class="previous"> <a href="${post.prev_post.permalink(lang)}">← ${messages[lang]["Previous post"]}</a> - </li> %endif %if post.next_post: <li class="next"> <a href="${post.next_post.permalink(lang)}">${messages[lang]["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)}"> + %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(lang)[:70]}"> + %if post.description(lang): + <meta name="og:description" content="${post.description(lang)[:200]}"> + %else: + <meta name="og:description" content="${post.text(lang, strip_html=True)[:200]}"> + %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/jinja-default/templates/base.tmpl b/nikola/data/themes/jinja-default/templates/base.tmpl index 32e5d48..97cddff 100644 --- a/nikola/data/themes/jinja-default/templates/base.tmpl +++ b/nikola/data/themes/jinja-default/templates/base.tmpl @@ -93,15 +93,19 @@ <!-- 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> <!-- End of social buttons --> {% endif %} + </div> + </div> + </div> +</div> {{analytics}} <!-- late load javascript --> {% if use_bundles %} diff --git a/nikola/data/themes/jinja-default/templates/gallery.tmpl b/nikola/data/themes/jinja-default/templates/gallery.tmpl index 34ff439..9b16df6 100644 --- a/nikola/data/themes/jinja-default/templates/gallery.tmpl +++ b/nikola/data/themes/jinja-default/templates/gallery.tmpl @@ -4,7 +4,7 @@ {% block content %} <ul class="breadcrumb"> {% for link, crumb in crumbs %} - <li><a href="{{link}}">/ {{crumb}}</a></li> + <li><a href="{{link}}">/ {{crumb}}</a> {% endfor %} </ul> {% if text %} @@ -14,13 +14,13 @@ {% endif %} <ul> {% for folder in folders %} - <li><a href="{{folder}}"><i class="icon-folder-open"></i> {{folder}}</a></li> + <li><a href="{{folder}}"><i class="icon-folder-open"></i> {{folder}}</a> {% endfor %} </ul> <ul class="thumbnails"> {% for image in images %} - <li><a href="{{image[0]}}" class="thumbnail image-reference"><img src="{{image[2]}}" /></a></li> - <img src="{{image[1]}}" /></a></li> + <li><a href="{{image[0]}}" class="thumbnail image-reference" {{image[2]}}> + <img src="{{image[1]}}" /></a> {% endfor %} </ul> {%if enable_comments %} diff --git a/nikola/data/themes/jinja-default/templates/index.tmpl b/nikola/data/themes/jinja-default/templates/index.tmpl index ad54c19..ab0392c 100644 --- a/nikola/data/themes/jinja-default/templates/index.tmpl +++ b/nikola/data/themes/jinja-default/templates/index.tmpl @@ -19,12 +19,10 @@ {%if prevlink %} <li class="previous"> <a href="{{prevlink}}">← {{messages[lang]["Newer posts"]}}</a> - </li> {% endif %} {% if nextlink %} <li class="next"> - <a href="{{nextlink}}">${messages[lang]["Older posts"]} →</a> - </li> + <a href="{{nextlink}}">{{messages[lang]["Older posts"]}} →</a> {% endif %} </ul> diff --git a/nikola/data/themes/jinja-default/templates/listing.tmpl b/nikola/data/themes/jinja-default/templates/listing.tmpl index 8310635..493624a 100644 --- a/nikola/data/themes/jinja-default/templates/listing.tmpl +++ b/nikola/data/themes/jinja-default/templates/listing.tmpl @@ -2,7 +2,7 @@ {% block content %} <ul class="breadcrumb"> {% for link, crumb in crumbs %} - <li><a href="{{link}}">/ {{crumb}}</a></li> + <li><a href="{{link}}">/ {{crumb}}</a> {% endfor %} </ul> {{code}} diff --git a/nikola/data/themes/jinja-default/templates/post.tmpl b/nikola/data/themes/jinja-default/templates/post.tmpl index 2a356c5..d14e973 100644 --- a/nikola/data/themes/jinja-default/templates/post.tmpl +++ b/nikola/data/themes/jinja-default/templates/post.tmpl @@ -32,12 +32,10 @@ {%if post.prev_post %} <li class="previous"> <a href="{{rel_link(permalink, post.prev_post.permalink(lang))}}">← {{messages[lang]["Previous post"]}}</a> - </li> {% endif %} {%if post.next_post %} <li class="next"> <a href="{{rel_link(permalink, post.next_post.permalink(lang))}}">{{messages[lang]["Next post"]}} →</a> - </li> {% endif %} </ul> {% if disqus_forum %} diff --git a/nikola/data/themes/site/README b/nikola/data/themes/site/README index 6cbc7dd..c89543c 100644 --- a/nikola/data/themes/site/README +++ b/nikola/data/themes/site/README @@ -29,4 +29,4 @@ default duckduckgo search form: <input type="submit" value="DuckDuckGo Search" style="visibility: hidden;" /> </form> <!-- End of custom search --> - """ % BLOG_URL + """ % SITE_URL diff --git a/nikola/data/themes/site/assets/css/theme.css b/nikola/data/themes/site/assets/css/theme.css index 0183c69..aa0ee4a 100644 --- a/nikola/data/themes/site/assets/css/theme.css +++ b/nikola/data/themes/site/assets/css/theme.css @@ -52,8 +52,15 @@ div.figure > a > img { margin-right: auto; } -div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { +div.sidebar, div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { /* Issue 277 */ border: 1px solid #aaa; border-radius: 5px; } + +blockquote p, blockquote { + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + diff --git a/nikola/data/themes/site/templates/post.tmpl b/nikola/data/themes/site/templates/post.tmpl deleted file mode 100644 index 785385f..0000000 --- a/nikola/data/themes/site/templates/post.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace name="helper" file="post_helper.tmpl"/> -<%namespace name="disqus" file="disqus_helper.tmpl"/> -<%inherit file="base.tmpl"/> -<%block name="content"> - <div class="postbox"> - ${helper.html_title()} - <hr> - <small> - ${messages[lang]["Posted"]}: ${post.date.strftime(date_format)} - ${helper.html_translations(post)} - ${helper.html_tags(post)} - </small> - <hr> - ${post.text(lang)} - ${helper.html_pager(post)} - ${disqus.html_disqus(post.permalink(absolute=True), post.title(lang), post.base_path)} - </div> -</%block> - -<%block name="sourcelink"> - <li> - <a href="${post.pagenames[lang]+post.source_ext()}" id="sourcelink">${messages[lang]["Source"]}</a> - </li> -</%block> diff --git a/nikola/filters.py b/nikola/filters.py index 4a63cb4..a3bff81 100644 --- a/nikola/filters.py +++ b/nikola/filters.py @@ -80,9 +80,10 @@ def tidy(inplace): # Tidy will give error exits, that we will ignore. output = subprocess.check_output("tidy -m -w 90 --indent no --quote-marks" - "no --keep-time yes --tidy-mark no '%s';" - "exit 0" % inplace, - stderr=subprocess.STDOUT, shell=True) + "no --keep-time yes --tidy-mark no " + "--force-output yes '{0}'; exit 0".format( + inplace), stderr=subprocess.STDOUT, + shell=True) for line in output.split("\n"): if "Warning:" in line: @@ -101,7 +102,15 @@ def tidy(inplace): elif '<table> lacks "summary" attribute' in line: # Happens for tables, TODO: Check this is normal. continue + elif 'proprietary attribute "data-toggle"' in line or \ + 'proprietary attribute "data-target"': + # Some of our own tricks + continue else: assert False, (inplace, line) elif "Error:" in line: - assert False, line + if '<time> is not recognized' in line: + # False alarm, time is proper HTML5. + continue + else: + assert False, line diff --git a/nikola/main.py b/nikola/main.py new file mode 100644 index 0000000..b390387 --- /dev/null +++ b/nikola/main.py @@ -0,0 +1,125 @@ +# -*- 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 sys +from operator import attrgetter + +from doit.loader import generate_tasks +from doit.cmd_base import TaskLoader +from doit.reporter import ExecutedOnlyReporter +from doit.doit_cmd import DoitMain +from doit.cmd_help import Help as DoitHelp +from doit.cmd_run import Run as DoitRun + +from .nikola import Nikola + + +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: + config = {} + + site = Nikola(**config) + return DoitNikola(site).run(args) + + +class Help(DoitHelp): + """show Nikola usage instead of doit """ + + @staticmethod + def print_usage(cmds): + """print nikola "usage" (basic help) instructions""" + print("Nikola") + print("Available commands:") + for cmd in sorted(cmds.values(), key=attrgetter('name')): + print(" nikola %s \t\t %s" % (cmd.name, cmd.doc_purpose)) + print("") + print(" nikola help show help / reference") + print(" nikola help <command> show command usage") + print(" nikola help <task-name> show task usage") + + +class Build(DoitRun): + """expose "run" command as "build" for backward compatibility""" + pass + + +class NikolaTaskLoader(TaskLoader): + """custom task loader to get tasks from Nikola instead of dodo.py file""" + def __init__(self, nikola): + self.nikola = nikola + + def load_tasks(self, cmd, opt_values, pos_args): + DOIT_CONFIG = { + 'reporter': ExecutedOnlyReporter, + 'default_tasks': ['render_site'], + } + tasks = generate_tasks('render_site', self.nikola.gen_tasks()) + return tasks, DOIT_CONFIG + + +class DoitNikola(DoitMain): + # overwite help command + DOIT_CMDS = list(DoitMain.DOIT_CMDS) + [Help, Build] + TASK_LOADER = NikolaTaskLoader + + def __init__(self, nikola): + self.nikola = nikola + self.task_loader = self.TASK_LOADER(nikola) + + def get_commands(self): + # core doit commands + cmds = DoitMain.get_commands(self) + + # load nikola commands + for name, cmd in self.nikola.commands.items(): + cmds[name] = cmd + return cmds + + def run(self, cmd_args): + sub_cmds = self.get_commands() + args = self.process_args(cmd_args) + + if len(args) == 0 or args == ["--help"]: + cmd_args = ['help'] + args = ['help'] + + if len(args) == 0 or args[0] not in sub_cmds.keys() or \ + args[0] in ('run', 'build'): + # Check for conf.py before launching run + if not self.nikola.configured: + print("This command needs to run inside an " + "existing Nikola site.") + return False + super(DoitNikola, self).run(cmd_args) diff --git a/nikola/nikola.py b/nikola/nikola.py index 88de88c..a1506e7 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -27,6 +27,7 @@ from __future__ import print_function, unicode_literals from collections import defaultdict from copy import copy import glob +import gzip import os import sys try: @@ -36,6 +37,7 @@ except ImportError: import lxml.html from yapsy.PluginManager import PluginManager +import pytz if os.getenv('DEBUG'): import logging @@ -75,6 +77,10 @@ class Nikola(object): self.timeline = [] self.pages = [] self._scanned = False + if not config: + self.configured = False + else: + self.configured = True # This is the default config # TODO: fill it @@ -90,12 +96,15 @@ class Nikola(object): 'DATE_FORMAT': '%Y-%m-%d %H:%M', 'DEFAULT_LANG': "en", 'DEPLOY_COMMANDS': [], + 'DISABLED_PLUGINS': (), 'DISQUS_FORUM': 'nikolademo', 'FAVICONS': {}, 'FILE_METADATA_REGEXP': None, 'FILES_FOLDERS': {'files': ''}, 'FILTERS': {}, 'GALLERY_PATH': 'galleries', + 'GZIP_FILES': False, + 'GZIP_EXTENSIONS': ('.txt', '.htm', '.html', '.css', '.js', '.json'), 'INDEX_DISPLAY_POST_COUNT': 10, 'INDEX_TEASERS': False, 'INDEXES_TITLE': "", @@ -104,6 +113,7 @@ class Nikola(object): 'LICENSE': '', 'LISTINGS_FOLDER': 'listings', 'MAX_IMAGE_SIZE': 1280, + 'MATHJAX_CONFIG': '', 'OUTPUT_FOLDER': 'output', 'post_compilers': { "rest": ('.txt', '.rst'), @@ -129,10 +139,13 @@ class Nikola(object): 'TAG_PATH': 'categories', 'TAG_PAGES_ARE_INDEXES': False, 'THEME': 'site', + 'THEME_REVEAL_CONGIF_SUBTHEME': 'sky', + 'THEME_REVEAL_CONGIF_TRANSITION': 'cube', 'THUMBNAIL_SIZE': 180, 'USE_BUNDLES': True, 'USE_CDN': False, 'USE_FILENAME_AS_TITLE': True, + 'TIMEZONE': None, } self.config.update(config) @@ -145,6 +158,18 @@ class Nikola(object): self.MESSAGES = utils.load_messages(self.THEMES, self.config['TRANSLATIONS']) + # 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'] + + # BASE_URL defaults to SITE_URL + if 'BASE_URL' not in self.config: + self.config['BASE_URL'] = self.config.get('SITE_URL') + self.plugin_manager = PluginManager(categories_filter={ "Command": Command, "Task": Task, @@ -157,24 +182,28 @@ class Nikola(object): str(os.path.join(os.path.dirname(__file__), 'plugins')), str(os.path.join(os.getcwd(), 'plugins')), ]) + self.plugin_manager.collectPlugins() 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") + 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 # Activate all task plugins - for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"): - self.plugin_manager.activatePluginByName(pluginInfo.name) - pluginInfo.plugin_object.set_site(self) - - for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"): - self.plugin_manager.activatePluginByName(pluginInfo.name) - pluginInfo.plugin_object.set_site(self) + 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) + continue + self.plugin_manager.activatePluginByName(pluginInfo.name) + pluginInfo.plugin_object.set_site(self) # set global_context for template rendering self.GLOBAL_CONTEXT = { @@ -195,20 +224,29 @@ class Nikola(object): self.GLOBAL_CONTEXT['use_bundles'] = self.config['USE_BUNDLES'] self.GLOBAL_CONTEXT['use_cdn'] = self.config.get("USE_CDN") self.GLOBAL_CONTEXT['favicons'] = self.config['FAVICONS'] - self.GLOBAL_CONTEXT['date_format'] = self.config.get('DATE_FORMAT', '%Y-%m-%d %H:%M') + self.GLOBAL_CONTEXT['date_format'] = self.config.get( + 'DATE_FORMAT', '%Y-%m-%d %H:%M') self.GLOBAL_CONTEXT['blog_author'] = self.config.get('BLOG_AUTHOR') self.GLOBAL_CONTEXT['blog_title'] = self.config.get('BLOG_TITLE') - self.GLOBAL_CONTEXT['blog_url'] = self.config.get('BLOG_URL') + + self.GLOBAL_CONTEXT['blog_url'] = self.config.get('SITE_URL', self.config.get('BLOG_URL')) self.GLOBAL_CONTEXT['blog_desc'] = self.config.get('BLOG_DESCRIPTION') self.GLOBAL_CONTEXT['analytics'] = self.config.get('ANALYTICS') self.GLOBAL_CONTEXT['translations'] = self.config.get('TRANSLATIONS') self.GLOBAL_CONTEXT['license'] = self.config.get('LICENSE') self.GLOBAL_CONTEXT['search_form'] = self.config.get('SEARCH_FORM') self.GLOBAL_CONTEXT['disqus_forum'] = self.config.get('DISQUS_FORUM') - self.GLOBAL_CONTEXT['content_footer'] = self.config.get('CONTENT_FOOTER') + self.GLOBAL_CONTEXT['mathjax_config'] = self.config.get( + 'MATHJAX_CONFIG') + self.GLOBAL_CONTEXT['subtheme'] = self.config.get('THEME_REVEAL_CONGIF_SUBTHEME') + self.GLOBAL_CONTEXT['transition'] = self.config.get('THEME_REVEAL_CONGIF_TRANSITION') + self.GLOBAL_CONTEXT['content_footer'] = self.config.get( + '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['twitter_card'] = self.config.get( + 'TWITTER_CARD', {}) self.GLOBAL_CONTEXT.update(self.config.get('GLOBAL_CONTEXT', {})) @@ -226,8 +264,8 @@ class Nikola(object): pi = self.plugin_manager.getPluginByName( template_sys_name, "TemplateSystem") if pi is None: - sys.stderr.write("Error loading %s template system plugin\n" % - template_sys_name) + sys.stderr.write("Error loading {0} template system " + "plugin\n".format(template_sys_name)) sys.exit(1) self.template_system = pi.plugin_object lookup_dirs = [os.path.join(utils.get_theme_path(name), "templates") @@ -265,12 +303,12 @@ class Nikola(object): exit("Your file extension->compiler definition is" "ambiguous.\nPlease remove one of the file extensions" "from 'post_compilers' in conf.py\n(The error is in" - "one of %s)" % ', '.join(langs)) + "one of {0})".format(', '.join(langs))) elif len(langs) > 1: langs = langs[:1] else: exit("post_compilers in conf.py does not tell me how to " - "handle '%s' extensions." % ext) + "handle '{0}' extensions.".format(ext)) lang = langs[0] compile_html = self.compilers[lang] @@ -292,10 +330,12 @@ class Nikola(object): url_part = output_name.decode('utf8')[len(self.config["OUTPUT_FOLDER"]) + 1:] - # This is to support windows paths - url_part = "/".join(url_part.split(os.sep)) - - src = urljoin(self.config["BLOG_URL"], url_part) + # Treat our site as if output/ is "/" and then make all URLs relative, + # making the site "relocatable" + src = os.sep + url_part + src = os.path.normpath(src) + # The os.sep is because normpath will change "/" to "\" on windows + src = "/".join(src.split(os.sep)) parsed_src = urlsplit(src) src_elems = parsed_src.path.split('/')[1:] @@ -398,7 +438,7 @@ class Nikola(object): if name not in [None, 0]: path = [_f for _f in [self.config['TRANSLATIONS'][lang], self.config['INDEX_PATH'], - 'index-%s.html' % name] if _f] + 'index-{0}.html'.format(name)] if _f] else: path = [_f for _f in [self.config['TRANSLATIONS'][lang], self.config['INDEX_PATH'], 'index.html'] @@ -434,13 +474,13 @@ class Nikola(object): def abs_link(self, dst): # Normalize - dst = urljoin(self.config['BLOG_URL'], dst) + dst = urljoin(self.config['BASE_URL'], dst) return urlparse(dst).path def rel_link(self, src, dst): # Normalize - src = urljoin(self.config['BLOG_URL'], src) + src = urljoin(self.config['BASE_URL'], src) dst = urljoin(src, dst) # Avoid empty links. if src == dst: @@ -471,19 +511,67 @@ class Nikola(object): return exists def gen_tasks(self): - task_dep = [] + + def create_gzipped_copy(in_path, out_path): + with gzip.GzipFile(out_path, 'wb+') as outf: + with open(in_path, 'rb') as inf: + outf.write(inf.read()) + + def flatten(task): + if isinstance(task, dict): + yield task + else: + for t in task: + for ft in flatten(t): + yield ft + + def add_gzipped_copies(task): + if not self.config['GZIP_FILES']: + return None + gzip_task = { + 'file_dep': [], + 'targets': [], + 'actions': [], + 'basename': 'gzip', + 'name': task.get('name', 'unknown'), + 'clean': True, + } + targets = task.get('targets', []) + flag = False + for target in targets: + ext = os.path.splitext(target)[1] + if (ext.lower() in self.config['GZIP_EXTENSIONS'] and + target.startswith(self.config['OUTPUT_FOLDER'])): + flag = True + gzipped = target + '.gz' + gzip_task['file_dep'].append(target) + gzip_task['targets'].append(gzipped) + gzip_task['actions'].append((create_gzipped_copy, (target, gzipped))) + if not flag: + return None + return gzip_task + + if self.config['GZIP_FILES']: + task_dep = ['gzip'] + else: + task_dep = [] for pluginInfo in self.plugin_manager.getPluginsOfCategory("Task"): - for task in pluginInfo.plugin_object.gen_tasks(): + for task in flatten(pluginInfo.plugin_object.gen_tasks()): + gztask = add_gzipped_copies(task) + if gztask: + yield gztask yield task if pluginInfo.plugin_object.is_default: task_dep.append(pluginInfo.plugin_object.name) for pluginInfo in self.plugin_manager.getPluginsOfCategory("LateTask"): for task in pluginInfo.plugin_object.gen_tasks(): + gztask = add_gzipped_copies(task) + if gztask: + yield gztask yield task if pluginInfo.plugin_object.is_default: task_dep.append(pluginInfo.plugin_object.name) - yield { 'name': b'all', 'actions': None, @@ -493,60 +581,65 @@ class Nikola(object): def scan_posts(self): """Scan all the posts.""" - if not self._scanned: - print("Scanning posts", end='') - targets = set([]) - for wildcard, destination, template_name, use_in_feeds in \ - self.config['post_pages']: - print(".", end='') - base_len = len(destination.split(os.sep)) - dirname = os.path.dirname(wildcard) - for dirpath, _, _ in os.walk(dirname): - dir_glob = os.path.join(dirpath, - os.path.basename(wildcard)) - dest_dir = os.path.join(*([destination] + - dirpath.split( - os.sep)[base_len:])) - for base_path in glob.glob(dir_glob): - post = Post( - base_path, - self.config['CACHE_FOLDER'], - dest_dir, - use_in_feeds, - self.config['TRANSLATIONS'], - self.config['DEFAULT_LANG'], - self.config['BLOG_URL'], - self.MESSAGES, - template_name, - self.config['FILE_METADATA_REGEXP']) - for lang, langpath in list( - self.config['TRANSLATIONS'].items()): - dest = (destination, langpath, dir_glob, - post.pagenames[lang]) - if dest in targets: - raise Exception( - 'Duplicated output path %r in post %r' % - (post.pagenames[lang], 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_tag[tag].append(post.post_name) - else: - self.pages.append(post) - for name, post in list(self.global_data.items()): - self.timeline.append(post) - self.timeline.sort(key=lambda p: p.date) - self.timeline.reverse() - post_timeline = [p for p in self.timeline if p.use_in_feeds] - for i, p in enumerate(post_timeline[1:]): - p.next_post = post_timeline[i] - for i, p in enumerate(post_timeline[:-1]): - p.prev_post = post_timeline[i + 1] - self._scanned = True - print("done!") + if self._scanned: + return + + print("Scanning posts", end='') + tzinfo = None + if self.config['TIMEZONE'] is not None: + tzinfo = pytz.timezone(self.config['TIMEZONE']) + targets = set([]) + for wildcard, destination, template_name, use_in_feeds in \ + self.config['post_pages']: + print(".", end='') + dirname = os.path.dirname(wildcard) + for dirpath, _, _ in os.walk(dirname): + 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): + post = Post( + base_path, + self.config['CACHE_FOLDER'], + dest_dir, + use_in_feeds, + self.config['TRANSLATIONS'], + self.config['DEFAULT_LANG'], + self.config['BASE_URL'], + self.MESSAGES, + template_name, + self.config['FILE_METADATA_REGEXP'], + tzinfo, + ) + for lang, langpath in list( + self.config['TRANSLATIONS'].items()): + dest = (destination, langpath, dir_glob, + post.pagenames[lang]) + if dest in targets: + raise Exception('Duplicated output path {0!r} ' + 'in post {1!r}'.format( + post.pagenames[lang], + 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_tag[tag].append(post.post_name) + else: + self.pages.append(post) + for name, post in list(self.global_data.items()): + self.timeline.append(post) + self.timeline.sort(key=lambda p: p.date) + self.timeline.reverse() + post_timeline = [p for p in self.timeline if p.use_in_feeds] + for i, p in enumerate(post_timeline[1:]): + p.next_post = post_timeline[i] + for i, p in enumerate(post_timeline[:-1]): + p.prev_post = post_timeline[i + 1] + self._scanned = True + print("done!") def generic_page_renderer(self, lang, post, filters): """Render post fragments to final HTML pages.""" diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 8c69dec..cff9b65 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -31,6 +31,7 @@ __all__ = [ ] from yapsy.IPlugin import IPlugin +from doit.cmd_base import Command as DoitCommand class BasePlugin(IPlugin): @@ -41,16 +42,57 @@ class BasePlugin(IPlugin): self.site = site -class Command(BasePlugin): - """These plugins are exposed via the command line.""" +class Command(BasePlugin, DoitCommand): + """These plugins are exposed via the command line. + They implement the doit Command interface.""" name = "dummy_command" - short_help = "A short explanation." + doc_purpose = "A short explanation." + doc_usage = "" + doc_description = None # None value will completely ommit line from doc + # see http://python-doit.sourceforge.net/cmd_run.html#parameters + cmd_options = () + needs_config = True + + def __init__(self, *args, **kwargs): + BasePlugin.__init__(self, *args, **kwargs) + DoitCommand.__init__(self) + + def execute(self, options={}, args=[]): + """Check if the command can run in the current environment, + fail if needed, or call _execute.""" + if self.needs_config and not self.site.configured: + print("This command needs to run inside an existing Nikola site.") + return False + self._execute(options, args) + + def _execute(self, options, args): + """Do whatever this command does. + @param options (dict) with values from cmd_options + @param args (list) list of positional arguments + """ + raise NotImplementedError() + + +def help(self): + """return help text""" + text = [] + text.append("Purpose: %s" % self.doc_purpose) + text.append("Usage: nikola %s %s" % (self.name, self.doc_usage)) + text.append('') + + text.append("Options:") + for opt in self.options: + text.extend(opt.help_doc()) + + if self.doc_description is not None: + text.append("") + text.append("Description:") + text.append(self.doc_description) + return "\n".join(text) - def run(self): - """Do whatever this command does.""" - raise Exception("Implement Me First") +DoitCommand.help = help class BaseTask(BasePlugin): @@ -64,7 +106,7 @@ class BaseTask(BasePlugin): def gen_tasks(self): """Task generator.""" - raise Exception("Implement Me First") + raise NotImplementedError() class Task(BaseTask): @@ -84,11 +126,11 @@ class TemplateSystem(object): def set_directories(self, directories, cache_folder): """Sets the list of folders where templates are located and cache.""" - raise Exception("Implement Me First") + raise NotImplementedError() def template_deps(self, template_name): """Returns filenames which are dependencies for a template.""" - raise Exception("Implement Me First") + raise NotImplementedError() def render_template(name, output_name, context): """Renders template to a file using context. @@ -96,7 +138,7 @@ class TemplateSystem(object): This must save the data to output_name *and* return it so that the caller may do additional processing. """ - raise Exception("Implement Me First") + raise NotImplementedError() class PageCompiler(object): @@ -106,9 +148,9 @@ class PageCompiler(object): def compile_html(self, source, dest): """Compile the source, save it on dest.""" - raise Exception("Implement Me First") + raise NotImplementedError() def create_post(self, path, onefile=False, title="", slug="", date="", tags=""): """Create post file with optional metadata.""" - raise Exception("Implement Me First") + raise NotImplementedError() diff --git a/nikola/plugins/__init__.py b/nikola/plugins/__init__.py index 2d9b1b2..b1de7f1 100644 --- a/nikola/plugins/__init__.py +++ b/nikola/plugins/__init__.py @@ -1,4 +1,3 @@ from __future__ import absolute_import from . import command_import_wordpress # NOQA -from . import command_build # NOQA diff --git a/nikola/plugins/command_bootswatch_theme.py b/nikola/plugins/command_bootswatch_theme.py index 6c1061f..8400c9f 100644 --- a/nikola/plugins/command_bootswatch_theme.py +++ b/nikola/plugins/command_bootswatch_theme.py @@ -23,7 +23,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser import os try: @@ -38,37 +37,53 @@ class CommandBootswatchTheme(Command): """Given a swatch name and a parent theme, creates a custom theme.""" name = "bootswatch_theme" + doc_usage = "[options]" + doc_purpose = "Given a swatch name and a parent theme, creates a custom"\ + " theme." + cmd_options = [ + { + 'name': 'name', + 'short': 'n', + 'long': 'name', + 'default': 'custom', + 'type': str, + 'help': 'New theme name (default: custom)', + }, + { + 'name': 'swatch', + 'short': 's', + 'default': 'slate', + 'type': str, + 'help': 'Name of the swatch from bootswatch.com.' + }, + { + 'name': 'parent', + 'short': 'p', + 'long': 'parent', + 'default': 'site', + 'help': 'Parent theme name (default: site)', + }, + ] - def run(self, *args): + def _execute(self, options, args): """Given a swatch name and a parent theme, creates a custom theme.""" if requests is None: print('To use the install_theme command, you need to install the ' '"requests" package.') return - parser = OptionParser(usage="nikola %s [options]" % self.name) - parser.add_option("-n", "--name", dest="name", - help="New theme name (default: custom)", - default='custom') - parser.add_option("-s", "--swatch", dest="swatch", - help="Name of the swatch from bootswatch.com " - "(default: slate)", default='slate') - parser.add_option("-p", "--parent", dest="parent", - help="Parent theme name (default: site)", - default='site') - (options, args) = parser.parse_args(list(args)) - name = options.name - swatch = options.swatch - parent = options.parent + name = options['name'] + swatch = options['swatch'] + parent = options['parent'] - print("Creating '%s' theme from '%s' and '%s'" % ( - name, swatch, parent)) + print("Creating '{0}' theme from '{1}' and '{2}'".format(name, swatch, + parent)) try: os.makedirs(os.path.join('themes', name, 'assets', 'css')) except: pass for fname in ('bootstrap.min.css', 'bootstrap.css'): - url = 'http://bootswatch.com/%s/%s' % (swatch, fname) + url = '/'.join(('http://bootswatch.com', swatch, fname)) print("Downloading: ", url) data = requests.get(url).text with open(os.path.join('themes', name, 'assets', 'css', fname), @@ -77,5 +92,5 @@ class CommandBootswatchTheme(Command): with open(os.path.join('themes', name, 'parent'), 'wb+') as output: output.write(parent) - print('Theme created. Change the THEME setting to "%s" to use it.' % - name) + print('Theme created. Change the THEME setting to "{0}" to use ' + 'it.'.format(name)) diff --git a/nikola/plugins/command_build.plugin b/nikola/plugins/command_build.plugin deleted file mode 100644 index 7d029a7..0000000 --- a/nikola/plugins/command_build.plugin +++ /dev/null @@ -1,10 +0,0 @@ -[Core] -Name = build -Module = command_build - -[Documentation] -Author = Roberto Alsina -Version = 0.1 -Website = http://nikola.ralsina.com.ar -Description = Build the site. - diff --git a/nikola/plugins/command_build.py b/nikola/plugins/command_build.py deleted file mode 100644 index 8a1de5f..0000000 --- a/nikola/plugins/command_build.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2012 Roberto Alsina y otros. - -# Permission is hereby granted, free of charge, to any -# person obtaining a copy of this software and associated -# documentation files (the "Software"), to deal in the -# Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the -# Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice -# shall be included in all copies or substantial portions of -# the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from __future__ import unicode_literals -import os -import tempfile - -from nikola.plugin_categories import Command - - -class CommandBuild(Command): - """Build the site.""" - - name = "build" - - def run(self, *args): - """Build the site using doit.""" - - # FIXME: this is crap, do it right - with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as self.dodo: - self.dodo.write(b''' -import sys -sys.path.insert(0, '.') -from doit.reporter import ExecutedOnlyReporter -DOIT_CONFIG = { - 'reporter': ExecutedOnlyReporter, - 'default_tasks': ['render_site'], -} -from nikola import Nikola -import conf -SITE = Nikola(**conf.__dict__) - - -def task_render_site(): - return SITE.gen_tasks() - ''') - self.dodo.flush() - first = args[0] if args else None - if first in ('auto', 'clean', 'forget', 'ignore', 'list', 'run'): - cmd = first - args = args[1:] - else: - cmd = 'run' - os.system('doit %s -f %s -d . %s' % (cmd, self.dodo.name, - ''.join(args))) - os.unlink(self.dodo.name) diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py index ae19c41..a396f63 100644 --- a/nikola/plugins/command_check.py +++ b/nikola/plugins/command_check.py @@ -23,9 +23,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser import os -import sys try: from urllib import unquote from urlparse import urlparse @@ -42,25 +40,48 @@ class CommandCheck(Command): name = "check" - def run(self, *args): + doc_usage = "-l [--find-sources] | -f" + doc_purpose = "Check links and files in the generated site." + cmd_options = [ + { + 'name': 'links', + 'short': 'l', + 'long': 'check-links', + 'type': bool, + 'default': False, + 'help': 'Check for dangling links', + }, + { + 'name': 'files', + 'short': 'f', + 'long': 'check-files', + 'type': bool, + 'default': False, + 'help': 'Check for unknown files', + }, + { + 'name': 'find_sources', + 'long': 'find-sources', + 'type': bool, + 'default': False, + 'help': 'List possible source files for files with broken links.', + }, + ] + + def _execute(self, options, args): """Check the generated site.""" - parser = OptionParser(usage="nikola %s [options]" % self.name) - parser.add_option('-l', '--check-links', dest='links', - action='store_true', - help='Check for dangling links.') - parser.add_option('-f', '--check-files', dest='files', - action='store_true', help='Check for unknown files.') - - (options, args) = parser.parse_args(list(args)) - if options.links: - scan_links() - if options.files: + if not options['links'] and not options['files']: + print(self.help()) + return False + if options['links']: + scan_links(options['find_sources']) + if options['files']: scan_files() existing_targets = set([]) -def analize(task): +def analize(task, find_sources=False): try: filename = task.split(":")[-1] d = lxml.html.fromstring(open(filename).read()) @@ -79,26 +100,26 @@ def analize(task): if os.path.exists(target_filename): existing_targets.add(target_filename) else: - print("In %s broken link: " % filename, target) - if '--find-sources' in sys.argv: + print("Broken link in {0}: ".format(filename), target) + if find_sources: print("Possible sources:") - print(os.popen( - 'nikola build list --deps %s' % task, 'r').read()) + print(os.popen('nikola list --deps ' + task, + 'r').read()) print("===============================\n") except Exception as exc: print("Error with:", filename, exc) -def scan_links(): +def scan_links(find_sources=False): print("Checking Links:\n===============\n") - for task in os.popen('nikola build list --all', 'r').readlines(): + for task in os.popen('nikola list --all', 'r').readlines(): task = task.strip() if task.split(':')[0] in ('render_tags', 'render_archive', 'render_galleries', 'render_indexes', 'render_pages', 'render_site') and '.html' in task: - analize(task) + analize(task, find_sources) def scan_files(): @@ -106,7 +127,7 @@ def scan_files(): task_fnames = set([]) real_fnames = set([]) # First check that all targets are generated in the right places - for task in os.popen('nikola build list --all', 'r').readlines(): + for task in os.popen('nikola list --all', 'r').readlines(): task = task.strip() if 'output' in task and ':' in task: fname = task.split(':')[-1] diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command_console.py index 7a009fd..4af759f 100644 --- a/nikola/plugins/command_console.py +++ b/nikola/plugins/command_console.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 print_function, unicode_literals + import os from nikola.plugin_categories import Command @@ -31,5 +33,31 @@ class Deploy(Command): """Start debugging console.""" name = "console" - def run(self, *args): - os.system('python -i -c "from nikola.console import *"') + def _execute(self, options, args): + """Start the console.""" + from nikola import Nikola + try: + import conf + SITE = Nikola(**conf.__dict__) + SITE.scan_posts() + print("You can now access your configuration as conf and your " + "site engine as SITE.") + except ImportError: + print("No configuration found.") + import code + try: + import readline + except ImportError: + pass + else: + import rlcompleter + readline.set_completer(rlcompleter.Completer(globals()).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=globals()) diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py index 48d6e91..ffa86ab 100644 --- a/nikola/plugins/command_deploy.py +++ b/nikola/plugins/command_deploy.py @@ -23,7 +23,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser import os from nikola.plugin_categories import Command @@ -33,9 +32,10 @@ class Deploy(Command): """Deploy site. """ name = "deploy" - def run(self, *args): - parser = OptionParser(usage="nikola %s [options]" % self.name) - (options, args) = parser.parse_args(list(args)) + doc_usage = "" + doc_purpose = "Deploy the site." + + def _execute(self, command, args): for command in self.site.config['DEPLOY_COMMANDS']: print("==>", command) os.system(command) diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command_import_blogger.py index aea210a..35a702e 100644 --- a/nikola/plugins/command_import_blogger.py +++ b/nikola/plugins/command_import_blogger.py @@ -27,7 +27,6 @@ import codecs import csv import datetime import os -from optparse import OptionParser import time try: @@ -52,9 +51,63 @@ class CommandImportBlogger(Command): """Import a blogger dump.""" name = "import_blogger" + needs_config = False + doc_usage = "[options] blogger_export_file" + doc_purpose = "Import a blogger dump." + cmd_options = [ + { + 'name': 'output_folder', + 'long': 'output-folder', + 'short': 'o', + 'default': 'new_site', + 'help': 'Location to write imported content.' + }, + { + 'name': 'exclude_drafts', + 'long': 'no-drafts', + 'short': 'd', + 'default': False, + 'type': bool, + 'help': "Don't import drafts", + }, + ] + + def _execute(self, options, args): + """Import a Wordpress blog from an export file into a Nikola site.""" + + # Parse the data + if feedparser is None: + print('To use the import_blogger command,' + ' you have to install the "feedparser" package.') + return + + if not args: + print(self.help()) + return + + options['filename'] = args[0] + self.blogger_export_file = options['filename'] + self.output_folder = options['output_folder'] + self.import_into_existing_site = False + self.exclude_drafts = options['exclude_drafts'] + self.url_map = {} + channel = self.get_channel_from_file(self.blogger_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + self.context['REDIRECTIONS'] = self.configure_redirections( + self.url_map) + + self.import_posts(channel) + self.write_urlmap_csv( + os.path.join(self.output_folder, 'url_map.csv'), self.url_map) + + self.write_configuration(self.get_configuration_output_path( + ), conf_template.render(**self.context)) @classmethod def get_channel_from_file(cls, filename): + if not os.path.isfile(filename): + raise Exception("Missing file: %s" % filename) return feedparser.parse(filename) @staticmethod @@ -65,7 +118,7 @@ class CommandImportBlogger(Command): src = (urlparse(k).path + 'index.html')[1:] dst = (urlparse(v).path) if src == 'index.html': - print("Can't do a redirect for: %r" % k) + print("Can't do a redirect for: {0!r}".format(k)) else: redirections.append((src, dst)) @@ -73,11 +126,11 @@ class CommandImportBlogger(Command): def generate_base_site(self): if not os.path.exists(self.output_folder): - os.system('nikola init --empty %s' % (self.output_folder, )) + os.system('nikola init --empty ' + self.output_folder) else: self.import_into_existing_site = True - print('The folder %s already exists - assuming that this is a ' - 'already existing nikola site.' % self.output_folder) + print('The folder {0} already exists - assuming that this is a ' + 'already existing nikola site.'.format(self.output_folder)) conf_template = Template(filename=os.path.join( os.path.dirname(utils.__file__), 'conf.py.in')) @@ -92,7 +145,7 @@ class CommandImportBlogger(Command): context['BLOG_TITLE'] = channel.feed.title context['BLOG_DESCRIPTION'] = '' # Missing in the dump - context['BLOG_URL'] = channel.feed.link.rstrip('/') + context['SITE_URL'] = channel.feed.link.rstrip('/') context['BLOG_EMAIL'] = channel.feed.author_detail.email context['BLOG_AUTHOR'] = channel.feed.author_detail.name context['POST_PAGES'] = '''( @@ -124,12 +177,8 @@ class CommandImportBlogger(Command): @staticmethod def write_metadata(filename, title, slug, post_date, description, tags): with codecs.open(filename, "w+", "utf8") as fd: - fd.write('%s\n' % title) - fd.write('%s\n' % slug) - fd.write('%s\n' % post_date) - fd.write('%s\n' % ','.join(tags)) - fd.write('\n') - fd.write('%s\n' % description) + fd.write('\n'.join((title, slug, post_date, ','.join(tags), '', + description))) def import_item(self, item, out_folder=None): """Takes an item from the feed and creates a post file.""" @@ -145,8 +194,8 @@ class CommandImportBlogger(Command): # blogger supports empty titles, which Nikola doesn't if not title: - print("Warning: Empty title in post with URL %s. Using NO_TITLE " - "as placeholder, please fix." % link) + print("Warning: Empty title in post with URL {0}. Using NO_TITLE " + "as placeholder, please fix.".format(link)) title = "NO_TITLE" if link_path.lower().endswith('.html'): @@ -179,11 +228,11 @@ class CommandImportBlogger(Command): else: is_draft = False - self.url_map[link] = self.context['BLOG_URL'] + '/' + \ + self.url_map[link] = self.context['SITE_URL'] + '/' + \ out_folder + '/' + slug + '.html' if is_draft and self.exclude_drafts: - print('Draft "%s" will not be imported.' % (title, )) + print('Draft "{0}" will not be imported.'.format(title)) elif content.strip(): # If no content is found, no files are written. content = self.transform_content(content) @@ -195,8 +244,8 @@ class CommandImportBlogger(Command): os.path.join(self.output_folder, out_folder, slug + '.html'), content) else: - print('Not going to import "%s" because it seems to contain' - ' no content.' % (title, )) + print('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) def process_item(self, item): post_type = item.tags[0].term @@ -235,10 +284,10 @@ class CommandImportBlogger(Command): if not self.import_into_existing_site: filename = 'conf.py' else: - filename = 'conf.py.wordpress_import-%s' % datetime.datetime.now( - ).strftime('%Y%m%d_%H%M%s') + filename = 'conf.py.wordpress_import-{0}'.format( + datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to: %s' % config_output_path) + print('Configuration will be written to: ' + config_output_path) return config_output_path @@ -247,54 +296,6 @@ class CommandImportBlogger(Command): with codecs.open(filename, 'w+', 'utf8') as fd: fd.write(rendered_template) - def run(self, *arguments): - """Import a Wordpress blog from an export file into a Nikola site.""" - # Parse the data - if feedparser is None: - print('To use the import_blogger command,' - ' you have to install the "feedparser" package.') - return - - parser = OptionParser( - usage="nikola %s [options] blogger_export_file" % self.name) - parser.add_option('-f', '--filename', dest='filename', - help='Blogger export file from which the import is ' - 'made.') - parser.add_option('-o', '--output-folder', dest='output_folder', - default='new_site', - help='The location into which the imported content ' - 'will be written') - parser.add_option('-d', '--no-drafts', dest='exclude_drafts', - default=False, action="store_true", help='Do not ' - 'import drafts.') - - (options, args) = parser.parse_args(list(arguments)) - - if not options.filename and args: - options.filename = args[0] - - if not options.filename: - parser.print_usage() - return - - self.blogger_export_file = options.filename - self.output_folder = options.output_folder - self.import_into_existing_site = False - self.exclude_drafts = options.exclude_drafts - self.url_map = {} - channel = self.get_channel_from_file(self.blogger_export_file) - self.context = self.populate_context(channel) - conf_template = self.generate_base_site() - self.context['REDIRECTIONS'] = self.configure_redirections( - self.url_map) - - self.import_posts(channel) - self.write_urlmap_csv( - os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - - self.write_configuration(self.get_configuration_output_path( - ), conf_template.render(**self.context)) - def replacer(dst): return links.get(dst, dst) diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py index 07028d8..e7ecca0 100644 --- a/nikola/plugins/command_import_wordpress.py +++ b/nikola/plugins/command_import_wordpress.py @@ -28,7 +28,6 @@ import csv import datetime import os import re -from optparse import OptionParser try: from urlparse import urlparse @@ -53,9 +52,104 @@ class CommandImportWordpress(Command): """Import a wordpress dump.""" name = "import_wordpress" + needs_config = False + doc_usage = "[options] wordpress_export_file" + doc_purpose = "Import a wordpress dump." + cmd_options = [ + { + 'name': 'output_folder', + 'long': 'output-folder', + 'short': 'o', + 'default': 'new_site', + 'help': 'Location to write imported content.' + }, + { + 'name': 'exclude_drafts', + 'long': 'no-drafts', + 'short': 'd', + 'default': False, + 'type': bool, + 'help': "Don't import drafts", + }, + { + 'name': 'squash_newlines', + 'long': 'squash-newlines', + 'default': False, + 'type': bool, + 'help': "Shorten multiple newlines in a row to only two newlines", + }, + { + 'name': 'no_downloads', + 'long': 'no-downloads', + 'default': False, + 'type': bool, + 'help': "Do not try to download files for the import", + }, + ] + + 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.') + return - @staticmethod - def read_xml_file(filename): + if not args: + print(self.help()) + return + + options['filename'] = args[0] + + if len(args) > 1: + options['output_folder'] = args[1] + + self.wordpress_export_file = options['filename'] + self.squash_newlines = options.get('squash_newlines', False) + self.no_downloads = options.get('no_downloads', False) + self.output_folder = options.get('output_folder', 'new_site') + self.import_into_existing_site = False + self.exclude_drafts = options.get('exclude_drafts', False) + self.url_map = {} + channel = self.get_channel_from_file(self.wordpress_export_file) + self.context = self.populate_context(channel) + conf_template = self.generate_base_site() + + self.import_posts(channel) + + self.context['REDIRECTIONS'] = self.configure_redirections( + self.url_map) + self.write_urlmap_csv( + os.path.join(self.output_folder, 'url_map.csv'), self.url_map) + rendered_template = conf_template.render(**self.context) + rendered_template = re.sub('# REDIRECTIONS = ', 'REDIRECTIONS = ', + rendered_template) + self.write_configuration(self.get_configuration_output_path(), + rendered_template) + + @classmethod + def _glue_xml_lines(cls, xml): + new_xml = xml[0] + previous_line_ended_in_newline = new_xml.endswith(b'\n') + previous_line_was_indentet = False + for line in xml[1:]: + if (re.match(b'^[ \t]+', line) and previous_line_ended_in_newline): + new_xml = b''.join((new_xml, line)) + previous_line_was_indentet = True + elif previous_line_was_indentet: + new_xml = b''.join((new_xml, line)) + previous_line_was_indentet = False + else: + new_xml = b'\n'.join((new_xml, line)) + previous_line_was_indentet = False + + previous_line_ended_in_newline = line.endswith(b'\n') + + return new_xml + + @classmethod + def read_xml_file(cls, filename): xml = [] with open(filename, 'rb') as fd: @@ -64,9 +158,8 @@ class CommandImportWordpress(Command): if b'<atom:link rel=' in line: continue xml.append(line) - xml = b'\n'.join(xml) - return xml + return cls._glue_xml_lines(xml) @classmethod def get_channel_from_file(cls, filename): @@ -82,7 +175,7 @@ class CommandImportWordpress(Command): src = (urlparse(k).path + 'index.html')[1:] dst = (urlparse(v).path) if src == 'index.html': - print("Can't do a redirect for: %r" % k) + print("Can't do a redirect for: {0!r}".format(k)) else: redirections.append((src, dst)) @@ -90,11 +183,11 @@ class CommandImportWordpress(Command): def generate_base_site(self): if not os.path.exists(self.output_folder): - os.system('nikola init --empty %s' % (self.output_folder, )) + os.system('nikola init ' + self.output_folder) else: self.import_into_existing_site = True - print('The folder %s already exists - assuming that this is a ' - 'already existing nikola site.' % self.output_folder) + print('The folder {0} already exists - assuming that this is a ' + 'already existing nikola site.'.format(self.output_folder)) conf_template = Template(filename=os.path.join( os.path.dirname(utils.__file__), 'conf.py.in')) @@ -111,15 +204,16 @@ class CommandImportWordpress(Command): 'PUT TITLE HERE') context['BLOG_DESCRIPTION'] = get_text_tag( channel, 'description', 'PUT DESCRIPTION HERE') - context['BLOG_URL'] = get_text_tag(channel, 'link', '#') - author = channel.find('{%s}author' % wordpress_namespace) + context['SITE_URL'] = get_text_tag(channel, 'link', '#') + context['BASE_URL'] = get_text_tag(channel, 'link', '#') + author = channel.find('{{{0}}}author'.format(wordpress_namespace)) context['BLOG_EMAIL'] = get_text_tag( author, - '{%s}author_email' % wordpress_namespace, + '{{{0}}}author_email'.format(wordpress_namespace), "joe@example.com") context['BLOG_AUTHOR'] = get_text_tag( author, - '{%s}author_display_name' % wordpress_namespace, + '{{{0}}}author_display_name'.format(wordpress_namespace), "Joe Example") context['POST_PAGES'] = '''( ("posts/*.wp", "posts", "post.tmpl", True), @@ -134,25 +228,29 @@ class CommandImportWordpress(Command): return context - @staticmethod - def download_url_content_to_file(url, dst_path): + def download_url_content_to_file(self, url, dst_path): + if self.no_downloads: + return + try: with open(dst_path, 'wb+') as fd: fd.write(requests.get(url).content) except requests.exceptions.ConnectionError as err: - print("Downloading %s to %s failed: %s" % (url, dst_path, err)) + print("Downloading {0} to {1} failed: {2}".format(url, dst_path, + err)) def import_attachment(self, item, wordpress_namespace): url = get_text_tag( - item, '{%s}attachment_url' % wordpress_namespace, 'foo') - link = get_text_tag(item, '{%s}link' % wordpress_namespace, 'foo') + item, '{{{0}}}attachment_url'.format(wordpress_namespace), 'foo') + link = get_text_tag(item, '{{{0}}}link'.format(wordpress_namespace), + 'foo') path = urlparse(url).path dst_path = os.path.join(*([self.output_folder, 'files'] + list(path.split('/')))) dst_dir = os.path.dirname(dst_path) if not os.path.isdir(dst_dir): os.makedirs(dst_dir) - print("Downloading %s => %s" % (url, dst_path)) + print("Downloading {0} => {1}".format(url, dst_path)) self.download_url_content_to_file(url, dst_path) dst_url = '/'.join(dst_path.split(os.sep)[2:]) links[link] = '/' + dst_url @@ -173,10 +271,18 @@ class CommandImportWordpress(Command): return new_caption - @classmethod - def transform_content(cls, content): - new_content = cls.transform_sourcecode(content) - return cls.transform_caption(new_content) + def transform_multiple_newlines(self, content): + """Replaces multiple newlines with only two.""" + if self.squash_newlines: + return re.sub(r'\n{3,}', r'\n\n', content) + else: + return content + + def transform_content(self, content): + new_content = self.transform_sourcecode(content) + new_content = self.transform_caption(new_content) + new_content = self.transform_multiple_newlines(new_content) + return new_content @classmethod def write_content(cls, filename, content): @@ -188,13 +294,16 @@ class CommandImportWordpress(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('%s\n' % title) - fd.write('%s\n' % slug) - fd.write('%s\n' % post_date) - fd.write('%s\n' % ','.join(tags)) + 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('%s\n' % description) + fd.write('{0}\n'.format(description)) def import_item(self, item, wordpress_namespace, out_folder=None): """Takes an item from the feed and creates a post file.""" @@ -208,19 +317,19 @@ class CommandImportWordpress(Command): slug = utils.slugify(urlparse(link).path) if not slug: # it happens if the post has no "nice" URL slug = get_text_tag( - item, '{%s}post_name' % wordpress_namespace, None) + item, '{{{0}}}post_name'.format(wordpress_namespace), None) if not slug: # it *may* happen slug = get_text_tag( - item, '{%s}post_id' % wordpress_namespace, None) + item, '{{{0}}}post_id'.format(wordpress_namespace), None) if not slug: # should never happen print("Error converting post:", title) return description = get_text_tag(item, 'description', '') post_date = get_text_tag( - item, '{%s}post_date' % wordpress_namespace, None) + item, '{{{0}}}post_date'.format(wordpress_namespace), None) status = get_text_tag( - item, '{%s}status' % wordpress_namespace, 'publish') + item, '{{{0}}}status'.format(wordpress_namespace), 'publish') content = get_text_tag( item, '{http://purl.org/rss/1.0/modules/content/}encoded', '') @@ -237,13 +346,13 @@ class CommandImportWordpress(Command): continue tags.append(text) - self.url_map[link] = self.context['BLOG_URL'] + '/' + \ - out_folder + '/' + slug + '.html' - if is_draft and self.exclude_drafts: - print('Draft "%s" will not be imported.' % (title, )) + print('Draft "{0}" will not be imported.'.format(title)) elif content.strip(): # If no content is found, no files are written. + self.url_map[link] = self.context['SITE_URL'] + '/' + \ + out_folder + '/' + slug + '.html' + content = self.transform_content(content) self.write_metadata(os.path.join(self.output_folder, out_folder, @@ -253,15 +362,15 @@ class CommandImportWordpress(Command): os.path.join(self.output_folder, out_folder, slug + '.wp'), content) else: - print('Not going to import "%s" because it seems to contain' - ' no content.' % (title, )) + print('Not going to import "{0}" because it seems to contain' + ' no content.'.format(title)) def process_item(self, item): # The namespace usually is something like: # http://wordpress.org/export/1.2/ wordpress_namespace = item.nsmap['wp'] post_type = get_text_tag( - item, '{%s}post_type' % wordpress_namespace, 'post') + item, '{{{0}}}post_type'.format(wordpress_namespace), 'post') if post_type == 'attachment': self.import_attachment(item, wordpress_namespace) @@ -285,10 +394,10 @@ class CommandImportWordpress(Command): if not self.import_into_existing_site: filename = 'conf.py' else: - filename = 'conf.py.wordpress_import-%s' % datetime.datetime.now( - ).strftime('%Y%m%d_%H%M%s') + filename = 'conf.py.wordpress_import-{0}'.format( + datetime.datetime.now().strftime('%Y%m%d_%H%M%s')) config_output_path = os.path.join(self.output_folder, filename) - print('Configuration will be written to: %s' % config_output_path) + print('Configuration will be written to:', config_output_path) return config_output_path @@ -297,53 +406,6 @@ class CommandImportWordpress(Command): with codecs.open(filename, 'w+', 'utf8') as fd: fd.write(rendered_template) - def run(self, *arguments): - """Import a Wordpress blog from an export file into a Nikola site.""" - # Parse the data - if requests is None: - print('To use the import_wordpress command,' - ' you have to install the "requests" package.') - return - - parser = OptionParser(usage="nikola %s [options] " - "wordpress_export_file" % self.name) - parser.add_option('-f', '--filename', dest='filename', - help='WordPress export file from which the import ' - 'made.') - parser.add_option('-o', '--output-folder', dest='output_folder', - default='new_site', help='The location into which ' - 'the imported content will be written') - parser.add_option('-d', '--no-drafts', dest='exclude_drafts', - default=False, action="store_true", help='Do not ' - 'import drafts.') - - (options, args) = parser.parse_args(list(arguments)) - - if not options.filename and args: - options.filename = args[0] - - if not options.filename: - parser.print_usage() - return - - self.wordpress_export_file = options.filename - self.output_folder = options.output_folder - self.import_into_existing_site = False - self.exclude_drafts = options.exclude_drafts - self.url_map = {} - channel = self.get_channel_from_file(self.wordpress_export_file) - self.context = self.populate_context(channel) - conf_template = self.generate_base_site() - self.context['REDIRECTIONS'] = self.configure_redirections( - self.url_map) - - self.import_posts(channel) - self.write_urlmap_csv( - os.path.join(self.output_folder, 'url_map.csv'), self.url_map) - - self.write_configuration(self.get_configuration_output_path( - ), conf_template.render(**self.context)) - def replacer(dst): return links.get(dst, dst) diff --git a/nikola/plugins/command_init.py b/nikola/plugins/command_init.py index e9bd001..bc36266 100644 --- a/nikola/plugins/command_init.py +++ b/nikola/plugins/command_init.py @@ -23,7 +23,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser, OptionGroup import os import shutil import codecs @@ -39,16 +38,23 @@ class CommandInit(Command): name = "init" - usage = """Usage: nikola init folder [options]. - -That will create a sample site in the specified folder. -The destination folder must not exist. -""" + doc_usage = "[--demo] folder" + needs_config = False + doc_purpose = """Create a Nikola site in the specified folder.""" + cmd_options = [ + { + 'name': 'demo', + 'long': 'demo', + 'default': False, + 'type': bool, + 'help': "Create a site filled with example data.", + } + ] SAMPLE_CONF = { 'BLOG_AUTHOR': "Your Name", 'BLOG_TITLE': "Demo Site", - 'BLOG_URL': "http://nikola.ralsina.com.ar", + 'SITE_URL': "http://nikola.ralsina.com.ar", 'BLOG_EMAIL': "joe@demo.site", 'BLOG_DESCRIPTION': "This is a demo site for Nikola.", 'DEFAULT_LANG': "en", @@ -67,7 +73,7 @@ The destination folder must not exist. "wiki": ('.wiki',), "ipynb": ('.ipynb',), "html": ('.html', '.htm') - }""", +}""", 'REDIRECTIONS': '[]', } @@ -95,32 +101,22 @@ The destination folder must not exist. def get_path_to_nikola_modules(): return os.path.dirname(nikola.__file__) - def run(self, *args): + def _execute(self, options={}, args=None): """Create a new site.""" - parser = OptionParser(usage=self.usage) - group = OptionGroup(parser, "Site Options") - group.add_option( - "--empty", action="store_true", dest='empty', default=True, - help="Create an empty site with only a config.") - group.add_option("--demo", action="store_false", dest='empty', - help="Create a site filled with example data.") - parser.add_option_group(group) - (options, args) = parser.parse_args(list(args)) - if not args: print("Usage: nikola init folder [options]") - return + return False target = args[0] if target is None: print(self.usage) else: - if options.empty: + if not options or not options.get('demo'): self.create_empty_site(target) - print('Created empty site at %s.' % target) + print('Created empty site at {0}.'.format(target)) else: self.copy_sample_site(target) - print("A new site with example data has been created at %s." - % target) + print("A new site with example data has been created at " + "{0}.".format(target)) print("See README.txt in that folder for more information.") self.create_configuration(target) diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py index 0dc000b..04a2cce 100644 --- a/nikola/plugins/command_install_theme.py +++ b/nikola/plugins/command_install_theme.py @@ -23,7 +23,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser import os import json from io import BytesIO @@ -41,31 +40,39 @@ class CommandInstallTheme(Command): """Start test server.""" name = "install_theme" + doc_usage = "[[-u] theme_name] | [[-u] -l]" + doc_purpose = "Install theme into current site." + cmd_options = [ + { + 'name': 'list', + 'short': 'l', + 'long': 'list', + 'type': bool, + 'default': False, + 'help': 'Show list of available themes.' + }, + { + 'name': 'url', + 'short': 'u', + 'long': 'url', + 'type': str, + 'help': "URL for the theme repository (default: " + "http://nikola.ralsina.com.ar/themes/index.json)", + 'default': 'http://nikola.ralsina.com.ar/themes/index.json' + }, + ] - def run(self, *args): + def _execute(self, options, args): """Install theme into current site.""" - if requests is None: - print('To use the install_theme command, you need to install the ' - '"requests" package.') - return - parser = OptionParser(usage="nikola %s [options]" % self.name) - parser.add_option("-l", "--list", dest="list", action="store_true", - help="Show list of available themes.") - parser.add_option("-n", "--name", dest="name", help="Theme name", - default=None) - parser.add_option("-u", "--url", dest="url", help="URL for the theme " - "repository" "(default: " - "http://nikola.ralsina.com.ar/themes/index.json)", - default='http://nikola.ralsina.com.ar/themes/' - 'index.json') - (options, args) = parser.parse_args(list(args)) - - listing = options.list - name = options.name - url = options.url + listing = options['list'] + url = options['url'] + if args: + name = args[0] + else: + name = None if name is None and not listing: - print("This command needs either the -n or the -l option.") + print("This command needs either a theme name or the -l option.") return False data = requests.get(url).text data = json.loads(data) @@ -84,11 +91,11 @@ class CommandInstallTheme(Command): os.makedirs("themes") except: raise OSError("mkdir 'theme' error!") - print('Downloading: %s' % data[name]) + print('Downloading: ' + data[name]) zip_file = BytesIO() zip_file.write(requests.get(data[name]).content) - print('Extracting: %s into themes' % name) + print('Extracting: {0} into themes'.format(name)) utils.extract_all(zip_file) else: - print("Can't find theme %s" % name) + print("Can't find theme " + name) return False diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py index 9b6397b..a823da3 100644 --- a/nikola/plugins/command_new_post.py +++ b/nikola/plugins/command_new_post.py @@ -25,7 +25,6 @@ from __future__ import unicode_literals, print_function import codecs import datetime -from optparse import OptionParser import os import sys @@ -51,9 +50,9 @@ 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" - "a %s in format %s. You may want to tweak " - "post_compilers or post_pages in conf.py" % - (type_name, compiler)) + "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] @@ -61,42 +60,88 @@ class CommandNewPost(Command): """Create a new post.""" name = "new_post" - - def run(self, *args): - """Create a new post.""" + doc_usage = "[options] [path]" + doc_purpose = "Create a new blog post or site page." + cmd_options = [ + { + 'name': 'is_page', + 'short': 'p', + 'long': 'page', + 'type': bool, + 'default': False, + 'help': 'Create a page instead of a blog post.' + }, + { + 'name': 'title', + 'short': 't', + 'long': 'title', + 'type': str, + 'default': '', + 'help': 'Title for the page/post.' + }, + { + 'name': 'tags', + 'long': 'tags', + 'type': str, + 'default': '', + 'help': 'Comma-separated tags for the page/post.' + }, + { + 'name': 'onefile', + 'short': '1', + 'type': bool, + 'default': False, + 'help': 'Create post with embedded metadata (single file format)' + }, + { + 'name': 'twofile', + 'short': '2', + 'type': bool, + 'default': False, + 'help': 'Create post with separate metadata (two file format)' + }, + { + 'name': 'post_format', + 'short': 'f', + 'long': 'format', + 'type': str, + 'default': 'rest', + 'help': 'Markup format for post, one of rest, markdown, wiki, ' + 'bbcode, html, textile, txt2tags', + } + ] + + def _execute(self, options, args): + """Create a new post or page.""" compiler_names = [p.name for p in self.site.plugin_manager.getPluginsOfCategory( "PageCompiler")] - parser = OptionParser(usage="nikola %s [options]" % self.name) - parser.add_option('-p', '--page', dest='is_post', action='store_false', - default=True, help='Create a page instead of a blog ' - 'post.') - parser.add_option('-t', '--title', dest='title', help='Title for the ' - 'page/post.', default=None) - parser.add_option('--tags', dest='tags', help='Comma-separated tags ' - 'for the page/post.', default='') - parser.add_option('-1', dest='onefile', action='store_true', - help='Create post with embedded metadata (single ' - 'file format).', - default=self.site.config.get('ONE_FILE_POSTS', True)) - parser.add_option('-2', dest='onefile', action='store_false', - help='Create post with separate metadata (two file ' - 'format).', - default=self.site.config.get('ONE_FILE_POSTS', True)) - parser.add_option('-f', '--format', dest='post_format', default='rest', - help='Format for post (one of %s)' % - ','.join(compiler_names)) - (options, args) = parser.parse_args(list(args)) - - is_post = options.is_post - title = options.title - tags = options.tags - onefile = options.onefile - post_format = options.post_format + if len(args) > 1: + print(self.help()) + return False + elif args: + path = args[0] + else: + path = None + + is_page = options.get('is_page', False) + is_post = not is_page + title = options['title'] or None + tags = options['tags'] + onefile = options['onefile'] + twofile = options['twofile'] + + if twofile: + onefile = False + if not onefile and not twofile: + onefile = self.site.config.get('ONE_FILE_POSTS', True) + + post_format = options['post_format'] + if post_format not in compiler_names: - print("ERROR: Unknown post format %s" % post_format) + print("ERROR: Unknown post format " + post_format) return compiler_plugin = self.site.plugin_manager.getPluginByName( post_format, "PageCompiler").plugin_object @@ -118,19 +163,29 @@ class CommandNewPost(Command): if isinstance(title, bytes): title = title.decode(sys.stdin.encoding) title = title.strip() - slug = utils.slugify(title) + if not path: + slug = utils.slugify(title) + else: + 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] output_path = os.path.dirname(entry[0]) meta_path = os.path.join(output_path, slug + ".meta") pattern = os.path.basename(entry[0]) suffix = pattern[1:] - txt_path = os.path.join(output_path, slug + suffix) + if not path: + txt_path = os.path.join(output_path, slug + suffix) + else: + txt_path = path if (not onefile and os.path.isfile(meta_path)) or \ os.path.isfile(txt_path): print("The title already exists!") exit() + + 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) if not onefile: # write metadata file diff --git a/nikola/plugins/command_serve.py b/nikola/plugins/command_serve.py index 75e07a9..64efe7d 100644 --- a/nikola/plugins/command_serve.py +++ b/nikola/plugins/command_serve.py @@ -23,7 +23,6 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function -from optparse import OptionParser import os try: from BaseHTTPServer import HTTPServer @@ -39,26 +38,39 @@ class CommandBuild(Command): """Start test server.""" name = "serve" + doc_usage = "[options]" + doc_purpose = "Start the test webserver." - def run(self, *args): - """Start test server.""" - - parser = OptionParser(usage="nikola %s [options]" % self.name) - parser.add_option("-p", "--port", dest="port", help="Port numer " - "(default: 8000)", default=8000, type="int") - parser.add_option("-a", "--address", dest="address", help="Address to " - "bind (default: 127.0.0.1)", default='127.0.0.1') - (options, args) = parser.parse_args(list(args)) + cmd_options = ( + { + 'name': 'port', + 'short': 'p', + 'long': 'port', + 'default': 8000, + 'type': int, + 'help': 'Port nummber (default: 8000)', + }, + { + 'name': 'address', + 'short': 'a', + 'long': '--address', + 'type': str, + 'default': '127.0.0.1', + 'help': 'Address to bind (default: 127.0.0.1)', + }, + ) + def _execute(self, options, args): + """Start test server.""" out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): - print("Error: Missing '%s' folder?" % out_dir) + print("Error: Missing '{0}' folder?".format(out_dir)) else: os.chdir(out_dir) - httpd = HTTPServer((options.address, options.port), + httpd = HTTPServer((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() - print("Serving HTTP on {0[0]} port {0[1]}...".format(sa)) + print("Serving HTTP on", sa[0], "port", sa[1], "...") httpd.serve_forever() diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile_bbcode.py index fd7fe1a..26de727 100644 --- a/nikola/plugins/compile_bbcode.py +++ b/nikola/plugins/compile_bbcode.py @@ -68,10 +68,10 @@ class CompileTextile(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('[note]<!--\n') - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write('-->[/note]\n\n') diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile_html.py index 850a3e5..6c1c381 100644 --- a/nikola/plugins/compile_html.py +++ b/nikola/plugins/compile_html.py @@ -51,10 +51,10 @@ class CompileHtml(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<!-- \n') - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write('-->\n\n') diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile_markdown/__init__.py index 5eb25c8..7aa03a9 100644 --- a/nikola/plugins/compile_markdown/__init__.py +++ b/nikola/plugins/compile_markdown/__init__.py @@ -55,8 +55,10 @@ class CompileMarkdown(PageCompiler): 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%i>' % n, '<h%i>' % (n + 1), output) - output = re.sub('</h%i>' % n, '</h%i>' % (n + 1), output) + 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' @@ -69,10 +71,10 @@ class CompileMarkdown(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<!-- \n') - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write('-->\n\n') diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py index 4191add..b0a0c00 100644 --- a/nikola/plugins/compile_rest/__init__.py +++ b/nikola/plugins/compile_rest/__init__.py @@ -44,6 +44,8 @@ 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) from nikola.plugin_categories import PageCompiler @@ -75,10 +77,10 @@ class CompileRest(PageCompiler): tags=""): with codecs.open(path, "wb+", "utf8") as fd: if onefile: - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write("\nWrite your post here.") diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile_rest/gist_directive.py index 3bfe818..0ea8f23 100644 --- a/nikola/plugins/compile_rest/gist_directive.py +++ b/nikola/plugins/compile_rest/gist_directive.py @@ -24,11 +24,11 @@ class GitHubGist(Directive): has_content = False def get_raw_gist_with_filename(self, gistID, filename): - url = "https://raw.github.com/gist/%s/%s" % (gistID, filename) + url = '/'.join(("https://raw.github.com/gist", gistID, filename)) return requests.get(url).text def get_raw_gist(self, gistID): - url = "https://raw.github.com/gist/%s/" % (gistID) + url = "https://raw.github.com/gist/{0}/".format(gistID) return requests.get(url).text def run(self): @@ -43,12 +43,12 @@ class GitHubGist(Directive): if 'file' in self.options: filename = self.options['file'] rawGist = (self.get_raw_gist_with_filename(gistID, filename)) - embedHTML = ('<script src="https://gist.github.com/%s.js?file=%s">' - '</script>') % (gistID, filename) + embedHTML = ('<script src="https://gist.github.com/{0}.js' + '?file={1}"></script>').format(gistID, filename) else: rawGist = (self.get_raw_gist(gistID)) - embedHTML = ('<script src="https://gist.github.com/%s.js">' - '</script>') % gistID + embedHTML = ('<script src="https://gist.github.com/{0}.js">' + '</script>').format(gistID) return [nodes.raw('', embedHTML, format='html'), nodes.raw('', '<noscript>', format='html'), diff --git a/nikola/plugins/compile_rest/pygments_code_block_directive.py b/nikola/plugins/compile_rest/pygments_code_block_directive.py index f858427..79bada2 100644 --- a/nikola/plugins/compile_rest/pygments_code_block_directive.py +++ b/nikola/plugins/compile_rest/pygments_code_block_directive.py @@ -165,9 +165,9 @@ def code_block_directive(name, arguments, options, content, lineno, after_index = content.find(after_text)
if after_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "start-at" option of "%s" '
- 'code-block directive:\nText not found.'
- % options['start-at'])
+ '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.
@@ -192,9 +192,9 @@ def code_block_directive(name, arguments, options, content, lineno, after_index = content.find(after_text)
if after_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "start-after" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['start-after'])
+ '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):]
@@ -207,9 +207,9 @@ def code_block_directive(name, arguments, options, content, lineno, before_index = content.find(before_text)
if before_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "end-at" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['end-at'])
+ '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)
@@ -219,9 +219,9 @@ def code_block_directive(name, arguments, options, content, lineno, before_index = content.find(before_text)
if before_index < 0:
raise state_machine.reporter.severe(
- 'Problem with "end-before" option of "%s" '
- 'code-block directive:\nText not found.' %
- options['end-before'])
+ 'Problem with "end-before" option of "{0}" '
+ 'code-block directive:\nText not found.'.format(
+ options['end-before']))
content = content[:before_index]
else:
@@ -246,8 +246,9 @@ def code_block_directive(name, arguments, options, content, lineno, lineno = 1 + line_offset
total_lines = content.count('\n') + 1 + line_offset
lnwidth = len(str(total_lines))
- fstr = "\n%%%dd " % lnwidth
- code_block += nodes.inline(fstr[1:] % lineno, fstr[1:] % lineno,
+ 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
@@ -272,7 +273,8 @@ def code_block_directive(name, arguments, options, content, lineno, linenos = list(range(lineno, lineno + len(values)))
for chunk, ln in zip(values, linenos)[1:]:
if ln <= total_lines:
- code_block += nodes.inline(fstr % ln, fstr % ln,
+ code_block += nodes.inline(fstr.format(ln),
+ fstr.format(ln),
classes=['linenumber'])
code_block += nodes.Text(chunk, chunk)
lineno += len(values) - 1
@@ -319,8 +321,8 @@ def string_bool(argument): elif argument.lower() == 'false':
return False
else:
- raise ValueError('"%s" unknown; choose from "True" or "False"' %
- argument)
+ raise ValueError('"{0}" unknown; choose from "True" or "False"'.format(
+ argument))
def csharp_unicodelevel(argument):
@@ -340,7 +342,8 @@ def listings_directive(name, arguments, options, content, lineno, fname = arguments[0]
options['include'] = os.path.join('listings', fname)
target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = [core.publish_doctree('`%s <%s>`_' % (fname, target))[0]]
+ 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)
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile_rest/slides.py index c9d55f3..f9901f5 100644 --- a/nikola/plugins/compile_rest/slides.py +++ b/nikola/plugins/compile_rest/slides.py @@ -77,12 +77,13 @@ class slides(Directive): options.update(self.options) options = json.dumps(options) output = [] - output.append('<script> $(function(){ $("#slides").slides(%s); });' - '</script>' % options) + 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="%s"></div>""" % image) + output.append("""<div><img src="{0}"></div>""".format(image)) output.append("""</div></div>""") return [nodes.raw('', '\n'.join(output), format='html')] diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile_rest/soundcloud.py new file mode 100644 index 0000000..d47bebf --- /dev/null +++ b/nikola/plugins/compile_rest/soundcloud.py @@ -0,0 +1,32 @@ +from docutils import nodes +from docutils.parsers.rst import directives + +CODE = ("""<iframe width="{width}" height="{height}" +scrolling="no" frameborder="no" +src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/""" + """{sid}"> +</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': '' + } + 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) diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile_rest/vimeo.py index 3eefcc4..34f2a50 100644 --- a/nikola/plugins/compile_rest/vimeo.py +++ b/nikola/plugins/compile_rest/vimeo.py @@ -37,8 +37,8 @@ except ImportError: except ImportError: json = None -CODE = """<iframe src="http://player.vimeo.com/video/%(vimeo_id)s" -width="%(width)s" height="%(height)s" +CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}" +width="{width}" height="{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen> </iframe> """ @@ -76,8 +76,8 @@ def vimeo(name, args, options, content, lineno, contentOffset, blockText, if json: # we can attempt to retrieve video attributes from vimeo try: - url = ('http://vimeo.com/api/v2/video/%(vimeo_id)s.json' % - string_vars) + 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'] @@ -86,7 +86,7 @@ def vimeo(name, args, options, content, lineno, contentOffset, blockText, # fall back to the defaults pass - return [nodes.raw('', CODE % string_vars, format='html')] + return [nodes.raw('', CODE.format(**string_vars), format='html')] vimeo.content = True directives.register_directive('vimeo', vimeo) diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile_rest/youtube.py index fe3b28b..30ac000 100644 --- a/nikola/plugins/compile_rest/youtube.py +++ b/nikola/plugins/compile_rest/youtube.py @@ -26,9 +26,9 @@ from docutils import nodes from docutils.parsers.rst import directives CODE = """\ -<iframe width="%(width)s" -height="%(height)s" -src="http://www.youtube.com/embed/%(yid)s?rel=0&hd=1&wmode=transparent" +<iframe width="{width}" +height="{height}" +src="http://www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" ></iframe>""" @@ -51,6 +51,6 @@ def youtube(name, args, options, content, lineno, string_vars['width'] = extra_args.pop('width') if 'height' in extra_args: string_vars['height'] = extra_args.pop('height') - return [nodes.raw('', CODE % (string_vars), format='html')] + return [nodes.raw('', CODE.format(**string_vars), format='html')] youtube.content = True directives.register_directive('youtube', youtube) diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile_textile.py index 7fa4e3f..3ca370d 100644 --- a/nikola/plugins/compile_textile.py +++ b/nikola/plugins/compile_textile.py @@ -62,10 +62,10 @@ class CompileTextile(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write('<notextile> <!--\n') - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write('--></notextile>\n\n') diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile_txt2tags.py index 2446dfd..90372bd 100644 --- a/nikola/plugins/compile_txt2tags.py +++ b/nikola/plugins/compile_txt2tags.py @@ -65,10 +65,10 @@ class CompileTextile(PageCompiler): with codecs.open(path, "wb+", "utf8") as fd: if onefile: fd.write("\n'''\n<!--\n") - fd.write('.. title: %s\n' % title) - fd.write('.. slug: %s\n' % slug) - fd.write('.. date: %s\n' % date) - fd.write('.. tags: %s\n' % tags) + 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') fd.write("-->\n'''\n") diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task_create_bundles.py index 95f10c2..ad670e1 100644 --- a/nikola/plugins/task_create_bundles.py +++ b/nikola/plugins/task_create_bundles.py @@ -77,7 +77,8 @@ class BuildBundles(LateTask): output_path = os.path.join(kw['output_folder'], name) dname = os.path.dirname(name) file_dep = [get_asset_path( - os.path.join(dname, fname), kw['themes'], kw['files_folders']) + os.path.join(dname, fname), kw['themes'], + kw['files_folders']) for fname in files ] file_dep = filter(None, file_dep) # removes missing files @@ -108,16 +109,17 @@ def get_asset_path(path, themes, files_folders={'files': ''}): 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']) + >>> 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']) + >>> 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':''}) + >>> get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''}) 'nikola/nikola.py' - >>> get_asset_path('nikola/nikola.py',['site','default'],{'nikola':'nikola'}) + >>> get_asset_path('nikola/nikola.py', ['site', 'default'], + ... {'nikola':'nikola'}) 'nikola/nikola.py' """ diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task_indexes.py index 757998e..7baf660 100644 --- a/nikola/plugins/task_indexes.py +++ b/nikola/plugins/task_indexes.py @@ -79,11 +79,11 @@ class Indexes(Task): context["nextlink"] = None context['index_teasers'] = kw['index_teasers'] if i > 1: - context["prevlink"] = "index-%s.html" % (i - 1) + context["prevlink"] = "index-{0}.html".format(i - 1) if i == 1: context["prevlink"] = "index.html" if i < num_pages - 1: - context["nextlink"] = "index-%s.html" % (i + 1) + context["nextlink"] = "index-{0}.html".format(i + 1) context["permalink"] = self.site.link("index", i, lang) output_name = os.path.join( kw['output_folder'], self.site.path("index", i, diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task_redirect.py index b133948..e440c30 100644 --- a/nikola/plugins/task_redirect.py +++ b/nikola/plugins/task_redirect.py @@ -71,5 +71,6 @@ def create_redirect(src, dst): except: pass with codecs.open(src, "wb+", "utf8") as fd: - fd.write('<head><meta http-equiv="refresh" content="0; ' - 'url=%s"></head>' % dst) + fd.write('<!DOCTYPE html><head><title>Redirecting...</title>' + '<meta http-equiv="refresh" content="0; ' + 'url={0}"></head>'.format(dst)) diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py index e69a457..0880e3e 100644 --- a/nikola/plugins/task_render_galleries.py +++ b/nikola/plugins/task_render_galleries.py @@ -136,7 +136,6 @@ class Galleries(Task): # Sort by date image_list.sort(key=lambda a: self.image_date(a)) image_name_list = [os.path.basename(x) for x in image_list] - thumbs = [] # Do thumbnails and copy originals for img, img_name in list(zip(image_list, image_name_list)): @@ -187,7 +186,7 @@ class Galleries(Task): output_gallery, ".thumbnail".join([fname, ext])) excluded_dest_path = os.path.join(output_gallery, img_name) yield { - 'basename': str('render_galleries'), + 'basename': str('render_galleries_clean'), 'name': excluded_thumb_dest_path.encode('utf8'), 'file_dep': [exclude_path], #'targets': [excluded_thumb_dest_path], @@ -198,7 +197,7 @@ class Galleries(Task): 'uptodate': [utils.config_changed(kw)], } yield { - 'basename': str('render_galleries'), + 'basename': str('render_galleries_clean'), 'name': excluded_dest_path.encode('utf8'), 'file_dep': [exclude_path], #'targets': [excluded_dest_path], @@ -214,9 +213,9 @@ class Galleries(Task): context["title"] = os.path.basename(gallery_path) context["description"] = kw["blog_description"] if kw['use_filename_as_title']: - img_titles = ['id="%s" alt="%s" title="%s"' % - (fn[:-4], fn[:-4], utils.unslugify(fn[:-4])) - for fn in image_name_list] + img_titles = ['id="{0}" alt="{1}" title="{2}"'.format( + fn[:-4], fn[:-4], utils.unslugify(fn[:-4])) for fn + in image_name_list] else: img_titles = [''] * len(image_name_list) context["images"] = list(zip(image_name_list, thumbs, img_titles)) diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task_render_listings.py index a899f10..b115a2f 100644 --- a/nikola/plugins/task_render_listings.py +++ b/nikola/plugins/task_render_listings.py @@ -50,29 +50,32 @@ class Listings(Task): # Things to ignore in listings ignored_extensions = (".pyc",) - def render_listing(in_name, out_name): - with open(in_name, 'r') as fd: - try: - lexer = get_lexer_for_filename(in_name) - except: - lexer = TextLexer() - code = highlight(fd.read(), lexer, - HtmlFormatter(cssclass='code', - linenos="table", nowrap=False, - lineanchors=utils.slugify(f), - anchorlinenos=True)) - title = os.path.basename(in_name) - print("CRUMBSINOUT", in_name, out_name) - #crumbs = out_name.split(os.sep)[1:-1] + [title] - # TODO: write this in human - #paths = ['/'.join(['..'] * (len(crumbs) - 2 - i)) for i in - #range(len(crumbs[:-2]))] + ['.', '#'] - crumbs = utils.get_crumbs(os.path.relpath(out_name, kw['output_folder']), is_file=True) + def render_listing(in_name, out_name, folders=[], files=[]): + if in_name: + with open(in_name, 'r') as fd: + try: + lexer = get_lexer_for_filename(in_name) + except: + lexer = TextLexer() + code = highlight(fd.read(), lexer, + HtmlFormatter(cssclass='code', + linenos="table", nowrap=False, + lineanchors=utils.slugify(f), + anchorlinenos=True)) + title = os.path.basename(in_name) + else: + code = '' + title = '' + crumbs = utils.get_crumbs(os.path.relpath(out_name, + kw['output_folder']), + is_file=True) context = { 'code': code, 'title': title, 'crumbs': crumbs, 'lang': kw['default_lang'], + 'folders': folders, + 'files': files, 'description': title, } self.site.render_template('listing.tmpl', out_name.encode('utf8'), @@ -80,12 +83,27 @@ class Listings(Task): flag = True template_deps = self.site.template_system.template_deps('listing.tmpl') for root, dirs, files in os.walk(kw['listings_folder']): + flag = False # Render all files + out_name = os.path.join( + kw['output_folder'], + root, 'index.html' + ) + yield { + 'basename': self.name, + 'name': out_name.encode('utf8'), + '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'])] + } for f in files: ext = os.path.splitext(f)[-1] if ext in ignored_extensions: continue - flag = False in_name = os.path.join(root, f) out_name = os.path.join( kw['output_folder'], diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task_render_rss.py index fb35843..9ce1d63 100644 --- a/nikola/plugins/task_render_rss.py +++ b/nikola/plugins/task_render_rss.py @@ -39,7 +39,7 @@ class RenderRSS(Task): "translations": self.site.config["TRANSLATIONS"], "filters": self.site.config["FILTERS"], "blog_title": self.site.config["BLOG_TITLE"], - "blog_url": self.site.config["BLOG_URL"], + "site_url": self.site.config["SITE_URL"], "blog_description": self.site.config["BLOG_DESCRIPTION"], "output_folder": self.site.config["OUTPUT_FOLDER"], "rss_teasers": self.site.config["RSS_TEASERS"], @@ -59,7 +59,7 @@ class RenderRSS(Task): 'file_dep': deps, 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"], kw["blog_url"], + (lang, kw["blog_title"], kw["site_url"], kw["blog_description"], posts, output_name, kw["rss_teasers"]))], 'clean': True, diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task_render_tags.py index a561a81..744f0cb 100644 --- a/nikola/plugins/task_render_tags.py +++ b/nikola/plugins/task_render_tags.py @@ -22,7 +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 __future__ import unicode_literals import codecs import json import os @@ -42,7 +42,7 @@ class RenderTags(Task): kw = { "translations": self.site.config["TRANSLATIONS"], "blog_title": self.site.config["BLOG_TITLE"], - "blog_url": self.site.config["BLOG_URL"], + "site_url": self.site.config["SITE_URL"], "blog_description": self.site.config["BLOG_DESCRIPTION"], "messages": self.site.MESSAGES, "output_folder": self.site.config['OUTPUT_FOLDER'], @@ -56,6 +56,8 @@ class RenderTags(Task): self.site.scan_posts() + yield self.list_tags_page(kw) + if not self.site.posts_per_tag: yield {'basename': str(self.name), 'actions': []} return @@ -73,8 +75,6 @@ class RenderTags(Task): else: yield self.tag_page_as_list(tag, lang, post_list, kw) - yield self.list_tags_page(kw) - # Tag cloud json file tag_cloud_data = {} for tag, posts in self.site.posts_per_tag.items(): @@ -136,7 +136,7 @@ class RenderTags(Task): """Given tag, n, returns a page name.""" name = self.site.path("tag", tag, lang) if i: - name = name.replace('.html', '-%s.html' % i) + name = name.replace('.html', '-{0}.html'.format(i)) return name # FIXME: deduplicate this with render_indexes @@ -152,11 +152,11 @@ class RenderTags(Task): # On a tag page, the feeds include the tag's feeds rss_link = ("""<link rel="alternate" type="application/rss+xml" """ """type="application/rss+xml" title="RSS for tag """ - """%s (%s)" href="%s">""" % - (tag, lang, self.site.link("tag_rss", tag, lang))) + """{0} ({1})" href="{2}">""".format( + tag, lang, self.site.link("tag_rss", tag, lang))) context['rss_link'] = rss_link - output_name = os.path.join(kw['output_folder'], page_name(tag, i, - lang)) + 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 @@ -231,8 +231,8 @@ class RenderTags(Task): 'file_dep': deps, 'targets': [output_name], 'actions': [(utils.generic_rss_renderer, - (lang, "%s (%s)" % (kw["blog_title"], tag), - kw["blog_url"], kw["blog_description"], post_list, + (lang, "{0} ({1})".format(kw["blog_title"], tag), + kw["site_url"], kw["blog_description"], post_list, output_name, kw["rss_teasers"]))], 'clean': True, 'uptodate': [utils.config_changed(kw)], diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py index 96b9dbd..9d89070 100644 --- a/nikola/plugins/task_sitemap/__init__.py +++ b/nikola/plugins/task_sitemap/__init__.py @@ -49,7 +49,8 @@ class Sitemap(LateTask): return """Generate Google sitemap.""" kw = { - "blog_url": self.site.config["BLOG_URL"], + "base_url": self.site.config["BASE_URL"], + "site_url": self.site.config["SITE_URL"], "output_folder": self.site.config["OUTPUT_FOLDER"], } output_path = os.path.abspath(kw['output_folder']) @@ -59,18 +60,14 @@ class Sitemap(LateTask): # Generate config config_data = """<?xml version="1.0" encoding="UTF-8"?> <site - base_url="%s" - store_into="%s" + base_url="{0}" + store_into="{1}" verbose="1" > - <directory path="%s" url="%s" /> + <directory path="{2}" url="{3}" /> <filter action="drop" type="wildcard" pattern="*~" /> <filter action="drop" type="regexp" pattern="/\.[^/]*" /> - </site>""" % ( - kw["blog_url"], - sitemap_path, - output_path, - kw["blog_url"], - ) + </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() @@ -82,14 +79,15 @@ class Sitemap(LateTask): 0) else: sitemap.Generate() - sitemap_gen.output.Log('Number of errors: %d' % - sitemap_gen.output.num_errors, 1) - sitemap_gen.output.Log('Number of warnings: %d' % - sitemap_gen.output.num_warns, 1) + 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) yield { "basename": "sitemap", + "name": os.path.join(kw['output_folder'], "sitemap.xml.gz"), "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 index a877c24..898325a 100644 --- a/nikola/plugins/task_sitemap/sitemap_gen.py +++ b/nikola/plugins/task_sitemap/sitemap_gen.py @@ -90,7 +90,7 @@ if sys.version_info[0] == 3: unichr = chr else: bytes_str = str - unicode_str = unicode + unicode_str = unicode # NOQA # Text encodings ENC_ASCII = 'ASCII' diff --git a/nikola/post.py b/nikola/post.py index 809e5b7..5060583 100644 --- a/nikola/post.py +++ b/nikola/post.py @@ -27,32 +27,43 @@ from __future__ import unicode_literals, print_function import codecs import os +import re +import sys +import string +import unidecode import lxml.html -from . import utils +from .utils import to_datetime, slugify __all__ = ['Post'] +TEASER_REGEXP = re.compile('<!--\s*TEASER_END(:(.+))?\s*-->', re.IGNORECASE) + class Post(object): """Represents a blog post or web page.""" - def __init__(self, source_path, cache_folder, destination, use_in_feeds, - translations, default_lang, blog_url, messages, template_name, - file_metadata_regexp=None): + 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 + ): """Initialize post. - The base path is the .txt post file. From it we calculate + The source path is the .txt post file. From it we calculate 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.blog_url = blog_url + self.base_url = base_url self.is_draft = False + self.is_mathjax = False self.source_path = source_path # posts/blah.txt self.post_name = os.path.splitext(source_path)[0] # posts/blah # cache/posts/blah.html @@ -63,24 +74,27 @@ class Post(object): self.default_lang = default_lang self.messages = messages self.template_name = template_name - if os.path.isfile(self.metadata_path): - with codecs.open(self.metadata_path, "r", "utf8") as meta_file: - meta_data = meta_file.readlines() - while len(meta_data) < 6: - meta_data.append("") - (default_title, default_pagename, self.date, self.tags, - self.link, default_description) = [x.strip() for x in - meta_data][:6] - else: - (default_title, default_pagename, self.date, self.tags, - self.link, default_description) = utils.get_meta( - self.source_path, file_metadata_regexp) + self.meta = get_meta(self, file_metadata_regexp) + + default_title = self.meta.get('title', '') + default_pagename = self.meta.get('slug', '') + default_description = self.meta.get('description', '') + + 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) if not default_title or not default_pagename or not self.date: - raise OSError("You must set a title and slug and date! [%s]" % - source_path) + 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)) - self.date = utils.to_datetime(self.date) + # 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] @@ -89,45 +103,30 @@ class Post(object): self.is_draft = 'draft' in self.tags self.tags = [t for t in self.tags if t != 'draft'] + # If mathjax is a tag, then enable mathjax rendering support + self.is_mathjax = 'mathjax' in self.tags + self.pagenames = {} self.titles = {} self.descriptions = {} - # Load internationalized titles - # TODO: this has gotten much too complicated. Rethink. + + # 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: - metadata_path = self.metadata_path + "." + lang - source_path = self.source_path + "." + lang - if os.path.isfile(source_path): + if os.path.isfile(self.source_path + "." + lang): self.translated_to.add(lang) - try: - if os.path.isfile(metadata_path): - with codecs.open( - metadata_path, "r", "utf8") as meta_file: - meta_data = [x.strip() for x in - meta_file.readlines()] - while len(meta_data) < 6: - meta_data.append("") - self.titles[lang] = meta_data[0] or default_title - self.pagenames[lang] = meta_data[1] or\ - default_pagename - self.descriptions[lang] = meta_data[5] or\ - default_description - else: - ttitle, ppagename, tmp1, tmp2, tmp3, ddescription = \ - utils.get_meta(source_path, file_metadata_regexp) - self.titles[lang] = ttitle or default_title - self.pagenames[lang] = ppagename or default_pagename - self.descriptions[lang] = ddescription or\ - default_description - except: - self.titles[lang] = default_title - self.pagenames[lang] = default_pagename - self.descriptions[lang] = default_description + + 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.""" @@ -164,12 +163,12 @@ class Post(object): """Return path to the translation's file, or to the original.""" file_name = self.base_path if lang != self.default_lang: - file_name_lang = file_name + ".%s" % lang + file_name_lang = '.'.join((file_name, lang)) if os.path.exists(file_name_lang): file_name = file_name_lang return file_name - def text(self, lang, teaser_only=False): + 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) @@ -177,22 +176,30 @@ class Post(object): data = post_file.read() if data: - data = lxml.html.make_links_absolute(data, self.permalink()) + 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') - if '<!-- TEASER_END -->' in elem_string.upper(): + 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="%s">%s...</a></p>' % - (self.permalink(lang), - self.messages[lang]["Read more"])) + teaser.append('<p><a href="{0}">{1}</a></p>'.format( + self.permalink(lang), teaser_str)) data = ''.join(teaser) + + 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'): @@ -206,9 +213,9 @@ class Post(object): pieces = list(os.path.split(self.translations[lang])) pieces += list(os.path.split(self.folder)) pieces += [self.pagenames[lang] + extension] - pieces = [_f for _f in pieces if _f] + pieces = [_f for _f in pieces if _f and _f != '.'] if absolute: - pieces = [self.blog_url] + pieces + pieces = [self.base_url] + pieces else: pieces = [""] + pieces link = "/".join(pieces) @@ -216,3 +223,166 @@ class Post(object): def source_ext(self): return os.path.splitext(self.source_path)[1] + +# Code that fetches metadata from different places + + +def re_meta(line, match=None): + """re.compile for meta""" + if match: + reStr = re.compile('^\.\. {0}: (.*)'.format(re.escape(match))) + else: + reStr = re.compile('^\.\. (.*?): (.*)') + result = reStr.findall(line.strip()) + if match and result: + return (match, result[0]) + elif not match and result: + return (result[0][0], result[0][1].strip()) + else: + return (None,) + + +def _get_metadata_from_filename_by_regex(filename, metadata_regexp): + """ + Tries to ried the metadata from the filename based on the given re. + This requires to use symbolic group names in the pattern. + + The part to read the metadata from the filename based on a regular + expression is taken from Pelican - pelican/readers.py + """ + match = re.match(metadata_regexp, filename) + meta = {} + + if match: + # .items() for py3k compat. + for key, value in match.groupdict().items(): + meta[key.lower()] = value # metadata must be lowercase + + return meta + + +def get_metadata_from_file(source_path, lang=None): + """Extracts metadata from the file itself, by parsing contents.""" + try: + if lang: + source_path = "{0}.{1}".format(source_path, lang) + with codecs.open(source_path, "r", "utf8") as meta_file: + meta_data = [x.strip() for x in meta_file.readlines()] + return _get_metadata_from_file(meta_data) + except Exception: # The file may not exist, for multilingual sites + return {} + + +def _get_metadata_from_file(meta_data): + """Parse file contents and obtain metadata. + + >>> g = _get_metadata_from_file + >>> list(g([]).values()) + [] + >>> str(g(["FooBar","======"])["title"]) + 'FooBar' + >>> str(g(["#FooBar"])["title"]) + 'FooBar' + >>> str(g([".. title: FooBar"])["title"]) + 'FooBar' + >>> 'title' in g(["",".. title: FooBar"]) + False + + """ + meta = {} + + re_md_title = re.compile(r'^{0}([^{0}].*)'.format(re.escape('#'))) + # Assuming rst titles are going to be at least 4 chars long + # otherwise this detects things like ''' wich breaks other markups. + re_rst_title = re.compile(r'^([{0}]{{4,}})'.format(re.escape( + string.punctuation))) + + for i, line in enumerate(meta_data): + if not line: + break + if 'title' not in meta: + match = re_meta(line, 'title') + if match[0]: + meta['title'] = match[1] + if 'title' not in meta: + if re_rst_title.findall(line) and i > 0: + meta['title'] = meta_data[i - 1].strip() + if 'title' not in meta: + if re_md_title.findall(line): + meta['title'] = re_md_title.findall(line)[0] + + match = re_meta(line) + if match[0]: + meta[match[0]] = match[1] + + return meta + + +def get_metadata_from_meta_file(path, lang=None): + """Takes a post path, and gets data from a matching .meta file.""" + meta_path = os.path.splitext(path)[0] + '.meta' + if lang: + meta_path += '.' + lang + if os.path.isfile(meta_path): + with codecs.open(meta_path, "r", "utf8") as meta_file: + meta_data = meta_file.readlines() + while len(meta_data) < 6: + meta_data.append("") + (title, slug, date, tags, link, description) = [ + x.strip() for x in meta_data][:6] + + meta = {} + + if title: + meta['title'] = title + if slug: + meta['slug'] = slug + if date: + meta['date'] = date + if tags: + meta['tags'] = tags + if link: + meta['link'] = link + if description: + meta['description'] = description + + return meta + else: + return {} + + +def get_meta(post, file_metadata_regexp=None, lang=None): + """Get post's meta from source. + + If ``file_metadata_regexp`` is given it will be tried to read + metadata from the filename. + If any metadata is then found inside the file the metadata from the + file will override previous findings. + """ + meta = {} + + meta.update(get_metadata_from_meta_file(post.metadata_path, lang)) + + if meta: + return meta + + if file_metadata_regexp is not None: + meta.update(_get_metadata_from_filename_by_regex(post.source_path, + file_metadata_regexp)) + + meta.update(get_metadata_from_file(post.source_path, lang)) + + if lang is None: + # Only perform these checks for the default language + + if 'slug' not in meta: + # If no slug is found in the metadata use the filename + meta['slug'] = slugify(os.path.splitext( + os.path.basename(post.source_path))[0]) + + if 'title' not in meta: + # If no title is found, use the filename without extension + meta['title'] = os.path.splitext( + os.path.basename(post.source_path))[0] + + return meta diff --git a/nikola/utils.py b/nikola/utils.py index eeb0c45..5589d68 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2012 Roberto Alsina y otros. # Permission is hereby granted, free of charge, to any @@ -33,7 +34,6 @@ import re import codecs import json import shutil -import string import subprocess import sys from zipfile import ZipFile as zip @@ -42,6 +42,7 @@ try: except ImportError: pass +import pytz if sys.version_info[0] == 3: # Python 3 @@ -50,7 +51,7 @@ if sys.version_info[0] == 3: unichr = chr else: bytes_str = str - unicode_str = unicode + unicode_str = unicode # NOQA from doit import tools from unidecode import unidecode @@ -58,9 +59,8 @@ from unidecode import unidecode import PyRSS2Gen as rss __all__ = ['get_theme_path', 'get_theme_chain', 'load_messages', 'copy_tree', - 'generic_rss_renderer', - 'copy_file', 'slugify', 'unslugify', 'get_meta', 'to_datetime', - 'apply_filters', 'config_changed', 'get_crumbs'] + 'generic_rss_renderer', 'copy_file', 'slugify', 'unslugify', + 'to_datetime', 'apply_filters', 'config_changed', 'get_crumbs'] class CustomEncoder(json.JSONEncoder): @@ -86,13 +86,13 @@ class config_changed(tools.config_changed): byte_data = data return hashlib.md5(byte_data).hexdigest() else: - raise Exception( - ('Invalid type of config_changed parameter got %s' + - ', must be string or dict') % (type(self.config),)) + raise Exception('Invalid type of config_changed parameter -- got ' + '{0}, must be string or dict'.format(type( + self.config))) def __repr__(self): - return "Change with config: %s" % json.dumps( - self.config, cls=CustomEncoder) + return "Change with config: {0}".format(json.dumps(self.config, + cls=CustomEncoder)) def get_theme_path(theme): @@ -107,111 +107,7 @@ def get_theme_path(theme): 'data', 'themes', theme) if os.path.isdir(dir_name): return dir_name - raise Exception("Can't find theme '%s'" % theme) - - -def re_meta(line, match): - """re.compile for meta""" - reStr = re.compile('^%s(.*)' % re.escape(match)) - result = reStr.findall(line) - if result: - return result[0].strip() - else: - return '' - - -def _get_metadata_from_filename_by_regex(filename, metadata_regexp): - """ - Tries to ried the metadata from the filename based on the given re. - This requires to use symbolic group names in the pattern. - - The part to read the metadata from the filename based on a regular - expression is taken from Pelican - pelican/readers.py - """ - title = slug = date = tags = link = description = '' - match = re.match(metadata_regexp, filename) - if match: - # .items() for py3k compat. - for key, value in match.groupdict().items(): - key = key.lower() # metadata must be lowercase - - if key == 'title': - title = value - if key == 'slug': - slug = value - if key == 'date': - date = value - if key == 'tags': - tags = value - if key == 'link': - link = value - if key == 'description': - description = value - - return (title, slug, date, tags, link, description) - - -def _get_metadata_from_file(source_path, title='', slug='', date='', tags='', - link='', description=''): - re_md_title = re.compile(r'^%s([^%s].*)' % - (re.escape('#'), re.escape('#'))) - # Assuming rst titles are going to be at least 4 chars long - # otherwise this detects things like ''' wich breaks other markups. - re_rst_title = re.compile(r'^([%s]{4,})' % re.escape(string.punctuation)) - - with codecs.open(source_path, "r", "utf8") as meta_file: - meta_data = meta_file.readlines(15) - - for i, meta in enumerate(meta_data): - if not title: - title = re_meta(meta, '.. title:') - if not title: - if re_rst_title.findall(meta) and i > 0: - title = meta_data[i - 1].strip() - if not title: - if re_md_title.findall(meta): - title = re_md_title.findall(meta)[0] - if not slug: - slug = re_meta(meta, '.. slug:') - if not date: - date = re_meta(meta, '.. date:') - if not tags: - tags = re_meta(meta, '.. tags:') - if not link: - link = re_meta(meta, '.. link:') - if not description: - description = re_meta(meta, '.. description:') - - return (title, slug, date, tags, link, description) - - -def get_meta(source_path, file_metadata_regexp=None): - """Get post's meta from source. - - If ``file_metadata_regexp`` ist given it will be tried to read - metadata from the filename. - If any metadata is then found inside the file the metadata from the - file will override previous findings. - """ - title = slug = date = tags = link = description = '' - - if not (file_metadata_regexp is None): - (title, slug, date, tags, link, - description) = _get_metadata_from_filename_by_regex( - source_path, file_metadata_regexp) - - (title, slug, date, tags, link, description) = _get_metadata_from_file( - source_path, title, slug, date, tags, link, description) - - if not slug: - # If no slug is found in the metadata use the filename - slug = slugify(os.path.splitext(os.path.basename(source_path))[0]) - - if not title: - # If no title is found, use the filename without extension - title = os.path.splitext(os.path.basename(source_path))[0] - - return (title, slug, date, tags, link, description) + raise Exception("Can't find theme '{0}'".format(theme)) def get_template_engine(themes): @@ -267,8 +163,8 @@ def load_messages(themes, translations): sorted(english.MESSAGES.keys()) and \ lang not in warned: # FIXME: get real logging in place - print("Warning: Incomplete translation for language '%s'." % - lang) + print("Warning: Incomplete translation for language " + "'{0}'.".format(lang)) warned.append(lang) messages[lang].update(english.MESSAGES) messages[lang].update(translation.MESSAGES) @@ -325,7 +221,9 @@ def generic_rss_renderer(lang, title, link, description, timeline, output_path, 'link': post.permalink(lang, absolute=True), 'description': post.text(lang, teaser_only=rss_teasers), 'guid': post.permalink(lang, absolute=True), - 'pubDate': post.date, + # PyRSS2Gen's pubDate is GMT time. + 'pubDate': (post.date if post.date.tzinfo is None else + post.date.astimezone(pytz.timezone('UTC'))), } items.append(rss.RSSItem(**args)) rss_obj = rss.RSS2( @@ -386,6 +284,16 @@ def slugify(value): and converts spaces to hyphens. From Django's "django/template/defaultfilters.py". + + >>> slugify('\xe1\xe9\xed.\xf3\xfa') + 'aeiou' + + >>> slugify('foo/bar') + 'foobar' + + >>> slugify('foo bar') + 'foo-bar' + """ value = unidecode(value) # WARNING: this may not be python2/3 equivalent @@ -418,22 +326,23 @@ def extract_all(zipfile): namelist = z.namelist() for f in namelist: if f.endswith('/') and '..' in f: - raise UnsafeZipException( - 'The zip file contains ".." and is not safe to expand.') + raise UnsafeZipException('The zip file contains ".." and is ' + 'not safe to expand.') for f in namelist: if f.endswith('/'): if not os.path.isdir(f): try: os.makedirs(f) except: - raise OSError("mkdir '%s' error!" % f) + raise OSError("Failed making {0} directory " + "tree!".format(f)) else: z.extract(f) os.chdir(pwd) # From https://github.com/lepture/liquidluck/blob/develop/liquidluck/utils.py -def to_datetime(value): +def to_datetime(value, tzinfo=None): if isinstance(value, datetime.datetime): return value supported_formats = [ @@ -451,10 +360,14 @@ def to_datetime(value): ] for format in supported_formats: try: - return datetime.datetime.strptime(value, format) + dt = datetime.datetime.strptime(value, format) + if tzinfo is None: + return dt + # Build a localized time by using a given timezone. + return tzinfo.localize(dt) except ValueError: pass - raise ValueError('Unrecognized date/time: %r' % value) + raise ValueError('Unrecognized date/time: {0!r}'.format(value)) def apply_filters(task, filters): |
