From 943e590c24892ce6fac4fdfd4bc4d581c216064d Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Wed, 4 Dec 2013 23:21:19 -0500 Subject: [PATCH 01/20] Read more link plugin -- initial commit --- read_more_link/Readme.md | 27 ++++++++++++ read_more_link/__init__.py | 1 + read_more_link/read_more_link.py | 71 ++++++++++++++++++++++++++++++++ read_more_link/requirements.txt | 1 + 4 files changed, 100 insertions(+) create mode 100644 read_more_link/Readme.md create mode 100644 read_more_link/__init__.py create mode 100644 read_more_link/read_more_link.py create mode 100644 read_more_link/requirements.txt diff --git a/read_more_link/Readme.md b/read_more_link/Readme.md new file mode 100644 index 0000000..7a8cfee --- /dev/null +++ b/read_more_link/Readme.md @@ -0,0 +1,27 @@ +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 +--- + :::python + # 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: 'continue') + + # This is the format of the read more link + READ_MORE_LINK_FORMAT = '{text}' + + diff --git a/read_more_link/__init__.py b/read_more_link/__init__.py new file mode 100644 index 0000000..6b3673b --- /dev/null +++ b/read_more_link/__init__.py @@ -0,0 +1 @@ +from .read_more_link import * \ No newline at end of file diff --git a/read_more_link/read_more_link.py b/read_more_link/read_more_link.py new file mode 100644 index 0000000..bf6f094 --- /dev/null +++ b/read_more_link/read_more_link.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +Read More Link +=========================== + +This plugin inserts an inline "read more" or "continue" link into the last html element of the object summary. + +For more information, please visit: http://vuongnguyen.com/creating-inline-read-more-link-python-pelican-lxml.html + +""" + +from pelican import signals, contents +from pelican.utils import truncate_html_words + +try: + from lxml.html import fragment_fromstring, fragments_fromstring, tostring + from lxml.etree import ParserError +except ImportError: + raise Exception("Unable to find lxml. To use READ_MORE_LINK, you need lxml") + + +def insert_into_last_element(html, element): + """ + function to insert an html element into another html fragment + example: + html = '

paragraph1

paragraph2...

' + element = 'read more' + ---> '

paragraph1

paragraph2...read more

