mkdocs-include-markdown-plugin 7.0.1__tar.gz → 7.1.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-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/PKG-INFO +15 -1
  2. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/README.md +14 -0
  3. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/pyproject.toml +1 -1
  4. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/cache.py +18 -11
  5. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/config.py +8 -0
  6. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/directive.py +47 -27
  7. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/event.py +45 -31
  8. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/plugin.py +12 -16
  9. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/process.py +53 -22
  10. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/.gitignore +0 -0
  11. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/LICENSE +0 -0
  12. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/__init__.py +0 -0
  13. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/files_watcher.py +0 -0
  14. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/logger.py +0 -0
  15. {mkdocs_include_markdown_plugin-7.0.1 → mkdocs_include_markdown_plugin-7.1.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: 7.0.1
3
+ Version: 7.1.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
@@ -143,6 +143,20 @@ You can include it in the installation of the plugin adding the `cache` extra:
143
143
  mkdocs-include-markdown-plugin[cache]
144
144
  ```
145
145
 
146
+ <!-- mdpo-disable-next-line -->
147
+
148
+ #### `directives`
149
+
150
+ Customize the names of the directives.
151
+
152
+ ```yaml
153
+ plugins:
154
+ - include-markdown:
155
+ directives:
156
+ include-markdown: include-md
157
+ include: replace
158
+ ```
159
+
146
160
  ### Reference
147
161
 
148
162
  This plugin provides two directives, one to include Markdown files and another
@@ -109,6 +109,20 @@ You can include it in the installation of the plugin adding the `cache` extra:
109
109
  mkdocs-include-markdown-plugin[cache]
110
110
  ```
111
111
 
112
+ <!-- mdpo-disable-next-line -->
113
+
114
+ #### `directives`
115
+
116
+ Customize the names of the directives.
117
+
118
+ ```yaml
119
+ plugins:
120
+ - include-markdown:
121
+ directives:
122
+ include-markdown: include-md
123
+ include: replace
124
+ ```
125
+
112
126
  ### Reference
113
127
 
114
128
  This plugin provides two directives, one to include Markdown files and another
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs-include-markdown-plugin"
3
- version = "7.0.1"
3
+ version = "7.1.1"
4
4
  description = "Mkdocs Markdown includer plugin."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -4,15 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import os
7
+ import stat
7
8
  import time
9
+ from importlib.util import find_spec
8
10
 
9
11
 
10
- try:
11
- from platformdirs import user_data_dir
12
- except ImportError: # pragma: no cover
13
- CACHE_AVAILABLE = False
14
- else:
15
- CACHE_AVAILABLE = True
12
+ CACHE_AVAILABLE = find_spec('platformdirs') is not None
16
13
 
17
14
 
18
15
  class Cache:
@@ -37,13 +34,19 @@ class Cache:
37
34
  return hashlib.blake2b(url.encode(), digest_size=16).digest().hex()
38
35
 
39
36
  def read_file(self, fpath: str, encoding: str = 'utf-8') -> str: # noqa: D102
40
- with open(fpath, encoding=encoding) as f:
41
- return f.read().split('\n', 1)[1]
37
+ f = open(fpath, encoding=encoding) # noqa: SIM115
38
+ content = f.read().split('\n', 1)[1]
39
+ f.close()
40
+ return content
42
41
 
43
42
  def get_(self, url: str, encoding: str = 'utf-8') -> str | None: # noqa: D102
44
43
  key = self.generate_unique_key_from_url(url)
45
44
  fpath = os.path.join(self.cache_dir, key)
46
- if os.path.isfile(fpath):
45
+ try:
46
+ is_file = stat.S_ISREG(os.stat(fpath).st_mode)
47
+ except (FileNotFoundError, OSError): # pragma: no cover
48
+ return None
49
+ if is_file:
47
50
  creation_time = self.get_creation_time_from_fpath(fpath)
48
51
  if time.time() < creation_time + self.expiration_seconds:
49
52
  return self.read_file(fpath, encoding=encoding)
@@ -72,9 +75,13 @@ def get_cache_directory() -> str | None:
72
75
  if not CACHE_AVAILABLE:
73
76
  return None
74
77
 
78
+ try:
79
+ from platformdirs import user_data_dir
80
+ except ImportError:
81
+ return None
82
+
75
83
  cache_dir = user_data_dir('mkdocs-include-markdown-plugin')
76
- if not os.path.isdir(cache_dir):
77
- os.makedirs(cache_dir)
84
+ os.makedirs(cache_dir, exist_ok=True)
78
85
 
79
86
  return cache_dir
80
87
 
@@ -25,3 +25,11 @@ class PluginConfig(Config): # noqa: D101
25
25
  exclude = ListOfItems(MkType(str), default=[])
26
26
  cache = MkType(int, default=0)
27
27
  recursive = MkType(bool, default=True)
28
+ directives = MkType(
29
+ dict,
30
+ default={
31
+ '__default': '',
32
+ 'include': 'include',
33
+ 'include-markdown': 'include-markdown',
34
+ },
35
+ )
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import functools
5
6
  import os
6
7
  import re
8
+ import stat
7
9
  import string
8
10
  from dataclasses import dataclass
9
11
  from typing import TYPE_CHECKING
@@ -22,7 +24,7 @@ class DirectiveBoolArgument: # noqa: D101
22
24
 
23
25
 
24
26
  if TYPE_CHECKING: # pragma: no cover
25
- from typing import Literal, TypedDict
27
+ from typing import Callable, Literal, TypedDict
26
28
 
27
29
  DirectiveBoolArgumentsDict = dict[str, DirectiveBoolArgument]
28
30
 
@@ -73,11 +75,13 @@ TRUE_FALSE_BOOL_STR = {
73
75
  }
74
76
 
75
77
 
78
+ @functools.lru_cache
76
79
  def arg(arg: str) -> re.Pattern[str]:
77
80
  """Return a compiled regexp to match a boolean argument."""
78
81
  return re.compile(rf'{arg}=([{RE_ESCAPED_PUNCTUATION}\w]*)')
79
82
 
80
83
 
84
+ @functools.lru_cache
81
85
  def str_arg(arg: str) -> re.Pattern[str]:
82
86
  """Return a compiled regexp to match a string argument."""
83
87
  return re.compile(
@@ -87,21 +91,23 @@ def str_arg(arg: str) -> re.Pattern[str]:
87
91
 
88
92
 
89
93
  ARGUMENT_REGEXES = {
90
- 'start': str_arg('start'),
91
- 'end': str_arg('end'),
92
- 'exclude': str_arg('exclude'),
93
- 'encoding': str_arg('encoding'),
94
+ 'start': functools.partial(str_arg, 'start'),
95
+ 'end': functools.partial(str_arg, 'end'),
96
+ 'exclude': functools.partial(str_arg, 'exclude'),
97
+ 'encoding': functools.partial(str_arg, 'encoding'),
94
98
 
95
99
  # bool
96
- 'comments': arg('comments'),
97
- 'preserve-includer-indent': arg('preserve-includer-indent'),
98
- 'dedent': arg('dedent'),
99
- 'trailing-newlines': arg('trailing-newlines'),
100
- 'rewrite-relative-urls': arg('rewrite-relative-urls'),
101
- 'recursive': arg('recursive'),
100
+ 'comments': functools.partial(arg, 'comments'),
101
+ 'preserve-includer-indent': functools.partial(
102
+ arg, 'preserve-includer-indent',
103
+ ),
104
+ 'dedent': functools.partial(arg, 'dedent'),
105
+ 'trailing-newlines': functools.partial(arg, 'trailing-newlines'),
106
+ 'rewrite-relative-urls': functools.partial(arg, 'rewrite-relative-urls'),
107
+ 'recursive': functools.partial(arg, 'recursive'),
102
108
 
103
109
  # int
104
- 'heading-offset': arg('heading-offset'),
110
+ 'heading-offset': functools.partial(arg, 'heading-offset'),
105
111
  }
106
112
 
107
113
  INCLUDE_DIRECTIVE_ARGS = {
@@ -110,8 +116,6 @@ INCLUDE_DIRECTIVE_ARGS = {
110
116
  )
111
117
  }
112
118
 
113
- INCLUDE_MARKDOWN_DIRECTIVE_ARGS = set(ARGUMENT_REGEXES)
114
-
115
119
  WARN_INVALID_DIRECTIVE_ARGS_REGEX = re.compile(
116
120
  rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*',
117
121
  )
@@ -119,7 +123,7 @@ WARN_INVALID_DIRECTIVE_ARGS_REGEX = re.compile(
119
123
 
120
124
  def warn_invalid_directive_arguments(
121
125
  arguments_string: str,
122
- directive_lineno: int,
126
+ directive_lineno: Callable[[], int],
123
127
  directive: Literal['include', 'include-markdown'],
124
128
  page_src_path: str | None,
125
129
  docs_dir: str,
@@ -127,15 +131,14 @@ def warn_invalid_directive_arguments(
127
131
  """Warns about the invalid arguments passed to a directive."""
128
132
  valid_args = (
129
133
  INCLUDE_DIRECTIVE_ARGS if directive == 'include'
130
- else INCLUDE_MARKDOWN_DIRECTIVE_ARGS
134
+ else set(ARGUMENT_REGEXES)
131
135
  )
132
- for arg_value in re.findall(
133
- WARN_INVALID_DIRECTIVE_ARGS_REGEX,
136
+ for arg_value in WARN_INVALID_DIRECTIVE_ARGS_REGEX.findall(
134
137
  arguments_string,
135
138
  ):
136
139
  if arg_value.split('=', 1)[0] not in valid_args:
137
140
  location = process.file_lineno_message(
138
- page_src_path, docs_dir, directive_lineno,
141
+ page_src_path, docs_dir, directive_lineno(),
139
142
  )
140
143
  logger.warning(
141
144
  f"Invalid argument '{arg_value}' in"
@@ -172,7 +175,7 @@ def parse_string_argument(match: re.Match[str]) -> str | None:
172
175
 
173
176
 
174
177
  def create_include_tag(
175
- opening_tag: str, closing_tag: str, tag: str = 'include',
178
+ opening_tag: str, closing_tag: str, tag: str,
176
179
  ) -> re.Pattern[str]:
177
180
  """Create a regex pattern to match an inclusion tag directive.
178
181
 
@@ -199,7 +202,7 @@ def parse_bool_options(
199
202
  for option_name in option_names:
200
203
  bool_options[option_name] = DirectiveBoolArgument(
201
204
  value=defaults[option_name], # type: ignore
202
- regex=ARGUMENT_REGEXES[option_name],
205
+ regex=ARGUMENT_REGEXES[option_name](),
203
206
  )
204
207
 
205
208
  for arg_name, arg in bool_options.items():
@@ -226,19 +229,28 @@ def resolve_file_paths_to_include( # noqa: PLR0912
226
229
  return [include_string], True
227
230
 
228
231
  if process.is_absolute_path(include_string):
229
- if os.name == 'nt': # pragma: nt cover
232
+ if os.name == 'nt': # pragma: no cover
230
233
  # Windows
231
234
  fpath = os.path.normpath(include_string)
232
- if not os.path.isfile(fpath):
235
+ try:
236
+ is_file = stat.S_ISREG(os.stat(fpath).st_mode)
237
+ except (FileNotFoundError, OSError):
238
+ is_file = False
239
+ if not is_file:
233
240
  return [], False
234
241
 
235
242
  return process.filter_paths(
236
243
  [fpath], ignore_paths,
237
244
  ), False
238
245
 
246
+ try:
247
+ is_file = stat.S_ISREG(os.stat(include_string).st_mode)
248
+ except (FileNotFoundError, OSError):
249
+ is_file = False
239
250
  return process.filter_paths(
240
- [include_string] if os.path.isfile(include_string)
241
- else glob.iglob(include_string, flags=GLOB_FLAGS),
251
+ [include_string] if is_file else glob.iglob(
252
+ include_string, flags=GLOB_FLAGS,
253
+ ),
242
254
  ignore_paths), False
243
255
 
244
256
  if process.is_relative_path(include_string):
@@ -253,7 +265,11 @@ def resolve_file_paths_to_include( # noqa: PLR0912
253
265
  )
254
266
  paths = []
255
267
  include_path = os.path.join(root_dir, include_string)
256
- if os.path.isfile(include_path):
268
+ try:
269
+ is_file = stat.S_ISREG(os.stat(include_path).st_mode)
270
+ except (FileNotFoundError, OSError):
271
+ is_file = False
272
+ if is_file:
257
273
  paths.append(include_path)
258
274
  else:
259
275
  for fp in glob.iglob(
@@ -268,7 +284,11 @@ def resolve_file_paths_to_include( # noqa: PLR0912
268
284
  paths = []
269
285
  root_dir = docs_dir
270
286
  include_path = os.path.join(root_dir, include_string)
271
- if os.path.isfile(include_path):
287
+ try:
288
+ is_file = stat.S_ISREG(os.stat(include_path).st_mode)
289
+ except (FileNotFoundError, OSError):
290
+ is_file = False
291
+ if is_file:
272
292
  paths.append(include_path)
273
293
  else:
274
294
  for fp in glob.iglob(
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import functools
5
6
  import html
6
7
  import os
7
8
  import re
@@ -17,6 +18,7 @@ from mkdocs_include_markdown_plugin.cache import Cache
17
18
  from mkdocs_include_markdown_plugin.directive import (
18
19
  ARGUMENT_REGEXES,
19
20
  GLOB_FLAGS,
21
+ create_include_tag,
20
22
  parse_bool_options,
21
23
  parse_filename_argument,
22
24
  parse_string_argument,
@@ -104,7 +106,8 @@ def get_file_content( # noqa: PLR0913, PLR0915
104
106
  match: re.Match[str],
105
107
  ) -> str:
106
108
  directive_match_start = match.start()
107
- directive_lineno = process.lineno_from_content_start(
109
+ directive_lineno = functools.partial(
110
+ process.lineno_from_content_start,
108
111
  markdown,
109
112
  directive_match_start,
110
113
  )
@@ -114,7 +117,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
114
117
  filename, raw_filename = parse_filename_argument(match)
115
118
  if filename is None:
116
119
  location = process.file_lineno_message(
117
- page_src_path, docs_dir, directive_lineno,
120
+ page_src_path, docs_dir, directive_lineno(),
118
121
  )
119
122
  raise PluginError(
120
123
  "Found no path passed including with 'include'"
@@ -131,13 +134,13 @@ def get_file_content( # noqa: PLR0913, PLR0915
131
134
  docs_dir,
132
135
  )
133
136
 
134
- exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
137
+ exclude_match = ARGUMENT_REGEXES['exclude']().search(arguments_string)
135
138
  ignore_paths = [*settings_ignore_paths]
136
139
  if exclude_match is not None:
137
140
  exclude_string = parse_string_argument(exclude_match)
138
141
  if exclude_string is None:
139
142
  location = process.file_lineno_message(
140
- page_src_path, docs_dir, directive_lineno,
143
+ page_src_path, docs_dir, directive_lineno(),
141
144
  )
142
145
  raise PluginError(
143
146
  "Invalid empty 'exclude' argument in 'include'"
@@ -159,7 +162,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
159
162
 
160
163
  if not file_paths_to_include:
161
164
  location = process.file_lineno_message(
162
- page_src_path, docs_dir, directive_lineno,
165
+ page_src_path, docs_dir, directive_lineno(),
163
166
  )
164
167
  raise PluginError(
165
168
  f"No files found including '{raw_filename}' at {location}",
@@ -176,7 +179,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
176
179
  )
177
180
  if invalid_bool_args:
178
181
  location = process.file_lineno_message(
179
- page_src_path, docs_dir, directive_lineno,
182
+ page_src_path, docs_dir, directive_lineno(),
180
183
  )
181
184
  raise PluginError(
182
185
  f"Invalid value for '{invalid_bool_args[0]}' argument of"
@@ -184,12 +187,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
184
187
  f' Possible values are true or false.',
185
188
  )
186
189
 
187
- start_match = ARGUMENT_REGEXES['start'].search(arguments_string)
190
+ start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
188
191
  if start_match:
189
192
  start = parse_string_argument(start_match)
190
193
  if start is None:
191
194
  location = process.file_lineno_message(
192
- page_src_path, docs_dir, directive_lineno,
195
+ page_src_path, docs_dir, directive_lineno(),
193
196
  )
194
197
  raise PluginError(
195
198
  "Invalid empty 'start' argument in 'include' directive at"
@@ -198,12 +201,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
198
201
  else:
199
202
  start = defaults['start']
200
203
 
201
- end_match = ARGUMENT_REGEXES['end'].search(arguments_string)
204
+ end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
202
205
  if end_match:
203
206
  end = parse_string_argument(end_match)
204
207
  if end is None:
205
208
  location = process.file_lineno_message(
206
- page_src_path, docs_dir, directive_lineno,
209
+ page_src_path, docs_dir, directive_lineno(),
207
210
  )
208
211
  raise PluginError(
209
212
  "Invalid empty 'end' argument in 'include' directive at"
@@ -212,12 +215,13 @@ def get_file_content( # noqa: PLR0913, PLR0915
212
215
  else:
213
216
  end = defaults['end']
214
217
 
215
- encoding_match = ARGUMENT_REGEXES['encoding'].search(arguments_string)
218
+ encoding_match = ARGUMENT_REGEXES['encoding']().search(
219
+ arguments_string)
216
220
  if encoding_match:
217
221
  encoding = parse_string_argument(encoding_match)
218
222
  if encoding is None:
219
223
  location = process.file_lineno_message(
220
- page_src_path, docs_dir, directive_lineno,
224
+ page_src_path, docs_dir, directive_lineno(),
221
225
  )
222
226
  raise PluginError(
223
227
  "Invalid empty 'encoding' argument in 'include'"
@@ -294,7 +298,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
294
298
  ])
295
299
  plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
296
300
  location = process.file_lineno_message(
297
- page_src_path, docs_dir, directive_lineno,
301
+ page_src_path, docs_dir, directive_lineno(),
298
302
  )
299
303
  logger.warning(
300
304
  f"Delimiter {delimiter_name} '{delimiter_value}'"
@@ -313,7 +317,8 @@ def get_file_content( # noqa: PLR0913, PLR0915
313
317
  match: re.Match[str],
314
318
  ) -> str:
315
319
  directive_match_start = match.start()
316
- directive_lineno = process.lineno_from_content_start(
320
+ directive_lineno = functools.partial(
321
+ process.lineno_from_content_start,
317
322
  markdown,
318
323
  directive_match_start,
319
324
  )
@@ -324,7 +329,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
324
329
  filename, raw_filename = parse_filename_argument(match)
325
330
  if filename is None:
326
331
  location = process.file_lineno_message(
327
- page_src_path, docs_dir, directive_lineno,
332
+ page_src_path, docs_dir, directive_lineno(),
328
333
  )
329
334
  raise PluginError(
330
335
  "Found no path passed including with 'include-markdown'"
@@ -341,13 +346,13 @@ def get_file_content( # noqa: PLR0913, PLR0915
341
346
  docs_dir,
342
347
  )
343
348
 
344
- exclude_match = ARGUMENT_REGEXES['exclude'].search(arguments_string)
349
+ exclude_match = ARGUMENT_REGEXES['exclude']().search(arguments_string)
345
350
  ignore_paths = [*settings_ignore_paths]
346
351
  if exclude_match is not None:
347
352
  exclude_string = parse_string_argument(exclude_match)
348
353
  if exclude_string is None:
349
354
  location = process.file_lineno_message(
350
- page_src_path, docs_dir, directive_lineno,
355
+ page_src_path, docs_dir, directive_lineno(),
351
356
  )
352
357
  raise PluginError(
353
358
  "Invalid empty 'exclude' argument in 'include-markdown'"
@@ -368,7 +373,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
368
373
 
369
374
  if not file_paths_to_include:
370
375
  location = process.file_lineno_message(
371
- page_src_path, docs_dir, directive_lineno,
376
+ page_src_path, docs_dir, directive_lineno(),
372
377
  )
373
378
  raise PluginError(
374
379
  f"No files found including '{raw_filename}' at {location}",
@@ -388,7 +393,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
388
393
  )
389
394
  if invalid_bool_args:
390
395
  location = process.file_lineno_message(
391
- page_src_path, docs_dir, directive_lineno,
396
+ page_src_path, docs_dir, directive_lineno(),
392
397
  )
393
398
  raise PluginError(
394
399
  f"Invalid value for '{invalid_bool_args[0]}' argument of"
@@ -397,12 +402,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
397
402
  )
398
403
 
399
404
  # start and end arguments
400
- start_match = ARGUMENT_REGEXES['start'].search(arguments_string)
405
+ start_match = ARGUMENT_REGEXES['start']().search(arguments_string)
401
406
  if start_match:
402
407
  start = parse_string_argument(start_match)
403
408
  if start is None:
404
409
  location = process.file_lineno_message(
405
- page_src_path, docs_dir, directive_lineno,
410
+ page_src_path, docs_dir, directive_lineno(),
406
411
  )
407
412
  raise PluginError(
408
413
  "Invalid empty 'start' argument in 'include-markdown'"
@@ -411,12 +416,12 @@ def get_file_content( # noqa: PLR0913, PLR0915
411
416
  else:
412
417
  start = defaults['start']
413
418
 
414
- end_match = ARGUMENT_REGEXES['end'].search(arguments_string)
419
+ end_match = ARGUMENT_REGEXES['end']().search(arguments_string)
415
420
  if end_match:
416
421
  end = parse_string_argument(end_match)
417
422
  if end is None:
418
423
  location = process.file_lineno_message(
419
- page_src_path, docs_dir, directive_lineno,
424
+ page_src_path, docs_dir, directive_lineno(),
420
425
  )
421
426
  raise PluginError(
422
427
  "Invalid empty 'end' argument in 'include-markdown'"
@@ -425,12 +430,13 @@ def get_file_content( # noqa: PLR0913, PLR0915
425
430
  else:
426
431
  end = defaults['end']
427
432
 
428
- encoding_match = ARGUMENT_REGEXES['encoding'].search(arguments_string)
433
+ encoding_match = ARGUMENT_REGEXES['encoding']().search(
434
+ arguments_string)
429
435
  if encoding_match:
430
436
  encoding = parse_string_argument(encoding_match)
431
437
  if encoding is None:
432
438
  location = process.file_lineno_message(
433
- page_src_path, docs_dir, directive_lineno,
439
+ page_src_path, docs_dir, directive_lineno(),
434
440
  )
435
441
  raise PluginError(
436
442
  "Invalid empty 'encoding' argument in 'include-markdown'"
@@ -440,14 +446,14 @@ def get_file_content( # noqa: PLR0913, PLR0915
440
446
  encoding = defaults['encoding']
441
447
 
442
448
  # heading offset
443
- offset_match = ARGUMENT_REGEXES['heading-offset'].search(
449
+ offset_match = ARGUMENT_REGEXES['heading-offset']().search(
444
450
  arguments_string,
445
451
  )
446
452
  if offset_match:
447
453
  offset_raw_value = offset_match[1]
448
454
  if offset_raw_value == '':
449
455
  location = process.file_lineno_message(
450
- page_src_path, docs_dir, directive_lineno,
456
+ page_src_path, docs_dir, directive_lineno(),
451
457
  )
452
458
  raise PluginError(
453
459
  "Invalid empty 'heading-offset' argument in"
@@ -457,7 +463,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
457
463
  offset = int(offset_raw_value)
458
464
  except ValueError:
459
465
  location = process.file_lineno_message(
460
- page_src_path, docs_dir, directive_lineno,
466
+ page_src_path, docs_dir, directive_lineno(),
461
467
  )
462
468
  raise PluginError(
463
469
  f"Invalid 'heading-offset' argument \"{offset_raw_value}\""
@@ -579,7 +585,7 @@ def get_file_content( # noqa: PLR0913, PLR0915
579
585
  ])
580
586
  plural_suffix = 's' if len(file_paths_to_include) > 1 else ''
581
587
  location = process.file_lineno_message(
582
- page_src_path, docs_dir, directive_lineno,
588
+ page_src_path, docs_dir, directive_lineno(),
583
589
  )
584
590
  logger.warning(
585
591
  f"Delimiter {delimiter_name} '{delimiter_value}' of"
@@ -630,8 +636,16 @@ def on_page_markdown(
630
636
  page.file.abs_src_path,
631
637
  docs_dir,
632
638
  {
633
- 'include': plugin._include_tag(),
634
- 'include-markdown': plugin._include_markdown_tag(),
639
+ 'include': create_include_tag(
640
+ config.opening_tag,
641
+ config.closing_tag,
642
+ config.directives.get('include', 'include'),
643
+ ),
644
+ 'include-markdown': create_include_tag(
645
+ config.opening_tag,
646
+ config.closing_tag,
647
+ config.directives.get('include-markdown', 'include-markdown'),
648
+ ),
635
649
  },
636
650
  {
637
651
  'encoding': config.encoding,
@@ -7,20 +7,18 @@ from functools import cached_property
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from mkdocs.exceptions import PluginError
10
- from mkdocs.livereload import LiveReloadServer
11
10
  from mkdocs.plugins import BasePlugin, event_priority
12
11
 
13
12
 
14
13
  if TYPE_CHECKING: # pragma: no cover
15
- import re
16
14
 
17
15
  from mkdocs.config.defaults import MkDocsConfig
16
+ from mkdocs.livereload import LiveReloadServer
18
17
  from mkdocs.structure.files import Files
19
18
  from mkdocs.structure.pages import Page
20
19
 
21
20
  from mkdocs_include_markdown_plugin.cache import Cache, initialize_cache
22
21
  from mkdocs_include_markdown_plugin.config import PluginConfig
23
- from mkdocs_include_markdown_plugin.directive import create_include_tag
24
22
  from mkdocs_include_markdown_plugin.event import (
25
23
  on_page_markdown as _on_page_markdown,
26
24
  )
@@ -44,20 +42,18 @@ class IncludeMarkdownPlugin(BasePlugin[PluginConfig]):
44
42
  cache.clean()
45
43
  self._cache = cache
46
44
 
47
- return config
48
-
49
- def _include_tag(self) -> re.Pattern[str]:
50
- return create_include_tag(
51
- self.config.opening_tag,
52
- self.config.closing_tag,
53
- )
45
+ if '__default' not in self.config.directives: # pragma: no cover
46
+ for directive in self.config.directives:
47
+ if directive not in {
48
+ 'include', 'include-markdown',
49
+ }:
50
+ raise PluginError(
51
+ f"Invalid directive name '{directive}' at 'directives'"
52
+ ' global setting. Valid values are "include" and'
53
+ ' "include-markdown".',
54
+ )
54
55
 
55
- def _include_markdown_tag(self) -> re.Pattern[str]:
56
- return create_include_tag(
57
- self.config.opening_tag,
58
- self.config.closing_tag,
59
- tag='include-markdown',
60
- )
56
+ return config
61
57
 
62
58
  @cached_property
63
59
  def _files_watcher(self) -> FilesWatcher:
@@ -6,10 +6,9 @@ import functools
6
6
  import io
7
7
  import os
8
8
  import re
9
+ import stat
9
10
  from collections.abc import Callable, Iterator
10
- from typing import TYPE_CHECKING, Any
11
- from urllib.parse import urlparse, urlunparse
12
- from urllib.request import Request, urlopen
11
+ from typing import TYPE_CHECKING
13
12
 
14
13
 
15
14
  if TYPE_CHECKING: # pragma: no cover
@@ -246,10 +245,15 @@ def rewrite_relative_urls(
246
245
  ``destination_path``.
247
246
  """
248
247
  def rewrite_url(url: str) -> str:
248
+ from urllib.parse import urlparse, urlunparse
249
+
250
+ if is_relative_path(url):
251
+ return url
252
+
249
253
  scheme, netloc, path, params, query, fragment = urlparse(url)
250
254
 
251
255
  # absolute or mail
252
- if is_relative_path(url) or path.startswith('/') or scheme == 'mailto':
256
+ if path.startswith('/') or scheme == 'mailto':
253
257
  return url
254
258
 
255
259
  new_path = os.path.relpath(
@@ -260,10 +264,13 @@ def rewrite_relative_urls(
260
264
  # ensure forward slashes are used, on Windows
261
265
  new_path = new_path.replace('\\', '/').replace('//', '/')
262
266
 
263
- if path.endswith('/'):
264
- # the above operation removes a trailing slash. Add it back if it
265
- # was present in the input
266
- new_path = new_path + '/'
267
+ try:
268
+ if path[-1] == '/':
269
+ # the above operation removes a trailing slash,
270
+ # so add it back if it was present in the input
271
+ new_path += '/'
272
+ except IndexError: # pragma: no cover
273
+ pass
267
274
 
268
275
  return urlunparse((scheme, netloc, new_path, params, query, fragment))
269
276
 
@@ -351,9 +358,10 @@ def filter_inclusions( # noqa: PLR0912
351
358
  if end not in text_to_include:
352
359
  expected_end_not_found = True
353
360
 
361
+ start_split = text_to_include.split(start)
354
362
  text_parts = (
355
- text_to_include.split(start)[1:]
356
- if start in text_to_include else [text_to_include]
363
+ start_split[1:]
364
+ if len(start_split) > 1 else [text_to_include]
357
365
  )
358
366
 
359
367
  for start_text in text_parts:
@@ -374,20 +382,34 @@ def _transform_negative_offset_func_factory(
374
382
  offset: int,
375
383
  ) -> Callable[[str], str]:
376
384
  heading_prefix = '#' * abs(offset)
377
- return lambda line: line if not line.startswith('#') else (
378
- heading_prefix + line.lstrip('#')
379
- if line.startswith(heading_prefix)
380
- else '#' + line.lstrip('#')
381
- )
385
+
386
+ def transform(line: str) -> str:
387
+ try:
388
+ if line[0] != '#':
389
+ return line
390
+ except IndexError: # pragma: no cover
391
+ return line
392
+ if line.startswith(heading_prefix):
393
+ return heading_prefix + line.lstrip('#')
394
+ return '#' + line.lstrip('#')
395
+
396
+ return transform
382
397
 
383
398
 
384
399
  def _transform_positive_offset_func_factory(
385
400
  offset: int,
386
401
  ) -> Callable[[str], str]:
387
402
  heading_prefix = '#' * offset
388
- return lambda line: (
389
- heading_prefix + line if line.startswith('#') else line
390
- )
403
+
404
+ def transform(line: str) -> str:
405
+ try:
406
+ prefix = line[0]
407
+ except IndexError: # pragma: no cover
408
+ return line
409
+ else:
410
+ return heading_prefix + line if prefix == '#' else line
411
+
412
+ return transform
391
413
 
392
414
 
393
415
  def increase_headings_offset(markdown: str, offset: int = 0) -> str:
@@ -442,8 +464,11 @@ def filter_paths(
442
464
  continue
443
465
 
444
466
  # ignore if is a directory
445
- if not os.path.isdir(filepath):
446
- response.append(filepath)
467
+ try:
468
+ if not stat.S_ISDIR(os.stat(filepath).st_mode):
469
+ response.append(filepath)
470
+ except (FileNotFoundError, OSError): # pragma: no cover
471
+ continue
447
472
  response.sort()
448
473
  return response
449
474
 
@@ -452,6 +477,8 @@ def is_url(string: str) -> bool:
452
477
  """Determine if a string is an URL."""
453
478
  if ':' not in string: # fast path
454
479
  return False
480
+ from urllib.parse import urlparse
481
+
455
482
  try:
456
483
  result = urlparse(string)
457
484
  return all([result.scheme, result.netloc])
@@ -471,8 +498,10 @@ def is_absolute_path(string: str) -> bool:
471
498
 
472
499
  def read_file(file_path: str, encoding: str) -> str:
473
500
  """Read a file and return its content."""
474
- with open(file_path, encoding=encoding) as f:
475
- return f.read()
501
+ f = open(file_path, encoding=encoding) # noqa: SIM115
502
+ content = f.read()
503
+ f.close()
504
+ return content
476
505
 
477
506
 
478
507
  def read_url(
@@ -481,6 +510,8 @@ def read_url(
481
510
  encoding: str = 'utf-8',
482
511
  ) -> Any:
483
512
  """Read an HTTP location and return its content."""
513
+ from urllib.request import Request, urlopen
514
+
484
515
  if http_cache is not None:
485
516
  cached_content = http_cache.get_(url, encoding)
486
517
  if cached_content is not None: