Merge branch 'master' into collapse-code

This commit is contained in:
Jörg Dietrich
2014-02-07 23:19:01 +01:00
23 changed files with 976 additions and 79 deletions

34
clean_summary/README.md Normal file
View File

@@ -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', ... ]

View File

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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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
disqus_static/__init__py Normal file
View File

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

73
i18n_subsites/README.rst Normal file
View File

@@ -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/

View File

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

View File

@@ -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'))

View File

@@ -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)

View File

@@ -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>`_.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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
read_more_link/Readme.md Normal file
View File

@@ -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>'

View File

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

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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):