zensical 0.0.3__cp310-abi3-musllinux_1_2_i686.whl → 0.0.9__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.

Files changed (30) hide show
  1. zensical/__init__.py +2 -2
  2. zensical/__main__.py +28 -0
  3. zensical/bootstrap/.github/workflows/docs.yml +9 -3
  4. zensical/bootstrap/zensical.toml +21 -21
  5. zensical/config.py +88 -39
  6. zensical/extensions/__init__.py +2 -2
  7. zensical/extensions/emoji.py +2 -2
  8. zensical/extensions/links.py +20 -7
  9. zensical/extensions/preview.py +2 -2
  10. zensical/extensions/search.py +2 -2
  11. zensical/extensions/utilities/__init__.py +2 -2
  12. zensical/extensions/utilities/filter.py +2 -2
  13. zensical/main.py +11 -9
  14. zensical/markdown.py +4 -4
  15. zensical/templates/assets/javascripts/bundle.21aa498e.min.js +3 -0
  16. zensical/templates/assets/stylesheets/classic/{main.c5ffb0a9.min.css → main.6eec86b3.min.css} +1 -1
  17. zensical/templates/assets/stylesheets/modern/main.2644c6b7.min.css +1 -0
  18. zensical/templates/base.html +3 -3
  19. zensical/templates/partials/javascripts/base.html +1 -1
  20. zensical/templates/partials/nav-item.html +1 -1
  21. zensical/templates/partials/search.html +3 -1
  22. zensical/zensical.abi3.so +0 -0
  23. zensical/zensical.pyi +2 -2
  24. {zensical-0.0.3.dist-info → zensical-0.0.9.dist-info}/METADATA +4 -3
  25. {zensical-0.0.3.dist-info → zensical-0.0.9.dist-info}/RECORD +28 -27
  26. {zensical-0.0.3.dist-info → zensical-0.0.9.dist-info}/WHEEL +1 -1
  27. {zensical-0.0.3.dist-info → zensical-0.0.9.dist-info}/licenses/LICENSE.md +1 -1
  28. zensical/templates/assets/javascripts/bundle.3c403d54.min.js +0 -3
  29. zensical/templates/assets/stylesheets/modern/main.1357c24d.min.css +0 -1
  30. {zensical-0.0.3.dist-info → zensical-0.0.9.dist-info}/entry_points.txt +0 -0
zensical/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
zensical/__main__.py ADDED
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2025 Zensical and contributors
2
+
3
+ # SPDX-License-Identifier: MIT
4
+ # Third-party contributions licensed under DCO
5
+
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to
8
+ # deal in the Software without restriction, including without limitation the
9
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10
+ # sell copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
+ # IN THE SOFTWARE.
23
+
24
+ # Allow running as a script, with `python -m zensical`.
25
+ from zensical.main import cli
26
+
27
+ if __name__ == "__main__": # pragma: no cover
28
+ cli()
@@ -1,21 +1,27 @@
1
- name: docs
1
+ name: Documentation
2
2
  on:
3
3
  push:
4
4
  branches:
5
5
  - master
6
6
  - main
7
7
  permissions:
8
- contents: write
8
+ contents: read
9
+ pages: write
10
+ id-token: write
9
11
  jobs:
10
12
  deploy:
13
+ environment:
14
+ name: github-pages
15
+ url: ${{ steps.deployment.outputs.page_url }}
11
16
  runs-on: ubuntu-latest
12
17
  steps:
18
+ - uses: actions/configure-pages@v5
13
19
  - uses: actions/checkout@v5
14
20
  - uses: actions/setup-python@v5
15
21
  with:
16
22
  python-version: 3.x
17
23
  - run: pip install zensical
18
- - run: zensical build
24
+ - run: zensical build --clean
19
25
  - uses: actions/upload-pages-artifact@v4
20
26
  with:
21
27
  path: site
@@ -49,6 +49,24 @@ Copyright &copy; 2025 The authors
49
49
  # { "Markdown in 5min" = "markdown.md" },
50
50
  # ]
51
51
 
