mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Merge b4a09947dc into 34103cd5dd
This commit is contained in:
commit
8c15b643d2
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
|
||||
-----------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,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.
|
||||
|
||||
|
|
@ -251,69 +313,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, Page, Static
|
||||
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