mkdocs-include-markdown-plugin 7.0.0__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.0 → mkdocs_include_markdown_plugin-7.1.1}/.gitignore +1 -0
  2. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/PKG-INFO +16 -3
  3. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/README.md +14 -0
  4. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/pyproject.toml +1 -1
  5. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/cache.py +24 -19
  6. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/config.py +8 -0
  7. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/directive.py +80 -52
  8. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/event.py +46 -32
  9. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/plugin.py +13 -19
  10. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/process.py +67 -40
  11. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/LICENSE +0 -0
  12. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/__init__.py +0 -0
  13. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/files_watcher.py +0 -0
  14. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/logger.py +0 -0
  15. {mkdocs_include_markdown_plugin-7.0.0 → mkdocs_include_markdown_plugin-7.1.1}/src/mkdocs_include_markdown_plugin/py.typed +0 -0
@@ -17,5 +17,6 @@ venv*/
17
17
  .mypy_cache/
18
18
  *.so
19
19
  .prettier-cache
20
+ /*.html
20
21
 
21
22
  site/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mkdocs-include-markdown-plugin
3
- Version: 7.0.0
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
@@ -9,8 +9,7 @@ Project-URL: Changelog, https://github.com/mondeja/mkdocs-include-markdown-plugi
9
9
  Author: Joe Rickerby
10
10
  Author-email: Álvaro Mondéjar Rubio <mondejar1994@gmail.com>
11
11
  Maintainer-email: Álvaro Mondéjar Rubio <mondejar1994@gmail.com>
12
- License-Expression: Apache-2.0
13
- License-File: LICENSE
12
+ License: Apache-2.0
14
13
  Keywords: includer,markdown,mkdocs,plugin
15
14
  Classifier: Development Status :: 5 - Production/Stable
16
15
  Classifier: Environment :: Console
@@ -144,6 +143,20 @@ You can include it in the installation of the plugin adding the `cache` extra:
144
143
  mkdocs-include-markdown-plugin[cache]
145
144
  ```
146
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
+
147
160
  ### Reference
148
161
 
149
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.0"
3
+ version = "7.1.1"
4
4
  description = "Mkdocs Markdown includer plugin."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -2,18 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import base64
6
5
  import hashlib
7
6
  import os
7
+ import stat
8
8
  import time
9
+ from importlib.util import find_spec
9
10
 
10
11
 
11
- try:
12
- from platformdirs import user_data_dir
13
- except ImportError: # pragma: no cover
14
- CACHE_AVAILABLE = False
15
- else:
16
- CACHE_AVAILABLE = True
12
+ CACHE_AVAILABLE = find_spec('platformdirs') is not None
17
13
 
18
14
 
19
15
  class Cache:
@@ -29,24 +25,28 @@ class Cache:
29
25
 
30
26
  def get_creation_time_from_fpath(self, fpath: str) -> int:
31
27
  """Get creation time of an entry in the cache given its path."""
32
- with open(fpath, encoding='utf-8') as f:
28
+ with open(fpath, 'rb') as f:
33
29
  return int(f.readline())
34
30
 
35
31
  @classmethod
36
32
  def generate_unique_key_from_url(cls, url: str) -> str:
37
33
  """Generate a unique key from an URL."""
38
- return base64.urlsafe_b64encode(
39
- hashlib.sha3_512(url.encode()).digest(),
40
- ).decode('utf-8')
34
+ return hashlib.blake2b(url.encode(), digest_size=16).digest().hex()
41
35
 
42
36
  def read_file(self, fpath: str, encoding: str = 'utf-8') -> str: # noqa: D102
43
- with open(fpath, encoding=encoding) as f:
44
- 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
45
41
 
46
42
  def get_(self, url: str, encoding: str = 'utf-8') -> str | None: # noqa: D102
47
43
  key = self.generate_unique_key_from_url(url)
48
44
  fpath = os.path.join(self.cache_dir, key)
49
- 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:
50
50
  creation_time = self.get_creation_time_from_fpath(fpath)
51
51
  if time.time() < creation_time + self.expiration_seconds:
52
52
  return self.read_file(fpath, encoding=encoding)
@@ -56,9 +56,10 @@ class Cache:
56
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=encoding) as f:
60
- f.write(f'{int(time.time())}\n')
61
- f.write(value)
59
+ with open(fpath, 'wb') as fp:
60
+ now = f'{int(time.time())}\n'
61
+ fp.write(now.encode(encoding))
62
+ fp.write(value.encode(encoding))
62
63
 
63
64
  def clean(self) -> None:
64
65
  """Clean expired entries from the cache."""
@@ -74,9 +75,13 @@ def get_cache_directory() -> str | None:
74
75
  if not CACHE_AVAILABLE:
75
76
  return None
76
77
 
78
+ try:
79
+ from platformdirs import user_data_dir
80
+ except ImportError:
81
+ return None
82
+
77
83
  cache_dir = user_data_dir('mkdocs-include-markdown-plugin')
78
- if not os.path.isdir(cache_dir):
79
- os.makedirs(cache_dir)
84
+ os.makedirs(cache_dir, exist_ok=True)
80
85
 
81
86
  return cache_dir
82
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
@@ -21,8 +23,8 @@ class DirectiveBoolArgument: # noqa: D101
21
23
  regex: re.Pattern[str]
22
24
 
23
25
 
24
- if TYPE_CHECKING:
25
- from typing import Literal, TypedDict
26
+ if TYPE_CHECKING: # pragma: no cover
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,26 +116,29 @@ INCLUDE_DIRECTIVE_ARGS = {
110
116
  )
111
117
  }
112
118
 
113
- INCLUDE_MARKDOWN_DIRECTIVE_ARGS = set(ARGUMENT_REGEXES)
119
+ WARN_INVALID_DIRECTIVE_ARGS_REGEX = re.compile(
120
+ rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*',
121
+ )
114
122
 
115
123
 
116
124
  def warn_invalid_directive_arguments(
117
125
  arguments_string: str,
118
- directive_lineno: int,
126
+ directive_lineno: Callable[[], int],
119
127
  directive: Literal['include', 'include-markdown'],
120
128
  page_src_path: str | None,
121
129
  docs_dir: str,
122
130
  ) -> None:
123
131
  """Warns about the invalid arguments passed to a directive."""
124
- arg_re = re.compile(rf'[\w-]*=[{RE_ESCAPED_PUNCTUATION}\w]*')
125
132
  valid_args = (
126
133
  INCLUDE_DIRECTIVE_ARGS if directive == 'include'
127
- else INCLUDE_MARKDOWN_DIRECTIVE_ARGS
134
+ else set(ARGUMENT_REGEXES)
128
135
  )
129
- for arg_value in re.findall(arg_re, arguments_string):
136
+ for arg_value in WARN_INVALID_DIRECTIVE_ARGS_REGEX.findall(
137
+ arguments_string,
138
+ ):
130
139
  if arg_value.split('=', 1)[0] not in valid_args:
131
140
  location = process.file_lineno_message(
132
- page_src_path, docs_dir, directive_lineno,
141
+ page_src_path, docs_dir, directive_lineno(),
133
142
  )
134
143
  logger.warning(
135
144
  f"Invalid argument '{arg_value}' in"
@@ -166,7 +175,7 @@ def parse_string_argument(match: re.Match[str]) -> str | None:
166
175
 
167
176
 
168
177
  def create_include_tag(
169
- opening_tag: str, closing_tag: str, tag: str = 'include',
178
+ opening_tag: str, closing_tag: str, tag: str,
170
179
  ) -> re.Pattern[str]:
171
180
  """Create a regex pattern to match an inclusion tag directive.
172
181
 
@@ -174,9 +183,9 @@ def create_include_tag(
174
183
  INCLUDE_TAG_RE by the effective tag.
175
184
  """
176
185
  return re.compile(
177
- INCLUDE_TAG_RE.replace(' include', f' {tag}').replace(
178
- '$OPENING_TAG', re.escape(opening_tag),
179
- ).replace('$CLOSING_TAG', re.escape(closing_tag)),
186
+ INCLUDE_TAG_RE.replace(' include', f' {tag}', 1).replace(
187
+ '$OPENING_TAG', re.escape(opening_tag), 1,
188
+ ).replace('$CLOSING_TAG', re.escape(closing_tag), 1),
180
189
  flags=re.VERBOSE | re.DOTALL,
181
190
  )
182
191
 
@@ -193,7 +202,7 @@ def parse_bool_options(
193
202
  for option_name in option_names:
194
203
  bool_options[option_name] = DirectiveBoolArgument(
195
204
  value=defaults[option_name], # type: ignore
196
- regex=ARGUMENT_REGEXES[option_name],
205
+ regex=ARGUMENT_REGEXES[option_name](),
197
206
  )
198
207
 
199
208
  for arg_name, arg in bool_options.items():
@@ -209,7 +218,7 @@ def parse_bool_options(
209
218
  return bool_options, invalid_args
210
219
 
211
220
 
212
- def resolve_file_paths_to_include(
221
+ def resolve_file_paths_to_include( # noqa: PLR0912
213
222
  include_string: str,
214
223
  includer_page_src_path: str | None,
215
224
  docs_dir: str,
@@ -220,22 +229,29 @@ def resolve_file_paths_to_include(
220
229
  return [include_string], True
221
230
 
222
231
  if process.is_absolute_path(include_string):
223
- if os.name == 'nt': # pragma: nt cover
232
+ if os.name == 'nt': # pragma: no cover
224
233
  # Windows
225
234
  fpath = os.path.normpath(include_string)
226
- 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:
227
240
  return [], False
228
241
 
229
242
  return process.filter_paths(
230
243
  [fpath], ignore_paths,
231
244
  ), False
245
+
246
+ try:
247
+ is_file = stat.S_ISREG(os.stat(include_string).st_mode)
248
+ except (FileNotFoundError, OSError):
249
+ is_file = False
232
250
  return process.filter_paths(
233
- glob.iglob(
234
- os.path.normpath(include_string),
235
- flags=GLOB_FLAGS,
251
+ [include_string] if is_file else glob.iglob(
252
+ include_string, flags=GLOB_FLAGS,
236
253
  ),
237
- ignore_paths,
238
- ), False
254
+ ignore_paths), False
239
255
 
240
256
  if process.is_relative_path(include_string):
241
257
  if includer_page_src_path is None: # pragma: no cover
@@ -247,29 +263,41 @@ def resolve_file_paths_to_include(
247
263
  root_dir = os.path.abspath(
248
264
  os.path.dirname(includer_page_src_path),
249
265
  )
250
- return process.filter_paths(
251
- (
252
- os.path.normpath(os.path.join(root_dir, fp))
253
- for fp in glob.iglob(
254
- include_string,
255
- flags=GLOB_FLAGS,
256
- root_dir=root_dir,
257
- )
258
- ), ignore_paths,
259
- ), False
260
-
261
- # relative to docs_dir
262
- return process.filter_paths(
263
- (
264
- os.path.normpath(os.path.join(docs_dir, fp))
266
+ paths = []
267
+ include_path = os.path.join(root_dir, include_string)
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:
273
+ paths.append(include_path)
274
+ else:
265
275
  for fp in glob.iglob(
266
276
  include_string,
267
277
  flags=GLOB_FLAGS,
268
- root_dir=docs_dir,
269
- )
270
- ),
271
- ignore_paths,
272
- ), False
278
+ root_dir=root_dir,
279
+ ):
280
+ paths.append(os.path.join(root_dir, fp))
281
+ return process.filter_paths(paths, ignore_paths), False
282
+
283
+ # relative to docs_dir
284
+ paths = []
285
+ root_dir = docs_dir
286
+ include_path = os.path.join(root_dir, include_string)
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:
292
+ paths.append(include_path)
293
+ else:
294
+ for fp in glob.iglob(
295
+ include_string,
296
+ flags=GLOB_FLAGS,
297
+ root_dir=root_dir,
298
+ ):
299
+ paths.append(os.path.join(root_dir, fp))
300
+ return process.filter_paths(paths, ignore_paths), False
273
301
 
274
302
 
275
303
  def resolve_file_paths_to_exclude(
@@ -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,
@@ -28,7 +30,7 @@ from mkdocs_include_markdown_plugin.files_watcher import FilesWatcher
28
30
  from mkdocs_include_markdown_plugin.logger import logger
29
31
 
30
32
 
31
- if TYPE_CHECKING:
33
+ if TYPE_CHECKING: # pragma: no cover
32
34
  from typing import Literal, TypedDict
33
35
 
34
36
  from mkdocs.structure.pages import Page
@@ -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
- if TYPE_CHECKING:
15
- import re
13
+ if TYPE_CHECKING: # pragma: no cover
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,22 +42,18 @@ class IncludeMarkdownPlugin(BasePlugin[PluginConfig]):
44
42
  cache.clean()
45
43
  self._cache = cache
46
44
 
47
- return config
48
-
49
- @cached_property
50
- def _include_tag(self) -> re.Pattern[str]:
51
- return create_include_tag(
52
- self.config.opening_tag,
53
- self.config.closing_tag,
54
- )
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
+ )
55
55
 
56
- @cached_property
57
- def _include_markdown_tag(self) -> re.Pattern[str]:
58
- return create_include_tag(
59
- self.config.opening_tag,
60
- self.config.closing_tag,
61
- tag='include-markdown',
62
- )
56
+ return config
63
57
 
64
58
  @cached_property
65
59
  def _files_watcher(self) -> FilesWatcher:
@@ -6,13 +6,12 @@ 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
- if TYPE_CHECKING:
14
+ if TYPE_CHECKING: # pragma: no cover
16
15
  from typing import Any
17
16
 
18
17
  from mkdocs_include_markdown_plugin.cache import Cache
@@ -51,9 +50,9 @@ MARKDOWN_LINK_REGEX = re.compile( # noqa: DUO138
51
50
  )
52
51
  \]
53
52
  \( # literal paren
54
- [ \t]*
53
+ \s*
55
54
  <?(.*?)>? # href = $3
56
- [ \t]*
55
+ \s*
57
56
  ( # $4
58
57
  (['"]) # quote char = $5
59
58
  (.*?) # Title = $6
@@ -145,10 +144,7 @@ def transform_p_by_p_skipping_codeblocks( # noqa: PLR0912, PLR0915
145
144
  for line in io.StringIO(markdown):
146
145
  if not _current_fcodeblock_delimiter:
147
146
  lstripped_line = line.lstrip()
148
- if (
149
- lstripped_line.startswith('```')
150
- or lstripped_line.startswith('~~~')
151
- ):
147
+ if lstripped_line.startswith(('```', '~~~')):
152
148
  _current_fcodeblock_delimiter = lstripped_line[:3]
153
149
  process_current_paragraph()
154
150
  current_paragraph = ''
@@ -226,10 +222,7 @@ def transform_line_by_line_skipping_codeblocks(
226
222
  for line in io.StringIO(markdown):
227
223
  if not _current_fcodeblock_delimiter:
228
224
  lstripped_line = line.lstrip()
229
- if (
230
- lstripped_line.startswith('```')
231
- or lstripped_line.startswith('~~~')
232
- ):
225
+ if lstripped_line.startswith(('```', '~~~')):
233
226
  _current_fcodeblock_delimiter = lstripped_line[:3]
234
227
  else:
235
228
  line = func(line) # noqa: PLW2901
@@ -252,28 +245,34 @@ def rewrite_relative_urls(
252
245
  ``destination_path``.
253
246
  """
254
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
+
255
253
  scheme, netloc, path, params, query, fragment = urlparse(url)
256
254
 
257
255
  # absolute or mail
258
256
  if path.startswith('/') or scheme == 'mailto':
259
257
  return url
260
258
 
261
- trailing_slash = path.endswith('/')
262
-
263
- path = os.path.relpath(
259
+ new_path = os.path.relpath(
264
260
  os.path.join(os.path.dirname(source_path), path),
265
261
  os.path.dirname(destination_path),
266
262
  )
267
263
 
268
264
  # ensure forward slashes are used, on Windows
269
- path = path.replace('\\', '/').replace('//', '/')
265
+ new_path = new_path.replace('\\', '/').replace('//', '/')
270
266
 
271
- if trailing_slash:
272
- # the above operation removes a trailing slash. Add it back if it
273
- # was present in the input
274
- path = 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
275
274
 
276
- return urlunparse((scheme, netloc, path, params, query, fragment))
275
+ return urlunparse((scheme, netloc, new_path, params, query, fragment))
277
276
 
278
277
  def found_href(m: re.Match[str], url_group_index: int = -1) -> str:
279
278
  match_start, match_end = m.span(0)
@@ -359,9 +358,10 @@ def filter_inclusions( # noqa: PLR0912
359
358
  if end not in text_to_include:
360
359
  expected_end_not_found = True
361
360
 
361
+ start_split = text_to_include.split(start)
362
362
  text_parts = (
363
- text_to_include.split(start)[1:]
364
- if start in text_to_include else [text_to_include]
363
+ start_split[1:]
364
+ if len(start_split) > 1 else [text_to_include]
365
365
  )
366
366
 
367
367
  for start_text in text_parts:
@@ -382,25 +382,39 @@ def _transform_negative_offset_func_factory(
382
382
  offset: int,
383
383
  ) -> Callable[[str], str]:
384
384
  heading_prefix = '#' * abs(offset)
385
- return lambda line: line if not line.startswith('#') else (
386
- heading_prefix + line.lstrip('#')
387
- if line.startswith(heading_prefix)
388
- else '#' + line.lstrip('#')
389
- )
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
390
397
 
391
398
 
392
399
  def _transform_positive_offset_func_factory(
393
400
  offset: int,
394
401
  ) -> Callable[[str], str]:
395
402
  heading_prefix = '#' * offset
396
- return lambda line: (
397
- heading_prefix + line if line.startswith('#') else line
398
- )
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
399
413
 
400
414
 
401
415
  def increase_headings_offset(markdown: str, offset: int = 0) -> str:
402
416
  """Increases the headings depth of a snippet of Makdown content."""
403
- if not offset:
417
+ if not offset: # pragma: no cover
404
418
  return markdown
405
419
  return transform_line_by_line_skipping_codeblocks(
406
420
  markdown,
@@ -444,18 +458,27 @@ def filter_paths(
444
458
  continue
445
459
 
446
460
  # ignore by dirpath (relative or absolute)
447
- if (os.sep).join(filepath.split(os.sep)[:-1]) in ignore_paths:
461
+ fp_split = filepath.split(os.sep)
462
+ fp_split.pop()
463
+ if (os.sep).join(fp_split) in ignore_paths:
448
464
  continue
449
465
 
450
466
  # ignore if is a directory
451
- if not os.path.isdir(filepath):
452
- 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
453
472
  response.sort()
454
473
  return response
455
474
 
456
475
 
457
476
  def is_url(string: str) -> bool:
458
477
  """Determine if a string is an URL."""
478
+ if ':' not in string: # fast path
479
+ return False
480
+ from urllib.parse import urlparse
481
+
459
482
  try:
460
483
  result = urlparse(string)
461
484
  return all([result.scheme, result.netloc])
@@ -470,13 +493,15 @@ def is_relative_path(string: str) -> bool:
470
493
 
471
494
  def is_absolute_path(string: str) -> bool:
472
495
  """Check if a string looks like an absolute path."""
473
- return os.path.isabs(string) or string.startswith((os.sep, '/'))
496
+ return string.startswith((os.sep, '/'))
474
497
 
475
498
 
476
499
  def read_file(file_path: str, encoding: str) -> str:
477
500
  """Read a file and return its content."""
478
- with open(file_path, encoding=encoding) as f:
479
- return f.read()
501
+ f = open(file_path, encoding=encoding) # noqa: SIM115
502
+ content = f.read()
503
+ f.close()
504
+ return content
480
505
 
481
506
 
482
507
  def read_url(
@@ -485,6 +510,8 @@ def read_url(
485
510
  encoding: str = 'utf-8',
486
511
  ) -> Any:
487
512
  """Read an HTTP location and return its content."""
513
+ from urllib.request import Request, urlopen
514
+
488
515
  if http_cache is not None:
489
516
  cached_content = http_cache.get_(url, encoding)
490
517
  if cached_content is not None: