Browse Source

enable plugins

pull/2/head
rra 1 year ago
parent
commit
11986f6e9b
14 changed files with 686 additions and 2 deletions
  1. +2
    -2
      pelicanconf.py
  2. +1
    -0
      plugins/addressable_paragraphs/__init__.py
  3. +48
    -0
      plugins/addressable_paragraphs/addressable_paragraphs.py
  4. +137
    -0
      plugins/extract_toc/README.md
  5. +1
    -0
      plugins/extract_toc/__init__.py
  6. +64
    -0
      plugins/extract_toc/extract_toc.py
  7. +49
    -0
      plugins/representative_image/Readme.md
  8. +1
    -0
      plugins/representative_image/__init__.py
  9. +58
    -0
      plugins/representative_image/representative_image.py
  10. +61
    -0
      plugins/representative_image/test_representative_image.py
  11. +56
    -0
      plugins/summary/Readme.rst
  12. +1
    -0
      plugins/summary/__init__.py
  13. +111
    -0
      plugins/summary/summary.py
  14. +96
    -0
      plugins/summary/test_summary.py

+ 2
- 2
pelicanconf.py View File

@@ -35,8 +35,8 @@ EXTRA_PATH_METADATA = {
}

#Pelican Plugins & Markdown extensions
#PLUGIN_PATHS = ['plugins']
#PLUGINS = ['extract_toc', 'summary','representative_image','addressable_paragraphs']
PLUGIN_PATHS = ['plugins']
PLUGINS = ['extract_toc', 'summary','representative_image','addressable_paragraphs']
MARKDOWN = {'extensions':
['markdown.extensions.codehilite',
'markdown.extensions.extra',


+ 1
- 0
plugins/addressable_paragraphs/__init__.py View File

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

+ 48
- 0
plugins/addressable_paragraphs/addressable_paragraphs.py View File

@@ -0,0 +1,48 @@
"""
Addressable Paragraphs
------------------------
In converting from .md to .html, images are wrapped in <p> tags.
This plugin gives those paragraphs the 'img' class for styling enhancements.
In case there is any description immediately following that image, it is wrapped in another paragraph with the 'caption' class.

Copyright (C) 2018 Roel Roscam Abbing

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from __future__ import unicode_literals
from pelican import signals
from bs4 import BeautifulSoup

def content_object_init(instance):

if instance._content is not None:
content = instance._content
soup = BeautifulSoup(content, 'html.parser')

for p in soup(['p', 'object']):
if p.findChild('img'):
p.attrs['class'] = 'img'
caption = soup.new_tag('p',**{'class':'caption'})
if len(p.contents) > 1: #if we have more than just the <img> tag
for i in reversed(p.contents):
if i.name != 'img': #if it is not an <img> tag
caption.insert(0,i.extract())
p.insert_after(caption)

instance._content = soup.decode()


def register():
signals.content_object_init.connect(content_object_init)

+ 137
- 0
plugins/extract_toc/README.md View File

@@ -0,0 +1,137 @@
Extract Table of Content
========================

A Pelican plugin to extract table of contents (ToC) from `article.content` and
place it in its own `article.toc` variable for use in templates.

Copyright (c) Talha Mansoor

Author | Talha Mansoor
----------------|-----
Author Email | talha131@gmail.com
Author Homepage | http://onCrashReboot.com
Github Account | https://github.com/talha131


Acknowledgement
---------------

Thanks to [Avaris](https://github.com/avaris) for going out of the way to help
me fix Unicode issues and doing a thorough code review.

Thanks to [gw0](http://gw.tnode.com/) for adding Pandoc reader support.


Why do you need it?
===================

Pelican can generate ToC of reST and Markdown files, using markup's respective
directive and extension. Such ToC is generated and placed at the beginning of
`article.content` like a string. Consequently it can not be placed anywhere
else on the page (eg. `<nav>` HTML5 tag, in header, or at the end of your
article's contents).

To solve this problem, this plugin extracts ToC from `article.content` and
places it in its own `article.toc` variable for use in templates.


Requirements
============

`extract_toc` requires BeautifulSoup.

```bash
pip install beautifulsoup4
```


How to Use
==========

This plugin works by extracting the first occurrence of enclosed in:

- `<div class="toc">` for the default Markdown reader
- `<div class="contents topic">` for the default reStructuredText reader
- `<nav class="TOC">` for the Pandoc reader

If ToC appears in your article at more than one places, `extract_toc` will
remove only the first occurrence. You shouldn't probably need to have multiple
ToC in your article. In case you need to display it multiple times, you can
print it via your template.


Template example
----------------

Add something like this to your Pelican templates if missing:

```python
{% if article.toc %}
<nav class="toc">
{{ article.toc }}
</nav>
{% endif %}
```


reStructuredText reader
-----------------------

To add a table of contents to your reStructuredText document (`.rst`) you need to add a `.. contents::` directive to its beginning. See the [docutils documentation](http://docutils.sourceforge.net/docs/ref/rst/directives.html#table-of-contents) for more details.

```rst
My super title
##############

:date: 2010-10-03
:tags: thats, awesome

.. contents::
..
1 Head 1
1.1 Head 2
2 Head 3
3 head 4

Heading 1
---------

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.
```


Markdown reader
---------------

To enable table of contents generation for the Markdown reader you need to set `MD_EXTENSIONS = (['toc'])` in your Pelican configuration file.

To add a table of contents to your Markdown document (`.md`) you need to place the `[TOC]` marker to its beginning. See the [Python Markdown documentation](http://pythonhosted.org/Markdown/extensions/toc.html) for more details.

```markdown
title: My super title
date: 4-4-2013
tags: thats, awesome

[TOC]

# Heading 1 #

Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.
```


Pandoc reader
-------------

To enable table of contents generation for the Pandoc reader you need to set `PANDOC_ARGS = (['--toc', '--template=pandoc-template-toc'])` in your Pelican configuration file.

Contents of the Pandoc template file `pandoc-template-toc.html5`:

```html
$if(toc)$
<nav id="TOC">
$toc$
</nav>
$endif$
$body$
```

+ 1
- 0
plugins/extract_toc/__init__.py View File

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

+ 64
- 0
plugins/extract_toc/extract_toc.py View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
Extract Table of Content
========================

A Pelican plugin to extract table of contents (ToC) from `article.content` and
place it in its own `article.toc` variable for use in templates.
"""

from os import path
from bs4 import BeautifulSoup
from pelican import signals, readers, contents
import logging

logger = logging.getLogger(__name__)


def extract_toc(content):
if isinstance(content, contents.Static):
return

soup = BeautifulSoup(content._content, 'html.parser')
filename = content.source_path
extension = path.splitext(filename)[1][1:]
toc = None

# default Markdown reader
if not toc and readers.MarkdownReader.enabled and extension in readers.MarkdownReader.file_extensions:
toc = soup.find('div', class_='toc')
if toc:
toc.extract()

# default reStructuredText reader
if not toc and readers.RstReader.enabled and extension in readers.RstReader.file_extensions:
toc = soup.find('div', class_='contents topic')
if toc:
toc.extract()
tag = BeautifulSoup(str(toc), 'html.parser')
tag.div['class'] = 'toc'
tag.div['id'] = ''
p = tag.find('p', class_='topic-title first')
if p:
p.extract()
toc = tag

# Pandoc reader (markdown and other formats)
if 'pandoc_reader' in content.settings['PLUGINS']:
try:
from pandoc_reader import PandocReader
except ImportError:
PandocReader = False
if not toc and PandocReader and PandocReader.enabled and extension in PandocReader.file_extensions:
toc = soup.find('nav', id='TOC')

if toc:
toc.extract()
content._content = soup.decode()
content.toc = toc.decode()
if content.toc.startswith('<html>'):
content.toc = content.toc[12:-14]


def register():
signals.content_object_init.connect(extract_toc)

+ 49
- 0
plugins/representative_image/Readme.md View File

@@ -0,0 +1,49 @@
# Summary

This plugin extracts a representative image (i.e, featured image) from the article's summary or content if not specified in the metadata.

The plugin also removes any images from the summary after extraction to avoid duplication.

It allows the flexibility on where and how to display the featured image of an article together with its summary in a template page. For example, the article metadata can be displayed in thumbnail format, in which there is a short summary and an image. The layout of the summary and the image can be varied for aesthetical purpose. It doesn't have to depend on article's content format.

## Installation

This plugin requires BeautifulSoup:

```bash
pip install beautifulsoup4
```

To enable, add the following to your settings.py:

```python
PLUGIN_PATH = 'path/to/pelican-plugins'
PLUGINS = ["representative_image"]
```

`PLUGIN_PATH` can be a path relative to your settings file or an absolute path.

## Usage

To override the default behavior of selecting the first image in the article's summary or content, set the image property the article's metadata to the URL of the image to display, e.g:

```markdown
Title: My super title
Date: 2010-12-03 10:20
Category: Python
Tags: pelican, publishing
Slug: my-super-post
Author: Alexis Metaireau
Summary: Short version for index and feeds
Image: /images/my-super-image.png

Article content...
```

To include a representative image in a page add the following to the template:

```html
{% if article.featured_image %}
<img src="{{ article.featured_image }}">
{% endif %}
```

+ 1
- 0
plugins/representative_image/__init__.py View File

@@ -0,0 +1 @@
from .representative_image import * # noqa

+ 58
- 0
plugins/representative_image/representative_image.py View File

@@ -0,0 +1,58 @@
import six
from bs4 import BeautifulSoup

from pelican import signals
from pelican.contents import Article, Page
from pelican.generators import ArticlesGenerator


def images_extraction(instance):
representativeImage = None
if type(instance) in (Article, Page):
if 'image' in instance.metadata:
representativeImage = instance.metadata['image']

# Process Summary:
# If summary contains images, extract one to be the representativeImage
# and remove images from summary
soup = BeautifulSoup(instance.summary, 'html.parser')
images = soup.find_all('img')
for i in images:
if not representativeImage:
representativeImage = i['src']
i.extract()
if len(images) > 0:
# set _summary field which is based on metadata. summary field is
# only based on article's content and not settable
instance._summary = six.text_type(soup)

# If there are no image in summary, look for it in the content body
if not representativeImage:
soup = BeautifulSoup(instance._content, 'html.parser')
imageTag = soup.find('img')
if imageTag:
representativeImage = imageTag['src']

# Set the attribute to content instance
instance.featured_image = representativeImage
instance.featured_alt = instance.metadata.get('alt', None)
instance.featured_link = instance.metadata.get('link', None)
instance.featured_caption = instance.metadata.get('caption', None)


def run_plugin(generators):
for generator in generators:
if isinstance(generator, ArticlesGenerator):
for article in generator.articles:
images_extraction(article)
for translation in article.translations:
images_extraction(translation)


def register():
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(images_extraction)

+ 61
- 0
plugins/representative_image/test_representative_image.py View File

@@ -0,0 +1,61 @@
#!/bin/sh
import unittest

import representative_image
from jinja2.utils import generate_lorem_ipsum
from pelican.contents import Article

# Generate content with image
TEST_CONTENT_IMAGE_URL = 'https://testimage.com/test.jpg'
TEST_CONTENT = str(generate_lorem_ipsum(n=3, html=True)) + '<img src="' + TEST_CONTENT_IMAGE_URL + '"/>'+ str(generate_lorem_ipsum(n=2,html=True)) # noqa
TEST_SUMMARY_IMAGE_URL = 'https://testimage.com/summary.jpg'
TEST_SUMMARY_WITHOUTIMAGE = str(generate_lorem_ipsum(n=1, html=True))
TEST_SUMMARY_WITHIMAGE = TEST_SUMMARY_WITHOUTIMAGE + '<img src="' + TEST_SUMMARY_IMAGE_URL + '"/>' # noqa
TEST_CUSTOM_IMAGE_URL = 'https://testimage.com/custom.jpg'


class TestRepresentativeImage(unittest.TestCase):

def setUp(self):
super(TestRepresentativeImage, self).setUp()
representative_image.register()

def test_extract_image_from_content(self):
args = {
'content': TEST_CONTENT,
'metadata': {
'summary': TEST_SUMMARY_WITHOUTIMAGE,
},
}

article = Article(**args)
self.assertEqual(article.featured_image, TEST_CONTENT_IMAGE_URL)

def test_extract_image_from_summary(self):
args = {
'content': TEST_CONTENT,
'metadata': {
'summary': TEST_SUMMARY_WITHIMAGE,
},
}

article = Article(**args)
self.assertEqual(article.featured_image, TEST_SUMMARY_IMAGE_URL)
self.assertEqual(article.summary, TEST_SUMMARY_WITHOUTIMAGE)

def test_extract_image_from_summary_with_custom_image(self):
args = {
'content': TEST_CONTENT,
'metadata': {
'summary': TEST_SUMMARY_WITHIMAGE,
'image': TEST_CUSTOM_IMAGE_URL,
},
}

article = Article(**args)
self.assertEqual(article.featured_image, TEST_CUSTOM_IMAGE_URL)
self.assertEqual(article.summary, TEST_SUMMARY_WITHOUTIMAGE)


if __name__ == '__main__':
unittest.main()

+ 56
- 0
plugins/summary/Readme.rst View File

@@ -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 ``<!-- PELICAN_BEGIN_SUMMARY -->`` and ``<!-- PELICAN_END_SUMMARY -->``.
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.
<!-- PELICAN_END_SUMMARY -->
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.

+ 1
- 0
plugins/summary/__init__.py View File

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

+ 111
- 0
plugins/summary/summary.py View File

@@ -0,0 +1,111 @@
"""
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',
'<!-- PELICAN_BEGIN_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False)
if pelican:
pelican.settings.setdefault('SUMMARY_BEGIN_MARKER',
'<!-- PELICAN_BEGIN_SUMMARY -->')
pelican.settings.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
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') or 'summary' in instance.metadata:
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._update_content(instance._content, instance.settings['SITEURL'])
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 = '<p>', '</p>'
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"<div.*>", "", summary)
summary = re.sub(r"</div>", "", summary)

instance._content = content
# default_status was added to Pelican Content objects after 3.7.1.
# Its use here is strictly to decide on how to set the summary.
# There's probably a better way to do this but I couldn't find it.
if hasattr(instance, 'default_status'):
instance.metadata['summary'] = summary
else:
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)

+ 96
- 0
plugins/summary/test_summary.py View File

@@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-

import unittest

from jinja2.utils import generate_lorem_ipsum

# generate one paragraph, enclosed with <p>
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 + '<!-- PELICAN_END_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<!-- PELICAN_BEGIN_SUMMARY -->' + 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<!-- PELICAN_BEGIN_SUMMARY -->' + TEST_SUMMARY +
'<!-- PELICAN_END_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'] = '<p>' + TEST_SUMMARY + '</p>' + 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, '<p>' + TEST_SUMMARY + '</p>' + TEST_CONTENT)


if __name__ == '__main__':
unittest.main()

Loading…
Cancel
Save