{"id":23969737,"url":"https://github.com/makukha/docsub","last_synced_at":"2026-02-14T12:01:37.682Z","repository":{"id":270125054,"uuid":"909315496","full_name":"makukha/docsub","owner":"makukha","description":"Embed text and data into Markdown files","archived":false,"fork":false,"pushed_at":"2025-02-21T10:05:51.000Z","size":275,"stargazers_count":1,"open_issues_count":9,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-01T11:34:31.586Z","etag":null,"topics":["docs","documentation","template-engine"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/makukha.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-12-28T10:36:50.000Z","updated_at":"2025-04-04T19:56:21.000Z","dependencies_parsed_at":"2025-02-03T19:23:39.157Z","dependency_job_id":"1868fe14-82f6-495c-ae5c-b58fb57b1310","html_url":"https://github.com/makukha/docsub","commit_stats":null,"previous_names":["makukha/docsub"],"tags_count":13,"template":false,"template_full_name":null,"purl":"pkg:github/makukha/docsub","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdocsub","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdocsub/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdocsub/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdocsub/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/makukha","download_url":"https://codeload.github.com/makukha/docsub/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/makukha%2Fdocsub/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29443468,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T10:51:12.367Z","status":"ssl_error","status_checked_at":"2026-02-14T10:50:52.088Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docs","documentation","template-engine"],"created_at":"2025-01-07T01:49:36.252Z","updated_at":"2026-02-14T12:01:37.665Z","avatar_url":"https://github.com/makukha.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# docsub\n\u003e Substitute dynamically generated content in Markdown files\n\n[![license](https://img.shields.io/github/license/makukha/docsub.svg)](https://github.com/makukha/docsub/blob/main/LICENSE)\n[![pypi](https://img.shields.io/pypi/v/docsub.svg#v0.9.0)](https://pypi.python.org/pypi/docsub)\n[![python versions](https://img.shields.io/pypi/pyversions/docsub.svg)](https://pypi.org/project/docsub)\n[![tests](https://raw.githubusercontent.com/makukha/docsub/v0.9.0/docs/_static/badge-tests.svg)](https://github.com/makukha/docsub)\n[![coverage](https://raw.githubusercontent.com/makukha/docsub/v0.9.0/docs/_static/badge-coverage.svg)](https://github.com/makukha/docsub)\n[![tested with multipython](https://img.shields.io/badge/tested_with-multipython-x)](https://github.com/makukha/multipython)\n[![uses docsub](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/makukha/docsub/refs/heads/main/docs/badge/v1.json)](https://github.com/makukha/docsub)\n[![mypy](https://img.shields.io/badge/type_checked-mypy-%231674b1)](http://mypy.readthedocs.io)\n[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/ruff)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n\n\u003e [!WARNING]\n\u003e * With `docsub`, every documentation file may become executable.\n\u003e * Never use `docsub` to process files from untrusted sources.\n\u003e * This project is in experimental state, syntax and functionality may change significantly.\n\u003e * If still want to try it, use pinned package version `docsub==0.9.0`\n\n\n# Features\n\n* Embed **static** files\n* Embed **command execution** results\n* **Idempotent** substitutions\n* **Invisible** non-intrusive markup using comment blocks\n* **Plays nicely** with other markups\n* **Extensible** with project-local commands\n* **Configurable** with config files and env vars\n\n\n# Use cases\n\n* Manage partially duplicate docs for multiple destinations\n* Manage docs for monorepositories\n* Embed CLI reference in docs\n* Embed dynamically generated content:\n  * Project metadata\n  * Test reports\n  * Models evaluation results\n\n\u003e [!NOTE]\n\u003e This file uses docsub itself. Dig into raw markup if interested.\n\n## Docsub is not...\n\n* ...a documentation engine like [Sphinx](https://www.sphinx-doc.org) or [MkDocs](https://www.mkdocs.org)\n* ...a full-featured static website generator like [Pelican](https://getpelican.com)\n* ...a templating engine like [Jinja](https://jinja.palletsprojects.com)\n* ...a replacement for [Bump My Version](https://callowayproject.github.io/bump-my-version)\n\n# Usage\n\n```shell\n$ uv run docsub sync -i README.md\n```\n\n## From separate files...\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd style=\"vertical-align:top\"\u003e\n\n### README.md\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/__input__.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n# Title\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include info.md --\u003e\n\u003c!-- docsub: include features.md --\u003e\n...\n\u003c!-- docsub: end --\u003e\n\n## Table\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include data.md --\u003e\n\u003c!-- docsub: lines after 2 --\u003e\n| Col 1 | Col 2 |\n|-------|-------|\n...\n\u003c!-- docsub: end --\u003e\n\n## Code\n\u003c!-- docsub: begin #code --\u003e\n\u003c!-- docsub: include func.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```python\n...\n```\n\u003c!-- docsub: end #code --\u003e\n````\n\u003c!-- docsub: end #readme --\u003e\n\n\u003c/td\u003e\n\u003ctd style=\"vertical-align:top\"\u003e\n\n### info.md\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/info.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n\u003e Long description.\n````\n\u003c!-- docsub: end #readme --\u003e\n\n### features.md\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/features.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n* Feature 1\n* Feature 2\n* Feature 3\n````\n\u003c!-- docsub: end #readme --\u003e\n\n### data.md\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/data.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n| Key 1 | value 1 |\n| Key 2 | value 2 |\n| Key 3 | value 3 |\n````\n\u003c!-- docsub: end #readme --\u003e\n\n### func.py\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/func.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````python\ndef func():\n    pass\n````\n\u003c!-- docsub: end #readme --\u003e\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n\n## Get merged document\n\n***and keep it updated!***\n\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_showcase/__result__.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n````markdown\n# Title\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include info.md --\u003e\n\u003c!-- docsub: include features.md --\u003e\n\u003e Long description.\n* Feature 1\n* Feature 2\n* Feature 3\n\u003c!-- docsub: end --\u003e\n\n## Table\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include data.md --\u003e\n\u003c!-- docsub: lines after 2 --\u003e\n| Col 1 | Col 2 |\n|-------|-------|\n| Key 1 | value 1 |\n| Key 2 | value 2 |\n| Key 3 | value 3 |\n\u003c!-- docsub: end --\u003e\n\n## Code\n\u003c!-- docsub: begin #code --\u003e\n\u003c!-- docsub: include func.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```python\ndef func():\n    pass\n```\n\u003c!-- docsub: end #code --\u003e\n````\n\u003c!-- docsub: end #readme --\u003e\n\n\n# Installation\n\n## Development dependency\n\nRecommended. The most flexible installation option, allowing [project-local commands](#project-local-commands) to utilize project codebase.\n\n```toml\n# pyproject.toml\n[dependency-groups]\ndev = [\n  \"docsub==0.9.0\",\n]\n```\n\n## Global installation\n\nWorks for simple cases.\n\n```shell\nuv tool install docsub==0.9.0\n```\n\n\n# Syntax\n\nThe syntax is purposefully verbose. This is fine, you are not supposed to edit it often. But it's searchable and sticks in eye when scrolling down large documents.\n\nDocsub uses line-based substitution syntax based on *directives* and *substitution blocks*.\n\n## Markdown\n\n### Directive\n\n*Markdown directive* is one-line comment:\n\n```text\n\u003c!-- docsub: \u003cdirective\u003e [directive args] --\u003e\n```\n\nThere are multiple [directive types](#directives).\n\n### Substitution block\n\n*Markdown substitution block* is a sequence of lines, starting with `begin` directive and ending with `end` directive.\n\n```markdown\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: help docsub --\u003e\n\u003c!-- docsub: include CHANGELOG.md --\u003e\nInner text will be replaced.\n\u003c!-- docsub: this whole line is treated as plain text --\u003e\nThis text will be replaced too.\n\u003c!-- docsub: end --\u003e\n```\n\nOne or many other directives must come at the top of the block, otherwise they are treated as plain text. Blocks without *producing directives* are not allowed. Block's inner text will be replaced upon substitution, unless modifier directives are used, e.g. `lines`.\n\nIf docsub substitution block lies inside markdown fenced code block, it is not substituted *(example: fenced code blocks above and below this paragraph, see the raw markup)*. To put dynamic content into a fenced code block, place `begin` and `end` around it and use `lines after N upto -M` *(example: [Usage](#usage) section)*.\n\nFor nested blocks, only top level substitution is performed. Use block `#identifier` to distinguish between nesting levels.\n\n```markdown\n\u003c!-- docsub: begin #top --\u003e\n\u003c!-- docsub: include part.md --\u003e\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include nested.md --\u003e\n\u003c!-- docsub: end --\u003e\n\u003c!-- docsub: end #top --\u003e\n```\n\nWhen substitution block is indented, the indentation is preserved:\n\n\u003c!-- docsub: begin #top --\u003e\n\u003c!-- docsub: include tests/test_readme_indent/__result__.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```markdown\n* List item\n    \u003c!-- docsub: begin --\u003e\n    \u003c!-- docsub: include sublist.md --\u003e\n    * Sub-item 1\n    * Sub-item 2\n    * Sub-item 3\n    \u003c!-- docsub: end --\u003e\n```\n\u003c!-- docsub: end #top --\u003e\n\n# Directives\n\n* *Block delimiters*: `begin`, `end`\n* *Producing commands*: `exec`, `help`, `include`, `x`\n* *Modifying commands*: `lines`, `strip`\n\n## `begin`\n```text\nbegin [#identifier]\n```\nOpen substitution target block. To distinguish between nesting levels, use block `#identifier`, starting with `#`.\n\n## `end`\n```text\nend [#identifier]\n```\nClose substitution target block.\n\n## `exec`\n```text\nexec \u003cshell commands\u003e\n```\nExecute `\u003cshell commands\u003e` with `sh -c` and substitute stdout. Allows pipes and other shell functionality. If possible, avoid using this directive.\n\n* `cmd.exec.work_dir` — shell working directory, default `'.'`\n* `cmd.exec.env_vars` — dict of additional environment variables, default `{}`\n\n## `help`\n\n```text\nhelp \u003ccommand\u003e [subcommand...]\nhelp python -m \u003ccommand\u003e [subcommand...]\n```\nDisplay help for CLI utility or Python module. Use this command to document CLI instead of `exec`. Runs `command [subcommand...] --help` or `python -m command [subcommand...] --help` respectively. *Directive args* must be a space-separated sequence of characters `[-._a-zA-Z0-9]`.\n\n* `cmd.help.env_vars` — dict of additional environment variables, default `{}`\n\n## `include`\n```text\ninclude path/to/file\n```\nLiterally include file specified by path relative to `base_dir` config option.\n\n* `cmd.include.base_dir` — base directory for relative paths\n\n## `lines`\n```text\nlines [after N] [upto -M]\n```\nUpon substitution, keep original target block lines: first `N` and/or last `M`. Only one `lines` command is allowed inside the block.\n\n## `strip`\n```text\nstrip\n```\nStrip whitespace in substitution result:\n* initial and trailing blank lines\n* trailing whitespace on every line\n\n## `x`\n```text\nx \u003cproject-command\u003e [args and --options]\n```\nExecute [project-local](#project-local-commands) command declared in `docsubfile.py` in project root. The naming is inspired by `X-` HTTP headers and `x-` convention for reusable YAML sections.\n\n* `cmd.x.docsubfile` — path to file with project-local commands, absolute or relative to project root (default: `docsubfile.py`)\n\n\n# Project-local commands\n\nWhen project root contains file `docsubfile.py` with commands defined as in example below, they can be used in `docsub: x ` directive. Project commands must be defined as [click](https://click.palletsprojects.com) command and gathered under `x` group. There is no need to install `click` separately as docsub depends on it.\n\nIf docsub is installed globally and called as `uvx docsub`, project commands in `docsubfile.py` have access to docsub dependencies only: `click`, `loguru`, `rich` (see docsub's pyproject.toml for details).\n\nIf docsub is installed as project dev dependency and called as `uv run docsub`, user commands also have access to project modules and dev dependencies. This allows more flexible scenarios.\n\nProject command author can get access to docsub `Environment` object (including command configs) from click context object (see example below). The docsub `Environment` object has some useful methods *(not documented yet)*.\n\n## Example\n\n```shell\n$ uv run docsub sync -i sample.md\n```\n\n### sample.md\n\u003c!-- docsub: begin #readme --\u003e\n\u003c!-- docsub: include tests/test_readme_docsubfile/__result__.md --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```markdown\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: x say-hello Alice Bob --\u003e\nHi there, Alice!\nHi there, Bob!\n\u003c!-- docsub: end --\u003e\n```\n\u003c!-- docsub: end #readme --\u003e\n\n### docsubfile.py\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: include tests/test_readme_docsubfile/docsubfile.py --\u003e\n\u003c!-- docsub: lines after 1 upto -1 --\u003e\n```python\nfrom docsub import Environment, click, pass_env\n\n@click.group()\ndef x() -\u003e None:\n    pass\n\n@x.command()\n@click.argument('users', nargs=-1)\ndef say_hello(users: tuple[str, ...]) -\u003e None:\n    for user in users:\n        click.echo(f'Hi there, {user}!')\n\n@x.command()\n@click.argument('users', nargs=-1)\n@pass_env\ndef log_hello(env: Environment, users: tuple[str, ...]) -\u003e None:\n    base = env.get_temp_dir('log_hello')\n    (base / 'hello.log').write_text(f'said hello to {users}')\n```\n\u003c!-- docsub: end --\u003e\n\n## Calling project-local commands\n\nDocsub exposes `x` as CLI command, letting project commands to be executed with project settings:\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: exec uv run docsub -x tests/test_readme_docsubfile/docsubfile.py x say-hello Alice Bob --\u003e\n\u003c!-- docsub: lines after 2 upto -1 --\u003e\n```shell\n$ uv run docsub x say-hello Alice Bob\nHi there, Alice!\nHi there, Bob!\n```\n\u003c!-- docsub: end --\u003e\n\n\n# Configuration\n\nConfiguration resolution order\n\n* command line options *(to be documented)*\n* environment variables *(to be documented)*\n* `docsub.toml` config file in current working directory\n* `pyproject.toml`, section `[tool.docsub]` *(to be implemented)*\n* default config values\n\n## Root settings\n\n* `local_dir` — internal working directory at the project root (default: `.docsub`)\n\n## Command settings\n\nSee [Commands](#commands).\n\n## Environment variables\n\n*(to be documented)*\n\n## Command line options\n\n*(to be documented)*\n\n## Complete config example\n\nAll config keys are optional.\n\n\n```toml\nlocal_dir = \".docsub\"  # default\n\n[logging]\n#level = \"DEBUG\"  # default: missing, logging disabled\n\n[cmd.exec]\nenv_vars = {}  # default\nwork_dir = \".\"  # default\n\n[cmd.help.env_vars]\nCOLUMNS = \"60\"  # more compact\n\n[cmd.include]\nbase_dir = \".\"  # default\n\n[cmd.x]\ndocsubfile = \"docsubfile.py\"  # default\n```\n\n\u003e [!WARNING]\n\u003e In future releases config keys will be moved under `[tool.docsub]` root for both `pyproject.toml` and `docsub.toml`, this will be a breaking change.\n\n\n# Logging\n\nDocsub uses [loguru](https://loguru.readthedocs.io) for logging. Logging is disabled by default. To enable logging, set config option `level` to one of [logging levels](https://loguru.readthedocs.io/en/stable/api/logger.html#levels) supported by loguru.\n\n*(logging is rudimentary at the moment)*\n\n\n# CLI Reference\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: help python -m docsub --\u003e\n\u003c!-- docsub: lines after 2 upto -1 --\u003e\n\u003c!-- docsub: strip --\u003e\n```shell\n$ docsub --help\nUsage: python -m docsub [OPTIONS] COMMAND [ARGS]...\n\n╭─ Options ──────────────────────────────────────────────────────────╮\n│ --config-file           -c  PATH                                   │\n│ --local-dir             -l  PATH                                   │\n│ --cmd-exec-work-dir         PATH                                   │\n│ --cmd-exec-env-vars         TEXT                                   │\n│ --cmd-help-env-vars         TEXT                                   │\n│ --cmd-include-base-dir      PATH                                   │\n│ --cmd-x-docsubfile      -x  PATH                                   │\n│ --version                         Show the version and exit.       │\n│ --help                            Show this message and exit.      │\n╰────────────────────────────────────────────────────────────────────╯\n╭─ Commands ─────────────────────────────────────────────────────────╮\n│ sync    Update Markdown files with embedded content.               │\n│ x       Project-local commands.                                    │\n╰────────────────────────────────────────────────────────────────────╯\n```\n\u003c!-- docsub: end --\u003e\n\n## `docsub sync`\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: help python -m docsub sync --\u003e\n\u003c!-- docsub: lines after 2 upto -1 --\u003e\n\u003c!-- docsub: strip --\u003e\n```shell\n$ docsub sync --help\nUsage: python -m docsub sync [OPTIONS] FILES...\n\nUpdate Markdown files with embedded content.\nRead FILES and perform substitutions one by one. If one file depends\non another, place it after that file.\n\n╭─ Options ──────────────────────────────────────────────────────────╮\n│ --in-place  -i    Process files in-place                           │\n│ --help            Show this message and exit.                      │\n╰────────────────────────────────────────────────────────────────────╯\n```\n\u003c!-- docsub: end --\u003e\n\n## `docsub x`\n\n\u003c!-- docsub: begin --\u003e\n\u003c!-- docsub: help python -m docsub x --\u003e\n\u003c!-- docsub: lines after 2 upto -1 --\u003e\n\u003c!-- docsub: strip --\u003e\n```shell\n$ docsub x --help\nUsage: python -m docsub x [OPTIONS] COMMAND [ARGS]...\n\nProject-local commands.\n\n╭─ Options ──────────────────────────────────────────────────────────╮\n│ --help      Show this message and exit.                            │\n╰────────────────────────────────────────────────────────────────────╯\n```\n\u003c!-- docsub: end --\u003e\n\n\n# History\n\nThis project appeared to maintain docs for [multipython](https://github.com/makukha/multipython) project. You may check it up for usage examples.\n\n\n# Authors\n\n* [Michael Makukha](https://github.com/makukha)\n\n\n# License\n\n[MIT License](https://github.com/makukha/docsub/blob/main/LICENSE)\n\n\n# Changelog\n\n[CHANGELOG.md](https://github.com/makukha/docsub/blob/main/CHANGELOG.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakukha%2Fdocsub","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmakukha%2Fdocsub","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmakukha%2Fdocsub/lists"}