fix(mkdocs): rebase cross-kind wiki links under directory URLs
All checks were successful
continuous-integration/drone/tag Build is passing
All checks were successful
continuous-integration/drone/tag Build is passing
Wiki pages linking to a sibling kind (for example ../lib/...) were emitted verbatim by MkDocs and, with use_directory_urls, resolved into a non-existent <kind>/<kind>/ path. Add a docforge-linkfix plugin (doc-forge 0.0.8) that rebases those links to the repository root.
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.8] - 2026-10-17
|
||||
|
||||
### Added
|
||||
- `docforge-linkfix` MkDocs plugin (registered via the `mkdocs.plugins`
|
||||
entry point) that rebases cross-kind relative links — for example
|
||||
`../lib/...` written in a wiki page — to the repository root so they resolve
|
||||
to `/<repo>/<kind>/` under directory URLs.
|
||||
|
||||
### Changed
|
||||
- The wiki template (`mkdocs.wiki.yml`) now enables `docforge-linkfix`;
|
||||
regenerating a wiki config (or passing `--refresh`) propagates the fix.
|
||||
|
||||
### Fixed
|
||||
- Cross-kind links (`../lib/`, `../api/`, `../wiki/`) in generated wikis no
|
||||
longer resolve into a non-existent `<kind>/<kind>/` path.
|
||||
|
||||
## [0.0.7] - 2026-10-17
|
||||
|
||||
### Added
|
||||
@@ -43,5 +59,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `import docforge` no longer requires the optional `mcp` dependency; the
|
||||
MCP server imports `mcp` lazily, matching the MkDocs backend.
|
||||
|
||||
[Unreleased]: https://git.aetoskia.com/aetos/doc-forge/compare/0.0.7...HEAD
|
||||
[Unreleased]: https://git.aetoskia.com/aetos/doc-forge/compare/0.0.8...HEAD
|
||||
[0.0.8]: https://git.aetoskia.com/aetos/doc-forge/compare/0.0.7...0.0.8
|
||||
[0.0.7]: https://git.aetoskia.com/aetos/doc-forge/compare/0.0.6...0.0.7
|
||||
|
||||
72
docforge/mkdocs_links.py
Normal file
72
docforge/mkdocs_links.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
MkDocs plugin that fixes cross-kind relative links in generated wikis.
|
||||
|
||||
A repository served under ``/<repo>/<kind>/`` (for example a wiki at
|
||||
``/myrepo/wiki/`` and a library reference at ``/myrepo/lib/``) is built as
|
||||
two independent MkDocs sites. Authors naturally write links between them
|
||||
using source-relative paths such as ``../lib/01_build_recipe/``.
|
||||
|
||||
MkDocs only rewrites links whose target lives inside ``docs_dir``; anything
|
||||
pointing at a sibling site is emitted verbatim. When ``use_directory_urls``
|
||||
is enabled (the default) the rendered page sits one or more directory levels
|
||||
deeper than its source, so ``../lib/`` resolves to ``/<repo>/wiki/lib/`` and
|
||||
breaks.
|
||||
|
||||
`docforge-linkfix` rewrites such links at render time, prefixing them with
|
||||
enough ``../`` segments to climb back to the repository root so they resolve
|
||||
to ``/<repo>/<kind>/`` regardless of how deeply the page is nested.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from mkdocs.plugins import BasePlugin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
from mkdocs.structure.files import Files
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
__all__ = ["LinkFixPlugin"]
|
||||
|
||||
_CROSS_KIND_LINK = re.compile(r"\]\((?:\.\./)+(lib|api|wiki)/")
|
||||
"""Matches a Markdown link whose target climbs out of the current doc kind."""
|
||||
|
||||
|
||||
class LinkFixPlugin(BasePlugin): # type: ignore[no-untyped-call, type-arg]
|
||||
"""
|
||||
Rewrite cross-kind relative links so they survive directory URLs.
|
||||
"""
|
||||
|
||||
def on_page_markdown(
|
||||
self,
|
||||
markdown: str,
|
||||
/,
|
||||
*,
|
||||
page: Page,
|
||||
config: MkDocsConfig,
|
||||
files: Files,
|
||||
) -> str:
|
||||
"""
|
||||
Prefix cross-kind links with the depth required to reach the repo root.
|
||||
|
||||
Args:
|
||||
markdown (str): The raw Markdown source of the current page.
|
||||
page (Page): The page being rendered; its destination URI is used
|
||||
to determine how many directory levels the output sits below
|
||||
the site root.
|
||||
config (MkDocsConfig): The active MkDocs configuration.
|
||||
files (Files): The collection of files in the site.
|
||||
|
||||
Returns:
|
||||
str: The Markdown with cross-kind links rebased to the repo root.
|
||||
"""
|
||||
depth = page.file.dest_uri.count("/")
|
||||
prefix = "../" * (depth + 1)
|
||||
return _CROSS_KIND_LINK.sub(
|
||||
lambda match: f"]({prefix}{match.group(1)}/", markdown
|
||||
)
|
||||
@@ -22,4 +22,5 @@ theme:
|
||||
logo: material/database
|
||||
repo: fontawesome/brands/github
|
||||
plugins:
|
||||
- search
|
||||
- search
|
||||
- docforge-linkfix
|
||||
@@ -56,6 +56,7 @@ extra_css:
|
||||
- https://unpkg.com/dracula-prism/dist/css/dracula-prism.css
|
||||
plugins:
|
||||
- search
|
||||
- docforge-linkfix
|
||||
site_name: doc-forge
|
||||
docs_dir: wiki
|
||||
site_dir: ../site/wiki
|
||||
|
||||
@@ -55,5 +55,20 @@ Commit `docs/mkdocs.{kind}.yml` so served docs are reproducible from source.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Cross-kind links
|
||||
|
||||
Kinds are separate MkDocs sites served side by side under `/<repo>/<kind>/`,
|
||||
so a wiki page may link to the library reference with a source-relative path
|
||||
such as `../lib/13_library_registry/`. MkDocs only rewrites links that point
|
||||
inside `docs_dir`; anything reaching a sibling kind is emitted verbatim and,
|
||||
under `use_directory_urls`, resolves into `<kind>/<kind>/` and breaks.
|
||||
|
||||
The `docforge-linkfix` plugin (part of doc-forge, enabled in the wiki
|
||||
template) fixes this at render time: it rewrites `../lib/`, `../api/`, and
|
||||
`../wiki/` links to climb to the repository root, so they resolve correctly
|
||||
however deeply the page is nested.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
- [04 – MCP Guide](04_mcp_guide.md)
|
||||
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "doc-forge"
|
||||
version = "0.0.7"
|
||||
version = "0.0.8"
|
||||
description = "A renderer-agnostic Python documentation compiler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -56,6 +56,9 @@ dependencies = [
|
||||
[project.scripts]
|
||||
doc-forge = "docforge.cli.main:main"
|
||||
|
||||
[project.entry-points."mkdocs.plugins"]
|
||||
docforge-linkfix = "docforge.mkdocs_links:LinkFixPlugin"
|
||||
|
||||
[project.optional-dependencies]
|
||||
mkdocs = [
|
||||
"mkdocs==1.6.1",
|
||||
|
||||
55
tests/cli/test_mkdocs_links.py
Normal file
55
tests/cli/test_mkdocs_links.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from docforge.mkdocs_links import LinkFixPlugin
|
||||
|
||||
|
||||
def _page(dest_uri: str):
|
||||
return SimpleNamespace(file=SimpleNamespace(dest_uri=dest_uri))
|
||||
|
||||
|
||||
def _fix(markdown: str, dest_uri: str) -> str:
|
||||
return LinkFixPlugin().on_page_markdown(
|
||||
markdown,
|
||||
page=_page(dest_uri),
|
||||
config=SimpleNamespace(),
|
||||
files=SimpleNamespace(),
|
||||
)
|
||||
|
||||
|
||||
def test_root_page_keeps_single_parent():
|
||||
src = "[lib](../lib/pkg/index.md)"
|
||||
assert _fix(src, "index.html") == src
|
||||
|
||||
|
||||
def test_nested_page_adds_directory_depth():
|
||||
assert _fix("[lib](../lib/pkg/index.md)", "01_overview/index.html") == (
|
||||
"[lib](../../lib/pkg/index.md)"
|
||||
)
|
||||
|
||||
|
||||
def test_deeply_nested_page_adds_two_levels():
|
||||
src = "[lib](../lib/pkg/index.md)"
|
||||
got = _fix(src, "04_development/02_releasing/index.html")
|
||||
assert got == "[lib](../../../lib/pkg/index.md)"
|
||||
|
||||
|
||||
def test_multiple_cross_kind_links_are_rebased():
|
||||
src = "See [a](../lib/a) and [b](../api/b)."
|
||||
got = _fix(src, "01_overview/index.html")
|
||||
assert got == "See [a](../../lib/a) and [b](../../api/b)."
|
||||
|
||||
|
||||
def test_wiki_kind_is_rebased():
|
||||
assert _fix("[w](../wiki/index.md)", "01_overview/index.html") == (
|
||||
"[w](../../wiki/index.md)"
|
||||
)
|
||||
|
||||
|
||||
def test_relative_within_kind_untouched():
|
||||
src = "[next](02_install.md) and [prev](../index.md)"
|
||||
assert _fix(src, "01_overview/index.html") == src
|
||||
|
||||
|
||||
def test_absolute_and_url_links_untouched():
|
||||
src = "[site](https://example.com/lib/x) and [root](/lib/x) and [r](lib/x)"
|
||||
assert _fix(src, "a/index.html") == src
|
||||
Reference in New Issue
Block a user