mkdocs-partial 1.2.1__py3-none-any.whl

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 (41) hide show
  1. mkdocs_partial/__init__.py +24 -0
  2. mkdocs_partial/argparse_types.py +14 -0
  3. mkdocs_partial/docs_package_plugin.py +224 -0
  4. mkdocs_partial/entry_point.py +199 -0
  5. mkdocs_partial/integrations/__init__.py +0 -0
  6. mkdocs_partial/integrations/macros_plugin_shim.py +36 -0
  7. mkdocs_partial/integrations/material_blog_integration.py +149 -0
  8. mkdocs_partial/integrations/redirect_plugin_shim.py +23 -0
  9. mkdocs_partial/integrations/spellcheck_plugin_shim.py +46 -0
  10. mkdocs_partial/mkdcos_helpers.py +97 -0
  11. mkdocs_partial/packages/__init__.py +0 -0
  12. mkdocs_partial/packages/packager.py +218 -0
  13. mkdocs_partial/packages/templates/docs-package/dist-info/METADATA.j2 +7 -0
  14. mkdocs_partial/packages/templates/docs-package/dist-info/WHEEL.j2 +4 -0
  15. mkdocs_partial/packages/templates/docs-package/dist-info/entry_points.txt.j2 +2 -0
  16. mkdocs_partial/packages/templates/docs-package/package/__init__.py.j2 +3 -0
  17. mkdocs_partial/packages/templates/docs-package/package/__pyinstaller/__init__.py.j2 +5 -0
  18. mkdocs_partial/packages/templates/docs-package/package/__pyinstaller/hook-{{module_name}}.py.j2 +31 -0
  19. mkdocs_partial/packages/templates/docs-package/package/plugin.py.j2 +5 -0
  20. mkdocs_partial/packages/templates/site-package/dist-info/METADATA.j2 +7 -0
  21. mkdocs_partial/packages/templates/site-package/dist-info/WHEEL.j2 +4 -0
  22. mkdocs_partial/packages/templates/site-package/dist-info/entry_points.txt.j2 +5 -0
  23. mkdocs_partial/packages/templates/site-package/package/__init__.py.j2 +3 -0
  24. mkdocs_partial/packages/templates/site-package/package/__main__.py.j2 +4 -0
  25. mkdocs_partial/packages/templates/site-package/package/__pyinstaller/__init__.py.j2 +5 -0
  26. mkdocs_partial/packages/templates/site-package/package/__pyinstaller/hook-{{module_name}}.py.j2 +30 -0
  27. mkdocs_partial/packages/templates/site-package/package/entry_point.py.j2 +11 -0
  28. mkdocs_partial/partial_docs_plugin.py +92 -0
  29. mkdocs_partial/site_entry_point.py +151 -0
  30. mkdocs_partial/templating/__init__.py +5 -0
  31. mkdocs_partial/templating/markdown_extension.py +38 -0
  32. mkdocs_partial/templating/templater.py +76 -0
  33. mkdocs_partial/templating/templater_extension.py +17 -0
  34. mkdocs_partial/templating/version.py +5 -0
  35. mkdocs_partial/version.py +1 -0
  36. mkdocs_partial-1.2.1.dist-info/LICENSE +19 -0
  37. mkdocs_partial-1.2.1.dist-info/METADATA +31 -0
  38. mkdocs_partial-1.2.1.dist-info/RECORD +41 -0
  39. mkdocs_partial-1.2.1.dist-info/WHEEL +5 -0
  40. mkdocs_partial-1.2.1.dist-info/entry_points.txt +6 -0
  41. mkdocs_partial-1.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ import re
