From 09118115a4f70eb5680096fd8f974ec0f10e4bb4 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Sat, 17 Oct 2026 17:22:23 +0530 Subject: [PATCH] fix(mkdocs): rebase cross-kind wiki links under directory URLs 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 // path. Add a docforge-linkfix plugin (doc-forge 0.0.8) that rebases those links to the repository root. --- CHANGELOG.md | 19 ++++- docforge/mkdocs_links.py | 72 +++++++++++++++++++ docforge/templates/mkdocs.wiki.yml | 3 +- docs/mkdocs.wiki.yml | 1 + docs/wiki/05_development/05_mkdocs_configs.md | 15 ++++ pyproject.toml | 5 +- tests/cli/test_mkdocs_links.py | 55 ++++++++++++++ 7 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 docforge/mkdocs_links.py create mode 100644 tests/cli/test_mkdocs_links.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd3b00..fe5c5f1 100644 --- a/CHANGELOG.md +++ b/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 `///` 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 `//` 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 diff --git a/docforge/mkdocs_links.py b/docforge/mkdocs_links.py new file mode 100644 index 0000000..1aa9bc4 --- /dev/null +++ b/docforge/mkdocs_links.py @@ -0,0 +1,72 @@ +""" +# Summary + +MkDocs plugin that fixes cross-kind relative links in generated wikis. + +A repository served under ``///`` (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 ``//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 ``///`` 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 + ) diff --git a/docforge/templates/mkdocs.wiki.yml b/docforge/templates/mkdocs.wiki.yml index 210e0f8..eece079 100644 --- a/docforge/templates/mkdocs.wiki.yml +++ b/docforge/templates/mkdocs.wiki.yml @@ -22,4 +22,5 @@ theme: logo: material/database repo: fontawesome/brands/github plugins: - - search \ No newline at end of file + - search + - docforge-linkfix \ No newline at end of file diff --git a/docs/mkdocs.wiki.yml b/docs/mkdocs.wiki.yml index e1eb24b..8ffd343 100644 --- a/docs/mkdocs.wiki.yml +++ b/docs/mkdocs.wiki.yml @@ -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 diff --git a/docs/wiki/05_development/05_mkdocs_configs.md b/docs/wiki/05_development/05_mkdocs_configs.md index 658d776..c29fd90 100644 --- a/docs/wiki/05_development/05_mkdocs_configs.md +++ b/docs/wiki/05_development/05_mkdocs_configs.md @@ -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 `///`, +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 `//` 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) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2cafe17..74cf760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/cli/test_mkdocs_links.py b/tests/cli/test_mkdocs_links.py new file mode 100644 index 0000000..f06c77f --- /dev/null +++ b/tests/cli/test_mkdocs_links.py @@ -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