' + """ + try: + item = fragment_fromstring(element) + except ParserError, TypeError: + item = fragment_fromstring('') + + 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', + '{text}') + + 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=3.2.1 \ No newline at end of file From a5269f599aa9899b1cafa2cc1f4f9bfab680464d Mon Sep 17 00:00:00 2001 From: Alex Nordlund Date: Sun, 8 Dec 2013 23:51:29 +0000 Subject: [PATCH 02/20] Adds THUMBNAIL_KEEP_NAME which if set puts the thumbnail in a folder named like the key in THUMBNAIL_SIZES. --- thumbnailer/Readme.md | 3 ++- thumbnailer/thumbnailer.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/thumbnailer/Readme.md b/thumbnailer/Readme.md index 3c0a6a2..8e6d447 100644 --- a/thumbnailer/Readme.md +++ b/thumbnailer/Readme.md @@ -16,7 +16,8 @@ Configuration * IMAGE_PATH is the path to the image directory. It should reside under content, and defaults to "pictures" * THUMBNAIL_DIR is the path to the output sub directory where the thumbnails are generated * THUMBNAIL_SIZES is a dictionary mapping name of size to size specifications. - The generated filename will be originalname_thumbnailname.ext + The generated filename will be originalname_thumbnailname.ext unless THUMBNAIL_KEEP_NAME is set. +* THUMBNAIL_KEEP_NAME is a boolean which if set puts the file with the original name in a thumbnailname folder. Sizes can be specified using any of the following formats: diff --git a/thumbnailer/thumbnailer.py b/thumbnailer/thumbnailer.py index 76ab942..1cf91de 100644 --- a/thumbnailer/thumbnailer.py +++ b/thumbnailer/thumbnailer.py @@ -92,14 +92,17 @@ class _resizer(object): new_filename = "{0}{1}".format(basename, ext) return new_filename - def resize_file_to(self, in_path, out_path): + def resize_file_to(self, in_path, out_path, keep_filename=False): """ Given a filename, resize and save the image per the specification into out_path :param in_path: path to image file to save. Must be supposed by PIL :param out_path: path to the directory root for the outputted thumbnails to be stored :return: None """ - filename = path.join(out_path, self.get_thumbnail_name(in_path)) + if keep_filename: + filename = path.join(out_path, path.basename(in_path)) + else: + filename = path.join(out_path, self.get_thumbnail_name(in_path)) if not path.exists(out_path): os.makedirs(out_path) if not path.exists(filename): @@ -131,7 +134,10 @@ def resize_thumbnails(pelican): for name, resizer in resizers.items(): in_filename = path.join(dirpath, filename) logger.debug("Processing thumbnail {0}=>{1}".format(filename, name)) - resizer.resize_file_to(in_filename, out_path) + if pelican.settings.get('THUMBNAIL_KEEP_NAME', False): + resizer.resize_file_to(in_filename, path.join(out_path, name), True) + else: + resizer.resize_file_to(in_filename, out_path) def _image_path(pelican): From 1cf85f7a06a5b718779c3d76df10e582c3f87a30 Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Mon, 16 Dec 2013 17:01:57 -0500 Subject: [PATCH 03/20] Update Readme.md --- read_more_link/Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/read_more_link/Readme.md b/read_more_link/Readme.md index 7a8cfee..42074ba 100644 --- a/read_more_link/Readme.md +++ b/read_more_link/Readme.md @@ -10,11 +10,11 @@ For more information, please visit: http://vuongnguyen.com/creating-inline-read- Requirements --- - `lxml` - for parsing html elements + lxml - for parsing html elements Settings --- - :::python + ```python # This settings indicates that you want to create summary at a certain length SUMMARY_MAX_LENGTH = 50 From 30638d8ac4563ccebe9b4129fcfd9770cabe6f20 Mon Sep 17 00:00:00 2001 From: Vuong Nguyen Date: Mon, 16 Dec 2013 17:02:16 -0500 Subject: [PATCH 04/20] Update Readme.md --- read_more_link/Readme.md | 1 - 1 file changed, 1 deletion(-) diff --git a/read_more_link/Readme.md b/read_more_link/Readme.md index 42074ba..cc9b98b 100644 --- a/read_more_link/Readme.md +++ b/read_more_link/Readme.md @@ -14,7 +14,6 @@ Requirements Settings --- - ```python # This settings indicates that you want to create summary at a certain length SUMMARY_MAX_LENGTH = 50 From f3549dd296fea1232418a352b0a553407c218b1a Mon Sep 17 00:00:00 2001 From: Alistair Magee Date: Thu, 30 Jan 2014 22:38:04 +0000 Subject: [PATCH 05/20] plugin to specify maxmium number of images to appear in summary --- clean_summary/README.md | 34 ++++++++++++++++++++++++++++++ clean_summary/__init__.py | 1 + clean_summary/clean_summary.py | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 clean_summary/README.md create mode 100644 clean_summary/__init__.py create mode 100644 clean_summary/clean_summary.py diff --git a/clean_summary/README.md b/clean_summary/README.md new file mode 100644 index 0000000..d7572b4 --- /dev/null +++ b/clean_summary/README.md @@ -0,0 +1,34 @@ +#Clean Summary Plugin# + +Plugin to clean your summary of excess images. Images can take up much more +space than text and lead to summaries being different sizes on archive and +index pages. With this plugin you can specify a maximum number of images that +will appear in your summaries. + +There is also an option to include a minimum of one image. + +##Settings## + +plugin has two settings. `CLEAN_SUMMARY_MAXIMUM` which takes an int, and +`CLEAN_SUMMARY_MINIMUM_ONE` which takes a boolean. They deafult 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', ... ] diff --git a/clean_summary/__init__.py b/clean_summary/__init__.py new file mode 100644 index 0000000..1b96611 --- /dev/null +++ b/clean_summary/__init__.py @@ -0,0 +1 @@ +from .clean_summary import * diff --git a/clean_summary/clean_summary.py b/clean_summary/clean_summary.py new file mode 100644 index 0000000..c09702d --- /dev/null +++ b/clean_summary/clean_summary.py @@ -0,0 +1,38 @@ +""" +Clean Summary +------------- + +adds option to specify maximum number of images to appear in article summary +also adds option to include an image by default if one exists in your article +""" + +from pelican import signals +from pelican.contents import Content, Article +from bs4 import BeautifulSoup +from six import text_type + +def clean_summary(instance): + if "CLEAN_SUMMARY_MAXIMUM" in instance.settings: + maximum_images = instance.settings["CLEAN_SUMMARY_MAXIMUM"] + else: + maximum_images = 0 + if "CLEAN_SUMMARY_MINIMUM_ONE" in instance.settings: + minimum_one = instance.settings['CLEAN_SUMMARY_MINIMUM_ONE'] + else: + minimum_one = False + if type(instance) == Article: + summary = instance.summary + summary = BeautifulSoup(instance.summary, 'html.parser') + images = summary.findAll('img') + if (len(images) > maximum_images): + for image in images[maximum_images:]: + image.extract() + if len(images) < 1 and minimum_one: #try to find one + content = BeautifulSoup(instance.content, 'html.parser') + first_image = content.find('img') + if first_image: + summary.insert(0, first_image) + instance._summary = text_type(summary) + +def register(): + signals.content_object_init.connect(clean_summary) From 250cef84f03971d44904392781d8ace159707967 Mon Sep 17 00:00:00 2001 From: Alistair Magee Date: Sat, 1 Feb 2014 04:50:53 +0000 Subject: [PATCH 06/20] subcategories with same basename but different parents were not unique. Fixed now. --- subcategory/README.md | 23 +++++++++++---- subcategory/subcategory.py | 57 +++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/subcategory/README.md b/subcategory/README.md index f9a3f24..67993aa 100644 --- a/subcategory/README.md +++ b/subcategory/README.md @@ -3,9 +3,7 @@ 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. +regular category or another subcategory. Feeds can be generated for each subcategory just like categories and tags. @@ -27,16 +25,31 @@ breadcrumb style navigation you might try something like this: +##Subcategory Names## +Each subcategory's name is a `/` seperated list of it parents and itself. +This is neccesary to keep each subcategory unique. It means you can have +`Category 1/Foo` and `Category 2/Foo` and the won't intefere with each other. +Each subcategory has an attribute `shortname` which is just the name without +it's parents associated. For example if you had + + Category/Sub Category1/Sub Category2 + +the name for Sub Category 2 would be `Category/Sub Category1/Sub Category2` and +the shortname would be `Sub Category2` + +If you need to use the slug, it is generated from the short name, not the full +name. + ##Settings## diff --git a/subcategory/subcategory.py b/subcategory/subcategory.py index be37b81..e037f74 100644 --- a/subcategory/subcategory.py +++ b/subcategory/subcategory.py @@ -6,64 +6,89 @@ Adds support for subcategories on pelican articles """ import os from collections import defaultdict -from pelican import signals -from pelican.urlwrappers import URLWrapper, Category from operator import attrgetter from functools import partial +from pelican import signals +from pelican.urlwrappers import URLWrapper, Category +from pelican.utils import (slugify, python_2_unicode_compatible) + from six import text_type class SubCategory(URLWrapper): - def __init__(self, name, parent, *args, **kwargs): - super(SubCategory, self).__init__(name, *args, **kwargs) + def __init__(self, name, parent, settings): + super(SubCategory, self).__init__(name, settings) self.parent = parent + self.shortname = name.split('/') + self.shortname = self.shortname.pop() + self.slug = slugify(self.shortname, settings.get('SLUG_SUBSTITUIONS', ())) if isinstance(self.parent, SubCategory): self.savepath = os.path.join(self.parent.savepath, self.slug) self.fullurl = '{}/{}'.format(self.parent.fullurl, self.slug) else: #parent is a category self.savepath = os.path.join(self.parent.slug, self.slug) self.fullurl = '{}/{}'.format(self.parent.slug, self.slug) - + def as_dict(self): d = self.__dict__ - d['name'] = self.name + d['shortname'] = self.shortname d['savepath'] = self.savepath d['fullurl'] = self.fullurl d['parent'] = self.parent return d + def __hash__(self): + return hash(self.fullurl) + + def _key(self): + return self.fullurl + def get_subcategories(generator, metadata): if 'SUBCATEGORY_SAVE_AS' not in generator.settings: generator.settings['SUBCATEGORY_SAVE_AS'] = os.path.join( 'subcategory', '{savepath}.html') if 'SUBCATEGORY_URL' not in generator.settings: generator.settings['SUBCATEGORY_URL'] = 'subcategory/{fullurl}.html' + category_list = text_type(metadata.get('category')).split('/') category = (category_list.pop(0)).strip() category = Category(category, generator.settings) metadata['category'] = category #generate a list of subcategories with their parents sub_list = [] - parent = category + parent = category.name for subcategory in category_list: subcategory.strip() - subcategory = SubCategory(subcategory, parent, generator.settings) + subcategory = parent + '/' + subcategory sub_list.append(subcategory) parent = subcategory metadata['subcategories'] = sub_list -def organize_subcategories(generator): - generator.subcategories = defaultdict(list) +def create_subcategories(generator): + generator.subcategories = [] for article in generator.articles: - subcategories = article.metadata.get('subcategories') - for cat in subcategories: - generator.subcategories[cat].append(article) + parent = article.category + actual_subcategories = [] + for subcategory in article.subcategories: + #following line returns a list of items, tuples in this case + sub_cat = [item for item in generator.subcategories + if item[0].name == subcategory] + if sub_cat: + sub_cat[0][1].append(article) + parent = sub_cat[0][0] + actual_subcategories.append(parent) + else: + new_sub = SubCategory(subcategory, parent, generator.settings) + generator.subcategories.append((new_sub, [article,])) + parent = new_sub + actual_subcategories.append(parent) + article.subcategories = actual_subcategories def generate_subcategories(generator, writer): write = partial(writer.write_file, relative_urls=generator.settings['RELATIVE_URLS']) subcategory_template = generator.get_template('subcategory') - for subcat, articles in generator.subcategories.items(): + for subcat, articles in generator.subcategories: articles.sort(key=attrgetter('date'), reverse=True) dates = [article for article in generator.dates if article in articles] write(subcat.save_as, subcategory_template, generator.context, @@ -72,7 +97,7 @@ def generate_subcategories(generator, writer): page_name=subcat.page_name, all_articles=generator.articles) def generate_subcategory_feeds(generator, writer): - for subcat, articles in generator.subcategories.items(): + for subcat, articles in generator.subcategories: articles.sort(key=attrgetter('date'), reverse=True) if generator.settings.get('SUBCATEGORY_FEED_ATOM'): writer.write_feed(articles, generator.context, @@ -89,5 +114,5 @@ def generate(generator, writer): def register(): signals.article_generator_context.connect(get_subcategories) - signals.article_generator_finalized.connect(organize_subcategories) + signals.article_generator_finalized.connect(create_subcategories) signals.article_writer_finalized.connect(generate) From b86be87c61304e149a27c85c4cc86e6c67df18de Mon Sep 17 00:00:00 2001 From: Alistair Magee Date: Sat, 1 Feb 2014 18:21:56 +0000 Subject: [PATCH 07/20] fixed typos in README --- clean_summary/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clean_summary/README.md b/clean_summary/README.md index d7572b4..b82f9c2 100644 --- a/clean_summary/README.md +++ b/clean_summary/README.md @@ -9,26 +9,26 @@ There is also an option to include a minimum of one image. ##Settings## -plugin has two settings. `CLEAN_SUMMARY_MAXIMUM` which takes an int, and -`CLEAN_SUMMARY_MINIMUM_ONE` which takes a boolean. They deafult to 0 and False. +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 +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 +Requires Beautiful Soup: pip install BeautifulSoup4 ##Usage with Summary Plugin## -if using the summary plugin, make sure summary appears in your plugins before +If using the summary plugin, make sure summary appears in your plugins before clean summary. Eg. PLUGINS = ['summary', 'clean_summary', ... ] From dcc95a46119ccaa891ae8e104f9db33a382f686b Mon Sep 17 00:00:00 2001 From: Alistair Magee Date: Sat, 1 Feb 2014 04:54:55 +0000 Subject: [PATCH 08/20] update to the neighbors plugin to retrieve neigbors for categories and subcategories --- neighbors/Readme.rst | 74 ++++++++++++++++++++++++++++++++++++++++-- neighbors/neighbors.py | 32 ++++++++++++++---- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/neighbors/Readme.rst b/neighbors/Readme.rst index e6f7eb5..573b914 100644 --- a/neighbors/Readme.rst +++ b/neighbors/Readme.rst @@ -2,7 +2,10 @@ Neighbor Articles Plugin for Pelican ==================================== This plugin adds ``next_article`` (newer) and ``prev_article`` (older) -variables to the article's context +variables to the article's context. + +Also adds ``next_article_in_category`` and ``prev_article_in_category``. + Usage ----- @@ -24,4 +27,71 @@ Usage {% endif %} - + + + +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 + + + + diff --git a/neighbors/neighbors.py b/neighbors/neighbors.py index 27784c3..a6dd2f4 100755 --- a/neighbors/neighbors.py +++ b/neighbors/neighbors.py @@ -6,7 +6,6 @@ Neighbor Articles Plugin for Pelican This plugin adds ``next_article`` (newer) and ``prev_article`` (older) variables to the article's context """ - from pelican import signals def iter3(seq): @@ -26,14 +25,33 @@ def get_translation(article, prefered_language): return translation return article -def neighbors(generator): - for nxt, cur, prv in iter3(generator.articles): - cur.next_article = nxt - cur.prev_article = prv +def set_neighbors(articles, next_name, prev_name): + for nxt, cur, prv in iter3(articles): + exec("cur.{} = nxt".format(next_name)) + exec("cur.{} = prv".format(prev_name)) for translation in cur.translations: - translation.next_article = get_translation(nxt, translation.lang) - translation.prev_article = get_translation(prv, translation.lang) + exec( + "translation.{} = get_translation(nxt, translation.lang)".format( + next_name)) + exec( + "translation.{} = get_translation(prv, translation.lang)".format( + prev_name)) + +def neighbors(generator): + set_neighbors(generator.articles, 'next_article', 'prev_article') + + for category, articles in generator.categories: + articles.sort(key=(lambda x: x.date), reverse=(True)) + set_neighbors( + articles, 'next_article_in_category', 'prev_article_in_category') + + for subcategory, articles in generator.subcategories: + articles.sort(key=(lambda x: x.date), reverse=(True)) + index = subcategory.name.count('/') + next_name = 'next_article_in_subcategory{}'.format(index) + prev_name = 'prev_article_in_subcategory{}'.format(index) + set_neighbors(articles, next_name, prev_name) def register(): signals.article_generator_finalized.connect(neighbors) From 02fb072aa3fc1b7dc5dc8bc318af9f99fb1148ca Mon Sep 17 00:00:00 2001 From: Alistair Magee Date: Sat, 1 Feb 2014 05:02:24 +0000 Subject: [PATCH 09/20] altered plugin to work with updated subcategory plugin --- custom_article_urls/custom_article_urls.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/custom_article_urls/custom_article_urls.py b/custom_article_urls/custom_article_urls.py index 7b8a4e8..e1f1093 100644 --- a/custom_article_urls/custom_article_urls.py +++ b/custom_article_urls/custom_article_urls.py @@ -11,12 +11,6 @@ from pelican import signals from pelican.contents import Content, Category from six import text_type -def recursive_name(self): - if type(self) is Category: - return self.name - else: - return '{}/{}'.format(recursive_name(self.parent), self.name) - def custom_url(generator, metadata): if 'CUSTOM_ARTICLE_URLS' in generator.settings: custom_urls = generator.settings['CUSTOM_ARTICLE_URLS'] @@ -28,9 +22,8 @@ def custom_url(generator, metadata): if 'subcategories' in metadata: #using subcategory plugin for subcategory in metadata['subcategories']: - subcategory_name = recursive_name(subcategory) - if subcategory_name in custom_urls: - pattern_matched = custom_urls[subcategory_name] + if subcategory in custom_urls: + pattern_matched = custom_urls[subcategory] if pattern_matched: #only alter url if hasn't been set in the metdata From 5e07f2dfc640aa075bcea77fdf8ac67523125d9b Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Fri, 31 Jan 2014 10:06:52 +0100 Subject: [PATCH 10/20] new plugin: i18n_subsites This plugin extends the translations functionality by creating i8n-ized sub-sites for the default site. This commit implements the basic generation functionality. --- i18n_subsites/README.rst | 52 +++++++ i18n_subsites/__init__.py | 1 + i18n_subsites/_regenerate_context_helpers.py | 81 +++++++++++ i18n_subsites/i18n_subsites.py | 138 +++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 i18n_subsites/README.rst create mode 100644 i18n_subsites/__init__.py create mode 100644 i18n_subsites/_regenerate_context_helpers.py create mode 100644 i18n_subsites/i18n_subsites.py diff --git a/i18n_subsites/README.rst b/i18n_subsites/README.rst new file mode 100644 index 0000000..c58fa6a --- /dev/null +++ b/i18n_subsites/README.rst @@ -0,0 +1,52 @@ +i18n subsites plugin +=================== + +This plugin extends the translations functionality by creating i8n-ized 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. + +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 subsite 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:: + + 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 i18n-ized 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 subpath. + Most importantly, a localized [#local]_ theme can be specified in *THEME*. + +.. [#local] It is convenient to add language buttons to your theme in addition to the translations links. + +Usage notes +----------- +- It is **mandatory** to specify *lang* metadata for each article and page as *DEFAULT_LANG* is later changed for each 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 +------------ +- Instead of specifying a different theme for each language, the ``jinja2.ext.i18n`` extension could be used. + This would require some gettext and babel infrastructure. + +Development +----------- +Please file issues, pull requests at https://github.com/smartass101/pelican-plugins diff --git a/i18n_subsites/__init__.py b/i18n_subsites/__init__.py new file mode 100644 index 0000000..7dfbde0 --- /dev/null +++ b/i18n_subsites/__init__.py @@ -0,0 +1 @@ +from .i18n_subsites import * diff --git a/i18n_subsites/_regenerate_context_helpers.py b/i18n_subsites/_regenerate_context_helpers.py new file mode 100644 index 0000000..556f448 --- /dev/null +++ b/i18n_subsites/_regenerate_context_helpers.py @@ -0,0 +1,81 @@ + +import math +import random +from collections import defaultdict +from operator import attrgetter, itemgetter + + +def regenerate_context_articles(generator): + """Helper to regenerate context after modifying articles draft state + + essentially just a copy from pelican.generators.ArticlesGenerator.generate_context + after process_translations up to signal sending + + This has to be kept in sync untill a better solution is found + This is for Pelican version 3.3.0 + """ + # Simulate __init__ for fields that need it + generator.dates = {} + generator.tags = defaultdict(list) + generator.categories = defaultdict(list) + generator.authors = defaultdict(list) + + + # Simulate ArticlesGenerator.generate_context + for article in generator.articles: + # only main articles are listed in categories and tags + # not translations + generator.categories[article.category].append(article) + if hasattr(article, 'tags'): + for tag in article.tags: + generator.tags[tag].append(article) + # ignore blank authors as well as undefined + if hasattr(article, 'author') and article.author.name != '': + generator.authors[article.author].append(article) + + + # sort the articles by date + generator.articles.sort(key=attrgetter('date'), reverse=True) + generator.dates = list(generator.articles) + generator.dates.sort(key=attrgetter('date'), + reverse=generator.context['NEWEST_FIRST_ARCHIVES']) + + # create tag cloud + tag_cloud = defaultdict(int) + for article in generator.articles: + for tag in getattr(article, 'tags', []): + tag_cloud[tag] += 1 + + tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True) + tag_cloud = tag_cloud[:generator.settings.get('TAG_CLOUD_MAX_ITEMS')] + + tags = list(map(itemgetter(1), tag_cloud)) + if tags: + max_count = max(tags) + steps = generator.settings.get('TAG_CLOUD_STEPS') + + # calculate word sizes + generator.tag_cloud = [ + ( + tag, + int(math.floor(steps - (steps - 1) * math.log(count) + / (math.log(max_count)or 1))) + ) + for tag, count in tag_cloud + ] + # put words in chaos + random.shuffle(generator.tag_cloud) + + # and generate the output :) + + # order the categories per name + generator.categories = list(generator.categories.items()) + generator.categories.sort( + reverse=generator.settings['REVERSE_CATEGORY_ORDER']) + + generator.authors = list(generator.authors.items()) + generator.authors.sort() + + generator._update_context(('articles', 'dates', 'tags', 'categories', + 'tag_cloud', 'authors', 'related_posts')) + diff --git a/i18n_subsites/i18n_subsites.py b/i18n_subsites/i18n_subsites.py new file mode 100644 index 0000000..155aaf3 --- /dev/null +++ b/i18n_subsites/i18n_subsites.py @@ -0,0 +1,138 @@ +"""i18n_subsites plugin creates i18n-ized subsites of the default site""" + + + +import os +import six +import logging +from itertools import chain + +from pelican import signals, Pelican +from pelican.contents import Page, Article + +from ._regenerate_context_helpers import regenerate_context_articles + + + +# Global vars +_main_site_generated = False +_main_site_lang = "en" +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 + """ + s = pelican_obj.settings + for content in ['ARTICLE', 'PAGE']: + for meta in ['_URL', '_SAVE_AS']: + s[content + '_LANG' + meta] = s[content + meta] + + + +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, _main_site_lang + if _main_site_generated: # make sure this is only called once + return + else: + _main_site_generated = True + + orig_settings = pelican_obj.settings + _main_site_lang = orig_settings['DEFAULT_LANG'] + for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items(): + settings = orig_settings.copy() + settings.update(overrides) + settings['SITEURL'] = orig_settings['SITEURL'] + '/' + 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 + + 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() + + + +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: + if content_object.lang != default_lang: + if isinstance(content_object, Page): + content_object.status = 'hidden' + elif isinstance(content_object, Article): + content_object.status = 'draft' + 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 register(): + signals.initialized.connect(disable_lang_vars) + signals.article_generator_finalized.connect(update_generator_contents) + signals.page_generator_finalized.connect(update_generator_contents) + signals.finalized.connect(create_lang_subsites) From ca377d918ed894063add3b77505ec656efcd22fe Mon Sep 17 00:00:00 2001 From: Ondrej Grover Date: Sat, 1 Feb 2014 20:13:32 +0100 Subject: [PATCH 11/20] i18n_subsites plugin: implement jinja2.ext.i18n support this commit introduces optional support for translatable templates --- i18n_subsites/README.rst | 59 ++++++++++++++-------- i18n_subsites/i18n_subsites.py | 61 +++++++++++++++++++---- i18n_subsites/localizing_using_jinja2.rst | 46 +++++++++++++++++ 3 files changed, 133 insertions(+), 33 deletions(-) create mode 100644 i18n_subsites/localizing_using_jinja2.rst diff --git a/i18n_subsites/README.rst b/i18n_subsites/README.rst index c58fa6a..29a2e73 100644 --- a/i18n_subsites/README.rst +++ b/i18n_subsites/README.rst @@ -1,24 +1,24 @@ -i18n subsites plugin -=================== +====================== + I18N Sub-sites Plugin +====================== -This plugin extends the translations functionality by creating i8n-ized sub-sites for the default site. -It is therefore redundant with the *\*_LANG_{SAVE_AS,URL}* variables, so it disables them to prevent conflicts. +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. 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 subsite could even have a different *PELICAN_CLASS* if specified in *I18N_SUBSITES* conf overrides. +.. [#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:: +For each extra used language code, a language-specific variables overrides dictionary must be given (but can be empty) in the *I18N_SUBSITES* dictionary:: PLUGINS = ['i18n_subsites', ...] @@ -29,24 +29,39 @@ For each extra used language code a language specific variables overrides dictio } } -- 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 i18n-ized 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 subpath. - Most importantly, a localized [#local]_ theme can be specified in *THEME*. +- 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. -.. [#local] It is convenient to add language buttons to your theme in addition to the translations links. +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 `_. 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. 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: + +extra_siteurls + A dictionary mapping languages to their *SITEURL*. The *DEFAULT_LANG* language of the current sub-site is not included, so this dictionary serves as a complement to current *DEFAULT_LANG* and *SITEURL*. This dictionary is useful for implementing global language buttons. +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* Usage notes ------------ +=========== - It is **mandatory** to specify *lang* metadata for each article and page as *DEFAULT_LANG* is later changed for each 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. +- 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 ------------- -- Instead of specifying a different theme for each language, the ``jinja2.ext.i18n`` extension could be used. - This would require some gettext and babel infrastructure. +============ + +- add a test suite Development ------------ -Please file issues, pull requests at https://github.com/smartass101/pelican-plugins +=========== + +- A demo and test site is in the ``gh-pages`` branch and can be seen at http://smartass101.github.io/pelican-plugins/ + +.. LocalWords: lang metadata diff --git a/i18n_subsites/i18n_subsites.py b/i18n_subsites/i18n_subsites.py index 155aaf3..d203ba6 100644 --- a/i18n_subsites/i18n_subsites.py +++ b/i18n_subsites/i18n_subsites.py @@ -6,8 +6,11 @@ import os import six import logging from itertools import chain +from collections import defaultdict -from pelican import signals, Pelican +import gettext + +from pelican import signals from pelican.contents import Page, Article from ._regenerate_context_helpers import regenerate_context_articles @@ -17,6 +20,7 @@ from ._regenerate_context_helpers import regenerate_context_articles # Global vars _main_site_generated = False _main_site_lang = "en" +_main_siteurl = '' logger = logging.getLogger(__name__) @@ -27,13 +31,17 @@ def disable_lang_vars(pelican_obj): e.g. ARTICLE_LANG_URL = ARTICLE_URL They would conflict with this plugin otherwise """ + global _main_site_lang, _main_siteurl 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'] - + def create_lang_subsites(pelican_obj): """For each language create a subsite using the lang-specific config @@ -42,22 +50,21 @@ def create_lang_subsites(pelican_obj): 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, _main_site_lang + 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 - _main_site_lang = orig_settings['DEFAULT_LANG'] for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items(): settings = orig_settings.copy() settings.update(overrides) - settings['SITEURL'] = orig_settings['SITEURL'] + '/' + lang + settings['SITEURL'] = _main_siteurl + '/' + 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['DELETE_OUTPUT_DIRECTORY'] = False # prevent deletion of previous runs + cls = settings['PELICAN_CLASS'] if isinstance(cls, six.string_types): module, cls_name = cls.rsplit('.', 1) @@ -105,22 +112,22 @@ def update_generator_contents(generator, *args): 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 + hidden_contents = generator.hidden_pages if is_pages_gen else generator.drafts default_lang = generator.settings['DEFAULT_LANG'] for content_object in contents: if content_object.lang != default_lang: if isinstance(content_object, Page): content_object.status = 'hidden' elif isinstance(content_object, Article): - content_object.status = 'draft' + content_object.status = 'draft' 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 + if hasattr(generator, '_generate_context_aggregate'): # if implemented # Simulate __init__ for fields that need it generator.dates = {} generator.tags = defaultdict(list) @@ -131,8 +138,40 @@ def update_generator_contents(generator, *args): 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 + extra_siteurls = { lang: _main_siteurl + '/' + lang for lang in generator.settings.get('I18N_SUBSITES', {}).keys() } + extra_siteurls[_main_site_lang] = _main_siteurl + extra_siteurls.pop(generator.settings['DEFAULT_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/') + languages = [generator.settings['DEFAULT_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) diff --git a/i18n_subsites/localizing_using_jinja2.rst b/i18n_subsites/localizing_using_jinja2.rst new file mode 100644 index 0000000..3d481f8 --- /dev/null +++ b/i18n_subsites/localizing_using_jinja2.rst @@ -0,0 +1,46 @@ +----------------------------- +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:: + + JINJA_EXTENSIONS = ['jinja2.ext.i18n', ...] + +Then follow the `Jinja2 templating documentation for the I18N plugin `_ to make your templates localizable. To enable `newstyle gettext calls `_ 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 `_ 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:: + + 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 ``*.mo`` files yourself, or you can use some helper tool as described in `the Python gettext library tutorial `_. + +Recommended tool: babel +....................... + +`Babel `_ makes it easy to extract translatable strings from the localized Jinja2 templates and assists with creating translations as documented in this `Jinja2-Babel tutorial `_ [#flask]_. + +.. [#flask] Although the tutorial is focused on Flask-based web applications, the linked translation tutorial is not Flask-specific. From 56ca351f9818ecb7f64433b2797ee91f0721e211 Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 2 Feb 2014 17:52:40 -0800 Subject: [PATCH 12/20] Minor fixes to Subcategory plugin README --- subcategory/README.md | 55 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/subcategory/README.md b/subcategory/README.md index 67993aa..074b374 100644 --- a/subcategory/README.md +++ b/subcategory/README.md @@ -2,12 +2,11 @@ Adds support for subcategories in addition to article categories. -Subcategories are heirachial. Each subcategory has a parent, which is either a +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## Subcategories are an extension to categories. Add subcategories to an article's @@ -15,12 +14,12 @@ 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: - + ##Subcategory Names## -Each subcategory's name is a `/` seperated list of it parents and itself. -This is neccesary to keep each subcategory unique. It means you can have -`Category 1/Foo` and `Category 2/Foo` and the won't intefere with each other. -Each subcategory has an attribute `shortname` which is just the name without -it's parents associated. For example if you had - + +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 name for Sub Category 2 would be `Category/Sub Category1/Sub Category2` and -the shortname would be `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 +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 From 84b1962afc81917302e0e6dd7d0f3300d864a14b Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 2 Feb 2014 17:56:08 -0800 Subject: [PATCH 13/20] More fixes for Subcategory plugin README --- subcategory/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/subcategory/README.md b/subcategory/README.md index 074b374..21c610f 100644 --- a/subcategory/README.md +++ b/subcategory/README.md @@ -1,11 +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 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. +Feeds can be generated for each subcategory, just like categories and tags. ##Usage## @@ -17,14 +17,14 @@ category metadata using a `/` like this: 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 +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: