diff --git a/docs/changelog.rst b/docs/changelog.rst index f52d64493c..8ddc432d26 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Release history Next release ============ -- Nothing yet +* Add support for the ``{include}`` syntax 3.6.3 (2015-08-14) ================== diff --git a/docs/content.rst b/docs/content.rst index 0fa8992108..dcf32b0488 100644 --- a/docs/content.rst +++ b/docs/content.rst @@ -335,6 +335,41 @@ Linking to authors, categories, index and tags You can link to authors, categories, index and tags using the ``{author}name``, ``{category}foobar``, ``{index}`` and ``{tag}tagname`` syntax. +Including common text into your content +--------------------------------------- + +From Pelican 3.6.4 you can include common text snippets into your content using +the ``{include}file.ext`` syntax. You can specify semi-absolute paths starting +from the ``PATH`` directory, e.g. ``{include}/pages/disclaimer.html`` or use +relative paths, e.g. ``{include}notice.html``. Relativity is +calculated based on the location of the file containing the ``{include}``. +For example when you have the following content layout:: + + content + └── notice2.html + └── pages + ├── page1.html + └── notice1.html + +Then the includes may look like:: + + + + PAGE 1 + + + This is the content of page 1 + + {include}../notice2.html + + + + +``notice2.html`` looks like:: + + {include}pages/notice1.html + This is the second warning about relative paths + Deprecated internal link syntax ------------------------------- diff --git a/pelican/contents.py b/pelican/contents.py index 0123384acf..50eb97894d 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -18,7 +18,8 @@ from pelican.utils import (SafeDatetime, deprecated_attribute, memoized, path_to_url, posixize_path, python_2_unicode_compatible, set_date_tzinfo, - slugify, strftime, truncate_html_words) + slugify, strftime, truncate_html_words, + pelican_open) # Import these so that they're avalaible when you import from pelican.contents. from pelican.urlwrappers import (Author, Category, Tag, URLWrapper) # NOQA @@ -186,6 +187,30 @@ def get_url_setting(self, key): key = key if self.in_default_lang else 'lang_%s' % key return self._expand_settings(key) + def _path_replacer(self, path, relative_dir = None): + """ + Update path depending on whether this is an absolute + or relative value. + """ + if not relative_dir: + relative_dir = self.relative_dir + + if path.startswith('/'): + path = path[1:] + else: + # relative to the source path of this content + path = self.get_relative_source_path( + os.path.join(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 + + return path + def _update_content(self, content, siteurl): """Update the content attribute. @@ -217,19 +242,7 @@ def replacer(m): # 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 + path = self._path_replacer(path) linked_content = self._context['filenames'].get(path) if linked_content: @@ -276,12 +289,41 @@ def replacer(m): def get_siteurl(self): return self._context.get('localsiteurl', '') + def _update_includes(self, content, source_path = None): + """ + Replace {include}some.file with the + contents of this file. + """ + regex = r"""[{|]include[|}](?P[\w./]+)""" + hrefs = re.compile(regex, re.X) + + def replacer(m): + path = m.group('path') + path = self._path_replacer(path, source_path) + path = posixize_path( + os.path.abspath( + os.path.join(self.settings['PATH'], path) + ) + ) + + with pelican_open(path) as text: + # if we recurse into another file to perform more includes + # self._path_replacer needs to know in which directory + # it operates otherwise it produces wrong paths + source_dir = posixize_path(os.path.dirname(path)) + + text = self._update_includes(text, source_dir) + return text + + return hrefs.sub(replacer, content) + @memoized def get_content(self, siteurl): if hasattr(self, '_get_content'): content = self._get_content() else: content = self._content + content = self._update_includes(content) return self._update_content(content, siteurl) @property diff --git a/pelican/tests/content/include/include3.html b/pelican/tests/content/include/include3.html new file mode 100644 index 0000000000..6933bccea8 --- /dev/null +++ b/pelican/tests/content/include/include3.html @@ -0,0 +1,2 @@ +this file includes another in a different directory +{include}../include1.html \ No newline at end of file diff --git a/pelican/tests/content/include/include4.html b/pelican/tests/content/include/include4.html new file mode 100644 index 0000000000..aa2181bdc6 --- /dev/null +++ b/pelican/tests/content/include/include4.html @@ -0,0 +1,2 @@ +this file includes another via absolute path +{include}/include1.html \ No newline at end of file diff --git a/pelican/tests/content/include1.html b/pelican/tests/content/include1.html new file mode 100644 index 0000000000..b307a825fc --- /dev/null +++ b/pelican/tests/content/include1.html @@ -0,0 +1 @@ +this content has been included \ No newline at end of file diff --git a/pelican/tests/content/include2.html b/pelican/tests/content/include2.html new file mode 100644 index 0000000000..b8c46657fb --- /dev/null +++ b/pelican/tests/content/include2.html @@ -0,0 +1,2 @@ +this file includes another +{include}include1.html \ No newline at end of file diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py index 3da3f7897f..1f724f82df 100644 --- a/pelican/tests/test_cache.py +++ b/pelican/tests/test_cache.py @@ -60,8 +60,11 @@ def test_article_object_caching(self): - article_with_comments.html - article_with_null_attributes.html - 2012-11-30_md_w_filename_meta#foo-bar.md + + There are 4 more include*.html files which are HTML snippets + and also not valid. """ - self.assertEqual(generator.readers.read_file.call_count, 4) + self.assertEqual(generator.readers.read_file.call_count, 8) @unittest.skipUnless(MagicMock, 'Needs Mock module') def test_article_reader_content_caching(self): diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py index 6f0f6dd939..948975a01d 100644 --- a/pelican/tests/test_contents.py +++ b/pelican/tests/test_contents.py @@ -22,6 +22,7 @@ TEST_CONTENT = str(generate_lorem_ipsum(n=1)) TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False) +CONTENT_PATH = os.path.join(os.path.dirname(__file__), 'content') class TestPage(LoggedTestCase): @@ -418,6 +419,90 @@ def test_intrasite_link_markdown_spaces(self): 'link' ) + def test_includes(self): + args = self.page_kwargs.copy() + args['settings'] = get_settings() + args['source_path'] = CONTENT_PATH + args['context']['filenames'] = {} + settings = get_settings() + settings['PATH'] = CONTENT_PATH + args['settings'] = settings + + # one include via relative path + args['content'] = ( + 'There is a simple include here ' + '{include}include1.html' + ' Included content is above' + ) + content = Page(**args).get_content('http://notmyidea.org') + self.assertEqual( + content, + 'There is a simple include here ' + 'this content has been included' + ' Included content is above' + ) + + # two nested includes via relative paths + args['content'] = ( + 'There is a simple include here ' + '{include}include2.html' + ' Included content is above' + ) + content = Page(**args).get_content('http://notmyidea.org') + self.assertEqual( + content, + 'There is a simple include here ' + 'this file includes another\n' + 'this content has been included' + ' Included content is above' + ) + + # include via full path + args['content'] = ( + 'There is a simple include here ' + '{include}/include1.html' + ' Included content is above' + ) + content = Page(**args).get_content('http://notmyidea.org') + self.assertEqual( + content, + 'There is a simple include here ' + 'this content has been included' + ' Included content is above' + ) + + # 2nd include is in different directory + # include paths are relative to the caller directory + args['content'] = ( + 'There is a simple include here ' + '{include}include/include3.html' + ' Included content is above' + ) + content = Page(**args).get_content('http://notmyidea.org') + self.assertEqual( + content, + 'There is a simple include here ' + 'this file includes another in a different directory\n' + 'this content has been included' + ' Included content is above' + ) + + # 2nd include using absolute path in the included file + args['content'] = ( + 'There is a simple include here ' + '{include}include/include4.html' + ' Included content is above' + ) + content = Page(**args).get_content('http://notmyidea.org') + self.assertEqual( + content, + 'There is a simple include here ' + 'this file includes another via absolute path\n' + 'this content has been included' + ' Included content is above' + ) + + def test_multiple_authors(self): """Test article with multiple authors.""" args = self.page_kwargs.copy()