mkdocs-include-markdown-plugin 6.1.1__tar.gz → 6.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/PKG-INFO +34 -27
  2. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/README.md +33 -26
  3. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/pyproject.toml +1 -1
  4. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/directive.py +36 -3
  5. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/event.py +143 -167
  6. mkdocs_include_markdown_plugin-6.2.0/src/mkdocs_include_markdown_plugin/logger.py +10 -0
  7. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/plugin.py +2 -2
  8. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/process.py +37 -0
  9. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/.gitignore +0 -0
  10. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/LICENSE +0 -0
  11. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/__init__.py +0 -0
  12. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/cache.py +0 -0
  13. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/config.py +0 -0
  14. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/files_watcher.py +0 -0
  15. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.0}/src/mkdocs_include_markdown_plugin/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mkdocs-include-markdown-plugin
3
- Version: 6.1.1
3
+ Version: 6.2.0
4
4
  Summary: Mkdocs Markdown includer plugin.
5
5
  Project-URL: Source, https://github.com/mondeja/mkdocs-include-markdown-plugin
6
6
  Project-URL: Documentation, https://github.com/mondeja/mkdocs-include-markdown-plugin#documentation
@@ -190,6 +190,9 @@ content to include.
190
190
  - <a name="include-markdown_trailing-newlines" href="#include-markdown_trailing-newlines">#</a>
191
191
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
192
192
  found in the content to include are stripped. Possible values are `true` and `false`.
193
+ - <a name="include-markdown_recursive" href="#include-markdown_recursive">#</a>
194
+ **recursive** (_true_): When this option is disabled, included files are not
195
+ processed for recursive includes. Possible values are `true` and `false`.
193
196
  - <a name="include-markdown_encoding" href="#include-markdown_encoding">#</a>
194
197
  **encoding** (_utf-8_): Specify the encoding of the included file.
195
198
  If not defined `utf-8` will be used.
@@ -212,42 +215,42 @@ content to include.
212
215
 
213
216
  ```jinja
214
217
  {%
215
- include-markdown "../README.md"
216
- start="<!--intro-start-->"
217
- end="<!--intro-end-->"
218
+ include-markdown "../README.md"
219
+ start="<!--intro-start-->"
220
+ end="<!--intro-end-->"
218
221
  %}
219
222
  ```
220
223
 
221
224
  ```jinja
222
225
  {%
223
- include-markdown 'includes/header.md'
224
- start='<!--\n\ttable-start\n-->'
225
- end='<!--\n\ttable-end\n-->'
226
- rewrite-relative-urls=false
227
- comments=false
226
+ include-markdown 'includes/header.md'
227
+ start='<!--\n\ttable-start\n-->'
228
+ end='<!--\n\ttable-end\n-->'
229
+ rewrite-relative-urls=false
230
+ comments=false
228
231
  %}
229
232
  ```
230
233
 
231
234
  ```jinja
232
235
  {%
233
- include-markdown "includes/header.md"
234
- heading-offset=1
236
+ include-markdown "includes/header.md"
237
+ heading-offset=1
235
238
  %}
236
239
  ```
237
240
 
238
241
  ```jinja
239
242
  {%
240
- include-markdown "../LICENSE*"
241
- start="<!--license \"start\" -->"
242
- end='<!--license "end" -->'
243
- exclude="../*.rst"
243
+ include-markdown "../LICENSE*"
244
+ start="<!--license \"start\" -->"
245
+ end='<!--license "end" -->'
246
+ exclude="../*.rst"
244
247
  %}
245
248
  ```
246
249
 
247
250
  ```jinja
248
251
  {%
249
- include-markdown "**"
250
- exclude="./{index,LICENSE}.md"
252
+ include-markdown "**"
253
+ exclude="./{index,LICENSE}.md"
251
254
  %}
252
255
  ```
253
256
 
@@ -278,12 +281,12 @@ Includes the content of a file or a group of files.
278
281
  - <a name="include_trailing-newlines" href="#include_trailing-newlines">#</a>
279
282
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
280
283
  found in the content to include are stripped. Possible values are `true` and `false`.
284
+ - <a name="include_recursive" href="#include_recursive">#</a>
285
+ **recursive** (_true_): When this option is disabled, included files are not
286
+ processed for recursive includes. Possible values are `true` and `false`.
281
287
  - <a name="include_encoding" href="#include_encoding">#</a>
282
288
  **encoding** (_utf-8_): Specify the encoding of the included file.
283
289
  If not defined `utf-8` will be used.
284
- - <a name="recursive" href="#include_recursive">#</a>
285
- **recursive** (_true_): When this option is disabled, included files are not
286
- processed for recursive includes. Possible values are `true` and `false`.
287
290
 
288
291
  ##### Examples
289
292
 
@@ -295,23 +298,24 @@ Includes the content of a file or a group of files.
295
298
 
