diff --git a/.gitignore b/.gitignore
index 459469464c..07c9956d08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
.*.swp
.*.swo
*.pyc
+.cache/
.DS_Store
docs/_build
docs/fr/_build
@@ -16,3 +17,4 @@ six-*.egg/
venv
samples/output
*.pem
+pip-wheel-metadata/
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 0000000000..4aafe7421c
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,3 @@
+Release type: minor
+
+Add support for the ``{include}`` syntax
diff --git a/docs/content.rst b/docs/content.rst
index f0022c35e1..be4ab0ce34 100644
--- a/docs/content.rst
+++ b/docs/content.rst
@@ -369,6 +369,45 @@ 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 4.2 onward, 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
+
+When using ``{include}`` it is best to blacklist the included files using the
+``IGNORE_FILES`` setting. Otherwise Pelican will try to render them as regular
+content and will most likely fail!
+
Deprecated internal link syntax
-------------------------------
diff --git a/pelican/contents.py b/pelican/contents.py
index a862db2d66..4ebd4ac7bd 100644
--- a/pelican/contents.py
+++ b/pelican/contents.py
@@ -359,14 +359,8 @@ def get_static_links(self):
path = value.path
if what not in {'static', 'attach'}:
continue
- 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)
- )
- path = path.replace('%20', ' ')
+ path = relativize_path(self.settings['PATH'],
+ self.relative_dir, path)
static_links.add(path)
return static_links
@@ -449,24 +443,11 @@ def get_relative_source_path(self, source_path=None):
"""
if not source_path:
source_path = self.source_path
- if source_path is None:
- return None
-
- return posixize_path(
- os.path.relpath(
- os.path.abspath(os.path.join(
- self.settings['PATH'],
- source_path)),
- os.path.abspath(self.settings['PATH'])
- ))
+ return get_relative_source_path(self.settings['PATH'], source_path)
@property
def relative_dir(self):
- return posixize_path(
- os.path.dirname(
- os.path.relpath(
- os.path.abspath(self.source_path),
- os.path.abspath(self.settings['PATH']))))
+ return relative_dir(self.settings['PATH'], self.source_path)
def refresh_metadata_intersite_links(self):
for key in self.settings['FORMATTED_FIELDS']:
@@ -613,3 +594,114 @@ def _log_reason(reason):
self.override_save_as = new_save_as
self.override_url = new_url
+
+
+def get_relative_source_path(content_path, source_path):
+ if source_path is None:
+ return None
+
+ return posixize_path(
+ os.path.relpath(
+ os.path.abspath(os.path.join(
+ content_path,
+ source_path)),
+ os.path.abspath(content_path)
+ ))
+
+
+def relativize_path(content_path, relative_dir, path):
+ """
+ Update path depending on whether this is an absolute
+ or relative value.
+ """
+ if path.startswith('/'):
+ path = path[1:]
+ else:
+ path = get_relative_source_path(content_path,
+ os.path.join(relative_dir, path))
+
+ path = path.replace('%20', ' ')
+
+ return path
+
+
+def relative_dir(content_path, path):
+ return posixize_path(
+ os.path.dirname(
+ os.path.relpath(
+ os.path.abspath(path),
+ os.path.abspath(content_path))))
+
+
+def insert_included_content(content,
+ source_path,
+ content_path,
+ exclude_exts=()):
+ """
+ Replace {include}some.file with the
+ contents of this file.
+
+ Perform conversion to HTML
+ """
+ regex = r"""(?P\n[ \t]+)?[{|]include[|}](?P[\w./]+)"""
+ hrefs = re.compile(regex, re.X)
+ processed_paths = []
+ # In Python 3.x we can use the `nonlocal` declaration, in `replacer()`,
+ # to tell Python we mean to assign to the `source_path` variable from
+ # `insert_included_content()`.
+ # In Python 2.x we simply can't assign to `source_path` in `replacer()`.
+ # However, we work around this by not assigning to the variable itself,
+ # but using a mutable container to keep track about the current working
+ # directory while doing the recursion.
+ source_dir = [relative_dir(content_path, source_path)]
+
+ def replacer(m):
+ path, indent = m.group('path'), m.group('indent')
+ path = relativize_path(content_path, source_dir[0], path)
+ path = posixize_path(
+ os.path.abspath(
+ os.path.join(content_path, path)
+ )
+ )
+
+ if not os.path.isfile(path):
+ logger.warning("Unable to find `%s`, skipping include.", path)
+ return ''.join(('{include}', m.group('path')))
+
+ _, ext = os.path.splitext(path)
+ # remove leading dot
+ ext = ext[1:]
+
+ if ext in exclude_exts:
+ return ''.join(('{include}', m.group('path')))
+
+ with open(path) as content_file:
+ text = content_file.read()
+
+ if indent:
+ prefix = ''
+ if indent[0] == '\n':
+ prefix = '\n'
+ indent = indent[1:]
+ text = prefix + ''.join(indent + line
+ for line in text.splitlines(keepends=True))
+
+ # recursion stop
+ if path in processed_paths:
+ raise RuntimeError("Circular inclusion detected for '%s'" % path)
+ processed_paths.append(path)
+
+ # if we recurse into another file to perform more includes
+ # _path_replacer needs to know in which directory
+ # it operates otherwise it produces wrong paths
+ source_dir[0] = posixize_path(os.path.dirname(path))
+ current_source_dir = source_dir[0]
+
+ # recursively replace other includes
+ text = hrefs.sub(replacer, text)
+
+ # restore source dir
+ source_dir[0] = current_source_dir
+ return text
+
+ return hrefs.sub(replacer, content)
diff --git a/pelican/generators.py b/pelican/generators.py
index 75eca3888c..9bb039b462 100644
--- a/pelican/generators.py
+++ b/pelican/generators.py
@@ -155,16 +155,21 @@ def get_files(self, paths, exclude=[], extensions=None):
if os.path.isdir(root):
for dirpath, dirs, temp_files in os.walk(
- root, followlinks=True):
- drop = []
+ root, topdown=True, followlinks=True):
excl = exclusions_by_dirpath.get(dirpath, ())
- for d in dirs:
+ # We copy the `dirs` list as we will modify it in the loop:
+ for d in list(dirs):
if (d in excl or
any(fnmatch.fnmatch(d, ignore)
for ignore in ignores)):
- drop.append(d)
- for d in drop:
- dirs.remove(d)
+ if d in dirs:
+ dirs.remove(d)
+
+ d = os.path.basename(dirpath)
+ if (d in excl or
+ any(fnmatch.fnmatch(d, ignore)
+ for ignore in ignores)):
+ continue
reldir = os.path.relpath(dirpath, self.path)
for f in temp_files:
diff --git a/pelican/readers.py b/pelican/readers.py
index 0edfed0eca..32d6248f43 100644
--- a/pelican/readers.py
+++ b/pelican/readers.py
@@ -5,6 +5,7 @@
import os
import re
from collections import OrderedDict
+from tempfile import NamedTemporaryFile
import docutils
import docutils.core
@@ -19,7 +20,8 @@
from pelican import rstdirectives # NOQA
from pelican import signals
from pelican.cache import FileStampDataCacher
-from pelican.contents import Author, Category, Page, Tag
+from pelican.contents import Author, Category, Page, Tag, \
+ insert_included_content
from pelican.utils import SafeDatetime, escape_html, get_date, pelican_open, \
posixize_path
@@ -286,9 +288,28 @@ def _get_publisher(self, source_path):
def read(self, source_path):
"""Parses restructured text"""
- pub = self._get_publisher(source_path)
- parts = pub.writer.parts
- content = parts.get('body')
+ with pelican_open(source_path) as content:
+ exclude_exts = set(Readers(self.settings).extensions)
+ exclude_exts -= set(self.file_extensions)
+ content = insert_included_content(content, source_path,
+ self.settings['PATH'],
+ exclude_exts)
+ # We have pre-processed the file content,
+ # but docutils require a file as input,
+ # so with use a temporary one:
+ with NamedTemporaryFile('w+', encoding='utf8') as tmp_file:
+ tmp_file.write(content)
+ tmp_file.seek(0)
+ try:
+ pub = self._get_publisher(tmp_file.name)
+ parts = pub.writer.parts
+ content = parts.get('body')
+ except docutils.ApplicationError as err:
+ # We fix any potential error message
+ # to reference the original file:
+ msg = err.args[0].replace(tmp_file.name, source_path)
+ err.args = (msg,)
+ raise err
metadata = self._parse_metadata(pub.document, source_path)
metadata.setdefault('title', parts.get('title'))
@@ -349,6 +370,11 @@ def read(self, source_path):
self._source_path = source_path
self._md = Markdown(**self.settings['MARKDOWN'])
with pelican_open(source_path) as text:
+ exclude_exts = set(Readers(self.settings).extensions)
+ exclude_exts -= set(self.file_extensions)
+ text = insert_included_content(text, source_path,
+ self.settings['PATH'],
+ exclude_exts)
content = self._md.convert(text)
if hasattr(self._md, 'Meta'):
@@ -500,7 +526,12 @@ def read(self, filename):
metadata = {}
for k in parser.metadata:
metadata[k] = self.process_metadata(k, parser.metadata[k])
- return parser.body, metadata
+
+ if parser.body:
+ return parser.body, metadata
+ else:
+ # in case we're parsing HTML includes
+ return content, metadata
class Readers(FileStampDataCacher):
@@ -596,6 +627,13 @@ def read_file(self, base_path, path, content_class=Page, fmt=None,
metadata.update(_filter_discardable_metadata(reader_metadata))
if content:
+ # We excluded file extensions already processed
+ # by the dedicated readers:
+ exclude_exts = set(MarkdownReader.file_extensions)
+ exclude_exts |= set(RstReader.file_extensions)
+ content = insert_included_content(content, path,
+ self.settings['PATH'],
+ exclude_exts)
# find images with empty alt
find_empty_alt(content, path)
diff --git a/pelican/tests/content/include/html_from_subdir_includer.md b/pelican/tests/content/include/html_from_subdir_includer.md
new file mode 100755
index 0000000000..1873a6bf7f
--- /dev/null
+++ b/pelican/tests/content/include/html_from_subdir_includer.md
@@ -0,0 +1,5 @@
+_includes HTML_:
+
+{include}subdir/include_other.html
+
+^Included content above^
diff --git a/pelican/tests/content/include/html_includer.md b/pelican/tests/content/include/html_includer.md
new file mode 100644
index 0000000000..94accee147
--- /dev/null
+++ b/pelican/tests/content/include/html_includer.md
@@ -0,0 +1,5 @@
+_includes HTML_:
+
+{include}included.html
+
+^Included content above^
diff --git a/pelican/tests/content/include/html_includer.rst b/pelican/tests/content/include/html_includer.rst
new file mode 100644
index 0000000000..4705abe651
--- /dev/null
+++ b/pelican/tests/content/include/html_includer.rst
@@ -0,0 +1,6 @@
+Article including some HTML file
+################################
+
+{include}included.html
+
+^Included content above^
diff --git a/pelican/tests/content/include/html_includer_with_full_path.md b/pelican/tests/content/include/html_includer_with_full_path.md
new file mode 100755
index 0000000000..f41503811d
--- /dev/null
+++ b/pelican/tests/content/include/html_includer_with_full_path.md
@@ -0,0 +1,5 @@
+_includes HTML_:
+
+{include}/pelican/tests/content/include/included.html
+
+^Included content above^
diff --git a/pelican/tests/content/include/include_other.html b/pelican/tests/content/include/include_other.html
new file mode 100644
index 0000000000..c62838b5bd
--- /dev/null
+++ b/pelican/tests/content/include/include_other.html
@@ -0,0 +1 @@
+{include}include_sibling.html
diff --git a/pelican/tests/content/include/include_sibling.html b/pelican/tests/content/include/include_sibling.html
new file mode 100644
index 0000000000..71f783808a
--- /dev/null
+++ b/pelican/tests/content/include/include_sibling.html
@@ -0,0 +1 @@
+{include}include_other.html
diff --git a/pelican/tests/content/include/included.html b/pelican/tests/content/include/included.html
new file mode 100644
index 0000000000..5d27544c65
--- /dev/null
+++ b/pelican/tests/content/include/included.html
@@ -0,0 +1 @@
+this content has been included
diff --git a/pelican/tests/content/include/included.md b/pelican/tests/content/include/included.md
new file mode 100644
index 0000000000..08f4b65992
--- /dev/null
+++ b/pelican/tests/content/include/included.md
@@ -0,0 +1,2 @@
+**this is Markdown**
+Here is a [link](https://docs.getpelican.com).
diff --git a/pelican/tests/content/include/included.py b/pelican/tests/content/include/included.py
new file mode 100644
index 0000000000..81cd2f9a29
--- /dev/null
+++ b/pelican/tests/content/include/included.py
@@ -0,0 +1,5 @@
+import antigravity
+
+import this
+
+_ = antigravity + this
diff --git a/pelican/tests/content/include/included.rst b/pelican/tests/content/include/included.rst
new file mode 100755
index 0000000000..32769be314
--- /dev/null
+++ b/pelican/tests/content/include/included.rst
@@ -0,0 +1,2 @@
+**this is reStructuredText**
+Here is a `link `_.
diff --git a/pelican/tests/content/include/includer_of_md_includer.md b/pelican/tests/content/include/includer_of_md_includer.md
new file mode 100755
index 0000000000..396e7cb56f
--- /dev/null
+++ b/pelican/tests/content/include/includer_of_md_includer.md
@@ -0,0 +1,5 @@
+START
+
+{include}md_includer.md
+
+END
diff --git a/pelican/tests/content/include/inexisting_file_includer.md b/pelican/tests/content/include/inexisting_file_includer.md
new file mode 100755
index 0000000000..258af104b8
--- /dev/null
+++ b/pelican/tests/content/include/inexisting_file_includer.md
@@ -0,0 +1,5 @@
+_includes HTML_:
+
+{include}inexisting_file.html
+
+^Included content above^
diff --git a/pelican/tests/content/include/md_includer.html b/pelican/tests/content/include/md_includer.html
new file mode 100755
index 0000000000..764f0ead92
--- /dev/null
+++ b/pelican/tests/content/include/md_includer.html
@@ -0,0 +1,2 @@
+includes Markdown: {include}included.md
+^Included content above^
diff --git a/pelican/tests/content/include/md_includer.md b/pelican/tests/content/include/md_includer.md
new file mode 100644
index 0000000000..942c719a70
--- /dev/null
+++ b/pelican/tests/content/include/md_includer.md
@@ -0,0 +1,2 @@
+_inline includes Markdown_: {include}included.md
+^Included content above^
diff --git a/pelican/tests/content/include/py_includer.md b/pelican/tests/content/include/py_includer.md
new file mode 100755
index 0000000000..c0bcd3bf74
--- /dev/null
+++ b/pelican/tests/content/include/py_includer.md
@@ -0,0 +1,3 @@
+```
+{include}included.py
+```
\ No newline at end of file
diff --git a/pelican/tests/content/include/py_includer.rst b/pelican/tests/content/include/py_includer.rst
new file mode 100755
index 0000000000..1d04031cbd
--- /dev/null
+++ b/pelican/tests/content/include/py_includer.rst
@@ -0,0 +1,6 @@
+Article with an indented code block
+###################################
+
+.. code-block:: python
+
+ {include}included.py
diff --git a/pelican/tests/content/include/rst_includer.rst b/pelican/tests/content/include/rst_includer.rst
new file mode 100644
index 0000000000..b7860f147a
--- /dev/null
+++ b/pelican/tests/content/include/rst_includer.rst
@@ -0,0 +1,5 @@
+Article with an inline included reStructuredText file
+#####################################################
+
+Inline includes *reStructuredText*: {include}included.rst
+^Included content above^
diff --git a/pelican/tests/content/include/subdir/include_other.html b/pelican/tests/content/include/subdir/include_other.html
new file mode 100644
index 0000000000..8f31fa29e5
--- /dev/null
+++ b/pelican/tests/content/include/subdir/include_other.html
@@ -0,0 +1,2 @@
+this file includes another via absolute path
+{include}/pelican/tests/content/include/subdir/include_parent.html
diff --git a/pelican/tests/content/include/subdir/include_parent.html b/pelican/tests/content/include/subdir/include_parent.html
new file mode 100644
index 0000000000..32e10f2ff7
--- /dev/null
+++ b/pelican/tests/content/include/subdir/include_parent.html
@@ -0,0 +1,2 @@
+this file includes another in a parent directory
+{include}../included.html
diff --git a/pelican/tests/support.py b/pelican/tests/support.py
index 93db8328ec..d95407a95d 100644
--- a/pelican/tests/support.py
+++ b/pelican/tests/support.py
@@ -188,16 +188,16 @@ class LogCountHandler(BufferingHandler):
"""Capturing and counting logged messages."""
def __init__(self, capacity=1000):
- logging.handlers.BufferingHandler.__init__(self, capacity)
+ super(LogCountHandler, self).__init__(capacity)
- def count_logs(self, msg=None, level=None):
- return len([
+ def get_logs(self, msg=None, level=None):
+ return [
l
for l
in self.buffer
- if (msg is None or re.match(msg, l.getMessage())) and
- (level is None or l.levelno == level)
- ])
+ if ((msg is None or re.match(msg, l.getMessage())) and
+ (level is None or l.levelno == level))
+ ]
class LoggedTestCase(unittest.TestCase):
@@ -213,8 +213,16 @@ def tearDown(self):
super(LoggedTestCase, self).tearDown()
def assertLogCountEqual(self, count=None, msg=None, **kwargs):
- actual = self._logcount_handler.count_logs(msg=msg, **kwargs)
+ actual_logs = self._logcount_handler.get_logs(msg=msg, **kwargs)
self.assertEqual(
- actual, count,
+ len(actual_logs), count,
msg='expected {} occurrences of {!r}, but found {}'.format(
- count, msg, actual))
+ count, msg, len(actual_logs)))
+
+ def assertNoLogs(self, count=None, msg=None, **kwargs):
+ 'Better than .assertLogCountEqual(0) because'
+ ' it prints the generated logs, if any'
+ actual_logs = self._logcount_handler.get_logs(msg=msg, **kwargs)
+ if actual_logs:
+ self.fail('Some logs were generated:\n'
+ + '\n'.join('{}: {}'.format(log.levelname, log.message) for log in actual_logs))
diff --git a/pelican/tests/test_cache.py b/pelican/tests/test_cache.py
index ceba649e30..764769cb70 100644
--- a/pelican/tests/test_cache.py
+++ b/pelican/tests/test_cache.py
@@ -33,6 +33,7 @@ def _get_cache_enabled_settings(self):
settings['CACHE_CONTENT'] = True
settings['LOAD_CONTENT_CACHE'] = True
settings['CACHE_PATH'] = self.temp_cache
+ settings['IGNORE_FILES'] = ['include']
return settings
def test_generator_caching(self):
@@ -155,13 +156,7 @@ def test_article_object_caching(self):
generator.readers.read_file = MagicMock()
generator.generate_context()
"""
- 6 files don't get cached because they were not valid
- - article_with_attributes_containing_double_quotes.html
- - article_with_comments.html
- - article_with_null_attributes.html
- - 2012-11-30_md_w_filename_meta#foo-bar.md
- - empty.md
- - empty_with_bom.md
+ Some files don't get cached because they were not valid
"""
self.assertEqual(generator.readers.read_file.call_count, 6)
diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py
index 267571da0b..8ad88b1956 100644
--- a/pelican/tests/test_generators.py
+++ b/pelican/tests/test_generators.py
@@ -183,6 +183,7 @@ def setUpClass(cls):
settings['DEFAULT_DATE'] = (1970, 1, 1)
settings['READERS'] = {'asc': None}
settings['CACHE_CONTENT'] = False
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
cls.generator = ArticlesGenerator(
@@ -307,6 +308,7 @@ def test_do_not_use_folder_as_category(self):
settings['USE_FOLDER_AS_CATEGORY'] = False
settings['CACHE_PATH'] = self.temp_cache
settings['READERS'] = {'asc': None}
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
@@ -404,6 +406,7 @@ def test_period_in_timeperiod_archive(self):
settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html'
settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/'
settings['CACHE_PATH'] = self.temp_cache
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
@@ -514,6 +517,7 @@ def test_standard_metadata_in_default_metadata(self):
# DEFAULT_CATEGORY
('category', 'Random'),
('tags', 'general, untagged'))
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
@@ -543,6 +547,7 @@ def test_article_order_by(self):
settings['DEFAULT_CATEGORY'] = 'Default'
settings['DEFAULT_DATE'] = (1970, 1, 1)
settings['ARTICLE_ORDER_BY'] = 'title'
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
generator = ArticlesGenerator(
@@ -590,6 +595,7 @@ def test_article_order_by(self):
settings['DEFAULT_CATEGORY'] = 'Default'
settings['DEFAULT_DATE'] = (1970, 1, 1)
settings['ARTICLE_ORDER_BY'] = 'reversed-title'
+ settings['IGNORE_FILES'] = ['include']
context = get_context(settings)
generator = ArticlesGenerator(
diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py
index 502a45ac74..9e54007893 100644
--- a/pelican/tests/test_pelican.py
+++ b/pelican/tests/test_pelican.py
@@ -191,6 +191,7 @@ def test_theme_static_paths_copy_single_file(self):
def test_write_only_selected(self):
"""Test that only the selected files are written"""
settings = read_settings(path=None, override={
+ 'DEBUG': True,
'PATH': INPUT_PATH,
'OUTPUT_PATH': self.temp_path,
'CACHE_PATH': self.temp_cache,
@@ -206,9 +207,11 @@ def test_write_only_selected(self):
logger.setLevel(logging.INFO)
mute(True)(pelican.run)()
logger.setLevel(orig_level)
- self.assertLogCountEqual(
- count=2,
- msg="Writing .*",
+ self.assertLogCountEqual(count=1,
+ msg='Writing .+/oh-yeah.html',
+ level=logging.INFO)
+ self.assertLogCountEqual(count=1,
+ msg='Writing .+/categories.html',
level=logging.INFO)
def test_cyclic_intersite_links_no_warnings(self):
diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py
index 30181f5449..38d689c6e0 100644
--- a/pelican/tests/test_readers.py
+++ b/pelican/tests/test_readers.py
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
+import logging
import os
import six
+from pelican import contents
from pelican import readers
-from pelican.tests.support import get_settings, unittest
+from pelican.tests.support import LoggedTestCase, get_settings, unittest
from pelican.utils import SafeDatetime
try:
@@ -25,7 +27,7 @@ def _path(*args):
return os.path.join(CONTENT_PATH, *args)
-class ReaderTest(unittest.TestCase):
+class ReaderTest(LoggedTestCase):
def read_file(self, path, **kwargs):
# Isolate from future API changes to readers.read_file
@@ -84,6 +86,20 @@ def test_fail_wrong_val(self):
class DefaultReaderTest(ReaderTest):
+ maxDiff = None
+
+ def setUp(self):
+ super(DefaultReaderTest, self).setUp()
+ self._initial_contents_log_level = contents.logger.level
+ contents.logger.level = logging.INFO
+ self._initial_readers_log_level = readers.logger.level
+ readers.logger.level = logging.INFO
+
+ def tearDown(self):
+ super(DefaultReaderTest, self).tearDown()
+ contents.logger.level = self._initial_contents_log_level
+ readers.logger.level = self._initial_readers_log_level
+
def test_readfile_unknown_extension(self):
with self.assertRaises(TypeError):
self.read_file(path='article_with_metadata.unknownextension')
@@ -104,6 +120,155 @@ def test_find_empty_alt(self):
'Other images have empty alt attributes'}
)
+ def test_include_markdown_from_markdown(self):
+ page = self.read_file('include/md_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'inline includes Markdown: '
+ 'this is Markdown\n'
+ 'Here is a link.
\n'
+ '^Included content above^
'
+ )
+ self.assertNoLogs()
+
+ def test_include_html_from_markdown(self):
+ page = self.read_file('include/html_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'includes HTML:
\n'
+ 'this content has been included\n'
+ '
\n'
+ '^Included content above^
'
+ )
+ self.assertNoLogs()
+
+ def test_include_markdown_from_html(self):
+ page = self.read_file('include/md_includer.html')
+ self.assertEqual(
+ page.get_content(''),
+ 'includes Markdown: {include}included.md\n'
+ '^Included content above^\n'
+ )
+ self.assertNoLogs()
+
+ def test_include_rst_from_rst(self):
+ page = self.read_file('include/rst_includer.rst')
+ self.assertEqual(
+ page.get_content(''),
+ 'Inline includes reStructuredText: '
+ 'this is reStructuredText\n'
+ 'Here is a link.
\n'
+ '^Included content above^
\n'
+ )
+ self.assertNoLogs()
+
+ def test_include_html_from_rst(self):
+ page = self.read_file('include/html_includer.rst')
+ self.assertEqual(
+ page.get_content(''),
+ 'this content has been included\n'
+ '
\n'
+ '^Included content above^
\n'
+ )
+ self.assertNoLogs()
+
+ def test_include_code_from_markdown(self):
+ page = self.read_file('include/py_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ ''
+ ''
+ 'import '
+ 'antigravity\n'
+ '\n'
+ 'import '
+ 'this\n'
+ '\n'
+ '_ '
+ '= '
+ 'antigravity '
+ '+ '
+ 'this\n'
+ '
'
+ )
+
+ def test_include_code_from_rst(self):
+ page = self.read_file('include/py_includer.rst')
+ self.assertEqual(
+ page.get_content(''),
+ ''
+ ''
+ 'import '
+ 'antigravity\n'
+ '\n'
+ 'import '
+ 'this\n'
+ '\n'
+ '_ '
+ '= '
+ 'antigravity '
+ '+ '
+ 'this\n'
+ '
\n'
+ )
+ self.assertNoLogs()
+
+ def test_include_nested_markdown(self):
+ page = self.read_file('include/includer_of_md_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'START
\n'
+ 'inline includes Markdown: '
+ 'this is Markdown\n'
+ 'Here is a link.
\n'
+ '^Included content above^
\n'
+ 'END
'
+ )
+ self.assertNoLogs()
+
+ def test_include_html_with_full_path(self):
+ page = self.read_file('include/html_includer_with_full_path.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'includes HTML:
\n'
+ 'this content has been included\n'
+ '
\n'
+ '^Included content above^
'
+ )
+ self.assertNoLogs()
+
+ def test_include_html_in_subdirectory(self):
+ page = self.read_file('include/html_from_subdir_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'includes HTML:
\n'
+ 'this file includes another via absolute path\n'
+ 'this file includes another in a parent directory\n'
+ 'this content has been included\n\n\n'
+ '
\n'
+ '^Included content above^
'
+ )
+ self.assertNoLogs()
+
+ def test_include_non_existing_file(self):
+ page = self.read_file('include/inexisting_file_includer.md')
+ self.assertEqual(
+ page.get_content(''),
+ 'includes HTML:
\n'
+ '{include}inexisting_file.html
\n'
+ '^Included content above^
'
+ )
+ self.assertLogCountEqual(
+ count=1,
+ msg='Unable to find `.*`, skipping include.',
+ level=logging.WARNING)
+
+ def test_include_with_recursion_loop(self):
+ expected_msg = 'Circular inclusion detected'
+ with self.assertRaisesRegex(RuntimeError, expected_msg):
+ self.read_file('include/include_sibling.html')
+
class RstReaderTest(ReaderTest):