diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 7ca3345fd0..a734d4216d 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -10,11 +10,13 @@ build:
tools:
python: "3.12"
jobs:
- post_create_environment:
+ install:
- python install-pdm.py --path ~/.local/pdm
- ~/.local/pdm/bin/pdm --version
- post_install:
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/pdm/bin/pdm install -dG doc
-
-mkdocs:
- configuration: mkdocs.yml
+ build:
+ html:
+ - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/pdm/bin/pdm run python tasks/render_reference_docs.py
+ - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH ~/.local/pdm/bin/pdm run zensical build
+ - mkdir --parents $READTHEDOCS_OUTPUT/html/
+ - cp --recursive site/. $READTHEDOCS_OUTPUT/html/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 859fe8688a..0215ec9c44 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -160,12 +160,22 @@ PDM docs development requires a few additional dependencies. Install them as:
sudo apt install libffi-dev # Or equivalent with the package manager of your choice
```
+The Zensical-based docs toolchain currently requires Python 3.10+.
+
Now, whenever you make some changes to the `docs/` and you want to preview the build result, simply do:
```bash
pdm run doc
```
+The CLI and configuration reference pages are generated from
+`tasks/doc_templates/cli.md.in` and `tasks/doc_templates/configuration.md.in`.
+If you change command help text or config metadata, regenerate them with:
+
+```bash
+pdm run python tasks/render_reference_docs.py
+```
+
## Release
Once all changes are done and ready to release, you can preview the changelog contents by running:
diff --git a/README.md b/README.md
index a0a9ffaaca..36fc39b51d 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ A modern Python package and dependency manager supporting the latest PEP standar

-[](https://pdm-project.org)
+[](https://pdm-project.org)
[](https://twitter.com/pdm_project)
[](https://discord.gg/Phn8smztpv)
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
index bfdc826259..68fdf8cc1e 100644
--- a/docs/assets/extra.css
+++ b/docs/assets/extra.css
@@ -1,9 +1,89 @@
+:root {
+ --pdm-brand-primary: #9b6bd3;
+ --pdm-brand-primary-light: #cdb2eb;
+ --pdm-brand-primary-dark: #7343a8;
+ --pdm-brand-accent: #0f8f87;
+ --pdm-brand-accent-soft: rgba(15, 143, 135, 0.14);
+ --pdm-brand-link: #7d46c3;
+ --pdm-brand-focus: rgba(155, 107, 211, 0.28);
+}
+
+[data-md-color-scheme="default"] {
+ --md-primary-fg-color: var(--pdm-brand-primary);
+ --md-primary-fg-color--light: var(--pdm-brand-primary-light);
+ --md-primary-fg-color--dark: var(--pdm-brand-primary-dark);
+ --md-accent-fg-color: var(--pdm-brand-accent);
+ --md-accent-fg-color--transparent: var(--pdm-brand-accent-soft);
+ --md-typeset-a-color: var(--pdm-brand-link);
+ --md-default-bg-color: #fcfaff;
+ --md-default-fg-color: #241a33;
+ --md-default-fg-color--light: #6f5a8d;
+ --md-code-bg-color: #f4eefc;
+ --md-code-fg-color: #472d6b;
+}
+
+[data-md-color-scheme="slate"] {
+ --md-primary-fg-color: #b88fe5;
+ --md-primary-fg-color--light: #d6bdf2;
+ --md-primary-fg-color--dark: #9262c8;
+ --md-accent-fg-color: #54d6ca;
+ --md-accent-fg-color--transparent: rgba(84, 214, 202, 0.16);
+ --md-typeset-a-color: #d0b2f2;
+ --md-default-bg-color: #110d19;
+ --md-default-fg-color: #f0ebf8;
+ --md-default-fg-color--light: #b7aacd;
+ --md-code-bg-color: #1d1629;
+ --md-code-fg-color: #e7dbf9;
+}
+
a.pdm-expansions {
cursor: pointer;
font-weight: bold;
color: currentColor;
}
+.md-typeset a:hover,
+.md-nav__link:hover,
+.md-tabs__link:hover {
+ color: var(--md-accent-fg-color);
+}
+
+.md-search__form,
+.md-search__input {
+ border-radius: 999px;
+}
+
+.md-search__input,
+.md-typeset .md-button,
+.md-header__button.md-logo {
+ transition: box-shadow 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
+}
+
+.md-search__input:focus,
+.md-search__input:focus-visible,
+.md-typeset .md-button:focus-visible,
+.md-nav__link:focus-visible,
+.md-tabs__link:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 0.2rem var(--pdm-brand-focus);
+}
+
+.md-typeset .md-button--primary {
+ box-shadow: 0 10px 24px rgba(115, 67, 168, 0.18);
+}
+
+.md-typeset .md-button--primary:hover {
+ transform: translateY(-1px);
+}
+
+.md-tabs {
+ box-shadow: inset 0 -1px 0 var(--md-accent-fg-color--transparent);
+}
+
+.md-typeset code {
+ border-radius: 0.35rem;
+}
+
.bot-container {
z-index: 9;
position: fixed;
diff --git a/docs/reference/.gitignore b/docs/reference/.gitignore
new file mode 100644
index 0000000000..c3f4ea541a
--- /dev/null
+++ b/docs/reference/.gitignore
@@ -0,0 +1,2 @@
+/cli.md
+/configuration.md
diff --git a/docs/reference/api.md b/docs/reference/api.md
index a60cd7ff38..e266537f63 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -14,7 +14,8 @@
## Signals
-+++ 1.12.0
+!!! tip
+ Added in 1.12.0.
::: pdm.signals
options:
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
deleted file mode 100644
index 0986439b3d..0000000000
--- a/docs/reference/cli.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# CLI Reference
-
-```python exec="1" idprefix=""
-import argparse
-import re
-from pdm.core import Core
-
-parser = Core().parser
-
-MONOSPACED = ("pyproject.toml", "pdm.lock", ".pdm-python", ":pre", ":post", ":all")
-
-def clean_help(help: str) -> str:
- # Make dunders monospaced avoiding italic markdown rendering
- help = re.sub(r"__([\w\d\_]+)__", r"`__\1__`", help)
- # Make env vars monospaced
- help = re.sub(r"env var: ([A-Z_]+)", r"env var: `\1`", help)
- for monospaced in MONOSPACED:
- help = re.sub(rf"\s(['\"]?{monospaced}['\"]?)", f"`{monospaced}`", help)
- return help
-
-
-def render_parser(
- parser: argparse.ArgumentParser, title: str, heading_level: int = 2
-) -> str:
- """Render the parser help documents as a string."""
- result = [f"{'#' * heading_level} {title}\n"]
- if parser.description and title != "pdm":
- result.append("> " + parser.description + "\n")
-
- for group in sorted(
- parser._action_groups, key=lambda g: g.title.lower(), reverse=True
- ):
- if not any(
- bool(action.option_strings or action.dest)
- or isinstance(action, argparse._SubParsersAction)
- for action in group._group_actions
- ):
- continue
-
- result.append(f"{group.title.title()}:\n")
- for action in group._group_actions:
- if isinstance(action, argparse._SubParsersAction):
- for name, subparser in action._name_parser_map.items():
- result.append(render_parser(subparser, name, heading_level + 1))
- continue
-
- opts = [f"`{opt}`" for opt in action.option_strings]
- if not opts:
- line = f"- `{action.dest}`"
- else:
- line = f"- {', '.join(opts)}"
- if action.metavar:
- line += f" `{action.metavar}`"
- line += f": {clean_help(action.help)}"
- if action.default and action.default != argparse.SUPPRESS:
- default = action.default
- if any(opt.startswith("--no-") for opt in action.option_strings) and default is True:
- default = not default
- line += f" (default: `{default}`)"
- result.append(line)
- result.append("")
-
- return "\n".join(result)
-
-
-print(render_parser(parser, "pdm"))
-```
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md
deleted file mode 100644
index 9e6dcf41c0..0000000000
--- a/docs/reference/configuration.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Configurations
-
-[pdm-config]: ../reference/cli.md#config
-
-## Color Theme
-
-The default theme used by PDM is as follows:
-
-| Key | Default Style |
-| --------- | ------------------------------------------------------------ |
-| `primary` | cyan |
-| `success` | green |
-| `warning` | yellow |
-| `error` | red |
-| `info` | blue |
-| `req` | bold green |
-
-You can change the theme colors with [`pdm config`][pdm-config] command. For example, to change the `primary` color to `magenta`:
-
-```bash
-pdm config theme.primary magenta
-```
-
-Or use a hex color code:
-
-```bash
-pdm config theme.success '#51c7bd'
-```
-
-## Available Configurations
-
-The following configuration items can be retrieved and modified by [`pdm config`][pdm-config] command.
-
-!!! note "Environment Variable Overrides"
- If the corresponding env var is set, the value will take precedence over what is saved in the config file.
-
-```python exec="on"
-from pdm.project.config import Config
-
-print("| Config Item | Description | Default Value | Available in Project | Env var |")
-print("| --- | --- | --- | --- | --- |")
-for key, value in Config._config_map.items():
- print(f"| `{key}` | {value.description} | {('`%s`' % value.default) if value.should_show() else ''} | {'No' if value.global_only else 'Yes'} | {('`%s`' % value.env_var) if value.env_var else ''} |")
-print("""\
-| `pypi..url` | The URL of custom package source | `https://pypi.org/simple` | Yes | |
-| `pypi..username` | The username to access custom source | | Yes | |
-| `pypi..password` | The password to access custom source | | Yes | |
-| `pypi..type` | `index` or `find_links` | `index` | Yes | |
-| `pypi..verify_ssl` | Verify SSL certificate when query custom source | `True` | Yes | |
-| `repository..url` | The URL of custom package source | `https://pypi.org/simple` | Yes | |
-| `repository..username` | The username to access custom repository | | Yes | |
-| `repository..password` | The password to access custom repository | | Yes | |
-| `repository..ca_certs` | Path to a PEM-encoded CA cert bundle (used for server cert verification) | The CA certificates from [certifi](https://pypi.org/project/certifi/) | Yes | |
-| `repository..verify_ssl` | Verify SSL certificate when uploading to repository | `True` | Yes | |
-""")
-```
diff --git a/docs/usage/config.md b/docs/usage/config.md
index dcaf6c5cbd..ece1038da6 100644
--- a/docs/usage/config.md
+++ b/docs/usage/config.md
@@ -336,7 +336,8 @@ url = "https://pkgs.dev.azure.com/[org name]/_packaging/[feed name]/pypi/simple/
## Exclude specific packages and their dependencies from the lock file
-+++ 2.12.0
+!!! tip
+ Added in 2.12.0.
Sometimes you don't even want to include certain packages in the locked file because you are sure they won't be used by any code. In this case, you can completely skip them and their dependencies during dependency resolution:
@@ -349,7 +350,8 @@ With this config, `requests` will not be locked in the lockfile, and its depende
## Passing constant arguments to every pdm invocation
-+++ 2.7.0
+!!! tip
+ Added in 2.7.0.
You can add extra options passed to individual pdm commands by `tool.pdm.options` configuration:
@@ -365,7 +367,8 @@ These options will be added right after the command name. For instance, based on
## Ignore package warnings
-+++ 2.10.0
+!!! tip
+ Added in 2.10.0.
You may see some warnings when resolving dependencies like this:
diff --git a/docs/usage/dependency.md b/docs/usage/dependency.md
index abfd326ef0..df2ada85a3 100644
--- a/docs/usage/dependency.md
+++ b/docs/usage/dependency.md
@@ -130,7 +130,8 @@ These variables will be read from the environment variables when installing the
### Add development only dependencies
-+++ 1.5.0
+!!! tip
+ Added in 1.5.0.
PDM also supports defining groups of dependencies that are useful for development,
e.g. some for testing and others for linting. We usually don't want these dependencies to appear in the distribution's metadata
@@ -270,7 +271,8 @@ pdm remove -dG test pytest-cov
## List outdated packages and the latest versions
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
To list outdated packages and the latest versions:
@@ -330,7 +332,8 @@ In PDM, there are two ways to specify overrides:
### In the project file
-+++ 1.12.0
+!!! tip
+ Added in 1.12.0.
You can specify the overrides in the `pyproject.toml` file, under the `[tool.pdm.resolution.overrides]` table:
@@ -345,7 +348,8 @@ Each entry in the table is a package name and a version specifier. The version s
### Via CLI option
-+++ 2.17.0
+!!! tip
+ Added in 2.17.0.
PDM also supports reading dependency overrides from a requirements file. The file works similarly to the constraint file in pip(`--constraint constraints.txt`), and the syntax is the same as the requirements file:
diff --git a/docs/usage/lock-targets.md b/docs/usage/lock-targets.md
index d458b29f97..5c08778cf6 100644
--- a/docs/usage/lock-targets.md
+++ b/docs/usage/lock-targets.md
@@ -1,6 +1,7 @@
# Lock for specific platforms or Python versions
-+++ 2.17.0
+!!! tip
+ Added in 2.17.0.
By default, PDM will try to make a lock file that works on all platforms within the Python versions specified by [`requires-python` in `pyproject.toml`](./project.md#specify-requires-python). This is very convenient during development. You can generate a lock file in your development environment and then use this lock file to replicate the same dependency versions in CI/CD or production environments.
diff --git a/docs/usage/lockfile.md b/docs/usage/lockfile.md
index b5f921f651..4924d0a703 100644
--- a/docs/usage/lockfile.md
+++ b/docs/usage/lockfile.md
@@ -49,7 +49,8 @@ This command also refreshes _all_ file hashes recorded in the lock file.
PDM supports two lock file formats: `pdm`(default file name is `pdm.lock`) and `pylock`(default file name is `pylock.toml`). The default format is `pdm`.
-+++ 2.25.0
+!!! tip
+ Added in 2.25.0.
Added experimental support for the [PEP 751](https://packaging.python.org/en/latest/specifications/pylock-toml/#pylock-toml-spec) pylock file format. It's a standard lock file format designed to minimize discrepancies among different Python package managers, enhancing interoperability with other tools. It is set to become the default in a future version of PDM. Read the specification for more details.
@@ -127,7 +128,8 @@ This command makes the lockfile not cross-platform.
### Cross platform
-+++ 2.6.0
+!!! tip
+ Added in 2.6.0.
!!! warning "Deprecated in 2.17.0"
See [Lock for specific platforms or Python versions](./lock-targets.md) for the new behavior.
@@ -143,7 +145,8 @@ pdm lock --strategy no_cross_platform
### Static URLs
-+++ 2.8.0
+!!! tip
+ Added in 2.8.0.
By default, PDM only stores the filenames of the packages in the lockfile, which benefits the reusability across different package indexes.
However, if you want to store the static URLs of the packages in the lockfile, you can pass the `--strategy static_urls` option to `pdm lock`:
@@ -156,7 +159,8 @@ The settings will be saved and remembered for the same lockfile. You can also pa
### Direct minimal versions
-+++ 2.10.0
+!!! tip
+ Added in 2.10.0.
When it is enabled by passing `--strategy direct_minimal_versions`, dependencies specified in the `pyproject.toml` will be resolved to the minimal versions available, rather than the latest versions. This is useful when you want to test the compatibility of your project within a range of dependency versions.
@@ -168,7 +172,8 @@ For example, if you specified `flask>=2.0` in the `pyproject.toml`, `flask` will
### Inherit the metadata from parents
-+++ 2.11.0
+!!! tip
+ Added in 2.11.0.
Previously, the `pdm lock` command would record package metadata as it is. When installing, PDM would start from the top requirements and traverse down to the leaf node of the dependency tree. It would then evaluate any marker it encounters against the current environment. If a marker is not satisfied, the package would be discarded. In other words, we need an additional "resolution" step in installation.
@@ -176,7 +181,8 @@ When the `inherit_metadata` strategy is enabled, PDM will inherit and merge envi
### Exclude packages newer than specific date
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
You can exclude packages that are newer than a specified date by passing the `--exclude-newer` option to `pdm lock`. This is useful when you want to lock the dependencies to a specific date, for example, to ensure reproducibility of the build.
@@ -262,7 +268,8 @@ pdm export -o requirements.txt
!!! tip
You can also run `pdm export` with a [`.pre-commit` hook](./advanced.md#hooks-for-pre-commit).
-+++ 2.24.0
+!!! tip
+ Added in 2.24.0.
Additionally, PDM supports exporting to `pylock.toml` format as defined by [PEP 751](https://packaging.python.org/en/latest/specifications/pylock-toml/#pylock-toml-spec). The following command will convert your lock file to a PEP 751 compatible format:
diff --git a/docs/usage/project.md b/docs/usage/project.md
index 04d5510d9c..0ab2d4368a 100644
--- a/docs/usage/project.md
+++ b/docs/usage/project.md
@@ -25,7 +25,8 @@ will be stored in `.pdm-python` and used by subsequent commands. You can also ch
Alternatively, you can specify the Python interpreter path via `PDM_PYTHON` environment variable. When it is set, the path saved in `.pdm-python` will be ignored.
-+++ 2.23.0
+!!! tip
+ Added in 2.23.0.
If `.python-version` is present in the project root or `PDM_PYTHON_VERSION` env var is set, PDM will use the Python version specified in it. The file or env var should contain a valid Python version string, such as `3.11`.
@@ -34,7 +35,8 @@ If `.python-version` is present in the project root or `PDM_PYTHON_VERSION` env
### Install Python interpreters with PDM
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
PDM supports installing additional Python interpreters from [@indygreg's python-build-standalone](https://github.com/indygreg/python-build-standalone)
with the `pdm python install` command. For example, to install CPython 3.9.8:
@@ -77,7 +79,8 @@ pdm python install 3.13t
### Installation strategy based on `requires-python`
-+++ 2.16.0
+!!! tip
+ Added in 2.16.0.
If Python `version` is not given, PDM will try to install the best match for the current platform/arch combination
based on `requires-python` from `pyproject.toml` (if pyproject.toml or requires-python attribute is not available,
diff --git a/docs/usage/scripts.md b/docs/usage/scripts.md
index b39cf862c4..f37ac40e47 100644
--- a/docs/usage/scripts.md
+++ b/docs/usage/scripts.md
@@ -12,7 +12,8 @@ It will run `flask run -p 54321` in the environment that is aware of packages in
## Single-file Scripts
-+++ 2.16.0
+!!! tip
+ Added in 2.16.0.
PDM can run single-file scripts with [inline script metadata](https://peps.python.org/pep-0723/) specified by PEP 723.
@@ -172,7 +173,8 @@ all = {composite = ["lint", "test"]}
Running `pdm run all` will run `lint` first and then `test` if `lint` succeeded.
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
To override the default behavior and continue the execution of the remaining scripts after a failure,
set the `keep_going` option to `true`:
@@ -272,7 +274,8 @@ start.env_file.override = ".env"
### `working_dir`
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
You can set the current working directory for the script:
@@ -284,7 +287,8 @@ start.working_dir = "subdir"
Relative paths are resolved against the project root.
-+++ 2.20.2
+!!! tip
+ Added in 2.20.2.
To identify the original calling working directory, each script gets the environment variable `PDM_RUN_CWD` injected.
diff --git a/docs/usage/uv.md b/docs/usage/uv.md
index 80c40df075..a74625bdbd 100644
--- a/docs/usage/uv.md
+++ b/docs/usage/uv.md
@@ -1,6 +1,7 @@
# Use uv (Experimental)
-+++ 2.19.0
+!!! tip
+ Added in 2.19.0.
PDM has experimental support for [uv](https://github.com/astral-sh/uv) as the resolver and installer. To enable it:
diff --git a/docs/usage/venv.md b/docs/usage/venv.md
index 5cd504519f..03ac3a3c9f 100644
--- a/docs/usage/venv.md
+++ b/docs/usage/venv.md
@@ -21,7 +21,8 @@ You can choose the backend used by PDM to create a virtualenv. Currently it supp
You can change it by `pdm config venv.backend [virtualenv|venv|conda]`.
-+++ 2.13.0
+!!! tip
+ Added in 2.13.0.
Moreover, when `python.use_venv` config is set to `true`, PDM will always try to create a virtualenv when using `pdm use` to switch the Python interpreter.
diff --git a/mkdocs.yml b/mkdocs.yml
index db8018c14c..ccc268ea42 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,7 +5,6 @@ repo_url: https://github.com/pdm-project/pdm
edit_uri: edit/main/docs
theme:
- name: material
palette:
- media: "(prefers-color-scheme)"
toggle:
@@ -13,15 +12,15 @@ theme:
name: Switch to light mode
- scheme: default
media: "(prefers-color-scheme: light)"
- primary: deep purple
- accent: teal
+ primary: custom
+ accent: custom
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
media: "(prefers-color-scheme: dark)"
- primary: deep purple
- accent: teal
+ primary: custom
+ accent: custom
toggle:
icon: material/brightness-4
name: Switch to system preference
@@ -37,35 +36,12 @@ theme:
custom_dir: docs/overrides
plugins:
- - search
- - markdown-exec
- - "mkdocs-version-annotations":
- version_added_admonition: "tip"
- mkdocstrings:
enable_inventory: true
handlers:
python:
options:
docstring_style: google
- - redirects:
- redirect_maps:
- 'plugin/fixtures.md': 'dev/fixtures.md'
- 'plugin/write.md': 'dev/write.md'
- 'pyproject/build.md': 'reference/build.md'
- 'plugin/reference.md': 'reference/api.md'
- 'usage/cli_reference.md': 'reference/cli.md'
- 'usage/configuration.md': 'reference/configuration.md'
- 'pyproject/pep621.md': 'reference/pep621.md'
- - llmstxt:
- full_output: llms-full.txt
- sections:
- Usage:
- - "index.md"
- - "usage/*.md"
- Reference:
- - "reference/*.md"
- Development:
- - "dev/*.md"
nav:
- Usage:
diff --git a/news/3752.doc.md b/news/3752.doc.md
new file mode 100644
index 0000000000..97ebe872cb
--- /dev/null
+++ b/news/3752.doc.md
@@ -0,0 +1 @@
+Switch to Zensical as docs generator.
diff --git a/pdm.lock b/pdm.lock
index d28f4978e8..5fc8f2bb67 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,7 +5,7 @@
groups = ["default", "all", "doc", "keyring", "pytest", "test", "tox", "workflow"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
-content_hash = "sha256:b1c393c736eace5d586399d77ecd039484cb1b9c1c594b08597c3c8fffed9918"
+content_hash = "sha256:df7b046ba54ea0b79f55ad9f548c5485c63248d7104dc53260c6a2c2c37fdb27"
[[metadata.targets]]
requires_python = ">=3.9"
@@ -61,20 +61,6 @@ files = [
{file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"},
]
-[[package]]
-name = "babel"
-version = "2.17.0"
-requires_python = ">=3.8"
-summary = "Internationalization utilities"
-groups = ["doc"]
-dependencies = [
- "pytz>=2015.7; python_version < \"3.9\"",
-]
-files = [
- {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"},
- {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"},
-]
-
[[package]]
name = "backports-tarfile"
version = "1.2.0"
@@ -87,37 +73,6 @@ files = [
{file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
]
-[[package]]
-name = "backrefs"
-version = "6.1"
-requires_python = ">=3.9"
-summary = "A wrapper around re and regex that adds additional back references."
-groups = ["doc"]
-files = [
- {file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"},
- {file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"},
- {file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"},
- {file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"},
- {file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"},
- {file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"},
- {file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"},
-]
-
-[[package]]
-name = "beautifulsoup4"
-version = "4.14.3"
-requires_python = ">=3.7.0"
-summary = "Screen-scraping library"
-groups = ["doc"]
-dependencies = [
- "soupsieve>=1.6.1",
- "typing-extensions>=4.0.0",
-]
-files = [
- {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"},
- {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"},
-]
-
[[package]]
name = "blinker"
version = "1.9.0"
@@ -145,7 +100,7 @@ name = "certifi"
version = "2025.11.12"
requires_python = ">=3.7"
summary = "Python package for providing Mozilla's CA Bundle."
-groups = ["default", "doc", "test"]
+groups = ["default", "test"]
files = [
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
@@ -264,7 +219,7 @@ name = "charset-normalizer"
version = "3.4.4"
requires_python = ">=3.7"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-groups = ["default", "doc"]
+groups = ["default"]
files = [
{file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"},
@@ -663,6 +618,21 @@ files = [
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
]
+[[package]]
+name = "deepmerge"
+version = "2.0"
+requires_python = ">=3.8"
+summary = "A toolset for deeply merging Python dictionaries."
+groups = ["doc"]
+marker = "python_version >= \"3.10\""
+dependencies = [
+ "typing-extensions; python_version <= \"3.9\"",
+]
+files = [
+ {file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"},
+ {file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"},
+]
+
[[package]]
name = "dep-logic"
version = "0.5.2"
@@ -877,7 +847,7 @@ name = "idna"
version = "3.11"
requires_python = ">=3.8"
summary = "Internationalized Domain Names in Applications (IDNA)"
-groups = ["default", "doc", "test"]
+groups = ["default", "test"]
files = [
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
@@ -1038,26 +1008,12 @@ files = [
{file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"},
]
-[[package]]
-name = "markdown-exec"
-version = "1.11.0"
-requires_python = ">=3.9"
-summary = "Utilities to execute code blocks in Markdown files."
-groups = ["doc"]
-dependencies = [
- "pymdown-extensions>=9",
-]
-files = [
- {file = "markdown_exec-1.11.0-py3-none-any.whl", hash = "sha256:0526957984980f55c02b425d32e8ac8bb21090c109c7012ff905d3ddcc468ceb"},
- {file = "markdown_exec-1.11.0.tar.gz", hash = "sha256:e0313a0dff715869a311d24853b3a7ecbbaa12e74eb0f3cf7d91401a7d8f0082"},
-]
-
[[package]]
name = "markdown-it-py"
version = "3.0.0"
requires_python = ">=3.8"
summary = "Python port of markdown-it. Markdown parsing, done right!"
-groups = ["default", "doc"]
+groups = ["default"]
dependencies = [
"mdurl~=0.1",
]
@@ -1066,20 +1022,6 @@ files = [
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
-[[package]]
-name = "markdownify"
-version = "1.2.2"
-summary = "Convert HTML to markdown."
-groups = ["doc"]
-dependencies = [
- "beautifulsoup4<5,>=4.9",
- "six<2,>=1.15",
-]
-files = [
- {file = "markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a"},
- {file = "markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09"},
-]
-
[[package]]
name = "markupsafe"
version = "3.0.3"
@@ -1178,43 +1120,12 @@ files = [
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
]
-[[package]]
-name = "mdformat"
-version = "0.7.22"
-requires_python = ">=3.9"
-summary = "CommonMark compliant Markdown formatter"
-groups = ["doc"]
-dependencies = [
- "importlib-metadata>=3.6.0; python_version < \"3.10\"",
- "markdown-it-py<4.0.0,>=1.0.0",
- "tomli>=1.1.0; python_version < \"3.11\"",
-]
-files = [
- {file = "mdformat-0.7.22-py3-none-any.whl", hash = "sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5"},
- {file = "mdformat-0.7.22.tar.gz", hash = "sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea"},
-]
-
-[[package]]
-name = "mdformat-tables"
-version = "1.0.0"
-requires_python = ">=3.7.0"
-summary = "An mdformat plugin for rendering tables."
-groups = ["doc"]
-dependencies = [
- "mdformat<0.8.0,>=0.7.5",
- "wcwidth>=0.2.13",
-]
-files = [
- {file = "mdformat_tables-1.0.0-py3-none-any.whl", hash = "sha256:94cd86126141b2adc3b04c08d1441eb1272b36c39146bab078249a41c7240a9a"},
- {file = "mdformat_tables-1.0.0.tar.gz", hash = "sha256:a57db1ac17c4a125da794ef45539904bb8a9592e80557d525e1f169c96daa2c8"},
-]
-
[[package]]
name = "mdurl"
version = "0.1.2"
requires_python = ">=3.7"
summary = "Markdown URL utilities"
-groups = ["default", "doc"]
+groups = ["default"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
@@ -1291,83 +1202,6 @@ files = [
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
]
-[[package]]
-name = "mkdocs-llmstxt"
-version = "0.4.0"
-requires_python = ">=3.9"
-summary = "MkDocs plugin to generate an /llms.txt file."
-groups = ["doc"]
-dependencies = [
- "beautifulsoup4>=4.12",
- "markdownify>=0.14",
- "mdformat-tables>=1.0",
- "mdformat>=0.7.21",
-]
-files = [
- {file = "mkdocs_llmstxt-0.4.0-py3-none-any.whl", hash = "sha256:7244bf0ac917c9964030c93e9c3e26c02d2d14a0f66fc113416007125b6da0fc"},
- {file = "mkdocs_llmstxt-0.4.0.tar.gz", hash = "sha256:a7e4d20496bc8c55b6773b55c8d69cf552448a9ad38915b6e8c657ae3a46c8b8"},
-]
-
-[[package]]
-name = "mkdocs-material"
-version = "9.7.0"
-requires_python = ">=3.8"
-summary = "Documentation that simply works"
-groups = ["doc"]
-dependencies = [
- "babel>=2.10",
- "backrefs>=5.7.post1",
- "colorama>=0.4",
- "jinja2>=3.1",
- "markdown>=3.2",
- "mkdocs-material-extensions>=1.3",
- "mkdocs>=1.6",
- "paginate>=0.5",
- "pygments>=2.16",
- "pymdown-extensions>=10.2",
- "requests>=2.26",
-]
-files = [
- {file = "mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887"},
- {file = "mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec"},
-]
-
-[[package]]
-name = "mkdocs-material-extensions"
-version = "1.3.1"
-requires_python = ">=3.8"
-summary = "Extension pack for Python Markdown and MkDocs Material."
-groups = ["doc"]
-files = [
- {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
- {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
-]
-
-[[package]]
-name = "mkdocs-redirects"
-version = "1.2.2"
-requires_python = ">=3.8"
-summary = "A MkDocs plugin for dynamic page redirects to prevent broken links"
-groups = ["doc"]
-dependencies = [
- "mkdocs>=1.1.1",
-]
-files = [
- {file = "mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5"},
- {file = "mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095"},
-]
-
-[[package]]
-name = "mkdocs-version-annotations"
-version = "1.0.0"
-requires_python = ">=3.7,<4.0"
-summary = "MkDocs plugin to add custom admonitions for documenting version differences"
-groups = ["doc"]
-files = [
- {file = "mkdocs-version-annotations-1.0.0.tar.gz", hash = "sha256:6786024b37d27b330fda240b76ebec8e7ce48bd5a9d7a66e99804559d088dffa"},
- {file = "mkdocs_version_annotations-1.0.0-py3-none-any.whl", hash = "sha256:385004eb4a7530dd87a227e08cd907ce7a8fe21fdf297720a4149c511bcf05f5"},
-]
-
[[package]]
name = "mkdocstrings"
version = "0.30.1"
@@ -1514,16 +1348,6 @@ files = [
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
]
-[[package]]
-name = "paginate"
-version = "0.5.7"
-summary = "Divides large result sets into pages for easier browsing"
-groups = ["doc"]
-files = [
- {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
- {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
-]
-
[[package]]
name = "parver"
version = "0.5"
@@ -1899,7 +1723,7 @@ name = "requests"
version = "2.32.5"
requires_python = ">=3.9"
summary = "Python HTTP for Humans."
-groups = ["default", "doc"]
+groups = ["default"]
dependencies = [
"certifi>=2017.4.17",
"charset-normalizer<4,>=2",
@@ -1997,17 +1821,6 @@ files = [
{file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"},
]
-[[package]]
-name = "soupsieve"
-version = "2.8.1"
-requires_python = ">=3.9"
-summary = "A modern CSS selector implementation for Beautiful Soup."
-groups = ["doc"]
-files = [
- {file = "soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434"},
- {file = "soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350"},
-]
-
[[package]]
name = "tomli"
version = "2.3.0"
@@ -2171,7 +1984,7 @@ name = "urllib3"
version = "2.6.2"
requires_python = ">=3.9"
summary = "HTTP library with thread-safe connection pooling, file post, and more."
-groups = ["default", "doc"]
+groups = ["default"]
files = [
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
@@ -2234,17 +2047,6 @@ files = [
{file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
]
-[[package]]
-name = "wcwidth"
-version = "0.2.14"
-requires_python = ">=3.6"
-summary = "Measures the displayed width of unicode strings in a terminal"
-groups = ["doc"]
-files = [
- {file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"},
- {file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
-]
-
[[package]]
name = "werkzeug"
version = "3.1.4"
@@ -2259,6 +2061,38 @@ files = [
{file = "werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e"},
]
+[[package]]
+name = "zensical"
+version = "0.0.28"
+requires_python = ">=3.10"
+summary = "A modern static site generator built by the creators of Material for MkDocs"
+groups = ["doc"]
+marker = "python_version >= \"3.10\""
+dependencies = [
+ "click>=8.1.8",
+ "deepmerge>=2.0",
+ "markdown>=3.7",
+ "pygments>=2.16",
+ "pymdown-extensions>=10.15",
+ "pyyaml>=6.0.2",
+ "tomli>=2.0; python_full_version < \"3.11\"",
+]
+files = [
+ {file = "zensical-0.0.28-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2db2997dd124dc9361b9d3228925df9e51281af9529c26187a865407588f8abb"},
+ {file = "zensical-0.0.28-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5c6e5ea5c057492a1473a68f0e71359d663057d7d864b32a8fd429c8ea390346"},
+ {file = "zensical-0.0.28-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2ee8a1d29b61de61e6b0f9123fa395c06c24c94e509170c7f7f9ccddaeaaad4"},
+ {file = "zensical-0.0.28-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cef68b363c0d3598d37a1090bfc5c6267e36a87a55e9fb6a6f9d7f2768f1dfd"},
+ {file = "zensical-0.0.28-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3175440fd526cf0273859d0de355e769ba43e082e09deb04b6f6afd77af6c91"},
+ {file = "zensical-0.0.28-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0887436c5fd8fe7008c0d93407876695db67bcf55c8aec9fb36c339d82bb7fce"},
+ {file = "zensical-0.0.28-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8a0ca92e04687f71aa20c9ae80fe8b840125545657e6b7c0f83adecd04d512e"},
+ {file = "zensical-0.0.28-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:acb31723ca82c367d1c41a6a7b0f52ce1ed87f0ee437de2ee2fc2e284e120e44"},
+ {file = "zensical-0.0.28-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3680b3a75560881e7fa32b450cf6de09895680b84d0dd2b611cb5fa552fdfc49"},
+ {file = "zensical-0.0.28-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93e1bc47981b50bcd9c4098edc66fb86fd881c5b52b355db92dcef626cc0b468"},
+ {file = "zensical-0.0.28-cp310-abi3-win32.whl", hash = "sha256:eee014ca1290463cf8471e3e1b05b7c627ac7afa0881635024d23d4794675980"},
+ {file = "zensical-0.0.28-cp310-abi3-win_amd64.whl", hash = "sha256:6077a85ee1f0154dbfe542db36789322fe8625d716235a000d4e0a8969b14175"},
+ {file = "zensical-0.0.28.tar.gz", hash = "sha256:af7d75a1b297721dfc9b897f729b601e56b3e566990a989e9e3e373a8cd04c40"},
+]
+
[[package]]
name = "zipp"
version = "3.23.0"
diff --git a/pyproject.toml b/pyproject.toml
index 4cb347dad4..d946c4ade6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -90,14 +90,9 @@ tox = [
"tox-pdm>=0.5",
]
doc = [
- "mkdocs>=1.1",
- "mkdocs-material>=7.3",
+ "zensical>=0.0.28; python_version >= '3.10'",
"mkdocstrings[python]>=0.18",
"setuptools>=62.3.3",
- "markdown-exec>=0.7.0",
- "mkdocs-redirects>=1.2.0",
- "mkdocs-version-annotations>=1.0.0",
- "mkdocs-llmstxt>=0.2.0",
]
workflow = [
"parver>=0.3.1",
@@ -258,6 +253,8 @@ coverage = {shell = """\
tests/
"""}
tox = "tox"
-doc = {cmd = "mkdocs serve", help = "Start the dev server for docs preview"}
+pre_doc = "python tasks/render_reference_docs.py"
+doc = {cmd= "zensical serve", help = "Start the dev server for docs preview"}
+doc-build = {composite = ["pre_doc", "zensical build"], help = "Build the documentation site"}
lint = "prek run --all-files"
complete = {call = "tasks.complete:main", help = "Create autocomplete files for bash and fish"}
diff --git a/tasks/doc_templates/cli.md.in b/tasks/doc_templates/cli.md.in
new file mode 100644
index 0000000000..f1cbcd3b11
--- /dev/null
+++ b/tasks/doc_templates/cli.md.in
@@ -0,0 +1,5 @@
+{{ generated_comment }}
+
+# CLI Reference
+
+{{ cli_reference }}
diff --git a/tasks/doc_templates/configuration.md.in b/tasks/doc_templates/configuration.md.in
new file mode 100644
index 0000000000..163b4a389c
--- /dev/null
+++ b/tasks/doc_templates/configuration.md.in
@@ -0,0 +1,39 @@
+{{ generated_comment }}
+
+# Configurations
+
+[pdm-config]: ../reference/cli.md#config
+
+## Color Theme
+
+The default theme used by PDM is as follows:
+
+| Key | Default Style |
+| --------- | ------------------------------------------------------------ |
+| `primary` | cyan |
+| `success` | green |
+| `warning` | yellow |
+| `error` | red |
+| `info` | blue |
+| `req` | bold green |
+
+You can change the theme colors with [`pdm config`][pdm-config] command. For example, to change the `primary` color to `magenta`:
+
+```bash
+pdm config theme.primary magenta
+```
+
+Or use a hex color code:
+
+```bash
+pdm config theme.success '#51c7bd'
+```
+
+## Available Configurations
+
+The following configuration items can be retrieved and modified by [`pdm config`][pdm-config] command.
+
+!!! note "Environment Variable Overrides"
+ If the corresponding env var is set, the value will take precedence over what is saved in the config file.
+
+{{ available_configurations }}
diff --git a/tasks/render_reference_docs.py b/tasks/render_reference_docs.py
new file mode 100644
index 0000000000..d48b5eb654
--- /dev/null
+++ b/tasks/render_reference_docs.py
@@ -0,0 +1,163 @@
+from __future__ import annotations
+
+import argparse
+import re
+from os import PathLike
+from pathlib import Path
+
+from pdm.core import Core
+from pdm.project.config import Config
+
+ROOT = Path(__file__).resolve().parent.parent
+REFERENCE_DIR = ROOT / "docs" / "reference"
+TEMPLATE_DIR = ROOT / "tasks" / "doc_templates"
+CLI_TEMPLATE = TEMPLATE_DIR / "cli.md.in"
+CONFIGURATION_TEMPLATE = TEMPLATE_DIR / "configuration.md.in"
+CLI_OUTPUT = REFERENCE_DIR / "cli.md"
+CONFIGURATION_OUTPUT = REFERENCE_DIR / "configuration.md"
+GENERATED_COMMENT = ""
+MONOSPACED = ("pyproject.toml", "pdm.lock", ".pdm-python", ":pre", ":post", ":all")
+ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
+
+
+def clean_help(help_text: str | None) -> str:
+ if not help_text:
+ return ""
+ help_text = strip_ansi_sequences(help_text)
+ help_text = help_text.replace(r"\[", "[").replace(r"\]", "]")
+ help_text = re.sub(r"__([\w\d_]+)__", r"`__\1__`", help_text)
+ help_text = re.sub(r"env var: ([A-Z_]+)", r"env var: `\1`", help_text)
+ for monospaced in MONOSPACED:
+ help_text = re.sub(rf"\s(['\"]?{re.escape(monospaced)}['\"]?)", f" `{monospaced}`", help_text)
+ return help_text
+
+
+def render_parser(parser: argparse.ArgumentParser, title: str, heading_level: int = 2) -> str:
+ result = [f"{'#' * heading_level} {title}\n"]
+ if parser.description and title != "pdm":
+ result.append("> " + parser.description + "\n")
+
+ for group in sorted(parser._action_groups, key=lambda item: item.title.lower(), reverse=True):
+ if not any(
+ bool(action.option_strings or action.dest) or isinstance(action, argparse._SubParsersAction)
+ for action in group._group_actions
+ ):
+ continue
+
+ result.append(f"{group.title.title()}:\n")
+ for action in group._group_actions:
+ if isinstance(action, argparse._SubParsersAction):
+ for name, subparser in action._name_parser_map.items():
+ result.append(render_parser(subparser, name, heading_level + 1))
+ continue
+
+ options = [f"`{option}`" for option in action.option_strings]
+ line = f"- `{action.dest}`" if not options else f"- {', '.join(options)}"
+ if action.metavar:
+ line += f" `{action.metavar}`"
+ line += f": {clean_help(action.help)}"
+
+ if action.default and action.default != argparse.SUPPRESS:
+ default = action.default
+ if any(option.startswith("--no-") for option in action.option_strings) and default is True:
+ default = False
+ line += f" (default: `{default}`)"
+
+ result.append(line)
+ result.append("")
+
+ return "\n".join(result).rstrip()
+
+
+def render_cli_reference() -> str:
+ return render_parser(Core().parser, "pdm")
+
+
+def render_configuration_table() -> str:
+ rows = [
+ "| Config Item | Description | Default Value | Available in Project | Env var |",
+ "| --- | --- | --- | --- | --- |",
+ ]
+
+ for key, value in Config._config_map.items():
+ default = f"`{format_default(value.default)}`" if value.should_show() else ""
+ env_var = f"`{value.env_var}`" if value.env_var else ""
+ available_in_project = "No" if value.global_only else "Yes"
+ rows.append(f"| `{key}` | {value.description} | {default} | {available_in_project} | {env_var} |")
+
+ rows.extend(
+ [
+ "| `pypi..url` | The URL of custom package source | `https://pypi.org/simple` | Yes | |",
+ "| `pypi..username` | The username to access custom source | | Yes | |",
+ "| `pypi..password` | The password to access custom source | | Yes | |",
+ "| `pypi..type` | `index` or `find_links` | `index` | Yes | |",
+ "| `pypi..verify_ssl` | Verify SSL certificate when query custom source | `True` | Yes | |",
+ "| `repository..url` | The URL of custom package source | `https://pypi.org/simple` | Yes | |",
+ "| `repository..username` | The username to access custom repository | | Yes | |",
+ "| `repository..password` | The password to access custom repository | | Yes | |",
+ (
+ "| `repository..ca_certs` | Path to a PEM-encoded CA cert bundle "
+ "(used for server cert verification) | The CA certificates from "
+ "[certifi](https://pypi.org/project/certifi/) | Yes | |"
+ ),
+ "| `repository..verify_ssl` | Verify SSL certificate when uploading to repository | `True` | Yes | |",
+ ]
+ )
+ return "\n".join(rows)
+
+
+def format_default(value: object) -> str:
+ if isinstance(value, PathLike):
+ return normalize_home_path(Path(value))
+ if isinstance(value, str):
+ return normalize_home_path(strip_ansi_sequences(value))
+ return str(value)
+
+
+def normalize_home_path(value: str | Path) -> str:
+ text = str(value)
+ home = str(Path.home())
+ if text.startswith(home):
+ return text.replace(home, "~", 1)
+ return text
+
+
+def strip_ansi_sequences(value: str) -> str:
+ return ANSI_ESCAPE_RE.sub("", value)
+
+
+def render_template(path: Path, **replacements: str) -> str:
+ content = path.read_text(encoding="utf-8")
+ for key, value in replacements.items():
+ content = content.replace(f"{{{{ {key} }}}}", value)
+ return content
+
+
+def write_if_changed(path: Path, content: str) -> None:
+ normalized = content.rstrip() + "\n"
+ if path.exists() and path.read_text(encoding="utf-8") == normalized:
+ return
+ path.write_text(normalized, encoding="utf-8")
+
+
+def main() -> None:
+ write_if_changed(
+ CLI_OUTPUT,
+ render_template(
+ CLI_TEMPLATE,
+ generated_comment=GENERATED_COMMENT,
+ cli_reference=render_cli_reference(),
+ ),
+ )
+ write_if_changed(
+ CONFIGURATION_OUTPUT,
+ render_template(
+ CONFIGURATION_TEMPLATE,
+ generated_comment=GENERATED_COMMENT,
+ available_configurations=render_configuration_table(),
+ ),
+ )
+
+
+if __name__ == "__main__":
+ main()