diff --git a/clean_summary/README.md b/clean_summary/README.md new file mode 100644 index 0000000..b82f9c2 --- /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## + +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', ... ] 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) diff --git a/custom_article_urls/README.md b/custom_article_urls/README.md index 2b87d18..c2b7e9e 100644 --- a/custom_article_urls/README.md +++ b/custom_article_urls/README.md @@ -1,17 +1,17 @@ #Custom Article URLs# -Adds support for defining different default urls for different categories, or -different subcategories if using the subcategory plugin. +This plugin adds support for defining different default URLs for different +categories, or different subcategories if using the subcategory plugin. ##Usage## -After adding `custom_article_urls` to your `PLUGINS` add a `CUSTOM_ARTICLE_URLS` -setting, which is a dictionary of rules. The rules are also a dictionary, -consisting of the `URL` and the `SAVE_AS` values. +After adding `custom_article_urls` to your `PLUGINS`, add a +`CUSTOM_ARTICLE_URLS` setting, which is a dictionary of rules. The rules are +also a dictionary, consisting of the `URL` and the `SAVE_AS` values. -For example, if you had two categories, Category 1 and Category 2 and you -would like Category 1 saved as category-1/article-slug/ and Category 2 saved as -/year/month/article-slug/ you would add: +For example, if you had two categories, *Category 1* and *Category 2*, and you +would like *Category 1* saved as `category-1/article-slug/` and *Category 2* +saved as `/year/month/article-slug/`, you would add: CUSTOM_ARTICLE_URLS = { 'Category 1': {'URL': '{category}/{slug}/, @@ -20,17 +20,17 @@ would like Category 1 saved as category-1/article-slug/ and Category 2 saved as 'SAVE_AS': '{date:%Y}/{date:%B}/{slug}/index.html}, } -If had any other categories they would use the default `ARTICLE_SAVE_AS` -and `ARTICLE_URL` +If you had any other categories, they would use the default `ARTICLE_SAVE_AS` +and `ARTICLE_URL` settings. If you are using the subcategory plugin, you can define them the same way. -For example if Category 1 had a subcategory Sub Category you could define -it's rules with +For example, if *Category 1* had a subcategory called *Sub Category*, you could +define its rules with:: 'Category 1/Sub Category`: ... ##Other Usage: Article Metadata## -If you define a url and save_as in your articles metadata, then this plugin -will not alter that value. So you can still specify special one off urls as -normal. +If you define `URL` and `Save_as` in your article metadata, then this plugin +will not alter that value. So you can still specify special one-off URLs as +you normally would. 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 diff --git a/disqus_static/README.rst b/disqus_static/README.rst index 4e96548..77cc5fa 100644 --- a/disqus_static/README.rst +++ b/disqus_static/README.rst @@ -15,7 +15,7 @@ We use disqus-python package for communication with disqus API: Put ``disqus_static.py`` plugin in ``plugins`` folder in pelican installation and use the following in your settings:: - PLUGINS = [u"pelican.plugins.disqus_static"] + PLUGINS = [u"disqus_static"] DISQUS_SITENAME = u'YOUR_SITENAME' DISQUS_SECRET_KEY = u'YOUR_SECRET_KEY' diff --git a/disqus_static/__init__py b/disqus_static/__init__py new file mode 100644 index 0000000..acda53f --- /dev/null +++ b/disqus_static/__init__py @@ -0,0 +1 @@ +from .disqus_static import * diff --git a/i18n_subsites/README.rst b/i18n_subsites/README.rst new file mode 100644 index 0000000..37ceef0 --- /dev/null +++ b/i18n_subsites/README.rst @@ -0,0 +1,73 @@ +====================== + I18N Sub-sites Plugin +====================== + +This plugin extends the translations functionality by creating internationalized sub-sites for the default site. It is therefore redundant with the *\*_LANG_{SAVE_AS,URL}* variables, so it disables them to prevent conflicts. + +What it does +============ +1. The *\*_LANG_URL* and *\*_LANG_SAVE_AS* variables are set to their normal counterparts (e.g. *ARTICLE_URL*) so they don't conflict with this scheme. +2. While building the site for *DEFAULT_LANG* the translations of pages and articles are not generated, but their relations to the original content is kept via links to them. +3. For each non-default language a "sub-site" with a modified config [#conf]_ is created [#run]_, linking the translations to the originals (if available). The configured language code is appended to the *OUTPUT_PATH* and *SITEURL* of each sub-site. For each sub-site, *DEFAULT_LANG* is changed to the language of the sub-site so that articles in a different language are treated as translations. + +If *HIDE_UNTRANSLATED_CONTENT* is True (default), content without a translation for a language is generated as hidden (for pages) or draft (for articles) for the corresponding language sub-site. + +.. [#conf] For each language a config override is given in the *I18N_SUBSITES* dictionary. +.. [#run] Using a new *PELICAN_CLASS* instance and its ``run`` method, so each sub-site could even have a different *PELICAN_CLASS* if specified in *I18N_SUBSITES* conf overrides. + +Setting it up +============= + +For each extra used language code, a language-specific variables overrides dictionary must be given (but can be empty) in the *I18N_SUBSITES* dictionary + +.. code-block:: python + + PLUGINS = ['i18n_subsites', ...] + + # mapping: language_code -> conf_overrides_dict + I18N_SUBSITES = { + 'cz': { + 'SITENAME': 'Hezkej blog', + } + } + +- The language code is the language identifier used in the *lang* metadata. It is appended to *OUTPUT_PATH* and *SITEURL* of each I18N sub-site. +- The internationalized config overrides dictionary may specify configuration variable overrides — e.g. a different *LOCALE*, *SITENAME*, *TIMEZONE*, etc. However, it **must not** override *OUTPUT_PATH* and *SITEURL* as they are modified automatically by appending the language code. + +Localizing templates +-------------------- + +Most importantly, this plugin can use localized templates for each sub-site. There are two approaches to having the templates localized: + +- You can set a different *THEME* override for each language in *I18N_SUBSITES*, e.g. by making a copy of a theme ``my_theme`` to ``my_theme_lang`` and then editing the templates in the new localized theme. This approach means you don't have to deal with gettext ``*.po`` files, but it is harder to maintain over time. +- You use only one theme and localize the templates using the `jinja2.ext.i18n Jinja2 extension `_. 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/ 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..2ae30e7 --- /dev/null +++ b/i18n_subsites/i18n_subsites.py @@ -0,0 +1,189 @@ +"""i18n_subsites plugin creates i18n-ized subsites of the default site""" + + + +import os +import six +import logging +from itertools import chain +from collections import defaultdict, OrderedDict + +import gettext + +from pelican import signals +from pelican.contents import Page, Article +from pelican.settings import configure_settings + +from ._regenerate_context_helpers import regenerate_context_articles + + + +# Global vars +_main_site_generated = False +_main_site_lang = "en" +_main_siteurl = '' +_lang_siteurls = None +logger = logging.getLogger(__name__) + + + +def disable_lang_vars(pelican_obj): + """Set lang specific url and save_as vars to the non-lang defaults + + e.g. ARTICLE_LANG_URL = ARTICLE_URL + They would conflict with this plugin otherwise + """ + global _main_site_lang, _main_siteurl, _lang_siteurls + s = pelican_obj.settings + for content in ['ARTICLE', 'PAGE']: + for meta in ['_URL', '_SAVE_AS']: + s[content + '_LANG' + meta] = s[content + meta] + if not _main_site_generated: + _main_site_lang = s['DEFAULT_LANG'] + _main_siteurl = s['SITEURL'] + _lang_siteurls = [(lang, _main_siteurl + '/' + lang) for lang in s.get('I18N_SUBSITES', {}).keys()] + # To be able to use url for main site root when SITEURL == '' (e.g. when developing) + _lang_siteurls = [(_main_site_lang, ('/' if _main_siteurl == '' else _main_siteurl))] + _lang_siteurls + _lang_siteurls = OrderedDict(_lang_siteurls) + + + +def create_lang_subsites(pelican_obj): + """For each language create a subsite using the lang-specific config + + for each generated lang append language subpath to SITEURL and OUTPUT_PATH + and set DEFAULT_LANG to the language code to change perception of what is translated + and set DELETE_OUTPUT_DIRECTORY to False to prevent deleting output from previous runs + Then generate the subsite using a PELICAN_CLASS instance and its run method. + """ + global _main_site_generated + if _main_site_generated: # make sure this is only called once + return + else: + _main_site_generated = True + + orig_settings = pelican_obj.settings + for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items(): + settings = orig_settings.copy() + settings.update(overrides) + settings['SITEURL'] = _lang_siteurls[lang] + settings['OUTPUT_PATH'] = os.path.join(orig_settings['OUTPUT_PATH'], lang, '') + settings['DEFAULT_LANG'] = lang # to change what is perceived as translations + settings['DELETE_OUTPUT_DIRECTORY'] = False # prevent deletion of previous runs + settings = configure_settings(settings) # to set LOCALE, etc. + + cls = settings['PELICAN_CLASS'] + if isinstance(cls, six.string_types): + module, cls_name = cls.rsplit('.', 1) + module = __import__(module) + cls = getattr(module, cls_name) + + pelican_obj = cls(settings) + logger.debug("Generating i18n subsite for lang '{}' using class '{}'".format(lang, str(cls))) + pelican_obj.run() + _main_site_generated = False # for autoreload mode + + + +def move_translations_links(content_object): + """This function points translations links to the sub-sites + + by prepending their location with the language code + or directs an original DEFAULT_LANG translation back to top level site + """ + for translation in content_object.translations: + if translation.lang == _main_site_lang: + # cannot prepend, must take to top level + lang_prepend = '../' + else: + lang_prepend = translation.lang + '/' + translation.override_url = lang_prepend + translation.url + + + +def update_generator_contents(generator, *args): + """Update the contents lists of a generator + + Empty the (hidden_)translation attribute of article and pages generators + to prevent generating the translations as they will be generated in the lang sub-site + and point the content translations links to the sub-sites + + Hide content without a translation for current DEFAULT_LANG + if HIDE_UNTRANSLATED_CONTENT is True + """ + generator.translations = [] + is_pages_gen = hasattr(generator, 'pages') + if is_pages_gen: + generator.hidden_translations = [] + for page in chain(generator.pages, generator.hidden_pages): + move_translations_links(page) + else: # is an article generator + for article in chain(generator.articles, generator.drafts): + move_translations_links(article) + + if not generator.settings.get('HIDE_UNTRANSLATED_CONTENT', True): + return + contents = generator.pages if is_pages_gen else generator.articles + hidden_contents = generator.hidden_pages if is_pages_gen else generator.drafts + default_lang = generator.settings['DEFAULT_LANG'] + for content_object in contents[:]: # loop over copy for removing + if content_object.lang != default_lang: + if isinstance(content_object, Article): + content_object.status = 'draft' + elif isinstance(content_object, Page): + content_object.status = 'hidden' + contents.remove(content_object) + hidden_contents.append(content_object) + if not is_pages_gen: # regenerate categories, tags, etc. for articles + if hasattr(generator, '_generate_context_aggregate'): # if implemented + # Simulate __init__ for fields that need it + generator.dates = {} + generator.tags = defaultdict(list) + generator.categories = defaultdict(list) + generator.authors = defaultdict(list) + generator._generate_context_aggregate() + else: # fallback for Pelican 3.3.0 + regenerate_context_articles(generator) + + + +def install_templates_translations(generator): + """Install gettext translations for current DEFAULT_LANG in the jinja2.Environment + + if the 'jinja2.ext.i18n' jinja2 extension is enabled + adds some useful variables into the template context + """ + generator.context['main_siteurl'] = _main_siteurl + generator.context['main_lang'] = _main_site_lang + generator.context['lang_siteurls'] = _lang_siteurls + current_def_lang = generator.settings['DEFAULT_LANG'] + extra_siteurls = _lang_siteurls.copy() + extra_siteurls.pop(current_def_lang) + generator.context['extra_siteurls'] = extra_siteurls + + if 'jinja2.ext.i18n' not in generator.settings['JINJA_EXTENSIONS']: + return + domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages') + localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR') + if localedir is None: + localedir = os.path.join(generator.theme, 'translations') + if current_def_lang == generator.settings.get('I18N_TEMPLATES_LANG', _main_site_lang): + translations = gettext.NullTranslations() + else: + languages = [current_def_lang] + try: + translations = gettext.translation(domain, localedir, languages) + except (IOError, OSError): + logger.error("Cannot find translations for language '{}' in '{}' with domain '{}'. Installing NullTranslations.".format(languages[0], localedir, domain)) + translations = gettext.NullTranslations() + newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True) + generator.env.install_gettext_translations(translations, newstyle) + + + +def register(): + signals.initialized.connect(disable_lang_vars) + signals.generator_init.connect(install_templates_translations) + signals.article_generator_finalized.connect(update_generator_contents) + signals.page_generator_finalized.connect(update_generator_contents) + signals.finalized.connect(create_lang_subsites) diff --git a/i18n_subsites/implementing_language_buttons.rst b/i18n_subsites/implementing_language_buttons.rst new file mode 100644 index 0000000..014cf1c --- /dev/null +++ b/i18n_subsites/implementing_language_buttons.rst @@ -0,0 +1,113 @@ +----------------------------- +Implementing language buttons +----------------------------- + +Each article with translations has translations links, but that's the only way to switch between language subsites. + +For this reason it is convenient to add language buttons to the top menu bar to make it simple to switch between the language subsites on all pages. + +Example designs +--------------- + +Language buttons showing other available languages +.................................................. + +The ``extra_siteurls`` dictionary is a mapping of all other (not the *DEFAULT_LANG* of the current (sub-)site) languages to the *SITEURL* of the respective (sub-)sites + +.. code-block:: jinja + + +