Merge branch 'master' into collapse-code
This commit is contained in:
34
clean_summary/README.md
Normal file
34
clean_summary/README.md
Normal 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', ... ]
|
||||
1
clean_summary/__init__.py
Normal file
1
clean_summary/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .clean_summary import *
|
||||
38
clean_summary/clean_summary.py
Normal file
38
clean_summary/clean_summary.py
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
disqus_static/__init__py
Normal file
@@ -0,0 +1 @@
|
||||
from .disqus_static import *
|
||||
73
i18n_subsites/README.rst
Normal file
73
i18n_subsites/README.rst
Normal 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/
|
||||
1
i18n_subsites/__init__.py
Normal file
1
i18n_subsites/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .i18n_subsites import *
|
||||
81
i18n_subsites/_regenerate_context_helpers.py
Normal file
81
i18n_subsites/_regenerate_context_helpers.py
Normal 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'))
|
||||
|
||||
189
i18n_subsites/i18n_subsites.py
Normal file
189
i18n_subsites/i18n_subsites.py
Normal 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)
|
||||
113
i18n_subsites/implementing_language_buttons.rst
Normal file
113
i18n_subsites/implementing_language_buttons.rst
Normal 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;"> </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;"> </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
i18n_subsites/localizing_using_jinja2.rst
Normal file
142
i18n_subsites/localizing_using_jinja2.rst
Normal 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.
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
26
read_more_link/Readme.md
Normal 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>'
|
||||
|
||||
|
||||
1
read_more_link/__init__.py
Normal file
1
read_more_link/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .read_more_link import *
|
||||
71
read_more_link/read_more_link.py
Normal file
71
read_more_link/read_more_link.py
Normal 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)
|
||||
1
read_more_link/requirements.txt
Normal file
1
read_more_link/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
lxml>=3.2.1
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user