Commits

Ben Bangert committed 6cd0522 Merge

merge

  • Participants
  • Parent commits ee10bf6, 9b8bf3e

Comments (0)

Files changed (76)

 syntax: glob
 
+*~
 *.pyc
 *.orig
 *.swp
 *.egg-info/*
 blog/blog/*
 build/*
+.tox/*
-Andrea Crotti - Code cleanup
+Andrea Crotti - Code cleanup, enabled nose and tox
 Bernhard Grotz - German translation
 Jordi Bofill - Tinkerer internationalization, Spanish and Catalan translations
 Rod Morehead - Non-XML entity patch for HTML-to-XML conversion
 Yoshihisa Tanaka - Fixed problem caused by non-ascii content
+Ińigo Serna - Sidebar widgets for categories, tags and tag cloud
+Emil Oppeln-Bronikowski - Polish translation
+Christian Jann - Configurable number of posts/page, "Read more" implementation, email obfuscator, other enhancements and numerous bug fixes
+Éric de la Musse - Localization bugfix, French translation
+Antti Kaihola - Bugfix for "more" directive
+Visa Kopu - Fix category links, XML declaration introduced in patch.py
 
 FreeBSD license:
 
-Copyright (c) 2011-2012 by Vlad Riscutia and contributors. 
+Copyright (c) 2011-2012 by Vlad Riscutia and contributors (see CONTRIBUTORS file).
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification, are
 authors and should not be interpreted as representing official policies, either expressed
 or implied, of Vlad Riscutia.
 
+License for email obfuscator code
+=================================
+
+Email obfuscator code under tinkerer/ext/hidemail.py
+
+BSD license:
+
+Copyright (c) 2011-2012, Kevin Teague.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+   must display the following acknowledgement:
+   This product includes software developed by Kevin Teague.
+4. Neither the name of Kevin Teague nor the names of its contributors may 
+   be used to endorse or promote products derived from this software without 
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY KEVIN TEAGUE ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL KEVIN TEAGUE BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
 License for WebSymbolsRegular Font
 ==================================
 
 
 Open Font License:
 
-Copyright (c) 2007-2011, Just Be Nice, www.justbenicestudio.com,
+Copyright (c) 2007-2012, Just Be Nice, www.justbenicestudio.com,
 with Reserved Font Name WebSymbolsRegular.
 
 PREAMBLE
 DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
 OTHER DEALINGS IN THE FONT SOFTWARE.
+
+License for Boilerplate theme
+=============================
+
+Parts of boilerplate theme under tinkerer/themes/boilerplate are derived from 
+the Sphinx basic theme. Boilerplate theme also includes the following 
+JavaScript scripts copied from Sphinx base theme theme: doctools.js, 
+searchtools.js_t. These are licensed under the following license:
+
+BSD license:
+
+Copyright (c) 2007-2012 by the Sphinx team.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+   must display the following acknowledgement:
+   This product includes software developed by the Sphinx team.
+4. Neither the name of the Sphinx team nor the names of its contributors may 
+   be used to endorse or promote products derived from this software without 
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE SPHINX TEAM ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE SPHINX TEAM BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+License for jQuery JavaScript Library
+=====================================
+
+jQuery JavaScript Library is distributed with Tinkerer under 
+tinkerer/themes/boilerplate/static and has the following license:
+
+MIT license:
+
+Copyright (c) 2012 jQuery Foundation and other contributors, 
+http://jquery.com/
+
+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.
+
+License for Modernizr JavaScript Library
+========================================
+
+Modernizr JavaScript Library is distributed with Tinkerer under
+tinkerer/themes/boilerplate/static and has the following license:
+
+MIT license:
+
+Copyright (c) 2009–2011
+
+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.
+

blog/2012/07/05/tinkerer_0_4_beta_released.rst

+Tinkerer 0.4 Beta Released
+==========================
+
+What's New
+----------
+
+New HTML5 Themes
+~~~~~~~~~~~~~~~~
+
+* *boilerplate* is a brand new theme based on 
+  `HTML5 Boilerplate <http://html5boilerplate.com>`_
+* *modern5* is a rewrite of the old *modern* theme based on the new 
+  *boilerplate* and including a lot more detail-work
+
+The old themes (*tinkerbase*, *modern* and *minimal*) are still around for
+backwards compatiblity, though future development will happen around
+HTML5-based themes.
+
+New Built-in Extensions
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* An email obfuscator: :ref:`hide_mail`
+* A new ``Read more`` directive - more info under :ref:`posts`
+
+New Sidebard Widgets
+~~~~~~~~~~~~~~~~~~~~
+
+* A *Categories List* widget
+* A *Tag List* widget
+* A *Tag Cloud* widget
+
+Details here: :ref:`sidebar`.
+
+New Translations
+~~~~~~~~~~~~~~~~
+
+* German translation
+* French translation
+* Polish translation
+
+Just update your ``conf.py`` with the following setting::
+
+    language = "de" # or "fr" or "pl"
+
+Acknowledgements
+----------------
+
+Many thanks to everyone who helped out with suggestions, bug reports, patches
+and pull requests and special thanks to the people on the ever-growing 
+`contributors`_ list for their valuable contributions.
+
+.. _contributors: https://bitbucket.org/vladris/tinkerer/raw/tip/CONTRIBUTORS
+
+.. author:: default
+.. categories:: tinkerer
+.. tags:: tinkerer, release
+.. comments::

blog/_static/fork_me.png

Old
Old image
New
New image

blog/_templates/get_involved.html

 <div class="widget">
-<h3>Get Involved</h3>
+<h1>Get Involved</h1>
 <p>Have a question? Have an answer? Join the <a href="http://groups.google.com/group/tinkerer-dev">google group</a>.</p>
 <p>Found an issue? Report it on the <a href="https://bitbucket.org/vladris/tinkerer/issues/new">issue tracker</a>.</p>
 <p>Also please consider contributing your themes and extensions. Patches are also welcomed :)</p>

blog/_templates/get_tinkerer.html

 <div class="widget">
-    <h3>How to Tinker?</h3>
+    <h1>How to Tinker?</h1>
     <p>Get Tinkerer:</p>
     <div class="highligh-bash">
         <div class="highlight">

blog/_templates/page.html

-{% extends "!page.html" %}
+{%- extends "!page.html" -%}
 
-{% set script_files = script_files + ["_static/google_analytics.js"] %}
+{%- set script_files = script_files + ["_static/google_analytics.js"] -%}
 
-{% block footer %}
+{%- block extrahead -%}
     {{ super() }}
-    <a href="http://bitbucket.org/vladris/tinkerer">
-        <img alt="Fork me" src="{{ pathto('_static/fork_me.png', 1) }}" style="position: fixed; top: 0; right: 0; border 0"></img>
+<style media="screen" type="text/css">
+    #fork_me { display: none; }
+
+    @media only screen and (min-width: 768px) {
+        #fork_me { display: inline; }
+    } 
+</style>
+{%- endblock -%}
+
+{%- block footer -%}
+    {{ super() }}
+    <a id="fork_me" href="http://bitbucket.org/vladris/tinkerer">
+        <img alt="Fork me" src="{{ pathto('_static/fork_me.png', 1) }}" style="position: fixed; top: 0; right: 0; border 0" />
     </a>
-{% endblock %}
+{%- endblock -%}

blog/_templates/sphinx.html

 <div class="widget">
-<h3>Sphinx Documentation</h3>
+<h1>Sphinx Documentation</h1>
 <p>
 Tinkerer is powered by Sphinx so a lot of useful information can be found on
 the <a href="http://sphinx.pocoo.org">Sphinx webiste</a>.
 tagline = 'Blogging for Pythonistas'                  
 author = 'Vlad Riscutia'
 copyright = '2011, ' + author         
-website = 'http://tinkerer.bitbucket.org/'                              
+website = 'http://tinkerer.me/'                              
 
 disqus_shortname = 'tinkerer'                                   
 html_favicon = 'tinkerer.ico'           
-html_theme = "modern"
+html_theme = "modern5"
 rss_service = "http://feeds.feedburner.com/tinkerer"
 
 extensions = ['tinkerer.ext.blog', 'tinkerer.ext.disqus'] 

blog/doc/deploying.rst

     link your posts. Note the trailing ``blog/html`` - the website variable 
     must point to the root of your blog's *build* directory, not root 
     directory.
+
+Copying extra files to the html output directory
+------------------------------------------------
+
+Files placed in an folder named ``_copy`` will be
+automatically copied to the html output directory.
+
+This could be useful for an ``.htaccess`` file,
+an ``robots.txt`` file or an extra ``favicon.ico``
+etc.
+
+Creating custom 404 and 403 error pages
+---------------------------------------
+
+If your webserver supports ``.htaccess`` files you can create these pages by placing
+an ``.htaccess`` file under ``_copy/.htaccess`` with the following content:
+
+.. code-block:: bash
+
+  ErrorDocument 404 http://www.yoursite.com/404.html
+  ErrorDocument 403 http://www.yoursite.com/403.html
+  Options -Indexes
+
+Add an file ``404.rst`` to the document root:
+
+.. code-block:: rst
+
+  The URL you requested was not found.
+  ====================================
+
+  .. comments:: 
+  
+  Your own text.
+
+Add an file ``403.rst`` to the document root:
+
+.. code-block:: rst
+
+  403 Permission Denied
+  =====================
+
+  .. comments:: 
+  
+  Your own text.
+  
+And add these two pages to the ``master.rst`` file:
+
+.. code-block:: rst
+
+  Sitemap
+  =======
+
+  .. toctree::
+    :hidden:
     
+    404.rst
+    403.rst
+    
+  .. toctree::
+    :maxdepth: 1
+
+    2012/04/21/a_blog_post
+    pages/about
+    
+Adding custom analytics code
+----------------------------
+
+If you don't want to use Google Analytics and for example `Piwik <http://piwik.org/>`_
+you can add custom JavaScript code by placing an file named ``page.html`` under
+``_templates/page.html``:
+
+.. code-block:: html
+
+  {% extends "!page.html" %}
+
+  {% block footer %}
+      {{ super() }}
+      {% include "../_static/piwik.js" %}
+  {% endblock %}
+  
+And the analytics code inside ``_static/piwik.js``:
+
+.. code-block:: html
+
+  <!-- Piwik -->
+  <script type="text/javascript">
+  var pkBaseURL = (("https:" == document.location.protocol) ? "https://piwik.yoursite.com/piwik/" : "http://piwik.yoursite.com/piwik/");
+  document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
+  </script><script type="text/javascript">
+  try {
+  var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 1);
+  piwikTracker.trackPageView();
+  piwikTracker.enableLinkTracking();
+  } catch( err ) {}
+  </script><noscript><p><img src="http://piwik.yoursite.com/piwik/piwik.php?idsite=1" style="border:0" alt="" /></p></noscript>
+  <!-- End Piwik Tracking Code -->
+
 Back to :ref:`tinkerer_reference`.

blog/doc/more_tinkering.rst

 To enable `Google Analytics <http://google.com/analytics>`_ for your blog, 
 setup your Google Analytics account. You will be provided some JS  code.
 Add the JS code to a file in your blog's ``_static`` directory as 
-``googl_analytics.js`` and create a new ``page.html`` file under your blog's 
+``google_analytics.js`` and create a new ``page.html`` file under your blog's 
 ``_templates`` directory with the following content::
 
    {% extends "!page.html" %}
 Theming
 -------
 
-Tinkerer comes with three themes: *modern* - the default theme, *minimal* - a
-minimalist black and white theme and a base *tinkerbase* theme from which the
-others inherit. *Tinkerbase* is not styled, rather it implements the basic
-layout. Due to the inherent differences between documentation and blogs, 
-Sphinx themes are not fully compatible with Tinkerer.
+Tinkerer comes with a base *boilerplate* theme. This is an unstyled theme based
+on HTML5 Boilerplate. Custom themes should inherit from it.
+
+The default Tinkerer theme is *modern5*, which is based on the *boilerplate*.
+
+Before version 0.4, Tinkerer came with other three themes: *modern* - the 
+default theme, *minimal* - a minimalist black and white theme and a base 
+*tinkerbase* theme from which the others inherit. These themes are still 
+available for backwards compatibility though future development will be based
+on the *boilerplate* theme and HTML5.
+
+Due to the inherent differences between documentation and blogs, Sphinx themes 
+are not fully compatible with Tinkerer.
 
 To tinker with the look of your blog, you have two options:
 
 Create your own theme
 ~~~~~~~~~~~~~~~~~~~~~
 
-Tinkerer themes should inherit from the *tinkerbase* theme. For more information 
-on creating themes see 
+Tinkerer themes should inherit from the *boilerplate* theme. For more 
+information on creating themes see 
 `Creating themes <http://sphinx.pocoo.org/theming.html#creating-themes>`_.
 
 Extensions
 enable blogging with Sphinx and the ``tinkerer.ext.disqus`` extension is the 
 Disqus comment handler.
 
+.. _sidebar:
+
 Sidebar
 -------
 
 The ``html_sidebars`` list contains the list of templates to be rendered on the 
-sidebar. Tinkerer includes ``recent.html`` and ``searchbox.html`` by default.
+sidebar. Tinkerer includes ``recent.html`` and ``searchbox.html`` by default. A
+list of categories, a list of tags and a tag cloud are also part of the Tinkerer
+distribution and can be easily added by updating the ``html_sidebars`` setting in
+``conf.py`` to include the corresponding files.
 
 **recent.html** 
 
 
 **searchbox.html**
 
-    This is the Sphinx quicksearch box.    
+    This is the equivalent of the Sphinx quicksearch box.    
+
+**categories.html**
+
+    Displays a list of categories under which posts were filed.
+
+**tags.html**
+
+    Displays a list of tags under which posts were filed.
+
+**tags_cloud.html**
+
+    Tag cloud.
 
 `More information on sidebars <http://sphinx.pocoo.org/config.html#confval-html_sidebars>`_.
 
 Back to :ref:`tinkerer_reference`.
 
+.. _hide_mail:
+
+Hide Email Addresses From Spam Bots
+-----------------------------------
+
+Tinkerer has a simple built in mechanism to hide your email address from spambots 
+by generating an obfuscated email address which than gets decrypted in the browser
+with the help of a little bit JavaScript.
+
+To insert an email address just use:
+
+.. code-block:: rst
+
+  :email:`tinkerer-dev <tinkerer-dev@googlegroups.com>`
+  
+:email:`tinkerer-dev <tinkerer-dev@googlegroups.com>`
+
+The encrypted html looks like this:
+
+.. code-block:: html
+
+  <noscript>(Javascript must be enabled to see this e-mail address)</noscript>
+  <script type="text/javascript">document.write(
+  "<n uers=\"znvygb:gvaxrere-qri\100tbbtyrtebhcf\056pbz\">gvaxrere-qri <\057n>".replace(/[a-zA-Z]/g,
+  function(c){
+  return String.fromCharCode(
+  (c<="Z"?90:122)>=(c=c.charCodeAt(0)+13)?c:c-26);}));
+  </script>
+
+If the user has JavaScript disabled he will see this:
+
+.. code-block:: html
+
+  (Javascript must be enabled to see this e-mail address)
+
+

blog/doc/tinkering.rst

     Hello World!
     ============
 
-
-
     .. author:: default
     .. categories:: none
     .. tags:: none
     .. comments::
-
+   
 Add content below the title.
 
 **author**
 
     This tells Tinkerer comments are enabled for this post. Remove the 
     directive to disable posts.
+    
+**more**
+
+    At any point in your text, you can insert the ``more`` directive::
+
+        Hello World!
+        ============
+
+        Some text.
+
+        .. more::
+
+        More text.
+
+    This tells Tinkerer to insert a "Read more..." link into the blog post.
+    A "Read more..." link will appear on the front page and the text after the
+    directive will be hidden. The full text will be displayed only on the page 
+    of the post.
    
 .. _pages:
     
-<html xmlns="http://www.w3.org/1999/xhtml">
+<!doctype html>
+<html>
     <head>
         <meta http-equiv="REFRESH" content="0; url=./blog/html/index.html" />
+        <title></title>
     </head>
-    <body/>
-</html>
+    <body></body>
+</html>
 .. toctree::
    :maxdepth: 1
 
+   2012/07/05/tinkerer_0_4_beta_released
    2012/02/09/tinkerer_beta_0_3_released
    2011/12/19/tinkerer_0_2_beta_released
    2011/12/13/tinkerer_0_1_beta_is_out_

tinkerer/__init__.py

     CONTRIBUTORS file)
     :license: FreeBSD, see LICENSE file
 '''
-__version__ = "0.3.1b"
+__version__ = "0.4b"
 
 master_doc = "master"
 source_suffix = ".rst"

tinkerer/__templates/conf.py

 # linked directly
 rss_service = None
 
+# Number of blog posts per page
+posts_per_page = 2
+
 # **************************************************************
 # Edit lines below to further customize Sphinx build
 # **************************************************************

tinkerer/__templates/index.html

-<html xmlns="http://www.w3.org/1999/xhtml">
+<!doctype html>
+<html>
     <head>
         <meta http-equiv="REFRESH" content="0; url=./blog/html/index.html" />
+        <title></title>
     </head>
-    <body/>
+    <body></body>
 </html>

tinkerer/cmdline.py

 import shutil
 import sphinx
 import sys
+import locale
 from tinkerer import draft, page, paths, post, writer
 
 
     if filename_only:
         print("index.html")
 
+    # copy some extra files to the output directory
+    if os.path.exists("_copy"):
+        shutil.copytree("_copy/", paths.html)
+        
     return sphinx.main(flags)
 
 
     '''
     Parses command line and executes required action.
     '''
+    locale.setlocale(locale.LC_ALL, '')
     parser = argparse.ArgumentParser()
     group = parser.add_mutually_exclusive_group()
     group.add_argument("-s", "--setup", action="store_true", help="setup a new blog")

tinkerer/ext/aggregator.py

     Generates aggregated pages.
     '''
     env = app.builder.env
+    posts_per_page = app.config.posts_per_page
 
     # get post groups
-    groups = [env.blog_posts[i:i+10] for i in range(0, len(env.blog_posts), 10)]
+    groups = [env.blog_posts[i:i+posts_per_page] for i in range(0, 
+                    len(env.blog_posts), posts_per_page)]
 
     # for each group
     for i, posts in enumerate(groups):
                     post[:11], # first 11 characters is path (YYYY/MM/DD/)
                     post[11:], # following characters represent filename
                     True)      # hyperlink title to post
+            metadata.body = patch.strip_xml_declaration(metadata.body)
             context["posts"].append(metadata)
 
 

tinkerer/ext/blog.py

     CONTRIBUTORS file)
     :license: FreeBSD, see LICENSE file
 '''
-from tinkerer.ext import aggregator, author, filing, metadata, rss, uistr
+from tinkerer.ext import aggregator, author, filing, hidemail, metadata, readmore, rss, uistr
 import gettext
 
 
     app.add_config_value("author", "Winston Smith", True)
     app.add_config_value("rss_service", None, True)
     app.add_config_value("website", "http://127.0.0.1/blog/html/", True)
-
+    app.add_config_value("posts_per_page", 10, True)
+    
     # new directives
     app.add_directive("author", author.AuthorDirective)
     app.add_directive("comments", metadata.CommentsDirective)
             filing.create_filing_directive("tags"))
     app.add_directive("categories", 
             filing.create_filing_directive("categories"))
+    app.add_directive("more", readmore.InsertReadMoreLink)
 
+    # new roles
+    app.add_role('email', hidemail.email_role)
+    
     # event handlers
     app.connect("builder-inited", initialize)
     app.connect("source-read", source_read)

tinkerer/ext/filing.py

File contents unchanged.

tinkerer/ext/hidemail.py

+'''
+    hidemail
+    ~~~~~~~~
+
+    Email obfuscation role
+    
+    The obfuscation code was taken from
+    
+        http://pypi.python.org/pypi/bud.nospam
+        
+    Email obfuscation role for Sphinx:
+    
+        http://pypi.python.org/pypi/sphinxcontrib-email
+
+    :copyright: Copyright 2011 by Kevin Teague
+    :copyright: Copyright 2012 by Christian Jann
+    :license: FreeBSD. Parts of this file are licensed under BSD license. See
+    LICENSE file.
+'''
+from docutils import nodes
+import re
+from tinkerer.ext.uistr import UIStr
+
+try:
+    maketrans = ''.maketrans
+except AttributeError:
+    # fallback for Python 2
+    from string import maketrans
+
+
+rot_13_trans = maketrans(
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+    "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"
+)
+
+
+def rot_13_encrypt(line):
+    """Rotate 13 encryption.
+
+    """
+    line = line.translate(rot_13_trans)
+    line = re.sub('(?=[\\"])', r'\\', line)
+    line = re.sub('\n', r'\n', line)
+    line = re.sub('@', r'\\100', line)
+    line = re.sub('\.', r'\\056', line)
+    line = re.sub('/', r'\\057', line)
+    return line
+
+
+def js_obfuscated_text(text):
+    """
+    ROT 13 encryption with embedded in Javascript code to decrypt
+    in the browser.
+    """
+    return """<noscript>(%s)</noscript>
+              <script type="text/javascript">document.write(
+              "%s".replace(/[a-zA-Z]/g,
+              function(c){
+                return String.fromCharCode(
+                (c<="Z"?90:122)>=(c=c.charCodeAt(0)+13)?c:c-26);}));
+              </script>""" % (UIStr.MAIL_HIDDEN_BY_JAVASCRIPT, rot_13_encrypt(text))
+
+
+def js_obfuscated_mailto(email, displayname=None):
+    """
+    ROT 13 encryption within an Anchor tag w/ a mailto: attribute
+    """
+    if not displayname:
+        displayname = email
+    return js_obfuscated_text("""<a href="mailto:%s">%s</a>""" % (
+        email, displayname
+    ))
+
+
+def email_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
+    """
+    Role to obfuscate e-mail addresses.
+    """
+    try:
+        # needed in Python 2
+        text = text.decode("utf-8").encode("utf-8")
+    except AttributeError:
+        pass
+    
+    # Handle addresses of the form "Name <name@domain.org>"
+    if '<' in text and '>' in text:
+        name, email = text.split('<')
+        email = email.split('>')[0]
+    elif '(' in text and ')' in text:
+        name, email = text.split('(')
+        email = email.split(')')[0]
+    else:
+        name = text
+        email = name
+
+    obfuscated = js_obfuscated_mailto(email, displayname=name)
+    node = nodes.raw('', obfuscated, format="html")
+    return [node], []

tinkerer/ext/locale/fr/LC_MESSAGES/tinkerer.mo

Binary file added.

tinkerer/ext/locale/fr/LC_MESSAGES/tinkerer.pot

+# French translations for tinkerer package.
+# Copyright (C) 2012
+# Eric de la Musse <eric@pouik.org>, 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2012-01-23 11:21+CET\n"
+"PO-Revision-Date: 2012-04-23 19:23+0200\n"
+"Last-Translator: Eric de la Musse <eric@pouik.org>\n"
+"Language-Team: French\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ext/aggregator.py:50 ext/metadata.py:118 ext/metadata.py:134
+msgid "Home"
+msgstr "Accueil"
+
+#: ext/aggregator.py:54
+msgid "Newer"
+msgstr "Nouveaux billets"
+
+#: ext/aggregator.py:56
+msgid "Page %d"
+msgstr "Page %d"
+
+#: ext/aggregator.py:63
+msgid "Older"
+msgstr "Anciens billets"
+
+#: ext/filing.py:78 ext/metadata.py:136
+msgid "Blog Archive"
+msgstr "Archives"
+
+#: ext/filing.py:89
+msgid "Posts tagged with <span class=\"title_tag\">%s</span>"
+msgstr "Billets marquĂŠs avec <span class=\"title_tag\">%s</span>"
+
+#: ext/filing.py:102
+msgid "Filed under <span class=\"title_category\">%s</span>"
+msgstr "CatĂŠgories <span class=\"title_category\">%s</span>"
+
+#: ext/metadata.py:133
+msgid "Recent Posts"
+msgstr "Billets rĂŠcents"
+
+#: ext/metadata.py:135
+msgid "Posted by"
+msgstr "Écrit par"
+
+#: ext/metadata.py:137
+msgid "Filed under"
+msgstr "CatĂŠgories"
+
+#: ext/metadata.py:138
+msgid "Tags"
+msgstr "Etiquettes"
+
+#: ext/metadata.py:139
+msgid "%B %d, %Y"
+msgstr "%d %B %Y"

tinkerer/ext/locale/pl/LC_MESSAGES/tinkerer.mo

Binary file added.

tinkerer/ext/locale/pl/LC_MESSAGES/tinkerer.pot

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: 2012-01-23 11:21+CET\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: ENCODING\n"
+"Generated-By: pygettext.py 1.5\n"
+
+
+#: ./ext/aggregator.py:50 ./ext/metadata.py:118 ./ext/metadata.py:134
+msgid "Home"
+msgstr "Strona głóna"
+
+#: ./ext/aggregator.py:54
+msgid "Newer"
+msgstr "Nowsze"
+
+#: ./ext/aggregator.py:56
+msgid "Page %d"
+msgstr "Strona %d"
+
+#: ./ext/aggregator.py:63
+msgid "Older"
+msgstr "Starsze"
+
+#: ./ext/filing.py:78 ./ext/metadata.py:136
+msgid "Blog Archive"
+msgstr "Archiwum"
+
+#: ./ext/filing.py:89
+msgid "Posts tagged with <span class=\"title_tag\">%s</span>"
+msgstr "Tagi wpisu <span class=\"title_tag\">%s</span>"
+
+#: ./ext/filing.py:102
+msgid "Filed under <span class=\"title_category\">%s</span>"
+msgstr "W kategorii <span class=\"title_category\">%s</span>"
+
+#: ./ext/metadata.py:133
+msgid "Recent Posts"
+msgstr "Ostatnie wpisy"
+
+#: ./ext/metadata.py:135
+msgid "Posted by"
+msgstr "Opublikowany przez"
+
+#: ./ext/metadata.py:137
+msgid "Filed under"
+msgstr "W kategorii"
+
+#: ./ext/metadata.py:138
+msgid "Tags"
+msgstr "Tagi"
+
+#: ./ext/metadata.py:139
+msgid "%B %d, %Y"
+msgstr "%d %B %Y"
+

tinkerer/ext/metadata.py

     metadata
     ~~~~~~~~
 
-    Blog metadata extension. The extension extracts and computes metadata 
+    Blog metadata extension. The extension extracts and computes metadata
     associated with blog posts/pages and stores it in the environment.
 
     :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
     :license: FreeBSD, see LICENSE file
 '''
 import re
-import datetime 
+import datetime
 from sphinx.util.compat import Directive
 from docutils.parsers.rst import directives
 import tinkerer
 from tinkerer.ext.uistr import UIStr
+from tinkerer.utils import name_from_title
 
 
 
         Initializes metadata with default values.
         '''
         self.is_post = False
+        self.is_page = False
         self.title = None
         self.link = None
         self.date = None
         self.filing = { "tags": [], "categories": [] }
         self.comments, self.comment_count = False, False
 
-               
+
 
 class CommentsDirective(Directive):
     '''
     env.blog_metadata[docname] = Metadata()
     metadata = env.blog_metadata[docname]
 
+    # if it's a page
+    if docname.startswith("pages/"):
+      metadata.is_page = True
+      return
+
     # posts are identified by ($YEAR)/($MONTH)/($DAY) paths
     match = re.match(r"\d{4}/\d{2}/\d{2}/", docname)
 
 
 def process_metadata(app, env):
     '''
-    Processes metadata after all sources are read - the function determines 
+    Processes metadata after all sources are read - the function determines
     post and page ordering, stores doc titles and adds "Home" link to page
     list.
     '''
             if relations[doc][0] == tinkerer.master_doc:
                 if env.blog_metadata[doc].is_post:
                     env.blog_posts.append(doc)
-                else:
+                elif env.blog_metadata[doc].is_page:
                     env.blog_pages.append(doc)
-     
+
     env.blog_page_list = [("index", UIStr.HOME)] + [(page, env.titles[page].astext()) for page in env.blog_pages]
 
 
     # blog tagline and pages
     context["tagline"] = app.config.tagline
     context["pages"] = env.blog_page_list
-    
+
     # set translation context variables
     context["text_recent_posts"] = UIStr.RECENT_POSTS
     context["text_posted_by"] = UIStr.POSTED_BY
     context["text_blog_archive"] = UIStr.BLOG_ARCHIVE
     context["text_filed_under"] = UIStr.FILED_UNDER
     context["text_tags"] = UIStr.TAGS
+    context["text_tags_cloud"] = UIStr.TAGS_CLOUD
+    context["text_categories"] = UIStr.CATEGORIES
     context["timestamp_format"] = UIStr.TIMESTAMP_FMT
 
     # recent posts
-    context["recent"] = [(post, env.titles[post].astext()) for post 
+    context["recent"] = [(post, env.titles[post].astext()) for post
             in env.blog_posts[:20]]
+    # tags & categories
+    tags = dict((t, 0) for t in env.filing["tags"])
+    taglinks = dict((t, name_from_title(t)) for t in env.filing["tags"])
+    categories = dict((c, 0) for c in env.filing["categories"])
+    catlinks = dict([(c, name_from_title(c)) for c in env.filing["categories"]])
+    for post in env.blog_posts:
+        p = env.blog_metadata[post]
+        for tag in p.filing["tags"]:
+            tags[tag[1]] += 1
+        for cat in p.filing["categories"]:
+            categories[cat[1]] += 1
+    context["tags"] = tags
+    context["taglinks"] = taglinks
+    context["categories"] = categories
+    context["catlinks"] = catlinks
 
     # if there is metadata for the page, it is not an auto-generated one
     if pagename in env.blog_metadata:
     # otherwise provide default metadata
     else:
         context["metadata"] = Metadata()
-

tinkerer/ext/patch.py

 '''
 import re
 import xml.dom.minidom
+from tinkerer.ext.uistr import UIStr
 
 try:
     from html.entities import name2codepoint
     doc = xml.dom.minidom.parseString(in_str)
     patch_node(doc, docpath)
 
+    body = doc.toxml()
+    if(docname!=None):
+        body = make_read_more_link(body, docpath, docname)
+    
     if link_title:
-        return hyperlink_title(doc.toxml(), docpath, docname)
+        return hyperlink_title(body, docpath, docname)
     else:
-        return doc.toxml()
+        return body
 
 
 
 def hyperlink_title(body, docpath, docname):
-    body = body.replace("<h1>", '<a href="%s.html"><h1>' % 
+    """
+    Hyperlink titles by embedding appropriate a tag inside
+    h1 tags (which should only be post titles). 
+    """
+    body = body.replace("<h1>", '<h1><a href="%s.html">' % 
             (docpath + docname), 1)
-    body = body.replace("</h1>", "</h1></a>", 1)
+    body = body.replace("</h1>", "</a></h1>", 1)
     return body
 
 
 
+def make_read_more_link(body, docpath, docname):            
+    """
+    Create "read more" link if marker exists.
+    """
+    marker_more = '<a name="more"> </a>'
+    pos = body.find(marker_more)
+
+    if pos == -1:
+        return body
+
+    body = body[:pos]
+    return body + ('<a class="readmore" href="%s.html#more">%s</a></div>' %
+                (docpath + docname, UIStr.READ_MORE))
+
+
+
 def patch_node(node, docpath):
     '''
     Recursively patches links in nodes.
             src.value = docpath + src.value 
     # if node is hyperlink            
     elif node_name == "a":
-        if "internal" in node.getAttribute("class"):
-            ref = node.getAttributeNode("href")
-            ref.value = docpath + ref.value
+        ref = node.getAttributeNode("href")
+        # skip anchor links <a name="anchor1"></a>, <a name="more"/>
+        if ref != None:
+            # patch links only - either starting with "../" or having
+            # "internal" class
+            is_relative = ref.value.startswith("../") 
+            if is_relative or "internal" in node.getAttribute("class"):
+                ref.value = docpath + ref.value
+            
 
     # recurse            
     for node in node.childNodes:
         patch_node(node, docpath)
 
+
+def strip_xml_declaration(body):
+    """
+    Remove XML declaration from document body.
+    """
+    return body.replace('<?xml version="1.0" ?>', '')

tinkerer/ext/readmore.py

+'''
+    readmore
+    ~~~~~~~~
+
+    Read more directive.
+
+    :copyright: Copyright 2012 by Christian Jann
+    :license: FreeBSD, see LICENSE file
+'''
+from docutils import nodes
+from sphinx.util.compat import Directive
+
+
+
+class InsertReadMoreLink(Directive):
+    '''
+    Sphinx extension for inserting a "Read more..." link.
+    '''
+
+    has_content = True
+    required_arguments = 0
+
+
+    def run(self):
+        return [nodes.raw("", '<a name="more"> </a>', format="html")] 
+

tinkerer/ext/rss.py

         context["items"].append({
                     "title": env.titles[post].astext(),
                     "link": link,
-                    "description": patch.patch_links(
+                    "description": patch.strip_xml_declaration(patch.patch_links(
                             env.blog_metadata[post].body, 
-                            app.config.website + post[:11]),
+                            app.config.website + post[:11])),
                     "categories": categories,
                     "pubDate": timestamp
                 })

tinkerer/ext/uistr.py

         UIStr.BLOG_ARCHIVE = unicode(_("Blog Archive"), "utf-8")
         UIStr.FILED_UNDER = unicode(_("Filed under"), "utf-8")
         UIStr.TAGS = unicode(_("Tags"), "utf-8")
+        UIStr.TAGS_CLOUD = unicode(_("Tags Cloud"), "utf-8")
+        UIStr.CATEGORIES = unicode(_("Categories"), "utf-8")
         UIStr.TIMESTAMP_FMT = unicode(_('%B %d, %Y'), "utf-8")
         UIStr.TAGGED_WITH_FMT = unicode(_('Posts tagged with <span class="title_tag">%s</span>'), "utf-8")
         UIStr.FILED_UNDER_FMT = unicode(_('Filed under <span class="title_category">%s</span>'), "utf-8")
         UIStr.NEWER = unicode(_("Newer"), "utf-8")
         UIStr.OLDER = unicode(_("Older"), "utf-8")
         UIStr.PAGE_FMT = unicode(_("Page %d"), "utf-8")
+        UIStr.READ_MORE = unicode(_("Read more..."), "utf-8")
+        UIStr.MAIL_HIDDEN_BY_JAVASCRIPT = unicode(_("Javascript must be enabled to see this e-mail address"), "utf-8")
 

tinkerer/themes/boilerplate/aggregated.html

+{#-
+    boilerplate/aggregated.html
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Front page and following pages aggregating multiple posts per page.
+
+    :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file). 
+    :license: FreeBSD, see LICENSE file
+-#}
+
+{%- extends "page.html" -%}
+
+{%- set archive_title = archive_title is not defined and ' Blog Archive ' or archive_title -%}
+
+{%- block body -%}
+    {%- for metadata in posts -%}
+        {{ timestamp(metadata.date, timestamp_format) }}
+        {{ metadata.body }}
+        {{ post_meta(metadata, metadata.comment_count) }}
+        {%- if not loop.last -%}<div class="separator post_separator"></div>{%- endif -%}
+    {%- endfor -%}
+    <div class="archive_link">
+        <a href="{{ pathto('archive') }}">{{ archivechar }}{{ archive_title }}{{ archivechar }}</a>
+    </div>
+{%- endblock -%}
+

tinkerer/themes/boilerplate/archive.html

+{#-
+    boilerplate/archive.html
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Archive page.
+
+    :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file)
+    :license: FreeBSD, see LICENSE file
+-#}
+
+{%- extends "page.html" -%}
+
+{%- block body -%}
+    <div class="archive">
+        <h1>{{ title }}</h1>
+        {%- for year in years|sort(reverse=True) -%}
+        <div class="year">
+            <h1>{{ year }}</h1>
+            <ul>
+                {%- for metadata in years[year] -%}
+                <li>
+                    {{ timestamp(metadata.date, timestamp_short_format) }}
+                    <h2><a href="{{ pathto(metadata.link) }}">{{ metadata.title }}</a></h2>
+                    {{ post_meta(metadata, metadata.comment_count) }}
+                </li>
+                {%- if not loop.last -%}<li><div class="separator summary_separator"></div></li>{%- endif -%}
+                {%- endfor -%}
+            </ul>
+        </div>
+        {%- if not loop.last -%}<div class="separator year_separator"></div>{%- endif -%}
+        {%- endfor -%}
+    </div>
+{%- endblock -%}

tinkerer/themes/boilerplate/categories.html

+{#-
+    boilerplate/categories.html
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Sidebar list of all categories.
+
+    :copyright: Copyright 2012 by IĂąigo Serna
+    :license: FreeBSD, see LICENSE file
+-#}
+
+<div class="widget">
+    <h1>{{ text_categories }}</h1>
+    <ul>
+      {%- for category, count in categories|dictsort -%}
+        <li><a href="{{ pathto('categories/' + category|lower) }}">{{ category }}</a> ({{ count }})</li>
+      {%- endfor -%}
+    </ul>
+</div>

tinkerer/themes/boilerplate/layout.html

+{#-
+    boilerplate/layout.html
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Master layout template for Tinkerer blog themes.
+
+    :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file). 
+    :license: FreeBSD. Parts of this file are licensed under BSD license. See
+    LICENSE file.
+-#}
+
+{#- Doctype -#}
+{%- block doctype -%}
+<!doctype html>
+{%- endblock -%}
+
+{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
+                         (sidebars != []) -%}
+{%- set url_root = pathto('', 1) -%}
+{%- if not embedded and docstitle -%}
+  {%- set titlesuffix = " &mdash; "|safe + docstitle|e -%}
+{%- else -%}
+  {%- set titlesuffix = "" -%}
+{%- endif -%}
+
+{%- set prevchar = prevchar is not defined and ' &laquo; ' or prevchar -%}
+{%- set nextchar = nextchar is not defined and ' &raquo; ' or nextchar -%}
+{%- set archivechar = archivechar is not defined and ' &mdash; ' or archivechar -%}
+
+{%- if rss_service -%}
+    {%- set rss_feed_link = rss_service -%}
+{%- else -%}
+    {%- set rss_feed_link = pathto('rss') -%}
+{%- endif -%}    
+
+{%- set rss_in_page_nav = rss_in_page_nav is not defined or rss_in_page_nav -%}
+{%- set rss_link_text = rss_link_text is not defined and None or rss_link_text -%}
+{%- set rss_symbol = rss_symbol is not defined or rss_symbol -%}
+{%- set timestamp_format = timestamp_format is not defined and 
+    '<span class="month">%B</span> <span class="day">%d</span>, <span class="year">%Y</day>' 
+    or timestamp_format -%}
+{%- set timestamp_short_format = timestamp_short_format is not defined and 
+    '<span class="month">%b</span> <span class="day">%d</span>' or timestamp_short_format -%}
+
+{%- macro script() -%}
+    <script type="text/javascript">
+      var DOCUMENTATION_OPTIONS = {
+        URL_ROOT:    '{{ url_root }}',
+        VERSION:     '{{ release|e }}',
+        COLLAPSE_INDEX: false,
+        FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
+        HAS_SOURCE:  {{ has_source|lower }}
+      };
+    </script>
+    {%- for scriptfile in script_files -%}
+        {#- Hack to filter out jquery.js auto-included by Sphinx since jquery is 
+           already included in head. -#}
+        {%- if scriptfile != "_static/jquery.js" -%}
+            <script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
+        {%- endif -%}
+    {%- endfor -%}
+{%- endmacro -%}
+
+{%- macro css() -%}
+    <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
+    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
+    {%- for cssfile in css_files -%}
+    <link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
+    {%- endfor -%}
+{%- endmacro -%}
+
+{#- RSS link -#}
+{%- macro rss_link() -%}
+    <div class="rss">
+        <a href="{{ rss_feed_link }}" title="Subscribe via RSS">
+            {%- if rss_symbol -%}<span class="webfont">B</span>{%- endif -%}
+            {% if rss_link_text -%}{{ rss_link_text }}{% endif -%}
+        </a>
+    </div>
+{%- endmacro -%}
+
+{#- prev/next -#}
+{%- macro relbar() -%}
+    {%- if prev or next -%}
+    <div class="related">
+        <ul>
+            <li class="left">
+            {%- if prev -%}
+                {{ prevchar }}<a href="{{ prev.link|e }}">{{ prev.title }}</a>
+            {%- endif -%}
+            </li>
+            <li class="right">
+            {%- if next -%}
+                <a href="{{ next.link|e }}">{{ next.title }}</a>{{ nextchar }}
+            {%- endif -%}
+            </li>
+        </ul>
+    </div>
+    {%- endif -%}
+{%- endmacro -%}
+
+{#- Timestamp -#}
+{%- macro timestamp(date, fmt) -%}
+    {%- if date -%}
+        <div class="timestamp postmeta">
+            <span>{{ date.strftime(fmt) }}</span> 
+        </div>
+    {%- endif -%}
+{%- endmacro -%}
+
+{#- Author -#}
+{%- macro author(author_name) -%}
+    {%- if author_name -%}
+        <div class="author">
+            <span>Posted by {{ author_name }}</span>
+        </div>
+    {%- endif -%}
+{%- endmacro -%}
+
+{#- Categories -#}
+{%- macro category_list(post_categories) -%}
+    {%- if post_categories -%}
+        <div class="categories">
+            <span>
+                Filed under:
+                {% for link, category in post_categories -%}
+                    <a href="{{ pathto('categories/' + link + '.html', 1) }}">{{ category }}</a>
+                    {%- if not loop.last -%}, {% endif -%}
+                {% endfor -%}
+            </span>
+        </div>
+    {%- endif -%}
+{%- endmacro -%}    
+
+{#- Tags -#}
+{%- macro tag_list(post_tags) -%}
+    {%- if post_tags -%}
+        <div class="tags">
+            <span>
+                Tags:
+                {% for link, tag in post_tags -%}
+                    <a href="{{ pathto('tags/' + link + '.html', 1) }}">{{ tag }}</a>
+                    {%- if not loop.last -%}, {% endif -%}
+                {% endfor -%}
+            </span>
+        </div>
+    {%- endif -%}
+{%- endmacro -%}
+
+{#- Comment count -#}
+{%- macro comment_count(code) -%}
+    {%- if code -%}
+        <div class="comments">
+            {{ code }}
+        </div>
+    {%- endif -%}
+{%- endmacro -%}
+
+{#- Post metadata -#}
+{%- macro post_meta(metadata, comment_count_code=None) -%}
+    <div class="postmeta">
+        {{ author(metadata.author) }}
+        {{ category_list(metadata.filing["categories"]) }}
+        {{ tag_list(metadata.filing["tags"]) }}
+        {{ comment_count(comment_count_code) }}
+    </div>
+{%- endmacro -%}
+
+<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
+<!--[if IE 7]>    <html class="no-js ie7 oldie" lang="en"> <![endif]-->
+<!--[if IE 8]>    <html class="no-js ie8 oldie" lang="en"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  {{ metatags }}
+  {%- block htmltitle -%}
+  <title>{{ title|striptags|e }}{{ titlesuffix }}</title>
+  {%- endblock -%}
+  <!-- Mobile viewport optimized: j.mp/bplateviewport -->
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <link rel="stylesheet" href="{{ pathto('_static/style.css', 1) }}" type="text/css">
+  {{ css() }}
+  <link rel="stylesheet" href="{{ pathto('_static/webfont.css', 1) }}" type="text/css">
+  {%- if not embedded -%}
+  {%- if favicon -%}
+  <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}" />
+  {%- endif -%}
+  <!-- Load modernizr and JQuery -->
+  <script src="{{ pathto('_static/modernizr-2.0.6.min.js', 1) }}"></script>
+  <!-- Grab Google CDN's jQuery, fall back to local if offline -->
+  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+  <script>window.jQuery || document.write('<script src="{{ pathto('_static/jquery-1.6.2.min.js', 1) }}"><\/script>')</script>
+  {%- if use_opensearch -%}
+  <link rel="search" type="application/opensearchdescription+xml"
+        title="{% trans docstitle=docstitle|e -%}Search within {{ docstitle }}{% endtrans -%}"
+        href="{{ pathto('_static/opensearch.xml', 1) }}"/>
+  {%- endif -%}
+  {%- endif -%}
+{%- block linktags -%}
+  {%- if hasdoc('about') -%}
+  <link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
+  {%- endif -%}
+  {%- if hasdoc('genindex') -%}
+  <link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
+  {%- endif -%}
+  {%- if hasdoc('search') -%}
+  <link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
+  {%- endif -%}
+  {%- if hasdoc('copyright') -%}
+  <link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
+  {%- endif -%}
+  <link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
+  {%- if parents -%}
+  <link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" />
+  {%- endif -%}
+  {%- if next -%}
+  <link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
+  {%- endif -%}
+  {%- if prev -%}
+  <link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
+  {%- endif -%}
+  <link rel="alternate" type="application/rss+xml" title="RSS" href="{{ rss_feed_link }}" />
+{%- endblock -%}
+{%- block extrahead -%} {% endblock -%}
+</head>
+<body>
+  <div id="container">
+
+{%- block header -%}
+    <header>
+      <hgroup>
+        <h1><a href="{{ pathto(pages[0][0]) }}">{{ shorttitle|e }}</a></h1>
+        <h2>{{ tagline|e }}</h2>
+      </hgroup>
+    </header>
+{% endblock -%}
+
+{%- block navigation -%}
+    <nav class="clearfix">
+      <ul class="main-navigation">
+        {%- block quicklinks -%}
+          {%- if rss_in_page_nav -%}
+          <li class="quicklink">{{ rss_link() }}</li>
+          {%- endif -%}
+        {%- endblock -%}
+        {% for page in pages -%}
+        <li class="main-nav">
+          <a href="{{ pathto(page[0]) }}">{{ page[1]|e }}</a>
+        </li>
+        {% endfor -%}  
+      </ul>
+    </nav>
+{%- endblock -%}
+
+    <div class="main">
+{%- block content -%}
+      <div class="content clearfix">
+      {%- block document -%}
+        <div class="documentwrapper">
+          {%- block relbar1 -%}{{ relbar() }}{% endblock -%}
+            <article class="document">
+              {% block body -%} {% endblock -%}
+            </article>
+          {%- block relbar2 -%}{{ relbar() }}{% endblock -%}
+        </div>
+      {%- endblock -%}
+      {%- if render_sidebar -%}
+        <aside class="sidebar">
+          {%- if sidebars != None -%}
+            {%- for sidebartemplate in sidebars -%}
+            <section>
+              {%- include sidebartemplate -%}
+            </section>
+            {%- endfor -%}
+          {%- endif -%}
+        </aside>
+      {%- endif -%}
+      </div>
+{%- endblock -%}
+    </div>
+
+{%- block footer -%}
+    <footer>
+    {%- if show_copyright -%}
+      {% trans copyright=copyright|e -%}&copy; Copyright {{ copyright }}. {% endtrans -%}
+    {%- endif -%}
+    {%- if show_sphinx -%}
+      Powered by <a href="http://www.tinkerer.me/">Tinkerer</a> and <a href="http://sphinx.pocoo.org/">Sphinx</a>.
+    {%- endif -%}
+    </footer>
+{%- endblock -%}
+
+  </div> <!--! end of #container -->
+
+  <!-- JavaScript at the bottom for fast page loading -->
+  {%- if not embedded -%}
+  {{ script() }}
+  {%- endif -%}
+  {#- Comment plug-in initialization -#}
+  {%- if comment_enabler -%}{{ comment_enabler }}{%- endif -%}
+  <!--[if lt IE 7 ]>
+    <script src="//ajax.googleapis.com/ajax/libs/chrome-frame/1.0.3/CFInstall.min.js"></script>
+    <script>window.attachEvent('onload',function(){CFInstall.check({mode:'overlay'})})</script>
+  <![endif]-->
+</body>
+</html>
+

tinkerer/themes/boilerplate/page.html

+{#-
+    boilerplate/page.html
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Master layout for Tinkerer pages.
+
+    :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file)
+    :license: FreeBSD, see LICENSE file
+-#}
+
+{%- extends "layout.html" -%}
+
+{%- block body -%}
+    {{ timestamp(metadata.date, timestamp_format) }}
+    {{ body }}
+    {{ post_meta(metadata) }}
+    {{ comments }}
+{%- endblock -%}

tinkerer/themes/boilerplate/recent.html

+{#-
+    boilerplate/recent.html
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Sidebar list of recent posts.
+
+    :copyright: Copyright 2011-2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file)
+    :license: FreeBSD, see LICENSE file
+-#}
+
+{%- set recent_count = recent_count is not defined and 10 or recent_count -%}
+
+<div class="widget">
+    <h1>Recent Posts</h1>
+    <ul>
+        {%- for post, post_title in recent[:recent_count] -%}
+        <li>
+            <a href="{{ pathto(post) }}">{{ post_title }}</a>
+        </li>
+        {%- endfor -%}
+    </ul>
+</div>
+

tinkerer/themes/boilerplate/rss.html

+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0">
+    <channel>
+        <title>{{ title }}</title>
+        <link>{{ link }}</link>
+        <description>{{ description }}</description>
+        <language>{{ language }}</language>
+        <pubDate>{{ pubDate }}</pubDate>
+        {% for item in items %}
+        <item>
+            <link>{{ item.link }}</link>
+            <guid>{{ item.link }}</guid>
+            <title>{{ item.title }}</title>
+            <description><![CDATA[{{ item.description }}]]></description>
+            {%- for category in item.categories %}
+            <category><![CDATA[ {{ category }} ]]></category>
+            {%- endfor %}
+             <pubDate>{{ item.pubDate }}</pubDate>
+        </item>
+    {% endfor %}
+    </channel>
+</rss>

tinkerer/themes/boilerplate/search.html

+{#-
+    boilerplate/search.html
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Template for the search page.
+
+    :copyright: Copyright 2007-2012 by the Sphinx team.
+    :license: BSD, see LICENSE file
+-#}
+
+{%- extends "layout.html" -%}
+{%- set title = _('Search') -%}
+{%- set script_files = script_files + ['_static/searchtools.js'] -%}
+{%- block extrahead -%}
+  <script type="text/javascript">
+    jQuery(function() { Search.loadIndex("{{ pathto('searchindex.js', 1) }}"); });
+  </script>
+  {{ super() }}
+{%- endblock -%}
+{%- block body -%}
+  <h1 id="search-documentation">{{ _('Search') }}</h1>
+  <div id="fallback" class="admonition warning">
+  <script type="text/javascript">$('#fallback').hide();</script>
+  <p>
+    {%- trans -%}Please activate JavaScript to enable the search
+    functionality.{%- endtrans -%}
+  </p>
+  </div>
+  <p>
+    {%- trans -%}From here you can search these documents. Enter your search
+    words into the box below and click "search". Note that the search
+    function will automatically search for all of the words. Pages
+    containing fewer words won't appear in the result list.{%- endtrans -%}
+  </p>
+  <form action="" method="get">
+    <input type="text" name="q" value="" />
+    <input type="submit" value="{{ _('search') }}" />
+    <span id="search-progress" style="padding-left: 10px"></span>
+  </form>
+  {%- if search_performed -%}
+    <h2>{{ _('Search Results') }}</h2>
+    {%- if not search_results -%}
+      <p>{{ _('Your search did not match any results.') }}</p>
+    {%- endif -%}
+  {%- endif -%}
+  <div id="search-results">
+  {%- if search_results -%}
+    <ul>
+    {%- for href, caption, context in search_results -%}
+      <li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
+{#- Temporary disabled as context is broken. 
+        <div class="context">{{ context|e }}</div>
+-#}
+      </li>
+    {%- endfor -%}
+    </ul>
+  {%- endif -%}
+  </div>
+{%- endblock -%}

tinkerer/themes/boilerplate/searchbox.html

+{#-
+    boilerplate/searchbox.html
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Sidebar search box.
+
+    :copyright: Copyright 2012 by Vlad Riscutia and contributors (see
+    CONTRIBUTORS file).
+    :license: FreeBSD, see LICENSE file
+-#}
+
+<div class="widget" id="searchbox">
+    <h1>Search</h1>
+    <form action="{{ pathto('search') }}" method="get">
+        <input type="text" name="q" />
+        <button type="submit"><span class="webfont">L</span></button>
+    </form>
+</div>

tinkerer/themes/boilerplate/static/doctools.js

+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Sphinx JavaScript utilities for all documentation.
+ *
+ * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+/**
+ * make the code below compatible with browsers without
+ * an installed firebug like debugger
+if (!window.console || !console.firebug) {
+  var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
+    "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
+    "profile", "profileEnd"];
+  window.console = {};
+  for (var i = 0; i < names.length; ++i)
+    window.console[names[i]] = function() {};
+}
+ */
+
+/**
+ * small helper function to urldecode strings
+ */
+jQuery.urldecode = function(x) {
+  return decodeURIComponent(x).replace(/\+/g, ' ');
+}
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+  if (typeof s == 'undefined')
+    s = document.location.search;
+  var parts = s.substr(s.indexOf('?') + 1).split('&');
+  var result = {};
+  for (var i = 0; i < parts.length; i++) {
+    var tmp = parts[i].split('=', 2);
+    var key = jQuery.urldecode(tmp[0]);
+    var value = jQuery.urldecode(tmp[1]);
+    if (key in result)
+      result[key].push(value);
+    else
+      result[key] = [value];
+  }
+  return result;
+};
+
+/**
+ * small function to check if an array contains
+ * a given item.
+ */
+jQuery.contains = function(arr, item) {
+  for (var i = 0; i < arr.length; i++) {
+    if (arr[i] == item)
+      return true;
+  }
+  return false;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+  function highlight(node) {
+    if (node.nodeType == 3) {
+      var val = node.nodeValue;
+      var pos = val.toLowerCase().indexOf(text);
+      if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) {
+        var span = document.createElement("span");
+        span.className = className;
+        span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+        node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+          document.createTextNode(val.substr(pos + text.length)),
+          node.nextSibling));
+        node.nodeValue = val.substr(0, pos);
+      }
+    }
+    else if (!jQuery(node).is("button, select, textarea")) {
+      jQuery.each(node.childNodes, function() {
+        highlight(this);
+      });
+    }
+  }
+  return this.each(function() {
+    highlight(this);
+  });
+};
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+var Documentation = {
+
+  init : function() {
+    this.fixFirefoxAnchorBug();