Browse Source

Merge branch 'master' into collapse-code

Jörg Dietrich 11 years ago
parent
commit
c6258ecf95

+ 34 - 0
clean_summary/README.md

@@ -0,0 +1,34 @@
+#Clean Summary Plugin#
+
+Plugin to clean your summary of excess images. Images can take up much more
+space than text and lead to summaries being different sizes on archive and 
+index pages. With this plugin you can specify a maximum number of images that
+will appear in your summaries.
+
+There is also an option to include a minimum of one image.
+
+##Settings##
+
+This plugin has two settings. `CLEAN_SUMMARY_MAXIMUM` which takes an int, and 
+`CLEAN_SUMMARY_MINIMUM_ONE` which takes a boolean. They default to 0 and False.
+
+`CLEAN_SUMMARY_MAXIMUM` sets the maximum number of images that will appear in 
+your summary.
+
+if `CLEAN_SUMMARY_MINIMUM_ONE` is set to `True` and your summary doesn't already
+contain an image, the plugin will add the first image in your article (if one 
+exists) to the beginning of the summary.
+
+##Requirements##
+
+Requires Beautiful Soup:
+
+    pip install BeautifulSoup4
+
+
+##Usage with Summary Plugin##
+
+If using the summary plugin, make sure summary appears in your plugins before
+clean summary. Eg.
+
+    PLUGINS = ['summary', 'clean_summary', ... ]

+ 1 - 0
clean_summary/__init__.py

@@ -0,0 +1 @@
+from .clean_summary import *

+ 38 - 0
clean_summary/clean_summary.py

@@ -0,0 +1,38 @@
+"""
+Clean Summary
+-------------
+
+adds option to specify maximum number of images to appear in article summary
+also adds option to include an image by default if one exists in your article
+"""
+
+from pelican import signals
+from pelican.contents import Content, Article
+from bs4 import BeautifulSoup
+from six import text_type
+
+def clean_summary(instance):
+    if "CLEAN_SUMMARY_MAXIMUM" in instance.settings:
+        maximum_images = instance.settings["CLEAN_SUMMARY_MAXIMUM"]
+    else:
+        maximum_images = 0
+    if "CLEAN_SUMMARY_MINIMUM_ONE" in instance.settings:
+        minimum_one = instance.settings['CLEAN_SUMMARY_MINIMUM_ONE']
+    else:
+        minimum_one = False
+    if type(instance) == Article:
+        summary = instance.summary
+        summary = BeautifulSoup(instance.summary, 'html.parser')
+        images = summary.findAll('img')
+        if (len(images) > maximum_images):
+            for image in images[maximum_images:]:
+                image.extract()
+        if len(images) < 1 and minimum_one: #try to find one
+            content = BeautifulSoup(instance.content, 'html.parser')
+            first_image = content.find('img')
+            if first_image:
+                summary.insert(0, first_image)
+        instance._summary = text_type(summary)
+
+def register():
+    signals.content_object_init.connect(clean_summary)

+ 15 - 15
custom_article_urls/README.md

@@ -1,17 +1,17 @@
 #Custom Article URLs#
 
-Adds support for defining different default urls for different categories, or 
-different subcategories if using the subcategory plugin.
+This plugin adds support for defining different default URLs for different
+categories, or different subcategories if using the subcategory plugin.
 
 ##Usage##
 
-After adding `custom_article_urls` to your `PLUGINS` add a `CUSTOM_ARTICLE_URLS`
-setting, which is a dictionary of rules. The rules are also a dictionary,
-consisting of the `URL` and the `SAVE_AS` values. 
+After adding `custom_article_urls` to your `PLUGINS`, add a
+`CUSTOM_ARTICLE_URLS` setting, which is a dictionary of rules. The rules are
+also a dictionary, consisting of the `URL` and the `SAVE_AS` values.
 