52
+ # With the "extra_css" option you can add your own CSS styling to customize
53
+ # your Zensical project according to your needs. You can add any number of
54
+ # CSS files.
55
+ #
56
+ # The path provided should be relative to the "docs_dir".
57
+ #
58
+ # Read more: https://zensical.org/docs/customization/#additional-css
59
+ #
60
+ #extra_css = ["assets/stylesheets/extra.css"]
61
+
62
+ # With the `extra_javascript` option you can add your own JavaScript to your
63
+ # project to customize the behavior according to your needs.
64
+ #
65
+ # The path provided should be relative to the "docs_dir".
66
+ #
67
+ # Read more: https://zensical.org/docs/customization/#additional-javascript
68
+ #extra_javascript = ["assets/javascript/extra.js"]
69
+
52
70
  # ----------------------------------------------------------------------------
53
71
  # Section for configuring theme options
54
72
  # ----------------------------------------------------------------------------
@@ -176,7 +194,7 @@ features = [
176
194
  # In order to provide a better user experience on slow connections when
177
195
  # using instant navigation, a progress indicator can be enabled.
178
196
  # https://zensical.org/docs/setup/navigation/#progress-indicator
179
- #"navigation.instant.progress"
197
+ #"navigation.instant.progress",
180
198
 
181
199
  # When navigation paths are activated, a breadcrumb navigation is rendered
182
200
  # above the title of each page
@@ -222,32 +240,14 @@ features = [
222
240
  # When anchor following for the table of contents is enabled, the sidebar
223
241
  # is automatically scrolled so that the active anchor is always visible.
224
242
  # https://zensical.org/docs/setup/navigation/#anchor-following
225
- # "toc.follow"
243
+ # "toc.follow",
226
244
 
227
245
  # When navigation integration for the table of contents is enabled, it is
228
246
  # always rendered as part of the navigation sidebar on the left.
229
247
  # https://zensical.org/docs/setup/navigation/#navigation-integration
230
- #"toc.integrate"
248
+ #"toc.integrate",
231
249
  ]
232
250
 
233
- # With the "extra_css" option you can add your own CSS styling to customize
234
- # your Zensical project according to your needs. You can add any number of
235
- # CSS files.
236
- #
237
- # The path provided should be relative to the "docs_dir".
238
- #
239
- # Read more: https://zensical.org/docs/customization/#additional-css
240
- #
241
- #extra_css = ["assets/stylesheets/extra.css"]
242
-
243
- # With the `extra_javascript` option you can add your own JavaScript to your
244
- # project to customize the behavior according to your needs.
245
- #
246
- # The path provided should be relative to the "docs_dir".
247
- #
248
- # Read more: https://zensical.org/docs/customization/#additional-javascript
249
- #extra_javascript = ["assets/javascript/extra.js"]
250
-
251
251
  # ----------------------------------------------------------------------------
252
252
  # In the "palette" subsection you can configure options for the color scheme.
253
253
  # You can configure different color # schemes, e.g., to turn on dark mode,
zensical/config.py CHANGED
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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,14 +23,19 @@
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
+ import hashlib
26
27
  import importlib
27
28
  import os
28
- import tomllib
29
+ import pickle
29
30
  import yaml
30
31
 
32
+ try:
33
+ import tomllib
34
+ except ModuleNotFoundError:
35
+ import tomli as tomllib # type: ignore
36
+
31
37
  from click import ClickException
32
38
  from deepmerge import always_merger
33
- from functools import partial
34
39
  from typing import Any, IO
35
40
  from yaml import BaseLoader, Loader, YAMLError
36
41
  from yaml.constructor import ConstructorError
@@ -73,7 +78,9 @@ def parse_config(path: str) -> dict:
73
78
  """
74
79
  Parse configuration file.
75
80
  """
76
- if path.endswith("zensical.toml"):
81
+ # Decide by extension; no need to convert to Path
82
+ _, ext = os.path.splitext(path)
83
+ if ext.lower() == ".toml":
77
84
  return parse_zensical_config(path)
78
85
  else:
79
86
  return parse_mkdocs_config(path)
@@ -107,6 +114,13 @@ def parse_mkdocs_config(path: str) -> dict:
107
114
  return _CONFIG
108
115
 
109
116
 
117
+ def get_config():
118
+ """
119
+ Return configuration.
120
+ """
121
+ return _CONFIG
122
+
123
+
110
124
  def get_theme_dir() -> str:
111
125
  """
112
126
  Return the theme directory.
@@ -131,12 +145,12 @@ def _apply_defaults(config: dict, path: str) -> dict:
131
145
  raise ConfigurationError("Missing required setting: site_name")
132
146
 
133
147
  # Set site directory
134
- config.setdefault("site_dir", "site")
148
+ set_default(config, "site_dir", "site", str)
135
149
  if ".." in config.get("site_dir"):
136
150
  raise ConfigurationError("site_dir must not contain '..'")
137
151
 
138
152
  # Set docs directory
139
- config.setdefault("docs_dir", "docs")
153
+ set_default(config, "docs_dir", "docs", str)
140
154
  if ".." in config.get("docs_dir"):
141
155
  raise ConfigurationError("docs_dir must not contain '..'")
142
156
 
@@ -157,16 +171,17 @@ def _apply_defaults(config: dict, path: str) -> dict:
157
171
  # Set defaults for repository name settings
158
172
  repo_url = config.get("repo_url")
159
173
  if repo_url and not config.get("repo_name"):
174
+ docs_dir = config.get("docs_dir")
160
175
  host = urlparse(repo_url).hostname or ""
161
176
  if host == "github.com":
162
- config["repo_name"] = "GitHub"
163
- config["edit_uri"] = "edit/master/docs"
177
+ set_default(config, "repo_name", "GitHub", str)
178
+ set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
164
179
  elif host == "gitlab.com":
165
- config["repo_name"] = "GitLab"
166
- config["edit_uri"] = "edit/master/docs"
180
+ set_default(config, "repo_name", "GitLab", str)
181
+ set_default(config, "edit_uri", f"edit/master/{docs_dir}", str)
167
182
  elif host == "bitbucket.org":
168
- config["repo_name"] = "Bitbucket"
169
- config["edit_uri"] = "src/default/docs"
183
+ set_default(config, "repo_name", "Bitbucket", str)
184
+ set_default(config, "edit_uri", f"src/default/{docs_dir}", str)
170
185
  elif host:
171
186
  config["repo_name"] = host.split(".")[0].title()
172
187
 
@@ -176,7 +191,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
176
191
  config["edit_uri"] = edit_uri.rstrip("/")
177
192
 
178
193
  # Set defaults for theme font settings
179
- theme = config.setdefault("theme", {})
194
+ theme = set_default(config, "theme", {}, dict)
180
195
  if isinstance(theme, str):
181
196
  theme = {"name": theme}
182
197
  config["theme"] = theme
@@ -212,7 +227,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
212
227
  set_default(theme["font"], "code", font["code"], str)
213
228
 
214
229
  # Set defaults for theme icons
215
- icon = theme.setdefault("icon", {})
230
+ icon = set_default(theme, "icon", {}, dict)
216
231
  set_default(icon, "repo", None, str)
217
232
  set_default(icon, "annotation", None, str)
218
233
  set_default(icon, "tag", {}, dict)
@@ -242,7 +257,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
242
257
  set_default(icon, "next", None, str)
243
258
 
244
259
  # Set defaults for theme admonition icons
245
- admonition = icon.setdefault("admonition", {})
260
+ admonition = set_default(icon, "admonition", {}, dict)
246
261
  set_default(admonition, "note", None, str)
247
262
  set_default(admonition, "abstract", None, str)
248
263
  set_default(admonition, "info", None, str)
@@ -277,7 +292,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
277
292
  set_default(toggle, "name", None, str)
278
293
 
279
294
  # Set defaults for extra settings
280
- extra = config.setdefault("extra", {})
295
+ extra = set_default(config, "extra", {}, dict)
281
296
  set_default(extra, "homepage", None, str)
282
297
  set_default(extra, "scope", None, str)
283
298
  set_default(extra, "annotate", {}, dict)
@@ -374,7 +389,7 @@ def _apply_defaults(config: dict, path: str) -> dict:
374
389
  "md_in_html": {},
375
390
  "toc": {"permalink": True},
376
391
  "pymdownx.arithmatex": {"generic": True},
377
- "pymdownx.betterem": {"smart_enable": "all"},
392
+ "pymdownx.betterem": {},
378
393
  "pymdownx.caret": {},
379
394
  "pymdownx.details": {},
380
395
  "pymdownx.emoji": {
@@ -416,18 +431,28 @@ def _apply_defaults(config: dict, path: str) -> dict:
416
431
  if isinstance(emoji.get("emoji_index"), str):
417
432
  emoji["emoji_index"] = _resolve(emoji.get("emoji_index"))
418
433
 
419
- # Tabbed extension configuration - we need to resolve the slugification
420
- # function.
434
+ # Tabbed extension configuration - resolve slugification function
421
435
  tabbed = config["mdx_configs"].get("pymdownx.tabbed", {})
422
436
  if isinstance(tabbed.get("slugify"), dict):
423
437
  object = tabbed["slugify"].get("object", "pymdownx.slugs.slugify")
424
- tabbed["slugify"] = partial(
425
- _resolve(object), tabbed["slugify"].get("kwds")
426
- )
438
+ tabbed["slugify"] = _resolve(object)(**tabbed["slugify"].get("kwds"))
439
+
440
+ # Table of contents extension configuration - resolve slugification function
441
+ toc = config["mdx_configs"]["toc"]
442
+ if isinstance(toc.get("slugify"), dict):
443
+ object = toc["slugify"].get("object", "pymdownx.slugs.slugify")
444
+ toc["slugify"] = _resolve(object)(**toc["slugify"].get("kwds"))
445
+
446
+ # Superfences extension configuration - resolve format function
447
+ superfences = config["mdx_configs"].get("pymdownx.superfences", {})
448
+ for fence in superfences.get("custom_fences", []):
449
+ if isinstance(fence.get("format"), str):
450
+ fence["format"] = _resolve(fence.get("format"))
427
451
 
428
452
  # Ensure the table of contents title is initialized, as it's used inside
429
453
  # the template, and the table of contents extension is always defined
430
454
  config["mdx_configs"]["toc"].setdefault("title", None)
455
+ config["mdx_configs_hash"] = _hash(mdx_configs)
431
456
 
432
457
  # Convert plugins configuration
433
458
  config["plugins"] = _convert_plugins(config.get("plugins", []), config)
@@ -436,10 +461,13 @@ def _apply_defaults(config: dict, path: str) -> dict:
436
461
 
437
462
  def set_default(
438
463
  entry: dict, key: str, default: Any, data_type: type = None
439
- ) -> None:
464
+ ) -> any:
440
465
  """
441
466
  Set a key to a default value if it isn't set, and optionally cast it to the specified data type.
442
467
  """
468
+ if key in entry and entry[key] is None:
469
+ del entry[key]
470
+
443
471
  # Set the default value if the key is not present
444
472
  entry.setdefault(key, default)
445
473
 
@@ -450,6 +478,17 @@ def set_default(
450
478
  except (ValueError, TypeError) as e:
451
479
  raise ValueError(f"Failed to cast key '{key}' to {data_type}: {e}")
452
480
 
481
+ # Return the resulting value
482
+ return entry[key]
483
+
484
+
485
+ def _hash(data: any) -> int:
486
+ """
487
+ Compute a hash for the given data.
488
+ """
489
+ hash = hashlib.sha1(pickle.dumps(data))
490
+ return int(hash.hexdigest(), 16) % (2**64)
491
+
453
492
 
454
493
  def _convert_extra(data: dict | list) -> dict | list:
455
494
  """
@@ -594,7 +633,13 @@ def _convert_markdown_extensions(value: any):
594
633
  if "pymdownx" in value:
595
634
  pymdownx = value.pop("pymdownx")
596
635
  for ext, config in pymdownx.items():
597
- value[f"pymdownx.{ext}"] = config
636
+ # Special case for blocks extension, which has another level of
637
+ # nesting. This is the only extension that requires this.
638
+ if ext == "blocks":
639
+ for block, config in config.items():
640
+ value[f"pymdownx.{ext}.{block}"] = config
641
+ else:
642
+ value[f"pymdownx.{ext}"] = config
598
643
 
599
644
  # Extensions can be defined as a dict
600
645
  if isinstance(value, dict):
@@ -640,13 +685,15 @@ def _convert_plugins(value: any, config: dict) -> list:
640
685
  plugins[item] = {}
641
686
 
642
687
  # Define defaults for search plugin
643
- search = plugins.setdefault("search", {})
644
- search.setdefault("enabled", True)
645
- search.setdefault("separator", '[\\s\\-_,:!=\\[\\]()\\\\"`/]+|\\.(?!\\d)')
688
+ search = set_default(plugins, "search", {}, dict)
689
+ set_default(search, "enabled", True, bool)
690
+ set_default(
691
+ search, "separator", '[\\s\\-_,:!=\\[\\]()\\\\"`/]+|\\.(?!\\d)', str
692
+ )
646
693
 
647
694
  # Define defaults for offline plugin
648
- offline = plugins.setdefault("offline", {"enabled": False})
649
- offline.setdefault("enabled", True)
695
+ offline = set_default(plugins, "offline", {"enabled": False}, dict)
696
+ set_default(offline, "enabled", True, bool)
650
697
 
651
698
  # Ensure correct resolution of links when viewing the site from the
652
699
  # file system by disabling directory URLs
@@ -658,18 +705,20 @@ def _convert_plugins(value: any, config: dict) -> list:
658
705
  "iframe-worker" in url for url in config["extra"]["polyfills"]
659
706
  ):
