diff --git a/3-column-plus/pelican-plugins/summary/Readme.rst b/3-column-plus/pelican-plugins/summary/Readme.rst new file mode 100644 index 0000000..29b3ed9 --- /dev/null +++ b/3-column-plus/pelican-plugins/summary/Readme.rst @@ -0,0 +1,56 @@ +Summary +------- + +This plugin allows easy, variable length summaries directly embedded into the +body of your articles. It introduces two new settings: ``SUMMARY_BEGIN_MARKER`` +and ``SUMMARY_END_MARKER``: strings which can be placed directly into an article +to mark the beginning and end of a summary. When found, the standard +``SUMMARY_MAX_LENGTH`` setting will be ignored. The markers themselves will also +be removed from your articles before they are published. The default values +are ```` and ````. +For example:: + + Title: My super title + Date: 2010-12-03 10:20 + Tags: thats, awesome + Category: yeah + Slug: my-super-post + Author: Alexis Metaireau + + This is the content of my super blog post. + + and this content occurs after the summary. + +Here, the summary is taken to be the first line of the post. Because no +beginning marker was found, it starts at the top of the body. It is possible +to leave out the end marker instead, in which case the summary will start at the +beginning marker and continue to the end of the body. + +If no beginning or end marker is found, and if ``SUMMARY_USE_FIRST_PARAGRAPH`` +is enabled in the settings, the summary will be the first paragraph of the post. + +The plugin also sets a ``has_summary`` attribute on every article. It is True +for articles with an explicitly-defined summary, and False otherwise. (It is +also False for an article truncated by ``SUMMARY_MAX_LENGTH``.) Your templates +can use this e.g. to add a link to the full text at the end of the summary. + +reST example +~~~~~~~~~~~~ + +Inserting the markers into a reStructuredText document makes use of the +comment directive, because raw HTML is automatically escaped. The reST equivalent of the above Markdown example looks like this:: + + My super title + ############## + + :date: 2010-12-03 10:20 + :tags: thats, awesome + :category: yeah + :slug: my-super-post + :author: Alexis Metaireau + + This is the content of my super blog post. + + .. PELICAN_END_SUMMARY + + and this content occurs after the summary. diff --git a/3-column-plus/pelican-plugins/summary/__init__.py b/3-column-plus/pelican-plugins/summary/__init__.py new file mode 100644 index 0000000..afe9311 --- /dev/null +++ b/3-column-plus/pelican-plugins/summary/__init__.py @@ -0,0 +1 @@ +from .summary import * diff --git a/3-column-plus/pelican-plugins/summary/summary.py b/3-column-plus/pelican-plugins/summary/summary.py new file mode 100644 index 0000000..0fd89d1 --- /dev/null +++ b/3-column-plus/pelican-plugins/summary/summary.py @@ -0,0 +1,105 @@ +""" +Summary +------- + +This plugin allows easy, variable length summaries directly embedded into the +body of your articles. +""" + +from __future__ import unicode_literals +from pelican import signals +from pelican.generators import ArticlesGenerator, StaticGenerator, PagesGenerator +import re + +def initialized(pelican): + from pelican.settings import DEFAULT_CONFIG + DEFAULT_CONFIG.setdefault('SUMMARY_BEGIN_MARKER', + '') + DEFAULT_CONFIG.setdefault('SUMMARY_END_MARKER', + '') + DEFAULT_CONFIG.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False) + if pelican: + pelican.settings.setdefault('SUMMARY_BEGIN_MARKER', + '') + pelican.settings.setdefault('SUMMARY_END_MARKER', + '') + pelican.settings.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False) + +def extract_summary(instance): + # if summary is already specified, use it + # if there is no content, there's nothing to do + if hasattr(instance, '_summary'): + instance.has_summary = True + return + + if not instance._content: + instance.has_summary = False + return + + begin_marker = instance.settings['SUMMARY_BEGIN_MARKER'] + end_marker = instance.settings['SUMMARY_END_MARKER'] + use_first_paragraph = instance.settings['SUMMARY_USE_FIRST_PARAGRAPH'] + remove_markers = True + + content = instance._content + begin_summary = -1 + end_summary = -1 + if begin_marker: + begin_summary = content.find(begin_marker) + if end_marker: + end_summary = content.find(end_marker) + + if begin_summary == -1 and end_summary == -1 and use_first_paragraph: + begin_marker, end_marker = '

', '

