mkdocs-include-markdown-plugin 6.1.1__tar.gz → 6.2.1__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.1}/PKG-INFO +40 -30
  2. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/README.md +39 -29
  3. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/pyproject.toml +1 -1
  4. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/cache.py +6 -6
  5. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/directive.py +38 -5
  6. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/event.py +151 -169
  7. mkdocs_include_markdown_plugin-6.2.1/src/mkdocs_include_markdown_plugin/logger.py +10 -0
  8. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/plugin.py +2 -2
  9. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/process.py +45 -4
  10. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/.gitignore +0 -0
  11. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/LICENSE +0 -0
  12. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/__init__.py +0 -0
  13. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/config.py +0 -0
  14. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/src/mkdocs_include_markdown_plugin/files_watcher.py +0 -0
  15. {mkdocs_include_markdown_plugin-6.1.1 → mkdocs_include_markdown_plugin-6.2.1}/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.1
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
@@ -43,6 +43,7 @@ Description-Content-Type: text/markdown
43
43
  [![License][license-image]][license-link]
44
44
  [![Tests][tests-image]][tests-link]
45
45
  [![Coverage status][coverage-image]][coverage-link]
46
+ [![Downloads][downloads-image]][downloads-link]
46
47
 
47
48
  <!-- mdpo-enable -->
48
49
 
@@ -190,9 +191,12 @@ content to include.
190
191
  - <a name="include-markdown_trailing-newlines" href="#include-markdown_trailing-newlines">#</a>
191
192
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
192
193
  found in the content to include are stripped. Possible values are `true` and `false`.
194
+ - <a name="include-markdown_recursive" href="#include-markdown_recursive">#</a>
195
+ **recursive** (_true_): When this option is disabled, included files are not
196
+ processed for recursive includes. Possible values are `true` and `false`.
193
197
  - <a name="include-markdown_encoding" href="#include-markdown_encoding">#</a>
194
- **encoding** (_utf-8_): Specify the encoding of the included file.
195
- If not defined `utf-8` will be used.
198
+ **encoding** (_'utf-8'_): Specify the encoding of the included file.
199
+ If not defined `'utf-8'` will be used.
196
200
  - <a name="include-markdown_rewrite-relative-urls" href="#include-markdown_rewrite-relative-urls">#</a>
197
201
  **rewrite-relative-urls** (_true_): When this option is enabled (default),
198
202
  Markdown links and images in the content that are specified by a relative URL
@@ -212,42 +216,42 @@ content to include.
212
216
 
213
217
  ```jinja
214
218
  {%
215
- include-markdown "../README.md"
216
- start="<!--intro-start-->"
217
- end="<!--intro-end-->"
219
+ include-markdown "../README.md"
220
+ start="<!--intro-start-->"
221
+ end="<!--intro-end-->"
218
222
  %}
219
223
  ```
220
224
 
221
225
  ```jinja
222
226
  {%
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
227
+ include-markdown 'includes/header.md'
228
+ start='<!--\n\ttable-start\n-->'
229
+ end='<!--\n\ttable-end\n-->'
230
+ rewrite-relative-urls=false
231
+ comments=false
228
232
  %}
229
233
  ```
230
234
 
231
235
  ```jinja
232
236
  {%
233
- include-markdown "includes/header.md"
234
- heading-offset=1
237
+ include-markdown "includes/header.md"
238
+ heading-offset=1
235
239
  %}
236
240
  ```
237
241
 
238
242
  ```jinja
239
243
  {%
240
- include-markdown "../LICENSE*"
241
- start="<!--license \"start\" -->"
242
- end='<!--license "end" -->'
243
- exclude="../*.rst"
244
+ include-markdown "../LICENSE*"
245
+ start="<!--license \"start\" -->"
246
+ end='<!--license "end" -->'
247
+ exclude="../*.rst"
244
248
  %}
245
249
  ```
246
250
 
247
251
  ```jinja
248
252
  {%
249
- include-markdown "**"
250
- exclude="./{index,LICENSE}.md"
253
+ include-markdown "**"
254
+ exclude="./{index,LICENSE}.md"
251
255
  %}
252
256
  ```
253
257
 
@@ -278,12 +282,12 @@ Includes the content of a file or a group of files.
278
282
  - <a name="include_trailing-newlines" href="#include_trailing-newlines">#</a>
279
283
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
280
284
  found in the content to include are stripped. Possible values are `true` and `false`.