-For example, if you had two categories, Category 1 and Category 2 and you
-would like Category 1 saved as category-1/article-slug/ and Category 2 saved as
-/year/month/article-slug/ you would add:
+For example, if you had two categories, *Category 1* and *Category 2*, and you
+would like *Category 1* saved as `category-1/article-slug/` and *Category 2*
+saved as `/year/month/article-slug/`, you would add:
 
     CUSTOM_ARTICLE_URLS = {
         'Category 1': {'URL': '{category}/{slug}/,
@@ -20,17 +20,17 @@ would like Category 1 saved as category-1/article-slug/ and Category 2 saved as
             'SAVE_AS': '{date:%Y}/{date:%B}/{slug}/index.html},
         }
 
-If had any other categories they would use the default `ARTICLE_SAVE_AS`
-and `ARTICLE_URL`
+If you had any other categories, they would use the default `ARTICLE_SAVE_AS`
+and `ARTICLE_URL` settings.
 
 If you are using the subcategory plugin, you can define them the same way.
-For example if Category 1 had a subcategory Sub Category you could define
-it's rules with
+For example, if *Category 1* had a subcategory called *Sub Category*, you could
+define its rules with::
 
     'Category 1/Sub Category`: ...
 
 ##Other Usage: Article Metadata##
 
-If you define a url and save_as in your articles metadata, then this plugin
-will not alter that value. So you can still specify special one off urls as 
-normal.
+If you define `URL` and `Save_as` in your article metadata, then this plugin
+will not alter that value. So you can still specify special one-off URLs as
+you normally would.

+ 2 - 9
custom_article_urls/custom_article_urls.py

@@ -11,12 +11,6 @@ from pelican import signals
 from pelican.contents import Content, Category
 from six import text_type
 
-def recursive_name(self):
-    if type(self) is Category:
-        return self.name
-    else:
-        return '{}/{}'.format(recursive_name(self.parent), self.name)
-
 def custom_url(generator, metadata):
     if 'CUSTOM_ARTICLE_URLS' in generator.settings:
         custom_urls = generator.settings['CUSTOM_ARTICLE_URLS']
@@ -28,9 +22,8 @@ def custom_url(generator, metadata):
 
         if 'subcategories' in metadata: #using subcategory plugin
             for subcategory in metadata['subcategories']:
-                subcategory_name = recursive_name(subcategory)
-                if subcategory_name in custom_urls:
-                    pattern_matched = custom_urls[subcategory_name]
+                if subcategory in custom_urls:
+                    pattern_matched = custom_urls[subcategory]
 
         if pattern_matched:
             #only alter url if hasn't been set in the metdata

+ 1 - 1
disqus_static/README.rst

@@ -15,7 +15,7 @@ We use disqus-python package for communication with disqus API:
 Put ``disqus_static.py`` plugin in ``plugins`` folder in pelican installation 
 and use the following in your settings::
 
-    PLUGINS = [u"pelican.plugins.disqus_static"]
+    PLUGINS = [u"disqus_static"]
 
     DISQUS_SITENAME = u'YOUR_SITENAME'
     DISQUS_SECRET_KEY = u'YOUR_SECRET_KEY'

+ 1 - 0
disqus_static/__init__py

@@ -0,0 +1 @@
+from .disqus_static import *

+ 73 - 0
i18n_subsites/README.rst

@@ -0,0 +1,73 @@
+======================
+ I18N Sub-sites Plugin
+======================
+
+This plugin extends the translations functionality by creating internationalized sub-sites for the default site. It is therefore redundant with the *\*_LANG_{SAVE_AS,URL}* variables, so it disables them to prevent conflicts.
+
+What it does
+============
+1. The *\*_LANG_URL* and *\*_LANG_SAVE_AS* variables are set to their normal counterparts (e.g. *ARTICLE_URL*) so they don't conflict with this scheme.
+2. While building the site for *DEFAULT_LANG* the translations of pages and articles are not generated, but their relations to the original content is kept via links to them.
+3. For each non-default language a "sub-site" with a modified config [#conf]_ is created [#run]_, linking the translations to the originals (if available). The configured language code is appended to the *OUTPUT_PATH* and *SITEURL* of each sub-site. For each sub-site, *DEFAULT_LANG* is changed to the language of the sub-site so that articles in a different language are treated as translations.
+
+If *HIDE_UNTRANSLATED_CONTENT* is True (default), content without a translation for a language is generated as hidden (for pages) or draft (for articles) for the corresponding language sub-site.
+
+.. [#conf] For each language a config override is given in the *I18N_SUBSITES* dictionary.
+.. [#run] Using a new *PELICAN_CLASS* instance and its ``run`` method, so each sub-site could even have a different *PELICAN_CLASS* if specified in *I18N_SUBSITES* conf overrides.
+
+Setting it up
+=============
+
+For each extra used language code, a language-specific variables overrides dictionary must be given (but can be empty) in the *I18N_SUBSITES* dictionary
+
+.. code-block:: python
+
+    PLUGINS = ['i18n_subsites', ...]
+
+    # mapping: language_code -> conf_overrides_dict
+    I18N_SUBSITES = {
+        'cz': {
+	    'SITENAME': 'Hezkej blog',
+	    }
+	}
+
+- The language code is the language identifier used in the *lang* metadata. It is appended to *OUTPUT_PATH* and *SITEURL* of each I18N sub-site.
+- The internationalized config overrides dictionary may specify configuration variable overrides — e.g. a different *LOCALE*, *SITENAME*, *TIMEZONE*, etc. However, it **must not** override *OUTPUT_PATH* and *SITEURL* as they are modified automatically by appending the language code.
+
+Localizing templates
+--------------------
+
+Most importantly, this plugin can use localized templates for each sub-site. There are two approaches to having the templates localized:
+
+- You can set a different *THEME* override for each language in *I18N_SUBSITES*, e.g. by making a copy of a theme ``my_theme`` to ``my_theme_lang`` and then editing the templates in the new localized theme. This approach means you don't have to deal with gettext ``*.po`` files, but it is harder to maintain over time.
+- You use only one theme and localize the templates using the `jinja2.ext.i18n Jinja2 extension <http://jinja.pocoo.org/docs/templates/#i18n>`_. For a kickstart read this `guide <./localizing_using_jinja2.rst>`_.
+
+It may be convenient to add language buttons to your theme in addition to the translation links of articles and pages. These buttons could, for example, point to the *SITEURL* of each (sub-)site. For this reason the plugin adds these variables to the template context:
+
+main_lang
+  The language of the top-level site — the original *DEFAULT_LANG*
+main_siteurl
+  The *SITEURL* of the top-level site — the original *SITEURL*
+lang_siteurls
+  An ordered dictionary, mapping all used languages to their *SITEURL*. The ``main_lang`` is the first key with ``main_siteurl`` as the value. This dictionary is useful for implementing global language buttons that show the language of the currently viewed (sub-)site too.
+extra_siteurls
+  An ordered dictionary, subset of ``lang_siteurls``, the current *DEFAULT_LANG* of the rendered (sub-)site is not included, so for each (sub-)site ``set(extra_siteurls) == set(lang_siteurls) - set([DEFAULT_LANG])``. This dictionary is useful for implementing global language buttons that do not show the current language.
+
+If you don't like the default ordering of the ordered dictionaries, use a Jinja2 filter to alter the ordering.
+
+This short `howto <./implementing_language_buttons.rst>`_ shows two example implementations of language buttons.
+
+Usage notes
+===========
+- It is **mandatory** to specify *lang* metadata for each article and page as *DEFAULT_LANG* is later changed for each sub-site, so content without *lang* metadata woudl be rendered in every (sub-)site.
+- As with the original translations functionality, *slug* metadata is used to group translations. It is therefore often convenient to compensate for this by overriding the content URL (which defaults to slug) using the *URL* and *Save_as* metadata.
+
+Future plans
+============
+
+- add a test suite
+
+Development
+===========
+
+- A demo and test site is in the ``gh-pages`` branch and can be seen at http://smartass101.github.io/pelican-plugins/

+ 1 - 0
i18n_subsites/__init__.py

@@ -0,0 +1 @@
+from .i18n_subsites import *

+ 81 - 0
i18n_subsites/_regenerate_context_helpers.py

@@ -0,0 +1,81 @@
+
+import math
+import random
+from collections import defaultdict
+from operator import attrgetter, itemgetter
+
+
+def regenerate_context_articles(generator):
+    """Helper to regenerate context after modifying articles draft state
+
+    essentially just a copy from pelican.generators.ArticlesGenerator.generate_context
+    after process_translations up to signal sending
+
+    This has to be kept in sync untill a better solution is found
+    This is for Pelican version 3.3.0
+    """
+    # Simulate __init__ for fields that need it
+    generator.dates = {}
+    generator.tags = defaultdict(list)
+    generator.categories = defaultdict(list)
+    generator.authors = defaultdict(list)
+    
+
+    # Simulate ArticlesGenerator.generate_context 
+    for article in generator.articles:
+        # only main articles are listed in categories and tags
+        # not translations
+        generator.categories[article.category].append(article)
+        if hasattr(article, 'tags'):
+            for tag in article.tags:
+                generator.tags[tag].append(article)
+        # ignore blank authors as well as undefined
+        if hasattr(article, 'author') and article.author.name != '':
+            generator.authors[article.author].append(article)
+
+
+    # sort the articles by date
+    generator.articles.sort(key=attrgetter('date'), reverse=True)
+    generator.dates = list(generator.articles)
+    generator.dates.sort(key=attrgetter('date'),
+            reverse=generator.context['NEWEST_FIRST_ARCHIVES'])
+
+    # create tag cloud
+    tag_cloud = defaultdict(int)
+    for article in generator.articles:
+        for tag in getattr(article, 'tags', []):
+            tag_cloud[tag] += 1
+
+    tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
+    tag_cloud = tag_cloud[:generator.settings.get('TAG_CLOUD_MAX_ITEMS')]
+
+    tags = list(map(itemgetter(1), tag_cloud))
+    if tags:
+        max_count = max(tags)
+    steps = generator.settings.get('TAG_CLOUD_STEPS')
+
+    # calculate word sizes
+    generator.tag_cloud = [
+        (
+            tag,
+            int(math.floor(steps - (steps - 1) * math.log(count)
+                / (math.log(max_count)or 1)))
+        )
+        for tag, count in tag_cloud
+    ]
+    # put words in chaos
+    random.shuffle(generator.tag_cloud)
+
+    # and generate the output :)
+
+    # order the categories per name
+    generator.categories = list(generator.categories.items())
+    generator.categories.sort(
+            reverse=generator.settings['REVERSE_CATEGORY_ORDER'])
+
+    generator.authors = list(generator.authors.items())
+    generator.authors.sort()
+
+    generator._update_context(('articles', 'dates', 'tags', 'categories',
+                          'tag_cloud', 'authors', 'related_posts'))
+    

+ 189 - 0
i18n_subsites/i18n_subsites.py

@@ -0,0 +1,189 @@
+"""i18n_subsites plugin creates i18n-ized subsites of the default site"""
+
+
+
+import os
+import six
+import logging
+from itertools import chain
+from collections import defaultdict, OrderedDict
+
+import gettext
+
+from pelican import signals
+from pelican.contents import Page, Article
+from pelican.settings import configure_settings
+
+from ._regenerate_context_helpers import regenerate_context_articles
+
+
+
+# Global vars
+_main_site_generated = False
+_main_site_lang = "en"
+_main_siteurl = ''
+_lang_siteurls = None
+logger = logging.getLogger(__name__)
+
+
+
+def disable_lang_vars(pelican_obj):
+    """Set lang specific url and save_as vars to the non-lang defaults
+
+    e.g. ARTICLE_LANG_URL = ARTICLE_URL
+    They would conflict with this plugin otherwise
+    """
+    global _main_site_lang, _main_siteurl, _lang_siteurls
+    s = pelican_obj.settings
+    for content in ['ARTICLE', 'PAGE']:
+        for meta in ['_URL', '_SAVE_AS']:
+            s[content + '_LANG' + meta] = s[content + meta]
+    if not _main_site_generated:
+        _main_site_lang = s['DEFAULT_LANG']
+        _main_siteurl = s['SITEURL']
+        _lang_siteurls = [(lang, _main_siteurl + '/' + lang) for lang in s.get('I18N_SUBSITES', {}).keys()]
+        # To be able to use url for main site root when SITEURL == '' (e.g. when developing)
+        _lang_siteurls = [(_main_site_lang, ('/' if _main_siteurl == '' else _main_siteurl))] + _lang_siteurls
+        _lang_siteurls = OrderedDict(_lang_siteurls)
+        
+
+    
+def create_lang_subsites(pelican_obj):
+    """For each language create a subsite using the lang-specific config
+
+    for each generated lang append language subpath to SITEURL and OUTPUT_PATH
+    and set DEFAULT_LANG to the language code to change perception of what is translated
+    and set DELETE_OUTPUT_DIRECTORY to False to prevent deleting output from previous runs
+    Then generate the subsite using a PELICAN_CLASS instance and its run method.
+    """
+    global _main_site_generated
+    if _main_site_generated:      # make sure this is only called once
+        return
+    else:
+        _main_site_generated = True
+
+    orig_settings = pelican_obj.settings
+    for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items():
+        settings = orig_settings.copy()
+        settings.update(overrides)
+        settings['SITEURL'] = _lang_siteurls[lang]
+        settings['OUTPUT_PATH'] = os.path.join(orig_settings['OUTPUT_PATH'], lang, '')
+        settings['DEFAULT_LANG'] = lang   # to change what is perceived as translations
+        settings['DELETE_OUTPUT_DIRECTORY'] = False  # prevent deletion of previous runs
+        settings = configure_settings(settings)      # to set LOCALE, etc.
+
+        cls = settings['PELICAN_CLASS']
+        if isinstance(cls, six.string_types):
+            module, cls_name = cls.rsplit('.', 1)
+            module = __import__(module)
+            cls = getattr(module, cls_name)
+
+        pelican_obj = cls(settings)
+        logger.debug("Generating i18n subsite for lang '{}' using class '{}'".format(lang, str(cls)))
+        pelican_obj.run()
+    _main_site_generated = False          # for autoreload mode
+
+
+
+def move_translations_links(content_object):
+    """This function points translations links to the sub-sites
+
+    by prepending their location with the language code
+    or directs an original DEFAULT_LANG translation back to top level site
+    """
+    for translation in content_object.translations:
+        if translation.lang == _main_site_lang:
+        # cannot prepend, must take to top level
+            lang_prepend = '../'
+        else:
+            lang_prepend = translation.lang + '/'
+        translation.override_url =  lang_prepend + translation.url
+
+
+
+def update_generator_contents(generator, *args):
+    """Update the contents lists of a generator
+
+    Empty the (hidden_)translation attribute of article and pages generators
+    to prevent generating the translations as they will be generated in the lang sub-site
+    and point the content translations links to the sub-sites
+
+    Hide content without a translation for current DEFAULT_LANG
+    if HIDE_UNTRANSLATED_CONTENT is True
+    """
+    generator.translations = []
+    is_pages_gen = hasattr(generator, 'pages')
+    if is_pages_gen:
+        generator.hidden_translations = []
+        for page in chain(generator.pages, generator.hidden_pages):
+            move_translations_links(page)
+    else:                                    # is an article generator
+        for article in chain(generator.articles, generator.drafts):
+            move_translations_links(article)
+
+    if not generator.settings.get('HIDE_UNTRANSLATED_CONTENT', True):
+        return
+    contents = generator.pages if is_pages_gen else generator.articles
+    hidden_contents = generator.hidden_pages if is_pages_gen else generator.drafts
+    default_lang = generator.settings['DEFAULT_LANG']
+    for content_object in contents[:]:   # loop over copy for removing
+        if content_object.lang != default_lang:
+            if isinstance(content_object, Article):
+                content_object.status = 'draft'
+            elif isinstance(content_object, Page):
+                content_object.status = 'hidden'
+            contents.remove(content_object)
+            hidden_contents.append(content_object)
+    if not is_pages_gen: # regenerate categories, tags, etc. for articles
+        if hasattr(generator, '_generate_context_aggregate'):                  # if implemented
+            # Simulate __init__ for fields that need it
+            generator.dates = {}
+            generator.tags = defaultdict(list)
+            generator.categories = defaultdict(list)
+            generator.authors = defaultdict(list)
+            generator._generate_context_aggregate()
+        else:                             # fallback for Pelican 3.3.0
+            regenerate_context_articles(generator)
+
+
+
+def install_templates_translations(generator):
+    """Install gettext translations for current DEFAULT_LANG in the jinja2.Environment
+
+    if the 'jinja2.ext.i18n' jinja2 extension is enabled
+    adds some useful variables into the template context
+    """
+    generator.context['main_siteurl'] = _main_siteurl
+    generator.context['main_lang'] = _main_site_lang
+    generator.context['lang_siteurls'] = _lang_siteurls
+    current_def_lang = generator.settings['DEFAULT_LANG']
+    extra_siteurls = _lang_siteurls.copy()
+    extra_siteurls.pop(current_def_lang)
+    generator.context['extra_siteurls'] = extra_siteurls
+    
+    if 'jinja2.ext.i18n' not in generator.settings['JINJA_EXTENSIONS']:
+        return
+    domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages')
+    localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR')
+    if localedir is None:
+        localedir = os.path.join(generator.theme, 'translations')
+    if current_def_lang == generator.settings.get('I18N_TEMPLATES_LANG', _main_site_lang):
+        translations = gettext.NullTranslations()
+    else:
+        languages = [current_def_lang]
+        try:
+            translations = gettext.translation(domain, localedir, languages)
+        except (IOError, OSError):
+            logger.error("Cannot find translations for language '{}' in '{}' with domain '{}'. Installing NullTranslations.".format(languages[0], localedir, domain))
+            translations = gettext.NullTranslations()
+    newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True)
+    generator.env.install_gettext_translations(translations, newstyle)    
+
+
+
+def register():
+    signals.initialized.connect(disable_lang_vars)
+    signals.generator_init.connect(install_templates_translations)
+    signals.article_generator_finalized.connect(update_generator_contents)
+    signals.page_generator_finalized.connect(update_generator_contents)
+    signals.finalized.connect(create_lang_subsites)

+ 113 - 0
i18n_subsites/implementing_language_buttons.rst

@@ -0,0 +1,113 @@
+-----------------------------
+Implementing language buttons
+-----------------------------
+
+Each article with translations has translations links, but that's the only way to switch between language subsites.
+
+For this reason it is convenient to add language buttons to the top menu bar to make it simple to switch between the language subsites on all pages.
+
+Example designs
+---------------
+
+Language buttons showing other available languages
+..................................................
+
+The ``extra_siteurls`` dictionary is a mapping of all other (not the *DEFAULT_LANG* of the current (sub-)site) languages to the *SITEURL* of the respective (sub-)sites
+
+.. code-block:: jinja
+
+   <!-- SNIP -->
+   <nav><ul>
+   {% if extra_siteurls %}
+   {% for lang, url in extra_siteurls.items() %}
+   <li><a href="{{ url }}">{{ lang }}</a></li>
+   {% endfor %}
+   <!-- separator -->
+   <li style="background-color: white; padding: 5px;">&nbsp</li>
+   {% endif %}
+   {% for title, link in MENUITEMS %}
+   <!-- SNIP -->
+
+Language buttons showing all available languages, current is active
+..................................................................
+
+The ``extra_siteurls`` dictionary is a mapping of all languages to the *SITEURL* of the respective (sub-)sites. This template sets the language of the current (sub-)site as active.
+
+.. code-block:: jinja
+
+   <!-- SNIP -->
+   <nav><ul>
+   {% if lang_siteurls %}
+   {% for lang, url in lang_siteurls.items() %}
+   <li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ url }}">{{ lang }}</a></li>
+   {% endfor %}
+   <!-- separator -->
+   <li style="background-color: white; padding: 5px;">&nbsp</li>
+   {% endif %}
+   {% for title, link in MENUITEMS %}
+   <!-- SNIP -->
+
+
+Tips and tricks
+---------------
+
+Showing more than language codes
+................................
+
+If you want the language buttons to show e.g. the names of the languages or flags [#flags]_, not just the language codes, you can use a jinja filter to translate the language codes
+
+
+.. code-block:: python
+
+   languages_lookup = {
+		'en': 'English',
+		'cz': 'Čeština',
+		}
+
+   def lookup_lang_name(lang_code):
+       return languages_lookup[lang_code]
+
+   JINJA_FILTERS = {
+		...
+		'lookup_lang_name': lookup_lang_name,
+		}
+
+And then the link content becomes
+
+.. code-block:: jinja
+
+   <!-- SNIP -->
+   {% for lang, siteurl in lang_siteurls.items() %}
+   <li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ siteurl }}">{{ lang | lookup_lang_name }}</a></li>
+   {% endfor %}
+   <!-- SNIP -->
+
+
+Changing the order of language buttons
+......................................
+
+Because ``lang_siteurls`` and ``extra_siteurls`` are instances of ``OrderedDict`` with ``main_lang`` being always the first key, you can change the order through a jinja filter.
+
+.. code-block:: python
+
+   def my_ordered_items(ordered_dict):
+       items = list(ordered_dict.items())
+       # swap first and last using tuple unpacking
+       items[0], items[-1] = items[-1], items[0]
+       return items
+
+   JINJA_FILTERS = {
+		...
+		'my_ordered_items': my_ordered_items,
+		}
+
+And then the ``for`` loop line in the template becomes
+
+.. code-block:: jinja
+
+   <!-- SNIP -->
+   {% for lang, siteurl in lang_siteurls | my_ordered_items %}
+   <!-- SNIP -->
+
+
+.. [#flags] Although it may look nice, `w3 discourages it <http://www.w3.org/TR/i18n-html-tech-lang/#ri20040808.173208643>`_.

+ 142 - 0
i18n_subsites/localizing_using_jinja2.rst

@@ -0,0 +1,142 @@
+-----------------------------
+Localizing themes with Jinja2
+-----------------------------
+
+1. Localize templates
+---------------------
+
+To enable the |ext| extension in your templates, you must add it to 
+*JINJA_EXTENSIONS* in your Pelican configuration
+
+.. code-block:: python
+
+  JINJA_EXTENSIONS = ['jinja2.ext.i18n', ...]
+
+Then follow the `Jinja2 templating documentation for the I18N plugin <http://jinja.pocoo.org/docs/templates/#i18n>`_ to make your templates localizable. This usually means surrounding strings with the ``{% trans %}`` directive or using ``gettext()`` in expressions
+
+.. code-block:: jinja
+
+    {% trans %}translatable content{% endtrans %}
+    {{ gettext('a translatable string') }}
+
+For pluralization support, etc. consult the documentation
+
+To enable `newstyle gettext calls <http://jinja.pocoo.org/docs/extensions/#newstyle-gettext>`_ the *I18N_GETTEXT_NEWSTYLE* config variable must be set to ``True`` (default).
+
+.. |ext| replace:: ``jinja2.ext.i18n``
+
+2. Specify translations location
+--------------------------------
+
+The |ext| extension uses the `Python gettext library <http://docs.python.org/library/gettext.html>`_ for translating strings.
+
+In your Pelican config you can give the path in which to look for translations in the *I18N_GETTEXT_LOCALEDIR* variable. If not given, it is assumed to be the ``translations`` subfolder in the top folder of the theme specified by *THEME*.
+
+The domain of the translations (the name of each translation file is ``domain.mo``) is controlled by the *I18N_GETTEXT_DOMAIN* config variable (defaults to ``messages``).
+
+Example
+.......
+
+With the following in your Pelican settings file
+
+.. code-block:: python
+
+  I18N_GETTEXT_LOCALEDIR = 'some/path/'
+  I18N_GETTEXT_DOMAIN = 'my_domain'
+
+… the translation for language 'cz' will be expected to be in ``some/path/cz/LC_MESSAGES/my_domain.mo``
+
+
+3. Extract translatable strings and translate them
+--------------------------------------------------
+
+There are many ways to extract translatable strings and create ``gettext`` compatible translations. You can create the ``*.po`` and ``*.mo`` message catalog files yourself, or you can use some helper tool as described in `the Python gettext library tutorial <http://docs.python.org/library/gettext.html#internationalizing-your-programs-and-modules>`_.
+
+You of course don't need to provide a translation for the language in which the templates are written which is assumed to be the original *DEFAULT_LANG*. This can be overridden in the *I18N_TEMPLATES_LANG* variable.
+
+Recommended tool: babel
+.......................
+
+`Babel <http://babel.pocoo.org/>`_ makes it easy to extract translatable strings from the localized Jinja2 templates and assists with creating translations as documented in this `Jinja2-Babel tutorial <http://pythonhosted.org/Flask-Babel/#translating-applications>`_ [#flask]_ on which the following is based.
+
+1. Add babel mapping
+~~~~~~~~~~~~~~~~~~~~
+
+Let's assume that you are localizing a theme in ``themes/my_theme/`` and that you use the default settings, i.e. the default domain ``messages`` and will put the translations in the ``translations`` subdirectory of the theme directory as ``themes/my_theme/translations/``.
+
+It is up to you where to store babel mappings and translation files templates (``*.pot``), but a convenient place is to put them in ``themes/my_theme/`` and work in that directory. From now on let's assume that it will be our current working directory (CWD).
+
+To tell babel to extract translatable strings from the templates create a mapping file ``babel.cfg`` with the following line
+
+.. code-block:: cfg
+
+    [jinja2: ./templates/**.html]
+
+
+2. Extract translatable strings from templates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Run the following command to create a ``messages.pot`` message catalog template file from extracted translatable strings
+
+.. code-block:: bash
+
+    pybabel extract --mapping babel.cfg --output messages.pot ./
+
+
+3. Initialize message catalogs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you want to translate the template to language ``lang``, run the following command to create a message catalog
+``translations/lang/LC_MESSAGES/messages.po`` using the template ``messages.pot``
+
+.. code-block:: bash
+
+    pybabel init --input-file messages.pot --output-dir translations/ --locale lang --domain messages
+
+babel expects ``lang`` to be a valid locale identifier, so if e.g. you are translating for language ``cz`` but the corresponding locale is ``cs``, you have to use the locale identifier. Nevertheless, the gettext infrastructure should later correctly find the locale for a given language.
+
+4. Fill the message catalogs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The message catalog files format is quite intuitive, it is fully documented in the `GNU gettext manual <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_. Essentially, you fill in the ``msgstr`` strings
+
+
+.. code-block:: po
+
+    msgid "just a simple string"
+    msgstr "jenom jednoduchý řetězec"
+
+    msgid ""
+    "some multiline string"
+    "looks like this"
+    msgstr ""
+    "nějaký více řádkový řetězec"
+    "vypadá takto"
+
+You might also want to remove ``#,fuzzy`` flags once the translation is complete and reviewed to show that it can be compiled.
+
+5. Compile the message catalogs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The message catalogs must be compiled into binary format using this command
+
+.. code-block:: bash
+
+    pybabel compile --directory translations/ --domain messages
+
+This command might complain about "fuzzy" translations, which means you should review the translations and once done, remove the fuzzy flag line.
+
+(6.) Update the catalogs when templates change
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you add any translatable patterns into your templates, you have to update your message catalogs too.
+First you extract a new message catalog template as described in the 2. step. Then you run the following command [#pybabel_error]_
+
+.. code-block:: bash
+
+   pybabel update --input-file messages.pot --output-dir translations/ --domain messages
+
+This will merge the new patterns with the old ones. Once you review and fill them, you have to recompile them as described in the 5. step.
+
+.. [#flask] Although the tutorial is focused on Flask-based web applications, the linked translation tutorial is not Flask-specific.
+.. [#pybabel_error] If you get an error ``TypeError: must be str, not bytes`` with Python 3.3, it is likely you are suffering from this `bug <https://github.com/mitsuhiko/flask-babel/issues/43>`_. Until the fix is released, you can use babel with Python 2.7.

+ 72 - 2
neighbors/Readme.rst

@@ -2,7 +2,10 @@ Neighbor Articles Plugin for Pelican
 ====================================
 
 This plugin adds ``next_article`` (newer) and ``prev_article`` (older) 
-variables to the article's context
+variables to the article's context.
+
+Also adds ``next_article_in_category`` and ``prev_article_in_category``.
+
 
 Usage
 -----
@@ -24,4 +27,71 @@ Usage
             </a>
         </li>
     {% endif %}
-    </ul>
+   </ul>    
+   <ul>
+    {% if article.prev_article_in_category %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.prev_article_in_category.url}}">
+                {{ article.prev_article_in_category.title }}
+            </a>
+        </li>
+    {% endif %}
+    {% if article.next_article %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.next_article_in_category.url}}">
+                {{ article.next_article_in_category.title }}
+            </a>
+        </li>
+    {% endif %}
+    </ul> 
+
+Usage with the Subcategory plugin
+---------------------------------
+
+If you want to get the neigbors within a subcategory it's a little different.
+Since an article can belong to more than one subcategory, subcategories are
+stored in a list. If you have an article with subcategories like 
+
+``Category/Foo/Bar``
+
+it will belong to both subcategory Foo, and Foo/Bar. Subcategory neighbors are
+added to an article as ``next_article_in_subcategory#`` and 
+``prev_article_in_subcategory#`` where ``#`` is the level of subcategory. So using
+the example from above, subcategory1 will be Foo, and subcategory2 Foo/Bar. 
+Therefor the usage with subcategories is:
+
+.. code-block:: html+jinja
+
+    <ul>
+    {% if article.prev_article_subcategory1 %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.prev_article_in_subcategory1.url}}">
+                {{ article.prev_article_in_subcategory1.title }}
+            </a>
+        </li>
+    {% endif %}
+    {% if article.next_article %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.next_article_subcategory1.url}}">
+                {{ article.next_article_subcategory1.title }}
+            </a>
+        </li>
+    {% endif %}
+   </ul>    
+   <ul>
+    {% if article.prev_article_in_subcategory2 %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.prev_article_in_subcategory2.url}}">
+                {{ article.prev_article_in_subcategory2.title }}
+            </a>
+        </li>
+    {% endif %}
+    {% if article.next_article %}
+        <li>
+            <a href="{{ SITEURL }}/{{ article.next_article_in_subcategory2.url}}">
+                {{ article.next_article_in_subcategory2.title }}
+            </a>
+        </li>
+    {% endif %}
+    </ul> 
+

+ 25 - 7
neighbors/neighbors.py

@@ -6,7 +6,6 @@ Neighbor Articles Plugin for Pelican
 This plugin adds ``next_article`` (newer) and ``prev_article`` (older) 
 variables to the article's context
 """
-
 from pelican import signals
 
 def iter3(seq):
@@ -26,14 +25,33 @@ def get_translation(article, prefered_language):
             return translation
     return article
 
-def neighbors(generator):
-    for nxt, cur, prv in iter3(generator.articles):
-        cur.next_article = nxt
-        cur.prev_article = prv
+def set_neighbors(articles, next_name, prev_name):
+    for nxt, cur, prv in iter3(articles):
+        exec("cur.{} = nxt".format(next_name))
+        exec("cur.{} = prv".format(prev_name))
 
         for translation in cur.translations:
-            translation.next_article = get_translation(nxt, translation.lang)
-            translation.prev_article = get_translation(prv, translation.lang)
+            exec(
+            "translation.{} = get_translation(nxt, translation.lang)".format(
+                next_name))
+            exec(
+            "translation.{} = get_translation(prv, translation.lang)".format(
+                prev_name))
+      
+def neighbors(generator):
+    set_neighbors(generator.articles, 'next_article', 'prev_article')
+    
+    for category, articles in generator.categories:
+        articles.sort(key=(lambda x: x.date), reverse=(True))
+        set_neighbors(
+            articles, 'next_article_in_category', 'prev_article_in_category')
+    
+    for subcategory, articles in generator.subcategories:
+        articles.sort(key=(lambda x: x.date), reverse=(True))
+        index = subcategory.name.count('/')
+        next_name = 'next_article_in_subcategory{}'.format(index)
+        prev_name = 'prev_article_in_subcategory{}'.format(index)
+        set_neighbors(articles, next_name, prev_name)
 
 def register():
     signals.article_generator_finalized.connect(neighbors)

+ 26 - 0
read_more_link/Readme.md

@@ -0,0 +1,26 @@
+Read More Link
+===
+
+**Author**: Vuong Nguyen (http://vuongnguyen.com)
+
+This plugin inserts an inline "read more" or "continue" link into the last html element of the object summary.
+
+For more information, please visit: http://vuongnguyen.com/creating-inline-read-more-link-python-pelican-lxml.html
+
+Requirements
+---
+
+    lxml - for parsing html elements
+
+Settings
+---
+    # This settings indicates that you want to create summary at a certain length
+    SUMMARY_MAX_LENGTH = 50
+
+    # This indicates what goes inside the read more link
+    READ_MORE_LINK = None (ex: '<span>continue</span>')
+
+    # This is the format of the read more link
+    READ_MORE_LINK_FORMAT = '<a class="read-more" href="/{url}">{text}</a>'
+
+

+ 1 - 0
read_more_link/__init__.py

@@ -0,0 +1 @@
+from .read_more_link import *

+ 71 - 0
read_more_link/read_more_link.py

@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+"""
+Read More Link
+===========================
+
+This plugin inserts an inline "read more" or "continue" link into the last html element of the object summary.
+
+For more information, please visit: http://vuongnguyen.com/creating-inline-read-more-link-python-pelican-lxml.html
+
+"""
+
+from pelican import signals, contents
+from pelican.utils import truncate_html_words
+
+try:
+    from lxml.html import fragment_fromstring, fragments_fromstring, tostring
+    from lxml.etree import ParserError
+except ImportError:
+    raise Exception("Unable to find lxml. To use READ_MORE_LINK, you need lxml")
+
+
+def insert_into_last_element(html, element):
+    """
+    function to insert an html element into another html fragment
+    example:
+        html = '<p>paragraph1</p><p>paragraph2...</p>'
+        element = '<a href="/read-more/">read more</a>'
+        ---> '<p>paragraph1</p><p>paragraph2...<a href="/read-more/">read more</a></p>'
+    """
+    try:
+        item = fragment_fromstring(element)
+    except ParserError, TypeError:
+        item = fragment_fromstring('<span></span>')
+
+    try:
+        doc = fragments_fromstring(html)
+        doc[-1].append(item)
+
+        return ''.join(tostring(e) for e in doc)
+    except ParserError, TypeError:
+        return ''
+
+def insert_read_more_link(instance):
+    """
+    Insert an inline "read more" link into the last element of the summary
+    :param instance:
+    :return:
+    """
+
+    # only deals with Article type
+    if type(instance) != contents.Article: return
+
+
+    SUMMARY_MAX_LENGTH = instance.settings.get('SUMMARY_MAX_LENGTH')
+    READ_MORE_LINK = instance.settings.get('READ_MORE_LINK', None)
+    READ_MORE_LINK_FORMAT = instance.settings.get('READ_MORE_LINK_FORMAT',
+                                                  '<a class="read-more" href="/{url}">{text}</a>')
+
+    if not (SUMMARY_MAX_LENGTH and READ_MORE_LINK and READ_MORE_LINK_FORMAT): return
+
+    if hasattr(instance, '_summary') and instance._summary:
+        summary = instance._summary
+    else:
+        summary = truncate_html_words(instance.content, SUMMARY_MAX_LENGTH)
+
+    if summary<instance.content:
+        read_more_link = READ_MORE_LINK_FORMAT.format(url=instance.url, text=READ_MORE_LINK)
+        instance._summary = insert_into_last_element(summary, read_more_link)
+
+def register():
+    signals.content_object_init.connect(insert_read_more_link)

+ 1 - 0
read_more_link/requirements.txt

@@ -0,0 +1 @@
+lxml>=3.2.1

+ 37 - 25
subcategory/README.md

@@ -1,14 +1,11 @@
 #Subcategory Plugin#
 
-Adds support for subcategories in addition to article categories.
+This plugin adds support for subcategories in addition to article categories.
 
-Subcategories are heirachial. Each subcategory has a parent, which is either a
-regular category or another subcategory. Subcategories with the same name but 
-different parents are not the same. Their articles won't be grouped together 
-under that name.
-
-Feeds can be generated for each subcategory just like categories and tags.
+Subcategories are hierarchical. Each subcategory has a parent, which is either a
+regular category or another subcategory.
 
+Feeds can be generated for each subcategory, just like categories and tags.
 
 ##Usage##
 
@@ -17,48 +14,63 @@ category metadata using a `/` like this:
 
     Category: Regular Category/Sub-Category/Sub-Sub-category
 
-then create a `subcategory.html` template in your theme similar to the 
-`category.html` or `tag.html`
+Then create a `subcategory.html` template in your theme, similar to the
+`category.html` or `tag.html` templates.
 
-In your templates `article.category` continues to act the same way. Your 
-subcategories are stored in a list `aricles.subcategories`. To create a 
-breadcrumb style navigation you might try something like this:
+In your templates, `article.category` continues to act the same way. Your
+subcategories are stored in the `articles.subcategories` list. To create
+breadcrumb-style navigation you might try something like this:
 
     <nav class="breadcrumb">
     <ol>
         <li>
-            <a href="{{ SITEURL }}/{{ arcticle.categor.url }}">{{ article.category}}</a>
+            <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category}}</a>
         </li>
     {% for subcategory in article.subcategories %}
         <li>
-            <a href="{{ SITEURL }}/{{ category.url }}>{{ subcategory }}</a>
+            <a href="{{ SITEURL }}/{{ subcategory.url }}>{{ subcategory.shortname }}</a>
         </li>
     {% endfor %}
     </ol>
     </nav>
- 
+
+##Subcategory Names##
+
+Each subcategory's full name is a `/`-separated list of it parents and itself.
+This is necessary to keep each subcategory unique. It means you can have
+`Category 1/Foo` and `Category 2/Foo` and they won't interfere with each other.
+Each subcategory has an attribute `shortname` which is just the name without
+its parents associated. For example if you had…
+
+    Category/Sub Category1/Sub Category2
+
+… the full name for Sub Category2 would be `Category/Sub Category1/Sub Category2` and
+the "short name" would be `Sub Category2`.
+
+If you need to use the slug, it is generated from the short name — not the full
+name.
 
 ##Settings##
 
-Consistent with the default settings for Tags and Categories, the default 
-settings for subcategoris are:
-    
+Consistent with the default settings for Tags and Categories, the default
+settings for subcategories are:
+
     'SUBCATEGORY_SAVE_AS' = os.path.join('subcategory', '{savepath}.html')
     'SUBCATEGORY_URL' = 'subcategory/(fullurl).html'
 
 `savepath` and `fullurl` are generated recursively, using slugs. So the full
-url would be:
-    
+URL would be:
+
     category-slug/sub-category-slug/sub-sub-category-slug
 
-with `savepath` being similar but joined using `os.path.join`
+with `savepath` being similar but joined using `os.path.join`.
 
-Similarily you can save a subcategory feeds by adding one of the following 
-to your pelicanconf file
+Similarly, you can save subcategory feeds by adding one of the following
+to your Pelican configuration file:
 
     SUBCATEGORY_FEED_ATOM = 'feeds/%s.atom.xml'
     SUBCATEGORY_FEED_RSS = 'feeds/%s.rss.xml'
 
-and this will create a feed with `fullurl` of the subcategory. Eg.
-    
+and this will create a feed with `fullurl` of the subcategory. For example:
+
     feeds/category/subcategory.atom.xml

+ 41 - 16
subcategory/subcategory.py

@@ -6,64 +6,89 @@ Adds support for subcategories on pelican articles
 """
 import os
 from collections import defaultdict
-from pelican import signals
-from pelican.urlwrappers import URLWrapper, Category
 from operator import attrgetter
 from functools import partial
 
+from pelican import signals
+from pelican.urlwrappers import URLWrapper, Category
+from pelican.utils import (slugify, python_2_unicode_compatible)
+
 from six import text_type
 
 class SubCategory(URLWrapper):
-    def __init__(self, name, parent, *args, **kwargs):
-        super(SubCategory, self).__init__(name, *args, **kwargs)
+    def __init__(self, name, parent, settings):
+        super(SubCategory, self).__init__(name, settings)
         self.parent = parent
+        self.shortname = name.split('/')
+        self.shortname = self.shortname.pop()
+        self.slug = slugify(self.shortname, settings.get('SLUG_SUBSTITUIONS', ()))
         if isinstance(self.parent, SubCategory):
             self.savepath = os.path.join(self.parent.savepath, self.slug)
             self.fullurl = '{}/{}'.format(self.parent.fullurl, self.slug)
         else: #parent is a category
             self.savepath = os.path.join(self.parent.slug, self.slug)
             self.fullurl = '{}/{}'.format(self.parent.slug, self.slug)
-
+        
     def as_dict(self):
         d = self.__dict__
-        d['name'] = self.name
+        d['shortname'] = self.shortname
         d['savepath'] = self.savepath
         d['fullurl'] = self.fullurl
         d['parent'] = self.parent
         return d
 
+    def __hash__(self):
+        return hash(self.fullurl)
+
+    def _key(self):
+        return self.fullurl
+
 def get_subcategories(generator, metadata):
     if 'SUBCATEGORY_SAVE_AS' not in generator.settings:
         generator.settings['SUBCATEGORY_SAVE_AS'] = os.path.join( 
                 'subcategory', '{savepath}.html')
     if 'SUBCATEGORY_URL' not in generator.settings:
         generator.settings['SUBCATEGORY_URL'] = 'subcategory/{fullurl}.html'
+    
     category_list = text_type(metadata.get('category')).split('/')
     category = (category_list.pop(0)).strip()
     category = Category(category, generator.settings)
     metadata['category'] = category
     #generate a list of subcategories with their parents
     sub_list = []
-    parent = category
+    parent = category.name
     for subcategory in category_list:
         subcategory.strip()
-        subcategory = SubCategory(subcategory, parent, generator.settings)
+        subcategory = parent + '/' + subcategory
         sub_list.append(subcategory)
         parent = subcategory
     metadata['subcategories'] = sub_list
 
-def organize_subcategories(generator):
-    generator.subcategories = defaultdict(list)
+def create_subcategories(generator):
+    generator.subcategories = []
     for article in generator.articles:
-        subcategories = article.metadata.get('subcategories')
-        for cat in subcategories:
-            generator.subcategories[cat].append(article)
+        parent = article.category
+        actual_subcategories = []
+        for subcategory in article.subcategories:
+            #following line returns a list of items, tuples in this case
+            sub_cat = [item for item in generator.subcategories 
+                    if item[0].name == subcategory]
+            if sub_cat:
+                sub_cat[0][1].append(article)
+                parent = sub_cat[0][0]
+                actual_subcategories.append(parent)
+            else:
+                new_sub = SubCategory(subcategory, parent, generator.settings)
+                generator.subcategories.append((new_sub, [article,]))
+                parent = new_sub
+                actual_subcategories.append(parent)
+        article.subcategories = actual_subcategories
 
 def generate_subcategories(generator, writer):
     write = partial(writer.write_file,
             relative_urls=generator.settings['RELATIVE_URLS'])
     subcategory_template = generator.get_template('subcategory')
-    for subcat, articles in generator.subcategories.items():
+    for subcat, articles in generator.subcategories:
         articles.sort(key=attrgetter('date'), reverse=True)
         dates = [article for article in generator.dates if article in articles]
         write(subcat.save_as, subcategory_template, generator.context, 
@@ -72,7 +97,7 @@ def generate_subcategories(generator, writer):
                 page_name=subcat.page_name, all_articles=generator.articles)
 
 def generate_subcategory_feeds(generator, writer):
-    for subcat, articles in generator.subcategories.items():
+    for subcat, articles in generator.subcategories:
         articles.sort(key=attrgetter('date'), reverse=True)
         if generator.settings.get('SUBCATEGORY_FEED_ATOM'):
             writer.write_feed(articles, generator.context,
@@ -89,5 +114,5 @@ def generate(generator, writer):
 
 def register():
     signals.article_generator_context.connect(get_subcategories)
-    signals.article_generator_finalized.connect(organize_subcategories)
+    signals.article_generator_finalized.connect(create_subcategories)
     signals.article_writer_finalized.connect(generate)

+ 2 - 1
thumbnailer/Readme.md

@@ -16,7 +16,8 @@ Configuration
 * IMAGE_PATH is the path to the image directory.  It should reside under content, and defaults to "pictures"
 * THUMBNAIL_DIR is the path to the output sub directory where the thumbnails are generated
 * THUMBNAIL_SIZES is a dictionary mapping name of size to size specifications.
-  The generated filename will be originalname_thumbnailname.ext
+  The generated filename will be originalname_thumbnailname.ext unless THUMBNAIL_KEEP_NAME is set.
+* THUMBNAIL_KEEP_NAME is a boolean which if set puts the file with the original name in a thumbnailname folder.
 
 Sizes can be specified using any of the following formats:
 

+ 9 - 3
thumbnailer/thumbnailer.py

@@ -92,14 +92,17 @@ class _resizer(object):
         new_filename = "{0}{1}".format(basename, ext)
         return new_filename
 
-    def resize_file_to(self, in_path, out_path):
+    def resize_file_to(self, in_path, out_path, keep_filename=False):
         """ Given a filename, resize and save the image per the specification into out_path
 
         :param in_path: path to image file to save.  Must be supposed by PIL
         :param out_path: path to the directory root for the outputted thumbnails to be stored
         :return: None
         """
-        filename = path.join(out_path, self.get_thumbnail_name(in_path))
+        if keep_filename:
+            filename = path.join(out_path, path.basename(in_path))
+        else:
+            filename = path.join(out_path, self.get_thumbnail_name(in_path))
         if not path.exists(out_path):
             os.makedirs(out_path)
         if not path.exists(filename):
@@ -131,7 +134,10 @@ def resize_thumbnails(pelican):
             for name, resizer in resizers.items():
                 in_filename = path.join(dirpath, filename)
                 logger.debug("Processing thumbnail {0}=>{1}".format(filename, name))
-                resizer.resize_file_to(in_filename, out_path)
+                if pelican.settings.get('THUMBNAIL_KEEP_NAME', False):
+                    resizer.resize_file_to(in_filename, path.join(out_path, name), True)
+                else:
+                    resizer.resize_file_to(in_filename, out_path)
 
 
 def _image_path(pelican):