' + remove_markers = False + begin_summary = content.find(begin_marker) + end_summary = content.find(end_marker) + + if begin_summary == -1 and end_summary == -1: + instance.has_summary = False + return + + # skip over the begin marker, if present + if begin_summary == -1: + begin_summary = 0 + else: + begin_summary = begin_summary + len(begin_marker) + + if end_summary == -1: + end_summary = None + + summary = content[begin_summary:end_summary] + + if remove_markers: + # remove the markers from the content + if begin_summary: + content = content.replace(begin_marker, '', 1) + if end_summary: + content = content.replace(end_marker, '', 1) + + summary = re.sub(r"", "", summary) + summary = re.sub(r"", "", summary) + + instance._content = content + instance._summary = summary + instance.has_summary = True + + +def run_plugin(generators): + for generator in generators: + if isinstance(generator, ArticlesGenerator): + for article in generator.articles: + extract_summary(article) + elif isinstance(generator, PagesGenerator): + for page in generator.pages: + extract_summary(page) + + +def register(): + signals.initialized.connect(initialized) + try: + signals.all_generators_finalized.connect(run_plugin) + except AttributeError: + # NOTE: This results in #314 so shouldn't really be relied on + # https://github.com/getpelican/pelican-plugins/issues/314 + signals.content_object_init.connect(extract_summary) diff --git a/3-column-plus/pelican-plugins/summary/test_summary.py b/3-column-plus/pelican-plugins/summary/test_summary.py new file mode 100644 index 0000000..6dda508 --- /dev/null +++ b/3-column-plus/pelican-plugins/summary/test_summary.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +import unittest + +from jinja2.utils import generate_lorem_ipsum + +# generate one paragraph, enclosed with

+TEST_CONTENT = str(generate_lorem_ipsum(n=1)) +TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False) + + +from pelican.contents import Page +import pelican.settings + +import summary + +class TestSummary(unittest.TestCase): + def setUp(self): + super(TestSummary, self).setUp() + pelican.settings.DEFAULT_CONFIG['SUMMARY_MAX_LENGTH'] = None + pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = False + + summary.register() + summary.initialized(None) + self.page_kwargs = { + 'content': TEST_CONTENT, + 'context': { + 'localsiteurl': '', + }, + 'metadata': { + 'summary': TEST_SUMMARY, + 'title': 'foo bar', + 'author': 'Blogger', + }, + } + + def _copy_page_kwargs(self): + # make a deep copy of page_kwargs + page_kwargs = dict([(key, self.page_kwargs[key]) for key in + self.page_kwargs]) + for key in page_kwargs: + if not isinstance(page_kwargs[key], dict): + break + page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey]) + for subkey in page_kwargs[key]]) + + return page_kwargs + + def test_end_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + TEST_SUMMARY + '' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, TEST_SUMMARY + TEST_CONTENT) + + def test_begin_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + 'FOOBAR' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_CONTENT) + self.assertEqual(page.content, 'FOOBAR' + TEST_CONTENT) + + def test_begin_end_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + 'FOOBAR' + TEST_SUMMARY + + '' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, 'FOOBAR' + TEST_SUMMARY + TEST_CONTENT) + + def test_use_first_paragraph(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = True + page_kwargs['content'] = '

' + TEST_SUMMARY + '

' + TEST_CONTENT + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, '

' + TEST_SUMMARY + '