281
- - <a name="include_encoding" href="#include_encoding">#</a>
282
- **encoding** (_utf-8_): Specify the encoding of the included file.
283
- If not defined `utf-8` will be used.
284
- - <a name="recursive" href="#include_recursive">#</a>
285
+ - <a name="include_recursive" href="#include_recursive">#</a>
285
286
  **recursive** (_true_): When this option is disabled, included files are not
286
287
  processed for recursive includes. Possible values are `true` and `false`.
288
+ - <a name="include_encoding" href="#include_encoding">#</a>
289
+ **encoding** (_'utf-8'_): Specify the encoding of the included file.
290
+ If not defined `'utf-8'` will be used.
287
291
 
288
292
  ##### Examples
289
293
 
@@ -295,23 +299,24 @@ Includes the content of a file or a group of files.
295
299
 
296
300
  ```jinja
297
301
  {%
298
- include "../examples.md"
299
- start="~~~yaml"
300
- end="~~~\n"
302
+ include "../examples.md"
303
+ start="~~~yaml"
304
+ end="~~~\n"
301
305
  %}
302
306
  ```
303
307
 
304
308
  ```jinja
305
309
  {%
306
- include '**'
307
- exclude='./*.md'
310
+ include '**'
311
+ exclude='./*.md'
308
312
  %}
309
313
  ```
310
314
 
311
315
  ## Acknowledgment
312
316
 
313
- - Joe Rickerby and contributors for
314
- [giving me the permissions][cibuildwheel-470] to separate this plugin from the
317
+ - [Joe Rickerby] and [contributors] for
318
+ [giving me the permissions][cibuildwheel-470] to
319
+ [separate this plugin][cibuildwheel-475] from the
315
320
  documentation of [cibuildwheel][cibuildwheel-repo-link].
316
321
 
