feat: build each doc kind with its own MkDocs config and site
This commit is contained in:
@@ -22,41 +22,49 @@ pip install doc-forge
|
||||
|
||||
# CLI usage
|
||||
|
||||
## Generate an MkDocs site from a Python package:
|
||||
Each site kind (`lib`, `api`, `wiki`) is built independently into `site/{kind}`.
|
||||
|
||||
## Build the library reference from a Python package:
|
||||
|
||||
```bash
|
||||
doc-forge build --mkdocs --module my_package
|
||||
```
|
||||
|
||||
## Build the API reference from an OpenAPI spec:
|
||||
|
||||
```bash
|
||||
doc-forge build --api --openapi-spec spec.json
|
||||
```
|
||||
|
||||
## Build the hand-written wiki:
|
||||
|
||||
```bash
|
||||
doc-forge build --wiki --site-name my_package
|
||||
```
|
||||
|
||||
## Generate MCP JSON documentation:
|
||||
|
||||
```bash
|
||||
doc-forge build --mcp --module my_package
|
||||
```
|
||||
|
||||
|
||||
## Generate MkDocs site and MCP JSON documentation:
|
||||
## Build several kinds in one pass:
|
||||
|
||||
```bash
|
||||
doc-forge build --mcp --mkdocs --module my_package
|
||||
doc-forge build --mcp --mkdocs --wiki --module my_package
|
||||
```
|
||||
|
||||
## Include a hand-written wiki in the MkDocs site:
|
||||
Each enabled kind gets its own MkDocs config (`docs/mkdocs.{lib,api,wiki}.yml`)
|
||||
and its own site under `site/`.
|
||||
|
||||
## Serve a site locally:
|
||||
|
||||
```bash
|
||||
doc-forge build --wiki --mkdocs --module my_package
|
||||
```
|
||||
|
||||
## Build wiki pages only (no module required):
|
||||
|
||||
```bash
|
||||
doc-forge build --wiki --site-name my_package
|
||||
```
|
||||
|
||||
## Serve MkDocs locally:
|
||||
|
||||
```bash
|
||||
doc-forge serve --mkdocs --module my_package
|
||||
doc-forge serve --wiki # preview from docs/mkdocs.wiki.yml
|
||||
doc-forge serve --lib
|
||||
doc-forge serve --api
|
||||
# or any config directly:
|
||||
doc-forge serve --mkdocs --mkdocs-yml docs/mkdocs.wiki.yml
|
||||
```
|
||||
|
||||
## Serve MCP locally:
|
||||
|
||||
@@ -9,12 +9,14 @@ Provides the CLI structure using Click, including build, serve, and tree command
|
||||
|
||||
Notes:
|
||||
- The `build` command validates requested modes before generating anything.
|
||||
- `--mkdocs`, `--api`, and `--wiki` share a single MkDocs build; `--mcp`
|
||||
generates a machine-readable bundle independently.
|
||||
- `--mkdocs`, `--api`, and `--wiki` each emit their own MkDocs config and
|
||||
build (`docs/mkdocs.{kind}.yml` into `site/{kind}`); `--mcp` generates a
|
||||
machine-readable bundle independently.
|
||||
|
||||
---
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
@@ -37,10 +39,10 @@ def cli() -> None:
|
||||
|
||||
@cli.command()
|
||||
@click.option("--mcp", is_flag=True, help="Build MCP resources")
|
||||
@click.option("--mkdocs", is_flag=True, help="Build MkDocs site")
|
||||
@click.option("--mkdocs", is_flag=True, help="Build the lib MkDocs site")
|
||||
@click.option("--api", is_flag=True, help="Build API docs from an OpenAPI spec")
|
||||
@click.option(
|
||||
"--wiki", is_flag=True, help="Include a hand-written wiki in the MkDocs site"
|
||||
"--wiki", is_flag=True, help="Build a hand-written wiki as its own MkDocs site"
|
||||
)
|
||||
@click.option(
|
||||
"--module-is-source",
|
||||
@@ -54,7 +56,7 @@ def cli() -> None:
|
||||
help="Path to the OpenAPI JSON specification",
|
||||
)
|
||||
@click.option("--project-name", help="Project name override")
|
||||
@click.option("--site-name", help="MkDocs site name")
|
||||
@click.option("--site-name", help="MkDocs site name for lib and wiki sites")
|
||||
@click.option(
|
||||
"--docs-dir",
|
||||
type=click.Path(path_type=Path),
|
||||
@@ -65,7 +67,7 @@ def cli() -> None:
|
||||
"--wiki-dir",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("docs/wiki"),
|
||||
help="Hand-written wiki directory included in the MkDocs site",
|
||||
help="Hand-written wiki directory built as its own MkDocs site",
|
||||
)
|
||||
@click.option(
|
||||
"--nav",
|
||||
@@ -77,12 +79,6 @@ def cli() -> None:
|
||||
@click.option(
|
||||
"--template", type=click.Path(path_type=Path), help="MkDocs template path"
|
||||
)
|
||||
@click.option(
|
||||
"--mkdocs-yml",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("mkdocs.yml"),
|
||||
help="Output config path",
|
||||
)
|
||||
@click.option(
|
||||
"--out-dir",
|
||||
type=click.Path(path_type=Path),
|
||||
@@ -103,7 +99,6 @@ def build(
|
||||
wiki_dir: Path,
|
||||
nav_file: Path,
|
||||
template: Path | None,
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -111,33 +106,36 @@ def build(
|
||||
|
||||
This command runs the full documentation pipeline: it loads Python
|
||||
modules, generates renderer-specific documentation sources, and
|
||||
optionally builds or serves the final output.
|
||||
optionally builds the final output.
|
||||
|
||||
Depending on the selected options, the build can target:
|
||||
|
||||
- MkDocs static documentation sites for library reference docs
|
||||
- Swagger-enabled API docs generated from an OpenAPI spec
|
||||
- Hand-written wiki pages included in the MkDocs site
|
||||
- MCP structured documentation resources
|
||||
- A lib MkDocs site (`--mkdocs`) for library reference docs
|
||||
- A swagger-enabled API MkDocs site (`--api`) built from an OpenAPI spec
|
||||
- A wiki MkDocs site (`--wiki`) built from hand-written markdown
|
||||
- MCP structured documentation resources (`--mcp`)
|
||||
|
||||
Each enabled site kind produces its own MkDocs configuration
|
||||
(`docs/mkdocs.{kind}.yml`) and its own build (`site/{kind}`).
|
||||
|
||||
Notes:
|
||||
- At least one of `--mcp`, `--mkdocs`, `--wiki`, or `--api` must be
|
||||
provided.
|
||||
- `--mkdocs`, `--api`, and `--wiki` are combined into a single MkDocs
|
||||
build, while `--mcp` emits a machine-readable bundle.
|
||||
- `--mkdocs`, `--api`, and `--wiki` emit independent MkDocs builds,
|
||||
while `--mcp` emits a machine-readable bundle.
|
||||
|
||||
Args:
|
||||
mcp (bool):
|
||||
Enable MCP documentation generation.
|
||||
|
||||
mkdocs (bool):
|
||||
Enable MkDocs library documentation generation.
|
||||
Enable the lib MkDocs documentation generation.
|
||||
|
||||
api (bool):
|
||||
Enable API documentation generation from an OpenAPI spec.
|
||||
|
||||
wiki (bool):
|
||||
Include a hand-written wiki directory in the MkDocs site.
|
||||
Build a hand-written wiki directory as its own MkDocs site.
|
||||
|
||||
module_is_source (bool):
|
||||
Treat the specified module directory as the project root.
|
||||
@@ -152,11 +150,10 @@ def build(
|
||||
Optional override for the project name.
|
||||
|
||||
site_name (str | None):
|
||||
Display name for the MkDocs site.
|
||||
Display name for the lib and wiki MkDocs sites.
|
||||
|
||||
docs_dir (Path):
|
||||
Shared documentation root used as the MkDocs ``docs_dir``.
|
||||
|
||||
Shared documentation root used for generated sources.
|
||||
wiki_dir (Path):
|
||||
Directory containing hand-written wiki markdown files.
|
||||
|
||||
@@ -166,9 +163,6 @@ def build(
|
||||
template (Path | None):
|
||||
Optional custom MkDocs configuration template.
|
||||
|
||||
mkdocs_yml (Path):
|
||||
Output path for the generated MkDocs configuration.
|
||||
|
||||
out_dir (Path):
|
||||
Output directory for generated MCP resources.
|
||||
|
||||
@@ -188,18 +182,18 @@ def build(
|
||||
"the OpenAPI spec provides the site name"
|
||||
)
|
||||
|
||||
if (mkdocs or mcp) and not module:
|
||||
raise click.UsageError(
|
||||
"--module is required for MkDocs build"
|
||||
if mkdocs
|
||||
else "--module is required for MCP build"
|
||||
)
|
||||
if mkdocs and not module:
|
||||
raise click.UsageError("--module is required for MkDocs build")
|
||||
if mcp and not module:
|
||||
raise click.UsageError("--module is required for MCP build")
|
||||
|
||||
spec: dict | None = None
|
||||
if api:
|
||||
spec = api_utils.load_openapi_spec(openapi_spec)
|
||||
|
||||
kinds: list[str] = []
|
||||
if mkdocs:
|
||||
kinds.append("lib")
|
||||
lib_dir = docs_dir / "lib"
|
||||
click.echo(f"Generating MkDocs sources in {lib_dir}...")
|
||||
mkdocs_utils.generate_sources(
|
||||
@@ -207,54 +201,64 @@ def build(
|
||||
lib_dir,
|
||||
project_name,
|
||||
module_is_source,
|
||||
readme_dir=mkdocs_yml.parent,
|
||||
readme_dir=Path(".").resolve(),
|
||||
)
|
||||
|
||||
if api:
|
||||
kinds.append("api")
|
||||
api_dir = docs_dir / "api"
|
||||
click.echo(f"Generating API sources in {api_dir}...")
|
||||
api_utils.generate_api_sources(spec, api_dir)
|
||||
|
||||
if mkdocs or api or wiki:
|
||||
modes: list[str] = []
|
||||
if mkdocs:
|
||||
modes.append("lib")
|
||||
if wiki:
|
||||
kinds.append("wiki")
|
||||
|
||||
if kinds:
|
||||
api_metadata: api_utils.OpenAPIMetadata | None = None
|
||||
if api:
|
||||
modes.append("api")
|
||||
if wiki:
|
||||
modes.append("wiki")
|
||||
api_metadata = api_utils.derive_metadata(spec)
|
||||
theme_icon = mkdocs_utils.load_spec_icon(nav_file)
|
||||
|
||||
site_description: str | None = None
|
||||
site_author: str | None = None
|
||||
effective_site_name = site_name or module or Path.cwd().name
|
||||
config_paths: list[Path] = []
|
||||
for kind in kinds:
|
||||
kind_root = wiki_dir if kind == "wiki" else docs_dir / kind
|
||||
|
||||
if api:
|
||||
metadata = api_utils.derive_metadata(spec)
|
||||
effective_site_name = metadata.site_name
|
||||
site_description = metadata.site_description
|
||||
site_author = metadata.site_author
|
||||
site_name_kind = site_name or module or Path.cwd().name
|
||||
site_description: str | None = None
|
||||
site_author: str | None = None
|
||||
if kind == "api" and api_metadata:
|
||||
site_name_kind = api_metadata.site_name
|
||||
site_description = api_metadata.site_description
|
||||
site_author = api_metadata.site_author
|
||||
|
||||
click.echo(f"Generating MkDocs config {mkdocs_yml}...")
|
||||
mkdocs_utils.generate_config(
|
||||
docs_dir,
|
||||
nav_file,
|
||||
template,
|
||||
mkdocs_yml,
|
||||
effective_site_name,
|
||||
modes=modes,
|
||||
site_description=site_description,
|
||||
site_author=site_author,
|
||||
wiki_dir=wiki_dir if wiki else None,
|
||||
)
|
||||
if kind == "lib":
|
||||
nav_block, _kind_icon = mkdocs_utils.build_lib_nav(nav_file, docs_dir)
|
||||
elif kind == "wiki":
|
||||
nav_block = mkdocs_utils.build_wiki_nav_block(wiki_dir)
|
||||
else:
|
||||
nav_block = [{"API Reference": "index.md"}]
|
||||
|
||||
click.echo("Running MkDocs build...")
|
||||
mkdocs_utils.build(mkdocs_yml)
|
||||
click.echo("MkDocs build completed.")
|
||||
out = docs_dir / f"mkdocs.{kind}.yml"
|
||||
click.echo(f"Generating MkDocs config {out}...")
|
||||
mkdocs_utils.generate_site_config(
|
||||
kind,
|
||||
kind_root,
|
||||
nav_block,
|
||||
out,
|
||||
site_name_kind,
|
||||
Path(os.path.relpath(kind_root, docs_dir)).as_posix(),
|
||||
Path(os.path.relpath(Path.cwd() / "site" / kind, docs_dir)).as_posix(),
|
||||
template=template,
|
||||
site_description=site_description,
|
||||
site_author=site_author,
|
||||
theme_icon=theme_icon,
|
||||
)
|
||||
config_paths.append(out)
|
||||
|
||||
click.echo("Running MkDocs builds...")
|
||||
mkdocs_utils.build_configs(config_paths)
|
||||
|
||||
if mcp:
|
||||
if not module:
|
||||
raise click.UsageError("--module is required for MCP build")
|
||||
|
||||
click.echo(f"Generating MCP resources in {out_dir}...")
|
||||
mcp_utils.generate_resources(module, project_name, out_dir)
|
||||
click.echo("MCP build completed.")
|
||||
@@ -262,12 +266,15 @@ def build(
|
||||
|
||||
@cli.command()
|
||||
@click.option("--mcp", is_flag=True, help="Serve MCP documentation")
|
||||
@click.option("--mkdocs", is_flag=True, help="Serve MkDocs site")
|
||||
@click.option("--mkdocs", is_flag=True, help="Serve an MkDocs site from --mkdocs-yml")
|
||||
@click.option("--lib", is_flag=True, help="Serve the lib MkDocs site")
|
||||
@click.option("--api", is_flag=True, help="Serve the API MkDocs site")
|
||||
@click.option("--wiki", is_flag=True, help="Serve the wiki MkDocs site")
|
||||
@click.option("--module", help="Python module to serve")
|
||||
@click.option(
|
||||
"--mkdocs-yml",
|
||||
type=click.Path(path_type=Path),
|
||||
default=Path("mkdocs.yml"),
|
||||
default=Path("docs/mkdocs.wiki.yml"),
|
||||
help="MkDocs config path",
|
||||
)
|
||||
@click.option(
|
||||
@@ -279,6 +286,9 @@ def build(
|
||||
def serve(
|
||||
mcp: bool,
|
||||
mkdocs: bool,
|
||||
lib: bool,
|
||||
api: bool,
|
||||
wiki: bool,
|
||||
module: str | None,
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
@@ -288,15 +298,28 @@ def serve(
|
||||
|
||||
Depending on the selected mode, this command starts either:
|
||||
|
||||
- A MkDocs development server for browsing documentation
|
||||
- A MkDocs development server for browsing a site, or
|
||||
- An MCP server exposing structured documentation resources
|
||||
|
||||
The kind flags (`--lib`, `--api`, `--wiki`) select the generated
|
||||
per-kind config (`docs/mkdocs.{kind}.yml`); `--mkdocs` serves the config
|
||||
passed via `--mkdocs-yml`.
|
||||
|
||||
Args:
|
||||
mcp (bool):
|
||||
Serve documentation using the MCP server.
|
||||
|
||||
mkdocs (bool):
|
||||
Serve the MkDocs development site.
|
||||
Serve the MkDocs development site from ``--mkdocs-yml``.
|
||||
|
||||
lib (bool):
|
||||
Serve the lib MkDocs site.
|
||||
|
||||
api (bool):
|
||||
Serve the API MkDocs site.
|
||||
|
||||
wiki (bool):
|
||||
Serve the wiki MkDocs site.
|
||||
|
||||
module (str | None):
|
||||
Python module import path to serve via MCP.
|
||||
@@ -311,17 +334,37 @@ def serve(
|
||||
click.UsageError:
|
||||
If invalid or conflicting options are provided.
|
||||
"""
|
||||
if mcp and mkdocs:
|
||||
raise click.UsageError("Cannot specify both --mcp and --mkdocs")
|
||||
if not mcp and not mkdocs:
|
||||
raise click.UsageError("Must specify either --mcp or --mkdocs")
|
||||
if mcp and not module:
|
||||
raise click.UsageError("--module is required for MCP serve")
|
||||
selected = [
|
||||
name
|
||||
for name, enabled in (
|
||||
("mcp", mcp),
|
||||
("mkdocs", mkdocs),
|
||||
("lib", lib),
|
||||
("api", api),
|
||||
("wiki", wiki),
|
||||
)
|
||||
if enabled
|
||||
]
|
||||
if len(selected) != 1:
|
||||
raise click.UsageError(
|
||||
"Must specify exactly one of --mcp, --mkdocs, --lib, --api, --wiki"
|
||||
)
|
||||
|
||||
if mkdocs:
|
||||
mkdocs_utils.serve(mkdocs_yml)
|
||||
elif mcp:
|
||||
if mcp:
|
||||
if not module:
|
||||
raise click.UsageError("--module is required for MCP serve")
|
||||
mcp_utils.serve(module, out_dir)
|
||||
return
|
||||
|
||||
config = mkdocs_yml
|
||||
if lib:
|
||||
config = Path("docs/mkdocs.lib.yml")
|
||||
elif api:
|
||||
config = Path("docs/mkdocs.api.yml")
|
||||
elif wiki:
|
||||
config = Path("docs/mkdocs.wiki.yml")
|
||||
|
||||
mkdocs_utils.serve(config)
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
||||
@@ -20,12 +20,14 @@ def build(
|
||||
wiki_dir: Path,
|
||||
nav_file: Path,
|
||||
template: Path | None,
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
) -> None: ...
|
||||
def serve(
|
||||
mcp: bool,
|
||||
mkdocs: bool,
|
||||
lib: bool,
|
||||
api: bool,
|
||||
wiki: bool,
|
||||
module: str | None,
|
||||
mkdocs_yml: Path,
|
||||
out_dir: Path,
|
||||
|
||||
@@ -6,17 +6,19 @@ Utilities for working with MkDocs in the doc-forge CLI.
|
||||
---
|
||||
|
||||
Notes:
|
||||
- A single generated `mkdocs.yml` serves lib, api, and wiki content with
|
||||
merged navigation. Wiki navigation, when enabled, precedes every other
|
||||
group and its `index.md` becomes the site `Home`.
|
||||
- A separate `mkdocs.{kind}.yml` configuration and build is emitted per
|
||||
enabled kind (lib, api, wiki), each scoped to its own `docs_dir` and
|
||||
written into its own `site_dir` (`site/lib`, `site/api`, `site/wiki`).
|
||||
- Navigation blocks are re-rooted per kind: the wiki navigation drops its
|
||||
leading `wiki/` scope and the resolved nav spec drops its `lib/` scope.
|
||||
|
||||
---
|
||||
"""
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
import click
|
||||
import yaml
|
||||
@@ -83,53 +85,171 @@ def generate_sources(
|
||||
)
|
||||
|
||||
|
||||
def generate_config(
|
||||
docs_dir: Path,
|
||||
def build_lib_nav(
|
||||
nav_file: Path,
|
||||
template: Path | None,
|
||||
out: Path,
|
||||
site_name: str,
|
||||
modes: Iterable[str] | None = None,
|
||||
site_description: str | None = None,
|
||||
site_author: str | None = None,
|
||||
wiki_dir: Path | None = None,
|
||||
) -> None:
|
||||
docs_root: Path,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, str] | None]:
|
||||
"""
|
||||
Generate an `mkdocs.yml` configuration file.
|
||||
Build the re-rooted navigation block for a lib site.
|
||||
|
||||
The configuration is created by combining a template configuration
|
||||
with a navigation structure derived from the docforge navigation
|
||||
specification (and, when a wiki directory is provided, from the wiki
|
||||
file structure).
|
||||
|
||||
The ``docs_dir`` is always written relative to the MkDocs root and is
|
||||
expected to be the shared documentation parent (for example ``docs``),
|
||||
with generated sources nested under ``lib/`` or ``api/`` subdirectories
|
||||
and hand-written wiki content under a ``wiki/`` subdirectory.
|
||||
The navigation specification is resolved against the shared documentation
|
||||
root and every resulting path is re-rooted relative to the ``lib``
|
||||
subdirectory by stripping its leading ``lib/`` scope component.
|
||||
|
||||
Args:
|
||||
docs_dir (Path):
|
||||
Shared documentation root used as the MkDocs ``docs_dir``.
|
||||
|
||||
nav_file (Path):
|
||||
Path to the `docforge.nav.yml` navigation specification.
|
||||
|
||||
template (Path | None):
|
||||
Optional path to a fully custom MkDocs configuration template.
|
||||
If not provided, built-in templates are merged; the provided
|
||||
template replaces the built-in templates entirely.
|
||||
docs_root (Path):
|
||||
Shared documentation root containing the ``lib`` sources.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict[str, Any]], dict[str, str] | None]:
|
||||
The re-rooted navigation block and the optional theme icon
|
||||
mapping from the specification.
|
||||
|
||||
Raises:
|
||||
click.FileError:
|
||||
If the navigation specification cannot be found.
|
||||
"""
|
||||
if not nav_file.exists():
|
||||
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
||||
|
||||
spec = load_nav_spec(nav_file)
|
||||
resolved = resolve_nav(spec, docs_root)
|
||||
block = MkDocsNavEmitter().emit(resolved)
|
||||
return cast(list[dict[str, Any]], _strip_scope(block, "lib")), spec.icon
|
||||
|
||||
|
||||
def build_wiki_nav_block(wiki_dir: Path) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Build the re-rooted navigation block for a wiki site.
|
||||
|
||||
The wiki navigation derived from the wiki file structure is re-rooted
|
||||
relative to the wiki directory itself by stripping the leading ``wiki/``
|
||||
scope component.
|
||||
|
||||
Args:
|
||||
wiki_dir (Path):
|
||||
Path to the hand-written wiki directory, for example ``docs/wiki``.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]:
|
||||
Navigation entries relative to the wiki directory.
|
||||
|
||||
Raises:
|
||||
click.FileError:
|
||||
If the wiki directory does not exist.
|
||||
"""
|
||||
if not wiki_dir.exists():
|
||||
raise click.FileError(str(wiki_dir), hint="Wiki dir not found")
|
||||
|
||||
return cast(list[dict[str, Any]], _strip_scope(build_wiki_nav(wiki_dir), "wiki"))
|
||||
|
||||
|
||||
def load_spec_icon(nav_file: Path) -> dict[str, str] | None:
|
||||
"""
|
||||
Load the theme icon mapping from a navigation specification.
|
||||
|
||||
Args:
|
||||
nav_file (Path):
|
||||
Path to the navigation specification file.
|
||||
|
||||
Returns:
|
||||
dict[str, str] | None:
|
||||
The icon mapping, or ``None`` when the specification file is
|
||||
absent or cannot be parsed.
|
||||
"""
|
||||
if not nav_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
return load_nav_spec(nav_file).icon
|
||||
except (ValueError, yaml.YAMLError):
|
||||
return None
|
||||
|
||||
|
||||
def _strip_scope(value: object, scope: str) -> object:
|
||||
"""
|
||||
Re-root navigation paths by removing a leading scope component.
|
||||
|
||||
The navigation block is walked recursively and every relative path
|
||||
string that starts with ``{scope}/`` has that prefix removed, so a lib
|
||||
or wiki site scoped to its own ``docs_dir`` can reuse paths that were
|
||||
originally written relative to the shared documentation root.
|
||||
|
||||
Args:
|
||||
value (object):
|
||||
Navigation block, group list, path string, or scalar value.
|
||||
scope (str):
|
||||
Leading path component to strip, for example ``lib`` or ``wiki``.
|
||||
|
||||
Returns:
|
||||
object:
|
||||
The navigation structure with re-rooted paths.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
prefix = f"{scope}/"
|
||||
return value[len(prefix) :] if value.startswith(prefix) else value
|
||||
if isinstance(value, list):
|
||||
return [_strip_scope(item, scope) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _strip_scope(val, scope) for key, val in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
def generate_site_config(
|
||||
kind: str,
|
||||
kind_root: Path,
|
||||
nav_block: list[dict[str, Any]],
|
||||
out: Path,
|
||||
site_name: str,
|
||||
docs_dir: str,
|
||||
site_dir: str,
|
||||
template: Path | None = None,
|
||||
site_description: str | None = None,
|
||||
site_author: str | None = None,
|
||||
theme_icon: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Generate a per-kind `mkdocs.{kind}.yml` configuration file.
|
||||
|
||||
The configuration is created by merging the shared ``mkdocs.common.yml``
|
||||
template with the fragment contributed by the kind (``lib``, ``api``, or
|
||||
``wiki``). Both ``docs_dir`` and ``site_dir`` are written relative to the
|
||||
configuration file's directory: the kind's sources when expressed as a
|
||||
sibling path (for example ``lib``) and the per-kind site output (for
|
||||
example ``../site/lib``).
|
||||
|
||||
Args:
|
||||
kind (str):
|
||||
Documentation kind, one of ``lib``, ``api``, or ``wiki``.
|
||||
|
||||
kind_root (Path):
|
||||
Directory scoped to the kind (for example ``docs/lib``) that
|
||||
serves as the MkDocs ``docs_dir``.
|
||||
|
||||
nav_block (list[dict[str, Any]]):
|
||||
Re-rooted navigation entries for the kind's site.
|
||||
|
||||
out (Path):
|
||||
Destination path where the generated `mkdocs.yml` file will be written.
|
||||
Destination path where the generated ``mkdocs.{kind}.yml`` file
|
||||
is written.
|
||||
|
||||
site_name (str):
|
||||
Display name for the generated documentation site.
|
||||
|
||||
modes (Iterable[str] | None):
|
||||
Documentation modes to enable. Each mode contributes its own
|
||||
built-in template fragment (for example ``lib``, ``api``, or
|
||||
``wiki``), merged on top of the shared ``mkdocs.common.yml``
|
||||
template.
|
||||
docs_dir (str):
|
||||
MkDocs ``docs_dir`` value, relative to the configuration
|
||||
file's directory.
|
||||
|
||||
site_dir (str):
|
||||
MkDocs ``site_dir`` value, relative to the configuration
|
||||
file's directory.
|
||||
|
||||
template (Path | None):
|
||||
Optional path to a fully custom MkDocs configuration template
|
||||
that replaces the built-in templates entirely.
|
||||
|
||||
site_description (str | None):
|
||||
Optional site description written into the configuration.
|
||||
@@ -137,50 +257,26 @@ def generate_config(
|
||||
site_author (str | None):
|
||||
Optional site author written into the configuration.
|
||||
|
||||
wiki_dir (Path | None):
|
||||
Optional path to a hand-written wiki directory (for example
|
||||
``docs/wiki``). When provided, the site navigation is derived
|
||||
from the wiki file structure and placed before the navigation
|
||||
groups defined in ``nav_file``.
|
||||
|
||||
Raises:
|
||||
click.FileError:
|
||||
If the navigation specification, template, or wiki directory
|
||||
cannot be found.
|
||||
theme_icon (dict[str, str] | None):
|
||||
Optional mapping of theme icon entries injected as
|
||||
``theme.icon``.
|
||||
"""
|
||||
if not nav_file.exists() and wiki_dir is None:
|
||||
raise click.FileError(str(nav_file), hint="Nav spec not found")
|
||||
|
||||
nav_block: list[dict] = []
|
||||
if nav_file.exists():
|
||||
spec = load_nav_spec(nav_file)
|
||||
resolved = resolve_nav(spec, docs_dir)
|
||||
nav_block = MkDocsNavEmitter().emit(resolved)
|
||||
else:
|
||||
spec = None
|
||||
|
||||
if wiki_dir is not None:
|
||||
if not wiki_dir.exists():
|
||||
raise click.FileError(str(wiki_dir), hint="Wiki dir not found")
|
||||
wiki_nav = build_wiki_nav(wiki_dir)
|
||||
if wiki_nav:
|
||||
nav_block = wiki_nav + [entry for entry in nav_block if "Home" not in entry]
|
||||
|
||||
data = _load_template(template, modes)
|
||||
data = _load_template(template, [kind])
|
||||
|
||||
data["site_name"] = site_name
|
||||
if site_description:
|
||||
data["site_description"] = site_description
|
||||
if site_author:
|
||||
data["site_author"] = site_author
|
||||
data["docs_dir"] = Path(os.path.relpath(docs_dir, out.parent)).as_posix()
|
||||
data["docs_dir"] = docs_dir
|
||||
data["site_dir"] = site_dir
|
||||
data["nav"] = nav_block
|
||||
|
||||
if spec is not None and spec.icon:
|
||||
if theme_icon:
|
||||
theme = data.setdefault("theme", {})
|
||||
if not isinstance(theme, dict):
|
||||
theme = {}
|
||||
theme["icon"] = spec.icon
|
||||
theme["icon"] = theme_icon
|
||||
data["theme"] = theme
|
||||
|
||||
out.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
||||
@@ -310,28 +406,28 @@ def _deep_merge(base: dict, part: dict) -> dict:
|
||||
return base
|
||||
|
||||
|
||||
def build(mkdocs_yml: Path) -> None:
|
||||
def build_configs(yml_paths: Iterable[Path]) -> None:
|
||||
"""
|
||||
Build the MkDocs documentation site.
|
||||
Build the MkDocs documentation site for every given configuration.
|
||||
|
||||
This function loads the MkDocs configuration and runs the MkDocs
|
||||
build command to generate the final static documentation site.
|
||||
Each configuration file is loaded and built in turn, producing the
|
||||
per-kind static sites (``site/lib``, ``site/api``, ``site/wiki``).
|
||||
|
||||
Args:
|
||||
mkdocs_yml (Path):
|
||||
Path to the `mkdocs.yml` configuration file.
|
||||
yml_paths (Iterable[Path]):
|
||||
Configuration files to build, in order.
|
||||
|
||||
Raises:
|
||||
click.ClickException:
|
||||
If the configuration file does not exist.
|
||||
If a configuration file does not exist.
|
||||
"""
|
||||
if not mkdocs_yml.exists():
|
||||
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
|
||||
|
||||
from mkdocs.commands.build import build as mkdocs_build
|
||||
from mkdocs.config import load_config
|
||||
|
||||
mkdocs_build(load_config(str(mkdocs_yml)))
|
||||
for yml in yml_paths:
|
||||
if not yml.exists():
|
||||
raise click.ClickException(f"mkdocs.yml not found: {yml}")
|
||||
mkdocs_build(load_config(str(yml)))
|
||||
|
||||
|
||||
def serve(mkdocs_yml: Path) -> None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
def generate_sources(
|
||||
module: str,
|
||||
@@ -8,16 +9,25 @@ def generate_sources(
|
||||
module_is_source: bool | None = None,
|
||||
readme_dir: Path | None = None,
|
||||
) -> None: ...
|
||||
def generate_config(
|
||||
docs_dir: Path,
|
||||
def build_lib_nav(
|
||||
nav_file: Path,
|
||||
template: Path | None,
|
||||
docs_root: Path,
|
||||
) -> tuple[list[dict[str, Any]], dict[str, str] | None]: ...
|
||||
def build_wiki_nav_block(wiki_dir: Path) -> list[dict[str, Any]]: ...
|
||||
def load_spec_icon(nav_file: Path) -> dict[str, str] | None: ...
|
||||
def _strip_scope(value: object, scope: str) -> object: ...
|
||||
def generate_site_config(
|
||||
kind: str,
|
||||
kind_root: Path,
|
||||
nav_block: list[dict[str, Any]],
|
||||
out: Path,
|
||||
site_name: str,
|
||||
modes: Iterable[str] | None = None,
|
||||
docs_dir: str,
|
||||
site_dir: str,
|
||||
template: Path | None = None,
|
||||
site_description: str | None = None,
|
||||
site_author: str | None = None,
|
||||
wiki_dir: Path | None = None,
|
||||
theme_icon: dict[str, str] | None = None,
|
||||
) -> None: ...
|
||||
def build(mkdocs_yml: Path) -> None: ...
|
||||
def build_configs(yml_paths: Iterable[Path]) -> None: ...
|
||||
def serve(mkdocs_yml: Path) -> None: ...
|
||||
|
||||
Reference in New Issue
Block a user