' + TEST_CONTENT) + + +if __name__ == '__main__': + unittest.main() diff --git a/3-column-plus/pelican-plugins/thumbnailer/Readme.md b/3-column-plus/pelican-plugins/thumbnailer/Readme.md new file mode 100644 index 0000000..c3db910 --- /dev/null +++ b/3-column-plus/pelican-plugins/thumbnailer/Readme.md @@ -0,0 +1,33 @@ +Thumbnail Creation of images +============================ + +This plugin creates thumbnails for all of the images found under a specific directory, in various thumbnail sizes. +It requires `PIL` to function properly since `PIL` is used to resize the images, and the thumbnail will only be re-built +if it doesn't already exist (to save processing time). + +Installation +------------- + +Set up like any other plugin, making sure to set `PLUGIN_PATH` and add `thumbnailer` to the `PLUGINS` list. + +[PIL](http://www.pythonware.com/products/pil/) or [Pillow](http://pillow.readthedocs.org/en/latest/installation.html#) +is required. If you choose Pillow, you need to install some additional +[external libraries](http://www.pythonware.com/products/pil/) to add support for image types like `jpg`. + +Configuration +------------- + +* `IMAGE_PATH` is the path to the image directory. It should reside inside your content directory, 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 size name to size specifications. + The generated filename will be `originalname_thumbnailname.ext` unless `THUMBNAIL_KEEP_NAME` is set. +* `THUMBNAIL_KEEP_NAME` is a Boolean that, if set, puts the file with the original name in a thumbnailname folder, named like the key in `THUMBNAIL_SIZES`. +* `THUMBNAIL_KEEP_TREE` is a Boolean that, if set, saves the image directory tree. +* `THUMBNAIL_INCLUDE_REGEX` is an optional string that is used as regular expression to restrict thumbnailing to matching files. By default all files not starting with a dot are respected. + +Sizes can be specified using any of the following formats: + +* wxh will resize to exactly wxh cropping as necessary to get that size +* wx? will resize so that the width is the specified size, and the height will scale to retain aspect ratio +* ?xh same as wx? but will height being a set size +* s is a shorthand for wxh where w=h diff --git a/3-column-plus/pelican-plugins/thumbnailer/__init__.py b/3-column-plus/pelican-plugins/thumbnailer/__init__.py new file mode 100644 index 0000000..20797b1 --- /dev/null +++ b/3-column-plus/pelican-plugins/thumbnailer/__init__.py @@ -0,0 +1 @@ +from .thumbnailer import * \ No newline at end of file diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_exact.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_exact.jpg new file mode 100644 index 0000000..5819792 Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_exact.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_height.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_height.jpg new file mode 100644 index 0000000..6459410 Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_height.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_square.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_square.jpg new file mode 100644 index 0000000..de99e5b Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_square.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_width.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_width.jpg new file mode 100644 index 0000000..9c2efc6 Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/expected_width.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/sample_image.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/sample_image.jpg new file mode 100644 index 0000000..cc83880 Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/sample_image.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg b/3-column-plus/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg new file mode 100644 index 0000000..cc83880 Binary files /dev/null and b/3-column-plus/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg differ diff --git a/3-column-plus/pelican-plugins/thumbnailer/test_thumbnails.py b/3-column-plus/pelican-plugins/thumbnailer/test_thumbnails.py new file mode 100644 index 0000000..fdb0c34 --- /dev/null +++ b/3-column-plus/pelican-plugins/thumbnailer/test_thumbnails.py @@ -0,0 +1,63 @@ +from thumbnailer import _resizer +from unittest import TestCase, main +import os +from PIL import Image + +class ThumbnailerTests(TestCase): + + def path(self, filename): + return os.path.join(self.img_path, filename) + + def setUp(self): + self.img_path = os.path.join(os.path.dirname(__file__), "test_data") + self.img = Image.open(self.path("sample_image.jpg")) + + def testSquare(self): + r = _resizer('square', '100', self.img_path) + output = r.resize(self.img) + self.assertEqual((100, 100), output.size) + + def testExact(self): + r = _resizer('exact', '250x100', self.img_path) + output = r.resize(self.img) + self.assertEqual((250, 100), output.size) + + def testWidth(self): + r = _resizer('aspect', '250x?', self.img_path) + output = r.resize(self.img) + self.assertEqual((250, 166), output.size) + + def testHeight(self): + r = _resizer('aspect', '?x250', self.img_path) + output = r.resize(self.img) + self.assertEqual((375, 250), output.size) + +class ThumbnailerFilenameTest(TestCase): + + def path(self, *parts): + return os.path.join(self.img_path, *parts) + + def setUp(self): + self.img_path = os.path.join(os.path.dirname(__file__), "test_data") + + def testRoot(self): + """Test a file that is in the root of img_path.""" + + r = _resizer('square', '100', self.img_path) + new_name = r.get_thumbnail_name(self.path('sample_image.jpg')) + self.assertEqual('sample_image_square.jpg', new_name) + + def testRootWithSlash(self): + r = _resizer('square', '100', self.img_path + '/') + new_name = r.get_thumbnail_name(self.path('sample_image.jpg')) + self.assertEqual('sample_image_square.jpg', new_name) + + def testSubdir(self): + """Test a file that is in a sub-directory of img_path.""" + + r = _resizer('square', '100', self.img_path) + new_name = r.get_thumbnail_name(self.path('subdir', 'sample_image.jpg')) + self.assertEqual('subdir/sample_image_square.jpg', new_name) + +if __name__=="__main__": + main() diff --git a/3-column-plus/pelican-plugins/thumbnailer/thumbnailer.py b/3-column-plus/pelican-plugins/thumbnailer/thumbnailer.py new file mode 100644 index 0000000..46f0ebd --- /dev/null +++ b/3-column-plus/pelican-plugins/thumbnailer/thumbnailer.py @@ -0,0 +1,208 @@ +import os +import os.path as path +import re +from pelican import signals + +import logging +logger = logging.getLogger(__name__) + +try: + from PIL import Image, ImageOps + enabled = True +except ImportError: + logging.warning("Unable to load PIL, disabling thumbnailer") + enabled = False + +DEFAULT_IMAGE_DIR = "pictures" +DEFAULT_THUMBNAIL_DIR = "thumbnails" +DEFAULT_THUMBNAIL_SIZES = { + 'thumbnail_square': '150', + 'thumbnail_wide': '150x?', + 'thumbnail_tall': '?x150', +} +DEFAULT_TEMPLATE = """{filename}""" +DEFAULT_GALLERY_THUMB = "thumbnail_square" + +class _resizer(object): + """ Resizes based on a text specification, see readme """ + + REGEX = re.compile(r'(\d+|\?)x(\d+|\?)') + + def __init__(self, name, spec, root): + self._name = name + self._spec = spec + # The location of input images from _image_path. + self._root = root + + def _null_resize(self, w, h, image): + return image + + def _exact_resize(self, w, h, image): + retval = ImageOps.fit(image, (w,h), Image.BICUBIC) + return retval + + def _aspect_resize(self, w, h, image): + retval = image.copy() + retval.thumbnail((w, h), Image.ANTIALIAS) + + return retval + + def resize(self, image): + resizer = self._null_resize + + # Square resize and crop + if 'x' not in self._spec: + resizer = self._exact_resize + targetw = int(self._spec) + targeth = targetw + else: + matches = self.REGEX.search(self._spec) + tmpw = matches.group(1) + tmph = matches.group(2) + + # Full Size + if tmpw == '?' and tmph == '?': + targetw = image.size[0] + targeth = image.size[1] + resizer = self._null_resize + + # Set Height Size + if tmpw == '?': + targetw = image.size[0] + targeth = int(tmph) + resizer = self._aspect_resize + + # Set Width Size + elif tmph == '?': + targetw = int(tmpw) + targeth = image.size[1] + resizer = self._aspect_resize + + # Scale and Crop + else: + targetw = int(tmpw) + targeth = int(tmph) + resizer = self._exact_resize + + logging.debug("Using resizer {0}".format(resizer.__name__)) + return resizer(targetw, targeth, image) + + def get_thumbnail_name(self, in_path): + # Find the partial path + filename beyond the input image directory. + prefix = path.commonprefix([in_path, self._root]) + new_filename = in_path[len(prefix) + 1:] + + # Generate the new filename. + (basename, ext) = path.splitext(new_filename) + return "{0}_{1}{2}".format(basename, self._name, ext) + + 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 supported by PIL + :param out_path: path to the directory root for the outputted thumbnails to be stored + :return: None + """ + if keep_filename: + filename = path.join(out_path, path.basename(in_path)) + else: + filename = path.join(out_path, self.get_thumbnail_name(in_path)) + out_path = path.dirname(filename) + if not path.exists(out_path): + os.makedirs(out_path) + if not path.exists(filename): + try: + image = Image.open(in_path) + thumbnail = self.resize(image) + thumbnail.save(filename) + logger.info("Generated Thumbnail {0}".format(path.basename(filename))) + except IOError: + logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename))) + + +def resize_thumbnails(pelican): + """ Resize a directory tree full of images into thumbnails + + :param pelican: The pelican instance + :return: None + """ + global enabled + if not enabled: + return + + in_path = _image_path(pelican) + + include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX') + if include_regex: + pattern = re.compile(include_regex) + is_included = lambda name: pattern.match(name) + else: + is_included = lambda name: not name.startswith('.') + + sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES) + resizers = dict((k, _resizer(k, v, in_path)) for k,v in sizes.items()) + logger.debug("Thumbnailer Started") + for dirpath, _, filenames in os.walk(in_path): + for filename in filenames: + if is_included(filename): + for name, resizer in resizers.items(): + in_filename = path.join(dirpath, filename) + out_path = get_out_path(pelican, in_path, in_filename, name) + resizer.resize_file_to( + in_filename, + out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME')) + + +def get_out_path(pelican, in_path, in_filename, name): + base_out_path = path.join(pelican.settings['OUTPUT_PATH'], + pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR)) + logger.debug("Processing thumbnail {0}=>{1}".format(in_filename, name)) + if pelican.settings.get('THUMBNAIL_KEEP_NAME', False): + if pelican.settings.get('THUMBNAIL_KEEP_TREE', False): + return path.join(base_out_path, name, path.dirname(path.relpath(in_filename, in_path))) + else: + return path.join(base_out_path, name) + else: + return base_out_path + + +def _image_path(pelican): + return path.join(pelican.settings['PATH'], + pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/') + + +def expand_gallery(generator, metadata): + """ Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH + + :param pelican: The pelican instance + :return: None + """ + if "gallery" not in metadata or metadata['gallery'] is None: + return # If no gallery specified, we do nothing + + lines = [ ] + base_path = _image_path(generator) + in_path = path.join(base_path, metadata['gallery']) + template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE) + thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB) + thumbnail_prefix = generator.settings.get("") + resizer = _resizer(thumbnail_name, '?x?', base_path) + for dirpath, _, filenames in os.walk(in_path): + for filename in filenames: + if not filename.startswith('.'): + url = path.join(dirpath, filename).replace(base_path, "")[1:] + url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/') + logger.debug("GALLERY: {0}".format(url)) + thumbnail = resizer.get_thumbnail_name(filename) + thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/') + lines.append(template.format( + filename=filename, + url=url, + thumbnail=thumbnail, + )) + metadata['gallery_content'] = "\n".join(lines) + + +def register(): + signals.finalized.connect(resize_thumbnails) + signals.article_generator_context.connect(expand_gallery) diff --git a/old/pelican-plugins/summary/Readme.rst b/old/pelican-plugins/summary/Readme.rst new file mode 100644 index 0000000..29b3ed9 --- /dev/null +++ b/old/pelican-plugins/summary/Readme.rst @@ -0,0 +1,56 @@ +Summary +------- + +This plugin allows easy, variable length summaries directly embedded into the +body of your articles. It introduces two new settings: ``SUMMARY_BEGIN_MARKER`` +and ``SUMMARY_END_MARKER``: strings which can be placed directly into an article +to mark the beginning and end of a summary. When found, the standard +``SUMMARY_MAX_LENGTH`` setting will be ignored. The markers themselves will also +be removed from your articles before they are published. The default values +are ```` and ````. +For example:: + + Title: My super title + Date: 2010-12-03 10:20 + Tags: thats, awesome + Category: yeah + Slug: my-super-post + Author: Alexis Metaireau + + This is the content of my super blog post. + + and this content occurs after the summary. + +Here, the summary is taken to be the first line of the post. Because no +beginning marker was found, it starts at the top of the body. It is possible +to leave out the end marker instead, in which case the summary will start at the +beginning marker and continue to the end of the body. + +If no beginning or end marker is found, and if ``SUMMARY_USE_FIRST_PARAGRAPH`` +is enabled in the settings, the summary will be the first paragraph of the post. + +The plugin also sets a ``has_summary`` attribute on every article. It is True +for articles with an explicitly-defined summary, and False otherwise. (It is +also False for an article truncated by ``SUMMARY_MAX_LENGTH``.) Your templates +can use this e.g. to add a link to the full text at the end of the summary. + +reST example +~~~~~~~~~~~~ + +Inserting the markers into a reStructuredText document makes use of the +comment directive, because raw HTML is automatically escaped. The reST equivalent of the above Markdown example looks like this:: + + My super title + ############## + + :date: 2010-12-03 10:20 + :tags: thats, awesome + :category: yeah + :slug: my-super-post + :author: Alexis Metaireau + + This is the content of my super blog post. + + .. PELICAN_END_SUMMARY + + and this content occurs after the summary. diff --git a/old/pelican-plugins/summary/__init__.py b/old/pelican-plugins/summary/__init__.py new file mode 100644 index 0000000..afe9311 --- /dev/null +++ b/old/pelican-plugins/summary/__init__.py @@ -0,0 +1 @@ +from .summary import * diff --git a/old/pelican-plugins/summary/summary.py b/old/pelican-plugins/summary/summary.py new file mode 100644 index 0000000..0fd89d1 --- /dev/null +++ b/old/pelican-plugins/summary/summary.py @@ -0,0 +1,105 @@ +""" +Summary +------- + +This plugin allows easy, variable length summaries directly embedded into the +body of your articles. +""" + +from __future__ import unicode_literals +from pelican import signals +from pelican.generators import ArticlesGenerator, StaticGenerator, PagesGenerator +import re + +def initialized(pelican): + from pelican.settings import DEFAULT_CONFIG + DEFAULT_CONFIG.setdefault('SUMMARY_BEGIN_MARKER', + '') + DEFAULT_CONFIG.setdefault('SUMMARY_END_MARKER', + '') + DEFAULT_CONFIG.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False) + if pelican: + pelican.settings.setdefault('SUMMARY_BEGIN_MARKER', + '') + pelican.settings.setdefault('SUMMARY_END_MARKER', + '') + pelican.settings.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False) + +def extract_summary(instance): + # if summary is already specified, use it + # if there is no content, there's nothing to do + if hasattr(instance, '_summary'): + instance.has_summary = True + return + + if not instance._content: + instance.has_summary = False + return + + begin_marker = instance.settings['SUMMARY_BEGIN_MARKER'] + end_marker = instance.settings['SUMMARY_END_MARKER'] + use_first_paragraph = instance.settings['SUMMARY_USE_FIRST_PARAGRAPH'] + remove_markers = True + + content = instance._content + begin_summary = -1 + end_summary = -1 + if begin_marker: + begin_summary = content.find(begin_marker) + if end_marker: + end_summary = content.find(end_marker) + + if begin_summary == -1 and end_summary == -1 and use_first_paragraph: + begin_marker, end_marker = '

', '

' + remove_markers = False + begin_summary = content.find(begin_marker) + end_summary = content.find(end_marker) + + if begin_summary == -1 and end_summary == -1: + instance.has_summary = False + return + + # skip over the begin marker, if present + if begin_summary == -1: + begin_summary = 0 + else: + begin_summary = begin_summary + len(begin_marker) + + if end_summary == -1: + end_summary = None + + summary = content[begin_summary:end_summary] + + if remove_markers: + # remove the markers from the content + if begin_summary: + content = content.replace(begin_marker, '', 1) + if end_summary: + content = content.replace(end_marker, '', 1) + + summary = re.sub(r"", "", summary) + summary = re.sub(r"", "", summary) + + instance._content = content + instance._summary = summary + instance.has_summary = True + + +def run_plugin(generators): + for generator in generators: + if isinstance(generator, ArticlesGenerator): + for article in generator.articles: + extract_summary(article) + elif isinstance(generator, PagesGenerator): + for page in generator.pages: + extract_summary(page) + + +def register(): + signals.initialized.connect(initialized) + try: + signals.all_generators_finalized.connect(run_plugin) + except AttributeError: + # NOTE: This results in #314 so shouldn't really be relied on + # https://github.com/getpelican/pelican-plugins/issues/314 + signals.content_object_init.connect(extract_summary) diff --git a/old/pelican-plugins/summary/test_summary.py b/old/pelican-plugins/summary/test_summary.py new file mode 100644 index 0000000..6dda508 --- /dev/null +++ b/old/pelican-plugins/summary/test_summary.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +import unittest + +from jinja2.utils import generate_lorem_ipsum + +# generate one paragraph, enclosed with

+TEST_CONTENT = str(generate_lorem_ipsum(n=1)) +TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False) + + +from pelican.contents import Page +import pelican.settings + +import summary + +class TestSummary(unittest.TestCase): + def setUp(self): + super(TestSummary, self).setUp() + pelican.settings.DEFAULT_CONFIG['SUMMARY_MAX_LENGTH'] = None + pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = False + + summary.register() + summary.initialized(None) + self.page_kwargs = { + 'content': TEST_CONTENT, + 'context': { + 'localsiteurl': '', + }, + 'metadata': { + 'summary': TEST_SUMMARY, + 'title': 'foo bar', + 'author': 'Blogger', + }, + } + + def _copy_page_kwargs(self): + # make a deep copy of page_kwargs + page_kwargs = dict([(key, self.page_kwargs[key]) for key in + self.page_kwargs]) + for key in page_kwargs: + if not isinstance(page_kwargs[key], dict): + break + page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey]) + for subkey in page_kwargs[key]]) + + return page_kwargs + + def test_end_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + TEST_SUMMARY + '' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, TEST_SUMMARY + TEST_CONTENT) + + def test_begin_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + 'FOOBAR' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_CONTENT) + self.assertEqual(page.content, 'FOOBAR' + TEST_CONTENT) + + def test_begin_end_summary(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + page_kwargs['content'] = ( + 'FOOBAR' + TEST_SUMMARY + + '' + TEST_CONTENT) + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, 'FOOBAR' + TEST_SUMMARY + TEST_CONTENT) + + def test_use_first_paragraph(self): + page_kwargs = self._copy_page_kwargs() + del page_kwargs['metadata']['summary'] + pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = True + page_kwargs['content'] = '