660
707
  script = "https://unpkg.com/iframe-worker/shim"
661
- config["extra"]["polyfills"].append(script)
662
-
663
- # Ensure extra polyfills/shims use structured format
664
- config["extra"]["polyfills"] = _convert_extra_javascript(
665
- config["extra"]["polyfills"]
666
- )
708
+ config["extra"]["polyfills"].append(
709
+ {
710
+ "path": script,
711
+ "type": "text/javascript",
712
+ "async": False,
713
+ "defer": False,
714
+ }
715
+ )
667
716
 
668
717
  # Now, add another level of indirection, by moving all plugin configuration
669
718
  # into a `config` property, making it compatible with Material for MkDocs.
670
- for name, config in plugins.items():
671
- if not isinstance(config, dict) or "config" not in config:
672
- plugins[name] = {"config": config}
719
+ for name, data in plugins.items():
720
+ if not isinstance(data, dict) or "config" not in data:
721
+ plugins[name] = {"config": data}
673
722
 
674
723
  # Return plugins
675
724
  return plugins
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -26,6 +26,7 @@ from __future__ import annotations
26
26
  from markdown import Extension, Markdown
27
27
  from markdown.treeprocessors import Treeprocessor
28
28
  from markdown.util import AMP_SUBSTITUTE
29
+ from pathlib import PurePosixPath
29
30
  from xml.etree.ElementTree import Element
