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.
- mkdocs_partial/__init__.py +24 -0
- mkdocs_partial/argparse_types.py +14 -0
- mkdocs_partial/docs_package_plugin.py +224 -0
- mkdocs_partial/entry_point.py +199 -0
- mkdocs_partial/integrations/__init__.py +0 -0
- mkdocs_partial/integrations/macros_plugin_shim.py +36 -0
- mkdocs_partial/integrations/material_blog_integration.py +149 -0
- mkdocs_partial/integrations/redirect_plugin_shim.py +23 -0
- mkdocs_partial/integrations/spellcheck_plugin_shim.py +46 -0
- mkdocs_partial/mkdcos_helpers.py +97 -0
- mkdocs_partial/packages/__init__.py +0 -0
- mkdocs_partial/packages/packager.py +218 -0
- mkdocs_partial/packages/templates/docs-package/dist-info/METADATA.j2 +7 -0
- mkdocs_partial/packages/templates/docs-package/dist-info/WHEEL.j2 +4 -0
- mkdocs_partial/packages/templates/docs-package/dist-info/entry_points.txt.j2 +2 -0
- mkdocs_partial/packages/templates/docs-package/package/__init__.py.j2 +3 -0
- mkdocs_partial/packages/templates/docs-package/package/__pyinstaller/__init__.py.j2 +5 -0
- mkdocs_partial/packages/templates/docs-package/package/__pyinstaller/hook-{{module_name}}.py.j2 +31 -0
- mkdocs_partial/packages/templates/docs-package/package/plugin.py.j2 +5 -0
- mkdocs_partial/packages/templates/site-package/dist-info/METADATA.j2 +7 -0
- mkdocs_partial/packages/templates/site-package/dist-info/WHEEL.j2 +4 -0
- mkdocs_partial/packages/templates/site-package/dist-info/entry_points.txt.j2 +5 -0
- mkdocs_partial/packages/templates/site-package/package/__init__.py.j2 +3 -0
- mkdocs_partial/packages/templates/site-package/package/__main__.py.j2 +4 -0
- mkdocs_partial/packages/templates/site-package/package/__pyinstaller/__init__.py.j2 +5 -0
- mkdocs_partial/packages/templates/site-package/package/__pyinstaller/hook-{{module_name}}.py.j2 +30 -0
- mkdocs_partial/packages/templates/site-package/package/entry_point.py.j2 +11 -0
- mkdocs_partial/partial_docs_plugin.py +92 -0
- mkdocs_partial/site_entry_point.py +151 -0
- mkdocs_partial/templating/__init__.py +5 -0
- mkdocs_partial/templating/markdown_extension.py +38 -0
- mkdocs_partial/templating/templater.py +76 -0
- mkdocs_partial/templating/templater_extension.py +17 -0
- mkdocs_partial/templating/version.py +5 -0
- mkdocs_partial/version.py +1 -0
- mkdocs_partial-1.2.1.dist-info/LICENSE +19 -0
- mkdocs_partial-1.2.1.dist-info/METADATA +31 -0
- mkdocs_partial-1.2.1.dist-info/RECORD +41 -0
- mkdocs_partial-1.2.1.dist-info/WHEEL +5 -0
- mkdocs_partial-1.2.1.dist-info/entry_points.txt +6 -0
- 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)
|