Repository: mondeja/mkdocs-include-markdown-plugin
Pull request #208 (2024-05-29 00:17:31)

Add new `recursive` argument to `include` directive
Fixes: #207

Additionally, it also excludes Jupyter Notebooks from the substitution, in case users are using `mkdocs-jupyter` to import files with very long text as cell outputs

carlocastoldi: somewhere, probably, i should write that if this is done to avoid very-long-text substitutions, than the user should use `include-markdown`.

If you think this can be handy only in this case scenario, i propose to make this directive only for `include` and swap the order of the substitutions.
carlocastoldi: > This `recursive` argument must be added to the `include-markdown` directive as well.

Do you see a scenario where markdown recursion might not be desirable, or is that just out of completeness? \
I would argue that stopping recursion in markdown is never desirable while it adds confusion on the order of the substitutions now mattering when including _very long lines_

mondeja: > I would argue that stopping recursion in markdown is never desirable while it adds confusion on the order of the substitutions now mattering when including very long lines

I don't think that it would add confusion, I don't understand where is the confusion here. Seems to me that it could be useful in certain scenarios, anyways not hurts to add it for completeness.
carlocastoldi: > I don't think that it would add confusion, I don't understand where is the confusion here. Seems to me that it could be useful in certain scenarios, anyways not hurts to add it for completeness.

This comment shows that it would add confusion, to me: https://github.com/mondeja/mkdocs-include-markdown-plugin/pull/208#discussion_r1619447095

But yeah, i'm okay with adding the possibility for markdown too. If you think it's really better. I just wanted to warn you on this

Patch:
diff --git a/README.md b/README.md
index a4130eb..6393c7f 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,7 @@ plugins:
       heading_offset: 0
       start: <!--start-->
       end: <!--end-->
+      recursive: true
 ```
 
 #### `opening_tag` and `closing_tag`
@@ -245,6 +246,9 @@ Includes the content of a file or a group of files.
 - <a name="include_encoding" href="#include_encoding">#</a>
   **encoding** (_utf-8_): Specify the encoding of the included file.
   If not defined `utf-8` will be used.
+- <a name="recursive" href="#include_recursive">#</a>
+  **recursive** (_true_): When this option is disabled, included files are not
+  processed for recursive includes. Possible values are `true` and `false`.
 
 ##### Examples
 
diff --git a/locale/es/README.md b/locale/es/README.md
index 006c8af..fd93fa1 100644
--- a/locale/es/README.md
+++ b/locale/es/README.md
@@ -48,6 +48,7 @@ plugins:
       heading_offset: 0
       start: <!--start-->
       end: <!--end-->
+      recursive: true
 ```
 
 #### `opening_tag` y `closing_tag`
@@ -236,6 +237,10 @@ Los valores posibles son `true` y `false`.
 - <a name="include_encoding" href="#include_encoding">#</a> **encoding**
 (*utf-8*): Especifica la codificación del archivo incluído. Si no se define,
 se usará `utf-8`.
+- <a name="recursive" href="#include_recursive">#</a> **recursive** (*true*):
+Cuando esta opción está deshabilitada, los archivos incluidos no son
+procesados para incluir de forma recursiva. Los valores posibles son `true` y
+`false`.
 
 ##### Ejemplos
 
diff --git a/locale/es/README.md.po b/locale/es/README.md.po
index 6cb4a9e..fa593f6 100644
--- a/locale/es/README.md.po
+++ b/locale/es/README.md.po
@@ -406,3 +406,13 @@ msgstr ""
 
 msgid "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin"
 msgstr "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin"
+
+msgid ""
+"<a name=\"recursive\" href=\"#include_recursive\">#</a> **recursive** "
+"(*true*): When this option is disabled, included files are not processed for"
+" recursive includes. Possible values are `true` and `false`."
+msgstr ""
+"<a name=\"recursive\" href=\"#include_recursive\">#</a> **recursive** "
+"(*true*): Cuando esta opción está deshabilitada, los archivos incluidos no "
+"son procesados para incluir de forma recursiva. Los valores posibles son "
+"`true` y `false`."
diff --git a/locale/fr/README.md b/locale/fr/README.md
index 2caeee1..6237c73 100644
--- a/locale/fr/README.md
+++ b/locale/fr/README.md
@@ -47,6 +47,7 @@ plugins:
       heading_offset: 0
       start: <!--start-->
       end: <!--end-->