30
31
  from urllib.parse import urlparse
31
32
 
@@ -52,7 +53,7 @@ class LinksProcessor(Treeprocessor):
52
53
  def run(self, root: Element):
53
54
  # Now, we determine whether the current page is an index page, as we
54
55
  # must apply slightly different handling in case of directory URLs
55
- current_is_index = self.path.endswith(("index.md", "README.md"))
56
+ current_is_index = get_name(self.path) in ("index.md", "README.md")
56
57
  for el in root.iter():
57
58
  # In case the element has a `href` or `src` attribute, we parse it
58
59
  # as an URL, so we can analyze and alter its path
@@ -81,10 +82,9 @@ class LinksProcessor(Treeprocessor):
81
82
  if path.endswith(".md"):
82
83
  path = path.removesuffix(".md") + ".html"
83
84
  if self.use_directory_urls:
84
- if path.endswith("index.html"):
85
- path = path[: -len("index.html")]
86
- elif path.endswith("README.html"):
87
- path = path[: -len("README.html")]
85
+ name = get_name(path)
86
+ if name in ("index.html", "README.html"):
87
+ path = path[: -len(name)]
88
88
  elif path.endswith(".html"):
89
89
  path = path[: -len(".html")] + "/"
90
90
 
@@ -124,3 +124,16 @@ class LinksExtension(Extension):
124
124
  # before they are resolved to URLs.