317
322
  [Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax
@@ -326,9 +331,14 @@ Includes the content of a file or a group of files.
326
331
  [coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-plugin
327
332
  [license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-plugin?color=light-green&logo=apache&logoColor=white
328
333
  [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE
334
+ [downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-plugin
335
+ [downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin
329
336
  [platformdirs]: https://pypi.org/project/platformdirs/
330
337
  [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470
338
+ [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475
331
339
  [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel
332
340
  [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md
333
341
  [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md
334
342
  [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir
343
+ [Joe Rickerby]: https://github.com/joerick
344
+ [contributors]: https://github.com/mondeja/mkdocs-include-markdown-plugin/graphs/contributors
@@ -8,6 +8,7 @@
8
8
  [![License][license-image]][license-link]
9
9
  [![Tests][tests-image]][tests-link]
10
10
  [![Coverage status][coverage-image]][coverage-link]
11
+ [![Downloads][downloads-image]][downloads-link]
11
12
 
12
13
  <!-- mdpo-enable -->
13
14
 
@@ -155,9 +156,12 @@ content to include.
155
156
  - <a name="include-markdown_trailing-newlines" href="#include-markdown_trailing-newlines">#</a>
156
157
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
157
158
  found in the content to include are stripped. Possible values are `true` and `false`.
159
+ - <a name="include-markdown_recursive" href="#include-markdown_recursive">#</a>
160
+ **recursive** (_true_): When this option is disabled, included files are not
161
+ processed for recursive includes. Possible values are `true` and `false`.
158
162
  - <a name="include-markdown_encoding" href="#include-markdown_encoding">#</a>
159
- **encoding** (_utf-8_): Specify the encoding of the included file.
160
- If not defined `utf-8` will be used.
163
+ **encoding** (_'utf-8'_): Specify the encoding of the included file.
164
+ If not defined `'utf-8'` will be used.
161
165
  - <a name="include-markdown_rewrite-relative-urls" href="#include-markdown_rewrite-relative-urls">#</a>
162
166
  **rewrite-relative-urls** (_true_): When this option is enabled (default),
163
167
  Markdown links and images in the content that are specified by a relative URL
@@ -177,42 +181,42 @@ content to include.
177
181
 
178
182
  ```jinja
179
183
  {%
180
- include-markdown "../README.md"
181
- start="<!--intro-start-->"
182
- end="<!--intro-end-->"
184
+ include-markdown "../README.md"
185
+ start="<!--intro-start-->"
186
+ end="<!--intro-end-->"
183
187
  %}
184
188
  ```
185
189
 
186
190
  ```jinja
187
191
  {%
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
192
+ include-markdown 'includes/header.md'
193
+ start='<!--\n\ttable-start\n-->'
194
+ end='<!--\n\ttable-end\n-->'
195
+ rewrite-relative-urls=false
196
+ comments=false
193
197
  %}
194
198
  ```
195
199
 
196
200
  ```jinja
197
201
  {%
198
- include-markdown "includes/header.md"
199
- heading-offset=1
202
+ include-markdown "includes/header.md"
203
+ heading-offset=1
200
204
  %}
201
205
  ```
202
206
 
203
207
  ```jinja
204
208
  {%
205
- include-markdown "../LICENSE*"
206
- start="<!--license \"start\" -->"
207
- end='<!--license "end" -->'
208
- exclude="../*.rst"
209
+ include-markdown "../LICENSE*"
210
+ start="<!--license \"start\" -->"
211
+ end='<!--license "end" -->'
212
+ exclude="../*.rst"
209
213
  %}
210
214
  ```
211
215
 
212
216
  ```jinja
213
217
  {%
214
- include-markdown "**"
215
- exclude="./{index,LICENSE}.md"
218
+ include-markdown "**"
219
+ exclude="./{index,LICENSE}.md"
216
220
  %}
217
221
  ```
218
222
 
@@ -243,12 +247,12 @@ Includes the content of a file or a group of files.
243
247
  - <a name="include_trailing-newlines" href="#include_trailing-newlines">#</a>
244
248
  **trailing-newlines** (_true_): When this option is disabled, the trailing newlines
245
249
  found in the content to include are stripped. Possible values are `true` and `false`.
246
- - <a name="include_encoding" href="#include_encoding">#</a>
247
- **encoding** (_utf-8_): Specify the encoding of the included file.
248
- If not defined `utf-8` will be used.
249
- - <a name="recursive" href="#include_recursive">#</a>
250
+ - <a name="include_recursive" href="#include_recursive">#</a>
250
251
  **recursive** (_true_): When this option is disabled, included files are not
251
252
  processed for recursive includes. Possible values are `true` and `false`.
253
+ - <a name="include_encoding" href="#include_encoding">#</a>
254
+ **encoding** (_'utf-8'_): Specify the encoding of the included file.
255
+ If not defined `'utf-8'` will be used.
252
256
 
253
257
  ##### Examples
254
258
 
@@ -260,23 +264,24 @@ Includes the content of a file or a group of files.
260
264
 
261
265
  ```jinja
262
266
  {%
263
- include "../examples.md"
264
- start="~~~yaml"
265
- end="~~~\n"
267
+ include "../examples.md"
268
+ start="~~~yaml"
269
+ end="~~~\n"
266
270
  %}
267
271
  ```
268
272
 
269
273
  ```jinja
270
274
  {%
271
- include '**'
272
- exclude='./*.md'
275
+ include '**'
276
+ exclude='./*.md'
273
277
  %}
274
278
  ```
275
279
 
276
280
  ## Acknowledgment
277
281
 
278
- - Joe Rickerby and contributors for
279
- [giving me the permissions][cibuildwheel-470] to separate this plugin from the
282
+ - [Joe Rickerby] and [contributors] for
283
+ [giving me the permissions][cibuildwheel-470] to
284
+ [separate this plugin][cibuildwheel-475] from the
280
285
  documentation of [cibuildwheel][cibuildwheel-repo-link].
281
286
 
282
287
  [Bash wildcard globs]: https://facelessuser.github.io/wcmatch/glob/#syntax
@@ -291,9 +296,14 @@ Includes the content of a file or a group of files.
291
296
  [coverage-link]: https://app.codecov.io/gh/mondeja/mkdocs-include-markdown-plugin
292
297
  [license-image]: https://img.shields.io/pypi/l/mkdocs-include-markdown-plugin?color=light-green&logo=apache&logoColor=white
293
298
  [license-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/LICENSE
299
+ [downloads-image]: https://img.shields.io/pypi/dm/mkdocs-include-markdown-plugin
300
+ [downloads-link]: https://pepy.tech/project/mkdocs-include-markdown-plugin
294
301
  [platformdirs]: https://pypi.org/project/platformdirs/
295
302
  [cibuildwheel-470]: https://github.com/pypa/cibuildwheel/issues/470
303
+ [cibuildwheel-475]: https://github.com/pypa/cibuildwheel/pull/475
296
304
  [cibuildwheel-repo-link]: https://github.com/pypa/cibuildwheel
297
305
  [es-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/es/README.md
298
306
  [fr-readme-link]: https://github.com/mondeja/mkdocs-include-markdown-plugin/blob/master/locale/fr/README.md
299
307
  [`docs_dir`]: https://www.mkdocs.org/user-guide/configuration/#docs_dir
308
+ [Joe Rickerby]: https://github.com/joerick
309
+ [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.1"
4
4
  description = "Mkdocs Markdown includer plugin."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -39,24 +39,24 @@ class Cache:
39
39
  hashlib.sha3_512(url.encode()).digest(),
40
40
  ).decode('utf-8')
41
41
 
42
- def read_file(self, fpath: str) -> str: # noqa: D102
43
- with open(fpath, encoding='utf-8') as f:
42
+ def read_file(self, fpath: str, encoding: str = 'utf-8') -> str: # noqa: D102
43
+ with open(fpath, encoding=encoding) as f:
44
44
  return f.read().split('\n', 1)[1]
45
45
 
46
- def get_(self, url: str) -> str | None: # noqa: D102
46
+ def get_(self, url: str, encoding: str = 'utf-8') -> str | None: # noqa: D102
47
47
  key = self.generate_unique_key_from_url(url)
48
48
  fpath = os.path.join(self.cache_dir, key)
49
49
  if os.path.isfile(fpath):
50
50
  creation_time = self.get_creation_time_from_fpath(fpath)
51
51
  if time.time() < creation_time + self.expiration_seconds:
52
- return self.read_file(fpath)
52
+ return self.read_file(fpath, encoding=encoding)
53
53
  os.remove(fpath)
54
54
  return None
55
55
 
56
- def set_(self, url: str, value: str) -> None: # noqa: D102
56
+ def set_(self, url: str, value: str, encoding: str = 'utf-8') -> None: # noqa: D102
57
57
  key = self.generate_unique_key_from_url(url)
58
58
  fpath = os.path.join(self.cache_dir, key)
59
- with open(fpath, 'w', encoding='utf-8') as f:
59
+ with open(fpath, 'w', encoding=encoding) as f:
60
60
  f.write(f'{int(time.time())}\n')
61
61
  f.write(value)
62
62
 
@@ -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,14 +45,14 @@ 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
52
53
  # closing tags in the `on_config` plugin event.
53
54
  INCLUDE_TAG_RE = rf'''
54
- (?P<_includer_indent>[ \t\f\v\w{re.escape(string.punctuation)}]*?)$OPENING_TAG
55
+ (?P<_includer_indent>[ \t\w\\.]*?)$OPENING_TAG
55
56
  \s*
56
57
  include
57
58
  \s+
@@ -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],
@@ -138,7 +171,7 @@ def create_include_tag(
138
171
  """Create a regex pattern to match an inclusion tag directive.
139
172
 
140
173
  Replaces the substrings '$OPENING_TAG' and '$CLOSING_TAG' from
141
- INCLUDE_TAG_REGEX by the effective tag.
174
+ INCLUDE_TAG_RE by the effective tag.
142
175
  """
143
176
  return re.compile(
144
177
  INCLUDE_TAG_RE.replace(' include', f' {tag}').replace(
@@ -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
-
55
-
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
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:'
59
53
 
60
54
 
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']
@@ -251,7 +230,9 @@ def get_file_content( # noqa: PLR0913, PLR0915
251
230
  expected_but_any_found = [start is not None, end is not None]
252
231
  for file_path in file_paths_to_include:
253
232
  if process.is_url(filename):
254
- new_text_to_include = process.read_url(file_path, http_cache)
233
+ new_text_to_include = process.read_url(
234
+ file_path, http_cache, encoding,
235
+ )
255
236
  else:
256
237
  new_text_to_include = process.read_file(file_path, encoding)
257
238
 
@@ -308,69 +289,72 @@ def get_file_content( # noqa: PLR0913, PLR0915
308
289
  if expected_but_any_found[i]:
309
290
  delimiter_value = locals()[delimiter_name]
310
291
  readable_files_to_include = ', '.join([
311
- safe_os_path_relpath(fpath, docs_dir)
292
+ process.safe_os_path_relpath(fpath, docs_dir)
312
293
  for fpath in file_paths_to_include
313
294
  ])
314
295
  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,
296
+ location = process.file_lineno_message(
297
+ page_src_path, docs_dir, directive_lineno,
321
298
  )
322
299
  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
- ),
300
+ f"Delimiter {delimiter_name} '{delimiter_value}'"
301
+ f" of 'include' directive at {location}"
302
+ f' not detected in the file{plural_suffix}'
303
+ f' {readable_files_to_include}',
330
304
  )
331
305
 
332
- return text_to_include
306
+ nonlocal new_found_include_contents
307
+ include_index = len(new_found_include_contents)
308
+ placeholder = build_placeholder(include_index, 'include')
309
+ new_found_include_contents.append((placeholder, text_to_include))
310
+ return placeholder
333
311
 
334
312
  def found_include_markdown_tag( # noqa: PLR0912, PLR0915
335
313
  match: re.Match[str],
336
314
  ) -> str:
337
315
  directive_match_start = match.start()
316
+ directive_lineno = process.lineno_from_content_start(
317
+ markdown,
318
+ directive_match_start,
319
+ )
338
320
 
339
321
  includer_indent = match['_includer_indent']
340
322
  empty_includer_indent = ' ' * len(includer_indent)
341
323
 
342
324
  filename, raw_filename = parse_filename_argument(match)
343
325
  if filename is None:
344
- lineno = lineno_from_content_start(
345
- markdown,
346
- directive_match_start,
326
+ location = process.file_lineno_message(
327
+ page_src_path, docs_dir, directive_lineno,
347
328
  )
348
329
  raise PluginError(
349
330
  "Found no path passed including with 'include-markdown'"
350
- f' directive at'
351
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
331
+ f' directive at {location}',
352
332
  )
353
333
 
354
334
  arguments_string = match['arguments']
355
335
 
336
+ warn_invalid_directive_arguments(
337
+ arguments_string,
338
+ directive_lineno,
339
+ 'include-markdown',
340
+ page_src_path,
341
+ docs_dir,
342
+ )
343
+
356
344
  exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
357
345
  ignore_paths = [*settings_ignore_paths]
358
346
  if exclude_match is not None:
359
347
  exclude_string = parse_string_argument(exclude_match)
360
348
  if exclude_string is None:
361
- lineno = lineno_from_content_start(
362
- markdown,
363
- directive_match_start,
349
+ location = process.file_lineno_message(
350
+ page_src_path, docs_dir, directive_lineno,
364
351
  )
365
352
  raise PluginError(
366
353
  "Invalid empty 'exclude' argument in 'include-markdown'"
367
- f' directive at'
368
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
354
+ f' directive at {location}',
369
355
  )
370
356
  for path in resolve_file_paths_to_exclude(
371
- exclude_string,
372
- page_src_path,
373
- docs_dir,
357
+ exclude_string, page_src_path, docs_dir,
374
358
  ):
375
359
  if path not in ignore_paths:
376
360
  ignore_paths.append(path)
@@ -383,13 +367,11 @@ def get_file_content( # noqa: PLR0913, PLR0915
383
367
  )
384
368
 
385
369
  if not file_paths_to_include:
386
- lineno = lineno_from_content_start(
387
- markdown,
388
- directive_match_start,
370
+ location = process.file_lineno_message(
371
+ page_src_path, docs_dir, directive_lineno,
389
372
  )
390
373
  raise PluginError(
391
- f"No files found including '{raw_filename}' at"
392
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
374
+ f"No files found including '{raw_filename}' at {location}",
393
375
  )
394
376
 
395
377
  if files_watcher is not None and not is_url:
@@ -399,21 +381,19 @@ def get_file_content( # noqa: PLR0913, PLR0915
399
381
  [
400
382
  'rewrite-relative-urls', 'comments',
401
383
  'preserve-includer-indent', 'dedent',
402
- 'trailing-newlines',
384
+ 'trailing-newlines', 'recursive',
403
385
  ],
404
386
  defaults,
405
387
  arguments_string,
406
388
  )
407
389
  if invalid_bool_args:
408
- lineno = lineno_from_content_start(
409
- markdown,
410
- directive_match_start,
390
+ location = process.file_lineno_message(
391
+ page_src_path, docs_dir, directive_lineno,
411
392
  )
412
393
  raise PluginError(
413
394
  f"Invalid value for '{invalid_bool_args[0]}' argument of"
414
395
  " 'include-markdown' directive at"
415
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}.'
416
- f' Possible values are true or false.',
396
+ f' {location}. Possible values are true or false.',
417
397
  )
418
398
 
419
399
  # start and end arguments
@@ -421,14 +401,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
421
401
  if start_match:
422
402
  start = parse_string_argument(start_match)
423
403
  if start is None:
424
- lineno = lineno_from_content_start(
425
- markdown,
426
- directive_match_start,
404
+ location = process.file_lineno_message(
405
+ page_src_path, docs_dir, directive_lineno,
427
406
  )
428
407
  raise PluginError(
429
408
  "Invalid empty 'start' argument in 'include-markdown'"
430
- f' directive at'
431
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
409
+ f' directive at {location}',
432
410
  )
433
411
  else:
434
412
  start = defaults['start']
@@ -437,14 +415,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
437
415
  if end_match:
438
416
  end = parse_string_argument(end_match)
439
417
  if end is None:
440
- lineno = lineno_from_content_start(
441
- markdown,
442
- directive_match_start,
418
+ location = process.file_lineno_message(
419
+ page_src_path, docs_dir, directive_lineno,
443
420
  )
444
421
  raise PluginError(
445
422
  "Invalid empty 'end' argument in 'include-markdown'"
446
- f' directive at'
447
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
423
+ f' directive at {location}',
448
424
  )
449
425
  else:
450
426
  end = defaults['end']
@@ -453,14 +429,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
453
429
  if encoding_match:
454
430
  encoding = parse_string_argument(encoding_match)
455
431
  if encoding is None:
456
- lineno = lineno_from_content_start(
457
- markdown,
458
- directive_match_start,
432
+ location = process.file_lineno_message(
433
+ page_src_path, docs_dir, directive_lineno,
459
434
  )
460
435
  raise PluginError(
461
436
  "Invalid empty 'encoding' argument in 'include-markdown'"
462
- ' directive at'
463
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
437
+ f' directive at {location}',
464
438
  )
465
439
  else:
466
440
  encoding = defaults['encoding']
@@ -472,26 +446,22 @@ def get_file_content( # noqa: PLR0913, PLR0915
472
446
  if offset_match:
473
447
  offset_raw_value = offset_match[1]
474
448
  if offset_raw_value == '':
475
- lineno = lineno_from_content_start(
476
- markdown,
477
- directive_match_start,
449
+ location = process.file_lineno_message(
450
+ page_src_path, docs_dir, directive_lineno,
478
451
  )
479
452
  raise PluginError(
480
453
  "Invalid empty 'heading-offset' argument in"
481
- " 'include-markdown' directive at"
482
- f' {file_lineno_message(page_src_path, docs_dir, lineno)}',
454
+ f" 'include-markdown' directive at {location}",
483
455
  )
484
456
  try:
485
457
  offset = int(offset_raw_value)
486
458
  except ValueError:
487
- lineno = lineno_from_content_start(
488
- markdown,
489
- directive_match_start,
459
+ location = process.file_lineno_message(
460
+ page_src_path, docs_dir, directive_lineno,
490
461
  )
491
462
  raise PluginError(
492
463
  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)}',
464
+ f" in 'include-markdown' directive at {location}",
495
465
  ) from None
496
466
  else:
497
467
  offset = defaults['heading-offset']
@@ -513,7 +483,9 @@ def get_file_content( # noqa: PLR0913, PLR0915
513
483
  text_to_include = ''
514
484
  for file_path in file_paths_to_include:
515
485
  if process.is_url(filename):
516
- new_text_to_include = process.read_url(file_path, http_cache)
486
+ new_text_to_include = process.read_url(
487
+ file_path, http_cache, encoding,
488
+ )
517
489
  else:
518
490
  new_text_to_include = process.read_file(file_path, encoding)
519
491
 
@@ -530,16 +502,17 @@ def get_file_content( # noqa: PLR0913, PLR0915
530
502
  expected_but_any_found[i] = False
531
503
 
532
504
  # 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
- )
505
+ if bool_options['recursive'].value:
506
+ new_text_to_include = get_file_content(
507
+ new_text_to_include,
508
+ file_path,
509
+ docs_dir,
510
+ tags,
511
+ defaults,
512
+ settings,
513
+ files_watcher=files_watcher,
514
+ http_cache=http_cache,
515
+ )
543
516
 
544
517
  # trailing newlines right stripping
545
518
  if not bool_options['trailing-newlines'].value:
@@ -601,38 +574,47 @@ def get_file_content( # noqa: PLR0913, PLR0915
601
574
  if expected_but_any_found[i]:
602
575
  delimiter_value = locals()[delimiter_name]
603
576
  readable_files_to_include = ', '.join([
604
- safe_os_path_relpath(fpath, docs_dir)
577
+ process.safe_os_path_relpath(fpath, docs_dir)
605
578
  for fpath in file_paths_to_include
606
579
  ])
607
580
  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,
581
+ location = process.file_lineno_message(
582
+ page_src_path, docs_dir, directive_lineno,
614
583
  )
615
584
  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
- ),
585
+ f"Delimiter {delimiter_name} '{delimiter_value}' of"
586
+ f" 'include-markdown' directive at {location}"
587
+ f' not detected in the file{plural_suffix}'
588
+ f' {readable_files_to_include}',
623
589
  )
624
590
 
625
- return text_to_include
591
+ nonlocal new_found_include_markdown_contents
592
+ markdown_include_index = len(new_found_include_markdown_contents)
593
+ placeholder = build_placeholder(
594
+ markdown_include_index, 'include-markdown',
595
+ )
596
+ new_found_include_markdown_contents.append(
597
+ (placeholder, text_to_include),
598
+ )
599
+ return placeholder
626
600
 
601
+ # Replace contents by placeholders
627
602
  markdown = tags['include-markdown'].sub(
628
603
  found_include_markdown_tag,
629
604
  markdown,
630
605
  )
631
- return tags['include'].sub(
606
+ markdown = tags['include'].sub(
632
607
  found_include_tag,
633
608
  markdown,
634
609
  )
635
610
 
611
+ # Replace placeholders by contents
612
+ for placeholder, text in new_found_include_contents:
613
+ markdown = markdown.replace(placeholder, text, 1)
614
+ for placeholder, text in new_found_include_markdown_contents:
615
+ markdown = markdown.replace(placeholder, text, 1)
616
+ return markdown
617
+
636
618
 
637
619
  def on_page_markdown(
638
620
  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
 
@@ -441,14 +443,53 @@ def read_file(file_path: str, encoding: str) -> str:
441
443
  return f.read()
442
444
 
443
445
 
444
- def read_url(url: str, http_cache: Cache | None) -> Any:
446
+ def read_url(
447
+ url: str,
448
+ http_cache: Cache | None,
449
+ encoding: str = 'utf-8',
450
+ ) -> Any:
445
451
  """Read an HTTP location and return its content."""
446
452
  if http_cache is not None:
447
- cached_content = http_cache.get_(url)
453
+ cached_content = http_cache.get_(url, encoding)
448
454
  if cached_content is not None:
449
455
  return cached_content
450
456
  with urlopen(Request(url)) as response:
451
- content = response.read().decode('UTF-8')
457
+ content = response.read().decode(encoding)
452
458
  if http_cache is not None:
453
- http_cache.set_(url, content)
459
+ http_cache.set_(url, content, encoding)
454
460
  return content
461
+
462
+
463
+ def safe_os_path_relpath(path: str, start: str) -> str:
464
+ """Return the relative path of a file from a start directory.
465
+
466
+ Safe version of `os.path.relpath` that catches `ValueError` exceptions
467
+ on Windows and returns the original path in case of error.
468
+ On Windows, `ValueError` is raised when `path` and `start` are on
469
+ different drives.
470
+ """
471
+ if os.name != 'nt': # pragma: nt no cover
472
+ return os.path.relpath(path, start)
473
+ try: # pragma: nt cover
474
+ return os.path.relpath(path, start)
475
+ except ValueError: # pragma: no cover
476
+ return path
477
+
478
+
479
+ def file_lineno_message(
480
+ page_src_path: str | None,
481
+ docs_dir: str,
482
+ lineno: int,
483
+ ) -> str:
484
+ """Return a message with the file path and line number."""
485
+ if page_src_path is None: # pragma: no cover
486
+ return f'generated page content (line {lineno})'
487
+ return (
488
+ f'{safe_os_path_relpath(page_src_path, docs_dir)}'
489
+ f':{lineno}'
490
+ )
491
+
492
+
493
+ def lineno_from_content_start(content: str, start: int) -> int:
494
+ """Return the line number of the first line of ``start`` in ``content``."""
495
+ return content[:start].count('\n') + 1