+      recursive: true
 ```
 
 #### `opening_tag` et `closing_tag`
@@ -235,6 +236,9 @@ valeurs possibles sont `true` et `false`.
 - <a name="include_encoding" href="#include_encoding">#</a> **encoding**
 (*utf-8*): Spécifiez l'encodage du fichier inclus. S'il n'est pas défini,
 `utf-8` sera utilisé.
+- <a name="recursive" href="#include_recursive">#</a> **recursive** (*true*):
+Lorsque cette option est désactivée, les fichiers inclus ne sont pas traités
+pour des inclusions récursives. Les valeurs possibles sont `true` et `false`.
 
 ##### Exemples
 
diff --git a/locale/fr/README.md.po b/locale/fr/README.md.po
index 20c3964..dd59dfa 100644
--- a/locale/fr/README.md.po
+++ b/locale/fr/README.md.po
@@ -402,6 +402,15 @@ msgid ""
 msgstr ""
 "[Globs génériques Bash]: https://facelessuser.github.io/wcmatch/glob/#syntax"
 
-#, fuzzy
 msgid "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin"
 msgstr "[pypi-link]: https://pypi.org/project/mkdocs-include-markdown-plugin"
+
+msgid ""
+"<a name=\"recursive\" href=\"#include_recursive\">#</a> **recursive** "
+"(*true*): When this option is disabled, included files are not processed for"
+" recursive includes. Possible values are `true` and `false`."
+msgstr ""
+"<a name=\"recursive\" href=\"#include_recursive\">#</a> **recursive** "
+"(*true*): Lorsque cette option est désactivée, les fichiers inclus ne sont "
+"pas traités pour des inclusions récursives. Les valeurs possibles sont "
+"`true` et `false`."
diff --git a/pyproject.toml b/pyproject.toml
index 32ddb17..573a040 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "mkdocs-include-markdown-plugin"
-version = "6.0.7"
+version = "6.1.0"
 description = "Mkdocs Markdown includer plugin."
 readme = "README.md"
 license = "Apache-2.0"
diff --git a/src/mkdocs_include_markdown_plugin/config.py b/src/mkdocs_include_markdown_plugin/config.py
index 251f55c..a633428 100644
--- a/src/mkdocs_include_markdown_plugin/config.py
+++ b/src/mkdocs_include_markdown_plugin/config.py
@@ -24,3 +24,4 @@ class PluginConfig(Config):  # noqa: D101
     end = Optional(MkType(str))
     exclude = ListOfItems(MkType(str), default=[])
     cache = MkType(int, default=0)
+    recursive = MkType(bool, default=True)
diff --git a/src/mkdocs_include_markdown_plugin/directive.py b/src/mkdocs_include_markdown_plugin/directive.py
index 4c8a432..89e0f17 100644
--- a/src/mkdocs_include_markdown_plugin/directive.py
+++ b/src/mkdocs_include_markdown_plugin/directive.py
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
             'comments': bool,
             'rewrite-relative-urls': bool,
             'heading-offset': int,
+            'recursive': bool,
             'start': str | None,
             'end': str | None,
         },
@@ -96,6 +97,7 @@ ARGUMENT_REGEXES = {
     'dedent': arg('dedent'),
     'trailing-newlines': arg('trailing-newlines'),
     'rewrite-relative-urls': arg('rewrite-relative-urls'),
+    'recursive': arg('recursive'),
 
     # int
     'heading-offset': arg('heading-offset'),
diff --git a/src/mkdocs_include_markdown_plugin/event.py b/src/mkdocs_include_markdown_plugin/event.py
index f390c92..d284ac1 100644
--- a/src/mkdocs_include_markdown_plugin/event.py
+++ b/src/mkdocs_include_markdown_plugin/event.py
@@ -103,6 +103,22 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         http_cache: Cache | None = None,
 ) -> str:
     """Return the content of the file to include."""
+    settings_ignore_paths = []
+    if settings.exclude is not None:
+        for path in glob.glob(
+                [
+                    os.path.join(docs_dir, fp)
+                    if not os.path.isabs(fp)
+                    else fp for fp in settings.exclude
+                ],
+                flags=GLOB_FLAGS,
+                root_dir=docs_dir,
+        ):
+            if path not in settings_ignore_paths:
+                settings_ignore_paths.append(path)
+        if page_src_path in settings_ignore_paths:
+            return markdown
+
     def found_include_tag(  # noqa: PLR0912, PLR0915
             match: re.Match[str],
     ) -> str:
@@ -125,19 +141,7 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         arguments_string = match['arguments']
 
         exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
-        ignore_paths = []
-        if settings.exclude:
-            ignore_paths.extend(
-                glob.glob(
-                    [
-                        os.path.join(docs_dir, fp)
-                        if not os.path.isabs(fp)
-                        else fp for fp in settings.exclude
-                    ],
-                    flags=GLOB_FLAGS,
-                    root_dir=docs_dir,
-                ),
-            )
+        ignore_paths = [*settings_ignore_paths]
         if exclude_match is not None:
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
@@ -151,14 +155,13 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                     f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
                 )
 
-            ignore_paths.extend(
-                resolve_file_paths_to_exclude(
+            for path in resolve_file_paths_to_exclude(
                     exclude_string,
                     page_src_path,
                     docs_dir,
-                ),
-            )
-        ignore_paths = list(set(ignore_paths))
+            ):
+                if path not in ignore_paths:
+                    ignore_paths.append(path)
 
         file_paths_to_include, is_url = resolve_file_paths_to_include(
             filename,
@@ -181,7 +184,8 @@ def get_file_content(  # noqa: PLR0913, PLR0915
             files_watcher.included_files.extend(file_paths_to_include)
 
         bool_options, invalid_bool_args = parse_bool_options(
-            ['preserve-includer-indent', 'dedent', 'trailing-newlines'],
+            ['preserve-includer-indent', 'dedent',
+                'trailing-newlines', 'recursive'],
             defaults,
             arguments_string,
         )
@@ -264,16 +268,17 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                         expected_but_any_found[i] = False
 
             # nested includes
-            new_text_to_include = get_file_content(
-                new_text_to_include,
-                file_path,
-                docs_dir,
-                tags,
-                defaults,
-                settings,
-                files_watcher=files_watcher,
-                http_cache=http_cache,
-            )
+            if bool_options['recursive'].value:
+                new_text_to_include = get_file_content(
+                    new_text_to_include,
+                    file_path,
+                    docs_dir,
+                    tags,
+                    defaults,
+                    settings,
+                    files_watcher=files_watcher,
+                    http_cache=http_cache,
+                )
 
             # trailing newlines right stripping
             if not bool_options['trailing-newlines'].value:
@@ -349,19 +354,7 @@ def get_file_content(  # noqa: PLR0913, PLR0915
         arguments_string = match['arguments']
 
         exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
-        ignore_paths = []
-        if settings.exclude is not None:
-            ignore_paths.extend(
-                glob.glob(
-                    [
-                        os.path.join(docs_dir, fp)
-                        if not os.path.isabs(fp)
-                        else fp for fp in settings.exclude
-                    ],
-                    flags=GLOB_FLAGS,
-                    root_dir=docs_dir,
-                ),
-            )
+        ignore_paths = [*settings_ignore_paths]
         if exclude_match is not None:
             exclude_string = parse_string_argument(exclude_match)
             if exclude_string is None:
@@ -374,14 +367,13 @@ def get_file_content(  # noqa: PLR0913, PLR0915
                     f' directive at'
                     f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
                 )
-            ignore_paths.extend(
-                resolve_file_paths_to_exclude(
+            for path in resolve_file_paths_to_exclude(
                     exclude_string,
                     page_src_path,
                     docs_dir,
-                ),
-            )
-        ignore_paths = list(set(ignore_paths))
+            ):
+                if path not in ignore_paths:
+                    ignore_paths.append(path)
 
         file_paths_to_include, is_url = resolve_file_paths_to_include(
             filename,
@@ -667,6 +659,7 @@ def on_page_markdown(
             'comments': config.comments,
             'rewrite-relative-urls': config.rewrite_relative_urls,
             'heading-offset': config.heading_offset,
+            'recursive': config.recursive,
             'start': config.start,
             'end': config.end,
         },


Test patch:
diff --git a/tests/test_unit/test_nested_includes.py b/tests/test_unit/test_nested_includes.py
index c2689c4..bd77777 100644
--- a/tests/test_unit/test_nested_includes.py
+++ b/tests/test_unit/test_nested_includes.py
@@ -226,6 +226,32 @@ Included content
             ],
             id='start-end-not-found (second-level)',
         ),
+        # recursive inclusion disabled with `include` directive
+        pytest.param(
+            '''# Header
+
+{%
+  include "{filepath}"
+  comments=false
+  recursive=false
+%}''',
+            '''# Header 2
+
+{% include "{filepath}" %}
+''',
+            '''# Header 3
+
+This content must not be included.
+''',
+            '''# Header
+
+# Header 2
+
+{% include "{filepath}" %}
+''',
+            [],
+            id='include-recursive=false',
+        ),
     ),
 )
 def test_nested_include(
@@ -249,6 +275,9 @@ def test_nested_include(
     second_includer_content = second_includer_content.replace(
         '{filepath}', included_file.as_posix(),
     )
+    expected_result = expected_result.replace(
+        '{filepath}', included_file.as_posix(),
+    )
 
     first_includer_file.write_text(first_includer_content)
     second_includer_file.write_text(second_includer_content)