Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c19fb997b | |||
| fb53d674a7 | |||
| 0574098ee5 | |||
| a8830b8825 | |||
|
|
7b3851a62b | ||
|
|
d47fdc1ea5 | ||
|
|
932537f26d | ||
|
|
8497de9413 | ||
|
|
47f5050463 | ||
|
|
9d9f272895 | ||
|
|
624734724b | ||
|
|
7f52729121 | ||
|
|
620d176f30 | ||
|
|
5d1b77a238 | ||
|
|
c15a9dc250 | ||
|
|
2b37362873 | ||
|
|
4252f8f230 | ||
|
|
42f90c94fb | ||
|
|
97e2d7549b | ||
|
|
3a804aec12 | ||
|
|
1d4d19aca5 | ||
|
|
9750c13665 | ||
|
|
046eaadd61 | ||
|
|
c96846c201 | ||
|
|
02a4645912 | ||
|
|
24fce038c3 | ||
|
|
98e2f16059 | ||
|
|
235c9e3b91 | ||
|
|
1a20cc84b2 | ||
|
|
057ab2b4b2 | ||
|
|
20f8a24971 | ||
|
|
90c915ef44 | ||
|
|
fb570c528b | ||
|
|
ad188b05c2 | ||
|
|
657505cf35 | ||
|
|
db4683c515 | ||
|
|
dc96679d03 | ||
|
|
b95142b415 | ||
|
|
c38851cf9c | ||
|
|
4386da4adf | ||
|
|
1100ccd872 | ||
|
|
8052ea9f9e | ||
|
|
3bcb54ac55 | ||
|
|
8ca6e5e514 | ||
|
|
f93764f737 | ||
|
|
0b69e1bdb2 | ||
|
|
47dc3e9747 | ||
|
|
9b47bbf67d | ||
|
|
5af9d08c06 | ||
|
|
7ba212ecc4 | ||
|
|
64dca25e86 | ||
|
|
5d3ab5b964 | ||
|
|
126a1290f3 | ||
|
|
8ac0e61720 | ||
|
|
474b7fd2ff | ||
|
|
30657eca3a | ||
|
|
b86e17607c | ||
|
|
5e7bf54a95 | ||
|
|
c85ea081c0 | ||
|
|
b412bb1c56 | ||
|
|
8d6a4a9d79 | ||
|
|
f7178ae0e9 | ||
|
|
82597bb62e | ||
|
|
b35996ca43 | ||
|
|
3bf1ac5e98 | ||
|
|
5d0a5d2ec5 | ||
|
|
e1fd114a0e | ||
|
|
d7cb218e46 | ||
|
|
c6258ecf95 | ||
|
|
1876bf4438 | ||
|
|
b898230913 | ||
|
|
073c997eea | ||
|
|
0a4bf7b9b0 | ||
|
|
6f7dafaee6 | ||
|
|
3d65e38700 | ||
|
|
88cd8305d2 | ||
|
|
6feff11273 | ||
|
|
0665333e8e | ||
|
|
dda48f6428 | ||
|
|
e76f3cf914 | ||
|
|
16437e1a05 | ||
|
|
aa268e4883 | ||
|
|
a73d9dddcb | ||
|
|
84b1962afc | ||
|
|
56ca351f98 | ||
|
|
ce06f87a43 | ||
|
|
614ea30efd | ||
|
|
ca377d918e | ||
|
|
5e07f2dfc6 | ||
|
|
d13465d1b2 | ||
|
|
a0797a498b | ||
|
|
02fb072aa3 | ||
|
|
dcc95a4611 | ||
|
|
b86be87c61 | ||
|
|
250cef84f0 | ||
|
|
f3549dd296 | ||
|
|
2ca07107d4 | ||
|
|
614e670026 | ||
|
|
1793ebbf79 | ||
|
|
9a9f9bdb83 | ||
|
|
2e98bd8bdf | ||
|
|
1f8b7e0f87 | ||
|
|
8f69faa77c | ||
|
|
5cb7b449f7 | ||
|
|
1facc9468b | ||
|
|
b268db7312 | ||
|
|
48b990c0cd | ||
|
|
5e6307d9e2 | ||
|
|
211d835f30 | ||
|
|
c4d7a761c8 | ||
|
|
95d7ebb470 | ||
|
|
f30a68fbf4 | ||
|
|
dacc64023d | ||
|
|
7e832f780e | ||
|
|
8de762f97d | ||
|
|
ddcd9ee4f6 | ||
|
|
30638d8ac4 | ||
|
|
1cf85f7a06 | ||
|
|
1c6fa7893e | ||
|
|
9a952032f4 | ||
|
|
a5269f599a | ||
|
|
943e590c24 | ||
|
|
f5d0f4ecb9 | ||
|
|
36bdaae588 | ||
|
|
2a564dce1a | ||
|
|
bd12282f92 | ||
|
|
64b03f9d43 | ||
|
|
ce58bd5704 | ||
|
|
3a8f323563 | ||
|
|
bd455c3dcc | ||
|
|
9aae49360a | ||
|
|
e00e7d35c9 | ||
|
|
507443b3b2 | ||
|
|
c6003cca22 | ||
|
|
744c1620f5 | ||
|
|
baa90d815e | ||
|
|
c5788c8e3a | ||
|
|
335c9c63e1 | ||
|
|
6ae722fefd | ||
|
|
df9b772f3e | ||
|
|
17980a031f | ||
|
|
936bb03d7e | ||
|
|
0553dbe36d | ||
|
|
bfaa9e690d | ||
|
|
c21c932433 | ||
|
|
370bc6c124 | ||
|
|
ee2684ea4b | ||
|
|
3b4737325e | ||
|
|
3df48a1b77 | ||
|
|
37809a83a8 | ||
|
|
3ba3bf1f9b | ||
|
|
0f55bd63f2 | ||
|
|
0cd5586664 | ||
|
|
a5fa7ddc0e | ||
|
|
800a53c1f8 | ||
|
|
d09e022bfd | ||
|
|
dd77741bf0 | ||
|
|
3f47cbd468 | ||
|
|
3e4dcc293f | ||
|
|
64e35fe195 | ||
|
|
13a8ea5d5f | ||
|
|
fb421f5ac5 | ||
|
|
532a7b01a2 | ||
|
|
e9e34856bf | ||
|
|
1e4ad04c61 | ||
|
|
768e02b2d5 | ||
|
|
10094868f2 | ||
|
|
8d1f6a940d | ||
|
|
8bea5f5670 | ||
|
|
db0454de1b | ||
|
|
76e799641f | ||
|
|
8a846c5888 | ||
|
|
92746659b1 | ||
|
|
d726fd9b05 | ||
|
|
506f265214 | ||
|
|
29b9e8d3ae | ||
|
|
4a37cee555 | ||
|
|
24b1332753 | ||
|
|
7013a82063 | ||
|
|
470c8237d1 | ||
|
|
562c57c4e9 | ||
|
|
0cafe10d16 | ||
|
|
0d30333281 | ||
|
|
7b4d03a204 | ||
|
|
1bda804dc2 | ||
|
|
ecf7c01825 | ||
|
|
b01cb35645 | ||
|
|
fecb834abb | ||
|
|
a20ca76b7c | ||
|
|
6cd4482143 | ||
|
|
e36dae6c11 | ||
|
|
9e416a73b2 | ||
|
|
4964604e7c | ||
|
|
1f2c383b7e | ||
|
|
d3495d0249 | ||
|
|
1ef014c91d | ||
|
|
4643ab7186 | ||
|
|
c545c9e56d | ||
|
|
499ee9890a | ||
|
|
59def6018d | ||
|
|
c9823a72f7 | ||
|
|
250b08a389 | ||
|
|
2ad2132261 | ||
|
|
6f8d398bb6 | ||
|
|
2c4f75e008 | ||
|
|
5af2ff3c81 | ||
|
|
3df9768a04 | ||
|
|
92e448340c | ||
|
|
ce11ec8b20 | ||
|
|
5977160bb6 | ||
|
|
76d27c2876 | ||
|
|
e0e303adf3 | ||
|
|
d091f2780b | ||
|
|
d5e4d179e9 | ||
|
|
c0e756209d | ||
|
|
a6ad0e66e6 | ||
|
|
790fe92e3e | ||
|
|
a934cde442 | ||
|
|
87f81ab4f5 | ||
|
|
1ae1833864 | ||
|
|
57d62ce99f | ||
|
|
fec36746b3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*.pyc
|
||||
*.log
|
||||
*.log
|
||||
*~
|
||||
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
[submodule "pelican_youtube"]
|
||||
path = pelican_youtube
|
||||
url = https://github.com/kura/pelican_youtube.git
|
||||
[submodule "pelican_vimeo"]
|
||||
path = pelican_vimeo
|
||||
url = https://github.com/kura/pelican_vimeo.git
|
||||
[submodule "cjk-auto-spacing"]
|
||||
path = cjk-auto-spacing
|
||||
url = https://github.com/yuex/cjk-auto-spacing.git
|
||||
@@ -13,7 +13,7 @@ functions, including:
|
||||
* CSS compiler (``less``, ``sass``, ...)
|
||||
* JS minifier (``uglifyjs``, ``yui_js``, ``closure``, ...)
|
||||
|
||||
Others filters include gzip compression, integration of images in CSS via data
|
||||
Others filters include CSS URL rewriting, integration of images in CSS via data
|
||||
URIs, and more. Webassets can also append a version identifier to your asset
|
||||
URL to convince browsers to download new versions of your assets when you use
|
||||
far-future expires headers. Please refer to the `Webassets documentation`_ for
|
||||
@@ -51,11 +51,11 @@ Another example for Javascript:
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% assets filters="uglifyjs,gzip", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %}
|
||||
{% assets filters="uglifyjs", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %}
|
||||
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
|
||||
The above will produce a minified and gzipped JS file:
|
||||
The above will produce a minified JS file:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
@@ -64,6 +64,19 @@ The above will produce a minified and gzipped JS file:
|
||||
Pelican's debug mode is propagated to Webassets to disable asset packaging
|
||||
and instead work with the uncompressed assets.
|
||||
|
||||
If you need to create named bundles (for example, if you need to compile SASS
|
||||
files before minifying with other CSS files), you can use the ``ASSET_BUNDLES``
|
||||
variable in your settings file. This is an ordered sequence of 3-tuples, where
|
||||
the 3-tuple is defined as ``(name, args, kwargs)``. This tuple is passed to the
|
||||
`environment's register() method`_. The following will compile two SCSS files
|
||||
into a named bundle, using the ``pyscss`` filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ASSET_BUNDLES = (
|
||||
('scss', ['colors.scss', 'main.scss'], {'filters': 'pyscss'}),
|
||||
)
|
||||
|
||||
Many of Webasset's available compilers have additional configuration options
|
||||
(i.e. 'Less', 'Sass', 'Stylus', 'Closure_js'). You can pass these options to
|
||||
Webassets using the ``ASSET_CONFIG`` in your settings file.
|
||||
@@ -76,5 +89,18 @@ LessCSS's binary:
|
||||
ASSET_CONFIG = (('closure_compressor_optimization', 'WHITESPACE_ONLY'),
|
||||
('less_bin', 'lessc.cmd'), )
|
||||
|
||||
If you wish to place your assets in locations other than the theme output
|
||||
directory, you can use ``ASSET_SOURCE_PATHS`` in your settings file to provide
|
||||
webassets with a list of additional directories to search, relative to the
|
||||
theme's top-level directory. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ASSET_SOURCE_PATHS = (
|
||||
'vendor/css',
|
||||
'scss',
|
||||
)
|
||||
|
||||
.. _Webassets: https://github.com/miracle2k/webassets
|
||||
.. _Webassets documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html
|
||||
.. _environment's register() method: http://webassets.readthedocs.org/en/latest/environment.html#registering-bundles
|
||||
|
||||
@@ -38,17 +38,30 @@ def add_jinja2_ext(pelican):
|
||||
def create_assets_env(generator):
|
||||
"""Define the assets environment and pass it to the generator."""
|
||||
|
||||
assets_url = 'theme/'
|
||||
assets_src = os.path.join(generator.output_path, 'theme')
|
||||
generator.env.assets_environment = Environment(assets_src, assets_url)
|
||||
theme_static_dir = generator.settings['THEME_STATIC_DIR']
|
||||
assets_src = os.path.join(generator.output_path, theme_static_dir)
|
||||
generator.env.assets_environment = Environment(
|
||||
assets_src, theme_static_dir)
|
||||
|
||||
if 'ASSET_CONFIG' in generator.settings:
|
||||
for item in generator.settings['ASSET_CONFIG']:
|
||||
generator.env.assets_environment.config[item[0]] = item[1]
|
||||
|
||||
if 'ASSET_BUNDLES' in generator.settings:
|
||||
for name, args, kwargs in generator.settings['ASSET_BUNDLES']:
|
||||
generator.env.assets_environment.register(name, *args, **kwargs)
|
||||
|
||||
if logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG":
|
||||
generator.env.assets_environment.debug = True
|
||||
|
||||
if 'ASSET_SOURCE_PATHS' in generator.settings:
|
||||
# the default load path gets overridden if additional paths are
|
||||
# specified, add it back
|
||||
generator.env.assets_environment.append_path(assets_src)
|
||||
for path in generator.settings['ASSET_SOURCE_PATHS']:
|
||||
full_path = os.path.join(generator.theme, path)
|
||||
generator.env.assets_environment.append_path(full_path)
|
||||
|
||||
|
||||
def register():
|
||||
"""Plugin registration."""
|
||||
|
||||
@@ -13,13 +13,15 @@ TODO: Need to add a test.py for this plugin.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
from os import path, access, R_OK
|
||||
|
||||
from pelican import signals
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from PIL import Image
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def content_object_init(instance):
|
||||
|
||||
@@ -29,9 +31,30 @@ def content_object_init(instance):
|
||||
|
||||
if 'img' in content:
|
||||
for img in soup('img'):
|
||||
# TODO: Pretty sure this isn't the right way to do this, too hard coded.
|
||||
# There must be a setting that I should be using?
|
||||
src = instance.settings['PATH'] + '/images/' + os.path.split(img['src'])[1]
|
||||
logger.debug('Better Fig. PATH: %s', instance.settings['PATH'])
|
||||
logger.debug('Better Fig. img.src: %s', img['src'])
|
||||
|
||||
img_path, img_filename = path.split(img['src'])
|
||||
|
||||
logger.debug('Better Fig. img_path: %s', img_path)
|
||||
logger.debug('Better Fig. img_fname: %s', img_filename)
|
||||
|
||||
# Strip off {filename}, |filename| or /static
|
||||
if img_path.startswith(('{filename}', '|filename|')):
|
||||
img_path = img_path[10:]
|
||||
elif img_path.startswith('/static'):
|
||||
img_path = img_path[7:]
|
||||
else:
|
||||
logger.warning('Better Fig. Error: img_path should start with either {filename}, |filename| or /static')
|
||||
|
||||
# Build the source image filename
|
||||
src = instance.settings['PATH'] + img_path + '/' + img_filename
|
||||
|
||||
logger.debug('Better Fig. src: %s', src)
|
||||
if not (path.isfile(src) and access(src, R_OK)):
|
||||
logger.error('Better Fig. Error: image not found: {}'.format(src))
|
||||
|
||||
# Open the source image and query dimensions; build style string
|
||||
im = Image.open(src)
|
||||
extra_style = 'width: {}px; height: auto;'.format(im.size[0])
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* pip install pillow beautifulsoup4
|
||||
|
||||
Summary
|
||||
===========
|
||||
|
||||
@@ -47,4 +52,4 @@ or this, if RESPONSIVE_IMAGES = True:
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
cjk-auto-spacing
Submodule
1
cjk-auto-spacing
Submodule
Submodule cjk-auto-spacing added at 92346597b8
34
clean_summary/README.md
Normal file
34
clean_summary/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
#Clean Summary Plugin#
|
||||
|
||||
Plugin to clean your summary of excess images. Images can take up much more
|
||||
space than text and lead to summaries being different sizes on archive and
|
||||
index pages. With this plugin you can specify a maximum number of images that
|
||||
will appear in your summaries.
|
||||
|
||||
There is also an option to include a minimum of one image.
|
||||
|
||||
##Settings##
|
||||
|
||||
This plugin has two settings. `CLEAN_SUMMARY_MAXIMUM` which takes an int, and
|
||||
`CLEAN_SUMMARY_MINIMUM_ONE` which takes a boolean. They default to 0 and False.
|
||||
|
||||
`CLEAN_SUMMARY_MAXIMUM` sets the maximum number of images that will appear in
|
||||
your summary.
|
||||
|
||||
if `CLEAN_SUMMARY_MINIMUM_ONE` is set to `True` and your summary doesn't already
|
||||
contain an image, the plugin will add the first image in your article (if one
|
||||
exists) to the beginning of the summary.
|
||||
|
||||
##Requirements##
|
||||
|
||||
Requires Beautiful Soup:
|
||||
|
||||
pip install BeautifulSoup4
|
||||
|
||||
|
||||
##Usage with Summary Plugin##
|
||||
|
||||
If using the summary plugin, make sure summary appears in your plugins before
|
||||
clean summary. Eg.
|
||||
|
||||
PLUGINS = ['summary', 'clean_summary', ... ]
|
||||
1
clean_summary/__init__.py
Normal file
1
clean_summary/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .clean_summary import *
|
||||
38
clean_summary/clean_summary.py
Normal file
38
clean_summary/clean_summary.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Clean Summary
|
||||
-------------
|
||||
|
||||
adds option to specify maximum number of images to appear in article summary
|
||||
also adds option to include an image by default if one exists in your article
|
||||
"""
|
||||
|
||||
from pelican import signals
|
||||
from pelican.contents import Content, Article
|
||||
from bs4 import BeautifulSoup
|
||||
from six import text_type
|
||||
|
||||
def clean_summary(instance):
|
||||
if "CLEAN_SUMMARY_MAXIMUM" in instance.settings:
|
||||
maximum_images = instance.settings["CLEAN_SUMMARY_MAXIMUM"]
|
||||
else:
|
||||
maximum_images = 0
|
||||
if "CLEAN_SUMMARY_MINIMUM_ONE" in instance.settings:
|
||||
minimum_one = instance.settings['CLEAN_SUMMARY_MINIMUM_ONE']
|
||||
else:
|
||||
minimum_one = False
|
||||
if type(instance) == Article:
|
||||
summary = instance.summary
|
||||
summary = BeautifulSoup(instance.summary, 'html.parser')
|
||||
images = summary.findAll('img')
|
||||
if (len(images) > maximum_images):
|
||||
for image in images[maximum_images:]:
|
||||
image.extract()
|
||||
if len(images) < 1 and minimum_one: #try to find one
|
||||
content = BeautifulSoup(instance.content, 'html.parser')
|
||||
first_image = content.find('img')
|
||||
if first_image:
|
||||
summary.insert(0, first_image)
|
||||
instance._summary = text_type(summary)
|
||||
|
||||
def register():
|
||||
signals.content_object_init.connect(clean_summary)
|
||||
41
creole_reader/Readme.md
Normal file
41
creole_reader/Readme.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Creole Reader
|
||||
|
||||
This plugins allows you to write your posts using the wikicreole syntax. Give to
|
||||
these files the creole extension. The metadata are between `<<header>> <</header>>`
|
||||
tags.
|
||||
|
||||
## Dependency
|
||||
This plugin relies on [python-creole](https://pypi.python.org/pypi/python-creole/) to work. Install it with:
|
||||
`pip install python-creole`
|
||||
|
||||
## Syntax
|
||||
Use ** for strong, // for emphasis, one = for 1st level titles. Please use the
|
||||
following macro for code highlighting:
|
||||
`<<code ext=".file_extension">> <</code>>`
|
||||
|
||||
For the complete syntax, look at: http://www.wikicreole.org/
|
||||
|
||||
## Basic example
|
||||
```
|
||||
<<header>>
|
||||
title: Créole
|
||||
tags: creole, python, pelican_open
|
||||
date: 2013-12-12
|
||||
<</header>>
|
||||
|
||||
= Title 1
|
||||
== Title 2
|
||||
|
||||
Some nice text with **strong** and //emphasis//.
|
||||
|
||||
* A nice list
|
||||
** With sub-elements
|
||||
* Python
|
||||
|
||||
<<code ext=".py">>
|
||||
print("Hello World")
|
||||
<</code>>
|
||||
|
||||
# An ordered list
|
||||
# A second item
|
||||
```
|
||||
1
creole_reader/__init__.py
Normal file
1
creole_reader/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .creole_reader import *
|
||||
101
creole_reader/creole_reader.py
Normal file
101
creole_reader/creole_reader.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#-*- conding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Creole Reader
|
||||
-------------
|
||||
|
||||
This plugins allows you to write your posts using the wikicreole syntax. Give to
|
||||
these files the creole extension.
|
||||
For the syntax, look at: http://www.wikicreole.org/
|
||||
'''
|
||||
|
||||
from pelican import readers
|
||||
from pelican import signals
|
||||
from pelican import settings
|
||||
|
||||
from pelican.utils import pelican_open
|
||||
|
||||
try:
|
||||
from creole import creole2html
|
||||
creole = True
|
||||
except ImportError:
|
||||
creole = False
|
||||
|
||||
try:
|
||||
from pygments import lexers
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from pygments import highlight
|
||||
PYGMENTS = True
|
||||
except:
|
||||
PYGMENTS = False
|
||||
|
||||
class CreoleReader(readers.BaseReader):
|
||||
enabled = creole
|
||||
|
||||
file_extensions = ['creole']
|
||||
|
||||
def __init__(self, settings):
|
||||
super(CreoleReader, self).__init__(settings)
|
||||
|
||||
def _parse_header_macro(self, text):
|
||||
for line in text.split('\n'):
|
||||
name, value = line.split(':')
|
||||
name, value = name.strip(), value.strip()
|
||||
if name == 'title':
|
||||
self._metadata[name] = value
|
||||
else:
|
||||
self._metadata[name] = self.process_metadata(name, value)
|
||||
return u''
|
||||
|
||||
def _no_highlight(self, text):
|
||||
html = u'\n<pre><code>{}</code></pre>\n'.format(text)
|
||||
return html
|
||||
|
||||
def _get_lexer(self, source_type, code):
|
||||
try:
|
||||
return lexers.get_lexer_by_name(source_type)
|
||||
except:
|
||||
return lexers.guess_lexer(code)
|
||||
|
||||
def _get_formatter(self):
|
||||
formatter = HtmlFormatter(lineos = True, encoding='utf-8',
|
||||
style='colorful', outencoding='utf-8',
|
||||
cssclass='pygments')
|
||||
return formatter
|
||||
|
||||
def _parse_code_macro(self, ext, text):
|
||||
if not PYGMENTS:
|
||||
return self._no_highlight(text)
|
||||
|
||||
try:
|
||||
source_type = ''
|
||||
if '.' in ext:
|
||||
source_type = ext.strip().split('.')[1]
|
||||
else:
|
||||
source_type = ext.strip()
|
||||
except IndexError:
|
||||
source_type = ''
|
||||
lexer = self._get_lexer(source_type, text)
|
||||
formatter = self._get_formatter()
|
||||
|
||||
try:
|
||||
return highlight(text, lexer, formatter).decode('utf-8')
|
||||
except:
|
||||
return self._no_highlight(text)
|
||||
|
||||
# You need to have a read method, which takes a filename and returns
|
||||
# some content and the associated metadata.
|
||||
def read(self, source_path):
|
||||
"""Parse content and metadata of creole files"""
|
||||
|
||||
self._metadata = {}
|
||||
with pelican_open(source_path) as text:
|
||||
content = creole2html(text, macros={'header': self._parse_header_macro,
|
||||
'code': self._parse_code_macro})
|
||||
return content, self._metadata
|
||||
|
||||
def add_reader(readers):
|
||||
readers.reader_classes['creole'] = CreoleReader
|
||||
|
||||
def register():
|
||||
signals.readers_init.connect(add_reader)
|
||||
36
custom_article_urls/README.md
Normal file
36
custom_article_urls/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
#Custom Article URLs#
|
||||
|
||||
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.
|
||||
|
||||
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}/,
|
||||
'SAVE_AS': '{category}/{slug}/index.html},
|
||||
'Category 2': {'URL': '{date:%Y}/{date:%B}/{slug}/,
|
||||
'SAVE_AS': '{date:%Y}/{date:%B}/{slug}/index.html},
|
||||
}
|
||||
|
||||
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 called *Sub Category*, you could
|
||||
define its rules with::
|
||||
|
||||
'Category 1/Sub Category`: ...
|
||||
|
||||
##Other Usage: Article Metadata##
|
||||
|
||||
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.
|
||||
1
custom_article_urls/__init__.py
Normal file
1
custom_article_urls/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .custom_article_urls import *
|
||||
45
custom_article_urls/custom_article_urls.py
Normal file
45
custom_article_urls/custom_article_urls.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Author: Alistair Magee
|
||||
|
||||
Adds ability to specify custom urls for different categories
|
||||
(or subcategories if using subcategory plugin) of article
|
||||
using a dictionary stored in pelican settings file as
|
||||
{category: {article_url_structure: stirng, article_save_as: string}}
|
||||
"""
|
||||
from pelican import signals
|
||||
from pelican.contents import Content, Category
|
||||
from six import text_type
|
||||
|
||||
def custom_url(generator, metadata):
|
||||
if 'CUSTOM_ARTICLE_URLS' in generator.settings:
|
||||
custom_urls = generator.settings['CUSTOM_ARTICLE_URLS']
|
||||
category = text_type(metadata['category'])
|
||||
pattern_matched = {}
|
||||
|
||||
if category in custom_urls:
|
||||
pattern_matched = custom_urls[category]
|
||||
|
||||
if 'subcategories' in metadata: #using subcategory plugin
|
||||
for subcategory in metadata['subcategories']:
|
||||
if subcategory in custom_urls:
|
||||
pattern_matched = custom_urls[subcategory]
|
||||
|
||||
if pattern_matched:
|
||||
#only alter url if hasn't been set in the metdata
|
||||
if ('url', 'save_as') in metadata:
|
||||
""" if both url and save_as are set in the metadata already
|
||||
then there is already a custom url set, skip this one
|
||||
"""
|
||||
pass
|
||||
else:
|
||||
temp_article = Content(None, metadata=metadata)
|
||||
url_format = pattern_matched['URL']
|
||||
save_as_format = pattern_matched['SAVE_AS']
|
||||
url = url_format.format(**temp_article.url_format)
|
||||
save_as = save_as_format.format(**temp_article.url_format)
|
||||
metadata.update({'url': url, 'save_as': save_as})
|
||||
|
||||
|
||||
def register():
|
||||
signals.article_generator_context.connect(custom_url)
|
||||
@@ -15,7 +15,7 @@ We use disqus-python package for communication with disqus API:
|
||||
Put ``disqus_static.py`` plugin in ``plugins`` folder in pelican installation
|
||||
and use the following in your settings::
|
||||
|
||||
PLUGINS = [u"pelican.plugins.disqus_static"]
|
||||
PLUGINS = [u"disqus_static"]
|
||||
|
||||
DISQUS_SITENAME = u'YOUR_SITENAME'
|
||||
DISQUS_SECRET_KEY = u'YOUR_SECRET_KEY'
|
||||
|
||||
1
disqus_static/__init__py
Normal file
1
disqus_static/__init__py
Normal file
@@ -0,0 +1 @@
|
||||
from .disqus_static import *
|
||||
@@ -10,9 +10,9 @@ from disqusapi import DisqusAPI, Paginator
|
||||
from pelican import signals
|
||||
|
||||
def initialized(pelican):
|
||||
from pelican.settings import _DEFAULT_CONFIG
|
||||
_DEFAULT_CONFIG.setdefault('DISQUS_SECRET_KEY', '')
|
||||
_DEFAULT_CONFIG.setdefault('DISQUS_PUBLIC_KEY', '')
|
||||
from pelican.settings import DEFAULT_CONFIG
|
||||
DEFAULT_CONFIG.setdefault('DISQUS_SECRET_KEY', '')
|
||||
DEFAULT_CONFIG.setdefault('DISQUS_PUBLIC_KEY', '')
|
||||
if pelican:
|
||||
pelican.settings.setdefault('DISQUS_SECRET_KEY', '')
|
||||
pelican.settings.setdefault('DISQUS_PUBLIC_KEY', '')
|
||||
|
||||
69
exif_info/README.md
Normal file
69
exif_info/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
EXIF Info
|
||||
==================
|
||||
|
||||
* Retrieve EXIF informations from pictures
|
||||
|
||||
##How to Use
|
||||
|
||||
1. Install the PIL (Python Imaging Library) libraries, please refer to http://pythonware.com/products/pil/ or your OS manual for instructions.
|
||||
2. This plugin is an extension to the Gallery plugin, so first install it, and refer to its README for usage instructions.
|
||||
3. Add 'exif_info' to the plugin list.
|
||||
|
||||
If you want, you can also specify in your configuration:
|
||||
|
||||
EXIF_INFO_DEFAULT = True/False
|
||||
|
||||
To set whether or not to retrieve by default the EXIF informations from your pictures. This setting can be overriden on a per article/album basis.
|
||||
|
||||
###Articles
|
||||
|
||||
Override on a per article/post basis the default behaviour by adding the following:
|
||||
|
||||
exifinfo: "True"
|
||||
|
||||
or
|
||||
|
||||
exifinfo: "False"
|
||||
|
||||
###Gallery Page
|
||||
|
||||
At this time the Gallery page is *not* supported.
|
||||
|
||||
##Examples
|
||||
|
||||
###article.html
|
||||
|
||||
{% if article.album %}
|
||||
{% for image in article.galleryimages %}
|
||||
{% if article.galleryexif and article.galleryexif.get(image) %}
|
||||
<table>
|
||||
{% if article.galleryexif.get(image).get("Model") %}
|
||||
<tr><th colspan="4">{{article.galleryexif.get(image).get("Model")}}</th></tr>
|
||||
{% endif %}
|
||||
{% if article.galleryexif.get(image).get("LensModel") %}
|
||||
<tr><th colspan="4">{{article.galleryexif.get(image).get("LensModel")}}</th></tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
{% if article.galleryexif.get(image).get("ISOSpeedRatings") %}
|
||||
<td>{{article.galleryexif.get(image).get("ISOSpeedRatings")}}</td>
|
||||
{% endif %}
|
||||
{% if article.galleryexif.get(image).get("FocalLength") %}
|
||||
<td>{{article.galleryexif.get(image).get("FocalLength")}}mm</td>
|
||||
{% endif %}
|
||||
{% if article.galleryexif.get(image).get("FNumber") %}
|
||||
<td>f/{{article.galleryexif.get(image).get("FNumber")}}</td>
|
||||
{% endif %}
|
||||
{% if article.galleryexif.get(image).get("ExposureTime") %}
|
||||
<td>{{article.galleryexif.get(image).get("ExposureTime")}}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
##Reasoning
|
||||
|
||||
This was developped as an external plugin, instead of adapting the Gallery plugin because this relies on the PIL libraries to be installed, and working.
|
||||
|
||||
As this set of library may - or not - be available on a given platform, it seemed unreasonable to limit the Gallery plugin to the systems where it is.
|
||||
1
exif_info/__init__.py
Normal file
1
exif_info/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .exif_info import *
|
||||
86
exif_info/exif_info.py
Normal file
86
exif_info/exif_info.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import re
|
||||
from pelican import signals
|
||||
from PIL import Image
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
def get_exif_data(fname):
|
||||
"""Get embedded EXIF data from an image file."""
|
||||
exif = {}
|
||||
ret = {}
|
||||
try:
|
||||
img = Image.open(fname)
|
||||
if hasattr( img, '_getexif' ):
|
||||
exifinfo = img._getexif()
|
||||
if exifinfo != None:
|
||||
for tag, value in exifinfo.items():
|
||||
decoded = TAGS.get(tag, tag)
|
||||
ret[decoded] = value
|
||||
except IOError:
|
||||
print 'IOERROR ' + fname
|
||||
|
||||
#print ret
|
||||
# Keep and format the most interesting fields
|
||||
for tag in ret:
|
||||
if tag == "DateTimeOriginal":
|
||||
exif[tag] = ret[tag]
|
||||
if tag == "Model":
|
||||
exif[tag] = ret[tag]
|
||||
if tag == "LensModel":
|
||||
exif[tag] = ret[tag]
|
||||
if tag == "ISOSpeedRatings":
|
||||
exif[tag] = ret[tag]
|
||||
if tag == "FocalLength":
|
||||
exif[tag] = "{:2.1f}".format(float(ret[tag][0]) / float(ret[tag][1]))
|
||||
if tag == "FNumber":
|
||||
exif[tag] = "{:2.1f}".format(float(ret[tag][0]) / float(ret[tag][1]))
|
||||
if tag == "ExposureTime":
|
||||
exif[tag] = str(ret[tag][0]) + "/" + str(ret[tag][1])
|
||||
|
||||
return exif
|
||||
|
||||
|
||||
def add_exif_post(generator):
|
||||
get_exif = generator.settings.get('EXIF_INFO_DEFAULT')
|
||||
if get_exif == None:
|
||||
get_exif = True;
|
||||
|
||||
contentpath = generator.settings.get('PATH')
|
||||
gallerycontentpath = os.path.join(contentpath,'images/gallery')
|
||||
|
||||
for article in generator.articles:
|
||||
if 'exifinfo' in article.metadata.keys():
|
||||
if article.metadata.get('exifinfo'):
|
||||
# Ignore anything which is not a capitalization variation of
|
||||
# true/false
|
||||
if article.metadata.get('exifinfo').lower() == "true":
|
||||
get_exif = True;
|
||||
if article.metadata.get('exifinfo').lower() == "false":
|
||||
get_exif = False;
|
||||
|
||||
if get_exif:
|
||||
if 'gallery' in article.metadata.keys():
|
||||
album = article.metadata.get('gallery')
|
||||
galleryexif = dict()
|
||||
|
||||
articlegallerypath=os.path.join(gallerycontentpath, album)
|
||||
|
||||
# If the gallery has not yet been generated generate one
|
||||
if article.metadata.get('galleryimages'):
|
||||
galleryimages = article.metadata.get('galleryimages');
|
||||
else:
|
||||
galleryimages = []
|
||||
if(os.path.isdir(articlegallerypath)):
|
||||
for i in os.listdir(articlegallerypath):
|
||||
if os.path.isfile(os.path.join(os.path.join(gallerycontentpath, album), i)):
|
||||
galleryimages.append(i)
|
||||
|
||||
# Retrieve the EXIF informations for all the images
|
||||
for img in galleryimages:
|
||||
galleryexif[img] = get_exif_data(articlegallerypath + "/" + img)
|
||||
|
||||
article.galleryexif = galleryexif
|
||||
|
||||
|
||||
def register():
|
||||
signals.article_generator_finalized.connect(add_exif_post)
|
||||
33
feed_summary/Readme.md
Normal file
33
feed_summary/Readme.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Feed Summary #
|
||||
This plugin allows article summaries to be used in ATOM and RSS feeds instead of the entire article. It uses the
|
||||
built-in pelican `Summary:` metadata.
|
||||
|
||||
The summary of an article can either be set explicitly with the `Summary:` metadata attribute as described in the
|
||||
[pelican getting started docs](http://docs.getpelican.com/en/latest/getting_started.html#file-metadata),
|
||||
or automatically generated using the number of words specified in the
|
||||
[SUMMARY_MAX_LENGTH](http://docs.getpelican.com/en/latest/settings.html) setting.
|
||||
|
||||
## Usage ##
|
||||
To use this plugin, ensure the following are set in your `pelicanconf.py` file:
|
||||
|
||||
PLUGIN_PATH = '/path/to/pelican-plugins'
|
||||
PLUGINS = [
|
||||
'feed_summary',
|
||||
]
|
||||
'FEED_USE_SUMMARY' = True
|
||||
|
||||
The default value of `'FEED_USE_SUMMARY'` is `False`, so it must be set to `True` to enable the plugin, even if it is loaded.
|
||||
|
||||
This plugin is written for pelican 3.3 and later.
|
||||
|
||||
|
||||
## Implementation Notes ##
|
||||
|
||||
This plugin derives `FeedSummaryWriter` from the `Writer` class, duplicating code of the `Writer._add_item_to_the_feed` method.
|
||||
|
||||
When the `initialized` signal is sent, it alternates the `get_writer` method of the `Pelican` object to use `FeedSummaryWriter` instead of `Writer`.
|
||||
|
||||
A little hackish, but currently this can't be done otherwise via the regular plugin methods.
|
||||
|
||||
* *Initial Code (PR #36): Michelle L. Gill <michelle.lynn.gill@gmail.com>*
|
||||
* *Resumption of PR and Maintainer: Florian Jacob ( projects[PLUS]pelican[ÄT]florianjacob.de )*
|
||||
1
feed_summary/__init__.py
Normal file
1
feed_summary/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .feed_summary import *
|
||||
57
feed_summary/feed_summary.py
Normal file
57
feed_summary/feed_summary.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Feed Summary
|
||||
============
|
||||
|
||||
This plugin allows summaries to be used in feeds instead of the full length article.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from jinja2 import Markup
|
||||
|
||||
import six
|
||||
if not six.PY3:
|
||||
from urlparse import urlparse
|
||||
else:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pelican import signals
|
||||
from pelican.writers import Writer
|
||||
from pelican.utils import set_date_tzinfo
|
||||
|
||||
from .magic_set import magic_set
|
||||
|
||||
class FeedSummaryWriter(Writer):
|
||||
def _add_item_to_the_feed(self, feed, item):
|
||||
if self.settings['FEED_USE_SUMMARY']:
|
||||
title = Markup(item.title).striptags()
|
||||
link = '%s/%s' % (self.site_url, item.url)
|
||||
feed.add_item(
|
||||
title=title,
|
||||
link=link,
|
||||
unique_id='tag:%s,%s:%s' % (urlparse(link).netloc,
|
||||
item.date.date(),
|
||||
urlparse(link).path.lstrip('/')),
|
||||
description=item.summary if hasattr(item, 'summary') else item.get_content(self.site_url),
|
||||
categories=item.tags if hasattr(item, 'tags') else None,
|
||||
author_name=getattr(item, 'author', ''),
|
||||
pubdate=set_date_tzinfo(item.modified if hasattr(item, 'modified') else item.date,
|
||||
self.settings.get('TIMEZONE', None)))
|
||||
else:
|
||||
super(FeedSummaryWriter, self)._add_item_to_the_feed(feed, item)
|
||||
|
||||
def set_feed_use_summary_default(pelican_object):
|
||||
# modifying DEFAULT_CONFIG doesn't have any effect at this point in pelican setup
|
||||
# everybody who uses DEFAULT_CONFIG is already used/copied it or uses the pelican_object.settings copy.
|
||||
|
||||
pelican_object.settings.setdefault('FEED_USE_SUMMARY', False)
|
||||
|
||||
def patch_pelican_writer(pelican_object):
|
||||
@magic_set(pelican_object)
|
||||
def get_writer(self):
|
||||
return FeedSummaryWriter(self.output_path,settings=self.settings)
|
||||
|
||||
def register():
|
||||
signals.initialized.connect(set_feed_use_summary_default)
|
||||
signals.initialized.connect(patch_pelican_writer)
|
||||
92
feed_summary/magic_set.py
Normal file
92
feed_summary/magic_set.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import types
|
||||
import inspect
|
||||
|
||||
# Modifies class methods (or instances of them) on the fly
|
||||
# http://blog.ianbicking.org/2007/08/08/opening-python-classes/
|
||||
# http://svn.colorstudy.com/home/ianb/recipes/magicset.py
|
||||
|
||||
def magic_set(obj):
|
||||
"""
|
||||
Adds a function/method to an object. Uses the name of the first
|
||||
argument as a hint about whether it is a method (``self``), class
|
||||
method (``cls`` or ``klass``), or static method (anything else).
|
||||
Works on both instances and classes.
|
||||
|
||||
>>> class color:
|
||||
... def __init__(self, r, g, b):
|
||||
... self.r, self.g, self.b = r, g, b
|
||||
>>> c = color(0, 1, 0)
|
||||
>>> c # doctest: +ELLIPSIS
|
||||
<__main__.color instance at ...>
|
||||
>>> @magic_set(color)
|
||||
... def __repr__(self):
|
||||
... return '<color %s %s %s>' % (self.r, self.g, self.b)
|
||||
>>> c
|
||||
<color 0 1 0>
|
||||
>>> @magic_set(color)
|
||||
... def red(cls):
|
||||
... return cls(1, 0, 0)
|
||||
>>> color.red()
|
||||
<color 1 0 0>
|
||||
>>> c.red()
|
||||
<color 1 0 0>
|
||||
>>> @magic_set(color)
|
||||
... def name():
|
||||
... return 'color'
|
||||
>>> color.name()
|
||||
'color'
|
||||
>>> @magic_set(c)
|
||||
... def name(self):
|
||||
... return 'red'
|
||||
>>> c.name()
|
||||
'red'
|
||||
>>> @magic_set(c)
|
||||
... def name(cls):
|
||||
... return cls.__name__
|
||||
>>> c.name()
|
||||
'color'
|
||||
>>> @magic_set(c)
|
||||
... def pr(obj):
|
||||
... print obj
|
||||
>>> c.pr(1)
|
||||
1
|
||||
"""
|
||||
def decorator(func):
|
||||
is_class = (isinstance(obj, type)
|
||||
or isinstance(obj, types.ClassType))
|
||||
args, varargs, varkw, defaults = inspect.getargspec(func)
|
||||
if not args or args[0] not in ('self', 'cls', 'klass'):
|
||||
# Static function/method
|
||||
if is_class:
|
||||
replacement = staticmethod(func)
|
||||
else:
|
||||
replacement = func
|
||||
elif args[0] == 'self':
|
||||
if is_class:
|
||||
replacement = func
|
||||
else:
|
||||
def replacement(*args, **kw):
|
||||
return func(obj, *args, **kw)
|
||||
try:
|
||||
replacement.func_name = func.func_name
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if is_class:
|
||||
replacement = classmethod(func)
|
||||
else:
|
||||
def replacement(*args, **kw):
|
||||
return func(obj.__class__, *args, **kw)
|
||||
try:
|
||||
replacement.func_name = func.func_name
|
||||
except:
|
||||
pass
|
||||
setattr(obj, func.func_name, replacement)
|
||||
return replacement
|
||||
return decorator
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
|
||||
|
||||
114
gallery/README.md
Normal file
114
gallery/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
Gallery
|
||||
==================
|
||||
|
||||
* Allows an article to contain an album of pictures.
|
||||
* All albums can also be syndicated into a central gallery page.
|
||||
|
||||
##How to Use
|
||||
|
||||
1. Group images into folders, with each folder representing an album.
|
||||
2. Place all album folders within a folder named gallery, which resides within the images folder.
|
||||
|
||||
./content/images/gallery/album_name
|
||||
|
||||
###Articles
|
||||
|
||||
Attach an album to an article/post by placing a gallery metadata tag with the name of the album.
|
||||
|
||||
gallery:album_name
|
||||
|
||||
Optionaly you can also add (on one(1) line!)
|
||||
|
||||
gallerycaptions:{'file1':'title1', 'file4':'title4'}
|
||||
|
||||
The template has access to the album name.
|
||||
|
||||
article.album
|
||||
|
||||
And the filename of images within an album.
|
||||
|
||||
article.albumimages
|
||||
|
||||
And the titles associated to the pictures, as a dictionary.
|
||||
|
||||
article.gallerycaptions
|
||||
|
||||
###Gallery Page
|
||||
|
||||
Create a page and a gallery template (named gallery.html). And inform pelican to use the gallery template for the page.
|
||||
|
||||
template:gallery
|
||||
|
||||
The template has access to a dictionary of lists.
|
||||
The dictionary key is the name of the album and the lists contain the filenames.
|
||||
|
||||
page.gallery
|
||||
|
||||
##Examples
|
||||
|
||||
###article.html
|
||||
|
||||
<h2><a href="{{ SITEURL }}/pages/gallery.html#{{ article.album }}">{{ article.album }}</a></h2>
|
||||
<ul>
|
||||
{% for image in article.galleryimages %}
|
||||
{% if article.gallerycaptions.get(image) %}
|
||||
<li><a class="{{ article.album }} cboxElement" href="{{ SITEURL }}/static/images/gallery/{{ article.album }}/{{ image }}" title="{{ article.gallerycaptions.get(image) }}">{{ article.gallerycaptions.get(image) }}<br><img src="{{ SITEURL }}/static/images/gallery200x200/{{ article.album }}/{{ image }}"></a></li>
|
||||
{% else %}
|
||||
<li><a class="{{ article.album }} cboxElement" href="{{ SITEURL }}/static/images/gallery/{{ article.album }}/{{ image }}"><img src="{{ SITEURL }}/static/images/gallery200x200/{{ article.album }}/{{ image }}"></a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
###gallery.html
|
||||
|
||||
{% for album, images in page.gallery.iteritems() %}
|
||||
<h2><a name="{{ album }}">{{ album }}</a></h2>
|
||||
<ul>
|
||||
{% for image in images %}
|
||||
<li><a class="{{ album }} cboxElement" href="{{ SITEURL }}/static/images/gallery/{{album}}/{{ image }}" title="{{ image }}"><img src="{{ SITEURL }}/static/images/gallery200x200/{{album}}/{{ image }}"></a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
###posts/foo.md
|
||||
|
||||
title:Foo
|
||||
gallery:albumname
|
||||
|
||||
or, to add captions:
|
||||
|
||||
title:Foo
|
||||
gallery:albumname
|
||||
gallerycaptions:{'file1':'title1', 'file4':'title4'}
|
||||
|
||||
###pages/gallery.md
|
||||
|
||||
title:All Images
|
||||
template:gallery
|
||||
|
||||
##Reasoning
|
||||
|
||||
The album name and filenames are returned as opposed to the direct path to the images,
|
||||
to allow flexibility of different thumbnail sizes to be used it different locations of a website.
|
||||
|
||||
href="{{ SITEURL }}/static/images/gallery/{{album}}/{{ image }}"
|
||||
href="{{ SITEURL }}/static/images/gallery200x200/{{album}}/{{ image }}"
|
||||
|
||||
It also allows a thumbnail to link to the full image,
|
||||
as well as the filename extension to be stripped and the title of an image to be displayed along side the title of an album.
|
||||
|
||||
##Recommendation
|
||||
|
||||
It is recommended to use this extension along with the thumbnailer plugin.
|
||||
|
||||
RESIZE = [
|
||||
('gallery', False, 200,200),
|
||||
]
|
||||
|
||||
You may also wish to use this along with a gallery plugin such as [Colorbox](http://www.jacklmoore.com/colorbox/).
|
||||
|
||||
##In Use
|
||||
|
||||
* [SESIF Article](http://sesif.github.io/my-super-title.html)
|
||||
* [SESIF Gallery](http://sesif.github.io/pages/gallery.html)
|
||||
* [SESIF Source](http://github.com/SESIF/SESIF.github.io/tree/source)
|
||||
1
gallery/__init__.py
Normal file
1
gallery/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .gallery import *
|
||||
61
gallery/gallery.py
Normal file
61
gallery/gallery.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
import ast
|
||||
from pelican import signals
|
||||
|
||||
|
||||
def add_gallery_post(generator):
|
||||
|
||||
contentpath = generator.settings.get('PATH')
|
||||
gallerycontentpath = os.path.join(contentpath,'images/gallery')
|
||||
|
||||
for article in generator.articles:
|
||||
if 'gallery' in article.metadata.keys():
|
||||
album = article.metadata.get('gallery')
|
||||
galleryimages = []
|
||||
gallerycaptions = dict()
|
||||
gallerycomments = dict()
|
||||
|
||||
articlegallerypath=os.path.join(gallerycontentpath, album)
|
||||
|
||||
if(os.path.isdir(articlegallerypath)):
|
||||
for i in os.listdir(articlegallerypath):
|
||||
if os.path.isfile(os.path.join(os.path.join(gallerycontentpath, album), i)):
|
||||
galleryimages.append(i)
|
||||
|
||||
if 'gallerycaptions' in article.metadata.keys():
|
||||
line = article.metadata.get('gallerycaptions').encode('ascii','xmlcharrefreplace')
|
||||
gallerycaptions = ast.literal_eval(line)
|
||||
|
||||
if 'gallerycomments' in article.metadata.keys():
|
||||
line = article.metadata.get('gallerycomments').encode('ascii','xmlcharrefreplace')
|
||||
gallerycomments = ast.literal_eval(line)
|
||||
|
||||
article.album = album
|
||||
article.galleryimages = sorted(galleryimages)
|
||||
article.gallerycaptions = gallerycaptions
|
||||
article.gallerycomments = gallerycomments
|
||||
|
||||
|
||||
def generate_gallery_page(generator):
|
||||
|
||||
contentpath = generator.settings.get('PATH')
|
||||
gallerycontentpath = os.path.join(contentpath,'images/gallery')
|
||||
|
||||
for page in generator.pages:
|
||||
if page.metadata.get('template') == 'gallery':
|
||||
gallery = dict()
|
||||
|
||||
for a in os.listdir(gallerycontentpath):
|
||||
if os.path.isdir(os.path.join(gallerycontentpath, a)):
|
||||
|
||||
for i in os.listdir(os.path.join(gallerycontentpath, a)):
|
||||
if os.path.isfile(os.path.join(os.path.join(gallerycontentpath, a), i)):
|
||||
gallery.setdefault(a, []).append(i)
|
||||
gallery[a].sort()
|
||||
|
||||
page.gallery=gallery
|
||||
|
||||
|
||||
def register():
|
||||
signals.article_generator_finalized.connect(add_gallery_post)
|
||||
signals.page_generator_finalized.connect(generate_gallery_page)
|
||||
@@ -9,6 +9,11 @@ For example, to track Pelican project activity, the setting would be::
|
||||
|
||||
GITHUB_ACTIVITY_FEED = 'https://github.com/getpelican.atom'
|
||||
|
||||
If you want to limit the amount of entries to a certain maximum set the
|
||||
``GITHUB_ACTIVITY_MAX_ENTRIES`` parameter.
|
||||
|
||||
GITHUB_ACTIVITY_MAX_ENTRIES = 10
|
||||
|
||||
On the template side, you just have to iterate over the ``github_activity``
|
||||
variable, as in this example::
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class GitHubActivity():
|
||||
import feedparser
|
||||
self.activities = feedparser.parse(
|
||||
generator.settings['GITHUB_ACTIVITY_FEED'])
|
||||
self.max_entries = generator.settings['GITHUB_ACTIVITY_MAX_ENTRIES']
|
||||
|
||||
def fetch(self):
|
||||
"""
|
||||
@@ -37,7 +38,7 @@ class GitHubActivity():
|
||||
[element for element in [activity['title'],
|
||||
activity['content'][0]['value']]])
|
||||
|
||||
return entries
|
||||
return entries[0:self.max_entries]
|
||||
|
||||
|
||||
def fetch_github_activity(gen, metadata):
|
||||
@@ -65,7 +66,7 @@ def register():
|
||||
"""
|
||||
try:
|
||||
signals.article_generator_init.connect(feed_parser_initialization)
|
||||
signals.article_generate_context.connect(fetch_github_activity)
|
||||
signals.article_generator_context.connect(fetch_github_activity)
|
||||
except ImportError:
|
||||
logger.warning('`github_activity` failed to load dependency `feedparser`.'
|
||||
'`github_activity` plugin not loaded.')
|
||||
|
||||
@@ -15,4 +15,4 @@ def add_license(generator, metadata):
|
||||
metadata['license'] = generator.settings['LICENSE']
|
||||
|
||||
def register():
|
||||
signals.article_generate_context.connect(add_license)
|
||||
signals.article_generator_context.connect(add_license)
|
||||
|
||||
@@ -57,7 +57,7 @@ def initialize_feedparser(generator):
|
||||
def register():
|
||||
try:
|
||||
signals.article_generator_init.connect(initialize_feedparser)
|
||||
signals.article_generate_context.connect(fetch_goodreads_activity)
|
||||
signals.article_generator_context.connect(fetch_goodreads_activity)
|
||||
except ImportError:
|
||||
logger.warning('`goodreads_activity` failed to load dependency `feedparser`.'
|
||||
'`goodreads_activity` plugin not loaded.')
|
||||
|
||||
20
googleplus_comments/Readme.md
Normal file
20
googleplus_comments/Readme.md
Normal file
@@ -0,0 +1,20 @@
|
||||
GooglePlus Comments Plugin For Pelican
|
||||
==================================
|
||||
|
||||
Adds GooglePlus comments to Pelican
|
||||
|
||||
Add the plugin to `pelicanconf.py`:
|
||||
|
||||
PLUGIN_PATH = 'pelican-plugins'
|
||||
PLUGINS = ["googleplus_comments"]
|
||||
|
||||
Add a `<div>` for comments to the `article.html` of your template:
|
||||
|
||||
<div id="commentswrap">
|
||||
<div id="comments"></div>
|
||||
</div>
|
||||
{{ article.metadata.googleplus_comments }}
|
||||
|
||||
See it working, and ask for support:
|
||||
|
||||
<http://zonca.github.io/2013/09/google-plus-comments-plugin-for-pelican.html>
|
||||
1
googleplus_comments/__init__.py
Normal file
1
googleplus_comments/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .googleplus_comments import *
|
||||
29
googleplus_comments/googleplus_comments.py
Normal file
29
googleplus_comments/googleplus_comments.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Google Comments Plugin For Pelican
|
||||
==================================
|
||||
|
||||
Adds Google comments to Pelican
|
||||
"""
|
||||
|
||||
from pelican import signals
|
||||
|
||||
googleplus_comments_snippet = """
|
||||
<script src="https://apis.google.com/js/plusone.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
gapi.comments.render('comments', {
|
||||
href: window.location,
|
||||
width: '600',
|
||||
first_party_property: 'BLOGGER',
|
||||
view_type: 'FILTERED_POSTMOD'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
def add_googleplus_comments(generator, metadata):
|
||||
metadata["googleplus_comments"] = googleplus_comments_snippet
|
||||
|
||||
def register():
|
||||
signals.article_generator_context.connect(add_googleplus_comments)
|
||||
@@ -7,9 +7,17 @@ makes the variable available within the article's context. You can add
|
||||
address. Obviously, that email address must be associated with a Gravatar
|
||||
account.
|
||||
|
||||
Alternatively, you can provide an email address from within article metadata::
|
||||
Alternatively, you can provide an email address from within article metadata.
|
||||
|
||||
In ReSTructuredText::
|
||||
|
||||
:email: john.doe@example.com
|
||||
|
||||
If the email address is defined via at least one of the two methods above,
|
||||
the ``author_gravatar`` variable is added to the article's context.
|
||||
In Markdown::
|
||||
|
||||
Email: john.doe@example.com
|
||||
|
||||
If the email address is defined via at least one of the two methods above, the
|
||||
``author_gravatar`` variable is added to the article's context. For markdown,
|
||||
it is critical the 'E' in Email is capitalized.
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ def add_gravatar(generator, metadata):
|
||||
|
||||
|
||||
def register():
|
||||
signals.article_generate_context.connect(add_gravatar)
|
||||
signals.article_generator_context.connect(add_gravatar)
|
||||
|
||||
73
i18n_subsites/README.rst
Normal file
73
i18n_subsites/README.rst
Normal file
@@ -0,0 +1,73 @@
|
||||
======================
|
||||
I18N Sub-sites Plugin
|
||||
======================
|
||||
|
||||
This plugin extends the translations functionality by creating internationalized sub-sites for the default site. It is therefore redundant with the *\*_LANG_{SAVE_AS,URL}* variables, so it disables them to prevent conflicts.
|
||||
|
||||
What it does
|
||||
============
|
||||
1. The *\*_LANG_URL* and *\*_LANG_SAVE_AS* variables are set to their normal counterparts (e.g. *ARTICLE_URL*) so they don't conflict with this scheme.
|
||||
2. While building the site for *DEFAULT_LANG* the translations of pages and articles are not generated, but their relations to the original content is kept via links to them.
|
||||
3. For each non-default language a "sub-site" with a modified config [#conf]_ is created [#run]_, linking the translations to the originals (if available). The configured language code is appended to the *OUTPUT_PATH* and *SITEURL* of each sub-site. For each sub-site, *DEFAULT_LANG* is changed to the language of the sub-site so that articles in a different language are treated as translations.
|
||||
|
||||
If *HIDE_UNTRANSLATED_CONTENT* is True (default), content without a translation for a language is generated as hidden (for pages) or draft (for articles) for the corresponding language sub-site.
|
||||
|
||||
.. [#conf] For each language a config override is given in the *I18N_SUBSITES* dictionary.
|
||||
.. [#run] Using a new *PELICAN_CLASS* instance and its ``run`` method, so each sub-site could even have a different *PELICAN_CLASS* if specified in *I18N_SUBSITES* conf overrides.
|
||||
|
||||
Setting it up
|
||||
=============
|
||||
|
||||
For each extra used language code, a language-specific variables overrides dictionary must be given (but can be empty) in the *I18N_SUBSITES* dictionary
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
PLUGINS = ['i18n_subsites', ...]
|
||||
|
||||
# mapping: language_code -> conf_overrides_dict
|
||||
I18N_SUBSITES = {
|
||||
'cz': {
|
||||
'SITENAME': 'Hezkej blog',
|
||||
}
|
||||
}
|
||||
|
||||
- The language code is the language identifier used in the *lang* metadata. It is appended to *OUTPUT_PATH* and *SITEURL* of each I18N sub-site.
|
||||
- The internationalized config overrides dictionary may specify configuration variable overrides — e.g. a different *LOCALE*, *SITENAME*, *TIMEZONE*, etc. However, it **must not** override *OUTPUT_PATH* and *SITEURL* as they are modified automatically by appending the language code.
|
||||
|
||||
Localizing templates
|
||||
--------------------
|
||||
|
||||
Most importantly, this plugin can use localized templates for each sub-site. There are two approaches to having the templates localized:
|
||||
|
||||
- You can set a different *THEME* override for each language in *I18N_SUBSITES*, e.g. by making a copy of a theme ``my_theme`` to ``my_theme_lang`` and then editing the templates in the new localized theme. This approach means you don't have to deal with gettext ``*.po`` files, but it is harder to maintain over time.
|
||||
- You use only one theme and localize the templates using the `jinja2.ext.i18n Jinja2 extension <http://jinja.pocoo.org/docs/templates/#i18n>`_. For a kickstart read this `guide <./localizing_using_jinja2.rst>`_.
|
||||
|
||||
It may be convenient to add language buttons to your theme in addition to the translation links of articles and pages. These buttons could, for example, point to the *SITEURL* of each (sub-)site. For this reason the plugin adds these variables to the template context:
|
||||
|
||||
main_lang
|
||||
The language of the top-level site — the original *DEFAULT_LANG*
|
||||
main_siteurl
|
||||
The *SITEURL* of the top-level site — the original *SITEURL*
|
||||
lang_siteurls
|
||||
An ordered dictionary, mapping all used languages to their *SITEURL*. The ``main_lang`` is the first key with ``main_siteurl`` as the value. This dictionary is useful for implementing global language buttons that show the language of the currently viewed (sub-)site too.
|
||||
extra_siteurls
|
||||
An ordered dictionary, subset of ``lang_siteurls``, the current *DEFAULT_LANG* of the rendered (sub-)site is not included, so for each (sub-)site ``set(extra_siteurls) == set(lang_siteurls) - set([DEFAULT_LANG])``. This dictionary is useful for implementing global language buttons that do not show the current language.
|
||||
|
||||
If you don't like the default ordering of the ordered dictionaries, use a Jinja2 filter to alter the ordering.
|
||||
|
||||
This short `howto <./implementing_language_buttons.rst>`_ shows two example implementations of language buttons.
|
||||
|
||||
Usage notes
|
||||
===========
|
||||
- It is **mandatory** to specify *lang* metadata for each article and page as *DEFAULT_LANG* is later changed for each sub-site, so content without *lang* metadata woudl be rendered in every (sub-)site.
|
||||
- As with the original translations functionality, *slug* metadata is used to group translations. It is therefore often convenient to compensate for this by overriding the content URL (which defaults to slug) using the *URL* and *Save_as* metadata.
|
||||
|
||||
Future plans
|
||||
============
|
||||
|
||||
- add a test suite
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
- A demo and test site is in the ``gh-pages`` branch and can be seen at http://smartass101.github.io/pelican-plugins/
|
||||
1
i18n_subsites/__init__.py
Normal file
1
i18n_subsites/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .i18n_subsites import *
|
||||
81
i18n_subsites/_regenerate_context_helpers.py
Normal file
81
i18n_subsites/_regenerate_context_helpers.py
Normal file
@@ -0,0 +1,81 @@
|
||||
|
||||
import math
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
|
||||
def regenerate_context_articles(generator):
|
||||
"""Helper to regenerate context after modifying articles draft state
|
||||
|
||||
essentially just a copy from pelican.generators.ArticlesGenerator.generate_context
|
||||
after process_translations up to signal sending
|
||||
|
||||
This has to be kept in sync untill a better solution is found
|
||||
This is for Pelican version 3.3.0
|
||||
"""
|
||||
# Simulate __init__ for fields that need it
|
||||
generator.dates = {}
|
||||
generator.tags = defaultdict(list)
|
||||
generator.categories = defaultdict(list)
|
||||
generator.authors = defaultdict(list)
|
||||
|
||||
|
||||
# Simulate ArticlesGenerator.generate_context
|
||||
for article in generator.articles:
|
||||
# only main articles are listed in categories and tags
|
||||
# not translations
|
||||
generator.categories[article.category].append(article)
|
||||
if hasattr(article, 'tags'):
|
||||
for tag in article.tags:
|
||||
generator.tags[tag].append(article)
|
||||
# ignore blank authors as well as undefined
|
||||
if hasattr(article, 'author') and article.author.name != '':
|
||||
generator.authors[article.author].append(article)
|
||||
|
||||
|
||||
# sort the articles by date
|
||||
generator.articles.sort(key=attrgetter('date'), reverse=True)
|
||||
generator.dates = list(generator.articles)
|
||||
generator.dates.sort(key=attrgetter('date'),
|
||||
reverse=generator.context['NEWEST_FIRST_ARCHIVES'])
|
||||
|
||||
# create tag cloud
|
||||
tag_cloud = defaultdict(int)
|
||||
for article in generator.articles:
|
||||
for tag in getattr(article, 'tags', []):
|
||||
tag_cloud[tag] += 1
|
||||
|
||||
tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
|
||||
tag_cloud = tag_cloud[:generator.settings.get('TAG_CLOUD_MAX_ITEMS')]
|
||||
|
||||
tags = list(map(itemgetter(1), tag_cloud))
|
||||
if tags:
|
||||
max_count = max(tags)
|
||||
steps = generator.settings.get('TAG_CLOUD_STEPS')
|
||||
|
||||
# calculate word sizes
|
||||
generator.tag_cloud = [
|
||||
(
|
||||
tag,
|
||||
int(math.floor(steps - (steps - 1) * math.log(count)
|
||||
/ (math.log(max_count)or 1)))
|
||||
)
|
||||
for tag, count in tag_cloud
|
||||
]
|
||||
# put words in chaos
|
||||
random.shuffle(generator.tag_cloud)
|
||||
|
||||
# and generate the output :)
|
||||
|
||||
# order the categories per name
|
||||
generator.categories = list(generator.categories.items())
|
||||
generator.categories.sort(
|
||||
reverse=generator.settings['REVERSE_CATEGORY_ORDER'])
|
||||
|
||||
generator.authors = list(generator.authors.items())
|
||||
generator.authors.sort()
|
||||
|
||||
generator._update_context(('articles', 'dates', 'tags', 'categories',
|
||||
'tag_cloud', 'authors', 'related_posts'))
|
||||
|
||||
189
i18n_subsites/i18n_subsites.py
Normal file
189
i18n_subsites/i18n_subsites.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""i18n_subsites plugin creates i18n-ized subsites of the default site"""
|
||||
|
||||
|
||||
|
||||
import os
|
||||
import six
|
||||
import logging
|
||||
from itertools import chain
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
import gettext
|
||||
|
||||
from pelican import signals
|
||||
from pelican.contents import Page, Article
|
||||
from pelican.settings import configure_settings
|
||||
|
||||
from ._regenerate_context_helpers import regenerate_context_articles
|
||||
|
||||
|
||||
|
||||
# Global vars
|
||||
_main_site_generated = False
|
||||
_main_site_lang = "en"
|
||||
_main_siteurl = ''
|
||||
_lang_siteurls = None
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
def disable_lang_vars(pelican_obj):
|
||||
"""Set lang specific url and save_as vars to the non-lang defaults
|
||||
|
||||
e.g. ARTICLE_LANG_URL = ARTICLE_URL
|
||||
They would conflict with this plugin otherwise
|
||||
"""
|
||||
global _main_site_lang, _main_siteurl, _lang_siteurls
|
||||
s = pelican_obj.settings
|
||||
for content in ['ARTICLE', 'PAGE']:
|
||||
for meta in ['_URL', '_SAVE_AS']:
|
||||
s[content + '_LANG' + meta] = s[content + meta]
|
||||
if not _main_site_generated:
|
||||
_main_site_lang = s['DEFAULT_LANG']
|
||||
_main_siteurl = s['SITEURL']
|
||||
_lang_siteurls = [(lang, _main_siteurl + '/' + lang) for lang in s.get('I18N_SUBSITES', {}).keys()]
|
||||
# To be able to use url for main site root when SITEURL == '' (e.g. when developing)
|
||||
_lang_siteurls = [(_main_site_lang, ('/' if _main_siteurl == '' else _main_siteurl))] + _lang_siteurls
|
||||
_lang_siteurls = OrderedDict(_lang_siteurls)
|
||||
|
||||
|
||||
|
||||
def create_lang_subsites(pelican_obj):
|
||||
"""For each language create a subsite using the lang-specific config
|
||||
|
||||
for each generated lang append language subpath to SITEURL and OUTPUT_PATH
|
||||
and set DEFAULT_LANG to the language code to change perception of what is translated
|
||||
and set DELETE_OUTPUT_DIRECTORY to False to prevent deleting output from previous runs
|
||||
Then generate the subsite using a PELICAN_CLASS instance and its run method.
|
||||
"""
|
||||
global _main_site_generated
|
||||
if _main_site_generated: # make sure this is only called once
|
||||
return
|
||||
else:
|
||||
_main_site_generated = True
|
||||
|
||||
orig_settings = pelican_obj.settings
|
||||
for lang, overrides in orig_settings.get('I18N_SUBSITES', {}).items():
|
||||
settings = orig_settings.copy()
|
||||
settings.update(overrides)
|
||||
settings['SITEURL'] = _lang_siteurls[lang]
|
||||
settings['OUTPUT_PATH'] = os.path.join(orig_settings['OUTPUT_PATH'], lang, '')
|
||||
settings['DEFAULT_LANG'] = lang # to change what is perceived as translations
|
||||
settings['DELETE_OUTPUT_DIRECTORY'] = False # prevent deletion of previous runs
|
||||
settings = configure_settings(settings) # to set LOCALE, etc.
|
||||
|
||||
cls = settings['PELICAN_CLASS']
|
||||
if isinstance(cls, six.string_types):
|
||||
module, cls_name = cls.rsplit('.', 1)
|
||||
module = __import__(module)
|
||||
cls = getattr(module, cls_name)
|
||||
|
||||
pelican_obj = cls(settings)
|
||||
logger.debug("Generating i18n subsite for lang '{}' using class '{}'".format(lang, str(cls)))
|
||||
pelican_obj.run()
|
||||
_main_site_generated = False # for autoreload mode
|
||||
|
||||
|
||||
|
||||
def move_translations_links(content_object):
|
||||
"""This function points translations links to the sub-sites
|
||||
|
||||
by prepending their location with the language code
|
||||
or directs an original DEFAULT_LANG translation back to top level site
|
||||
"""
|
||||
for translation in content_object.translations:
|
||||
if translation.lang == _main_site_lang:
|
||||
# cannot prepend, must take to top level
|
||||
lang_prepend = '../'
|
||||
else:
|
||||
lang_prepend = translation.lang + '/'
|
||||
translation.override_url = lang_prepend + translation.url
|
||||
|
||||
|
||||
|
||||
def update_generator_contents(generator, *args):
|
||||
"""Update the contents lists of a generator
|
||||
|
||||
Empty the (hidden_)translation attribute of article and pages generators
|
||||
to prevent generating the translations as they will be generated in the lang sub-site
|
||||
and point the content translations links to the sub-sites
|
||||
|
||||
Hide content without a translation for current DEFAULT_LANG
|
||||
if HIDE_UNTRANSLATED_CONTENT is True
|
||||
"""
|
||||
generator.translations = []
|
||||
is_pages_gen = hasattr(generator, 'pages')
|
||||
if is_pages_gen:
|
||||
generator.hidden_translations = []
|
||||
for page in chain(generator.pages, generator.hidden_pages):
|
||||
move_translations_links(page)
|
||||
else: # is an article generator
|
||||
for article in chain(generator.articles, generator.drafts):
|
||||
move_translations_links(article)
|
||||
|
||||
if not generator.settings.get('HIDE_UNTRANSLATED_CONTENT', True):
|
||||
return
|
||||
contents = generator.pages if is_pages_gen else generator.articles
|
||||
hidden_contents = generator.hidden_pages if is_pages_gen else generator.drafts
|
||||
default_lang = generator.settings['DEFAULT_LANG']
|
||||
for content_object in contents[:]: # loop over copy for removing
|
||||
if content_object.lang != default_lang:
|
||||
if isinstance(content_object, Article):
|
||||
content_object.status = 'draft'
|
||||
elif isinstance(content_object, Page):
|
||||
content_object.status = 'hidden'
|
||||
contents.remove(content_object)
|
||||
hidden_contents.append(content_object)
|
||||
if not is_pages_gen: # regenerate categories, tags, etc. for articles
|
||||
if hasattr(generator, '_generate_context_aggregate'): # if implemented
|
||||
# Simulate __init__ for fields that need it
|
||||
generator.dates = {}
|
||||
generator.tags = defaultdict(list)
|
||||
generator.categories = defaultdict(list)
|
||||
generator.authors = defaultdict(list)
|
||||
generator._generate_context_aggregate()
|
||||
else: # fallback for Pelican 3.3.0
|
||||
regenerate_context_articles(generator)
|
||||
|
||||
|
||||
|
||||
def install_templates_translations(generator):
|
||||
"""Install gettext translations for current DEFAULT_LANG in the jinja2.Environment
|
||||
|
||||
if the 'jinja2.ext.i18n' jinja2 extension is enabled
|
||||
adds some useful variables into the template context
|
||||
"""
|
||||
generator.context['main_siteurl'] = _main_siteurl
|
||||
generator.context['main_lang'] = _main_site_lang
|
||||
generator.context['lang_siteurls'] = _lang_siteurls
|
||||
current_def_lang = generator.settings['DEFAULT_LANG']
|
||||
extra_siteurls = _lang_siteurls.copy()
|
||||
extra_siteurls.pop(current_def_lang)
|
||||
generator.context['extra_siteurls'] = extra_siteurls
|
||||
|
||||
if 'jinja2.ext.i18n' not in generator.settings['JINJA_EXTENSIONS']:
|
||||
return
|
||||
domain = generator.settings.get('I18N_GETTEXT_DOMAIN', 'messages')
|
||||
localedir = generator.settings.get('I18N_GETTEXT_LOCALEDIR')
|
||||
if localedir is None:
|
||||
localedir = os.path.join(generator.theme, 'translations')
|
||||
if current_def_lang == generator.settings.get('I18N_TEMPLATES_LANG', _main_site_lang):
|
||||
translations = gettext.NullTranslations()
|
||||
else:
|
||||
languages = [current_def_lang]
|
||||
try:
|
||||
translations = gettext.translation(domain, localedir, languages)
|
||||
except (IOError, OSError):
|
||||
logger.error("Cannot find translations for language '{}' in '{}' with domain '{}'. Installing NullTranslations.".format(languages[0], localedir, domain))
|
||||
translations = gettext.NullTranslations()
|
||||
newstyle = generator.settings.get('I18N_GETTEXT_NEWSTYLE', True)
|
||||
generator.env.install_gettext_translations(translations, newstyle)
|
||||
|
||||
|
||||
|
||||
def register():
|
||||
signals.initialized.connect(disable_lang_vars)
|
||||
signals.generator_init.connect(install_templates_translations)
|
||||
signals.article_generator_finalized.connect(update_generator_contents)
|
||||
signals.page_generator_finalized.connect(update_generator_contents)
|
||||
signals.finalized.connect(create_lang_subsites)
|
||||
113
i18n_subsites/implementing_language_buttons.rst
Normal file
113
i18n_subsites/implementing_language_buttons.rst
Normal file
@@ -0,0 +1,113 @@
|
||||
-----------------------------
|
||||
Implementing language buttons
|
||||
-----------------------------
|
||||
|
||||
Each article with translations has translations links, but that's the only way to switch between language subsites.
|
||||
|
||||
For this reason it is convenient to add language buttons to the top menu bar to make it simple to switch between the language subsites on all pages.
|
||||
|
||||
Example designs
|
||||
---------------
|
||||
|
||||
Language buttons showing other available languages
|
||||
..................................................
|
||||
|
||||
The ``extra_siteurls`` dictionary is a mapping of all other (not the *DEFAULT_LANG* of the current (sub-)site) languages to the *SITEURL* of the respective (sub-)sites
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<!-- SNIP -->
|
||||
<nav><ul>
|
||||
{% if extra_siteurls %}
|
||||
{% for lang, url in extra_siteurls.items() %}
|
||||
<li><a href="{{ url }}">{{ lang }}</a></li>
|
||||
{% endfor %}
|
||||
<!-- separator -->
|
||||
<li style="background-color: white; padding: 5px;"> </li>
|
||||
{% endif %}
|
||||
{% for title, link in MENUITEMS %}
|
||||
<!-- SNIP -->
|
||||
|
||||
Language buttons showing all available languages, current is active
|
||||
..................................................................
|
||||
|
||||
The ``extra_siteurls`` dictionary is a mapping of all languages to the *SITEURL* of the respective (sub-)sites. This template sets the language of the current (sub-)site as active.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<!-- SNIP -->
|
||||
<nav><ul>
|
||||
{% if lang_siteurls %}
|
||||
{% for lang, url in lang_siteurls.items() %}
|
||||
<li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ url }}">{{ lang }}</a></li>
|
||||
{% endfor %}
|
||||
<!-- separator -->
|
||||
<li style="background-color: white; padding: 5px;"> </li>
|
||||
{% endif %}
|
||||
{% for title, link in MENUITEMS %}
|
||||
<!-- SNIP -->
|
||||
|
||||
|
||||
Tips and tricks
|
||||
---------------
|
||||
|
||||
Showing more than language codes
|
||||
................................
|
||||
|
||||
If you want the language buttons to show e.g. the names of the languages or flags [#flags]_, not just the language codes, you can use a jinja filter to translate the language codes
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
languages_lookup = {
|
||||
'en': 'English',
|
||||
'cz': 'Čeština',
|
||||
}
|
||||
|
||||
def lookup_lang_name(lang_code):
|
||||
return languages_lookup[lang_code]
|
||||
|
||||
JINJA_FILTERS = {
|
||||
...
|
||||
'lookup_lang_name': lookup_lang_name,
|
||||
}
|
||||
|
||||
And then the link content becomes
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<!-- SNIP -->
|
||||
{% for lang, siteurl in lang_siteurls.items() %}
|
||||
<li{% if lang == DEFAULT_LANG %} class="active"{% endif %}><a href="{{ siteurl }}">{{ lang | lookup_lang_name }}</a></li>
|
||||
{% endfor %}
|
||||
<!-- SNIP -->
|
||||
|
||||
|
||||
Changing the order of language buttons
|
||||
......................................
|
||||
|
||||
Because ``lang_siteurls`` and ``extra_siteurls`` are instances of ``OrderedDict`` with ``main_lang`` being always the first key, you can change the order through a jinja filter.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def my_ordered_items(ordered_dict):
|
||||
items = list(ordered_dict.items())
|
||||
# swap first and last using tuple unpacking
|
||||
items[0], items[-1] = items[-1], items[0]
|
||||
return items
|
||||
|
||||
JINJA_FILTERS = {
|
||||
...
|
||||
'my_ordered_items': my_ordered_items,
|
||||
}
|
||||
|
||||
And then the ``for`` loop line in the template becomes
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<!-- SNIP -->
|
||||
{% for lang, siteurl in lang_siteurls | my_ordered_items %}
|
||||
<!-- SNIP -->
|
||||
|
||||
|
||||
.. [#flags] Although it may look nice, `w3 discourages it <http://www.w3.org/TR/i18n-html-tech-lang/#ri20040808.173208643>`_.
|
||||
142
i18n_subsites/localizing_using_jinja2.rst
Normal file
142
i18n_subsites/localizing_using_jinja2.rst
Normal file
@@ -0,0 +1,142 @@
|
||||
-----------------------------
|
||||
Localizing themes with Jinja2
|
||||
-----------------------------
|
||||
|
||||
1. Localize templates
|
||||
---------------------
|
||||
|
||||
To enable the |ext| extension in your templates, you must add it to
|
||||
*JINJA_EXTENSIONS* in your Pelican configuration
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
JINJA_EXTENSIONS = ['jinja2.ext.i18n', ...]
|
||||
|
||||
Then follow the `Jinja2 templating documentation for the I18N plugin <http://jinja.pocoo.org/docs/templates/#i18n>`_ to make your templates localizable. This usually means surrounding strings with the ``{% trans %}`` directive or using ``gettext()`` in expressions
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% trans %}translatable content{% endtrans %}
|
||||
{{ gettext('a translatable string') }}
|
||||
|
||||
For pluralization support, etc. consult the documentation
|
||||
|
||||
To enable `newstyle gettext calls <http://jinja.pocoo.org/docs/extensions/#newstyle-gettext>`_ the *I18N_GETTEXT_NEWSTYLE* config variable must be set to ``True`` (default).
|
||||
|
||||
.. |ext| replace:: ``jinja2.ext.i18n``
|
||||
|
||||
2. Specify translations location
|
||||
--------------------------------
|
||||
|
||||
The |ext| extension uses the `Python gettext library <http://docs.python.org/library/gettext.html>`_ for translating strings.
|
||||
|
||||
In your Pelican config you can give the path in which to look for translations in the *I18N_GETTEXT_LOCALEDIR* variable. If not given, it is assumed to be the ``translations`` subfolder in the top folder of the theme specified by *THEME*.
|
||||
|
||||
The domain of the translations (the name of each translation file is ``domain.mo``) is controlled by the *I18N_GETTEXT_DOMAIN* config variable (defaults to ``messages``).
|
||||
|
||||
Example
|
||||
.......
|
||||
|
||||
With the following in your Pelican settings file
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
I18N_GETTEXT_LOCALEDIR = 'some/path/'
|
||||
I18N_GETTEXT_DOMAIN = 'my_domain'
|
||||
|
||||
… the translation for language 'cz' will be expected to be in ``some/path/cz/LC_MESSAGES/my_domain.mo``
|
||||
|
||||
|
||||
3. Extract translatable strings and translate them
|
||||
--------------------------------------------------
|
||||
|
||||
There are many ways to extract translatable strings and create ``gettext`` compatible translations. You can create the ``*.po`` and ``*.mo`` message catalog files yourself, or you can use some helper tool as described in `the Python gettext library tutorial <http://docs.python.org/library/gettext.html#internationalizing-your-programs-and-modules>`_.
|
||||
|
||||
You of course don't need to provide a translation for the language in which the templates are written which is assumed to be the original *DEFAULT_LANG*. This can be overridden in the *I18N_TEMPLATES_LANG* variable.
|
||||
|
||||
Recommended tool: babel
|
||||
.......................
|
||||
|
||||
`Babel <http://babel.pocoo.org/>`_ makes it easy to extract translatable strings from the localized Jinja2 templates and assists with creating translations as documented in this `Jinja2-Babel tutorial <http://pythonhosted.org/Flask-Babel/#translating-applications>`_ [#flask]_ on which the following is based.
|
||||
|
||||
1. Add babel mapping
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Let's assume that you are localizing a theme in ``themes/my_theme/`` and that you use the default settings, i.e. the default domain ``messages`` and will put the translations in the ``translations`` subdirectory of the theme directory as ``themes/my_theme/translations/``.
|
||||
|
||||
It is up to you where to store babel mappings and translation files templates (``*.pot``), but a convenient place is to put them in ``themes/my_theme/`` and work in that directory. From now on let's assume that it will be our current working directory (CWD).
|
||||
|
||||
To tell babel to extract translatable strings from the templates create a mapping file ``babel.cfg`` with the following line
|
||||
|
||||
.. code-block:: cfg
|
||||
|
||||
[jinja2: ./templates/**.html]
|
||||
|
||||
|
||||
2. Extract translatable strings from templates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Run the following command to create a ``messages.pot`` message catalog template file from extracted translatable strings
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pybabel extract --mapping babel.cfg --output messages.pot ./
|
||||
|
||||
|
||||
3. Initialize message catalogs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to translate the template to language ``lang``, run the following command to create a message catalog
|
||||
``translations/lang/LC_MESSAGES/messages.po`` using the template ``messages.pot``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pybabel init --input-file messages.pot --output-dir translations/ --locale lang --domain messages
|
||||
|
||||
babel expects ``lang`` to be a valid locale identifier, so if e.g. you are translating for language ``cz`` but the corresponding locale is ``cs``, you have to use the locale identifier. Nevertheless, the gettext infrastructure should later correctly find the locale for a given language.
|
||||
|
||||
4. Fill the message catalogs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The message catalog files format is quite intuitive, it is fully documented in the `GNU gettext manual <http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files>`_. Essentially, you fill in the ``msgstr`` strings
|
||||
|
||||
|
||||
.. code-block:: po
|
||||
|
||||
msgid "just a simple string"
|
||||
msgstr "jenom jednoduchý řetězec"
|
||||
|
||||
msgid ""
|
||||
"some multiline string"
|
||||
"looks like this"
|
||||
msgstr ""
|
||||
"nějaký více řádkový řetězec"
|
||||
"vypadá takto"
|
||||
|
||||
You might also want to remove ``#,fuzzy`` flags once the translation is complete and reviewed to show that it can be compiled.
|
||||
|
||||
5. Compile the message catalogs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The message catalogs must be compiled into binary format using this command
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pybabel compile --directory translations/ --domain messages
|
||||
|
||||
This command might complain about "fuzzy" translations, which means you should review the translations and once done, remove the fuzzy flag line.
|
||||
|
||||
(6.) Update the catalogs when templates change
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you add any translatable patterns into your templates, you have to update your message catalogs too.
|
||||
First you extract a new message catalog template as described in the 2. step. Then you run the following command [#pybabel_error]_
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pybabel update --input-file messages.pot --output-dir translations/ --domain messages
|
||||
|
||||
This will merge the new patterns with the old ones. Once you review and fill them, you have to recompile them as described in the 5. step.
|
||||
|
||||
.. [#flask] Although the tutorial is focused on Flask-based web applications, the linked translation tutorial is not Flask-specific.
|
||||
.. [#pybabel_error] If you get an error ``TypeError: must be str, not bytes`` with Python 3.3, it is likely you are suffering from this `bug <https://github.com/mitsuhiko/flask-babel/issues/43>`_. Until the fix is released, you can use babel with Python 2.7.
|
||||
@@ -48,5 +48,5 @@ def add_ical(generator, metadata):
|
||||
|
||||
|
||||
def register():
|
||||
signals.pages_generator_init.connect(init_cal)
|
||||
signals.pages_generate_context.connect(add_ical)
|
||||
signals.page_generator_init.connect(init_cal)
|
||||
signals.page_generator_context.connect(add_ical)
|
||||
|
||||
1
interlinks/__init__.py
Normal file
1
interlinks/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .interlinks import *
|
||||
46
interlinks/interlinks.py
Normal file
46
interlinks/interlinks.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Interlinks
|
||||
=========================
|
||||
|
||||
This plugin allows you to include "interwiki" or shortcuts links into the blog, as keyword>rest_of_url
|
||||
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from pelican import signals
|
||||
import re
|
||||
|
||||
interlinks = {}
|
||||
|
||||
def getSettings (generator):
|
||||
|
||||
global interlinks
|
||||
|
||||
interlinks = {'this': generator.settings['SITEURL']+"/"}
|
||||
if 'INTERLINKS' in generator.settings:
|
||||
for key, value in generator.settings['INTERLINKS'].items():
|
||||
interlinks[key] = value
|
||||
|
||||
def content_object_init(instance):
|
||||
|
||||
if instance._content is not None:
|
||||
content = instance._content
|
||||
# use Python's built-in parser so no duplicated html & body tags appear, or use tag.unwrap()
|
||||
text = BeautifulSoup(content, "html.parser")
|
||||
|
||||
if 'a' in content:
|
||||
for link in text.find_all(href=re.compile("(.+?)>")):
|
||||
url = link.get('href')
|
||||
m = re.search(r"(.+?)>", url).groups()
|
||||
name = m[0]
|
||||
if name in interlinks:
|
||||
hi = url.replace(name+">",interlinks[name])
|
||||
link['href'] = hi
|
||||
|
||||
instance._content = text.decode()
|
||||
|
||||
def register():
|
||||
signals.generator_init.connect(getSettings)
|
||||
signals.content_object_init.connect(content_object_init)
|
||||
49
interlinks/readme.md
Normal file
49
interlinks/readme.md
Normal file
@@ -0,0 +1,49 @@
|
||||
Interlinks
|
||||
=========================
|
||||
|
||||
This plugin lets you add frequently used URLs to your markup using short keywords. Short URL format is
|
||||
``keyword>rest-of-url`` where ``keyword`` is defined in the settings.py. Later it is replaced with acutal URL in
|
||||
the generated HTML output.
|
||||
|
||||
|
||||
Requirements
|
||||
--------------------
|
||||
``interlinks`` requires BeautifulSoup
|
||||
|
||||
pip install beautifulsoup4
|
||||
|
||||
Installation
|
||||
--------------------
|
||||
|
||||
Install the plugin normally (plugins folder), then add interlinks in the settings.py:
|
||||
|
||||
PLUGINS = ["interlinks"]
|
||||
|
||||
Usage
|
||||
------------------
|
||||
|
||||
The interlinks are specified in the settings.py file as (example):
|
||||
|
||||
INTERLINKS = {
|
||||
'wikipedia_en': 'http://en.wikipedia.org/wiki/',
|
||||
'wikipedia_es': 'http://es.wikipedia.org/wiki/',
|
||||
'ddg': 'https://duckduckgo.com/?q='
|
||||
}
|
||||
|
||||
There's also a default key, ``this``, that is mapped to the ``SITEURL`` variable.
|
||||
|
||||
Then, in a blog post, you just create a normal link but adding the ``keyword>`` syntax in the url specification, followed by the rest of the url.
|
||||
|
||||
Example
|
||||
-------------------
|
||||
(markdown syntax)
|
||||
|
||||
[Normal boring link](http://www.example.com). But this is a [cool link](this>) that links to this site.
|
||||
|
||||
Search in [Wikipedia](wikipedia_en>python), ([here](wikipedia_es>python) in spanish). Also can [search](ddg>python) it.
|
||||
|
||||
All the above will be rendered as:
|
||||
|
||||
<p><a href="http://www.example.com">Normal boring link</a>. But this is a <a href="http://[yoursite]/index.html">cool link</a> that links to this site.</p>
|
||||
|
||||
<p>Search in <a href="http://en.wikipedia.org/wiki/python">Wikipedia</a>, (<a href="http://es.wikipedia.org/wiki/python">here</a> in spanish). Also can <a href="https://duckduckgo.com/?q=python">search</a> it.</p>
|
||||
9
interlinks/test_data/testme.md
Normal file
9
interlinks/test_data/testme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
Title: Testing
|
||||
Date: 3000-07-09
|
||||
Slug: plugin-test
|
||||
|
||||
Testeando un poco la cosa
|
||||
|
||||
[Normal boring link](http://www.example.com). But this is a [cool link](this>) that links to this site.
|
||||
|
||||
Search in [Wikipedia](wikipedia_en>python), ([here](wikipedia_es>python) in spanish). Also can [search](ddg>python) it.
|
||||
@@ -1,75 +0,0 @@
|
||||
Latex Plugin For Pelican
|
||||
========================
|
||||
|
||||
This plugin allows you to write mathematical equations in your articles using Latex.
|
||||
It uses the MathJax Latex JavaScript library to render latex that is embedded in
|
||||
between `$..$` for inline math and `$$..$$` for displayed math. It also allows for
|
||||
writing equations in by using `\begin{equation}`...`\end{equation}`.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
To enable, ensure that `latex.py` is put somewhere that is accessible.
|
||||
Then use as follows by adding the following to your settings.py:
|
||||
|
||||
PLUGINS = ["latex"]
|
||||
|
||||
Be careful: Not loading the plugin is easy to do, and difficult to detect. To
|
||||
make life easier, find where pelican is installed, and then copy the plugin
|
||||
there. An easy way to find where pelican is installed is to verbose list the
|
||||
available themes by typing `pelican-themes -l -v`.
|
||||
|
||||
Once the pelican folder is found, copy `latex.py` to the `plugins` folder. Then
|
||||
add to settings.py like this:
|
||||
|
||||
PLUGINS = ["pelican.plugins.latex"]
|
||||
|
||||
Now all that is left to do is to embed the following to your template file
|
||||
between the `<head>` parameters (for the NotMyIdea template, this file is base.html)
|
||||
|
||||
{% if article and article.latex %}
|
||||
{{ article.latex }}
|
||||
{% endif %}
|
||||
{% if page and page.latex %}
|
||||
{{ page.latex }}
|
||||
{% endif %}
|
||||
|
||||
Usage
|
||||
-----
|
||||
Latex will be embedded in every article. If however you want latex only for
|
||||
selected articles, then in settings.py, add
|
||||
|
||||
LATEX = 'article'
|
||||
|
||||
And in each article, add the metadata key `latex:`. For example, with the above
|
||||
settings, creating an article that I want to render latex math, I would just
|
||||
include 'Latex' as part of the metadata without any value:
|
||||
|
||||
Date: 1 sep 2012
|
||||
Status: draft
|
||||
Latex:
|
||||
|
||||
Latex Examples
|
||||
--------------
|
||||
###Inline
|
||||
Latex between `$`..`$`, for example, `$`x^2`$`, will be rendered inline
|
||||
with respect to the current html block.
|
||||
|
||||
###Displayed Math
|
||||
Latex between `$$`..`$$`, for example, `$$`x^2`$$`, will be rendered centered in a
|
||||
new paragraph.
|
||||
|
||||
###Equations
|
||||
Latex between `\begin` and `\end`, for example, `begin{equation}` x^2 `\end{equation}`,
|
||||
will be rendered centered in a new paragraph with a right justified equation number
|
||||
at the top of the paragraph. This equation number can be referenced in the document.
|
||||
To do this, use a `label` inside of the equation format and then refer to that label
|
||||
using `ref`. For example: `begin{equation}` `\label{eq}` X^2 `\end{equation}`. Now
|
||||
refer to that equation number by `$`\ref{eq}`$`.
|
||||
|
||||
Template And Article Examples
|
||||
-----------------------------
|
||||
To see an example of this plugin in action, look at
|
||||
[this article](http://doctrina.org/How-RSA-Works-With-Examples.html). To see how
|
||||
this plugin works with a template, look at
|
||||
[this template](https://github.com/barrysteyn/pelican_theme-personal_blog).
|
||||
@@ -1 +0,0 @@
|
||||
from .latex import *
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Latex Plugin For Pelican
|
||||
========================
|
||||
|
||||
This plugin allows you to write mathematical equations in your articles using Latex.
|
||||
It uses the MathJax Latex JavaScript library to render latex that is embedded in
|
||||
between `$..$` for inline math and `$$..$$` for displayed math. It also allows for
|
||||
writing equations in by using `\begin{equation}`...`\end{equation}`.
|
||||
"""
|
||||
|
||||
from pelican import signals
|
||||
|
||||
latexScript = """
|
||||
<script src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type= "text/javascript">
|
||||
MathJax.Hub.Config({
|
||||
config: ["MMLorHTML.js"],
|
||||
jax: ["input/TeX","input/MathML","output/HTML-CSS","output/NativeMML"],
|
||||
TeX: { extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"], equationNumbers: { autoNumber: "AMS" } },
|
||||
extensions: ["tex2jax.js","mml2jax.js","MathMenu.js","MathZoom.js"],
|
||||
tex2jax: {
|
||||
inlineMath: [ [\'$\',\'$\'] ],
|
||||
displayMath: [ [\'$$\',\'$$\'] ],
|
||||
processEscapes: true },
|
||||
"HTML-CSS": {
|
||||
styles: { ".MathJax .mo, .MathJax .mi": {color: "black ! important"}}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
def addLatex(gen, metadata):
|
||||
"""
|
||||
The registered handler for the latex plugin. It will add
|
||||
the latex script to the article metadata
|
||||
"""
|
||||
if 'LATEX' in gen.settings.keys() and gen.settings['LATEX'] == 'article':
|
||||
if 'latex' in metadata.keys():
|
||||
metadata['latex'] = latexScript
|
||||
else:
|
||||
metadata['latex'] = latexScript
|
||||
|
||||
def register():
|
||||
"""
|
||||
Plugin registration
|
||||
"""
|
||||
signals.article_generate_context.connect(addLatex)
|
||||
signals.pages_generate_context.connect(addLatex)
|
||||
130
liquid_tags/Readme.md
Normal file
130
liquid_tags/Readme.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Liquid-style Tags
|
||||
*Author: Jake Vanderplas <jakevdp@cs.washington.edu>*
|
||||
|
||||
This plugin allows liquid-style tags to be inserted into markdown within
|
||||
Pelican documents. Liquid uses tags bounded by ``{% ... %}``, and is used
|
||||
to extend markdown in other blogging platforms such as octopress.
|
||||
|
||||
This set of extensions does not actually interface with liquid, but allows
|
||||
users to define their own liquid-style tags which will be inserted into
|
||||
the markdown preprocessor stream. There are several built-in tags, which
|
||||
can be added as follows.
|
||||
|
||||
First, in your pelicanconf.py file, add the plugins you want to use:
|
||||
|
||||
PLUGIN_PATH = '/path/to/pelican-plugins'
|
||||
PLUGINS = ['liquid_tags.img', 'liquid_tags.video',
|
||||
'liquid_tags.youtube', 'liquid_tags.vimeo',
|
||||
'liquid_tags.include_code', 'liquid_tags.notebook']
|
||||
|
||||
There are several options available
|
||||
|
||||
## Image Tag
|
||||
To insert a sized and labeled image in your document, enable the
|
||||
``liquid_tags.img`` plugin and use the following:
|
||||
|
||||
{% img [class name(s)] path/to/image [width [height]] [title text | "title text" ["alt text"]] %}
|
||||
|
||||
## Youtube Tag
|
||||
To insert youtube video into a post, enable the
|
||||
``liquid_tags.youtube`` plugin, and add to your document:
|
||||
|
||||
{% youtube youtube_id [width] [height] %}
|
||||
|
||||
The width and height are in pixels, and can be optionally specified. If they
|
||||
are not, then the dimensions will be 640 (wide) by 390 (tall).
|
||||
|
||||
## Vimeo Tag
|
||||
To insert a Vimeo video into a post, enable the
|
||||
``liquid_tags.vimeo`` plugin, and add to your document:
|
||||
|
||||
{% vimeo vimeo_id [width] [height] %}
|
||||
|
||||
The width and height are in pixels, and can be optionally specified. If they
|
||||
are not, then the dimensions will be 640 (wide) by 390 (tall).
|
||||
|
||||
## Video Tag
|
||||
To insert flash/HTML5-friendly video into a post, enable the
|
||||
``liquid_tags.video`` plugin, and add to your document:
|
||||
|
||||
{% video /url/to/video.mp4 [width] [height] [/path/to/poster.png] %}
|
||||
|
||||
The width and height are in pixels, and can be optionally specified. If they
|
||||
are not, then the original video size will be used. The poster is an image
|
||||
which is used as a preview of the video.
|
||||
|
||||
To use a video from file, make sure it's in a static directory and put in
|
||||
the appropriate url.
|
||||
|
||||
## Include Code
|
||||
To include code from a file in your document with a link to the original
|
||||
file, enable the ``liquid_tags.include_code`` plugin, and add to your
|
||||
document:
|
||||
|
||||
{% include_code /path/to/code.py [lang:python] [lines:X-Y] [:hidefilename:] [title] %}
|
||||
|
||||
All arguments are optional but their order must be kept. `:hidefilename:` is
|
||||
only allowed if a title is also given.
|
||||
|
||||
{% include_code /path/to/code.py lines:1-10 :hidefilename: Test Example %}
|
||||
|
||||
This example will show the first 10 lines of the file while hiding the actual
|
||||
filename.
|
||||
|
||||
The script must be in the ``code`` subdirectory of your content folder:
|
||||
this default location can be changed by specifying
|
||||
|
||||
CODE_DIR = 'code'
|
||||
|
||||
within your configuration file. Additionally, in order for the resulting
|
||||
hyperlink to work, this directory must be listed under the STATIC_PATHS
|
||||
setting, e.g.:
|
||||
|
||||
STATIC_PATHS = ['images', 'code']
|
||||
|
||||
## IPython notebooks
|
||||
To insert an ipython notebook into your post, enable the
|
||||
``liquid_tags.notebook`` plugin and add to your document:
|
||||
|
||||
{% notebook filename.ipynb %}
|
||||
|
||||
The file should be specified relative to the ``notebooks`` subdirectory of the
|
||||
content directory. Optionally, this subdirectory can be specified in the
|
||||
config file:
|
||||
|
||||
NOTEBOOK_DIR = 'notebooks'
|
||||
|
||||
Because the conversion and rendering of notebooks is rather involved, there
|
||||
are a few extra steps required for this plugin:
|
||||
|
||||
- First, you will need to install IPython >= 1.0 [1]_
|
||||
|
||||
- After typing "make html" when using the notebook tag, a file called
|
||||
``_nb_header.html`` will be produced in the main directory. The content
|
||||
of the file should be included in the header of the theme. An easy way
|
||||
to accomplish this is to add the following lines within the header template
|
||||
of the theme you use:
|
||||
|
||||
{% if EXTRA_HEADER %}
|
||||
{{ EXTRA_HEADER }}
|
||||
{% endif %}
|
||||
|
||||
and in your configuration file, include the line:
|
||||
|
||||
EXTRA_HEADER = open('_nb_header.html').read().decode('utf-8')
|
||||
|
||||
this will insert the proper css formatting into your document.
|
||||
|
||||
### Collapsible Code in IPython Notebooks
|
||||
|
||||
The plugin also enables collapsible code input boxes. For this to work
|
||||
you first need to copy the file ``pelicanhtml_1.tpl`` (for IPython
|
||||
1.x) ``pelicanhtml_2.tpl`` (for IPython 2.x) to the top level of your
|
||||
Pelican blog. Notebook input cells containing the comment line ``#
|
||||
<!-- collapse=True -->`` will be collapsed when the html page is
|
||||
loaded and can be expanded by clicking on them. Cells containing the
|
||||
comment line ``# <!-- collapse=False -->`` will be open on load but
|
||||
can be collapsed by clicking on their header. Cells without collapse
|
||||
comments are rendered as standard code input cells.
|
||||
|
||||
[1] http://ipython.org/
|
||||
1
liquid_tags/__init__.py
Normal file
1
liquid_tags/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .liquid_tags import *
|
||||
65
liquid_tags/img.py
Normal file
65
liquid_tags/img.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Image Tag
|
||||
---------
|
||||
This implements a Liquid-style image tag for Pelican,
|
||||
based on the octopress image tag [1]_
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% img [class name(s)] [http[s]:/]/path/to/image [width [height]] [title text | "title text" ["alt text"]] %}
|
||||
|
||||
Examples
|
||||
--------
|
||||
{% img /images/ninja.png Ninja Attack! %}
|
||||
{% img left half http://site.com/images/ninja.png Ninja Attack! %}
|
||||
{% img left half http://site.com/images/ninja.png 150 150 "Ninja Attack!" "Ninja in attack posture" %}
|
||||
|
||||
Output
|
||||
------
|
||||
<img src="/images/ninja.png">
|
||||
<img class="left half" src="http://site.com/images/ninja.png" title="Ninja Attack!" alt="Ninja Attack!">
|
||||
<img class="left half" src="http://site.com/images/ninja.png" width="150" height="150" title="Ninja Attack!" alt="Ninja in attack posture">
|
||||
|
||||
[1] https://github.com/imathis/octopress/blob/master/plugins/image_tag.rb
|
||||
"""
|
||||
import re
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
SYNTAX = '{% img [class name(s)] [http[s]:/]/path/to/image [width [height]] [title text | "title text" ["alt text"]] %}'
|
||||
|
||||
# Regular expression to match the entire syntax
|
||||
ReImg = re.compile("""(?P<class>\S.*\s+)?(?P<src>(?:https?:\/\/|\/|\S+\/)\S+)(?:\s+(?P<width>\d+))?(?:\s+(?P<height>\d+))?(?P<title>\s+.+)?""")
|
||||
|
||||
# Regular expression to split the title and alt text
|
||||
ReTitleAlt = re.compile("""(?:"|')(?P<title>[^"']+)?(?:"|')\s+(?:"|')(?P<alt>[^"']+)?(?:"|')""")
|
||||
|
||||
|
||||
@LiquidTags.register('img')
|
||||
def img(preprocessor, tag, markup):
|
||||
attrs = None
|
||||
|
||||
# Parse the markup string
|
||||
match = ReImg.search(markup)
|
||||
if match:
|
||||
attrs = dict([(key, val.strip())
|
||||
for (key, val) in match.groupdict().iteritems() if val])
|
||||
else:
|
||||
raise ValueError('Error processing input. '
|
||||
'Expected syntax: {0}'.format(SYNTAX))
|
||||
|
||||
# Check if alt text is present -- if so, split it from title
|
||||
if 'title' in attrs:
|
||||
match = ReTitleAlt.search(attrs['title'])
|
||||
if match:
|
||||
attrs.update(match.groupdict())
|
||||
if not attrs.get('alt'):
|
||||
attrs['alt'] = attrs['title']
|
||||
|
||||
# Return the formatted text
|
||||
return "<img {0}>".format(' '.join('{0}="{1}"'.format(key, val)
|
||||
for (key, val) in attrs.iteritems()))
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows image tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
|
||||
128
liquid_tags/include_code.py
Normal file
128
liquid_tags/include_code.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Include Code Tag
|
||||
----------------
|
||||
This implements a Liquid-style video tag for Pelican,
|
||||
based on the octopress video tag [1]_
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% include_code path/to/code [lang:python] [Title text] %}
|
||||
|
||||
The "path to code" is specified relative to the ``code`` subdirectory of
|
||||
the content directory Optionally, this subdirectory can be specified in the
|
||||
config file:
|
||||
|
||||
CODE_DIR = 'code'
|
||||
|
||||
Example
|
||||
-------
|
||||
{% include_code myscript.py %}
|
||||
|
||||
This will import myscript.py from content/code/myscript.py
|
||||
and output the contents in a syntax highlighted code block inside a figure,
|
||||
with a figcaption listing the file name and download link.
|
||||
|
||||
The file link will be valid only if the 'code' directory is listed
|
||||
in the STATIC_PATHS setting, e.g.:
|
||||
|
||||
STATIC_PATHS = ['images', 'code']
|
||||
|
||||
[1] https://github.com/imathis/octopress/blob/master/plugins/include_code.rb
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
|
||||
SYNTAX = "{% include_code /path/to/code.py [lang:python] [lines:X-Y] [:hidefilename:] [title] %}"
|
||||
FORMAT = re.compile(r"""
|
||||
^(?:\s+)? # Allow whitespace at beginning
|
||||
(?P<src>\S+) # Find the path
|
||||
(?:\s+)? # Whitespace
|
||||
(?:(?:lang:)(?P<lang>\S+))? # Optional language
|
||||
(?:\s+)? # Whitespace
|
||||
(?:(?:lines:)(?P<lines>\d+-\d+))? # Optional lines
|
||||
(?:\s+)? # Whitespace
|
||||
(?P<hidefilename>:hidefilename:)? # Hidefilename flag
|
||||
(?:\s+)? # Whitespace
|
||||
(?P<title>.+)?$ # Optional title
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
@LiquidTags.register('include_code')
|
||||
def include_code(preprocessor, tag, markup):
|
||||
|
||||
title = None
|
||||
lang = None
|
||||
src = None
|
||||
|
||||
match = FORMAT.search(markup)
|
||||
if match:
|
||||
argdict = match.groupdict()
|
||||
title = argdict['title'] or ""
|
||||
lang = argdict['lang']
|
||||
lines = argdict['lines']
|
||||
hide_filename = bool(argdict['hidefilename'])
|
||||
if lines:
|
||||
first_line, last_line = map(int, lines.split("-"))
|
||||
src = argdict['src']
|
||||
|
||||
if not src:
|
||||
raise ValueError("Error processing input, "
|
||||
"expected syntax: {0}".format(SYNTAX))
|
||||
|
||||
settings = preprocessor.configs.config['settings']
|
||||
code_dir = settings.get('CODE_DIR', 'code')
|
||||
code_path = os.path.join('content', code_dir, src)
|
||||
|
||||
if not os.path.exists(code_path):
|
||||
raise ValueError("File {0} could not be found".format(code_path))
|
||||
|
||||
with open(code_path) as fh:
|
||||
if lines:
|
||||
code = fh.readlines()[first_line - 1: last_line]
|
||||
code[-1] = code[-1].rstrip()
|
||||
code = "".join(code)
|
||||
else:
|
||||
code = fh.read()
|
||||
|
||||
if not title and hide_filename:
|
||||
raise ValueError("Either title must be specified or filename must "
|
||||
"be available")
|
||||
|
||||
if not hide_filename:
|
||||
title += " %s" % os.path.basename(src)
|
||||
if lines:
|
||||
title += " [Lines %s]" % lines
|
||||
title = title.strip()
|
||||
|
||||
url = '/{0}/{1}'.format(code_dir, src)
|
||||
url = re.sub('/+', '/', url)
|
||||
|
||||
open_tag = ("<figure class='code'>\n<figcaption><span>{title}</span> "
|
||||
"<a href='{url}'>download</a></figcaption>".format(title=title,
|
||||
url=url))
|
||||
close_tag = "</figure>"
|
||||
|
||||
# store HTML tags in the stash. This prevents them from being
|
||||
# modified by markdown.
|
||||
open_tag = preprocessor.configs.htmlStash.store(open_tag, safe=True)
|
||||
close_tag = preprocessor.configs.htmlStash.store(close_tag, safe=True)
|
||||
|
||||
if lang:
|
||||
lang_include = ':::' + lang + '\n '
|
||||
else:
|
||||
lang_include = ''
|
||||
|
||||
source = (open_tag
|
||||
+ '\n\n '
|
||||
+ lang_include
|
||||
+ '\n '.join(code.split('\n')) + '\n\n'
|
||||
+ close_tag + '\n')
|
||||
|
||||
return source
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows image tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
16
liquid_tags/liquid_tags.py
Normal file
16
liquid_tags/liquid_tags.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from pelican import signals
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
|
||||
def addLiquidTags(gen):
|
||||
if not gen.settings.get('MD_EXTENSIONS'):
|
||||
from pelican.settings import DEFAULT_CONFIG
|
||||
gen.settings['MD_EXTENSIONS'] = DEFAULT_CONFIG['MD_EXTENSIONS']
|
||||
|
||||
if LiquidTags not in gen.settings['MD_EXTENSIONS']:
|
||||
configs = dict(settings=gen.settings)
|
||||
gen.settings['MD_EXTENSIONS'].append(LiquidTags(configs))
|
||||
|
||||
|
||||
def register():
|
||||
signals.initialized.connect(addLiquidTags)
|
||||
27
liquid_tags/literal.py
Normal file
27
liquid_tags/literal.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Literal Tag
|
||||
-----------
|
||||
This implements a tag that allows explicitly showing commands which would
|
||||
otherwise be interpreted as a liquid tag.
|
||||
|
||||
For example, the line
|
||||
|
||||
{% literal video arg1 arg2 %}
|
||||
|
||||
would result in the following line:
|
||||
|
||||
{% video arg1 arg2 %}
|
||||
|
||||
This is useful when the resulting line would be interpreted as another
|
||||
liquid-style tag.
|
||||
"""
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
@LiquidTags.register('literal')
|
||||
def literal(preprocessor, tag, markup):
|
||||
return '{%% %s %%}' % markup
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows image tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
|
||||
77
liquid_tags/mdx_liquid_tags.py
Normal file
77
liquid_tags/mdx_liquid_tags.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Markdown Extension for Liquid-style Tags
|
||||
----------------------------------------
|
||||
A markdown extension to allow user-defined tags of the form::
|
||||
|
||||
{% tag arg1 arg2 ... argn %}
|
||||
|
||||
Where "tag" is associated with some user-defined extension.
|
||||
These result in a preprocess step within markdown that produces
|
||||
either markdown or html.
|
||||
"""
|
||||
import warnings
|
||||
import markdown
|
||||
import itertools
|
||||
import re
|
||||
import os
|
||||
from functools import wraps
|
||||
|
||||
# Define some regular expressions
|
||||
LIQUID_TAG = re.compile(r'\{%.*?%\}')
|
||||
EXTRACT_TAG = re.compile(r'(?:\s*)(\S+)(?:\s*)')
|
||||
|
||||
|
||||
class _LiquidTagsPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
_tags = {}
|
||||
def __init__(self, configs):
|
||||
self.configs = configs
|
||||
|
||||
def run(self, lines):
|
||||
page = '\n'.join(lines)
|
||||
liquid_tags = LIQUID_TAG.findall(page)
|
||||
|
||||
for i, markup in enumerate(liquid_tags):
|
||||
# remove {% %}
|
||||
markup = markup[2:-2]
|
||||
tag = EXTRACT_TAG.match(markup).groups()[0]
|
||||
markup = EXTRACT_TAG.sub('', markup, 1)
|
||||
if tag in self._tags:
|
||||
liquid_tags[i] = self._tags[tag](self, tag, markup.strip())
|
||||
|
||||
# add an empty string to liquid_tags so that chaining works
|
||||
liquid_tags.append('')
|
||||
|
||||
# reconstruct string
|
||||
page = ''.join(itertools.chain(*zip(LIQUID_TAG.split(page),
|
||||
liquid_tags)))
|
||||
|
||||
# resplit the lines
|
||||
return page.split("\n")
|
||||
|
||||
|
||||
class LiquidTags(markdown.Extension):
|
||||
"""Wrapper for MDPreprocessor"""
|
||||
@classmethod
|
||||
def register(cls, tag):
|
||||
"""Decorator to register a new include tag"""
|
||||
def dec(func):
|
||||
if tag in _LiquidTagsPreprocessor._tags:
|
||||
warnings.warn("Enhanced Markdown: overriding tag '%s'" % tag)
|
||||
_LiquidTagsPreprocessor._tags[tag] = func
|
||||
return func
|
||||
return dec
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
self.htmlStash = md.htmlStash
|
||||
md.registerExtension(self)
|
||||
# for the include_code preprocessor, we need to re-run the
|
||||
# fenced code block preprocessor after substituting the code.
|
||||
# Because the fenced code processor is run before, {% %} tags
|
||||
# within equations will not be parsed as an include.
|
||||
md.preprocessors.add('mdincludes',
|
||||
_LiquidTagsPreprocessor(self), ">html_block")
|
||||
|
||||
|
||||
def makeExtension(configs=None):
|
||||
"""Wrapper for a MarkDown extension"""
|
||||
return LiquidTags(configs=configs)
|
||||
319
liquid_tags/notebook.py
Normal file
319
liquid_tags/notebook.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Notebook Tag
|
||||
------------
|
||||
This is a liquid-style tag to include a static html rendering of an IPython
|
||||
notebook in a blog post.
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% notebook filename.ipynb [ cells[start:end] ]%}
|
||||
|
||||
The file should be specified relative to the ``notebooks`` subdirectory of the
|
||||
content directory. Optionally, this subdirectory can be specified in the
|
||||
config file:
|
||||
|
||||
NOTEBOOK_DIR = 'notebooks'
|
||||
|
||||
The cells[start:end] statement is optional, and can be used to specify which
|
||||
block of cells from the notebook to include.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- The plugin requires IPython version 1.0 or above. It no longer supports the
|
||||
standalone nbconvert package, which has been deprecated.
|
||||
|
||||
Details
|
||||
-------
|
||||
Because the notebook relies on some rather extensive custom CSS, the use of
|
||||
this plugin requires additional CSS to be inserted into the blog theme.
|
||||
After typing "make html" when using the notebook tag, a file called
|
||||
``_nb_header.html`` will be produced in the main directory. The content
|
||||
of the file should be included in the header of the theme. An easy way
|
||||
to accomplish this is to add the following lines within the header template
|
||||
of the theme you use:
|
||||
|
||||
{% if EXTRA_HEADER %}
|
||||
{{ EXTRA_HEADER }}
|
||||
{% endif %}
|
||||
|
||||
and in your ``pelicanconf.py`` file, include the line:
|
||||
|
||||
EXTRA_HEADER = open('_nb_header.html').read().decode('utf-8')
|
||||
|
||||
this will insert the appropriate CSS. All efforts have been made to ensure
|
||||
that this CSS will not override formats within the blog theme, but there may
|
||||
still be some conflicts.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
import IPython
|
||||
if not LooseVersion(IPython.__version__) >= '1.0':
|
||||
raise ValueError("IPython version 1.0+ required for notebook tag")
|
||||
|
||||
from IPython import nbconvert
|
||||
|
||||
try:
|
||||
from IPython.nbconvert.filters.highlight import _pygments_highlight
|
||||
except ImportError:
|
||||
# IPython < 2.0
|
||||
from IPython.nbconvert.filters.highlight import _pygment_highlight as _pygments_highlight
|
||||
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
||||
from IPython.nbconvert.exporters import HTMLExporter
|
||||
from IPython.config import Config
|
||||
|
||||
from IPython.nbformat import current as nbformat
|
||||
|
||||
try:
|
||||
from IPython.nbconvert.preprocessors import Preprocessor
|
||||
except ImportError:
|
||||
# IPython < 2.0
|
||||
from IPython.nbconvert.transformers import Transformer as Preprocessor
|
||||
|
||||
from IPython.utils.traitlets import Integer
|
||||
from copy import deepcopy
|
||||
|
||||
from jinja2 import DictLoader
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Some code that will be added to the header:
|
||||
# Some of the following javascript/css include is adapted from
|
||||
# IPython/nbconvert/templates/fullhtml.tpl, while some are custom tags
|
||||
# specifically designed to make the results look good within the
|
||||
# pelican-octopress theme.
|
||||
JS_INCLUDE = r"""
|
||||
<style type="text/css">
|
||||
/* Overrides of notebook CSS for static HTML export */
|
||||
div.entry-content {
|
||||
overflow: visible;
|
||||
padding: 8px;
|
||||
}
|
||||
.input_area {
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
a.heading-anchor {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.rendered_html
|
||||
code {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
pre.ipynb {
|
||||
color: black;
|
||||
background: #f7f7f7;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
margin: 0px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* remove the prompt div from text cells */
|
||||
div.text_cell .prompt {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* remove horizontal padding from text cells, */
|
||||
/* so it aligns with outer body text */
|
||||
div.text_cell_render {
|
||||
padding: 0.5em 0em;
|
||||
}
|
||||
|
||||
img.anim_icon{padding:0; border:0; vertical-align:middle; -webkit-box-shadow:none; -box-shadow:none}
|
||||
|
||||
div.collapseheader {
|
||||
width=100%;
|
||||
background-color:#d3d3d3;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS_HTML" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
init_mathjax = function() {
|
||||
if (window.MathJax) {
|
||||
// MathJax loaded
|
||||
MathJax.Hub.Config({
|
||||
tex2jax: {
|
||||
inlineMath: [ ['$','$'], ["\\(","\\)"] ],
|
||||
displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
|
||||
},
|
||||
displayAlign: 'left', // Change this to 'center' to center equations.
|
||||
"HTML-CSS": {
|
||||
styles: {'.MathJax_Display': {"margin": 0}}
|
||||
}
|
||||
});
|
||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||
}
|
||||
}
|
||||
init_mathjax();
|
||||
</script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(document).ready(function($) {
|
||||
$("div.collapseheader").click(function () {
|
||||
$header = $(this).children("span").first();
|
||||
$codearea = $(this).children(".input_area");
|
||||
console.log($(this).children());
|
||||
$codearea.slideToggle(500, function () {
|
||||
$header.text(function () {
|
||||
return $codearea.is(":visible") ? "Collapse Code" : "Expand Code";
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
"""
|
||||
|
||||
CSS_WRAPPER = """
|
||||
<style type="text/css">
|
||||
{0}
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Create a custom preprocessor
|
||||
class SliceIndex(Integer):
|
||||
"""An integer trait that accepts None"""
|
||||
default_value = None
|
||||
|
||||
def validate(self, obj, value):
|
||||
if value is None:
|
||||
return value
|
||||
else:
|
||||
return super(SliceIndex, self).validate(obj, value)
|
||||
|
||||
|
||||
class SubCell(Preprocessor):
|
||||
"""A transformer to select a slice of the cells of a notebook"""
|
||||
start = SliceIndex(0, config=True,
|
||||
help="first cell of notebook to be converted")
|
||||
end = SliceIndex(None, config=True,
|
||||
help="last cell of notebook to be converted")
|
||||
|
||||
def preprocess(self, nb, resources):
|
||||
nbc = deepcopy(nb)
|
||||
for worksheet in nbc.worksheets:
|
||||
cells = worksheet.cells[:]
|
||||
worksheet.cells = cells[self.start:self.end]
|
||||
return nbc, resources
|
||||
|
||||
call = preprocess # IPython < 2.0
|
||||
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Custom highlighter:
|
||||
# instead of using class='highlight', use class='highlight-ipynb'
|
||||
def custom_highlighter(source, language='ipython', metadata=None):
|
||||
formatter = HtmlFormatter(cssclass='highlight-ipynb')
|
||||
if not language:
|
||||
language = 'ipython'
|
||||
output = _pygments_highlight(source, formatter, language)
|
||||
return output.replace('<pre>', '<pre class="ipynb">')
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Below is the pelican plugin code.
|
||||
#
|
||||
SYNTAX = "{% notebook /path/to/notebook.ipynb [ cells[start:end] ] %}"
|
||||
FORMAT = re.compile(r"""^(\s+)?(?P<src>\S+)(\s+)?((cells\[)(?P<start>-?[0-9]*):(?P<end>-?[0-9]*)(\]))?(\s+)?$""")
|
||||
|
||||
|
||||
@LiquidTags.register('notebook')
|
||||
def notebook(preprocessor, tag, markup):
|
||||
match = FORMAT.search(markup)
|
||||
if match:
|
||||
argdict = match.groupdict()
|
||||
src = argdict['src']
|
||||
start = argdict['start']
|
||||
end = argdict['end']
|
||||
else:
|
||||
raise ValueError("Error processing input, "
|
||||
"expected syntax: {0}".format(SYNTAX))
|
||||
|
||||
if start:
|
||||
start = int(start)
|
||||
else:
|
||||
start = 0
|
||||
|
||||
if end:
|
||||
end = int(end)
|
||||
else:
|
||||
end = None
|
||||
|
||||
settings = preprocessor.configs.config['settings']
|
||||
nb_dir = settings.get('NOTEBOOK_DIR', 'notebooks')
|
||||
nb_path = os.path.join('content', nb_dir, src)
|
||||
|
||||
if not os.path.exists(nb_path):
|
||||
raise ValueError("File {0} could not be found".format(nb_path))
|
||||
|
||||
# Create the custom notebook converter
|
||||
c = Config({'CSSHTMLHeaderTransformer':
|
||||
{'enabled':True, 'highlight_class':'.highlight-ipynb'},
|
||||
'SubCell':
|
||||
{'enabled':True, 'start':start, 'end':end}})
|
||||
|
||||
template_file = 'basic'
|
||||
if LooseVersion(IPython.__version__) >= '2.0':
|
||||
if os.path.exists('pelicanhtml_2.tpl'):
|
||||
template_file = 'pelicanhtml_2'
|
||||
else:
|
||||
if os.path.exists('pelicanhtml_1.tpl'):
|
||||
template_file = 'pelicanhtml_1'
|
||||
|
||||
if LooseVersion(IPython.__version__) >= '2.0':
|
||||
subcell_kwarg = dict(preprocessors=[SubCell])
|
||||
else:
|
||||
subcell_kwarg = dict(transformers=[SubCell])
|
||||
|
||||
exporter = HTMLExporter(config=c,
|
||||
template_file=template_file,
|
||||
filters={'highlight2html': custom_highlighter},
|
||||
**subcell_kwarg)
|
||||
|
||||
# read and parse the notebook
|
||||
with open(nb_path) as f:
|
||||
nb_text = f.read()
|
||||
nb_json = nbformat.reads_json(nb_text)
|
||||
(body, resources) = exporter.from_notebook_node(nb_json)
|
||||
|
||||
# if we haven't already saved the header, save it here.
|
||||
if not notebook.header_saved:
|
||||
print ("\n ** Writing styles to _nb_header.html: "
|
||||
"this should be included in the theme. **\n")
|
||||
|
||||
header = '\n'.join(CSS_WRAPPER.format(css_line)
|
||||
for css_line in resources['inlining']['css'])
|
||||
header += JS_INCLUDE
|
||||
|
||||
with open('_nb_header.html', 'w') as f:
|
||||
f.write(header)
|
||||
notebook.header_saved = True
|
||||
|
||||
# this will stash special characters so that they won't be transformed
|
||||
# by subsequent processes.
|
||||
body = preprocessor.configs.htmlStash.store(body, safe=True)
|
||||
return body
|
||||
|
||||
notebook.header_saved = False
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows notebook to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
44
liquid_tags/pelicanhtml_1.tpl
Normal file
44
liquid_tags/pelicanhtml_1.tpl
Normal file
@@ -0,0 +1,44 @@
|
||||
{%- extends 'html_basic.tpl' -%}
|
||||
|
||||
{% block stream_stdout -%}
|
||||
<div class="box-flex1 output_subarea output_stream output_stdout">
|
||||
<pre class="ipynb">{{output.text |ansi2html}}</pre>
|
||||
</div>
|
||||
{%- endblock stream_stdout %}
|
||||
|
||||
{% block stream_stderr -%}
|
||||
<div class="box-flex1 output_subarea output_stream output_stderr">
|
||||
<pre class="ipynb">{{output.text |ansi2html}}</pre>
|
||||
</div>
|
||||
{%- endblock stream_stderr %}
|
||||
|
||||
{% block pyerr -%}
|
||||
<div class="box-flex1 output_subarea output_pyerr">
|
||||
<pre class="ipynb">{{super()}}</pre>
|
||||
</div>
|
||||
{%- endblock pyerr %}
|
||||
|
||||
{%- block data_text %}
|
||||
<pre class="ipynb">{{output.text | ansi2html}}</pre>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block input %}
|
||||
{% if "# <!-- collapse=True -->" in cell.input %}
|
||||
<div class="collapseheader box-flex1"><span style="font-weight: bold;">Expand Code</span>
|
||||
<div class="input_area box-flex1" style="display:none">
|
||||
{{ cell.input.replace("# <!-- collapse=True -->\n", "") | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
</div>
|
||||
{% elif "# <!-- collapse=False -->" in cell.input %}
|
||||
<div class="collapseheader box-flex1"><span style="font-weight: bold;">Collapse Code</span>
|
||||
<div class="input_area box-flex1">
|
||||
{{ cell.input.replace("# <!-- collapse=False -->\n", "") | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="input_area box-flex1">
|
||||
{{ cell.input | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endblock input %}
|
||||
|
||||
44
liquid_tags/pelicanhtml_2.tpl
Normal file
44
liquid_tags/pelicanhtml_2.tpl
Normal file
@@ -0,0 +1,44 @@
|
||||
{%- extends 'basic.tpl' -%}
|
||||
|
||||
{% block stream_stdout -%}
|
||||
<div class="box-flex1 output_subarea output_stream output_stdout">
|
||||
<pre class="ipynb">{{output.text |ansi2html}}</pre>
|
||||
</div>
|
||||
{%- endblock stream_stdout %}
|
||||
|
||||
{% block stream_stderr -%}
|
||||
<div class="box-flex1 output_subarea output_stream output_stderr">
|
||||
<pre class="ipynb">{{output.text |ansi2html}}</pre>
|
||||
</div>
|
||||
{%- endblock stream_stderr %}
|
||||
|
||||
{% block pyerr -%}
|
||||
<div class="box-flex1 output_subarea output_pyerr">
|
||||
<pre class="ipynb">{{super()}}</pre>
|
||||
</div>
|
||||
{%- endblock pyerr %}
|
||||
|
||||
{%- block data_text %}
|
||||
<pre class="ipynb">{{output.text | ansi2html}}</pre>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block input %}
|
||||
{% if "# <!-- collapse=True -->" in cell.input %}
|
||||
<div class="collapseheader box-flex1"><span style="font-weight: bold;">Expand Code</span>
|
||||
<div class="input_area box-flex1" style="display:none">
|
||||
{{ cell.input.replace("# <!-- collapse=True -->\n", "") | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
</div>
|
||||
{% elif "# <!-- collapse=False -->" in cell.input %}
|
||||
<div class="collapseheader box-flex1"><span style="font-weight: bold;">Collapse Code</span>
|
||||
<div class="input_area box-flex1">
|
||||
{{ cell.input.replace("# <!-- collapse=False -->\n", "") | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="input_area box-flex1">
|
||||
{{ cell.input | highlight2html(metadata=cell.metadata) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endblock input %}
|
||||
|
||||
70
liquid_tags/video.py
Normal file
70
liquid_tags/video.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Video Tag
|
||||
---------
|
||||
This implements a Liquid-style video tag for Pelican,
|
||||
based on the octopress video tag [1]_
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% video url/to/video [width height] [url/to/poster] %}
|
||||
|
||||
Example
|
||||
-------
|
||||
{% video http://site.com/video.mp4 720 480 http://site.com/poster-frame.jpg %}
|
||||
|
||||
Output
|
||||
------
|
||||
<video width='720' height='480' preload='none' controls poster='http://site.com/poster-frame.jpg'>
|
||||
<source src='http://site.com/video.mp4' type='video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"'/>
|
||||
</video>
|
||||
|
||||
[1] https://github.com/imathis/octopress/blob/master/plugins/video_tag.rb
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
SYNTAX = "{% video url/to/video [url/to/video] [url/to/video] [width height] [url/to/poster] %}"
|
||||
|
||||
VIDEO = re.compile(r'(/\S+|https?:\S+)(\s+(/\S+|https?:\S+))?(\s+(/\S+|https?:\S+))?(\s+(\d+)\s(\d+))?(\s+(/\S+|https?:\S+))?')
|
||||
|
||||
VID_TYPEDICT = {'.mp4':"type='video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"'",
|
||||
'.ogv':"type='video/ogg; codecs=theora, vorbis'",
|
||||
'.webm':"type='video/webm; codecs=vp8, vorbis'"}
|
||||
|
||||
|
||||
@LiquidTags.register('video')
|
||||
def video(preprocessor, tag, markup):
|
||||
videos = []
|
||||
width = None
|
||||
height = None
|
||||
poster = None
|
||||
|
||||
match = VIDEO.search(markup)
|
||||
if match:
|
||||
groups = match.groups()
|
||||
videos = [g for g in groups[0:6:2] if g]
|
||||
width = groups[6]
|
||||
height = groups[7]
|
||||
poster = groups[9]
|
||||
|
||||
if any(videos):
|
||||
video_out = "<video width='{width}' height='{height}' preload='none' controls poster='{poster}'>".format(width=width, height=height, poster=poster)
|
||||
for vid in videos:
|
||||
base, ext = os.path.splitext(vid)
|
||||
if ext not in VID_TYPEDICT:
|
||||
raise ValueError("Unrecognized video extension: "
|
||||
"{0}".format(ext))
|
||||
video_out += ("<source src='{0}' "
|
||||
"{1}>".format(vid, VID_TYPEDICT[ext]))
|
||||
video_out += "</video>"
|
||||
else:
|
||||
raise ValueError("Error processing input, "
|
||||
"expected syntax: {0}".format(SYNTAX))
|
||||
|
||||
return video_out
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows image tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
54
liquid_tags/vimeo.py
Normal file
54
liquid_tags/vimeo.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Vimeo Tag
|
||||
---------
|
||||
This implements a Liquid-style vimeo tag for Pelican,
|
||||
based on the youtube tag which is in turn based on
|
||||
the jekyll / octopress youtube tag [1]_
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% vimeo id [width height] %}
|
||||
|
||||
Example
|
||||
-------
|
||||
{% vimeo 10739054 640 480 %}
|
||||
|
||||
Output
|
||||
------
|
||||
<div style="width:640px; height:480px;"><iframe src="//player.vimeo.com/video/10739054?title=0&byline=0&portrait=0" width="640" height="480" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>
|
||||
|
||||
[1] https://gist.github.com/jamieowen/2063748
|
||||
"""
|
||||
import re
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
SYNTAX = "{% vimeo id [width height] %}"
|
||||
|
||||
VIMEO = re.compile(r'(\w+)(\s+(\d+)\s(\d+))?')
|
||||
|
||||
|
||||
@LiquidTags.register('vimeo')
|
||||
def vimeo(preprocessor, tag, markup):
|
||||
width = 640
|
||||
height = 390
|
||||
vimeo_id = None
|
||||
|
||||
match = VIMEO.search(markup)
|
||||
if match:
|
||||
groups = match.groups()
|
||||
vimeo_id = groups[0]
|
||||
width = groups[2] or width
|
||||
height = groups[3] or height
|
||||
|
||||
if vimeo_id:
|
||||
vimeo_out = '<div style="width:{width}px; height:{height}px;"><iframe src="//player.vimeo.com/video/{vimeo_id}?title=0&byline=0&portrait=0" width="{width}" height="{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe></div>'.format(width=width, height=height, vimeo_id=vimeo_id)
|
||||
else:
|
||||
raise ValueError("Error processing input, "
|
||||
"expected syntax: {0}".format(SYNTAX))
|
||||
|
||||
return vimeo_out
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows vimeo tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
53
liquid_tags/youtube.py
Normal file
53
liquid_tags/youtube.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Youtube Tag
|
||||
---------
|
||||
This implements a Liquid-style youtube tag for Pelican,
|
||||
based on the jekyll / octopress youtube tag [1]_
|
||||
|
||||
Syntax
|
||||
------
|
||||
{% youtube id [width height] %}
|
||||
|
||||
Example
|
||||
-------
|
||||
{% youtube dQw4w9WgXcQ 640 480 %}
|
||||
|
||||
Output
|
||||
------
|
||||
<iframe width="640" height="480" src="http://www.youtube.com/embed/dQw4w9WgXcQ" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>
|
||||
|
||||
[1] https://gist.github.com/jamieowen/2063748
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from .mdx_liquid_tags import LiquidTags
|
||||
|
||||
SYNTAX = "{% youtube id [width height] %}"
|
||||
|
||||
YOUTUBE = re.compile(r'(\w+)(\s+(\d+)\s(\d+))?')
|
||||
|
||||
@LiquidTags.register('youtube')
|
||||
def youtube(preprocessor, tag, markup):
|
||||
width = 640
|
||||
height = 390
|
||||
youtube_id = None
|
||||
|
||||
match = YOUTUBE.search(markup)
|
||||
if match:
|
||||
groups = match.groups()
|
||||
youtube_id = groups[0]
|
||||
width = groups[2] or width
|
||||
height = groups[3] or height
|
||||
|
||||
if youtube_id:
|
||||
youtube_out = "<iframe width='{width}' height='{height}' src='http://www.youtube.com/embed/{youtube_id}' frameborder='0' webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>".format(width=width, height=height, youtube_id=youtube_id)
|
||||
else:
|
||||
raise ValueError("Error processing input, "
|
||||
"expected syntax: {0}".format(SYNTAX))
|
||||
|
||||
return youtube_out
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# This import allows image tag to be a Pelican plugin
|
||||
from liquid_tags import register
|
||||
@@ -2,7 +2,10 @@ Neighbor Articles Plugin for Pelican
|
||||
====================================
|
||||
|
||||
This plugin adds ``next_article`` (newer) and ``prev_article`` (older)
|
||||
variables to the article's context
|
||||
variables to the article's context.
|
||||
|
||||
Also adds ``next_article_in_category`` and ``prev_article_in_category``.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
@@ -24,4 +27,71 @@ Usage
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</ul>
|
||||
<ul>
|
||||
{% if article.prev_article_in_category %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.prev_article_in_category.url}}">
|
||||
{{ article.prev_article_in_category.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if article.next_article %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.next_article_in_category.url}}">
|
||||
{{ article.next_article_in_category.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
Usage with the Subcategory plugin
|
||||
---------------------------------
|
||||
|
||||
If you want to get the neigbors within a subcategory it's a little different.
|
||||
Since an article can belong to more than one subcategory, subcategories are
|
||||
stored in a list. If you have an article with subcategories like
|
||||
|
||||
``Category/Foo/Bar``
|
||||
|
||||
it will belong to both subcategory Foo, and Foo/Bar. Subcategory neighbors are
|
||||
added to an article as ``next_article_in_subcategory#`` and
|
||||
``prev_article_in_subcategory#`` where ``#`` is the level of subcategory. So using
|
||||
the example from above, subcategory1 will be Foo, and subcategory2 Foo/Bar.
|
||||
Therefor the usage with subcategories is:
|
||||
|
||||
.. code-block:: html+jinja
|
||||
|
||||
<ul>
|
||||
{% if article.prev_article_subcategory1 %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.prev_article_in_subcategory1.url}}">
|
||||
{{ article.prev_article_in_subcategory1.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if article.next_article %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.next_article_subcategory1.url}}">
|
||||
{{ article.next_article_subcategory1.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul>
|
||||
{% if article.prev_article_in_subcategory2 %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.prev_article_in_subcategory2.url}}">
|
||||
{{ article.prev_article_in_subcategory2.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if article.next_article %}
|
||||
<li>
|
||||
<a href="{{ SITEURL }}/{{ article.next_article_in_subcategory2.url}}">
|
||||
{{ article.next_article_in_subcategory2.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ Neighbor Articles Plugin for Pelican
|
||||
This plugin adds ``next_article`` (newer) and ``prev_article`` (older)
|
||||
variables to the article's context
|
||||
"""
|
||||
|
||||
from pelican import signals
|
||||
|
||||
def iter3(seq):
|
||||
@@ -18,10 +17,42 @@ def iter3(seq):
|
||||
nxt, cur = cur, prv
|
||||
yield nxt, cur, None
|
||||
|
||||
def get_translation(article, prefered_language):
|
||||
if not article:
|
||||
return None
|
||||
for translation in article.translations:
|
||||
if translation.lang == prefered_language:
|
||||
return translation
|
||||
return article
|
||||
|
||||
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:
|
||||
exec(
|
||||
"translation.{} = get_translation(nxt, translation.lang)".format(
|
||||
next_name))
|
||||
exec(
|
||||
"translation.{} = get_translation(prv, translation.lang)".format(
|
||||
prev_name))
|
||||
|
||||
def neighbors(generator):
|
||||
for nxt, cur, prv in iter3(generator.articles):
|
||||
cur.next_article = nxt
|
||||
cur.prev_article = prv
|
||||
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')
|
||||
|
||||
if hasattr(generator, 'subcategories'):
|
||||
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)
|
||||
|
||||
31
pelican_comment_system/Readme.md
Normal file
31
pelican_comment_system/Readme.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Pelican comment system
|
||||
The pelican comment system allows you to add static comments to your articles.
|
||||
The comments are stored in Markdown files. Each comment in it own file.
|
||||
|
||||
#### Features
|
||||
- Static comments for each article
|
||||
- Replies to comments
|
||||
- Avatars and [Identicons](https://en.wikipedia.org/wiki/Identicon)
|
||||
- Comment Atom Feed for each article
|
||||
- Easy styleable via the themes
|
||||
|
||||
|
||||
See it in action here: [blog.scheirle.de](http://blog.scheirle.de/posts/2014/March/29/static-comments-via-email/)
|
||||
|
||||
Author | Website | Github
|
||||
-------------------|---------------------------|------------------------------
|
||||
Bernhard Scheirle | <http://blog.scheirle.de> | <https://github.com/Scheirle>
|
||||
|
||||
## Instructions
|
||||
- [Installation and basic usage](doc/installation.md)
|
||||
- [Avatars and Identicons](doc/avatars.md)
|
||||
- [Comment Atom Feed](doc/feed.md)
|
||||
- [Comment Form (aka: never gather Metadata)](doc/form.md)
|
||||
|
||||
## Requirements
|
||||
To create identicons the Python Image Library is needed. Therefore you either need PIL **or** Pillow (recommended).
|
||||
|
||||
##### Install Pillow
|
||||
easy_install Pillow
|
||||
|
||||
If you don't use avatars or identicons this plugin works fine without PIL/Pillow. You will however get a warning that identicons are deactivated (as expected).
|
||||
1
pelican_comment_system/__init__.py
Normal file
1
pelican_comment_system/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .pelican_comment_system import *
|
||||
94
pelican_comment_system/avatars.py
Normal file
94
pelican_comment_system/avatars.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import hashlib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_log = "pelican_comment_system: avatars: "
|
||||
try:
|
||||
from . identicon import identicon
|
||||
_identiconImported = True
|
||||
except ImportError as e:
|
||||
logger.warning(_log + "identicon deactivated: " + str(e))
|
||||
_identiconImported = False
|
||||
|
||||
# Global Variables
|
||||
_identicon_save_path = None
|
||||
_identicon_output_path = None
|
||||
_identicon_data = None
|
||||
_identicon_size = None
|
||||
_initialized = False
|
||||
_authors = None
|
||||
_missingAvatars = []
|
||||
|
||||
def _ready():
|
||||
if not _initialized:
|
||||
logger.warning(_log + "Module not initialized. use init")
|
||||
if not _identicon_data:
|
||||
logger.debug(_log + "No identicon data set")
|
||||
return _identiconImported and _initialized and _identicon_data
|
||||
|
||||
|
||||
def init(pelican_output_path, identicon_output_path, identicon_data, identicon_size, authors):
|
||||
global _identicon_save_path
|
||||
global _identicon_output_path
|
||||
global _identicon_data
|
||||
global _identicon_size
|
||||
global _initialized
|
||||
global _authors
|
||||
_identicon_save_path = os.path.join(pelican_output_path, identicon_output_path)
|
||||
_identicon_output_path = identicon_output_path
|
||||
_identicon_data = identicon_data
|
||||
_identicon_size = identicon_size
|
||||
_authors = authors
|
||||
_initialized = True
|
||||
|
||||
def _createIdenticonOutputFolder():
|
||||
if not _ready():
|
||||
return
|
||||
|
||||
if not os.path.exists(_identicon_save_path):
|
||||
os.makedirs(_identicon_save_path)
|
||||
|
||||
|
||||
def getAvatarPath(comment_id, metadata):
|
||||
if not _ready():
|
||||
return ''
|
||||
|
||||
md5 = hashlib.md5()
|
||||
author = tuple()
|
||||
for data in _identicon_data:
|
||||
if data in metadata:
|
||||
string = str(metadata[data])
|
||||
md5.update(string.encode('utf-8'))
|
||||
author += tuple([string])
|
||||
else:
|
||||
logger.warning(_log + data + " is missing in comment: " + comment_id)
|
||||
|
||||
if author in _authors:
|
||||
return _authors[author]
|
||||
|
||||
global _missingAvatars
|
||||
|
||||
code = md5.hexdigest()
|
||||
|
||||
if not code in _missingAvatars:
|
||||
_missingAvatars.append(code)
|
||||
|
||||
return os.path.join(_identicon_output_path, '%s.png' % code)
|
||||
|
||||
def generateAndSaveMissingAvatars():
|
||||
_createIdenticonOutputFolder()
|
||||
for code in _missingAvatars:
|
||||
avatar_path = '%s.png' % code
|
||||
avatar = identicon.render_identicon(int(code, 16), _identicon_size)
|
||||
avatar_save_path = os.path.join(_identicon_save_path, avatar_path)
|
||||
avatar.save(avatar_save_path, 'PNG')
|
||||
45
pelican_comment_system/comment.py
Normal file
45
pelican_comment_system/comment.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from pelican import contents
|
||||
from pelican.contents import Content
|
||||
|
||||
class Comment(Content):
|
||||
mandatory_properties = ('author', 'date')
|
||||
default_template = 'None'
|
||||
|
||||
def __init__(self, id, avatar, content, metadata, settings, source_path, context):
|
||||
super(Comment,self).__init__( content, metadata, settings, source_path, context )
|
||||
self.id = id
|
||||
self.replies = []
|
||||
self.avatar = avatar
|
||||
self.title = "Posted by: " + str(metadata['author'])
|
||||
|
||||
def addReply(self, comment):
|
||||
self.replies.append(comment)
|
||||
|
||||
def getReply(self, id):
|
||||
for reply in self.replies:
|
||||
if reply.id == id:
|
||||
return reply
|
||||
else:
|
||||
deepReply = reply.getReply(id)
|
||||
if deepReply != None:
|
||||
return deepReply
|
||||
return None
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.metadata['date'] < other.metadata['date']
|
||||
|
||||
def sortReplies(self):
|
||||
for r in self.replies:
|
||||
r.sortReplies()
|
||||
self.replies = sorted(self.replies)
|
||||
|
||||
def countReplies(self):
|
||||
amount = 0
|
||||
for r in self.replies:
|
||||
amount += r.countReplies()
|
||||
return amount + len(self.replies)
|
||||
35
pelican_comment_system/doc/avatars.md
Normal file
35
pelican_comment_system/doc/avatars.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Avatars and Identicons
|
||||
To activate the avatars and [identicons](https://en.wikipedia.org/wiki/Identicon) you have to set `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`.
|
||||
|
||||
##### Example
|
||||
```python
|
||||
PELICAN_COMMENT_SYSTEM_IDENTICON_DATA = ('author')
|
||||
```
|
||||
Now every comment with the same author tag will be treated as if written from the same person. And therefore have the same avatar/identicon. Of cause you can modify this tuple so other metadata are checked.
|
||||
|
||||
## Specific Avatars
|
||||
To set a specific avatar for a author you have to add them to the `PELICAN_COMMENT_SYSTEM_AUTHORS` dictionary.
|
||||
|
||||
The `key` of the dictionary has to be a tuple of the form of `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA`, so in our case only the author's name.
|
||||
|
||||
The `value` of the dictionary is the path to the specific avatar.
|
||||
|
||||
##### Example
|
||||
```python
|
||||
PELICAN_COMMENT_SYSTEM_AUTHORS = {
|
||||
('John'): "images/authors/john.png",
|
||||
('Tom'): "images/authors/tom.png",
|
||||
}
|
||||
```
|
||||
|
||||
## Theme
|
||||
To display the avatars and identicons simply add the following in the "comment for loop" in your theme:
|
||||
|
||||
```html
|
||||
<img src="{{ SITEURL }}/{{ comment.avatar }}"
|
||||
alt="Avatar"
|
||||
height="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}"
|
||||
width="{{ PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE }}">
|
||||
```
|
||||
|
||||
Of cause the `height` and `width` are optional, but they make sure that everything has the same size (in particular specific avatars).
|
||||
28
pelican_comment_system/doc/feed.md
Normal file
28
pelican_comment_system/doc/feed.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Comment Atom Feed
|
||||
## Custom comment url
|
||||
Be sure that the id of the html tag containing the comment matches `COMMENT_URL`.
|
||||
|
||||
##### pelicanconf.py
|
||||
```python
|
||||
COMMENT_URL = "#my_own_comment_id_{path}"
|
||||
```
|
||||
|
||||
##### Theme
|
||||
```html
|
||||
{% for comment in article.comments recursive %}
|
||||
...
|
||||
<article id="my_own_comment_id_{{comment.id}}">{{ comment.content }}</article>
|
||||
...
|
||||
{% endfor %}
|
||||
```
|
||||
## Theme
|
||||
#### Link
|
||||
To display a link to the article feed simply add the following to your theme:
|
||||
|
||||
```html
|
||||
{% if article %}
|
||||
<a href="{{ FEED_DOMAIN }}/{{ PELICAN_COMMENT_SYSTEM_FEED|format(article.slug) }}">Comment Atom Feed</a>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
|
||||
83
pelican_comment_system/doc/form.md
Normal file
83
pelican_comment_system/doc/form.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Comment Form (aka: never gather Metadata)
|
||||
Add a form, which allows your visitors to easily write comments.
|
||||
|
||||
But more importantly, on submit the form generates a mailto-link.
|
||||
The resulting email contains a valid markdown block. Now you only have to copy this block in a new file. And therefore there is no need to gather the metadata (like date, author, replyto) yourself.
|
||||
|
||||
#### Reply button
|
||||
Add this in the "comment for loop" in your article theme, so your visitors can reply to a comment.
|
||||
|
||||
```html
|
||||
<button onclick="reply('{{comment.id | urlencode}}');">Reply</button>
|
||||
```
|
||||
|
||||
#### Form
|
||||
A basic form so your visitors can write comments.
|
||||
|
||||
```html
|
||||
<form role="form" id="commentForm" action="#">
|
||||
<input name="Name" type="text" id="commentForm_inputName" placeholder="Enter your name or synonym">
|
||||
<textarea name="Text" id="commentForm_inputText" rows="10" style="resize:vertical;" placeholder="Your comment"></textarea>
|
||||
<button type="submit" id="commentForm_button">Post via email</button>
|
||||
<input name="replyto" type="hidden" id="commentForm_replyto">
|
||||
</form>
|
||||
```
|
||||
You may want to add a button to reset the `replyto` field.
|
||||
|
||||
#### Javascript
|
||||
To generate the mailto-Link and set the `replyto` field there is some javascript required.
|
||||
|
||||
```javascript
|
||||
<script type="text/javascript">
|
||||
function reply(id)
|
||||
{
|
||||
id = decodeURIComponent(id);
|
||||
$('#commentForm_replyto').val(id);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
function generateMailToLink()
|
||||
{
|
||||
var user = 'your_user_name'; //user@domain = your email address
|
||||
var domain = 'your_email_provider';
|
||||
var subject = 'Comment for \'{{ article.slug }}\'' ;
|
||||
|
||||
var d = new Date();
|
||||
var body = ''
|
||||
+ 'Hey,\nI posted a new comment on ' + document.URL + '\n\nGreetings ' + $("#commentForm_inputName").val() + '\n\n\n'
|
||||
+ 'Raw comment data:\n'
|
||||
+ '----------------------------------------\n'
|
||||
+ 'date: ' + d.getFullYear() + '-' + (d.getMonth()+1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + '\n'
|
||||
+ 'author: ' + $("#commentForm_inputName").val() + '\n';
|
||||
|
||||
var replyto = $('#commentForm_replyto').val();
|
||||
if (replyto.length != 0)
|
||||
{
|
||||
body += 'replyto: ' + replyto + '\n'
|
||||
}
|
||||
|
||||
body += '\n'
|
||||
+ $("#commentForm_inputText").val() + '\n'
|
||||
+ '----------------------------------------\n';
|
||||
|
||||
var link = 'mailto:' + user + '@' + domain + '?subject='
|
||||
+ encodeURIComponent(subject)
|
||||
+ "&body="
|
||||
+ encodeURIComponent(body);
|
||||
return link;
|
||||
}
|
||||
|
||||
|
||||
$('#commentForm').on("submit",
|
||||
function( event )
|
||||
{
|
||||
event.preventDefault();
|
||||
$(location).attr('href', generateMailToLink());
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
(jQuery is required for this script)
|
||||
|
||||
Don't forget to set the Variables `user` and `domain`.
|
||||
106
pelican_comment_system/doc/installation.md
Normal file
106
pelican_comment_system/doc/installation.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Installation
|
||||
Activate the plugin by adding it to your `pelicanconf.py`
|
||||
|
||||
PLUGIN_PATH = '/path/to/pelican-plugins'
|
||||
PLUGINS = ['pelican_comment_system']
|
||||
PELICAN_COMMENT_SYSTEM = True
|
||||
|
||||
And modify your `article.html` theme (see below).
|
||||
|
||||
## Settings
|
||||
Name | Type | Default | Description
|
||||
-----------------------------------------------|-----------|----------------------------|-------
|
||||
`PELICAN_COMMENT_SYSTEM` | `boolean` | `False` | Activates or deactivates the comment system
|
||||
`PELICAN_COMMENT_SYSTEM_DIR` | `string` | `comments` | Folder where the comments are stored
|
||||
`PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH` | `string` | `images/identicon` | Relative URL to the output folder where the identicons are stored
|
||||
`PELICAN_COMMENT_SYSTEM_IDENTICON_DATA` | `tuple` | `()` | Contains all Metadata tags, which in combination identifies a comment author (like `('author', 'email')`)
|
||||
`PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE` | `int` | `72` | Width and height of the identicons. Has to be a multiple of 3.
|
||||
`PELICAN_COMMENT_SYSTEM_AUTHORS` | `dict` | `{}` | Comment authors, which should have a specific avatar. More info [here](avatars.md)
|
||||
`PELICAN_COMMENT_SYSTEM_FEED` | `string` |`feeds/comment.%s.atom.xml` | Relative URL to output the Atom feed for each article.`%s` gets replaced with the slug of the article. More info [here](http://docs.getpelican.com/en/latest/settings.html#feed-settings)
|
||||
`COMMENT_URL` | `string` | `#comment-{path}` | `{path}` gets replaced with the id of the comment. More info [here](feed.md)
|
||||
|
||||
## Folder structure
|
||||
Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`.
|
||||
Sub folders are named after the `slug` of the articles.
|
||||
|
||||
So the comments to your `foo-bar` article are stored in `comments/foo-bar/`
|
||||
|
||||
The filenames of the comment files are up to you. But the filename is the Identifier of the comment (**with** extension).
|
||||
|
||||
##### Example folder structure
|
||||
|
||||
.
|
||||
└── comments
|
||||
└── foo-bar
|
||||
│ ├── 1.md
|
||||
│ └── 0.md
|
||||
└── some-other-slug
|
||||
├── random-Name.md
|
||||
├── 1.md
|
||||
└── 0.md
|
||||
|
||||
|
||||
## Comment file
|
||||
### Meta information
|
||||
Tag | Required | Description
|
||||
--------------|-----------|----------------
|
||||
`date` | yes | Date when the comment was posted
|
||||
`author` | yes | Name of the comment author
|
||||
`replyto` | no | Identifier of the parent comment. Identifier = Filename (**with** extension)
|
||||
|
||||
Every other (custom) tag gets parsed as well and will be available through the theme.
|
||||
|
||||
##### Example of a comment file
|
||||
|
||||
date: 2014-3-21 15:02
|
||||
author: Author of the comment
|
||||
website: http://authors.website.com
|
||||
replyto: 7
|
||||
anothermetatag: some random tag
|
||||
|
||||
Content of the comment.
|
||||
|
||||
## Theme
|
||||
In the `article.html` theme file are now two more variables available.
|
||||
|
||||
Variables | Description
|
||||
-------------------------|--------------------------
|
||||
`article.comments_count` | Amount of total comments for this article (including replies to comments)
|
||||
`article.comments` | Array containing the top level comments for this article (no replies to comments)
|
||||
|
||||
### Comment object
|
||||
The comment object is a [content](https://github.com/getpelican/pelican/blob/master/pelican/contents.py#L34) object, so all common attributes are available (like author, content, date, local_date, metadata, ...).
|
||||
|
||||
Additional following attributes are added:
|
||||
|
||||
Attribute | Description
|
||||
-----------|--------------------------
|
||||
`id` | Identifier of this comment
|
||||
`replies` | Array containing the top level replies for this comment
|
||||
`avatar` | Path to the avatar or identicon of the comment author
|
||||
|
||||
##### Example article.html theme
|
||||
(only the comment section)
|
||||
```html
|
||||
{% if article.comments %}
|
||||
{% for comment in article.comments recursive %}
|
||||
{% if loop.depth0 == 0 %}
|
||||
{% set marginLeft = 0 %}
|
||||
{% else %}
|
||||
{% set marginLeft = 50 %}
|
||||
{% endif %}
|
||||
<article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;">
|
||||
<a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a>
|
||||
<h4>{{ comment.author }}</h4>
|
||||
<p>Posted on <abbr class="published" title="{{ comment.date.isoformat() }}">{{ comment.locale_date }}</abbr></p>
|
||||
{{ comment.metadata['my_custom_metadata'] }}
|
||||
{{ comment.content }}
|
||||
{% if comment.replies %}
|
||||
{{ loop(comment.replies) }}
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>There are no comments yet.<p>
|
||||
{% endif %}
|
||||
```
|
||||
11
pelican_comment_system/identicon/LICENSE
Executable file
11
pelican_comment_system/identicon/LICENSE
Executable file
@@ -0,0 +1,11 @@
|
||||
identicon.py is Licesensed under FreeBSD License.
|
||||
(http://www.freebsd.org/copyright/freebsd-license.html)
|
||||
|
||||
Copyright 1994-2009 Shin Adachi. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
17
pelican_comment_system/identicon/README.md
Executable file
17
pelican_comment_system/identicon/README.md
Executable file
@@ -0,0 +1,17 @@
|
||||
identicon.py: identicon python implementation.
|
||||
==============================================
|
||||
:Author:Shin Adachi <shn@glucose.jp>
|
||||
|
||||
## usage
|
||||
|
||||
### commandline
|
||||
|
||||
python identicon.py [code]
|
||||
|
||||
### python
|
||||
|
||||
import identicon
|
||||
identicon.render_identicon(code, size)
|
||||
|
||||
Return a PIL Image class instance which have generated identicon image.
|
||||
`size` specifies patch size. Generated image size is 3 * `size`.
|
||||
0
pelican_comment_system/identicon/__init__.py
Normal file
0
pelican_comment_system/identicon/__init__.py
Normal file
256
pelican_comment_system/identicon/identicon.py
Executable file
256
pelican_comment_system/identicon/identicon.py
Executable file
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
identicon.py
|
||||
identicon python implementation.
|
||||
by Shin Adachi <shn@glucose.jp>
|
||||
|
||||
= usage =
|
||||
|
||||
== commandline ==
|
||||
>>> python identicon.py [code]
|
||||
|
||||
== python ==
|
||||
>>> import identicon
|
||||
>>> identicon.render_identicon(code, size)
|
||||
|
||||
Return a PIL Image class instance which have generated identicon image.
|
||||
```size``` specifies `patch size`. Generated image size is 3 * ```size```.
|
||||
"""
|
||||
# g
|
||||
# PIL Modules
|
||||
from PIL import Image, ImageDraw, ImagePath, ImageColor
|
||||
|
||||
|
||||
__all__ = ['render_identicon', 'IdenticonRendererBase']
|
||||
|
||||
|
||||
class Matrix2D(list):
|
||||
"""Matrix for Patch rotation"""
|
||||
def __init__(self, initial=[0.] * 9):
|
||||
assert isinstance(initial, list) and len(initial) == 9
|
||||
list.__init__(self, initial)
|
||||
|
||||
def clear(self):
|
||||
for i in xrange(9):
|
||||
self[i] = 0.
|
||||
|
||||
def set_identity(self):
|
||||
self.clear()
|
||||
for i in xrange(3):
|
||||
self[i] = 1.
|
||||
|
||||
def __str__(self):
|
||||
return '[%s]' % ', '.join('%3.2f' % v for v in self)
|
||||
|
||||
def __mul__(self, other):
|
||||
r = []
|
||||
if isinstance(other, Matrix2D):
|
||||
for y in range(3):
|
||||
for x in range(3):
|
||||
v = 0.0
|
||||
for i in range(3):
|
||||
v += (self[i * 3 + x] * other[y * 3 + i])
|
||||
r.append(v)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return Matrix2D(r)
|
||||
|
||||
def for_PIL(self):
|
||||
return self[0:6]
|
||||
|
||||
@classmethod
|
||||
def translate(kls, x, y):
|
||||
return kls([1.0, 0.0, float(x),
|
||||
0.0, 1.0, float(y),
|
||||
0.0, 0.0, 1.0])
|
||||
|
||||
@classmethod
|
||||
def scale(kls, x, y):
|
||||
return kls([float(x), 0.0, 0.0,
|
||||
0.0, float(y), 0.0,
|
||||
0.0, 0.0, 1.0])
|
||||
|
||||
"""
|
||||
# need `import math`
|
||||
@classmethod
|
||||
def rotate(kls, theta, pivot=None):
|
||||
c = math.cos(theta)
|
||||
s = math.sin(theta)
|
||||
|
||||
matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
|
||||
if not pivot:
|
||||
return matR
|
||||
return kls.translate(-pivot[0], -pivot[1]) * matR *
|
||||
kls.translate(*pivot)
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def rotateSquare(kls, theta, pivot=None):
|
||||
theta = theta % 4
|
||||
c = [1., 0., -1., 0.][theta]
|
||||
s = [0., 1., 0., -1.][theta]
|
||||
|
||||
matR = kls([c, -s, 0., s, c, 0., 0., 0., 1.])
|
||||
if not pivot:
|
||||
return matR
|
||||
return kls.translate(-pivot[0], -pivot[1]) * matR * \
|
||||
kls.translate(*pivot)
|
||||
|
||||
|
||||
class IdenticonRendererBase(object):
|
||||
PATH_SET = []
|
||||
|
||||
def __init__(self, code):
|
||||
"""
|
||||
@param code code for icon
|
||||
"""
|
||||
if not isinstance(code, int):
|
||||
code = int(code)
|
||||
self.code = code
|
||||
|
||||
def render(self, size):
|
||||
"""
|
||||
render identicon to PIL.Image
|
||||
|
||||
@param size identicon patchsize. (image size is 3 * [size])
|
||||
@return PIL.Image
|
||||
"""
|
||||
|
||||
# decode the code
|
||||
middle, corner, side, foreColor, backColor = self.decode(self.code)
|
||||
size = int(size)
|
||||
# make image
|
||||
image = Image.new("RGB", (size * 3, size * 3))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# fill background
|
||||
draw.rectangle((0, 0, image.size[0], image.size[1]), fill=0)
|
||||
|
||||
kwds = {
|
||||
'draw': draw,
|
||||
'size': size,
|
||||
'foreColor': foreColor,
|
||||
'backColor': backColor}
|
||||
# middle patch
|
||||
self.drawPatch((1, 1), middle[2], middle[1], middle[0], **kwds)
|
||||
|
||||
# side patch
|
||||
kwds['type'] = side[0]
|
||||
for i in range(4):
|
||||
pos = [(1, 0), (2, 1), (1, 2), (0, 1)][i]
|
||||
self.drawPatch(pos, side[2] + 1 + i, side[1], **kwds)
|
||||
|
||||
# corner patch
|
||||
kwds['type'] = corner[0]
|
||||
for i in range(4):
|
||||
pos = [(0, 0), (2, 0), (2, 2), (0, 2)][i]
|
||||
self.drawPatch(pos, corner[2] + 1 + i, corner[1], **kwds)
|
||||
|
||||
return image
|
||||
|
||||
def drawPatch(self, pos, turn, invert, type, draw, size, foreColor,
|
||||
backColor):
|
||||
"""
|
||||
@param size patch size
|
||||
"""
|
||||
path = self.PATH_SET[type]
|
||||
if not path:
|
||||
# blank patch
|
||||
invert = not invert
|
||||
path = [(0., 0.), (1., 0.), (1., 1.), (0., 1.), (0., 0.)]
|
||||
patch = ImagePath.Path(path)
|
||||
if invert:
|
||||
foreColor, backColor = backColor, foreColor
|
||||
|
||||
mat = Matrix2D.rotateSquare(turn, pivot=(0.5, 0.5)) *\
|
||||
Matrix2D.translate(*pos) *\
|
||||
Matrix2D.scale(size, size)
|
||||
|
||||
patch.transform(mat.for_PIL())
|
||||
draw.rectangle((pos[0] * size, pos[1] * size, (pos[0] + 1) * size,
|
||||
(pos[1] + 1) * size), fill=backColor)
|
||||
draw.polygon(patch, fill=foreColor, outline=foreColor)
|
||||
|
||||
### virtual functions
|
||||
def decode(self, code):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DonRenderer(IdenticonRendererBase):
|
||||
"""
|
||||
Don Park's implementation of identicon
|
||||
see : http://www.docuverse.com/blog/donpark/2007/01/19/identicon-updated-and-source-released
|
||||
"""
|
||||
|
||||
PATH_SET = [
|
||||
[(0, 0), (4, 0), (4, 4), (0, 4)], # 0
|
||||
[(0, 0), (4, 0), (0, 4)],
|
||||
[(2, 0), (4, 4), (0, 4)],
|
||||
[(0, 0), (2, 0), (2, 4), (0, 4)],
|
||||
[(2, 0), (4, 2), (2, 4), (0, 2)], # 4
|
||||
[(0, 0), (4, 2), (4, 4), (2, 4)],
|
||||
[(2, 0), (4, 4), (2, 4), (3, 2), (1, 2), (2, 4), (0, 4)],
|
||||
[(0, 0), (4, 2), (2, 4)],
|
||||
[(1, 1), (3, 1), (3, 3), (1, 3)], # 8
|
||||
[(2, 0), (4, 0), (0, 4), (0, 2), (2, 2)],
|
||||
[(0, 0), (2, 0), (2, 2), (0, 2)],
|
||||
[(0, 2), (4, 2), (2, 4)],
|
||||
[(2, 2), (4, 4), (0, 4)],
|
||||
[(2, 0), (2, 2), (0, 2)],
|
||||
[(0, 0), (2, 0), (0, 2)],
|
||||
[]] # 15
|
||||
MIDDLE_PATCH_SET = [0, 4, 8, 15]
|
||||
|
||||
# modify path set
|
||||
for idx in range(len(PATH_SET)):
|
||||
if PATH_SET[idx]:
|
||||
p = map(lambda vec: (vec[0] / 4.0, vec[1] / 4.0), PATH_SET[idx])
|
||||
p = list(p)
|
||||
PATH_SET[idx] = p + p[:1]
|
||||
|
||||
def decode(self, code):
|
||||
# decode the code
|
||||
middleType = self.MIDDLE_PATCH_SET[code & 0x03]
|
||||
middleInvert= (code >> 2) & 0x01
|
||||
cornerType = (code >> 3) & 0x0F
|
||||
cornerInvert= (code >> 7) & 0x01
|
||||
cornerTurn = (code >> 8) & 0x03
|
||||
sideType = (code >> 10) & 0x0F
|
||||
sideInvert = (code >> 14) & 0x01
|
||||
sideTurn = (code >> 15) & 0x03
|
||||
blue = (code >> 16) & 0x1F
|
||||
green = (code >> 21) & 0x1F
|
||||
red = (code >> 27) & 0x1F
|
||||
|
||||
foreColor = (red << 3, green << 3, blue << 3)
|
||||
|
||||
return (middleType, middleInvert, 0),\
|
||||
(cornerType, cornerInvert, cornerTurn),\
|
||||
(sideType, sideInvert, sideTurn),\
|
||||
foreColor, ImageColor.getrgb('white')
|
||||
|
||||
|
||||
def render_identicon(code, size, renderer=None):
|
||||
if not renderer:
|
||||
renderer = DonRenderer
|
||||
return renderer(code).render(size)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print('usage: python identicon.py [CODE]....')
|
||||
raise SystemExit
|
||||
|
||||
for code in sys.argv[1:]:
|
||||
if code.startswith('0x') or code.startswith('0X'):
|
||||
code = int(code[2:], 16)
|
||||
elif code.startswith('0'):
|
||||
code = int(code[1:], 8)
|
||||
else:
|
||||
code = int(code)
|
||||
|
||||
icon = render_identicon(code, 24)
|
||||
icon.save('%08x.png' % code, 'PNG')
|
||||
121
pelican_comment_system/pelican_comment_system.py
Normal file
121
pelican_comment_system/pelican_comment_system.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Pelican Comment System
|
||||
======================
|
||||
|
||||
A Pelican plugin, which allows you to add comments to your articles.
|
||||
|
||||
Author: Bernhard Scheirle
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
import os
|
||||
import copy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from itertools import chain
|
||||
from pelican import signals
|
||||
from pelican.readers import MarkdownReader
|
||||
from pelican.writers import Writer
|
||||
|
||||
from . comment import Comment
|
||||
from . import avatars
|
||||
|
||||
|
||||
def pelican_initialized(pelican):
|
||||
from pelican.settings import DEFAULT_CONFIG
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM', False)
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_DIR' 'comments')
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH' 'images/identicon')
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
|
||||
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
|
||||
DEFAULT_CONFIG.setdefault('COMMENT_URL', '#comment-{path}')
|
||||
if pelican:
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments')
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH', 'images/identicon')
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
|
||||
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
|
||||
pelican.settings.setdefault('COMMENT_URL', '#comment-{path}')
|
||||
|
||||
|
||||
def initialize(article_generator):
|
||||
avatars.init(
|
||||
article_generator.settings['OUTPUT_PATH'],
|
||||
article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH'],
|
||||
article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_DATA'],
|
||||
article_generator.settings['PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE']/3,
|
||||
article_generator.settings['PELICAN_COMMENT_SYSTEM_AUTHORS'],
|
||||
)
|
||||
|
||||
def add_static_comments(gen, content):
|
||||
if gen.settings['PELICAN_COMMENT_SYSTEM'] != True:
|
||||
return
|
||||
|
||||
content.comments_count = 0
|
||||
content.comments = []
|
||||
|
||||
#Modify the local context, so we get proper values for the feed
|
||||
context = copy.copy(gen.context)
|
||||
context['SITEURL'] += "/" + content.url
|
||||
context['SITENAME'] = "Comments for: " + content.title
|
||||
context['SITESUBTITLE'] = ""
|
||||
path = gen.settings['PELICAN_COMMENT_SYSTEM_FEED'] % content.slug
|
||||
writer = Writer(gen.output_path, settings=gen.settings)
|
||||
|
||||
folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], content.slug)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
logger.debug("No comments found for: " + content.slug)
|
||||
writer.write_feed( [], context, path)
|
||||
return
|
||||
|
||||
reader = MarkdownReader(gen.settings)
|
||||
comments = []
|
||||
replies = []
|
||||
|
||||
for file in os.listdir(folder):
|
||||
name, extension = os.path.splitext(file)
|
||||
if extension[1:].lower() in reader.file_extensions:
|
||||
com_content, meta = reader.read(os.path.join(folder, file))
|
||||
|
||||
avatar_path = avatars.getAvatarPath(name, meta)
|
||||
|
||||
com = Comment(file, avatar_path, com_content, meta, gen.settings, file, context)
|
||||
|
||||
if 'replyto' in meta:
|
||||
replies.append( com )
|
||||
else:
|
||||
comments.append( com )
|
||||
|
||||
writer.write_feed( comments + replies, context, path)
|
||||
|
||||
#TODO: Fix this O(n²) loop
|
||||
for reply in replies:
|
||||
for comment in chain(comments, replies):
|
||||
if comment.id == reply.metadata['replyto']:
|
||||
comment.addReply(reply)
|
||||
|
||||
count = 0
|
||||
for comment in comments:
|
||||
comment.sortReplies()
|
||||
count += comment.countReplies()
|
||||
|
||||
comments = sorted(comments)
|
||||
|
||||
content.comments_count = len(comments) + count
|
||||
content.comments = comments
|
||||
|
||||
def writeIdenticonsToDisk(gen, writer):
|
||||
avatars.generateAndSaveMissingAvatars()
|
||||
|
||||
def register():
|
||||
signals.initialized.connect(pelican_initialized)
|
||||
signals.article_generator_init.connect(initialize)
|
||||
signals.article_generator_write_article.connect(add_static_comments)
|
||||
signals.article_writer_finalized.connect(writeIdenticonsToDisk)
|
||||
1
pelican_vimeo
Submodule
1
pelican_vimeo
Submodule
Submodule pelican_vimeo added at d18f1ddfe9
1
pelican_youtube
Submodule
1
pelican_youtube
Submodule
Submodule pelican_youtube added at 045c43dd4d
26
read_more_link/Readme.md
Normal file
26
read_more_link/Readme.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Read More Link
|
||||
===
|
||||
|
||||
**Author**: Vuong Nguyen (http://vuongnguyen.com)
|
||||
|
||||
This plugin inserts an inline "read more" or "continue" link into the last html element of the object summary.
|
||||
|
||||
For more information, please visit: http://vuongnguyen.com/creating-inline-read-more-link-python-pelican-lxml.html
|
||||
|
||||
Requirements
|
||||
---
|
||||
|
||||
lxml - for parsing html elements
|
||||
|
||||
Settings
|
||||
---
|
||||
# This settings indicates that you want to create summary at a certain length
|
||||
SUMMARY_MAX_LENGTH = 50
|
||||
|
||||
# This indicates what goes inside the read more link
|
||||
READ_MORE_LINK = None (ex: '<span>continue</span>')
|
||||
|
||||
# This is the format of the read more link
|
||||
READ_MORE_LINK_FORMAT = '<a class="read-more" href="/{url}">{text}</a>'
|
||||
|
||||
|
||||
1
read_more_link/__init__.py
Normal file
1
read_more_link/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .read_more_link import *
|
||||
71
read_more_link/read_more_link.py
Normal file
71
read_more_link/read_more_link.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Read More Link
|
||||
===========================
|
||||
|
||||
This plugin inserts an inline "read more" or "continue" link into the last html element of the object summary.
|
||||
|
||||
For more information, please visit: http://vuongnguyen.com/creating-inline-read-more-link-python-pelican-lxml.html
|
||||
|
||||
"""
|
||||
|
||||
from pelican import signals, contents
|
||||
from pelican.utils import truncate_html_words
|
||||
|
||||
try:
|
||||
from lxml.html import fragment_fromstring, fragments_fromstring, tostring
|
||||
from lxml.etree import ParserError
|
||||
except ImportError:
|
||||
raise Exception("Unable to find lxml. To use READ_MORE_LINK, you need lxml")
|
||||
|
||||
|
||||
def insert_into_last_element(html, element):
|
||||
"""
|
||||
function to insert an html element into another html fragment
|
||||
example:
|
||||
html = '<p>paragraph1</p><p>paragraph2...</p>'
|
||||
element = '<a href="/read-more/">read more</a>'
|
||||
---> '<p>paragraph1</p><p>paragraph2...<a href="/read-more/">read more</a></p>'
|
||||
"""
|
||||
try:
|
||||
item = fragment_fromstring(element)
|
||||
except ParserError, TypeError:
|
||||
item = fragment_fromstring('<span></span>')
|
||||
|
||||
try:
|
||||
doc = fragments_fromstring(html)
|
||||
doc[-1].append(item)
|
||||
|
||||
return ''.join(tostring(e) for e in doc)
|
||||
except ParserError, TypeError:
|
||||
return ''
|
||||
|
||||
def insert_read_more_link(instance):
|
||||
"""
|
||||
Insert an inline "read more" link into the last element of the summary
|
||||
:param instance:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# only deals with Article type
|
||||
if type(instance) != contents.Article: return
|
||||
|
||||
|
||||
SUMMARY_MAX_LENGTH = instance.settings.get('SUMMARY_MAX_LENGTH')
|
||||
READ_MORE_LINK = instance.settings.get('READ_MORE_LINK', None)
|
||||
READ_MORE_LINK_FORMAT = instance.settings.get('READ_MORE_LINK_FORMAT',
|
||||
'<a class="read-more" href="/{url}">{text}</a>')
|
||||
|
||||
if not (SUMMARY_MAX_LENGTH and READ_MORE_LINK and READ_MORE_LINK_FORMAT): return
|
||||
|
||||
if hasattr(instance, '_summary') and instance._summary:
|
||||
summary = instance._summary
|
||||
else:
|
||||
summary = truncate_html_words(instance.content, SUMMARY_MAX_LENGTH)
|
||||
|
||||
if summary<instance.content:
|
||||
read_more_link = READ_MORE_LINK_FORMAT.format(url=instance.url, text=READ_MORE_LINK)
|
||||
instance._summary = insert_into_last_element(summary, read_more_link)
|
||||
|
||||
def register():
|
||||
signals.content_object_init.connect(insert_read_more_link)
|
||||
1
read_more_link/requirements.txt
Normal file
1
read_more_link/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
lxml>=3.2.1
|
||||
@@ -17,3 +17,11 @@ For example::
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Your related posts should share a common tag. You can also use ``related_posts:`` in your post's meta data.
|
||||
The 'related_posts:' meta data works together with your existing slugs:
|
||||
|
||||
related_posts: slug1,slug2,slug3...slugN
|
||||
|
||||
N represents the RELATED_POSTS_MAX
|
||||
|
||||
@@ -13,23 +13,38 @@ def add_related_posts(generator):
|
||||
# get the max number of entries from settings
|
||||
# or fall back to default (5)
|
||||
numentries = generator.settings.get('RELATED_POSTS_MAX', 5)
|
||||
|
||||
for article in generator.articles:
|
||||
# no tag, no relation
|
||||
if not hasattr(article, 'tags'):
|
||||
continue
|
||||
# set priority in case of forced related posts
|
||||
if hasattr(article,'related_posts'):
|
||||
# split slugs
|
||||
related_posts = article.related_posts.split(',')
|
||||
posts = []
|
||||
# get related articles
|
||||
for slug in related_posts:
|
||||
i = 0
|
||||
for a in generator.articles:
|
||||
if i >= numentries: # break in case there are max related psots
|
||||
break
|
||||
if a.slug == slug:
|
||||
posts.append(a)
|
||||
i += 1
|
||||
|
||||
# score = number of common tags
|
||||
scores = Counter()
|
||||
for tag in article.tags:
|
||||
scores += Counter(generator.tags[tag])
|
||||
article.related_posts = posts
|
||||
else:
|
||||
# no tag, no relation
|
||||
if not hasattr(article, 'tags'):
|
||||
continue
|
||||
|
||||
# remove itself
|
||||
scores.pop(article)
|
||||
# score = number of common tags
|
||||
scores = Counter()
|
||||
for tag in article.tags:
|
||||
scores += Counter(generator.tags[tag])
|
||||
|
||||
article.related_posts = [other for other, count
|
||||
in scores.most_common(numentries)]
|
||||
# remove itself
|
||||
scores.pop(article)
|
||||
|
||||
article.related_posts = [other for other, count
|
||||
in scores.most_common(numentries)]
|
||||
|
||||
def register():
|
||||
signals.article_generator_finalized.connect(add_related_posts)
|
||||
173
render_math/Readme.md
Normal file
173
render_math/Readme.md
Normal file
@@ -0,0 +1,173 @@
|
||||
Math Render Plugin For Pelican
|
||||
==============================
|
||||
This plugin gives pelican the ability to render mathematics. It accomplishes
|
||||
this by using the [MathJax](http://www.mathjax.org/) javascript engine. Both
|
||||
[LaTex](http://en.wikipedia.org/wiki/LaTeX) and [MathML](http://en.wikipedia.org/wiki/MathML)
|
||||
can be rendered within the content.
|
||||
|
||||
The plugin also ensures that pelican and recognized math "play" nicely together, by
|
||||
ensuring [Typogrify](https://github.com/mintchaos/typogrify) does not alter math content
|
||||
and summaries that get cut off are repaired.
|
||||
|
||||
Recognized math in the context of this plugin is either LaTex or MathML as described below.
|
||||
|
||||
### LaTex
|
||||
Anything between `$`...`$` (inline math) and `$$`..`$$` (displayed math) will be recognized as
|
||||
LaTex. In addition, anything the `\begin` and `\end` LaTex macros will also be
|
||||
recognized as LaTex. For example, `\begin{equation}`...`\end{equation}` would be used to
|
||||
render math equations with numbering.
|
||||
|
||||
Within recognized LaTex as described above, any supported LaTex macro can be used.
|
||||
|
||||
### MathML
|
||||
Anything between `<math>` and `</math>` tags will be recognized as MathML
|
||||
|
||||
Installation
|
||||
------------
|
||||
To enable, ensure that `render_math` plugin is accessible.
|
||||
Then add the following to settings.py:
|
||||
|
||||
PLUGINS = ["render_math"]
|
||||
|
||||
Your site is now capable of rendering math math using the mathjax JavaScript
|
||||
engine. No alterations to the template is needed, just use and enjoy!
|
||||
|
||||
### Typogrify
|
||||
In the past, using [Typgogrify](https://github.com/mintchaos/typogrify) would alter the math contents resulting
|
||||
in math that could not be rendered by MathJax. The only option was to ensure
|
||||
that Typogrify was disabled in the settings.
|
||||
|
||||
The problem has been recitified in this plugin, but it requires [Typogrify version 2.04](https://pypi.python.org/pypi/typogrify)
|
||||
(or higher). If this version of Typogrify is not present, the plugin will inform that an incorrect
|
||||
version of Typogrify is not present and disable Typogrify for the entire site
|
||||
|
||||
Usage
|
||||
-----
|
||||
### Backward Compatibility
|
||||
This plugin is backward compatible in the sense that it
|
||||
will render previous setups correctly. This is because those
|
||||
settings and metadata information is ignored by this version. Therefore
|
||||
you can remove them to neaten up your site
|
||||
|
||||
### Templates
|
||||
No alteration is needed to a template for this plugin to work. Just install
|
||||
the plugin and start writing your Math.
|
||||
|
||||
If on the other hand, you are template designer and want total control
|
||||
over the MathJax JavaScript, you can set the `auto_insert` setting to
|
||||
`False` which will cause no MathJax JavaScript to be added to the content.
|
||||
|
||||
If you choose this option, you should really know what you are doing. Therefore
|
||||
only do this if you are designing your template. There is no real advantage to
|
||||
to letting template logic handle the insertion of the MathJax JavaScript other
|
||||
than it being slightly neater.
|
||||
|
||||
By setting `auto_insert` to `False`, metadata with `key` value of `mathjax`
|
||||
will be present in all pages and articles where MathJax should be present.
|
||||
The template designer can detect this and then use the `MATHJAXSCRIPT` setting
|
||||
which will contain the user specified MathJax script to insert into the content.
|
||||
|
||||
For example, this code could be used:
|
||||
```
|
||||
{% if not MATH['auto_insert'] %}
|
||||
{% if page and page.mathjax or article and article.mathjax %}
|
||||
{{ MATHJAXSCRIPT }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Settings
|
||||
Certain MathJax rendering options can be set. These options
|
||||
are in a dictionary variable called `MATH` in the pelican
|
||||
settings file.
|
||||
|
||||
The dictionary can be set with the following keys:
|
||||
|
||||
* `auto_insert`: controls whether plugin should automatically insert
|
||||
MathJax JavaScript in content that has Math. It is only recommended
|
||||
to set this to False if you are a template designer and you want
|
||||
extra control over where the MathJax JavaScript is renderd. **Default Value**:
|
||||
True
|
||||
* `wrap_latex`: controls the tags that LaTex math is wrapped with inside the resulting
|
||||
html. For example, setting `wrap_latex` to `mathjax` would wrap all LaTex math inside
|
||||
`<mathjax>...</mathjax>` tags. If typogrify is set to True, then math needs
|
||||
to be wrapped in tags and `wrap_latex` will therefore default to `mathjax` if not
|
||||
set. `wrap_latex` cannot be set to `'math'` because this tag is reserved for
|
||||
mathml notation. **Default Value**: None unless Typogrify is enabled in which case,
|
||||
it defaults to `mathjax`
|
||||
* `align`: controls how displayed math will be aligned. Can be set to either
|
||||
`left`, `right` or `center`. **Default Value**: `center`.
|
||||
* `indent`: if `align` not set to `center`, then this controls the indent
|
||||
level. **Default Value**: `0em`.
|
||||
* `show_menu`: a boolean value that controls whether the mathjax contextual
|
||||
menu is shown. **Default Value**: True
|
||||
* `process_escapes`: a boolean value that controls whether mathjax processes escape
|
||||
sequences. **Default Value**: True
|
||||
* `latex_preview`: controls the preview message users are seen while mathjax is
|
||||
rendering LaTex. If set to `Tex`, then the TeX code is used as the preview
|
||||
(which will be visible until it is processed by MathJax). **Default Value**: `Tex`
|
||||
* `color`: controls the color of the mathjax rendered font. **Default Value**: `black`
|
||||
* `ssl`: specifies if ssl should be used to load MathJax engine. Can be set to one
|
||||
of three things
|
||||
* `auto`: **Default Value** will automatically determine what protodol to use
|
||||
based on current protocol of the site.
|
||||
* `force`: will force ssl to be used.
|
||||
* `off`: will ensure that ssl is not used
|
||||
|
||||
For example, in settings.py, the following would make math render in blue and
|
||||
displaymath align to the left:
|
||||
|
||||
MATH = {'color':'blue','align':left}
|
||||
|
||||
LaTex Examples
|
||||
--------------
|
||||
###Inline
|
||||
LaTex between `$`..`$`, for example, `$`x^2`$`, will be rendered inline
|
||||
with respect to the current html block.
|
||||
|
||||
###Displayed Math
|
||||
LaTex between `$$`..`$$`, for example, `$$`x^2`$$`, will be rendered centered in a
|
||||
new paragraph.
|
||||
|
||||
###Equations
|
||||
LaTex between `\begin` and `\end`, for example, `begin{equation}` x^2 `\end{equation}`,
|
||||
will be rendered centered in a new paragraph with a right justified equation number
|
||||
at the top of the paragraph. This equation number can be referenced in the document.
|
||||
To do this, use a `label` inside of the equation format and then refer to that label
|
||||
using `ref`. For example: `begin{equation}` `\label{eq}` X^2 `\end{equation}`. Now
|
||||
refer to that equation number by `$`\ref{eq}`$`.
|
||||
|
||||
MathML Examples
|
||||
---------------
|
||||
The following will render the Quadratic formula:
|
||||
```
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
|
||||
<mrow>
|
||||
<mi>x</mi>
|
||||
<mo>=</mo>
|
||||
<mfrac>
|
||||
<mrow>
|
||||
<mo>−</mo>
|
||||
<mi>b</mi>
|
||||
<mo>±</mo>
|
||||
<msqrt>
|
||||
<mrow>
|
||||
<msup>
|
||||
<mi>b</mi>
|
||||
<mn>2</mn>
|
||||
</msup>
|
||||
<mo>−</mo>
|
||||
<mn>4</mn>
|
||||
<mi>a</mi>
|
||||
<mi>c</mi>
|
||||
</mrow>
|
||||
</msqrt>
|
||||
</mrow>
|
||||
<mrow>
|
||||
<mn>2</mn>
|
||||
<mi>a</mi>
|
||||
</mrow>
|
||||
</mfrac>
|
||||
</mrow>
|
||||
</math>
|
||||
```
|
||||
1
render_math/__init__.py
Normal file
1
render_math/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .math import *
|
||||
379
render_math/math.py
Normal file
379
render_math/math.py
Normal file
@@ -0,0 +1,379 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Math Render Plugin for Pelican
|
||||
==============================
|
||||
This plugin allows your site to render Math. It supports both LaTeX and MathML
|
||||
using the MathJax JavaScript engine.
|
||||
|
||||
Typogrify Compatibility
|
||||
-----------------------
|
||||
This plugin now plays nicely with Typogrify, but it requires
|
||||
Typogrify version 2.04 or above.
|
||||
|
||||
User Settings
|
||||
-------------
|
||||
Users are also able to pass a dictionary of settings in the settings file which
|
||||
will control how the MathJax library renders things. This could be very useful
|
||||
for template builders that want to adjust the look and feel of the math.
|
||||
See README for more details.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from pelican import signals
|
||||
from pelican import contents
|
||||
|
||||
|
||||
# Global Variables
|
||||
_TYPOGRIFY = None # if Typogrify is enabled, this is set to the typogrify.filter function
|
||||
_WRAP_LATEX = None # the tag to wrap LaTeX math in (needed to play nicely with Typogrify or for template designers)
|
||||
_MATH_REGEX = re.compile(r'(\$\$|\$|\\begin\{(.+?)\}|<(math)(?:\s.*?)?>).*?(\1|\\end\{\2\}|</\3>)', re.DOTALL | re.IGNORECASE) # used to detect math
|
||||
_MATH_SUMMARY_REGEX = None # used to match math in summary
|
||||
_MATH_INCOMPLETE_TAG_REGEX = None # used to match math that has been cut off in summary
|
||||
_MATHJAX_SETTINGS = {} # settings that can be specified by the user, used to control mathjax script settings
|
||||
with open (os.path.dirname(os.path.realpath(__file__))+'/mathjax_script.txt', 'r') as mathjax_script: # Read the mathjax javascript from file
|
||||
_MATHJAX_SCRIPT=mathjax_script.read()
|
||||
|
||||
|
||||
# Python standard library for binary search, namely bisect is cool but I need
|
||||
# specific business logic to evaluate my search predicate, so I am using my
|
||||
# own version
|
||||
def binary_search(match_tuple, ignore_within):
|
||||
"""Determines if t is within tupleList. Using the fact that tupleList is
|
||||
ordered, binary search can be performed which is O(logn)
|
||||
"""
|
||||
|
||||
ignore = False
|
||||
if ignore_within == []:
|
||||
return False
|
||||
|
||||
lo = 0
|
||||
hi = len(ignore_within)-1
|
||||
|
||||
# Find first value in array where predicate is False
|
||||
# predicate function: tupleList[mid][0] < t[index]
|
||||
while lo < hi:
|
||||
mid = lo + (hi-lo+1)//2
|
||||
if ignore_within[mid][0] < match_tuple[0]:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid-1
|
||||
|
||||
if lo >= 0 and lo <= len(ignore_within)-1:
|
||||
ignore = (ignore_within[lo][0] <= match_tuple[0] and ignore_within[lo][1] >= match_tuple[1])
|
||||
|
||||
return ignore
|
||||
|
||||
|
||||
def ignore_content(content):
|
||||
"""Creates a list of match span tuples for which content should be ignored
|
||||
e.g. <pre> and <code> tags
|
||||
"""
|
||||
ignore_within = []
|
||||
|
||||
# used to detect all <pre> and <code> tags. NOTE: Alter this regex should
|
||||
# additional tags need to be ignored
|
||||
ignore_regex = re.compile(r'<(pre|code)(?:\s.*?)?>.*?</(\1)>', re.DOTALL | re.IGNORECASE)
|
||||
|
||||
for match in ignore_regex.finditer(content):
|
||||
ignore_within.append(match.span())
|
||||
|
||||
return ignore_within
|
||||
|
||||
|
||||
def wrap_math(content, ignore_within):
|
||||
"""Wraps math in user specified tags.
|
||||
|
||||
This is needed for Typogrify to play nicely with math but it can also be
|
||||
styled by template providers
|
||||
"""
|
||||
|
||||
wrap_math.found_math = False
|
||||
|
||||
def math_tag_wrap(match):
|
||||
"""function for use in re.sub"""
|
||||
|
||||
# determine if the tags are within <pre> and <code> blocks
|
||||
ignore = binary_search(match.span(1), ignore_within) or binary_search(match.span(4), ignore_within)
|
||||
|
||||
if ignore or match.group(3) == 'math':
|
||||
if match.group(3) == 'math':
|
||||
# Will detect mml, but not wrap anything around it
|
||||
wrap_math.found_math = True
|
||||
|
||||
return match.group(0)
|
||||
else:
|
||||
wrap_math.found_math = True
|
||||
return '<%s>%s</%s>' % (_WRAP_LATEX, match.group(0), _WRAP_LATEX)
|
||||
|
||||
return (_MATH_REGEX.sub(math_tag_wrap, content), wrap_math.found_math)
|
||||
|
||||
|
||||
def process_summary(instance, ignore_within):
|
||||
"""Summaries need special care. If Latex is cut off, it must be restored.
|
||||
|
||||
In addition, the mathjax script must be included if necessary thereby
|
||||
making it independent to the template
|
||||
"""
|
||||
|
||||
process_summary.altered_summary = False
|
||||
insert_mathjax = False
|
||||
end_tag = '</%s>' % _WRAP_LATEX if _WRAP_LATEX is not None else ''
|
||||
|
||||
# use content's _get_summary method to obtain summary
|
||||
summary = instance._get_summary()
|
||||
|
||||
# Determine if there is any math in the summary which are not within the
|
||||
# ignore_within tags
|
||||
math_item = None
|
||||
for math_item in _MATH_SUMMARY_REGEX.finditer(summary):
|
||||
ignore = binary_search(math_item.span(2), ignore_within)
|
||||
if '...' not in math_item.group(5):
|
||||
ignore = ignore or binary_search(math_item.span(5), ignore_within)
|
||||
else:
|
||||
ignore = ignore or binary_search(math_item.span(6), ignore_within)
|
||||
|
||||
if ignore:
|
||||
math_item = None # In <code> or <pre> tags, so ignore
|
||||
else:
|
||||
insert_mathjax = True
|
||||
|
||||
# Repair the math if it was cut off math_item will be the final math
|
||||
# code matched that is not within <pre> or <code> tags
|
||||
if math_item and '...' in math_item.group(5):
|
||||
if math_item.group(3) is not None:
|
||||
end = r'\end{%s}' % math_item.group(3)
|
||||
elif math_item.group(4) is not None:
|
||||
end = r'</math>'
|
||||
elif math_item.group(2) is not None:
|
||||
end = math_item.group(2)
|
||||
|
||||
search_regex = r'%s(%s.*?%s)' % (re.escape(instance._content[0:math_item.start(1)]), re.escape(math_item.group(1)), re.escape(end))
|
||||
math_match = re.search(search_regex, instance._content, re.DOTALL | re.IGNORECASE)
|
||||
|
||||
if math_match:
|
||||
new_summary = summary.replace(math_item.group(0), math_match.group(1)+'%s ...' % end_tag)
|
||||
|
||||
if new_summary != summary:
|
||||
if _MATHJAX_SETTINGS['auto_insert']:
|
||||
return new_summary+_MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
|
||||
else:
|
||||
instance.mathjax = True
|
||||
return new_summary
|
||||
|
||||
def incomplete_end_latex_tag(match):
|
||||
"""function for use in re.sub"""
|
||||
if binary_search(match.span(3), ignore_within):
|
||||
return match.group(0)
|
||||
|
||||
process_summary.altered_summary = True
|
||||
return match.group(1) + match.group(4)
|
||||
|
||||
# check for partial math tags at end. These must be removed
|
||||
summary = _MATH_INCOMPLETE_TAG_REGEX.sub(incomplete_end_latex_tag, summary)
|
||||
|
||||
if process_summary.altered_summary or insert_mathjax:
|
||||
if insert_mathjax:
|
||||
if _MATHJAX_SETTINGS['auto_insert']:
|
||||
summary+= _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
|
||||
else:
|
||||
instance.mathjax = True
|
||||
|
||||
return summary
|
||||
|
||||
return None # Making it explicit that summary was not altered
|
||||
|
||||
|
||||
def process_settings(settings):
|
||||
"""Sets user specified MathJax settings (see README for more details)"""
|
||||
|
||||
global _MATHJAX_SETTINGS
|
||||
|
||||
# NOTE TO FUTURE DEVELOPERS: Look at the README and what is happening in
|
||||
# this function if any additional changes to the mathjax settings need to
|
||||
# be incorporated. Also, please inline comment what the variables
|
||||
# will be used for
|
||||
|
||||
# Default settings
|
||||
_MATHJAX_SETTINGS['align'] = 'center' # controls alignment of of displayed equations (values can be: left, right, center)
|
||||
_MATHJAX_SETTINGS['indent'] = '0em' # if above is not set to 'center', then this setting acts as an indent
|
||||
_MATHJAX_SETTINGS['show_menu'] = 'true' # controls whether to attach mathjax contextual menu
|
||||
_MATHJAX_SETTINGS['process_escapes'] = 'true' # controls whether escapes are processed
|
||||
_MATHJAX_SETTINGS['latex_preview'] = 'TeX' # controls what user sees while waiting for LaTex to render
|
||||
_MATHJAX_SETTINGS['color'] = 'black' # controls color math is rendered in
|
||||
|
||||
# Source for MathJax: default (below) is to automatically determine what protocol to use
|
||||
_MATHJAX_SETTINGS['source'] = """'https:' == document.location.protocol
|
||||
? 'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
|
||||
: 'http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"""
|
||||
|
||||
# This next setting controls whether the mathjax script should be automatically
|
||||
# inserted into the content. The mathjax script will not be inserted into
|
||||
# the content if no math is detected. For summaries that are present in the
|
||||
# index listings, mathjax script will also be automatically inserted.
|
||||
# Setting this value to false means the template must be altered if this
|
||||
# plugin is to work, and so it is only recommended for the template
|
||||
# designer who wants maximum control.
|
||||
_MATHJAX_SETTINGS['auto_insert'] = True # controls whether mathjax script is automatically inserted into the content
|
||||
|
||||
if not isinstance(settings, dict):
|
||||
return
|
||||
|
||||
# The following mathjax settings can be set via the settings dictionary
|
||||
# Iterate over dictionary in a way that is compatible with both version 2
|
||||
# and 3 of python
|
||||
for key, value in ((key, settings[key]) for key in settings):
|
||||
if key == 'auto_insert' and isinstance(value, bool):
|
||||
_MATHJAX_SETTINGS[key] = value
|
||||
|
||||
if key == 'align' and isinstance(value, str):
|
||||
if value == 'left' or value == 'right' or value == 'center':
|
||||
_MATHJAX_SETTINGS[key] = value
|
||||
else:
|
||||
_MATHJAX_SETTINGS[key] = 'center'
|
||||
|
||||
if key == 'indent':
|
||||
_MATHJAX_SETTINGS[key] = value
|
||||
|
||||
if key == 'show_menu' and isinstance(value, bool):
|
||||
_MATHJAX_SETTINGS[key] = 'true' if value else 'false'
|
||||
|
||||
if key == 'process_escapes' and isinstance(value, bool):
|
||||
_MATHJAX_SETTINGS[key] = 'true' if value else 'false'
|
||||
|
||||
if key == 'latex_preview' and isinstance(value, str):
|
||||
_MATHJAX_SETTINGS[key] = value
|
||||
|
||||
if key == 'color' and isinstance(value, str):
|
||||
_MATHJAX_SETTINGS[key] = value
|
||||
|
||||
if key == 'ssl' and isinstance(value, str):
|
||||
if value == 'off':
|
||||
_MATHJAX_SETTINGS['source'] = "'http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"
|
||||
|
||||
if value == 'force':
|
||||
_MATHJAX_SETTINGS['source'] = "'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'"
|
||||
|
||||
|
||||
def process_content(instance):
|
||||
"""Processes content, with logic to ensure that Typogrify does not clash
|
||||
with math.
|
||||
|
||||
In addition, mathjax script is inserted at the end of the content thereby
|
||||
making it independent of the template
|
||||
"""
|
||||
|
||||
if not instance._content:
|
||||
return
|
||||
|
||||
ignore_within = ignore_content(instance._content)
|
||||
|
||||
if _WRAP_LATEX:
|
||||
instance._content, math = wrap_math(instance._content, ignore_within)
|
||||
else:
|
||||
math = True if _MATH_REGEX.search(instance._content) else False
|
||||
|
||||
# The user initially set Typogrify to be True, but since it would clash
|
||||
# with math, we set it to False. This means that the default reader will
|
||||
# not call Typogrify, so it is called here, where we are able to control
|
||||
# logic for it ignore math if necessary
|
||||
if _TYPOGRIFY:
|
||||
# Tell Typogrify to ignore the tags that math has been wrapped in
|
||||
# also, Typogrify must always ignore mml (math) tags
|
||||
ignore_tags = [_WRAP_LATEX,'math'] if _WRAP_LATEX else ['math']
|
||||
|
||||
# Exact copy of the logic as found in the default reader
|
||||
instance._content = _TYPOGRIFY(instance._content, ignore_tags)
|
||||
instance.metadata['title'] = _TYPOGRIFY(instance.metadata['title'], ignore_tags)
|
||||
|
||||
if math:
|
||||
if _MATHJAX_SETTINGS['auto_insert']:
|
||||
# Mathjax script added to content automatically. Now it
|
||||
# does not need to be explicitly added to the template
|
||||
instance._content += _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
|
||||
else:
|
||||
# Place the burden on ensuring mathjax script is available to
|
||||
# browser on the template designer (see README for more details)
|
||||
instance.mathjax = True
|
||||
|
||||
# The summary needs special care because math math cannot just be cut
|
||||
# off
|
||||
summary = process_summary(instance, ignore_within)
|
||||
if summary is not None:
|
||||
instance._summary = summary
|
||||
|
||||
|
||||
def pelican_init(pelicanobj):
|
||||
"""Intialializes certain global variables and sets typogogrify setting to
|
||||
False should it be set to True.
|
||||
"""
|
||||
|
||||
global _TYPOGRIFY
|
||||
global _WRAP_LATEX
|
||||
global _MATH_SUMMARY_REGEX
|
||||
global _MATH_INCOMPLETE_TAG_REGEX
|
||||
|
||||
try:
|
||||
settings = pelicanobj.settings['MATH']
|
||||
except:
|
||||
settings = None
|
||||
|
||||
process_settings(settings)
|
||||
|
||||
# Allows MathJax script to be accessed from template should it be needed
|
||||
pelicanobj.settings['MATHJAXSCRIPT'] = _MATHJAX_SCRIPT.format(**_MATHJAX_SETTINGS)
|
||||
|
||||
# If Typogrify set to True, then we need to handle it manually so it does
|
||||
# not conflict with LaTeX
|
||||
try:
|
||||
if pelicanobj.settings['TYPOGRIFY'] is True:
|
||||
pelicanobj.settings['TYPOGRIFY'] = False
|
||||
try:
|
||||
from typogrify.filters import typogrify
|
||||
|
||||
# Determine if this is the correct version of Typogrify to use
|
||||
import inspect
|
||||
typogrify_args = inspect.getargspec(typogrify).args
|
||||
if len(typogrify_args) < 2 or 'ignore_tags' not in typogrify_args:
|
||||
raise TypeError('Incorrect version of Typogrify')
|
||||
|
||||
# At this point, we are happy to use Typogrify, meaning
|
||||
# it is installed and it is a recent enough version
|
||||
# that can be used to ignore all math
|
||||
_TYPOGRIFY = typogrify
|
||||
_WRAP_LATEX = 'mathjax' # default to wrap mathjax content inside of
|
||||
except ImportError:
|
||||
print("\nTypogrify is not installed, so it is being ignored.\nIf you want to use it, please install via: pip install typogrify\n")
|
||||
except TypeError:
|
||||
print("\nA more recent version of Typogrify is needed for the render_math module.\nPlease upgrade Typogrify to the latest version (anything above version 2.04 is okay).\nTypogrify will be turned off due to this reason.\n")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Set _WRAP_LATEX to the settings tag if defined. The idea behind this is
|
||||
# to give template designers control over how math would be rendered
|
||||
try:
|
||||
if pelicanobj.settings['MATH']['wrap_latex']:
|
||||
_WRAP_LATEX = pelicanobj.settings['MATH']['wrap_latex']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# regular expressions that depend on _WRAP_LATEX are set here
|
||||
tag_start= r'<%s>' % _WRAP_LATEX if not _WRAP_LATEX is None else ''
|
||||
tag_end = r'</%s>' % _WRAP_LATEX if not _WRAP_LATEX is None else ''
|
||||
math_summary_regex = r'((\$\$|\$|\\begin\{(.+?)\}|<(math)(?:\s.*?)?>).+?)(\2|\\end\{\3\}|</\4>|\s?\.\.\.)(%s|</\4>)?' % tag_end
|
||||
|
||||
# NOTE: The logic in _get_summary will handle <math> correctly because it
|
||||
# is perceived as an html tag. Therefore we are only interested in handling
|
||||
# non mml (i.e. LaTex)
|
||||
incomplete_end_latex_tag = r'(.*)(%s)(\\\S*?|\$)\s*?(\s?\.\.\.)(%s)?$' % (tag_start, tag_end)
|
||||
|
||||
_MATH_SUMMARY_REGEX = re.compile(math_summary_regex, re.DOTALL | re.IGNORECASE)
|
||||
_MATH_INCOMPLETE_TAG_REGEX = re.compile(incomplete_end_latex_tag, re.DOTALL | re.IGNORECASE)
|
||||
|
||||
|
||||
def register():
|
||||
"""Plugin registration"""
|
||||
|
||||
signals.initialized.connect(pelican_init)
|
||||
signals.content_object_init.connect(process_content)
|
||||
28
render_math/mathjax_script.txt
Normal file
28
render_math/mathjax_script.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
<script type= "text/javascript">
|
||||
if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {{
|
||||
var mathjaxscript = document.createElement('script');
|
||||
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
|
||||
mathjaxscript.type = 'text/javascript';
|
||||
mathjaxscript.src = {source};
|
||||
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
|
||||
"MathJax.Hub.Config({{" +
|
||||
" config: ['MMLorHTML.js']," +
|
||||
" TeX: {{ extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: {{ autoNumber: 'AMS' }} }}," +
|
||||
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
|
||||
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
|
||||
" displayAlign: '{align}'," +
|
||||
" displayIndent: '{indent}'," +
|
||||
" showMathMenu: {show_menu}," +
|
||||
" tex2jax: {{ " +
|
||||
" inlineMath: [ ['$','$'] ], " +
|
||||
" displayMath: [ ['$$','$$'] ]," +
|
||||
" processEscapes: {process_escapes}," +
|
||||
" preview: '{latex_preview}'," +
|
||||
" }}, " +
|
||||
" 'HTML-CSS': {{ " +
|
||||
" styles: {{ '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {{color: '{color} ! important'}} }}" +
|
||||
" }} " +
|
||||
"}}); ";
|
||||
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
|
||||
}}
|
||||
</script>
|
||||
8
representative_image/Readme.md
Normal file
8
representative_image/Readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Summary
|
||||
|
||||
This plugin extracts a representative image (i.e, featured image) from the article's summary or article's content. The image can be access at `article.featured_image`.
|
||||
|
||||
The plugin also remove 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.
|
||||
|
||||
1
representative_image/__init__.py
Normal file
1
representative_image/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .representative_image import *
|
||||
31
representative_image/representative_image.py
Normal file
31
representative_image/representative_image.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pelican import signals
|
||||
from pelican.contents import Content, Article
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def images_extraction(instance):
|
||||
representativeImage = None
|
||||
if type(instance) == Article:
|
||||
# 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 = unicode(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
|
||||
|
||||
def register():
|
||||
signals.content_object_init.connect(images_extraction)
|
||||
54
representative_image/test_representative_image.py
Normal file
54
representative_image/test_representative_image.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
import unittest
|
||||
|
||||
from jinja2.utils import generate_lorem_ipsum
|
||||
|
||||
# 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))
|
||||
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 + '"/>'
|
||||
|
||||
|
||||
from pelican.contents import Article
|
||||
import representative_image
|
||||
|
||||
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)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
65
share_post/README.md
Normal file
65
share_post/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
Share Post
|
||||
==========
|
||||
|
||||
A Pelican plugin to create share URLs of article
|
||||
|
||||
Copyright (c) Talha Mansoor
|
||||
|
||||
Author | Talha Mansoor
|
||||
----------------|-----
|
||||
Author Email | talha131@gmail.com
|
||||
Author Homepage | http://onCrashReboot.com
|
||||
Github Account | https://github.com/talha131
|
||||
|
||||
Why do you need it?
|
||||
===================
|
||||
|
||||
Almost all website have share widgets to let readers share posts on social
|
||||
networks. Most of these widgets are used by vendors for online tracking. These
|
||||
widgets are also visual which quite often become a distraction and negatively
|
||||
affect readers attention.
|
||||
|
||||
`share_post` creates old school URLs for some popular sites which your theme
|
||||
can use. These links do not have the ability to track the users. They can also
|
||||
be unobtrusive depending on how Pelican theme uses them.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
`share_post` requires BeautifulSoup
|
||||
|
||||
```bash
|
||||
pip install beautifulsoup4
|
||||
```
|
||||
|
||||
How to Use
|
||||
==========
|
||||
|
||||
`share_post` adds a dictionary attribute to `article` which can be accessed via
|
||||
`article.share_post`. Keys of the dictionary are as follows,
|
||||
|
||||
1. `facebook`
|
||||
1. `google-plus`
|
||||
1. `email`
|
||||
1. `twitter`
|
||||
|
||||
Template Example
|
||||
================
|
||||
|
||||
```python
|
||||
{% if article.share_post and article.status != 'draft' %}
|
||||
<section>
|
||||
<p id="post-share-links">
|
||||
Share on:
|
||||
<a href="{{article.share_post['twitter']}}" target="_blank" title="Share on Twitter">Twitter</a>
|
||||
❄
|
||||
<a href="{{article.share_post['facebook']}}" target="_blank" title="Share on Facebook">Facebook</a>
|
||||
❄
|
||||
<a href="{{article.share_post['google-plus']}}" target="_blank" title="Share on Google Plus">Google+</a>
|
||||
❄
|
||||
<a href="{{article.share_post['email']}}" target="_blank" title="Share via Email">Email</a>
|
||||
</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
1
share_post/__init__.py
Normal file
1
share_post/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .share_post import *
|
||||
58
share_post/share_post.py
Normal file
58
share_post/share_post.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Share Post
|
||||
==========
|
||||
|
||||
This plugin adds share URL to article. These links are textual which means no
|
||||
online tracking of your readers.
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
from pelican import signals, contents
|
||||
|
||||
|
||||
def article_title(content):
|
||||
main_title = BeautifulSoup(content.title, 'html.parser').prettify().strip()
|
||||
sub_title = ''
|
||||
if hasattr(content, 'subtitle'):
|
||||
sub_title = BeautifulSoup(content.subtitle, 'html.parser').prettify().strip()
|
||||
return quote(('%s %s' % (main_title, sub_title)).encode('utf-8'))
|
||||
|
||||
|
||||
def article_url(content):
|
||||
site_url = content.settings['SITEURL']
|
||||
return quote(('%s/%s' % (site_url, content.url)).encode('utf-8'))
|
||||
|
||||
|
||||
def article_summary(content):
|
||||
return quote(content.summary.encode('utf-8'))
|
||||
|
||||
|
||||
def share_post(content):
|
||||
if isinstance(content, contents.Static):
|
||||
return
|
||||
title = article_title(content)
|
||||
url = article_url(content)
|
||||
summary = article_summary(content)
|
||||
|
||||
tweet = '%s %s' % (title, url)
|
||||
facebook_link = 'http://www.facebook.com/sharer/sharer.php?s=100' \
|
||||
'&p[url]=%s&p[images][0]=&p[title]=%s&p[summary]=%s' \
|
||||
% (url, title, summary)
|
||||
gplus_link = 'https://plus.google.com/share?url=%s' % url
|
||||
twitter_link = 'http://twitter.com/home?status=%s' % tweet
|
||||
mail_link = 'mailto:?subject=%s&body=%s' % (title, url)
|
||||
|
||||
share_links = {'twitter': twitter_link,
|
||||
'facebook': facebook_link,
|
||||
'google-plus': gplus_link,
|
||||
'email': mail_link
|
||||
}
|
||||
content.share_post = share_links
|
||||
|
||||
|
||||
def register():
|
||||
signals.content_object_init.connect(share_post)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user