' + TEST_SUMMARY + '

' + TEST_CONTENT + page = Page(**page_kwargs) + summary.extract_summary(page) + # test both the summary and the marker removal + self.assertEqual(page.summary, TEST_SUMMARY) + self.assertEqual(page.content, '

' + TEST_SUMMARY + '

' + TEST_CONTENT) + + +if __name__ == '__main__': + unittest.main() diff --git a/old/pelican-plugins/thumbnailer/Readme.md b/old/pelican-plugins/thumbnailer/Readme.md new file mode 100644 index 0000000..c3db910 --- /dev/null +++ b/old/pelican-plugins/thumbnailer/Readme.md @@ -0,0 +1,33 @@ +Thumbnail Creation of images +============================ + +This plugin creates thumbnails for all of the images found under a specific directory, in various thumbnail sizes. +It requires `PIL` to function properly since `PIL` is used to resize the images, and the thumbnail will only be re-built +if it doesn't already exist (to save processing time). + +Installation +------------- + +Set up like any other plugin, making sure to set `PLUGIN_PATH` and add `thumbnailer` to the `PLUGINS` list. + +[PIL](http://www.pythonware.com/products/pil/) or [Pillow](http://pillow.readthedocs.org/en/latest/installation.html#) +is required. If you choose Pillow, you need to install some additional +[external libraries](http://www.pythonware.com/products/pil/) to add support for image types like `jpg`. + +Configuration +------------- + +* `IMAGE_PATH` is the path to the image directory. It should reside inside your content directory, 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 size name to size specifications. + The generated filename will be `originalname_thumbnailname.ext` unless `THUMBNAIL_KEEP_NAME` is set. +* `THUMBNAIL_KEEP_NAME` is a Boolean that, if set, puts the file with the original name in a thumbnailname folder, named like the key in `THUMBNAIL_SIZES`. +* `THUMBNAIL_KEEP_TREE` is a Boolean that, if set, saves the image directory tree. +* `THUMBNAIL_INCLUDE_REGEX` is an optional string that is used as regular expression to restrict thumbnailing to matching files. By default all files not starting with a dot are respected. + +Sizes can be specified using any of the following formats: + +* wxh will resize to exactly wxh cropping as necessary to get that size +* wx? will resize so that the width is the specified size, and the height will scale to retain aspect ratio +* ?xh same as wx? but will height being a set size +* s is a shorthand for wxh where w=h diff --git a/old/pelican-plugins/thumbnailer/__init__.py b/old/pelican-plugins/thumbnailer/__init__.py new file mode 100644 index 0000000..20797b1 --- /dev/null +++ b/old/pelican-plugins/thumbnailer/__init__.py @@ -0,0 +1 @@ +from .thumbnailer import * \ No newline at end of file diff --git a/old/pelican-plugins/thumbnailer/test_data/expected_exact.jpg b/old/pelican-plugins/thumbnailer/test_data/expected_exact.jpg new file mode 100644 index 0000000..5819792 Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/expected_exact.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_data/expected_height.jpg b/old/pelican-plugins/thumbnailer/test_data/expected_height.jpg new file mode 100644 index 0000000..6459410 Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/expected_height.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_data/expected_square.jpg b/old/pelican-plugins/thumbnailer/test_data/expected_square.jpg new file mode 100644 index 0000000..de99e5b Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/expected_square.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_data/expected_width.jpg b/old/pelican-plugins/thumbnailer/test_data/expected_width.jpg new file mode 100644 index 0000000..9c2efc6 Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/expected_width.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_data/sample_image.jpg b/old/pelican-plugins/thumbnailer/test_data/sample_image.jpg new file mode 100644 index 0000000..cc83880 Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/sample_image.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg b/old/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg new file mode 100644 index 0000000..cc83880 Binary files /dev/null and b/old/pelican-plugins/thumbnailer/test_data/subdir/sample_image.jpg differ diff --git a/old/pelican-plugins/thumbnailer/test_thumbnails.py b/old/pelican-plugins/thumbnailer/test_thumbnails.py new file mode 100644 index 0000000..fdb0c34 --- /dev/null +++ b/old/pelican-plugins/thumbnailer/test_thumbnails.py @@ -0,0 +1,63 @@ +from thumbnailer import _resizer +from unittest import TestCase, main +import os +from PIL import Image + +class ThumbnailerTests(TestCase): + + def path(self, filename): + return os.path.join(self.img_path, filename) + + def setUp(self): + self.img_path = os.path.join(os.path.dirname(__file__), "test_data") + self.img = Image.open(self.path("sample_image.jpg")) + + def testSquare(self): + r = _resizer('square', '100', self.img_path) + output = r.resize(self.img) + self.assertEqual((100, 100), output.size) + + def testExact(self): + r = _resizer('exact', '250x100', self.img_path) + output = r.resize(self.img) + self.assertEqual((250, 100), output.size) + + def testWidth(self): + r = _resizer('aspect', '250x?', self.img_path) + output = r.resize(self.img) + self.assertEqual((250, 166), output.size) + + def testHeight(self): + r = _resizer('aspect', '?x250', self.img_path) + output = r.resize(self.img) + self.assertEqual((375, 250), output.size) + +class ThumbnailerFilenameTest(TestCase): + + def path(self, *parts): + return os.path.join(self.img_path, *parts) + + def setUp(self): + self.img_path = os.path.join(os.path.dirname(__file__), "test_data") + + def testRoot(self): + """Test a file that is in the root of img_path.""" + + r = _resizer('square', '100', self.img_path) + new_name = r.get_thumbnail_name(self.path('sample_image.jpg')) + self.assertEqual('sample_image_square.jpg', new_name) + + def testRootWithSlash(self): + r = _resizer('square', '100', self.img_path + '/') + new_name = r.get_thumbnail_name(self.path('sample_image.jpg')) + self.assertEqual('sample_image_square.jpg', new_name) + + def testSubdir(self): + """Test a file that is in a sub-directory of img_path.""" + + r = _resizer('square', '100', self.img_path) + new_name = r.get_thumbnail_name(self.path('subdir', 'sample_image.jpg')) + self.assertEqual('subdir/sample_image_square.jpg', new_name) + +if __name__=="__main__": + main() diff --git a/old/pelican-plugins/thumbnailer/thumbnailer.py b/old/pelican-plugins/thumbnailer/thumbnailer.py new file mode 100644 index 0000000..46f0ebd --- /dev/null +++ b/old/pelican-plugins/thumbnailer/thumbnailer.py @@ -0,0 +1,208 @@ +import os +import os.path as path +import re +from pelican import signals + +import logging +logger = logging.getLogger(__name__) + +try: + from PIL import Image, ImageOps + enabled = True +except ImportError: + logging.warning("Unable to load PIL, disabling thumbnailer") + enabled = False + +DEFAULT_IMAGE_DIR = "pictures" +DEFAULT_THUMBNAIL_DIR = "thumbnails" +DEFAULT_THUMBNAIL_SIZES = { + 'thumbnail_square': '150', + 'thumbnail_wide': '150x?', + 'thumbnail_tall': '?x150', +} +DEFAULT_TEMPLATE = """{filename}""" +DEFAULT_GALLERY_THUMB = "thumbnail_square" + +class _resizer(object): + """ Resizes based on a text specification, see readme """ + + REGEX = re.compile(r'(\d+|\?)x(\d+|\?)') + + def __init__(self, name, spec, root): + self._name = name + self._spec = spec + # The location of input images from _image_path. + self._root = root + + def _null_resize(self, w, h, image): + return image + + def _exact_resize(self, w, h, image): + retval = ImageOps.fit(image, (w,h), Image.BICUBIC) + return retval + + def _aspect_resize(self, w, h, image): + retval = image.copy() + retval.thumbnail((w, h), Image.ANTIALIAS) + + return retval + + def resize(self, image): + resizer = self._null_resize + + # Square resize and crop + if 'x' not in self._spec: + resizer = self._exact_resize + targetw = int(self._spec) + targeth = targetw + else: + matches = self.REGEX.search(self._spec) + tmpw = matches.group(1) + tmph = matches.group(2) + + # Full Size + if tmpw == '?' and tmph == '?': + targetw = image.size[0] + targeth = image.size[1] + resizer = self._null_resize + + # Set Height Size + if tmpw == '?': + targetw = image.size[0] + targeth = int(tmph) + resizer = self._aspect_resize + + # Set Width Size + elif tmph == '?': + targetw = int(tmpw) + targeth = image.size[1] + resizer = self._aspect_resize + + # Scale and Crop + else: + targetw = int(tmpw) + targeth = int(tmph) + resizer = self._exact_resize + + logging.debug("Using resizer {0}".format(resizer.__name__)) + return resizer(targetw, targeth, image) + + def get_thumbnail_name(self, in_path): + # Find the partial path + filename beyond the input image directory. + prefix = path.commonprefix([in_path, self._root]) + new_filename = in_path[len(prefix) + 1:] + + # Generate the new filename. + (basename, ext) = path.splitext(new_filename) + return "{0}_{1}{2}".format(basename, self._name, ext) + + 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 supported by PIL + :param out_path: path to the directory root for the outputted thumbnails to be stored + :return: None + """ + if keep_filename: + filename = path.join(out_path, path.basename(in_path)) + else: + filename = path.join(out_path, self.get_thumbnail_name(in_path)) + out_path = path.dirname(filename) + if not path.exists(out_path): + os.makedirs(out_path) + if not path.exists(filename): + try: + image = Image.open(in_path) + thumbnail = self.resize(image) + thumbnail.save(filename) + logger.info("Generated Thumbnail {0}".format(path.basename(filename))) + except IOError: + logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename))) + + +def resize_thumbnails(pelican): + """ Resize a directory tree full of images into thumbnails + + :param pelican: The pelican instance + :return: None + """ + global enabled + if not enabled: + return + + in_path = _image_path(pelican) + + include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX') + if include_regex: + pattern = re.compile(include_regex) + is_included = lambda name: pattern.match(name) + else: + is_included = lambda name: not name.startswith('.') + + sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES) + resizers = dict((k, _resizer(k, v, in_path)) for k,v in sizes.items()) + logger.debug("Thumbnailer Started") + for dirpath, _, filenames in os.walk(in_path): + for filename in filenames: + if is_included(filename): + for name, resizer in resizers.items(): + in_filename = path.join(dirpath, filename) + out_path = get_out_path(pelican, in_path, in_filename, name) + resizer.resize_file_to( + in_filename, + out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME')) + + +def get_out_path(pelican, in_path, in_filename, name): + base_out_path = path.join(pelican.settings['OUTPUT_PATH'], + pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR)) + logger.debug("Processing thumbnail {0}=>{1}".format(in_filename, name)) + if pelican.settings.get('THUMBNAIL_KEEP_NAME', False): + if pelican.settings.get('THUMBNAIL_KEEP_TREE', False): + return path.join(base_out_path, name, path.dirname(path.relpath(in_filename, in_path))) + else: + return path.join(base_out_path, name) + else: + return base_out_path + + +def _image_path(pelican): + return path.join(pelican.settings['PATH'], + pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/') + + +def expand_gallery(generator, metadata): + """ Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH + + :param pelican: The pelican instance + :return: None + """ + if "gallery" not in metadata or metadata['gallery'] is None: + return # If no gallery specified, we do nothing + + lines = [ ] + base_path = _image_path(generator) + in_path = path.join(base_path, metadata['gallery']) + template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE) + thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB) + thumbnail_prefix = generator.settings.get("") + resizer = _resizer(thumbnail_name, '?x?', base_path) + for dirpath, _, filenames in os.walk(in_path): + for filename in filenames: + if not filename.startswith('.'): + url = path.join(dirpath, filename).replace(base_path, "")[1:] + url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/') + logger.debug("GALLERY: {0}".format(url)) + thumbnail = resizer.get_thumbnail_name(filename) + thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/') + lines.append(template.format( + filename=filename, + url=url, + thumbnail=thumbnail, + )) + metadata['gallery_content'] = "\n".join(lines) + + +def register(): + signals.finalized.connect(resize_thumbnails) + signals.article_generator_context.connect(expand_gallery)