125
125
  processor = LinksProcessor(md, self.path, self.use_directory_urls)
126
126
  md.treeprocessors.register(processor, "relpath", 0)
127
+
128
+
129
+ # -----------------------------------------------------------------------------
130
+ # Functions
131
+ # -----------------------------------------------------------------------------
132
+
133
+
134
+ def get_name(path: str) -> str:
135
+ """
136
+ Get the name of a file from a given path.
137
+ """
138
+ path = PurePosixPath(path)
139
+ return path.name
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
zensical/main.py CHANGED
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -26,11 +26,11 @@ from __future__ import annotations
26
26
  import click
27
27
  import os
28
28
  import shutil
29
- import webbrowser
30
29
 
31
30
  from click import ClickException
32
31
  from zensical import build, serve, version
33
32
 
33
+
34
34
  # ----------------------------------------------------------------------------
35
35
  # Commands
36
36
  # ----------------------------------------------------------------------------
@@ -122,17 +122,12 @@ def execute_serve(config_file: str | None, **kwargs):
122
122
  break
123
123
  else:
124
124
  raise ClickException("No config file found in the current folder.")
125
-
126
- # Obtain development server address and open in browser, if desired
127
- dev_addr = kwargs.get("dev_addr") or "localhost:8000"
128
- if kwargs.get("open", False):
129
- webbrowser.open(f"http://{dev_addr}")
130
125
  if kwargs.get("strict", False):