296
299
  ```jinja
297
300
  {%
298
- include "../examples.md"
299
- start="~~~yaml"
300
- end="~~~\n"
301
+ include "../examples.md"
302
+ start="~~~yaml"
303
+ end="~~~\n"
301
304
  %}
302
305
  ```
303
306
 
304
307
  ```jinja
305
308
  {%
306
- include '**'
307
- exclude='./*.md'
309
+ include '**'
310
+ exclude='./*.md'
308
311
  %}
309
312
  ```
310
313
 
311
314
  ## Acknowledgment
312
315
 
313
- - Joe Rickerby and contributors for
314
- [giving me the permissions][cibuildwheel-470] to separate this plugin from the
316
+ - [Joe Rickerby] and [contributors] for
317
+ [giving me the permissions][cibuildwheel-470] to
318
+ [separate this plugin][cibuildwheel-475] from the
315
319
  documentation of [cibuildwheel][cibuildwheel-repo-link].
316
320
 
317
321
  [Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax
@@ -328,7 +332,10 @@ Includes the content of a file or a group of files.
328
332
  [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE
329
333
  [platformdirs]: https://pypi.org/project/platformdirs/
330
334
  [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470
335
+ [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475
331
336
  [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel
332
337
  [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md
333
338
  [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md
334
339
  [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir
340
+ [Joe Rickerby]: https://github.com/joerick
341
+ [contributors]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors
@@ -155,6 +155,9 @@ content to include.
155
155
  - <a name="include-markdown_trailing-newlines" href="#include-markdown_trailing-newlines">#</a>
156
156
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
157
157
  found in the content to include are stripped. Possible values are `true` and `false`.
158
+ - <a name="include-markdown_recursive" href="#include-markdown_recursive">#</a>
159
+ **recursive** (_true_): When this option is disabled, included files are not
160
+ processed for recursive includes. Possible values are `true` and `false`.
158
161
  - <a name="include-markdown_encoding" href="#include-markdown_encoding">#</a>
159
162
  **encoding** (_utf-8_): Specify the encoding of the included file.
160
163
  If not defined `utf-8` will be used.
@@ -177,42 +180,42 @@ content to include.
177
180
 
178
181
  ```jinja
179
182
  {%
180
- include-markdown "../README.md"
181
- start="<!--intro-start-->"
182
- end="<!--intro-end-->"
183
+ include-markdown "../README.md"
184
+ start="<!--intro-start-->"
185
+ end="<!--intro-end-->"
183
186
  %}
184
187
  ```
185
188
 
186
189
  ```jinja
187
190
  {%
188
- include-markdown 'includes/header.md'
189
- start='<!--\n\ttable-start\n-->'
190
- end='<!--\n\ttable-end\n-->'
191
- rewrite-relative-urls=false
192
- comments=false
191
+ include-markdown 'includes/header.md'
192
+ start='<!--\n\ttable-start\n-->'
193
+ end='<!--\n\ttable-end\n-->'
194
+ rewrite-relative-urls=false
195
+ comments=false
193
196
  %}
194
197
  ```
195
198
 
196
199
  ```jinja
197
200
  {%
198
- include-markdown "includes/header.md"
199
- heading-offset=1
201
+ include-markdown "includes/header.md"
202
+ heading-offset=1
200
203
  %}
201
204
  ```
202
205
 
203
206
  ```jinja
204
207
  {%
205
- include-markdown "../LICENSE*"
206
- start="<!--license \"start\" -->"
207
- end='<!--license "end" -->'
208
- exclude="../*.rst"
208
+ include-markdown "../LICENSE*"
209
+ start="<!--license \"start\" -->"
210
+ end='<!--license "end" -->'
211
+ exclude="../*.rst"
209
212
  %}
210
213
  ```
211
214
 
212
215
  ```jinja
213
216
  {%
214
- include-markdown "**"
215
- exclude="./{index,LICENSE}.md"
217
+ include-markdown "**"
218
+ exclude="./{index,LICENSE}.md"
216
219
  %}
217
220
  ```
218
221
 
@@ -243,12 +246,12 @@ Includes the content of a file or a group of files.
243
246
  - <a name="include_trailing-newlines" href="#include_trailing-newlines">#</a>
244
247
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
245
248
  found in the content to include are stripped. Possible values are `true` and `false`.
249
+ - <a name="include_recursive" href="#include_recursive">#</a>
250
+ **recursive** (_true_): When this option is disabled, included files are not
251
+ processed for recursive includes. Possible values are `true` and `false`.
246
252
  - <a name="include_encoding" href="#include_encoding">#</a>
247
253
  **encoding** (_utf-8_): Specify the encoding of the included file.
248
254
  If not defined `utf-8` will be used.
249
- - <a name="recursive" href="#include_recursive">#</a>
250
- **recursive** (_true_): When this option is disabled, included files are not
251
- processed for recursive includes. Possible values are `true` and `false`.
252
255
 
253
256
  ##### Examples
254
257
 
@@ -260,23 +263,24 @@ Includes the content of a file or a group of files.
260
263
 
261
264
  ```jinja
262
265
  {%
263
- include "../examples.md"
264
- start="~~~yaml"
265
- end="~~~\n"
266
+ include "../examples.md"
267
+ start="~~~yaml"
268
+ end="~~~\n"
266
269
  %}
267
270
  ```
268
271
 
269
272
  ```jinja
270
273
  {%
271
- include '**'
272
- exclude='./*.md'
274
+ include '**'
275
+ exclude='./*.md'
273
276
  %}
274
277
  ```
275
278
 
276
279
  ## Acknowledgment
277
280
 
278
- - Joe Rickerby and contributors for
279
- [giving me the permissions][cibuildwheel-470] to separate this plugin from the
281
+ - [Joe Rickerby] and [contributors] for
282
+ [giving me the permissions][cibuildwheel-470] to
283
+ [separate this plugin][cibuildwheel-475] from the
280
284
  documentation of [cibuildwheel][cibuildwheel-repo-link].
281
285
 
282
286
  [Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax
@@ -293,7 +297,10 @@ Includes the content of a file or a group of files.
293
297
  [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE
294
298
  [platformdirs]: https://pypi.org/project/platformdirs/
295
299
  [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470
300
+ [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475
296
301
  [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel
297
302
  [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md
298
303
  [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md
299
304
  [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir
305
+ [Joe Rickerby]: https://github.com/joerick
306
+ [contributors]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs-include-markdown-plugin"
3
- version = "6.1.1"
3
+ version = "6.2.0"
4
4
  description = "Mkdocs Markdown includer plugin."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -12,6 +12,7 @@ from mkdocs.exceptions import PluginError
12
12
  from wcmatch import glob
13
13
 
14
14
  from mkdocs_include_markdown_plugin import process
15
+ from mkdocs_include_markdown_plugin.logger import logger
15
16
 
16
17
 
17
18
  @dataclass
@@ -21,7 +22,7 @@ class DirectiveBoolArgument: # noqa: D101
21
22
 
22
23
 
23
24
  if TYPE_CHECKING:
24
- from typing import TypedDict
25
+ from typing import Literal, TypedDict
25
26
 
26
27
  DirectiveBoolArgumentsDict = dict[str, DirectiveBoolArgument]
27
28
 
@@ -44,8 +45,8 @@ if TYPE_CHECKING:
44
45
  GLOB_FLAGS = glob.NEGATE | glob.EXTGLOB | glob.GLOBSTAR | glob.BRACE
45
46
  RE_ESCAPED_PUNCTUATION = re.escape(string.punctuation)
46
47
 
47
- DOUBLE_QUOTED_STR_RE = r'([^"]|(?<=\\)["])+'
48
- SINGLE_QUOTED_STR_RE = r"([^']|(?<=\\)['])+"
48
+ DOUBLE_QUOTED_STR_RE = r'([^"]|(?<=\\)")+'
49
+ SINGLE_QUOTED_STR_RE = r"([^']|(?<=\\)')+"
49
50
 
50
51
  # In the following regular expression, the substrings "$OPENING_TAG"
51
52
  # and "$CLOSING_TAG" will be replaced by the effective opening and
@@ -103,6 +104,38 @@ ARGUMENT_REGEXES = {
103
104
  'heading-offset': arg('heading-offset'),
104
105
  }
105
106
 
107
+ INCLUDE_DIRECTIVE_ARGS = {
108
+ key for key in ARGUMENT_REGEXES if key not in (
109
+ 'rewrite-relative-urls', 'heading-offset', 'comments',
110
+ )
111
+ }
112
+
113
+ INCLUDE_MARKDOWN_DIRECTIVE_ARGS = set(ARGUMENT_REGEXES)
114
+
115
+
116
+ def warn_invalid_directive_arguments(
117
+ arguments_string: str,
118
+ directive_lineno: int,
119
+ directive: Literal['include', 'include-markdown'],
120
+ page_src_path: str | None,
121
+ docs_dir: str,
122
+ ) -> None:
123
+ """Warns about the invalid arguments passed to a directive."""
124
+ arg_re = re.compile(rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*')
125
+ valid_args = (
126
+ INCLUDE_DIRECTIVE_ARGS if directive == 'include'
127
+ else INCLUDE_MARKDOWN_DIRECTIVE_ARGS
128
+ )
129
+ for arg_value in re.findall(arg_re, arguments_string):
130
+ if arg_value.split('=', 1)[0] not in valid_args:
131
+ location = process.file_lineno_message(
132
+ page_src_path, docs_dir, directive_lineno,
133
+ )
134
+ logger.warning(
135
+ f"Invalid argument '{arg_value}' in"
136
+ f" '{directive}' directive at {location}. Ignoring...",
137
+ )
138
+
106
139
 
107
140
  def parse_filename_argument(
108
141
  match: re.Match[str],
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import html
6
- import logging
7
6
  import os
8
7
  import re
9
8
  import textwrap
@@ -23,12 +22,14 @@ from mkdocs_include_markdown_plugin.directive import (
23
22
  parse_string_argument,
24
23
  resolve_file_paths_to_exclude,
25
24
  resolve_file_paths_to_include,
25
+ warn_invalid_directive_arguments,
26
26
  )
27
27
  from mkdocs_include_markdown_plugin.files_watcher import FilesWatcher
28
+ from mkdocs_include_markdown_plugin.logger import logger
28
29
 
29
30
 
30
31
  if TYPE_CHECKING:
31
- from typing import TypedDict
32
+ from typing import Literal, TypedDict
32
33
 
33
34
  from mkdocs.structure.pages import Page
34
35
 
@@ -43,49 +44,26 @@ if TYPE_CHECKING:
43
44
  )
44
45
 
45
46
 
46
- @dataclass
47
- class Settings: # noqa: D101
48
- exclude: list[str] | None
49
-
50
-
51
- # TODO: when Mkdocs < 1.5.0 support is dropped, use
52
- # mkdocs.plugin.get_plugin_logger
53
- logger = logging.getLogger('mkdocs.plugins.include_markdown')
54
-
47
+ # Placeholders (taken from Python-Markdown)
48
+ STX = '\u0002'
49
+ ''' "Start of Text" marker for placeholder templates. '''
50
+ ETX = '\u0003'
51
+ ''' "End of Text" marker for placeholder templates. '''
52
+ INLINE_PLACEHOLDER_PREFIX = f'{STX}klzzwxh:'
55
53
 
56
- def lineno_from_content_start(content: str, start: int) -> int:
57
- """Return the line number of the first line of ``start`` in ``content``."""
58
- return content[:start].count('\n') + 1
59
54
 
60
-
61
- def safe_os_path_relpath(path: str, start: str) -> str:
62
- """Return the relative path of a file from a start directory.
63
-
64
- Safe version of `os.path.relpath` that catches `ValueError` exceptions
65
- on Windows and returns the original path in case of error.
66
- On Windows, `ValueError` is raised when `path` and `start` are on
67
- different drives.
68
- """
69
- if os.name != 'nt': # pragma: nt no cover
70
- return os.path.relpath(path, start)
71
- try: # pragma: nt cover
72
- return os.path.relpath(path, start)
73
- except ValueError: # pragma: no cover
74
- return path
55
+ def build_placeholder(
56
+ num: int,
57
+ directive: Literal['include', 'include-markdown'],
58
+ ) -> str:
59
+ """Return a placeholder."""
60
+ directive_prefix = 'im' if directive == 'include-markdown' else 'i'
61
+ return f'{INLINE_PLACEHOLDER_PREFIX}{directive_prefix}{num}{ETX}'
75
62
 
76
63
 
77
- def file_lineno_message(
78
- page_src_path: str | None,
79
- docs_dir: str,
80
- lineno: int,
81
- ) -> str:
82
- """Return a message with the file path and line number."""
83
- if page_src_path is None: # pragma: no cover
84
- return f'generated page content (line {lineno})'
85
- return (
86
- f'{safe_os_path_relpath(page_src_path, docs_dir)}'
87
- f':{lineno}'
88
- )
64
+ @dataclass
65
+ class Settings: # noqa: D101
66
+ exclude: list[str] | None
89
67
 
90
68
 
91
69
  def get_file_content( # noqa: PLR0913, PLR0915
@@ -119,46 +97,55 @@ def get_file_content( # noqa: PLR0913, PLR0915
119
97
  if page_src_path in settings_ignore_paths:
120
98
  return markdown
121
99
 
100
+ new_found_include_contents: list[tuple[str, str]] = []
101
+ new_found_include_markdown_contents: list[tuple[str, str]] = []
102
+
122
103
  def found_include_tag( # noqa: PLR0912, PLR0915
123
104
  match: re.Match[str],
124
105
  ) -> str:
125
106
  directive_match_start = match.start()
107
+ directive_lineno = process.lineno_from_content_start(
108
+ markdown,
109
+ directive_match_start,
110
+ )
126
111
 
127
112
  includer_indent = match['_includer_indent']
128
113
 
129
114
  filename, raw_filename = parse_filename_argument(match)
130
115
  if filename is None:
131
- lineno = lineno_from_content_start(
132
- markdown,
133
- directive_match_start,
116
+ location = process.file_lineno_message(
117
+ page_src_path, docs_dir, directive_lineno,
134
118
  )
135
119
  raise PluginError(
136
120
  "Found no path passed including with 'include'"
137
- ' directive at'
138
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
121
+ f' directive at {location}',
139
122
  )
140
123
 
141
124
  arguments_string = match['arguments']
142
125
 
126
+ warn_invalid_directive_arguments(
127
+ arguments_string,
128
+ directive_lineno,
129
+ 'include',
130
+ page_src_path,
131
+ docs_dir,
132
+ )
133
+
143
134
  exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
144
135
  ignore_paths = [*settings_ignore_paths]
145
136
  if exclude_match is not None:
146
137
  exclude_string = parse_string_argument(exclude_match)
147
138
  if exclude_string is None:
148
- lineno = lineno_from_content_start(
149
- markdown,
150
- directive_match_start,
139
+ location = process.file_lineno_message(
140
+ page_src_path, docs_dir, directive_lineno,
151
141
  )
152
142
  raise PluginError(
153
143
  "Invalid empty 'exclude' argument in 'include'"
154
- ' directive at'
155
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
144
+ f' directive at {location}',
156
145
  )
157
146
 
158
147
  for path in resolve_file_paths_to_exclude(
159
- exclude_string,
160
- page_src_path,
161
- docs_dir,
148
+ exclude_string, page_src_path, docs_dir,
162
149
  ):
163
150
  if path not in ignore_paths:
164
151
  ignore_paths.append(path)
@@ -171,13 +158,11 @@ def get_file_content( # noqa: PLR0913, PLR0915
171
158
  )
172
159
 
173
160
  if not file_paths_to_include:
174
- lineno = lineno_from_content_start(
175
- markdown,
176
- directive_match_start,
161
+ location = process.file_lineno_message(
162
+ page_src_path, docs_dir, directive_lineno,
177
163
  )
178
164
  raise PluginError(
179
- f"No files found including '{raw_filename}' at"
180
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
165
+ f"No files found including '{raw_filename}' at {location}",
181
166
  )
182
167
 
183
168
  if files_watcher is not None and not is_url:
@@ -190,14 +175,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
190
175
  arguments_string,
191
176
  )
192
177
  if invalid_bool_args:
193
- lineno = lineno_from_content_start(
194
- markdown,
195
- directive_match_start,
178
+ location = process.file_lineno_message(
179
+ page_src_path, docs_dir, directive_lineno,
196
180
  )
197
181
  raise PluginError(
198
182
  f"Invalid value for '{invalid_bool_args[0]}' argument of"
199
- " 'include' directive at"
200
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}.'
183
+ f" 'include' directive at {location}."
201
184
  f' Possible values are true or false.',
202
185
  )
203
186
 
@@ -205,13 +188,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
205
188
  if start_match:
206
189
  start = parse_string_argument(start_match)
207
190
  if start is None:
208
- lineno = lineno_from_content_start(
209
- markdown,
210
- directive_match_start,
191
+ location = process.file_lineno_message(
192
+ page_src_path, docs_dir, directive_lineno,
211
193
  )
212
194
  raise PluginError(
213
195
  "Invalid empty 'start' argument in 'include' directive at"
214
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
196
+ f' {location}',
215
197
  )
216
198
  else:
217
199
  start = defaults['start']
@@ -220,13 +202,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
220
202
  if end_match:
221
203
  end = parse_string_argument(end_match)
222
204
  if end is None:
223
- lineno = lineno_from_content_start(
224
- markdown,
225
- directive_match_start,
205
+ location = process.file_lineno_message(
206
+ page_src_path, docs_dir, directive_lineno,
226
207
  )
227
208
  raise PluginError(
228
209
  "Invalid empty 'end' argument in 'include' directive at"
229
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
210
+ f' {location}',
230
211
  )
231
212
  else:
232
213
  end = defaults['end']
@@ -235,14 +216,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
235
216
  if encoding_match:
236
217
  encoding = parse_string_argument(encoding_match)
237
218
  if encoding is None:
238
- lineno = lineno_from_content_start(
239
- markdown,
240
- directive_match_start,
219
+ location = process.file_lineno_message(
220
+ page_src_path, docs_dir, directive_lineno,
241
221
  )
242
222
  raise PluginError(
243
223
  "Invalid empty 'encoding' argument in 'include'"
244
- ' directive at'
245
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
224
+ f' directive at {location}',
246
225
  )
247
226
  else:
248
227
  encoding = defaults['encoding']
@@ -308,69 +287,72 @@ def get_file_content( # noqa: PLR0913, PLR0915
308
287
  if expected_but_any_found[i]:
309
288
  delimiter_value = locals()[delimiter_name]
310
289
  readable_files_to_include = ', '.join([
311
- safe_os_path_relpath(fpath, docs_dir)
290
+ process.safe_os_path_relpath(fpath, docs_dir)
312
291
  for fpath in file_paths_to_include
313
292
  ])
314
293
  plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
315
- lineno = lineno_from_content_start(
316
- markdown,
317
- directive_match_start,
318
- )
319
- file_lineno = file_lineno_message(
320
- page_src_path, docs_dir, lineno,
294
+ location = process.file_lineno_message(
295
+ page_src_path, docs_dir, directive_lineno,
321
296
  )
322
297
  logger.warning(
323
- (
324
- f"Delimiter {delimiter_name} '{delimiter_value}'"
325
- " of 'include' directive at"
326
- f' {file_lineno}'
327
- f' not detected in the file{plural_suffix}'
328
- f' {readable_files_to_include}'
329
- ),
298
+ f"Delimiter {delimiter_name} '{delimiter_value}'"
299
+ f" of 'include' directive at {location}"
300
+ f' not detected in the file{plural_suffix}'
301
+ f' {readable_files_to_include}',
330
302
  )
331
303
 
332
- return text_to_include
304
+ nonlocal new_found_include_contents
305
+ include_index = len(new_found_include_contents)
306
+ placeholder = build_placeholder(include_index, 'include')
307
+ new_found_include_contents.append((placeholder, text_to_include))
308
+ return placeholder
333
309
 
334
310
  def found_include_markdown_tag( # noqa: PLR0912, PLR0915
335
311
  match: re.Match[str],
336
312
  ) -> str:
337
313
  directive_match_start = match.start()
314
+ directive_lineno = process.lineno_from_content_start(
315
+ markdown,
316
+ directive_match_start,
317
+ )
338
318
 
339
319
  includer_indent = match['_includer_indent']
340
320
  empty_includer_indent = ' ' * len(includer_indent)
341
321
 
342
322
  filename, raw_filename = parse_filename_argument(match)
343
323
  if filename is None:
344
- lineno = lineno_from_content_start(
345
- markdown,
346
- directive_match_start,
324
+ location = process.file_lineno_message(
325
+ page_src_path, docs_dir, directive_lineno,
347
326
  )
348
327
  raise PluginError(
349
328
  "Found no path passed including with 'include-markdown'"
350
- f' directive at'
351
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
329
+ f' directive at {location}',
352
330
  )
353
331
 
354
332
  arguments_string = match['arguments']
355
333
 
334
+ warn_invalid_directive_arguments(
335
+ arguments_string,
336
+ directive_lineno,
337
+ 'include-markdown',
338
+ page_src_path,
339
+ docs_dir,
340
+ )
341
+
356
342
  exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
357
343
  ignore_paths = [*settings_ignore_paths]
358
344
  if exclude_match is not None:
359
345
  exclude_string = parse_string_argument(exclude_match)
360
346
  if exclude_string is None:
361
- lineno = lineno_from_content_start(
362
- markdown,
363
- directive_match_start,
347
+ location = process.file_lineno_message(
348
+ page_src_path, docs_dir, directive_lineno,
364
349
  )
365
350
  raise PluginError(
366
351
  "Invalid empty 'exclude' argument in 'include-markdown'"
367
- f' directive at'
368
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
352
+ f' directive at {location}',
369
353
  )
370
354
  for path in resolve_file_paths_to_exclude(
371
- exclude_string,
372
- page_src_path,
373
- docs_dir,
355
+ exclude_string, page_src_path, docs_dir,
374
356
  ):
375
357
  if path not in ignore_paths:
376
358
  ignore_paths.append(path)
@@ -383,13 +365,11 @@ def get_file_content( # noqa: PLR0913, PLR0915
383
365
  )
384
366
 
385
367
  if not file_paths_to_include:
386
- lineno = lineno_from_content_start(
387
- markdown,
388
- directive_match_start,
368
+ location = process.file_lineno_message(
369
+ page_src_path, docs_dir, directive_lineno,
389
370
  )
390
371
  raise PluginError(
391
- f"No files found including '{raw_filename}' at"
392
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
372
+ f"No files found including '{raw_filename}' at {location}",
393
373
  )
394
374
 
395
375
  if files_watcher is not None and not is_url:
@@ -399,21 +379,19 @@ def get_file_content( # noqa: PLR0913, PLR0915
399
379
  [
400
380
  'rewrite-relative-urls', 'comments',
401
381
  'preserve-includer-indent', 'dedent',
402
- 'trailing-newlines',
382
+ 'trailing-newlines', 'recursive',
403
383
  ],
404
384
  defaults,
405
385
  arguments_string,
406
386
  )
407
387
  if invalid_bool_args:
408
- lineno = lineno_from_content_start(
409
- markdown,
410
- directive_match_start,
388
+ location = process.file_lineno_message(
389
+ page_src_path, docs_dir, directive_lineno,
411
390
  )
412
391
  raise PluginError(
413
392
  f"Invalid value for '{invalid_bool_args[0]}' argument of"
414
393
  " 'include-markdown' directive at"
415
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}.'
416
- f' Possible values are true or false.',
394
+ f' {location}. Possible values are true or false.',
417
395
  )
418
396
 
419
397
  # start and end arguments
@@ -421,14 +399,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
421
399
  if start_match:
422
400
  start = parse_string_argument(start_match)
423
401
  if start is None:
424
- lineno = lineno_from_content_start(
425
- markdown,
426
- directive_match_start,
402
+ location = process.file_lineno_message(
403
+ page_src_path, docs_dir, directive_lineno,
427
404
  )
428
405
  raise PluginError(
429
406
  "Invalid empty 'start' argument in 'include-markdown'"
430
- f' directive at'
431
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
407
+ f' directive at {location}',
432
408
  )
433
409
  else:
434
410
  start = defaults['start']
@@ -437,14 +413,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
437
413
  if end_match:
438
414
  end = parse_string_argument(end_match)
439
415
  if end is None:
440
- lineno = lineno_from_content_start(
441
- markdown,
442
- directive_match_start,
416
+ location = process.file_lineno_message(
417
+ page_src_path, docs_dir, directive_lineno,
443
418
  )
444
419
  raise PluginError(
445
420
  "Invalid empty 'end' argument in 'include-markdown'"
446
- f' directive at'
447
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
421
+ f' directive at {location}',
448
422
  )
449
423
  else:
450
424
  end = defaults['end']
@@ -453,14 +427,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
453
427
  if encoding_match:
454
428
  encoding = parse_string_argument(encoding_match)
455
429
  if encoding is None:
456
- lineno = lineno_from_content_start(
457
- markdown,
458
- directive_match_start,
430
+ location = process.file_lineno_message(
431
+ page_src_path, docs_dir, directive_lineno,
459
432
  )
460
433
  raise PluginError(
461
434
  "Invalid empty 'encoding' argument in 'include-markdown'"
462
- ' directive at'
463
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
435
+ f' directive at {location}',
464
436
  )
465
437
  else:
466
438
  encoding = defaults['encoding']
@@ -472,26 +444,22 @@ def get_file_content( # noqa: PLR0913, PLR0915
472
444
  if offset_match:
473
445
  offset_raw_value = offset_match[1]
474
446
  if offset_raw_value == '':
475
- lineno = lineno_from_content_start(
476
- markdown,
477
- directive_match_start,
447
+ location = process.file_lineno_message(
448
+ page_src_path, docs_dir, directive_lineno,
478
449
  )
479
450
  raise PluginError(
480
451
  "Invalid empty 'heading-offset' argument in"
481
- " 'include-markdown' directive at"
482
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
452
+ f" 'include-markdown' directive at {location}",
483
453
  )
484
454
  try:
485
455
  offset = int(offset_raw_value)
486
456
  except ValueError:
487
- lineno = lineno_from_content_start(
488
- markdown,
489
- directive_match_start,
457
+ location = process.file_lineno_message(
458
+ page_src_path, docs_dir, directive_lineno,
490
459
  )
491
460
  raise PluginError(
492
461
  f"Invalid 'heading-offset' argument \"{offset_raw_value}\""
493
- " in 'include-markdown' directive at"
494
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
462
+ f" in 'include-markdown' directive at {location}",
495
463
  ) from None
496
464
  else:
497
465
  offset = defaults['heading-offset']
@@ -530,16 +498,17 @@ def get_file_content( # noqa: PLR0913, PLR0915
530
498
  expected_but_any_found[i] = False
531
499
 
532
500
  # nested includes
533
- new_text_to_include = get_file_content(
534
- new_text_to_include,
535
- file_path,
536
- docs_dir,
537
- tags,
538
- defaults,
539
- settings,
540
- files_watcher=files_watcher,
541
- http_cache=http_cache,
542
- )
501
+ if bool_options['recursive'].value:
502
+ new_text_to_include = get_file_content(
503
+ new_text_to_include,
504
+ file_path,
505
+ docs_dir,
506
+ tags,
507
+ defaults,
508
+ settings,
509
+ files_watcher=files_watcher,
510
+ http_cache=http_cache,
511
+ )
543
512
 
544
513
  # trailing newlines right stripping
545
514
  if not bool_options['trailing-newlines'].value:
@@ -601,38 +570,45 @@ def get_file_content( # noqa: PLR0913, PLR0915
601
570
  if expected_but_any_found[i]:
602
571
  delimiter_value = locals()[delimiter_name]
603
572
  readable_files_to_include = ', '.join([
604
- safe_os_path_relpath(fpath, docs_dir)
573
+ process.safe_os_path_relpath(fpath, docs_dir)
605
574
  for fpath in file_paths_to_include
606
575
  ])
607
576
  plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
608
- lineno = lineno_from_content_start(
609
- markdown,
610
- directive_match_start,
611
- )
612
- file_lineno = file_lineno_message(
613
- page_src_path, docs_dir, lineno,
577
+ location = process.file_lineno_message(
578
+ page_src_path, docs_dir, directive_lineno,
614
579
  )
615
580
  logger.warning(
616
- (
617
- f"Delimiter {delimiter_name} '{delimiter_value}' of"
618
- " 'include-markdown' directive at"
619
- f' {file_lineno}'
620
- f' not detected in the file{plural_suffix}'
621
- f' {readable_files_to_include}'
622
- ),
581
+ f"Delimiter {delimiter_name} '{delimiter_value}' of"
582
+ f" 'include-markdown' directive at {location}"
583
+ f' not detected in the file{plural_suffix}'
584
+ f' {readable_files_to_include}',
623
585
  )
624
586
 
625
- return text_to_include
587
+ nonlocal new_found_include_markdown_contents
588
+ markdown_include_index = len(new_found_include_markdown_contents)
589
+ placeholder = build_placeholder(
590
+ markdown_include_index, 'include-markdown')
591
+ new_found_include_markdown_contents.append(
592
+ (placeholder, text_to_include))
593
+ return placeholder
626
594
 
595
+ # Replace contents by placeholders
627
596
  markdown = tags['include-markdown'].sub(
628
597
  found_include_markdown_tag,
629
598
  markdown,
630
599
  )
631
- return tags['include'].sub(
600
+ markdown = tags['include'].sub(
632
601
  found_include_tag,
633
602
  markdown,
634
603
  )
635
604
 
605
+ # Replace placeholders by contents
606
+ for placeholder, text in new_found_include_contents:
607
+ markdown = markdown.replace(placeholder, text, 1)
608
+ for placeholder, text in new_found_include_markdown_contents:
609
+ markdown = markdown.replace(placeholder, text, 1)
610
+ return markdown
611
+
636
612
 
637
613
  def on_page_markdown(
638
614
  markdown: str,
@@ -0,0 +1,10 @@
1
+ """Mkdocs plugin logger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+
8
+ # TODO: when Mkdocs < 1.5.0 support is dropped, use
9
+ # mkdocs.plugin.get_plugin_logger
10
+ logger = logging.getLogger('mkdocs.plugins.include_markdown')
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
6
  from functools import cached_property
7
- from typing import TYPE_CHECKING, Any
7
+ from typing import TYPE_CHECKING
8
8
 
9
9
  from mkdocs.exceptions import PluginError
10
10
  from mkdocs.livereload import LiveReloadServer
@@ -98,7 +98,7 @@ class IncludeMarkdownPlugin(BasePlugin[PluginConfig]):
98
98
  self,
99
99
  server: LiveReloadServer,
100
100
  config: MkDocsConfig, # noqa: ARG002
101
- builder: Callable[[Any], Any], # noqa: ARG002
101
+ builder: Callable, # noqa: ARG002
102
102
  ) -> None:
103
103
  if self._server is None: # pragma: no cover
104
104
  self._server = server
@@ -13,6 +13,8 @@ from urllib.request import Request, urlopen
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
+ from typing import Any
17
+
16
18
  from mkdocs_include_markdown_plugin.cache import Cache
17
19
 
18
20
 
@@ -452,3 +454,38 @@ def read_url(url: str, http_cache: Cache | None) -> Any:
452
454
  if http_cache is not None:
453
455
  http_cache.set_(url, content)
454
456
  return content
457
+
458
+
459
+ def safe_os_path_relpath(path: str, start: str) -> str:
460
+ """Return the relative path of a file from a start directory.
461
+
462
+ Safe version of `os.path.relpath` that catches `ValueError` exceptions
463
+ on Windows and returns the original path in case of error.
464
+ On Windows, `ValueError` is raised when `path` and `start` are on
465
+ different drives.
466
+ """
467
+ if os.name != 'nt': # pragma: nt no cover
468
+ return os.path.relpath(path, start)
469
+ try: # pragma: nt cover
470
+ return os.path.relpath(path, start)
471
+ except ValueError: # pragma: no cover
472
+ return path
473
+
474
+
475
+ def file_lineno_message(
476
+ page_src_path: str | None,
477
+ docs_dir: str,
478
+ lineno: int,
479
+ ) -> str:
480
+ """Return a message with the file path and line number."""
481
+ if page_src_path is None: # pragma: no cover
482
+ return f'generated page content (line {lineno})'
483
+ return (
484
+ f'{safe_os_path_relpath(page_src_path, docs_dir)}'
485
+ f':{lineno}'
486
+ )
487
+
488
+
489
+ def lineno_from_content_start(content: str, start: int) -> int:
490
+ """Return the line number of the first line of ``start`` in ``content``."""
491
+ return content[:start].count('\n') + 1