chore(release): 0.0.7
Squash-merge lib-standardisation: adopt repo standards across packaging, tooling, docs, and CI. - standardize pyproject.toml (canonical packaging, lint config, extras, GSDFC pydoclint) - add py.typed marker, modernize type hints and Google-style docstrings - nest generated docs under docs/lib and docs/mcp; add hand-written wiki - adopt standard Type A PyPI CI pipeline (tag==version guard + quality gate) - promote generated CHANGELOG.md; bump version to 0.0.7
This commit is contained in:
165
docforge/nav/wiki.py
Normal file
165
docforge/nav/wiki.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
Wiki navigation derivation.
|
||||
|
||||
This module provides ``build_wiki_nav``, which derives an MkDocs-ready
|
||||
navigation block from the file structure of a hand-written wiki directory
|
||||
(typically ``docs/wiki``). wiki content is authored by hand and is never
|
||||
modified by doc-forge; only the navigation layout is inferred.
|
||||
|
||||
# Notes
|
||||
|
||||
- ``index.md`` at the wiki root becomes the ``Home`` entry.
|
||||
- Page labels are derived from filenames: numeric order prefixes such as
|
||||
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
|
||||
names are title-cased (``01_overview.md`` becomes ``Overview``).
|
||||
- Subdirectories become nested navigation groups. A nested ``index.md`` is
|
||||
rendered as the section root placed first inside the group.
|
||||
- Only ``.md`` files are considered; hidden entries are ignored.
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_wiki_nav(wiki_dir: Path) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Derive an MkDocs navigation block from a wiki directory.
|
||||
|
||||
Returned paths are relative to the parent of ``wiki_dir`` and carry the
|
||||
wiki directory name as their leading component (for example
|
||||
``wiki/01_overview.md`` when the wiki lives at ``docs/wiki``). This makes
|
||||
the result directly usable in an MkDocs ``nav`` block with
|
||||
|
||||
- ``index.md`` at the wiki root becomes the ``Home`` entry.
|
||||
- Page labels are derived from filenames: numeric order prefixes such as
|
||||
``01_`` or ``02-`` are stripped, separators are replaced with spaces, and
|
||||
names are title-cased (``01_overview.md`` becomes ``Overview``).
|
||||
- Subdirectories become nested navigation groups. A nested ``index.md`` is
|
||||
rendered as the section root placed first inside the group.
|
||||
- Only ``.md`` files are considered; hidden entries are ignored.
|
||||
|
||||
Args:
|
||||
wiki_dir (Path):
|
||||
Path to the hand-written wiki directory, for example ``docs/wiki``.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
Navigation entries compatible with the MkDocs ``nav`` configuration.
|
||||
The list is empty if the wiki contains no Markdown files.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError:
|
||||
If the wiki directory does not exist.
|
||||
"""
|
||||
if not wiki_dir.exists():
|
||||
raise FileNotFoundError(wiki_dir)
|
||||
|
||||
root = wiki_dir.parent
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
try:
|
||||
return path.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
return path.as_posix()
|
||||
|
||||
nav: list[dict[str, Any]] = []
|
||||
|
||||
if (wiki_dir / "index.md").exists():
|
||||
nav.append({"Home": rel(wiki_dir / "index.md")})
|
||||
|
||||
nav.extend(_render_entries(wiki_dir, rel))
|
||||
|
||||
return nav
|
||||
|
||||
|
||||
def _render_entries(
|
||||
base_dir: Path,
|
||||
rel: Callable[[Path], str],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Render navigation entries for the children of a wiki directory.
|
||||
|
||||
Markdown pages become labeled entries, subdirectories containing
|
||||
Markdown become nested groups, and a nested ``index.md`` is emitted
|
||||
first as the section root.
|
||||
|
||||
Args:
|
||||
base_dir (Path):
|
||||
Directory whose children are rendered.
|
||||
|
||||
rel (Callable[[Path], str]):
|
||||
Callable converting a wiki file path into a docs-relative path.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
Navigation entries for ``base_dir`` in natural sort order.
|
||||
"""
|
||||
children = sorted(
|
||||
(child for child in base_dir.iterdir() if not child.name.startswith(".")),
|
||||
key=lambda child: _natural_key(child.name),
|
||||
)
|
||||
|
||||
entries: list[dict[str, Any]] = []
|
||||
|
||||
for child in children:
|
||||
if child.is_dir():
|
||||
if not any(child.rglob("*.md")):
|
||||
continue
|
||||
|
||||
group: list[dict[str, Any]] = []
|
||||
nested_index = child / "index.md"
|
||||
if nested_index.exists():
|
||||
group.append({_prettify(child.name): rel(nested_index)})
|
||||
group.extend(_render_entries(child, rel))
|
||||
entries.append({_prettify(child.name): group})
|
||||
elif child.suffix == ".md":
|
||||
if child.name == "index.md":
|
||||
continue
|
||||
entries.append({_prettify(child.stem): rel(child)})
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _prettify(name: str) -> str:
|
||||
"""
|
||||
Convert a filename or directory name into a navigation label.
|
||||
|
||||
Numeric order prefixes (``01_``, ``02-``, ``03.``) are stripped,
|
||||
separators are replaced with spaces, and the result is title-cased.
|
||||
|
||||
Args:
|
||||
name (str):
|
||||
Filename stem or directory name to prettify.
|
||||
|
||||
Returns:
|
||||
str:
|
||||
Title-cased navigation label.
|
||||
"""
|
||||
name = re.sub(r"^\d+[_\-. ]?", "", name)
|
||||
name = re.sub(r"[_-]+", " ", name).strip()
|
||||
return name.title()
|
||||
|
||||
|
||||
def _natural_key(name: str) -> list[object]:
|
||||
"""
|
||||
Build a natural sort key from a filename.
|
||||
|
||||
The key splits ``name`` into alternating non-digit and digit parts so
|
||||
that numeric prefixes sort numerically (``02`` before ``10``).
|
||||
|
||||
Args:
|
||||
name (str):
|
||||
Filename or directory name to key.
|
||||
|
||||
Returns:
|
||||
list[object]:
|
||||
Mixed list of lowercased strings and integers used for sorting.
|
||||
"""
|
||||
return [
|
||||
int(part) if part.isdigit() else part.lower()
|
||||
for part in re.split(r"(\d+)", name)
|
||||
]
|
||||
Reference in New Issue
Block a user