mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Introduce Jinja2 filters for converting links in additional fields.
Until now, the link replacing worked only on article and page
contents or summaries. With this patch, if one needes to replace
links in custom fields, there are two new Jinja2 filters that can do
that. For fields that are referenced in the `FORMATTED_FIELDS` setting,
one can use the `expand_links` Jinja2 filter in the template, passing
the field name as a parameter:
{{ article|expand_links('legal') }}
If the custom field consists of just one link (for example a link to
article cover image for a social meta tag), one can use the
`expand_link` Jinja2 filter:
{{ article|expand_link('cover') }}
With the above being in a template and `FORMATTED_FIELDS` setting
containing the `'legal'` field, a RST article making use of both fields
could look like this:
An article
##########
📅 2017-06-22
:legal: This article is released under `CC0 {filename}/license.rst`.
:cover: {filename}/img/article-cover.jpg
This commit is contained in:
parent
42b205d75c
commit
b4a09947dc
6 changed files with 171 additions and 65 deletions
|
|
@ -5,6 +5,8 @@ Next release
|
|||
============
|
||||
|
||||
* New signal: ``feed_generated``
|
||||
* Introduced ``expand_link`` and ``expand_links`` Jinja2 filters to allow URL
|
||||
replacement in user-defined metadata fields.
|
||||
|
||||
3.7.1 (2017-01-10)
|
||||
==================
|
||||
|
|
|
|||
|
|
@ -202,6 +202,29 @@ and ``article2.md``::
|
|||
[a link relative to the current file]({filename}category/article1.rst)
|
||||
[a link relative to the content root]({filename}/category/article1.rst)
|
||||
|
||||
The link replacing works by default on article and page contents as well as
|
||||
summaries. If you need to replace links in custom formatted fields that are
|
||||
referenced in the ``FORMATTED_FIELDS`` setting, use the ``expand_links``
|
||||
Jinja2 filter in your template, passing the field name as a parameter::
|
||||
|
||||
{{ article|expand_links('legal') }}
|
||||
|
||||
If your custom field consists of just one link (for example a link to article
|
||||
cover image for a social meta tag), use the ``expand_link`` Jinja2 filter::
|
||||
|
||||
{{ article|expand_link('cover') }}
|
||||
|
||||
With the above being in a template and ``FORMATTED_FIELDS`` setting containing
|
||||
the ``'legal'`` field, a RST article making use of both fields could look like
|
||||
this::
|
||||
|
||||
An article
|
||||
##########
|
||||
|
||||
:date: 2017-06-22
|
||||
:legal: This article is released under `CC0 {filename}/license.rst`.
|
||||
:cover: {filename}/img/article-cover.jpg
|
||||
|
||||
Linking to static files
|
||||
-----------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,68 @@ class Content(object):
|
|||
key = key if self.in_default_lang else 'lang_%s' % key
|
||||
return self._expand_settings(key)
|
||||
|
||||
def _link_replacer(self, siteurl, m):
|
||||
what = m.group('what')
|
||||
value = urlparse(m.group('value'))
|
||||
path = value.path
|
||||
origin = m.group('path')
|
||||
|
||||
# XXX Put this in a different location.
|
||||
if what in {'filename', 'attach'}:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
else:
|
||||
# relative to the source path of this content
|
||||
path = self.get_relative_source_path(
|
||||
os.path.join(self.relative_dir, path)
|
||||
)
|
||||
|
||||
if path not in self._context['filenames']:
|
||||
unquoted_path = path.replace('%20', ' ')
|
||||
|
||||
if unquoted_path in self._context['filenames']:
|
||||
path = unquoted_path
|
||||
|
||||
linked_content = self._context['filenames'].get(path)
|
||||
if linked_content:
|
||||
if what == 'attach':
|
||||
if isinstance(linked_content, Static):
|
||||
linked_content.attach_to(self)
|
||||
else:
|
||||
logger.warning(
|
||||
"%s used {attach} link syntax on a "
|
||||
"non-static file. Use {filename} instead.",
|
||||
self.get_relative_source_path())
|
||||
origin = '/'.join((siteurl, linked_content.url))
|
||||
origin = origin.replace('\\', '/') # for Windows paths.
|
||||
else:
|
||||
logger.warning(
|
||||
"Unable to find '%s', skipping url replacement.",
|
||||
value.geturl(), extra={
|
||||
'limit_msg': ("Other resources were not found "
|
||||
"and their urls not replaced")})
|
||||
elif what == 'category':
|
||||
origin = '/'.join((siteurl, Category(path, self.settings).url))
|
||||
elif what == 'tag':
|
||||
origin = '/'.join((siteurl, Tag(path, self.settings).url))
|
||||
elif what == 'index':
|
||||
origin = '/'.join((siteurl, self.settings['INDEX_SAVE_AS']))
|
||||
elif what == 'author':
|
||||
origin = '/'.join((siteurl, Author(path, self.settings).url))
|
||||
else:
|
||||
logger.warning(
|
||||
"Replacement Indicator '%s' not recognized, "
|
||||
"skipping replacement",
|
||||
what)
|
||||
|
||||
# keep all other parts, such as query, fragment, etc.
|
||||
parts = list(value)
|
||||
parts[2] = origin
|
||||
origin = urlunparse(parts)
|
||||
|
||||
return ''.join((m.group('markup'), m.group('quote'), origin,
|
||||
m.group('quote')))
|
||||
|
||||
def _update_content(self, content, siteurl):
|
||||
"""Update the content attribute.
|
||||
|
||||
|
|
@ -227,69 +289,7 @@ class Content(object):
|
|||
\2""".format(instrasite_link_regex)
|
||||
hrefs = re.compile(regex, re.X)
|
||||
|
||||
def replacer(m):
|
||||
what = m.group('what')
|
||||
value = urlparse(m.group('value'))
|
||||
path = value.path
|
||||
origin = m.group('path')
|
||||
|
||||
# XXX Put this in a different location.
|
||||
if what in {'filename', 'attach'}:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
else:
|
||||
# relative to the source path of this content
|
||||
path = self.get_relative_source_path(
|
||||
os.path.join(self.relative_dir, path)
|
||||
)
|
||||
|
||||
if path not in self._context['filenames']:
|
||||
unquoted_path = path.replace('%20', ' ')
|
||||
|
||||
if unquoted_path in self._context['filenames']:
|
||||
path = unquoted_path
|
||||
|
||||
linked_content = self._context['filenames'].get(path)
|
||||
if linked_content:
|
||||
if what == 'attach':
|
||||
if isinstance(linked_content, Static):
|
||||
linked_content.attach_to(self)
|
||||
else:
|
||||
logger.warning(
|
||||
"%s used {attach} link syntax on a "
|
||||
"non-static file. Use {filename} instead.",
|
||||
self.get_relative_source_path())
|
||||
origin = '/'.join((siteurl, linked_content.url))
|
||||
origin = origin.replace('\\', '/') # for Windows paths.
|
||||
else:
|
||||
logger.warning(
|
||||
"Unable to find '%s', skipping url replacement.",
|
||||
value.geturl(), extra={
|
||||
'limit_msg': ("Other resources were not found "
|
||||
"and their urls not replaced")})
|
||||
elif what == 'category':
|
||||
origin = '/'.join((siteurl, Category(path, self.settings).url))
|
||||
elif what == 'tag':
|
||||
origin = '/'.join((siteurl, Tag(path, self.settings).url))
|
||||
elif what == 'index':
|
||||
origin = '/'.join((siteurl, self.settings['INDEX_SAVE_AS']))
|
||||
elif what == 'author':
|
||||
origin = '/'.join((siteurl, Author(path, self.settings).url))
|
||||
else:
|
||||
logger.warning(
|
||||
"Replacement Indicator '%s' not recognized, "
|
||||
"skipping replacement",
|
||||
what)
|
||||
|
||||
# keep all other parts, such as query, fragment, etc.
|
||||
parts = list(value)
|
||||
parts[2] = origin
|
||||
origin = urlunparse(parts)
|
||||
|
||||
return ''.join((m.group('markup'), m.group('quote'), origin,
|
||||
m.group('quote')))
|
||||
|
||||
return hrefs.sub(replacer, content)
|
||||
return hrefs.sub(lambda m: self._link_replacer(siteurl, m), content)
|
||||
|
||||
def get_siteurl(self):
|
||||
return self._context.get('localsiteurl', '')
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ from pelican import signals
|
|||
from pelican.cache import FileStampDataCacher
|
||||
from pelican.contents import Article, Draft, Page, Static, is_valid_content
|
||||
from pelican.readers import Readers
|
||||
from pelican.utils import (DateFormatter, copy, mkdir_p, posixize_path,
|
||||
process_translations, python_2_unicode_compatible)
|
||||
from pelican.utils import (DateFormatter, HtmlLinkExpander, LinkExpander, copy,
|
||||
mkdir_p, posixize_path, process_translations,
|
||||
python_2_unicode_compatible)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -74,6 +75,10 @@ class Generator(object):
|
|||
# provide utils.strftime as a jinja filter
|
||||
self.env.filters.update({'strftime': DateFormatter()})
|
||||
|
||||
# provide link expansion as a jinja filter
|
||||
self.env.filters.update({'expand_link': LinkExpander(settings)})
|
||||
self.env.filters.update({'expand_links': HtmlLinkExpander()})
|
||||
|
||||
# get custom Jinja filters from user settings
|
||||
custom_filters = self.settings['JINJA_FILTERS']
|
||||
self.env.filters.update(custom_filters)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import time
|
|||
from sys import platform
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from jinja2 import DictLoader, Environment
|
||||
import pytz
|
||||
|
||||
import six
|
||||
|
||||
from pelican import utils
|
||||
from pelican.contents import Article, Page, Static
|
||||
from pelican.generators import TemplatePagesGenerator
|
||||
from pelican.settings import read_settings
|
||||
from pelican.tests.support import (LoggedTestCase, get_article,
|
||||
|
|
@ -670,6 +672,46 @@ class TestDateFormatter(unittest.TestCase):
|
|||
utils.strftime(self.date, 'date = %A, %d %B %Y'))
|
||||
|
||||
|
||||
class TestLinkExpanders(unittest.TestCase):
|
||||
"""Tests Jinja2 expand_link() and expand_links() filters."""
|
||||
|
||||
def test_expand_link(self):
|
||||
settings = read_settings()
|
||||
env = Environment(
|
||||
loader=DictLoader({'a.html': "{{article|expand_link('cover')}}"})
|
||||
)
|
||||
env.filters.update({'expand_link': utils.LinkExpander(settings)})
|
||||
|
||||
linked_image = Static('', source_path='image.png')
|
||||
context = {'filenames': {'image.png': linked_image},
|
||||
'localsiteurl': 'https://my.cool.site'}
|
||||
content_mock = Article('', metadata={
|
||||
'title': 'Article',
|
||||
'cover': "{filename}/image.png"}, context=context)
|
||||
result = env.get_template('a.html').render(article=content_mock)
|
||||
self.assertEqual('https://my.cool.site/image.png', result)
|
||||
|
||||
def test_expand_links(self):
|
||||
env = Environment(
|
||||
loader=DictLoader({'a.html': "{{article|expand_links('legal')}}"})
|
||||
)
|
||||
env.filters.update({'expand_links': utils.HtmlLinkExpander()})
|
||||
|
||||
linked_page = Page('', source_path='legal.rst',
|
||||
metadata={'slug': 'license'})
|
||||
context = {'filenames': {'legal.rst': linked_page},
|
||||
'localsiteurl': 'https://my.cool.site'}
|
||||
content_mock = Article(
|
||||
'', metadata={
|
||||
'title': 'Article',
|
||||
'legal': "<a href=\"{filename}/legal.rst\">License</a>"
|
||||
}, context=context)
|
||||
result = env.get_template('a.html').render(article=content_mock)
|
||||
self.assertEqual(
|
||||
'<a href="https://my.cool.site/pages/license.html">License</a>',
|
||||
result)
|
||||
|
||||
|
||||
class TestSanitisedJoin(unittest.TestCase):
|
||||
def test_detect_parent_breakout(self):
|
||||
with six.assertRaisesRegex(
|
||||
|
|
|
|||
|
|
@ -147,6 +147,40 @@ class DateFormatter(object):
|
|||
return formatted
|
||||
|
||||
|
||||
class LinkExpander(object):
|
||||
"""Link expander object used as a jinja filter
|
||||
|
||||
Expands a custom field that contains just a link to internal content. The
|
||||
same rules as when links are expanded in article/page contents and
|
||||
summaries apply.
|
||||
"""
|
||||
|
||||
def __init__(self, settings):
|
||||
self.intrasite_link_regex = settings['INTRASITE_LINK_REGEX']
|
||||
|
||||
def __call__(self, content, attr):
|
||||
link_regex = r"""^
|
||||
(?P<markup>)(?P<quote>)
|
||||
(?P<path>{0}(?P<value>.*))
|
||||
$""".format(self.intrasite_link_regex)
|
||||
links = re.compile(link_regex, re.X)
|
||||
return links.sub(
|
||||
lambda m: content._link_replacer(content.get_siteurl(), m),
|
||||
getattr(content, attr))
|
||||
|
||||
|
||||
class HtmlLinkExpander(object):
|
||||
"""HTML link expander object used as a jinja filter
|
||||
|
||||
Expands links to internal contents in a custom HTML field. The same rules
|
||||
as when links are expanded in article/page contents and summaries apply.
|
||||
"""
|
||||
|
||||
def __call__(self, content, attr):
|
||||
return content._update_content(getattr(content, attr),
|
||||
content.get_siteurl())
|
||||
|
||||
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue