zensical 0.0.3__cp310-abi3-musllinux_1_2_i686.whl → 0.0.12__cp310-abi3-musllinux_1_2_i686.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.
Potentially problematic release.
This version of zensical might be problematic. Click here for more details.
- zensical/__init__.py +6 -6
- zensical/__main__.py +28 -0
- zensical/bootstrap/.github/workflows/docs.yml +10 -3
- zensical/bootstrap/zensical.toml +22 -22
- zensical/config.py +191 -197
- zensical/extensions/__init__.py +2 -2
- zensical/extensions/emoji.py +22 -27
- zensical/extensions/links.py +33 -24
- zensical/extensions/preview.py +29 -41
- zensical/extensions/search.py +83 -83
- zensical/extensions/utilities/__init__.py +2 -2
- zensical/extensions/utilities/filter.py +5 -10
- zensical/main.py +36 -47
- zensical/markdown.py +21 -20
- zensical/templates/assets/javascripts/bundle.21aa498e.min.js +3 -0
- zensical/templates/assets/javascripts/workers/{search.5e1f2129.min.js → search.5df7522c.min.js} +1 -1
- zensical/templates/assets/stylesheets/classic/main.6f483be1.min.css +1 -0
- zensical/templates/assets/stylesheets/modern/main.09f707be.min.css +1 -0
- zensical/templates/base.html +4 -4
- zensical/templates/partials/javascripts/base.html +1 -1
- zensical/templates/partials/nav-item.html +1 -1
- zensical/templates/partials/search.html +3 -1
- zensical/zensical.abi3.so +0 -0
- zensical/zensical.pyi +7 -13
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/METADATA +9 -4
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/RECORD +30 -29
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/WHEEL +1 -1
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/licenses/LICENSE.md +1 -1
- zensical.libs/libgcc_s-f5fcfe20.so.1 +0 -0
- zensical/templates/assets/javascripts/bundle.3c403d54.min.js +0 -3
- zensical/templates/assets/stylesheets/classic/main.c5ffb0a9.min.css +0 -1
- zensical/templates/assets/stylesheets/modern/main.1357c24d.min.css +0 -1
- zensical.libs/libgcc_s-27e5a392.so.1 +0 -0
- {zensical-0.0.3.dist-info → zensical-0.0.12.dist-info}/entry_points.txt +0 -0
zensical/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Copyright (c) Zensical
|
|
1
|
+
# Copyright (c) 2025 Zensical and contributors
|
|
2
2
|
|
|
3
3
|
# SPDX-License-Identifier: MIT
|
|
4
|
-
# Third-party contributions licensed under
|
|
4
|
+
# Third-party contributions licensed under DCO
|
|
5
5
|
|
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
7
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -23,20 +23,26 @@
|
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
import hashlib
|
|
26
27
|
import importlib
|
|
27
28
|
import os
|
|
28
|
-
import
|
|
29
|
-
import
|
|
29
|
+
import pickle
|
|
30
|
+
from typing import IO, Any
|
|
31
|
+
from urllib.parse import urlparse
|
|
30
32
|
|
|
33
|
+
import yaml
|
|
31
34
|
from click import ClickException
|
|
32
35
|
from deepmerge import always_merger
|
|
33
|
-
from functools import partial
|
|
34
|
-
from typing import Any, IO
|
|
35
36
|
from yaml import BaseLoader, Loader, YAMLError
|
|
36
37
|
from yaml.constructor import ConstructorError
|
|
37
|
-
from urllib.parse import urlparse
|
|
38
38
|
|
|
39
|
-
from .extensions.emoji import to_svg, twemoji
|
|
39
|
+
from zensical.extensions.emoji import to_svg, twemoji
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import tomllib
|
|
43
|
+
except ModuleNotFoundError:
|
|
44
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
45
|
+
|
|
40
46
|
|
|
41
47
|
# ----------------------------------------------------------------------------
|
|
42
48
|
# Globals
|
|
@@ -59,9 +65,7 @@ side, and use it directly when needed. It's a hack but will do for now.
|
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
class ConfigurationError(ClickException):
|
|
62
|
-
"""
|
|
63
|
-
Configuration resolution or validation failed.
|
|
64
|
-
"""
|
|
68
|
+
"""Configuration resolution or validation failed."""
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
# ----------------------------------------------------------------------------
|
|
@@ -70,20 +74,17 @@ class ConfigurationError(ClickException):
|
|
|
70
74
|
|
|
71
75
|
|
|
72
76
|
def parse_config(path: str) -> dict:
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
+
"""Parse configuration file."""
|
|
78
|
+
# Decide by extension; no need to convert to Path
|
|
79
|
+
_, ext = os.path.splitext(path)
|
|
80
|
+
if ext.lower() == ".toml":
|
|
77
81
|
return parse_zensical_config(path)
|
|
78
|
-
|
|
79
|
-
return parse_mkdocs_config(path)
|
|
82
|
+
return parse_mkdocs_config(path)
|
|
80
83
|
|
|
81
84
|
|
|
82
85
|
def parse_zensical_config(path: str) -> dict:
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
"""
|
|
86
|
-
global _CONFIG
|
|
86
|
+
"""Parse zensical.toml configuration file."""
|
|
87
|
+
global _CONFIG # noqa: PLW0603
|
|
87
88
|
with open(path, "rb") as f:
|
|
88
89
|
config = tomllib.load(f)
|
|
89
90
|
if "project" in config:
|
|
@@ -95,11 +96,9 @@ def parse_zensical_config(path: str) -> dict:
|
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def parse_mkdocs_config(path: str) -> dict:
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
""
|
|
101
|
-
global _CONFIG
|
|
102
|
-
with open(path, "r") as f:
|
|
99
|
+
"""Parse mkdocs.yml configuration file."""
|
|
100
|
+
global _CONFIG # noqa: PLW0603
|
|
101
|
+
with open(path, encoding="utf-8") as f:
|
|
103
102
|
config = _yaml_load(f)
|
|
104
103
|
|
|
105
104
|
# Apply defaults and return parsed configuration
|
|
@@ -107,17 +106,20 @@ def parse_mkdocs_config(path: str) -> dict:
|
|
|
107
106
|
return _CONFIG
|
|
108
107
|
|
|
109
108
|
|
|
109
|
+
def get_config() -> dict:
|
|
110
|
+
"""Return configuration."""
|
|
111
|
+
# We assume this function is only called after populating `_CONFIG`.
|
|
112
|
+
return _CONFIG # type: ignore[return-value]
|
|
113
|
+
|
|
114
|
+
|
|
110
115
|
def get_theme_dir() -> str:
|
|
111
|
-
"""
|
|
112
|
-
Return the theme directory.
|
|
113
|
-
"""
|
|
116
|
+
"""Return the theme directory."""
|
|
114
117
|
path = os.path.dirname(os.path.abspath(__file__))
|
|
115
118
|
return os.path.join(path, "templates")
|
|
116
119
|
|
|
117
120
|
|
|
118
121
|
def _apply_defaults(config: dict, path: str) -> dict:
|
|
119
|
-
"""
|
|
120
|
-
Apply default settings in configuration.
|
|
122
|
+
"""Apply default settings in configuration.
|
|
121
123
|
|
|
122
124
|
Note that this is loosely based on the defaults that MkDocs sets in its own
|
|
123
125
|
configuration system, which we won't port for compatibility right now, as
|
|
@@ -131,13 +133,13 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
131
133
|
raise ConfigurationError("Missing required setting: site_name")
|
|
132
134
|
|
|
133
135
|
# Set site directory
|
|
134
|
-
config
|
|
135
|
-
if ".." in config.get("site_dir"):
|
|
136
|
+
set_default(config, "site_dir", "site", str)
|
|
137
|
+
if ".." in config.get("site_dir", ""):
|
|
136
138
|
raise ConfigurationError("site_dir must not contain '..'")
|
|
137
139
|
|
|
138
140
|
# Set docs directory
|
|
139
|
-
config
|
|
140
|
-
if ".." in config.get("docs_dir"):
|
|
141
|
+
set_default(config, "docs_dir", "docs", str)
|
|
142
|
+
if ".." in config.get("docs_dir", ""):
|
|
141
143
|
raise ConfigurationError("docs_dir must not contain '..'")
|
|
142
144
|
|
|
143
145
|
# Set defaults for core settings
|
|
@@ -155,20 +157,26 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
155
157
|
set_default(config, "edit_uri", None, str)
|
|
156
158
|
|
|
157
159
|
# Set defaults for repository name settings
|
|
160
|
+
docs_dir = config.get("docs_dir")
|
|
161
|
+
repo_names = {
|
|
162
|
+
"github.com": "GitHub",
|
|
163
|
+
"gitlab.com": "Gitlab",
|
|
164
|
+
"bitbucket.org": "Bitbucket"
|
|
165
|
+
}
|
|
166
|
+
edit_uris = {
|
|
167
|
+
"github.com": f"edit/master/{docs_dir}",
|
|
168
|
+
"gitlab.com": f"edit/master/{docs_dir}",
|
|
169
|
+
"bitbucket.org": f"src/default/{docs_dir}"
|
|
170
|
+
}
|
|
158
171
|
repo_url = config.get("repo_url")
|
|
159
|
-
if repo_url
|
|
172
|
+
if repo_url:
|
|
160
173
|
host = urlparse(repo_url).hostname or ""
|
|
161
|
-
if host
|
|
162
|
-
config
|
|
163
|
-
config["edit_uri"] = "edit/master/docs"
|
|
164
|
-
elif host == "gitlab.com":
|
|
165
|
-
config["repo_name"] = "GitLab"
|
|
166
|
-
config["edit_uri"] = "edit/master/docs"
|
|
167
|
-
elif host == "bitbucket.org":
|
|
168
|
-
config["repo_name"] = "Bitbucket"
|
|
169
|
-
config["edit_uri"] = "src/default/docs"
|
|
174
|
+
if not config.get("repo_name") and host in repo_names:
|
|
175
|
+
set_default(config, "repo_name", repo_names[host], str)
|
|
170
176
|
elif host:
|
|
171
177
|
config["repo_name"] = host.split(".")[0].title()
|
|
178
|
+
if host in edit_uris:
|
|
179
|
+
set_default(config, "edit_uri", edit_uris[host], str)
|
|
172
180
|
|
|
173
181
|
# Remove trailing slash from edit_uri if present
|
|
174
182
|
edit_uri = config.get("edit_uri")
|
|
@@ -176,7 +184,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
176
184
|
config["edit_uri"] = edit_uri.rstrip("/")
|
|
177
185
|
|
|
178
186
|
# Set defaults for theme font settings
|
|
179
|
-
theme = config
|
|
187
|
+
theme = set_default(config, "theme", {}, dict)
|
|
180
188
|
if isinstance(theme, str):
|
|
181
189
|
theme = {"name": theme}
|
|
182
190
|
config["theme"] = theme
|
|
@@ -212,7 +220,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
212
220
|
set_default(theme["font"], "code", font["code"], str)
|
|
213
221
|
|
|
214
222
|
# Set defaults for theme icons
|
|
215
|
-
icon = theme
|
|
223
|
+
icon = set_default(theme, "icon", {}, dict)
|
|
216
224
|
set_default(icon, "repo", None, str)
|
|
217
225
|
set_default(icon, "annotation", None, str)
|
|
218
226
|
set_default(icon, "tag", {}, dict)
|
|
@@ -242,7 +250,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
242
250
|
set_default(icon, "next", None, str)
|
|
243
251
|
|
|
244
252
|
# Set defaults for theme admonition icons
|
|
245
|
-
admonition = icon
|
|
253
|
+
admonition = set_default(icon, "admonition", {}, dict)
|
|
246
254
|
set_default(admonition, "note", None, str)
|
|
247
255
|
set_default(admonition, "abstract", None, str)
|
|
248
256
|
set_default(admonition, "info", None, str)
|
|
@@ -277,73 +285,13 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
277
285
|
set_default(toggle, "name", None, str)
|
|
278
286
|
|
|
279
287
|
# Set defaults for extra settings
|
|
280
|
-
extra
|
|
281
|
-
|
|
282
|
-
set_default(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
288
|
+
if "extra" in config and not isinstance(config["extra"], dict):
|
|
289
|
+
raise ConfigurationError("The 'extra' setting must be a mapping/dictionary.")
|
|
290
|
+
extra = set_default(config, "extra", {}, dict)
|
|
291
|
+
|
|
292
|
+
if "polyfills" in extra and not isinstance(extra["polyfills"], list):
|
|
293
|
+
raise ConfigurationError("The 'extra.polyfills' setting must be a list.")
|
|
286
294
|
set_default(extra, "polyfills", [], list)
|
|
287
|
-
set_default(extra, "analytics", None, dict)
|
|
288
|
-
|
|
289
|
-
# Set defaults for extra analytics settings
|
|
290
|
-
analytics = extra.get("analytics")
|
|
291
|
-
if analytics:
|
|
292
|
-
set_default(analytics, "provider", None, str)
|
|
293
|
-
set_default(analytics, "property", None, str)
|
|
294
|
-
set_default(analytics, "feedback", None, dict)
|
|
295
|
-
|
|
296
|
-
# Set defaults for extra analytics feedback settings
|
|
297
|
-
feedback = analytics.get("feedback")
|
|
298
|
-
if feedback:
|
|
299
|
-
set_default(feedback, "title", None, str)
|
|
300
|
-
set_default(feedback, "ratings", [], list)
|
|
301
|
-
|
|
302
|
-
# Set defaults for each rating entry
|
|
303
|
-
ratings = feedback.setdefault("ratings", [])
|
|
304
|
-
for entry in ratings:
|
|
305
|
-
set_default(entry, "icon", None, str)
|
|
306
|
-
set_default(entry, "name", None, str)
|
|
307
|
-
set_default(entry, "data", None, str)
|
|
308
|
-
set_default(entry, "note", None, str)
|
|
309
|
-
|
|
310
|
-
# Set defaults for extra consent settings
|
|
311
|
-
consent = extra.setdefault("consent", None)
|
|
312
|
-
if consent:
|
|
313
|
-
set_default(consent, "title", None, str)
|
|
314
|
-
set_default(consent, "description", None, str)
|
|
315
|
-
set_default(consent, "actions", [], list)
|
|
316
|
-
|
|
317
|
-
# Set defaults for extra consent cookie settings
|
|
318
|
-
cookies = consent.setdefault("cookies", {})
|
|
319
|
-
for key, value in cookies.items():
|
|
320
|
-
if isinstance(value, str):
|
|
321
|
-
cookies[key] = {"name": value, "checked": False}
|
|
322
|
-
|
|
323
|
-
# Set defaults for each cookie entry
|
|
324
|
-
set_default(cookies[key], "name", None, str)
|
|
325
|
-
set_default(cookies[key], "checked", False, bool)
|
|
326
|
-
|
|
327
|
-
# Set defaults for extra social settings
|
|
328
|
-
social = extra.setdefault("social", [])
|
|
329
|
-
for entry in social:
|
|
330
|
-
set_default(entry, "icon", None, str)
|
|
331
|
-
set_default(entry, "name", None, str)
|
|
332
|
-
set_default(entry, "link", None, str)
|
|
333
|
-
|
|
334
|
-
# Set defaults for extra alternate settings
|
|
335
|
-
alternate = extra.setdefault("alternate", [])
|
|
336
|
-
for entry in alternate:
|
|
337
|
-
set_default(entry, "name", None, str)
|
|
338
|
-
set_default(entry, "link", None, str)
|
|
339
|
-
set_default(entry, "lang", None, str)
|
|
340
|
-
|
|
341
|
-
# Set defaults for extra version settings
|
|
342
|
-
version = extra.setdefault("version", None)
|
|
343
|
-
if version:
|
|
344
|
-
set_default(version, "provider", None, str)
|
|
345
|
-
set_default(version, "default", None, str)
|
|
346
|
-
set_default(version, "alias", False, bool)
|
|
347
295
|
|
|
348
296
|
# Ensure all non-existent values are all empty strings (for now)
|
|
349
297
|
config["extra"] = _convert_extra(extra)
|
|
@@ -374,7 +322,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
374
322
|
"md_in_html": {},
|
|
375
323
|
"toc": {"permalink": True},
|
|
376
324
|
"pymdownx.arithmatex": {"generic": True},
|
|
377
|
-
"pymdownx.betterem": {
|
|
325
|
+
"pymdownx.betterem": {},
|
|
378
326
|
"pymdownx.caret": {},
|
|
379
327
|
"pymdownx.details": {},
|
|
380
328
|
"pymdownx.emoji": {
|
|
@@ -416,30 +364,61 @@ def _apply_defaults(config: dict, path: str) -> dict:
|
|
|
416
364
|
if isinstance(emoji.get("emoji_index"), str):
|
|
417
365
|
emoji["emoji_index"] = _resolve(emoji.get("emoji_index"))
|
|
418
366
|
|
|
419
|
-
# Tabbed extension configuration -
|
|
420
|
-
# function.
|
|
367
|
+
# Tabbed extension configuration - resolve slugification function
|
|
421
368
|
tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
|
|
422
369
|
if isinstance(tabbed.get("slugify"), dict):
|
|
423
370
|
object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
|
|
424
|
-
tabbed["slugify"] =
|
|
425
|
-
|
|
426
|
-
|
|
371
|
+
tabbed["slugify"] = _resolve(object)(**tabbed["slugify"].get("kwds", {}))
|
|
372
|
+
|
|
373
|
+
# Table of contents extension configuration - resolve slugification function
|
|
374
|
+
toc = config["mdx_configs"]["toc"]
|
|
375
|
+
if isinstance(toc.get("slugify"), dict):
|
|
376
|
+
object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
|
|
377
|
+
toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds", {}))
|
|
378
|
+
|
|
379
|
+
# Superfences extension configuration - resolve format function
|
|
380
|
+
superfences = config["mdx_configs"].get("pymdownx.superfences", {})
|
|
381
|
+
for fence in superfences.get("custom_fences", []):
|
|
382
|
+
if isinstance(fence.get("format"), str):
|
|
383
|
+
fence["format"] = _resolve(fence.get("format"))
|
|
384
|
+
elif isinstance(fence.get("format"), dict):
|
|
385
|
+
object = fence["format"].get("object", "pymdownx.superfences.fence_code_format")
|
|
386
|
+
fence["format"] = _resolve(object)(**fence["format"].get("kwds", {}))
|
|
387
|
+
if isinstance(fence.get("validator"), str):
|
|
388
|
+
fence["validator"] = _resolve(fence.get("validator"))
|
|
389
|
+
elif isinstance(fence.get("validator"), dict):
|
|
390
|
+
object = fence["validator"].get("object")
|
|
391
|
+
callable_object = _resolve(object) if object else lambda *args, **kwargs: True
|
|
392
|
+
fence["validator"] = callable_object(**fence["validator"].get("kwds", {}))
|
|
427
393
|
|
|
428
394
|
# Ensure the table of contents title is initialized, as it's used inside
|
|
429
395
|
# the template, and the table of contents extension is always defined
|
|
430
396
|
config["mdx_configs"]["toc"].setdefault("title", None)
|
|
397
|
+
config["mdx_configs_hash"] = _hash(mdx_configs)
|
|
431
398
|
|
|
432
399
|
# Convert plugins configuration
|
|
433
400
|
config["plugins"] = _convert_plugins(config.get("plugins", []), config)
|
|
401
|
+
|
|
402
|
+
# mkdocstrings configuration
|
|
403
|
+
if "mkdocstrings" in config["plugins"]:
|
|
404
|
+
mkdocstrings_config = config["plugins"]["mkdocstrings"]["config"]
|
|
405
|
+
if mkdocstrings_config.pop("enabled", True):
|
|
406
|
+
mkdocstrings_config["markdown_extensions"] = [
|
|
407
|
+
{ext: mdx_configs.get(ext, {})} for ext in markdown_extensions
|
|
408
|
+
]
|
|
409
|
+
config["markdown_extensions"].append("mkdocstrings")
|
|
410
|
+
config["mdx_configs"]["mkdocstrings"] = mkdocstrings_config
|
|
411
|
+
|
|
434
412
|
return config
|
|
435
413
|
|
|
436
414
|
|
|
437
415
|
def set_default(
|
|
438
|
-
entry: dict, key: str, default: Any, data_type: type = None
|
|
439
|
-
) ->
|
|
440
|
-
"""
|
|
441
|
-
|
|
442
|
-
|
|
416
|
+
entry: dict, key: str, default: Any, data_type: type | None = None
|
|
417
|
+
) -> Any:
|
|
418
|
+
"""Set a key to a default value if it isn't set, and optionally cast it to the specified data type."""
|
|
419
|
+
if key in entry and entry[key] is None:
|
|
420
|
+
del entry[key]
|
|
421
|
+
|
|
443
422
|
# Set the default value if the key is not present
|
|
444
423
|
entry.setdefault(key, default)
|
|
445
424
|
|
|
@@ -448,13 +427,22 @@ def set_default(
|
|
|
448
427
|
try:
|
|
449
428
|
entry[key] = data_type(entry[key])
|
|
450
429
|
except (ValueError, TypeError) as e:
|
|
451
|
-
raise ValueError(
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Failed to cast key '{key}' to {data_type}: {e}"
|
|
432
|
+
) from e
|
|
433
|
+
|
|
434
|
+
# Return the resulting value
|
|
435
|
+
return entry[key]
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _hash(data: Any) -> int:
|
|
439
|
+
"""Compute a hash for the given data."""
|
|
440
|
+
hash = hashlib.sha1(pickle.dumps(data)) # noqa: S324
|
|
441
|
+
return int(hash.hexdigest(), 16) % (2**64)
|
|
452
442
|
|
|
453
443
|
|
|
454
444
|
def _convert_extra(data: dict | list) -> dict | list:
|
|
455
|
-
"""
|
|
456
|
-
Recursively convert all None values in a dictionary or list to empty strings.
|
|
457
|
-
"""
|
|
445
|
+
"""Recursively convert all None values in a dictionary or list to empty strings."""
|
|
458
446
|
if isinstance(data, dict):
|
|
459
447
|
# Process each key-value pair in the dictionary
|
|
460
448
|
return {
|
|
@@ -463,7 +451,7 @@ def _convert_extra(data: dict | list) -> dict | list:
|
|
|
463
451
|
else ("" if value is None else value)
|
|
464
452
|
for key, value in data.items()
|
|
465
453
|
}
|
|
466
|
-
|
|
454
|
+
if isinstance(data, list):
|
|
467
455
|
# Process each item in the list
|
|
468
456
|
return [
|
|
469
457
|
_convert_extra(item)
|
|
@@ -471,14 +459,11 @@ def _convert_extra(data: dict | list) -> dict | list:
|
|
|
471
459
|
else ("" if item is None else item)
|
|
472
460
|
for item in data
|
|
473
461
|
]
|
|
474
|
-
|
|
475
|
-
return data
|
|
462
|
+
return data
|
|
476
463
|
|
|
477
464
|
|
|
478
|
-
def _resolve(symbol: str):
|
|
479
|
-
"""
|
|
480
|
-
Resolve a symbol to its corresponding Python object.
|
|
481
|
-
"""
|
|
465
|
+
def _resolve(symbol: str) -> Any:
|
|
466
|
+
"""Resolve a symbol to its corresponding Python object."""
|
|
482
467
|
module_path, func_name = symbol.rsplit(".", 1)
|
|
483
468
|
module = importlib.import_module(module_path)
|
|
484
469
|
return getattr(module, func_name)
|
|
@@ -487,17 +472,15 @@ def _resolve(symbol: str):
|
|
|
487
472
|
# -----------------------------------------------------------------------------
|
|
488
473
|
|
|
489
474
|
|
|
490
|
-
def _convert_nav(nav:
|
|
491
|
-
"""
|
|
492
|
-
Convert MkDocs navigation
|
|
493
|
-
"""
|
|
475
|
+
def _convert_nav(nav: list) -> list:
|
|
476
|
+
"""Convert MkDocs navigation."""
|
|
494
477
|
return [_convert_nav_item(entry) for entry in nav]
|
|
495
478
|
|
|
496
479
|
|
|
497
|
-
def _convert_nav_item(item: str | dict | list) -> dict:
|
|
498
|
-
"""
|
|
499
|
-
|
|
500
|
-
|
|
480
|
+
def _convert_nav_item(item: str | dict | list) -> dict | list:
|
|
481
|
+
"""Convert MkDocs shorthand navigation structure into something more manageable.
|
|
482
|
+
|
|
483
|
+
We need to annotate each item with a title, URL, icon, and children.
|
|
501
484
|
"""
|
|
502
485
|
if isinstance(item, str):
|
|
503
486
|
return {
|
|
@@ -511,19 +494,19 @@ def _convert_nav_item(item: str | dict | list) -> dict:
|
|
|
511
494
|
}
|
|
512
495
|
|
|
513
496
|
# Handle Title: URL
|
|
514
|
-
|
|
497
|
+
if isinstance(item, dict):
|
|
515
498
|
for title, value in item.items():
|
|
516
499
|
if isinstance(value, str):
|
|
517
500
|
return {
|
|
518
501
|
"title": str(title),
|
|
519
|
-
"url": value,
|
|
502
|
+
"url": value.strip(),
|
|
520
503
|
"canonical_url": None,
|
|
521
504
|
"meta": None,
|
|
522
505
|
"children": [],
|
|
523
|
-
"is_index": _is_index(value),
|
|
506
|
+
"is_index": _is_index(value.strip()),
|
|
524
507
|
"active": False,
|
|
525
508
|
}
|
|
526
|
-
|
|
509
|
+
if isinstance(value, list):
|
|
527
510
|
return {
|
|
528
511
|
"title": str(title),
|
|
529
512
|
"url": None,
|
|
@@ -533,28 +516,25 @@ def _convert_nav_item(item: str | dict | list) -> dict:
|
|
|
533
516
|
"is_index": False,
|
|
534
517
|
"active": False,
|
|
535
518
|
}
|
|
519
|
+
raise TypeError(f"Unknown nav item value type: {type(value)}")
|
|
536
520
|
|
|
537
521
|
# Handle a list of items
|
|
538
522
|
elif isinstance(item, list):
|
|
539
523
|
return [_convert_nav_item(child) for child in item]
|
|
540
|
-
|
|
541
|
-
|
|
524
|
+
|
|
525
|
+
raise TypeError(f"Unknown nav item type: {type(item)}")
|
|
542
526
|
|
|
543
527
|
|
|
544
528
|
def _is_index(path: str) -> bool:
|
|
545
|
-
"""
|
|
546
|
-
|
|
547
|
-
"""
|
|
548
|
-
return path.endswith(("index.md", "README.md"))
|
|
529
|
+
"""Returns, whether the given path points to a section index."""
|
|
530
|
+
return os.path.basename(path) in ("index.md", "README.md")
|
|
549
531
|
|
|
550
532
|
|
|
551
533
|
# -----------------------------------------------------------------------------
|
|
552
534
|
|
|
553
535
|
|
|
554
|
-
def _convert_extra_javascript(value: list
|
|
555
|
-
"""
|
|
556
|
-
Ensure extra_javascript uses a structured format.
|
|
557
|
-
"""
|
|
536
|
+
def _convert_extra_javascript(value: list) -> list:
|
|
537
|
+
"""Ensure extra_javascript uses a structured format."""
|
|
558
538
|
for i, item in enumerate(value):
|
|
559
539
|
if isinstance(item, str):
|
|
560
540
|
value[i] = {
|
|
@@ -569,9 +549,7 @@ def _convert_extra_javascript(value: list[any]) -> list:
|
|
|
569
549
|
item.setdefault("async", False)
|
|
570
550
|
item.setdefault("defer", False)
|
|
571
551
|
else:
|
|
572
|
-
raise
|
|
573
|
-
f"Unknown extra_javascript item type: {type(item)}"
|
|
574
|
-
)
|
|
552
|
+
raise TypeError(f"Unknown extra_javascript item type: {type(item)}")
|
|
575
553
|
|
|
576
554
|
# Return resulting value
|
|
577
555
|
return value
|
|
@@ -580,12 +558,10 @@ def _convert_extra_javascript(value: list[any]) -> list:
|
|
|
580
558
|
# -----------------------------------------------------------------------------
|
|
581
559
|
|
|
582
560
|
|
|
583
|
-
def _convert_markdown_extensions(value:
|
|
584
|
-
"""
|
|
585
|
-
Convert Markdown extensions configuration to what Python Markdown expects.
|
|
586
|
-
"""
|
|
561
|
+
def _convert_markdown_extensions(value: Any) -> tuple[list[str], dict]:
|
|
562
|
+
"""Convert Markdown extensions configuration to what Python Markdown expects."""
|
|
587
563
|
markdown_extensions = ["toc", "tables"]
|
|
588
|
-
mdx_configs = {"toc": {}, "tables": {}}
|
|
564
|
+
mdx_configs: dict[str, dict[str, Any]] = {"toc": {}, "tables": {}}
|
|
589
565
|
|
|
590
566
|
# In case of Python Markdown Extensions, we allow to omit the necessary
|
|
591
567
|
# quotes around the extension names, so we need to hoist the extensions
|
|
@@ -593,8 +569,24 @@ def _convert_markdown_extensions(value: any):
|
|
|
593
569
|
# actually parse the configuration.
|
|
594
570
|
if "pymdownx" in value:
|
|
595
571
|
pymdownx = value.pop("pymdownx")
|
|
596
|
-
for ext,
|
|
597
|
-
|
|
572
|
+
for ext, conf in pymdownx.items():
|
|
573
|
+
# Special case for blocks extension, which has another level of
|
|
574
|
+
# nesting. This is the only extension that requires this.
|
|
575
|
+
if ext == "blocks":
|
|
576
|
+
for block, config in conf.items():
|
|
577
|
+
value[f"pymdownx.{ext}.{block}"] = config
|
|
578
|
+
else:
|
|
579
|
+
value[f"pymdownx.{ext}"] = conf
|
|
580
|
+
|
|
581
|
+
# Same as for Python Markdown extensions, see above
|
|
582
|
+
if "zensical" in value:
|
|
583
|
+
zensical = value.pop("zensical")
|
|
584
|
+
for ext, conf in zensical.items():
|
|
585
|
+
if ext == "extensions":
|
|
586
|
+
for key, config in conf.items():
|
|
587
|
+
value[f"zensical.{ext}.{key}"] = config
|
|
588
|
+
else:
|
|
589
|
+
value[f"zensical.{ext}"] = conf
|
|
598
590
|
|
|
599
591
|
# Extensions can be defined as a dict
|
|
600
592
|
if isinstance(value, dict):
|
|
@@ -619,16 +611,13 @@ def _convert_markdown_extensions(value: any):
|
|
|
619
611
|
# ----------------------------------------------------------------------------
|
|
620
612
|
|
|
621
613
|
|
|
622
|
-
def _convert_plugins(value:
|
|
623
|
-
"""
|
|
624
|
-
Convert plugins configuration to something we can work with.
|
|
625
|
-
"""
|
|
614
|
+
def _convert_plugins(value: Any, config: dict) -> dict:
|
|
615
|
+
"""Convert plugins configuration to something we can work with."""
|
|
626
616
|
plugins = {}
|
|
627
617
|
|
|
628
618
|
# Plugins can be defined as a dict
|
|
629
619
|
if isinstance(value, dict):
|
|
630
|
-
|
|
631
|
-
plugins[name] = data
|
|
620
|
+
plugins.update(value)
|
|
632
621
|
|
|
633
622
|
# Plugins can also be defined as a list
|
|
634
623
|
else:
|
|
@@ -640,13 +629,15 @@ def _convert_plugins(value: any, config: dict) -> list:
|
|
|
640
629
|
plugins[item] = {}
|
|
641
630
|
|
|
642
631
|
# Define defaults for search plugin
|
|
643
|
-
search = plugins
|
|
644
|
-
search
|
|
645
|
-
|
|
632
|
+
search = set_default(plugins, "search", {}, dict)
|
|
633
|
+
set_default(search, "enabled", True, bool)
|
|
634
|
+
set_default(
|
|
635
|
+
search, "separator", '[\\s\\-_,:!=\\[\\]()\\\\"`/]+|\\.(?!\\d)', str
|
|
636
|
+
)
|
|
646
637
|
|
|
647
638
|
# Define defaults for offline plugin
|
|
648
|
-
offline = plugins
|
|
649
|
-
offline
|
|
639
|
+
offline = set_default(plugins, "offline", {"enabled": False}, dict)
|
|
640
|
+
set_default(offline, "enabled", True, bool)
|
|
650
641
|
|
|
651
642
|
# Ensure correct resolution of links when viewing the site from the
|
|
652
643
|
# file system by disabling directory URLs
|
|
@@ -658,18 +649,20 @@ def _convert_plugins(value: any, config: dict) -> list:
|
|
|
658
649
|
"iframe-worker" in url for url in config["extra"]["polyfills"]
|
|
659
650
|
):
|
|
660
651
|
script = "https://unpkg.com/iframe-worker/shim"
|
|
661
|
-
config["extra"]["polyfills"].append(
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
652
|
+
config["extra"]["polyfills"].append(
|
|
653
|
+
{
|
|
654
|
+
"path": script,
|
|
655
|
+
"type": "text/javascript",
|
|
656
|
+
"async": False,
|
|
657
|
+
"defer": False,
|
|
658
|
+
}
|
|
659
|
+
)
|
|
667
660
|
|
|
668
661
|
# Now, add another level of indirection, by moving all plugin configuration
|
|
669
662
|
# into a `config` property, making it compatible with Material for MkDocs.
|
|
670
|
-
for name,
|
|
671
|
-
if not isinstance(
|
|
672
|
-
plugins[name] = {"config":
|
|
663
|
+
for name, data in plugins.items():
|
|
664
|
+
if not isinstance(data, dict) or "config" not in data:
|
|
665
|
+
plugins[name] = {"config": data}
|
|
673
666
|
|
|
674
667
|
# Return plugins
|
|
675
668
|
return plugins
|
|
@@ -681,8 +674,7 @@ def _convert_plugins(value: any, config: dict) -> list:
|
|
|
681
674
|
def _yaml_load(
|
|
682
675
|
source: IO, loader: type[BaseLoader] | None = None
|
|
683
676
|
) -> dict[str, Any]:
|
|
684
|
-
"""
|
|
685
|
-
Load configuration file and resolve environment variables and parent files.
|
|
677
|
+
"""Load configuration file and resolve environment variables and parent files.
|
|
686
678
|
|
|
687
679
|
Note that INHERIT is only a bandaid that was introduced to allow for some
|
|
688
680
|
degree of modularity, but with serious shortcomings. Zensical will use a
|
|
@@ -697,12 +689,12 @@ def _yaml_load(
|
|
|
697
689
|
source.read()
|
|
698
690
|
.replace("material.extensions", "zensical.extensions")
|
|
699
691
|
.replace("materialx", "zensical.extensions"),
|
|
700
|
-
Loader=Loader,
|
|
692
|
+
Loader=Loader, # noqa: S506
|
|
701
693
|
)
|
|
702
694
|
except YAMLError as e:
|
|
703
695
|
raise ConfigurationError(
|
|
704
696
|
f"Encountered an error parsing the configuration file: {e}"
|
|
705
|
-
)
|
|
697
|
+
) from e
|
|
706
698
|
if config is None:
|
|
707
699
|
return {}
|
|
708
700
|
|
|
@@ -716,7 +708,7 @@ def _yaml_load(
|
|
|
716
708
|
raise ConfigurationError(
|
|
717
709
|
f"Inherited config file '{relpath}' doesn't exist at '{abspath}'."
|
|
718
710
|
)
|
|
719
|
-
with open(abspath, "
|
|
711
|
+
with open(abspath, encoding="utf-8") as fd:
|
|
720
712
|
parent = _yaml_load(fd, loader)
|
|
721
713
|
config = always_merger.merge(parent, config)
|
|
722
714
|
|
|
@@ -724,9 +716,11 @@ def _yaml_load(
|
|
|
724
716
|
return config
|
|
725
717
|
|
|
726
718
|
|
|
727
|
-
def _construct_env_tag(
|
|
728
|
-
|
|
729
|
-
|
|
719
|
+
def _construct_env_tag(
|
|
720
|
+
loader: yaml.Loader,
|
|
721
|
+
node: yaml.ScalarNode | yaml.SequenceNode | yaml.MappingNode,
|
|
722
|
+
) -> Any:
|
|
723
|
+
"""Assign value of ENV variable referenced at node.
|
|
730
724
|
|
|
731
725
|
MkDocs supports the use of !ENV to reference environment variables in YAML
|
|
732
726
|
configuration files. We won't likely support this in Zensical, but for now
|
|
@@ -755,7 +749,7 @@ def _construct_env_tag(loader: yaml.Loader, node: yaml.Node):
|
|
|
755
749
|
else:
|
|
756
750
|
raise ConstructorError(
|
|
757
751
|
context=f"expected a scalar or sequence node, but found {node.id}",
|
|
758
|
-
|
|
752
|
+
context_mark=node.start_mark,
|
|
759
753
|
)
|
|
760
754
|
|
|
761
755
|
# Resolve environment variable
|