131
126
  print("Warning: Strict mode is currently unsupported.")
132
127
 
133
128
  # Build project in Rust runtime, calling back into Python when necessary,
134
129
  # e.g., to parse MkDocs configuration format or render Markdown
135
- serve(os.path.abspath(config_file), dev_addr)
130
+ serve(os.path.abspath(config_file), kwargs)
136
131
 
137
132
 
138
133
  @cli.command(name="new")
@@ -156,6 +151,7 @@ def new_project(directory: str | None, **kwargs):
156
151
  directory = "."
157
152
  docs_dir = os.path.join(directory, "docs")
158
153
  config_file = os.path.join(directory, "zensical.toml")
154
+ github_dir = os.path.join(directory, ".github")
159
155
 
160
156
  if os.path.exists(directory):
161
157
  if not os.path.isdir(directory):
@@ -164,6 +160,8 @@ def new_project(directory: str | None, **kwargs):
164
160
  raise (ClickException(f"{config_file} already exists."))
165
161
  if os.path.exists(docs_dir):
166
162
  raise (ClickException(f"{docs_dir} already exists."))
163
+ if os.path.exists(github_dir):
164
+ raise (ClickException(f"{github_dir} already exists."))
167
165
  else:
168
166
  os.makedirs(directory)
169
167
 
@@ -173,6 +171,10 @@ def new_project(directory: str | None, **kwargs):
173
171
  os.path.join(package_dir, "bootstrap/docs"),
174
172
  os.path.join(directory, "docs"),
175
173
  )
174
+ shutil.copytree(
175
+ os.path.join(package_dir, "bootstrap/.github"),
176
+ os.path.join(directory, ".github"),
177
+ )
176
178
 
177
179
 
178
180
  # ----------------------------------------------------------------------------
zensical/markdown.py CHANGED
@@ -1,7 +1,7 @@
1
- # Copyright (c) Zensical LLC <https://zensical.org>
1
+ # Copyright (c) 2025 Zensical and contributors
2
2
 
3
3
  # SPDX-License-Identifier: MIT
4
- # Third-party contributions licensed under CLA
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
@@ -30,7 +30,7 @@ from datetime import date, datetime
30
30
  from markdown import Markdown
31
31
  from yaml import SafeLoader
32
32
 
33
- from .config import _CONFIG
33
+ from .config import get_config
34
34
  from .extensions.links import LinksExtension
35
35
  from .extensions.search import SearchExtension
36
36
 
@@ -61,7 +61,7 @@ def render(content: str, path: str) -> dict:
61
61
  in order to support the specific syntax of Python Markdown. We're working
62
62
  on moving the entire rendering chain to Rust.
63
63
  """
64
- config = _CONFIG
64
+ config = get_config()
65
65
 
66
66
  # Initialize Markdown parser
67
67
  md = Markdown(