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):