2
+
3
+ from mkdocs_partial.mkdcos_helpers import replace_mkdocs_plugin_entrypoint
4
+
5
+ PACKAGE_NAME_RESTRICTED_CHARS = re.compile(r"[^A-Za-z0-9+_-]")
6
+ MODULE_NAME_RESTRICTED_CHARS = re.compile(r"[^a-z0-9+_]")
7
+ PACKAGE_NAME = re.compile(r"^[A-Za-z0-9+_-]+$")
8
+
9
+ SPELLCHECK_ENTRYPOINT_NAME = "spellcheck"
10
+ SPELLCHECK_ENTRYPOINT_VALUE = "mkdocs_spellcheck.plugin:SpellCheckPlugin"
11
+ SPELLCHECK_ENTRYPOINT_SHIM = "mkdocs_partial.integrations.spellcheck_plugin_shim:SpellCheckShim"
12
+ SpellCheckShimActive = False # pylint: disable=invalid-name
13
+
14
+ MACROS_ENTRYPOINT_NAME = "macros"
15
+ MACROS_ENTRYPOINT_VALUE = "mkdocs_macros.plugin:MacrosPlugin"
16
+ MACROS_ENTRYPOINT_SHIM = "mkdocs_partial.integrations.macros_plugin_shim:MacrosPluginShim"
17
+
18
+ REDIRECTS_ENTRYPOINT_NAME = "redirects"
19
+ REDIRECTS_ENTRYPOINT_VALUE = "mkdocs_redirects.plugin:RedirectPlugin"
20
+ REDIRECTS_ENTRYPOINT_SHIM = "mkdocs_partial.integrations.redirect_plugin_shim:RedirectPluginShim"
21
+
22
+ replace_mkdocs_plugin_entrypoint(SPELLCHECK_ENTRYPOINT_NAME, SPELLCHECK_ENTRYPOINT_VALUE, SPELLCHECK_ENTRYPOINT_SHIM)
23
+ replace_mkdocs_plugin_entrypoint(REDIRECTS_ENTRYPOINT_NAME, REDIRECTS_ENTRYPOINT_VALUE, REDIRECTS_ENTRYPOINT_SHIM)
24
+ replace_mkdocs_plugin_entrypoint(MACROS_ENTRYPOINT_NAME, MACROS_ENTRYPOINT_VALUE, MACROS_ENTRYPOINT_SHIM)
@@ -0,0 +1,14 @@
1
+ import os
2
+ from argparse import ArgumentTypeError
3
+
4
+
5
+ def directory(value):
6
+ if not os.path.isdir(value):
7
+ raise ArgumentTypeError("Must be an existing directory")
8
+ return value
9
+
10
+
11
+ def file(value):
12
+ if not os.path.isfile(value):
13
+ raise ArgumentTypeError("Must be an existing file")
14
+ return value
@@ -0,0 +1,224 @@
1
+ # pylint: disable=unused-argument
2
+ from __future__ import annotations
3
+
4
+ import glob
5
+ import inspect
6
+ import logging
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ import frontmatter
13
+ from mkdocs import plugins
14
+ from mkdocs.config import Config, config_options
15
+ from mkdocs.config.defaults import MkDocsConfig
16
+ from mkdocs.livereload import LiveReloadServer
17
+ from mkdocs.plugins import BasePlugin, PrefixedLogger, get_plugin_logger
18
+ from mkdocs.structure.files import File, Files
19
+ from mkdocs.structure.nav import Navigation
20
+ from mkdocs.structure.pages import Page
21
+ from mkdocs.utils.templates import TemplateContext
22
+
23
+ import mkdocs_partial
24
+ from mkdocs_partial import (
25
+ MACROS_ENTRYPOINT_NAME,
26
+ MACROS_ENTRYPOINT_SHIM,
27
+ REDIRECTS_ENTRYPOINT_NAME,
28
+ REDIRECTS_ENTRYPOINT_SHIM,
29
+ SPELLCHECK_ENTRYPOINT_NAME,
30
+ SPELLCHECK_ENTRYPOINT_SHIM,
31
+ )
32
+ from mkdocs_partial.integrations.material_blog_integration import MaterialBlogsIntegration
33
+ from mkdocs_partial.mkdcos_helpers import get_mkdocs_plugin, get_mkdocs_plugin_name, normalize_path
34
+
35
+
36
+ class DocsPackagePluginConfig(Config):
37
+ enabled = config_options.Type(bool, default=True)
38
+ docs_path = config_options.Optional(config_options.Type(str))
39
+ directory = config_options.Optional(config_options.Type(str))
40
+ edit_url_template = config_options.Optional(config_options.Type(str))
41
+ name = config_options.Optional(config_options.Type(str))
42
+ blog_categories = config_options.Optional(config_options.Type(str))
43
+
44
+
45
+ class DocsPackagePlugin(BasePlugin[DocsPackagePluginConfig]):
46
+ supports_multiple_instances = True
47
+ H1_TITLE = re.compile(r"^#[^#]", flags=re.MULTILINE)
48
+ TITLE = re.compile(r"^#", flags=re.MULTILINE)
49
+
50
+ @property
51
+ def directory(self):
52
+ return self.__directory
53
+
54
+ def __init__(self, directory=None, edit_url_template=None, title=None, blog_categories=None):
55
+ self.__title = title
56
+ script_dir = os.path.dirname(os.path.realpath(inspect.getfile(self.__class__)))
57
+ self.__docs_path = os.path.join(script_dir, "docs")
58
+ self.__directory = directory
59
+ self.__edit_url_template = edit_url_template
60
+ self.__files: list[File] = []
61
+ self.__blog_integration = MaterialBlogsIntegration()
62
+ self.__plugin_name = ""
63
+ self.__log = get_plugin_logger("partial_docs")
64
+ self.__blog_categories = blog_categories
65
+ if self.__blog_categories is None:
66
+ self.__blog_categories = self.__title
67
+ if self.__blog_categories is None:
68
+ self.__blog_categories = self.__directory
69
+
70
+ def on_startup(self, *, command, dirty):
71
+ # Mkdocs handles plugins with on_startup singletons
72
+ pass
73
+
74
+ def on_shutdown(self) -> None:
75
+ # Disable shin in case mkdocs is rebuilding without doc_package plugins enabled
76
+ mkdocs_partial.SpellCheckShimActive = False
77
+ self.__blog_integration.shutdown()
78
+
79
+ @plugins.event_priority(100)
80
+ def on_pre_build(self, *, config: MkDocsConfig) -> None:
81
+ self.__blog_integration.sync()
82
+
83
+ @plugins.event_priority(-100)
84
+ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
85
+ if not self.config.enabled:
86
+ self.__blog_integration.stop()
87
+ return
88
+ if self.config.docs_path is not None:
89
+ self.__docs_path = self.config.docs_path
90
+
91
+ if self.config.directory is not None:
92
+ self.__directory = self.config.directory
93
+ if self.__directory is None:
94
+ self.__directory = ""
95
+ self.__directory = self.__directory.rstrip("/")
96
+
97
+ if self.config.name is not None:
98
+ self.__plugin_name = self.config.name
99
+ else:
100
+ self.__plugin_name = get_mkdocs_plugin_name(self, config)
101
+
102
+ logger = logging.getLogger(f"mkdocs.plugins.{__name__}")
103
+ self.__log = PrefixedLogger(f"partial_docs[{self.__plugin_name}]", logger)
104
+
105
+ if self.config.edit_url_template is not None:
106
+ self.__edit_url_template = self.config.edit_url_template
107
+ if self.config.blog_categories is not None:
108
+ self.__blog_categories = self.config.blog_categories
109
+
110
+ self.__blog_integration.init(config, self.__docs_path, self.__plugin_name, self.__blog_categories)
111
+
112
+ spellcheck_plugin = get_mkdocs_plugin(SPELLCHECK_ENTRYPOINT_NAME, SPELLCHECK_ENTRYPOINT_SHIM, config)
113
+ if spellcheck_plugin is not None and not mkdocs_partial.SpellCheckShimActive:
114
+ self.__log.info("Enabling `mkdocs_spellcheck` integration.")
115
+ mkdocs_partial.SpellCheckShimActive = True
116
+
117
+ macros_plugin = get_mkdocs_plugin(MACROS_ENTRYPOINT_NAME, MACROS_ENTRYPOINT_SHIM, config)
118
+ if macros_plugin is not None:
119
+ self.__log.info("Detected configured mkdocs_macros plugin. Registering filters")
120
+ macros_plugin.register_docs_package(self.__plugin_name, self)
121
+
122
+ def on_serve(
123
+ self, server: LiveReloadServer, /, *, config: MkDocsConfig, builder: Callable
124
+ ) -> LiveReloadServer | None:
125
+ if not self.config.enabled:
126
+ return server
127
+
128
+ if self.config.docs_path is not None:
129
+ if not self.__blog_integration.watch(server, config):
130
+ server.watch(self.config.docs_path)
131
+ return server
132
+
133
+ def on_files(self, files: Files, /, *, config: MkDocsConfig) -> Files | None:
134
+ if not self.config.enabled:
135
+ return files
136
+
137
+ self.__files = []
138
+ if not os.path.isdir(self.__docs_path):
139
+ return files
140
+
141
+ for file_path in glob.glob(os.path.join(self.__docs_path, "**/*.md"), recursive=True):
142
+ self.add_md_file(file_path, files, config)
143
+ for file_path in glob.glob(os.path.join(self.__docs_path, "**/*.png"), recursive=True):
144
+ self.add_media_file(file_path, files, config)
145
+
146
+ if mkdocs_partial.SpellCheckShimActive:
147
+ known_words = os.path.join(self.__docs_path, "known_words.txt")
148
+ if os.path.isfile(known_words):
149
+ self.add_media_file(known_words, files, config)
150
+
151
+ return files
152
+
153
+ def add_md_file(self, file_path, files: Files, config):
154
+ if self.__blog_integration.is_blog_related(file_path):
155
+ return
156
+
157
+ md = frontmatter.loads(Path(file_path).read_text(encoding="utf8"))
158
+ src_uri, is_index = self.get_src_uri(file_path)
159
+ existing_file = files.src_uris.get(src_uri, None)
160
+ if existing_file is not None:
161
+ existing = frontmatter.loads(existing_file.content_string)
162
+ content = existing.content + "\n\n" + md.content
163
+ if len(self.H1_TITLE.findall(content)) > 1:
164
+ content = self.TITLE.sub("##", content)
165
+
166
+ meta = dict(existing.metadata)
167
+ meta.update(md.metadata)
168
+ md = frontmatter.Post(content)
169
+ md.metadata.update(meta)
170
+ files.remove(existing_file)
171
+ if is_index and self.__title is not None:
172
+ md.metadata["title"] = self.__title
173
+ md.metadata["partial"] = True
174
+ md.metadata["docs_package"] = self.__plugin_name
175
+ file = File.generated(config=config, src_uri=src_uri, content=frontmatter.dumps(md))
176
+ files.append(file)
177
+ self.__files.append(file)
178
+
179
+ redirects_plugin = get_mkdocs_plugin(REDIRECTS_ENTRYPOINT_NAME, REDIRECTS_ENTRYPOINT_SHIM, config)
180
+ if redirects_plugin is not None:
181
+ normalized_redirects = [
182
+ f"{self.directory}/{redirect}".replace("\\", "/").replace("//", "/")
183
+ for redirect in md.metadata.get("redirects", [])
184
+ ]
185
+ redirects_plugin.add_redirects(files, file, normalized_redirects, config)
186
+
187
+ def on_page_context(
188
+ self, context: TemplateContext, /, *, page: Page, config: MkDocsConfig, nav: Navigation
189
+ ) -> TemplateContext | None:
190
+ if page.file in self.__files:
191
+ path = os.path.relpath(page.file.src_path, self.config.directory)
192
+ path = self.get_edit_url_template_path(path)
193
+ else:
194
+ path = self.__blog_integration.get_src_path(page.file.src_path)
195
+ if self.__edit_url_template is not None and path is not None:
196
+ page.edit_url = str(self.__edit_url_template).format(path=path)
197
+ return context
198
+
199
+ def add_media_file(self, path, files, config):
200
+ if self.__blog_integration.is_blog_related(path):
201
+ return
202
+ src_uri = self.get_src_uri(path)[0]
203
+ existing_file = files.src_uris.get(src_uri, None)
204
+ if existing_file is not None:
205
+ plugin_info = ""
206
+ if existing_file.generated_by is not None:
207
+ plugin_info = f"registered by '{existing_file.generated_by}' plugin"
208
+ self.__log.warning(
209
+ f"Can not register file '{src_uri}' as there is already file with same path.{plugin_info}"
210
+ )
211
+ return
212
+ file = File.generated(config=config, src_uri=src_uri, content=Path(path).read_bytes())
213
+ files.append(file)
214
+
215
+ def get_src_uri(self, file_path):
216
+ is_index = False
217
+ path = normalize_path(os.path.relpath(file_path, self.__docs_path))
218
+ if path.lower() == "index.md":
219
+ is_index = True
220
+ path = normalize_path(os.path.join(self.__directory, path)).lstrip("/")
221
+ return path, is_index
222
+
223
+ def get_edit_url_template_path(self, path):
224
+ return normalize_path(os.path.relpath(path, self.__directory))
@@ -0,0 +1,199 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from argparse import ArgumentParser, ArgumentTypeError
5
+
6
+ from mkdocs_partial import PACKAGE_NAME, PACKAGE_NAME_RESTRICTED_CHARS
7
+ from mkdocs_partial.argparse_types import directory, file
8
+ from mkdocs_partial.packages.packager import Packager
9
+ from mkdocs_partial.version import __version__
10
+
11
+
12
+ def package_name(value):
13
+ if not PACKAGE_NAME.match(value):
14
+ raise ArgumentTypeError(f"'{value}' is invalid package name")
15
+ return value
16
+
17
+
18
+ def run():
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format="{asctime} [{levelname}] {message}",
22
+ style="{",
23
+ )
24
+ parser = ArgumentParser(description=f"v{__version__}", prog="mkdocs-partial")
25
+ subparsers = parser.add_subparsers(help="commands")
26
+ package_command = add_command_parser(
27
+ subparsers, "package", "Creates partial documentation package from directory", func=package
28
+ )
29
+
30
+ add_packager_args(
31
+ package_command,
32
+ package_name_extra_help=" Default - normalized `--directory` value directory name.",
33
+ output_dir_extra_help=" Default - `--source-dir` value directory name.",
34
+ )
35
+ package_command.add_argument(
36
+ "--directory",
37
+ required=False,
38
+ help="Path in target documentation to inject documentation, relative to mkdocs `doc_dir`. "
39
+ "Pass empty string to inject files directly to mkdocs `docs_dir`"
40
+ "Default - `--source-dir` value directory name",
41
+ )
42
+ package_command.add_argument(
43
+ "--title",
44
+ required=False,
45
+ help="Override title if defined in package root index.md",
46
+ )
47
+ package_command.add_argument(
48
+ "--blog-categories",
49
+ required=False,
50
+ default="",
51
+ help="`/` separated list of categories to be prepended to defined in blog posts of the package."
52
+ "Empty by default",
53
+ )
54
+ package_command.add_argument(
55
+ "--edit-url-template",
56
+ required=False,
57
+ help="f-string template for page edit url with {path} as placeholder for markdown file path "
58
+ "relative to directory from --docs-dir",
59
+ )
60
+
61
+ site_package_command = add_command_parser(
62
+ subparsers, "site-package", "Creates documentation site-package package from directory", func=site_package
63
+ )
64
+
65
+ add_packager_args(
66
+ site_package_command,
67
+ package_name_extra_help=" Default - `--source-dir` value directory name.",
68
+ output_dir_extra_help=" Default - `--source-dir` value directory name.",
69
+ )
70
+
71
+ freeze_command = add_command_parser(
72
+ subparsers, "freeze", "Pins doc package versions in requirements.txt to currently installed", func=freeze
73
+ )
74
+ freeze_command.add_argument(
75
+ "path",
76
+ type=file,
77
+ help="path to requirements.txt",
78
+ )
79
+
80
+ args = parser.parse_args()
81
+
82
+ if not hasattr(args, "func"):
83
+ parser.print_help()
84
+ sys.exit(0)
85
+ try:
86
+ success, message = args.func(args)
87
+ if not success:
88
+ print(message, file=sys.stderr)
89
+ elif message is not None:
90
+ print(message)
91
+ except Exception as e: # pylint: disable=broad-exception-caught
92
+ logging.exception(f"FAIL! {e}")
93
+ success = False
94
+
95
+ sys.exit(0 if success else 1)
96
+
97
+
98
+ def add_command_parser(subparsers, name, help_text, func):
99
+ command_parser = subparsers.add_parser(name, help=help_text)
100
+ command_parser.set_defaults(func=func, commandName=name)
101
+ return command_parser
102
+
103
+
104
+ def add_packager_args(
105
+ parser,
106
+ source_dir_help="Directory to be packaged. Default - current directory",
107
+ package_name_extra_help="",
108
+ output_dir_extra_help="",
109
+ ):
110
+ parser.add_argument(
111
+ "--source-dir",
112
+ required=False,
113
+ default=os.getcwd(),
114
+ type=directory,
115
+ help=source_dir_help,
116
+ )
117
+ parser.add_argument(
118
+ "--package-name",
119
+ required=False,
120
+ type=package_name,
121
+ help=f"Name of the package to build. {package_name_extra_help}",
122
+ )
123
+ parser.add_argument("--package-version", required=True, help="Version of the package to build")
124
+ parser.add_argument("--package-description", required=False, help="Description of the package to build")
125
+ parser.add_argument(
126
+ "--output-dir",
127
+ required=False,
128
+ type=directory,
129
+ help=f"Directory to write generated package file.{output_dir_extra_help}",
130
+ )
131
+ parser.add_argument(
132
+ "--exclude",
133
+ action="append",
134
+ required=False,
135
+ default=[],
136
+ help="Exclude glob (should be relative to directory provided with `--source-dir` ",
137
+ )
138
+ parser.add_argument(
139
+ "--freeze",
140
+ dest="freeze",
141
+ action="store_true",
142
+ help="Pin doc package versions in requirements.txt to currently installed."
143
+ " (if there is no requirements.txt in `--source-dir` directory, has no effect)",
144
+ )
145
+
146
+
147
+ def package(args):
148
+ if args.directory is None:
149
+ args.directory = os.path.basename(args.source_dir)
150
+ if args.output_dir is None:
151
+ args.output_dir = args.source_dir
152
+ if args.package_name is None:
153
+ args.package_name = PACKAGE_NAME_RESTRICTED_CHARS.sub("-", args.directory.lower())
154
+
155
+ Packager("docs-package").pack(
156
+ package_name=args.package_name,
157
+ package_version=args.package_version,
158
+ package_description=args.package_description,
159
+ resources_src_dir=args.source_dir,
160
+ output_dir=args.output_dir,
161
+ resources_package_dir="docs",
162
+ requirements_path="requirements.txt",
163
+ freeze=args.freeze,
164
+ excludes=["requirements.txt", "requirements.txt.j2"] + args.exclude,
165
+ directory="None" if args.directory is None else f'"{args.directory}"',
166
+ edit_url_template="None" if args.edit_url_template is None else f'"{args.edit_url_template}"',
167
+ title="None" if args.title is None else f'"{args.title}"',
168
+ blog_categories="None" if args.blog_categories is None else f'"{args.blog_categories}"',
169
+ )
170
+ return True, None
171
+
172
+
173
+ def site_package(args):
174
+ if args.output_dir is None:
175
+ args.output_dir = args.source_dir
176
+ if args.package_name is None:
177
+ args.package_name = PACKAGE_NAME_RESTRICTED_CHARS.sub("-", os.path.basename(args.source_dir).lower())
178
+
179
+ Packager("site-package").pack(
180
+ package_name=args.package_name,
181
+ package_version=args.package_version,
182
+ package_description=args.package_description,
183
+ resources_src_dir=args.source_dir,
184
+ output_dir=args.output_dir,
185
+ resources_package_dir="site",
186
+ requirements_path="requirements.txt",
187
+ freeze=args.freeze,
188
+ excludes=["requirements.txt", "requirements.txt.j2"] + args.exclude,
189
+ )
190
+ return True, None
191
+
192
+
193
+ def freeze(args):
194
+ Packager.freeze(args.__path)
195
+ return True, None
196
+
197
+
198
+ if __name__ == "__main__":
199
+ run()
File without changes
@@ -0,0 +1,36 @@
1
+ import os.path
2
+ from typing import Dict
3
+
4
+ from mkdocs.config.defaults import MkDocsConfig
5
+ from mkdocs_macros.plugin import MacrosPlugin # pylint: disable=import-error
6
+
7
+ from mkdocs_partial.docs_package_plugin import DocsPackagePlugin
8
+
9
+
10
+ # NOTE: has to be replaced with register_filters implementation in PartialDocsPlugin
11
+ # once https://github.com/fralau/mkdocs-macros-plugin/issues/237 is released
12
+ class MacrosPluginShim(MacrosPlugin):
13
+ def __init__(self):
14
+ super().__init__()
15
+ self.__docs_packages: Dict[str, DocsPackagePlugin] = {}
16
+
17
+ def register_docs_package(self, name: str, package: DocsPackagePlugin):
18
+ self.__docs_packages[name] = package
19
+
20
+ def package_link(self, value, name: str = None):
21
+ page = self.page
22
+ if name is None:
23
+ name = page.meta.get("docs_package", None)
24
+
25
+ package = self.__docs_packages.get(name, None)
26
+ if package is not None:
27
+ url = os.path.relpath(f"{package.directory}/{value}", os.path.dirname(page.file.src_path))
28
+ url = url.replace("\\", "/")
29
+ return url
30
+ if name is None:
31
+ raise LookupError("`package_link` may be used only on pages managed with `docs_package` plugin")
32
+ raise LookupError(f"Package {name} is not installed")
33
+
34
+ def on_config(self, config: MkDocsConfig):
35
+ self.filter(self.package_link)
36
+ return super().on_config(config)
@@ -0,0 +1,149 @@
1
+ import filecmp
2
+ import glob
3
+ import os
4
+ import posixpath
5
+ import shutil
6
+ from abc import ABC
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ import frontmatter
11
+ import watchdog.events
12
+ from mkdocs.config.defaults import MkDocsConfig
13
+ from mkdocs.livereload import LiveReloadServer
14
+
15
+ from mkdocs_partial.mkdcos_helpers import get_mkdocs_plugin, mkdocs_watch_ignore_path
16
+
17
+
18
+ class MaterialBlogsIntegration(ABC):
19
+ def __init__(self):
20
+ super().__init__()
21
+ self.__enabled: bool = False
22
+
23
+ self.__partial: str | None = None
24
+ self.__source: str | None = None
25
+ self.__target: str | None = None
26
+ self.__categories: list[str] = []
27
+ self.__docs_path: str | None = None
28
+ self.__docs_dir: str | None = None
29
+ self.__stop = lambda *args: None
30
+
31
+ def init(self, config: MkDocsConfig, docs_path: str, name: str, categories: str = ""):
32
+ blog_plugin = get_mkdocs_plugin("material/blog", "material.plugins.blog.plugin:BlogPlugin", config)
33
+ self.__enabled = blog_plugin is not None
34
+ if self.__enabled:
35
+ root = posixpath.normpath(blog_plugin.config.data.get("blog_dir", "blog"))
36
+ blog_posts = blog_plugin.config.data.get("post_dir", "{blog}/posts").format(blog=root)
37
+ self.__source = os.path.join(docs_path, blog_posts)
38
+ self.__docs_dir = config.docs_dir
39
+ self.__partial = os.path.join(self.__docs_dir, blog_posts, "partial")
40
+ self.__target = os.path.join(self.__partial, name)
41
+ self.__categories = [] if categories == "" else categories.split("/")
42
+ self.__docs_path = docs_path
43
+ return self.__enabled
44
+
45
+ def watch(self, server: LiveReloadServer, config: MkDocsConfig):
46
+ if not self.__enabled:
47
+ return False
48
+ mkdocs_watch_ignore_path(server, config, self.__source, self.__docs_path)
49
+
50
+ def blogs_callback(event: watchdog.events.FileSystemEvent):
51
+ # ignore directory events - teh do not affect blogs
52
+ if event.is_directory:
53
+ return
54
+
55
+ # ignore events for files out of self.__source, likely self.__source was created after
56
+ # watch started and watched dir its parent
57
+ if not (event.src_path is not None and Path(event.src_path).is_relative_to(self.__source)) and not (
58
+ event.dest_path is not None and Path(event.dest_path).is_relative_to(self.__source)
59
+ ):
60
+ return
61
+
62
+ self.sync()
63
+
64
+ handler = watchdog.events.FileSystemEventHandler()
65
+ handler.on_any_event = blogs_callback # type: ignore[method-assign]
66
+
67
+ # If source dir does not exist get up the tree in case it would be created later
68
+ source_watch_dir = self.__source
69
+ while not os.path.isdir(source_watch_dir) and source_watch_dir is not None:
70
+ if os.path.dirname(source_watch_dir) != source_watch_dir:
71
+ source_watch_dir = os.path.dirname(source_watch_dir)
72
+ else:
73
+ source_watch_dir = None
74
+
75
+ if source_watch_dir is not None:
76
+ watch = server.observer.schedule(handler, source_watch_dir, recursive=True)
77
+
78
+ def unsubscribe():
79
+ try:
80
+ server.observer.unschedule(watch)
81
+ except KeyError:
82
+ # At the moment mkdocs unschedules all watches,
83
+ # unsubscribe is just in case it changes in later releases
84
+ pass
85
+ self.__stop = lambda *args: None
86
+
87
+ self.__stop = unsubscribe
88
+ return True
89
+
90
+ def sync(self):
91
+ if not self.__enabled:
92
+ return
93
+ posts = []
94
+ for file_path in glob.glob(os.path.join(self.__source, "**/*.md"), recursive=True):
95
+ if os.path.isfile(file_path):
96
+ md = frontmatter.loads(Path(file_path).read_text(encoding="utf8"))
97
+ abs_path = os.path.join(self.__target, os.path.relpath(file_path, self.__source))
98
+ Path(os.path.dirname(abs_path)).mkdir(parents=True, exist_ok=True)
99
+ categories: List[str] = md.metadata.setdefault("categories", [])
100
+ if not isinstance(categories, list):
101
+ md.metadata["categories"] = self.__categories
102
+ else:
103
+ md.metadata["categories"] = self.__categories + categories
104
+ if not os.path.isfile(abs_path) or Path(abs_path).read_text(encoding="utf8") != frontmatter.dumps(md):
105
+ frontmatter.dump(md, abs_path)
106
+ posts.append(os.path.normpath(abs_path))
107
+
108
+ for file_path in glob.glob(os.path.join(self.__target, "**/*.md"), recursive=True):
109
+ if os.path.isfile(file_path):
110
+ if os.path.normpath(file_path) not in posts:
111
+ os.remove(file_path)
112
+
113
+ media = []
114
+ for file_path in glob.glob(os.path.join(self.__source, "**/*.png"), recursive=True):
115
+ if os.path.isfile(file_path):
116
+ abs_path = os.path.join(self.__target, os.path.relpath(file_path, self.__source))
117
+ if not os.path.isfile(abs_path) or not filecmp.cmp(abs_path, file_path):
118
+ shutil.copyfile(file_path, abs_path)
119
+ media.append(os.path.normpath(abs_path))
120
+ for file_path in glob.glob(os.path.join(self.__target, "**/*.png"), recursive=True):
121
+ if os.path.isfile(file_path):
122
+ if os.path.normpath(file_path) not in media:
123
+ os.remove(file_path)
124
+
125
+ def is_blog_related(self, path):
126
+ return self.__enabled and Path(path).is_relative_to(self.__source)
127
+
128
+ def shutdown(self):
129
+ if not self.__enabled:
130
+ return
131
+ self.stop()
132
+
133
+ def get_src_path(self, path):
134
+ if not self.__enabled:
135
+ return None
136
+ path = os.path.join(self.__docs_dir, path)
137
+ if Path(path).is_relative_to(self.__target):
138
+ path = os.path.join(self.__source, os.path.relpath(path, self.__target))
139
+ path = os.path.relpath(path, self.__docs_path)
140
+ return path
141
+ return None
142
+
143
+ def stop(self):
144
+ self.__stop()
145
+ if self.__enabled:
146
+ shutil.rmtree(self.__target, ignore_errors=True)
147
+ if os.path.isdir(self.__partial) and not os.listdir(self.__partial):
148
+ shutil.rmtree(self.__partial, ignore_errors=True)
149
+ self.__enabled = False
@@ -0,0 +1,23 @@
1
+ import frontmatter
2
+ from mkdocs import plugins
3
+ from mkdocs.config.defaults import MkDocsConfig
4
+ from mkdocs.structure.files import File, Files, InclusionLevel
5
+ from mkdocs_redirects.plugin import RedirectPlugin # pylint: disable=import-error
6
+
7
+
8
+ class RedirectPluginShim(RedirectPlugin):
9
+
10
+ @plugins.event_priority(-100)
11
+ def on_files(self, files, config, **kwargs):
12
+ return super().on_files(files, config, **kwargs)
13
+
14
+ def add_redirects(self, files: Files, file, redirect_from: list[str], config: MkDocsConfig):
15
+ for redirect in redirect_from:
16
+ self.config.setdefault("redirect_maps", {})[redirect] = file.src_path.replace("\\", "/")
17
+ # Register stub page to avoid warnings about missing link targets
18
+ stub = frontmatter.Post("Redirect")
19
+ stub.metadata["layout"] = "redirect"
20
+ file = File.generated(
21
+ config=config, src_uri=redirect, content=frontmatter.dumps(stub), inclusion=InclusionLevel.EXCLUDED
22
+ )
23
+ files.append(file)