pax_global_header00006660000000000000000000000064151066311610014512gustar00rootroot0000000000000052 comment=a8f37466b596bdeee1130bb92d4dbaec3cd39457 adamtheturtle-sphinx-substitution-extensions-a8f3746/000077500000000000000000000000001510663116100231775ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/.git_archival.txt000066400000000000000000000000331510663116100264460ustar00rootroot00000000000000ref-names: tag: 2025.11.17 adamtheturtle-sphinx-substitution-extensions-a8f3746/.gitattributes000066400000000000000000000000631510663116100260710ustar00rootroot00000000000000.git_archival.txt export-subst * text=auto eol=lf adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/000077500000000000000000000000001510663116100245375ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/dependabot.yml000066400000000000000000000003451510663116100273710ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: / schedule: interval: daily adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/workflows/000077500000000000000000000000001510663116100265745ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/workflows/ci.yml000066400000000000000000000043451510663116100277200ustar00rootroot00000000000000--- name: CI on: push: branches: [main] pull_request: branches: [main] schedule: # * is a special character in YAML so you have to quote this string # Run at 1:00 every day - cron: 0 1 * * * jobs: build: strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] uv-resolution: [highest, lowest-direct] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v5 - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' - name: Lint run: | uv run --extra=dev pre-commit run --all-files --hook-stage pre-commit --verbose uv run --extra=dev pre-commit run --all-files --hook-stage pre-push --verbose uv run --extra=dev pre-commit run --all-files --hook-stage manual --verbose env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} - name: Freeze for debugging run: | uv pip freeze - name: Build sample run: | uv run --extra=dev sphinx-build -W -b html sample/source sample/build env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} - name: Build sample parallel run: | uv run --extra=dev sphinx-build -j 2 -W -b html sample/source sample/build env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} - name: Run tests run: | uv run --extra=dev pytest -s -vvv --cov-fail-under 100 --cov=src/ --cov=tests . env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} - uses: pre-commit-ci/lite-action@v1.1.0 if: always() completion-ci: needs: build runs-on: ubuntu-latest if: always() # Run even if one matrix job fails steps: - name: Check matrix job status run: |- if ! ${{ needs.build.result == 'success' }}; then echo "One or more matrix jobs failed" exit 1 fi adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/workflows/dependabot-merge.yml000066400000000000000000000011141510663116100325160ustar00rootroot00000000000000--- name: Dependabot auto-merge on: pull_request permissions: contents: write pull-requests: write jobs: dependabot: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GH_TOKEN: ${{secrets.GITHUB_TOKEN}} adamtheturtle-sphinx-substitution-extensions-a8f3746/.github/workflows/release.yml000066400000000000000000000066751510663116100307550ustar00rootroot00000000000000--- name: Release on: workflow_dispatch jobs: build: name: Publish a release runs-on: ubuntu-latest # Specifying an environment is strongly recommended by PyPI. # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1/?tab=readme-ov-file#trusted-publishing. environment: release permissions: # This is needed for PyPI publishing. # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1/?tab=readme-ov-file#trusted-publishing. id-token: write # This is needed for https://github.com/stefanzweifel/git-auto-commit-action. contents: write steps: - uses: actions/checkout@v5 with: # See # https://github.com/stefanzweifel/git-auto-commit-action?tab=readme-ov-file#push-to-protected-branches token: ${{ secrets.RELEASE_PAT }} # Fetch all history including tags. # Needed to find the latest tag. # # Also, avoids # https://github.com/stefanzweifel/git-auto-commit-action/issues/99. fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' - name: Calver calculate version uses: StephaneBour/actions-calver@master id: calver with: date_format: '%Y.%m.%d' release: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Get the changelog underline id: changelog_underline run: | underline="$(echo "${{ steps.calver.outputs.release }}" | tr -c '\n' '-')" echo "underline=${underline}" >> "$GITHUB_OUTPUT" - name: Update changelog uses: jacobtomlinson/gha-find-replace@v3 with: find: "Next\n----" replace: "Next\n----\n\n${{ steps.calver.outputs.release }}\n${{ steps.changelog_underline.outputs.underline\ \ }}" include: CHANGELOG.rst regex: false - uses: stefanzweifel/git-auto-commit-action@v7 id: commit with: commit_message: Bump CHANGELOG file_pattern: CHANGELOG.rst # Error if there are no changes. skip_dirty_check: true - name: Bump version and push tag id: tag_version uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} custom_tag: ${{ steps.calver.outputs.release }} tag_prefix: '' commit_sha: ${{ steps.commit.outputs.commit_hash }} - name: Create a GitHub release uses: ncipollo/release-action@v1 with: tag: ${{ steps.tag_version.outputs.new_tag }} makeLatest: true name: Release ${{ steps.tag_version.outputs.new_tag }} body: ${{ steps.tag_version.outputs.changelog }} - name: Build a binary wheel and a source tarball run: | git fetch --tags git checkout ${{ steps.tag_version.outputs.new_tag }} uv build --sdist --wheel --out-dir dist/ uv run --extra=release check-wheel-contents dist/*.whl # We use PyPI trusted publishing rather than a PyPI API token. # See https://github.com/pypa/gh-action-pypi-publish/tree/release/v1/?tab=readme-ov-file#trusted-publishing. - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true adamtheturtle-sphinx-substitution-extensions-a8f3746/.gitignore000066400000000000000000000025211510663116100251670ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # direnv file .envrc # IDEA ide .idea/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # setuptools_scm src/*/_setuptools_scm_version.txt uv.lock # Ignore Mac DS_Store files .DS_Store **/.DS_Store adamtheturtle-sphinx-substitution-extensions-a8f3746/.pre-commit-config.yaml000066400000000000000000000221531510663116100274630ustar00rootroot00000000000000--- fail_fast: true # We use system Python, with required dependencies specified in pyproject.toml. # We therefore cannot use those dependencies in pre-commit CI. ci: skip: - actionlint - sphinx-lint - check-manifest - deptry - doc8 - interrogate - interrogate-docs - mypy - mypy-docs - pylint - pyproject-fmt-fix - pyright - pyright-docs - pyright-verifytypes - pyroma - ruff-check-fix - ruff-check-fix-docs - ruff-format-fix - ruff-format-fix-docs - docformatter - shellcheck - shellcheck-docs - shfmt - shfmt-docs - vulture - vulture-docs - yamlfix # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_install_hook_types: [pre-commit, pre-push, commit-msg] repos: - repo: meta hooks: - id: check-useless-excludes stages: [pre-commit] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files stages: [pre-commit] - id: check-case-conflict stages: [pre-commit] - id: check-executables-have-shebangs stages: [pre-commit] - id: check-merge-conflict stages: [pre-commit] - id: check-shebang-scripts-are-executable stages: [pre-commit] - id: check-symlinks stages: [pre-commit] - id: check-json stages: [pre-commit] - id: check-toml stages: [pre-commit] - id: check-vcs-permalinks stages: [pre-commit] - id: check-yaml stages: [pre-commit] - id: end-of-file-fixer stages: [pre-commit] - id: file-contents-sorter files: spelling_private_dict\.txt$ stages: [pre-commit] - id: trailing-whitespace stages: [pre-commit] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-directive-colons stages: [pre-commit] - id: rst-inline-touching-normal stages: [pre-commit] - id: text-unicode-replacement-char stages: [pre-commit] - id: rst-backticks stages: [pre-commit] - repo: local hooks: - id: actionlint name: actionlint entry: uv run --extra=dev actionlint language: python pass_filenames: false types_or: [yaml] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: docformatter name: docformatter entry: uv run --extra=dev -m docformatter --in-place language: python types_or: [python] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: shellcheck name: shellcheck entry: uv run --extra=dev shellcheck --shell=bash language: python types_or: [shell] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: shellcheck-docs name: shellcheck-docs entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=shell --language=console --command="shellcheck --shell=bash" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: shfmt name: shfmt entry: shfmt --write --space-redirects --indent=4 language: python types_or: [shell] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: shfmt-docs name: shfmt-docs entry: uv run --extra=dev doccmd --language=shell --language=console --skip-marker=shfmt --no-pad-file --command="shfmt --write --space-redirects --indent=4" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: mypy name: mypy stages: [pre-push] entry: uv run --extra=dev -m mypy language: python types_or: [python, toml] pass_filenames: false additional_dependencies: [uv==0.9.5] - id: mypy-docs name: mypy-docs stages: [pre-push] entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="mypy" language: python types_or: [markdown, rst] - id: check-manifest name: check-manifest stages: [pre-push] entry: uv run --extra=dev -m check_manifest language: python pass_filenames: false additional_dependencies: [uv==0.9.5] - id: pyright name: pyright stages: [pre-push] entry: uv run --extra=dev -m pyright . language: python types_or: [python, toml] pass_filenames: false additional_dependencies: [uv==0.9.5] - id: pyright-docs name: pyright-docs stages: [pre-push] entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="pyright" language: python types_or: [markdown, rst] - id: pyright-verifytypes name: pyright-verifytypes stages: [pre-push] # Use `--ignoreexternal` because we expose parts of the Sphinx API and Sphinx is not # thoroughly typed enough. entry: uv run --extra=dev -m pyright --ignoreexternal --verifytypes sphinx_substitution_extensions language: python pass_filenames: false types_or: [python] additional_dependencies: [uv==0.9.5] - id: vulture name: vulture entry: uv run --extra=dev -m vulture . language: python types_or: [python] pass_filenames: false additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: vulture-docs name: vulture docs entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="vulture" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: pyroma name: pyroma entry: uv run --extra=dev -m pyroma --min 10 . language: python pass_filenames: false types_or: [toml] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: deptry name: deptry entry: uv run --extra=dev -m deptry src/ language: python pass_filenames: false additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: pylint name: pylint entry: uv run --extra=dev -m pylint src/ tests/ language: python stages: [manual] pass_filenames: false additional_dependencies: [uv==0.9.5] - id: ruff-check-fix name: Ruff check fix entry: uv run --extra=dev -m ruff check --fix language: python types_or: [python] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: ruff-check-fix-docs name: Ruff check fix docs entry: uv run --extra=dev doccmd --language=python --command="ruff check --fix" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: ruff-format-fix name: Ruff format entry: uv run --extra=dev -m ruff format language: python types_or: [python] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: ruff-format-fix-docs name: Ruff format docs entry: uv run --extra=dev doccmd --language=python --no-pad-file --command="ruff format" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: doc8 name: doc8 entry: uv run --extra=dev -m doc8 language: python types_or: [rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: interrogate name: interrogate entry: uv run --extra=dev -m interrogate language: python types_or: [python] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: interrogate-docs name: interrogate docs entry: uv run --extra=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="interrogate" language: python types_or: [markdown, rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: pyproject-fmt-fix name: pyproject-fmt entry: uv run --extra=dev pyproject-fmt language: python types_or: [toml] files: pyproject.toml stages: [pre-commit] - id: yamlfix name: yamlfix entry: uv run --extra=dev yamlfix language: python types_or: [yaml] additional_dependencies: [uv==0.9.5] stages: [pre-commit] - id: sphinx-lint name: sphinx-lint entry: uv run --extra=dev sphinx-lint --enable=all --disable=line-too-long language: python types_or: [rst] additional_dependencies: [uv==0.9.5] stages: [pre-commit] adamtheturtle-sphinx-substitution-extensions-a8f3746/.vscode/000077500000000000000000000000001510663116100245405ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/.vscode/extensions.json000066400000000000000000000001341510663116100276300ustar00rootroot00000000000000{ "recommendations": [ "charliermarsh.ruff", "ms-python.python" ] } adamtheturtle-sphinx-substitution-extensions-a8f3746/.vscode/settings.json000066400000000000000000000005371510663116100273000ustar00rootroot00000000000000{ "[python]": { "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true }, "python.testing.pytestArgs": [ "." ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true } adamtheturtle-sphinx-substitution-extensions-a8f3746/CHANGELOG.rst000066400000000000000000000102761510663116100252260ustar00rootroot00000000000000Changelog ========= .. contents:: Next ---- 2025.11.17 ---------- - Give version in extension metadata. - ``literalinclude`` directive now supports the following options: - ``:content-substitutions:`` - Performs substitutions on the included file content. - ``:path-substitutions:`` - Performs substitutions on the file path. - ``image`` directive now supports the following option: - ``:path-substitutions:`` - Performs substitutions on the image file path. - Add ``substitutions_default_enabled`` configuration option to enable substitutions by default. When set to ``True`` in ``conf.py``: - Substitutions are applied to all ``code-block`` directives without requiring the ``:substitutions:`` flag. Use the ``:nosubstitutions:`` flag on individual code blocks to disable substitutions when the default is enabled. - Substitutions are applied to all ``literalinclude`` directives (both content and path) without requiring the ``:content-substitutions:`` or ``:path-substitutions:`` flags. Use the ``:nocontent-substitutions:`` or ``:nopath-substitutions:`` flags on individual literalinclude directives to disable substitutions when the default is enabled. - Substitutions are applied to all ``image`` directives (path) without requiring the ``:path-substitutions:`` flag. Use the ``:nopath-substitutions:`` flag on individual image directives to disable substitutions when the default is enabled. 2025.10.24 ---------- 2025.06.06 ---------- 2025.04.03 ---------- 2025.03.03 ---------- - Add support for Python 3.10. 2025.02.19 ---------- - Support the ``substitution-code`` role in MyST documents. - Support the ``substitution-download`` role in MyST documents. - Drop support for Python 3.10. 2025.01.02 ---------- - Supports situations where there is no source file name available to the extension, such as when using ``sphinx_toolbox.rest_example``. 2024.10.17 ---------- - Support Python 3.13. - In MyST documents, support the ``myst_sub_delimiters`` option. This means you can use the ``{{replace-me}}`` syntax in MyST documents. 2024.08.06 ------------ - Bump the minimum supported version of Sphinx to 7.3.5. - Remove support for ``sphinx-prompt``. Please create a GitHub issue if you have a use case for this extension which is not covered by the built-in Sphinx functionality. 2024.02.25 ------------ - Add ``substitution-download`` role. 2024.02.24.1 ------------ - Add support for MyST. Thanks to Václav Votípka (@eNcacz) for the contribution. 2024.02.24 ------------ - Bump the minimum supported version of Sphinx to 7.2.0. - Bump the minimum supported version of docutils to 0.19. - ``sphinx-prompt`` is no longer an optional dependency, meaning you can remove the ``[prompt]`` extras dependency specification. - Remove the need to specify the ``sphinx-prompt`` extension in ``conf.py`` in order to use the ``prompt`` directive. - Support Python 3.12 - Drop support for Python 3.9 2022.02.16 ------------ - Breaking change: The required Sphinx version is at least 4.0. - ``sphinx-prompt`` is now an optional dependency. Thanks go to @dgarcia360 for this change. 2020.09.30.0 ------------ 2020.07.04.1 ------------ - Ensure non-lower-case replacements can also be substituted in the inline substitution code role. 2020.07.04.0 ------------ - Ensure non-lower-case replacements can also be substituted. Thanks go to @Julian for this change. 2020.05.30.0 ------------ 2020.05.27.0 ------------ - Breaking change: Use ``:substitutions:`` option on ``code-block`` or ``prompt`` rather than new directives. 2020.05.23.0 ------------ - Breaking change: Use the default Sphinx replacements, rather than a custom variable. Thanks go to @sbaudoin for the original code for this change. Please make a GitHub issue if you have a use case which this does not suit. 2020.04.05.0 ------------ 2020.02.21.0 ------------ 2019.12.28.1 ------------ 2019.12.28.0 ------------ 2019.06.15.0 ------------ 2019.04.04.1 ------------ 2019.04.04.0 ------------ - Support Sphinx 2.0.0. 2018.11.12.3 ------------ - Make ``substitution`` a list, not a tuple. 2018.11.12.2 ------------ - Add ``substitution-code-block`` directive. 2018.11.12.0 ------------ - Initial release with ``substitution-prompt``. adamtheturtle-sphinx-substitution-extensions-a8f3746/CONTRIBUTING.rst000066400000000000000000000027751510663116100256530ustar00rootroot00000000000000Contributing ============ Contributions to this repository must pass tests and linting. CI is the canonical source of truth. Install contribution dependencies --------------------------------- Install Python dependencies in a virtual environment. .. code-block:: shell pip install --editable '.[dev]' Spell checking requires ``enchant``. This can be installed on macOS, for example, with `Homebrew`_: .. code-block:: shell brew install enchant and on Ubuntu with ``apt``: .. code-block:: shell apt-get install -y enchant Install ``pre-commit`` hooks: .. code-block:: shell pre-commit install Linting ------- Run lint tools either by committing, or with: .. code-block:: shell pre-commit run --all-files --hook-stage pre-commit --verbose pre-commit run --all-files --hook-stage pre-push --verbose pre-commit run --all-files --hook-stage manual --verbose .. _Homebrew: https://brew.sh Running tests ------------- Run ``pytest``: .. code-block:: shell pytest Continuous integration ---------------------- Tests are run on GitHub Actions. The configuration for this is in ```.github/workflows/``. Release Process --------------- Outcomes ~~~~~~~~ * A new ``git`` tag available to install. * A new package on PyPI. Perform a Release ~~~~~~~~~~~~~~~~~ #. `Install GitHub CLI`_. #. Perform a release: .. code-block:: shell $ gh workflow run release.yml --repo adamtheturtle/sphinx-substitution-extensions .. _Install GitHub CLI: https://cli.github.com/manual/installation adamtheturtle-sphinx-substitution-extensions-a8f3746/LICENSE000066400000000000000000000020551510663116100242060ustar00rootroot00000000000000MIT License Copyright (c) 2025 Adam Dangoor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. adamtheturtle-sphinx-substitution-extensions-a8f3746/README.rst000066400000000000000000000147031510663116100246730ustar00rootroot00000000000000|Build Status| |PyPI| Sphinx Substitution Extensions ============================== Extensions for Sphinx which allow substitutions within code blocks. .. contents:: Installation ------------ Sphinx Substitution Extensions is compatible with Sphinx 8.2.0+ using Python |minimum-python-version|\+. .. code-block:: console $ pip install Sphinx-Substitution-Extensions rST setup --------- 1. Add the following to ``conf.py`` to enable the extension: .. code-block:: python """Configuration for Sphinx.""" extensions = ["sphinxcontrib.spelling"] # Example existing extensions extensions += ["sphinx_substitution_extensions"] 2. Set the following variable in ``conf.py`` to define substitutions: .. code-block:: python """Configuration for Sphinx.""" rst_prolog = """ .. |release| replace:: 0.1 .. |author| replace:: Eleanor """ This will replace ``|release|`` in the new directives with ``0.1``, and ``|author|`` with ``Eleanor``. Using substitutions in rST documents ------------------------------------ ``code-block`` ~~~~~~~~~~~~~~ This adds a ``:substitutions:`` option to Sphinx's built-in `code-block`_ directive. .. code-block:: rst .. code-block:: shell :substitutions: echo "|author| released version |release|" Inline ``:substitution-code:`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: rst :substitution-code:`echo "|author| released version |release|"` ``substitution-download`` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: rst :substitution-download:`|author|'s manuscript <|author|_manuscript.txt>` ``literalinclude`` ~~~~~~~~~~~~~~~~~~ This adds ``:content-substitutions:`` and ``:path-substitutions:`` options to Sphinx's built-in `literalinclude`_ directive. Replace substitutions in the content of the included file: .. code-block:: rst .. literalinclude:: path/to/file.txt :content-substitutions: Replace substitutions in the file path: .. code-block:: rst .. literalinclude:: path/to/|author|_file.txt :path-substitutions: ``image`` ~~~~~~~~~ This adds a ``:path-substitutions:`` option to Sphinx's built-in `image`_ directive. Replace substitutions in the image path: .. code-block:: rst .. image:: path/to/|author|_diagram.png :path-substitutions: :alt: Diagram MyST Markdown setup ------------------- 1. Add ``sphinx_substitution_extensions`` to ``extensions`` in ``conf.py`` to enable the extension: .. code-block:: python """Configuration for Sphinx.""" extensions = ["myst_parser"] # Example existing extensions extensions += ["sphinx_substitution_extensions"] 2. Set the following variables in ``conf.py`` to define substitutions: .. code-block:: python """Configuration for Sphinx.""" myst_enable_extensions = ["substitution"] myst_substitutions = { "release": "0.1", "author": "Eleanor", } This will replace ``|release|`` in the new directives with ``0.1``, and ``|author|`` with ``Eleanor``. Enabling substitutions by default ---------------------------------- By default, you need to explicitly add the ``:substitutions:`` flag to ``code-block`` directives, and ``:content-substitutions:`` or ``:path-substitutions:`` flags to ``literalinclude`` directives. If you want substitutions to be applied by default without needing these flags, you can set the following in ``conf.py``: .. code-block:: python """Configuration for Sphinx.""" substitutions_default_enabled = True When this is enabled: - All ``code-block`` directives will have substitutions applied automatically - All ``literalinclude`` directives will have both content and path substitutions applied automatically You can disable substitutions for specific directives when the default is enabled: .. code-block:: rst .. code-block:: shell :nosubstitutions: echo "This |will| not be substituted" .. literalinclude:: path/to/file.txt :nocontent-substitutions: .. literalinclude:: path/to/|literal|_file.txt :nopath-substitutions: Using substitutions in MyST Markdown ------------------------------------ ``code-block`` ~~~~~~~~~~~~~~ This adds a ``:substitutions:`` option to Sphinx's built-in `code-block`_ directive. .. code-block:: markdown ```{code-block} bash :substitutions: echo "|author| released version |release|" ``` As well as using ``|author|``, you can also use ``{{author}}``. This will respect the value of ``myst_sub_delimiters`` as set in ``conf.py``. Inline ``:substitution-code:`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: rst {substitution-code}`echo "|author| released version |release|"` ``substitution-download`` ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: rst {substitution-download}`|author|'s manuscript <|author|_manuscript.txt>` ``literalinclude`` ~~~~~~~~~~~~~~~~~~ This adds ``:content-substitutions:`` and ``:path-substitutions:`` options to Sphinx's built-in `literalinclude`_ directive. Replace substitutions in the content of the included file: .. code-block:: markdown ```{literalinclude} path/to/file.txt :content-substitutions: ``` Replace substitutions in the file path: .. code-block:: markdown ```{literalinclude} path/to/|author|_file.txt :path-substitutions: ``` ``image`` ~~~~~~~~~ This adds a ``:path-substitutions:`` option to Sphinx's built-in `image`_ directive. Replace substitutions in the image path: .. code-block:: markdown ```{image} path/to/|author|_diagram.png :path-substitutions: :alt: Diagram ``` Credits ------- ClusterHQ Developers ~~~~~~~~~~~~~~~~~~~~ This package is largely inspired by code written for Flocker by ClusterHQ. Developers of the relevant code include, at least, Jon Giddy and Tom Prince. Contributing ------------ See `CONTRIBUTING.rst <./CONTRIBUTING.rst>`_. .. |Build Status| image:: https://github.com/adamtheturtle/sphinx-substitution-extensions/actions/workflows/ci.yml/badge.svg?branch=main :target: https://github.com/adamtheturtle/sphinx-substitution-extensions/actions .. _code-block: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block .. _literalinclude: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude .. _image: http://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-image .. |PyPI| image:: https://badge.fury.io/py/Sphinx-Substitution-Extensions.svg :target: https://badge.fury.io/py/Sphinx-Substitution-Extensions .. |minimum-python-version| replace:: 3.10 adamtheturtle-sphinx-substitution-extensions-a8f3746/pyproject.toml000066400000000000000000000227351510663116100261240ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = [ "setuptools", "setuptools-scm>=8.1.0", ] [project] name = "sphinx-substitution-extensions" description = "Extensions for Sphinx which allow for substitutions." readme = { file = "README.rst", content-type = "text/x-rst" } keywords = [ "documentation", "rst", "sphinx", ] license = "MIT" authors = [ { name = "Adam Dangoor", email = "adamdangoor@gmail.com" }, ] requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Pytest", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] dynamic = [ "version", ] dependencies = [ "beartype>=0.18.5", "docutils>=0.19", "myst-parser>=4.0.0", "sphinx>=8.1.0", ] optional-dependencies.dev = [ "actionlint-py==1.7.8.24", "check-manifest==0.51", "deptry==0.24.0", "doc8==2.0.0", "doccmd==2025.11.8.1", "docformatter==1.7.7", "interrogate==1.7.0", "mypy[faster-cache]==1.18.2", "mypy-strict-kwargs==2025.4.3", "pre-commit==4.4.0", "pylint[spelling]==4.0.3", "pyproject-fmt==2.11.1", "pyright==1.1.407", "pyroma==5.0", "pytest==9.0.1", "pytest-cov==7.0.0", "ruff==0.14.5", # We add shellcheck-py not only for shell scripts and shell code blocks, # but also because having it installed means that ``actionlint-py`` will # use it to lint shell commands in GitHub workflow files. "shellcheck-py==0.11.0.1", "shfmt-py==3.12.0.2", "sphinx-lint==1.0.1", "sphinx-toolbox==4.0.0", "types-docutils==0.22.2.20251006", "vulture==2.14", "yamlfix==1.19.0", ] optional-dependencies.release = [ "check-wheel-contents==0.6.3" ] urls.Source = "https://github.com/adamtheturtle/sphinx-substitution-extensions" [tool.setuptools] zip-safe = false [tool.setuptools.packages.find] where = [ "src", ] [tool.setuptools.package-data] sphinx_substitution_extensions = [ "py.typed", ] [tool.distutils.bdist_wheel] universal = true [tool.setuptools_scm] # This keeps the start of the version the same as the last release. # This is useful for our documentation to include e.g. binary links # to the latest released binary. # # Code to match this is in ``conf.py``. version_scheme = "post-release" [tool.ruff] line-length = 79 lint.select = [ "ALL", ] lint.ignore = [ # Ruff warns that this conflicts with the formatter. "COM812", # Allow our chosen docstring line-style - no one-line summary. "D200", "D205", "D212", "D415", # Ruff warns that this conflicts with the formatter. "ISC001", # Ignore "too-many-*" errors as they seem to get in the way more than # helping. "PLR0913", # Allow 'assert' as we use it for tests. "S101", ] # Do not automatically remove commented out code. # We comment out code during development, and with VSCode auto-save, this code # is sometimes annoyingly removed. lint.unfixable = [ "ERA001", ] lint.pydocstyle.convention = "google" [tool.pylint] [tool.pylint.'MASTER'] # Pickle collected data for later comparisons. persistent = true # Use multiple processes to speed up Pylint. jobs = 0 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. # See https://chezsoi.org/lucas/blog/pylint-strict-base-configuration.html. # We do not use the plugins: # - pylint.extensions.code_style # - pylint.extensions.magic_value # - pylint.extensions.while_used # as they seemed to get in the way. load-plugins = [ 'pylint.extensions.bad_builtin', 'pylint.extensions.comparison_placement', 'pylint.extensions.consider_refactoring_into_while_condition', 'pylint.extensions.docparams', 'pylint.extensions.dunder', 'pylint.extensions.eq_without_hash', 'pylint.extensions.for_any_all', 'pylint.extensions.mccabe', 'pylint.extensions.no_self_use', 'pylint.extensions.overlapping_exceptions', 'pylint.extensions.private_import', 'pylint.extensions.redefined_loop_name', 'pylint.extensions.redefined_variable_type', 'pylint.extensions.set_membership', 'pylint.extensions.typing', ] # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension = false [tool.pylint.'MESSAGES CONTROL'] # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. enable = [ 'bad-inline-option', 'deprecated-pragma', 'file-ignored', 'spelling', 'use-symbolic-message-instead', 'useless-suppression', ] # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" disable = [ 'too-few-public-methods', 'too-many-positional-arguments', 'too-many-locals', 'too-many-arguments', 'too-many-instance-attributes', 'too-many-return-statements', 'too-many-lines', 'locally-disabled', # Let flake8 handle long lines 'line-too-long', # Let ruff handle unused imports 'unused-import', # Let ruff deal with sorting 'ungrouped-imports', # We don't need everything to be documented because of mypy 'missing-type-doc', 'missing-return-type-doc', # Too difficult to please 'duplicate-code', # Let ruff handle imports 'wrong-import-order', # Let ruff find protected member access. 'protected-access', ] [tool.pylint.'FORMAT'] # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt = false [tool.pylint.'SPELLING'] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict = 'en_US' # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file = 'spelling_private_dict.txt' # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words = 'no' [tool.docformatter] make-summary-multi-line = true [tool.check-manifest] ignore = [ ".checkmake-config.ini", ".yamlfmt", "*.enc", ".pre-commit-config.yaml", "readthedocs.yaml", "CHANGELOG.rst", "CODE_OF_CONDUCT.rst", "CONTRIBUTING.rst", "LICENSE", "Makefile", "ci", "ci/**", "docs", "docs/**", ".git_archival.txt", "sample", "sample/**", "spelling_private_dict.txt", "tests", "tests-pylintrc", "tests/**", "lint.mk", ] [tool.deptry] pep621_dev_dependency_groups = [ "dev", "release", ] [tool.pyproject-fmt] indent = 4 keep_full_version = true max_supported_python = "3.13" [tool.pytest.ini_options] xfail_strict = true log_cli = true [tool.coverage.run] branch = true [tool.coverage.report] exclude_also = [ "if TYPE_CHECKING:", ] [tool.mypy] strict = true files = [ "." ] exclude = [ "build" ] follow_untyped_imports = true plugins = [ "mypy_strict_kwargs", ] [tool.pyright] enableTypeIgnoreComments = false reportUnnecessaryTypeIgnoreComment = true typeCheckingMode = "strict" [tool.interrogate] fail-under = 100 omit-covered-files = true verbose = 2 [tool.doc8] max_line_length = 2000 ignore_path = [ "./.eggs", "./docs/build", "./docs/build/spelling/output.txt", "./node_modules", "./sample/build", "./src/*.egg-info/", "./src/*/_setuptools_scm_version.txt", ] [tool.vulture] # Ideally we would limit the paths to the source code where we want to ignore names, # but Vulture does not enable this. ignore_names = [ # pytest configuration "pytest_collect_file", "pytest_collection_modifyitems", "pytest_plugins", # pytest fixtures - we name fixtures like this for this purpose "fixture_*", # Sphinx "autoclass_content", "autoclass_content", "autodoc_member_order", "copybutton_exclude", "extensions", "html_show_copyright", "html_show_sourcelink", "html_show_sphinx", "html_theme", "html_theme_options", "html_title", "htmlhelp_basename", "intersphinx_mapping", "language", "linkcheck_ignore", "linkcheck_retries", "master_doc", "myst_enable_extensions", "myst_substitutions", "nitpicky", "project_copyright", "pygments_style", "rst_prolog", "setup", "source_suffix", "spelling_word_list_filename", "substitutions_default_enabled", "templates_path", "warning_is_error", ] # Duplicate some of .gitignore exclude = [ ".venv" ] [tool.yamlfix] section_whitelines = 1 whitelines = 1 adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/000077500000000000000000000000001510663116100244605ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/000077500000000000000000000000001510663116100257605ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/Eleanor.txt000066400000000000000000000000461510663116100301060ustar00rootroot00000000000000This is a downloadable text document. adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/Eleanor_diagram.png000066400000000000000000000102161510663116100315370ustar00rootroot00000000000000PNG  IHDR,brUIDATx{xS$iRnATP P)"YTy)QZ>*S[&<ʘ)ozKd#KzIi9_iH7i< ,                            M钦i<VX 땒Ñ|KAAAAAAAAAAAAAAAAA?PHx%\5x3t)U쏨i5NGMvk2F5߃)آӋ_#VWV"ڡ.m,S;QVsOO_^Vd UJe:"k;Jo끤c*+c[9s>]N[m9‡'%_;Ի{c?}J[m7AwأsyJG޹>gL{;Kv;dd`x|ej>ddԢӓ)6,/yęϭ^S83WYRK]v嫯Ha#ӕRNGi+s#}K3{പrm|w=7gN_rѴ C2s/['+,Sq:&g{'vNg6_J**.W渇['{W E)vʏ;5O)OY=]5.k/3!>ڹ㎙)zbzo,N{ZV[Tԅ{jKuUe t`3,Wk磢=nvZֹ^>8mȜM/,}o\Vg;|VMԹZdw}5NzQ;u{,֓]AaHoSril٠z7V=@)?:v: BozM)Morqx7v][7*yCf to /{}t`3 FOqV,ZmcfQJ];"kBFzBnqGxo?1-x}it                 (/$=ϙ#Q0&ϡDZ M=A/(B`Y#[h"FΘ&8Ep(l,,44E} #X{rWAP۴oQ-ԏ`٠f@}n"`-D,Y[fBLG/ztDt xzxy.pH9‹` a>e"XFq2TF*F$`t2%b̉` CC3#Xb*K _цGGAWHt`?%jN V3/Jn5}n J o_SMdjBZK(Q, +|ڊ,VX! | %UFmpnQXa5/Rel,ŒVsҒ*k3BV1;MV Pr14-4t: 0BE 3 K7 Gz_߫:6uj7                   J^B^z-Msw\0e gO=}{7>8P{Q6vQ1 -8y yf[p݂GoE潼!B~;vLOIQJLKIjY>?woYp +.^3ׯ-,L-*=jEpŝ799&:}e]vw;yTZ7cViBw=ڣMߗ=۴)9z[N4(n_r:Y{OܴۣVq<O҈$y UUVS=z(O?6_8OUù[BbB Z^                 ;@ 3 VX K"+?C C3!XPenLfEmd)ɰÀ`5-`/ (-bφ X-`WF ĠS.wE"7dKH{# X!C"XA"XzȣVK ?K %bEo<`_֢,FA["XM |[N. !+W= ~HgkaJ޾hB0Xa!ZY#BXa!2-: !^p-oFpXa!| .R&bЯ'U &ϡo~"$\, 9BY",~J(s(қ`dM@Xa`````````````````?hѥoߞ95Mӧ'9|HMsϽرJ{KڵVoscƹ.}7aÆ]ӫnq'NTl٢pf͝vlcN9r;}{.X%Mwy ԏ`qnWbb7Tu[ү[o'##=3sxqxjxcZFFzQS*S[&<ʘ)ozKd#KzIi9_iH7i< ,                            M钦i<VX 땒Ñ|KAAAAAAAAAAAAAAAAA?PHx%\5x3t)U쏨i5NGMvk2F5߃)آӋ_#VWV"ڡ.m,S;QVsOO_^Vd UJe:"k;Jo끤c*+c[9s>]N[m9‡'%_;Ի{c?}J[m7AwأsyJG޹>gL{;Kv;dd`x|ej>ddԢӓ)6,/yęϭ^S83WYRK]v嫯Ha#ӕRNGi+s#}K3{പrm|w=7gN_rѴ C2s/['+,Sq:&g{'vNg6_J**.W渇['{W E)vʏ;5O)OY=]5.k/3!>ڹ㎙)zbzo,N{ZV[Tԅ{jKuUe t`3,Wk磢=nvZֹ^>8mȜM/,}o\Vg;|VMԹZdw}5NzQ;u{,֓]AaHoSril٠z7V=@)?:v: BozM)Morqx7v][7*yCf to /{}t`3 FOqV,ZmcfQJ];"kBFzBnqGxo?1-x}it                 (/$=ϙ#Q0&ϡDZ M=A/(B`Y#[h"FΘ&8Ep(l,,44E} #X{rWAP۴oQ-ԏ`٠f@}n"`-D,Y[fBLG/ztDt xzxy.pH9‹` a>e"XFq2TF*F$`t2%b̉` CC3#Xb*K _цGGAWHt`?%jN V3/Jn5}n J o_SMdjBZK(Q, +|ڊ,VX! | %UFmpnQXa5/Rel,ŒVsҒ*k3BV1;MV Pr14-4t: 0BE 3 K7 Gz_߫:6uj7                   J^B^z-Msw\0e gO=}{7>8P{Q6vQ1 -8y yf[p݂GoE潼!B~;vLOIQJLKIjY>?woYp +.^3ׯ-,L-*=jEpŝ799&:}e]vw;yTZ7cViBw=ڣMߗ=۴)9z[N4(n_r:Y{OܴۣVq<O҈$y UUVS=z(O?6_8OUù[BbB Z^                 ;@ 3 VX K"+?C C3!XPenLfEmd)ɰÀ`5-`/ (-bφ X-`WF ĠS.wE"7dKH{# X!C"XA"XzȣVK ?K %bEo<`_֢,FA["XM |[N. !+W= ~HgkaJ޾hB0Xa!ZY#BXa!2-: !^p-oFpXa!| .R&bЯ'U &ϡo~"$\, 9BY",~J(s(қ`dM@Xa`````````````````?hѥoߞ95Mӧ'9|HMsϽرJ{KڵVoscƹ.}7aÆ]ӫnq'NTl٢pf͝vlcN9r;}{.X%Mwy ԏ`qnWbb7Tu[ү[o'##=3sxqxjxcZFFzQS`. :substitution-download:`Script by |author| <../source/|author|.txt>`. ``literalinclude`` ------------------ Content substitutions ~~~~~~~~~~~~~~~~~~~~~ .. rest-example:: .. literalinclude:: sample_include.txt .. literalinclude:: sample_include.txt :content-substitutions: Path substitutions ~~~~~~~~~~~~~~~~~~ .. rest-example:: .. literalinclude:: |author|.txt :path-substitutions: ``image`` --------- Path substitutions ~~~~~~~~~~~~~~~~~~ .. rest-example:: .. image:: |author|_diagram.png :path-substitutions: :alt: Diagram for |author| .. This is a test of parallel document builds. You need at least 5 documents. See: https://github.com/adamtheturtle/sphinx-substitution-extensions/pull/173 .. toctree:: :hidden: one two three four five .. toctree:: markdown_sample adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/markdown_sample.md000066400000000000000000000042301510663116100314640ustar00rootroot00000000000000Samples for substitution directives in Markdown =============================================== Configuration ------------- ```{literalinclude} conf.py :language: python ``` ``code-block`` -------------- ```{code-block} markdown ```{code-block} markdown echo "The author is |author|" ``` ```{code-block} markdown :substitutions: echo "The author is |author|" ``` or, with the value of the `myst_sub_delimiters` `conf.py` setting: ```{code-block} markdown echo "The author is {{author}}" ``` ```{code-block} markdown :substitutions: echo "The author is {{author}}" ``` ``` => ```{code-block} markdown echo "The author is |author|" ``` ```{code-block} markdown :substitutions: echo "The author is |author|" ``` ```{code-block} markdown echo "The author is {{author}}" ``` ```{code-block} markdown :substitutions: echo "The author is {{author}}" ``` Inline ``:substitution-code:`` ------------------------------ ```{code-block} markdown {substitution-code}`The author is {{author}}` ``` => {substitution-code}`The author is {{author}}` ``substitution-download`` ------------------------- ```{code-block} markdown {substitution-download}`Script by {{author}} <../source/Eleanor.txt>` ``` => {substitution-download}`Script by {{author}} <../source/Eleanor.txt>` ``literalinclude`` ------------------ ### Content substitutions ```{code-block} markdown ```{literalinclude} sample_include.txt ``` ```{literalinclude} sample_include.txt :content-substitutions: ``` ``` => ```{literalinclude} sample_include.txt ``` ```{literalinclude} sample_include.txt :content-substitutions: ``` ### Path substitutions ```{code-block} markdown ```{literalinclude} {{author}}.txt :path-substitutions: ``` ``` => ```{literalinclude} {{author}}.txt :path-substitutions: ``` ``image`` --------- ### Path substitutions ```{code-block} markdown ```{image} {{author}}_diagram.png :path-substitutions: :alt: Diagram for {{author}} ``` ``` => ```{image} {{author}}_diagram.png :path-substitutions: :alt: Diagram for {{author}} ``` adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/one.rst000066400000000000000000000000211510663116100272640ustar00rootroot00000000000000===== One ===== adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/sample_image.png000066400000000000000000000001031510663116100311030ustar00rootroot00000000000000PNG  IHDRĉ IDATxc -IENDB`adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/sample_include.txt000066400000000000000000000001201510663116100314760ustar00rootroot00000000000000This is a sample file for demonstrating literalinclude. The author is |author|. adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/three.rst000066400000000000000000000000271510663116100276200ustar00rootroot00000000000000======= Three ======= adamtheturtle-sphinx-substitution-extensions-a8f3746/sample/source/two.rst000066400000000000000000000000211510663116100273140ustar00rootroot00000000000000===== Two ===== adamtheturtle-sphinx-substitution-extensions-a8f3746/spelling_private_dict.txt000066400000000000000000000002571510663116100303160ustar00rootroot00000000000000admin beartype changelog conf hardcoded inline linters py pyright pytest reportUnknownMemberType reportUnknownParameterType reportUnknownVariableType rst str tuple whitespace adamtheturtle-sphinx-substitution-extensions-a8f3746/src/000077500000000000000000000000001510663116100237665ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/src/sphinx_substitution_extensions/000077500000000000000000000000001510663116100324125ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/src/sphinx_substitution_extensions/__init__.py000066400000000000000000000360741510663116100345350ustar00rootroot00000000000000""" Custom Sphinx extensions. """ from importlib.metadata import version from typing import Any, ClassVar from beartype import beartype from docutils.nodes import ( Element, Node, Text, substitution_definition, system_message, ) from docutils.parsers.rst import directives from docutils.parsers.rst.directives.images import Image from docutils.parsers.rst.roles import code_role from docutils.parsers.rst.states import Inliner from docutils.statemachine import StringList from myst_parser.mocking import MockInliner from sphinx import addnodes from sphinx.application import Sphinx from sphinx.config import Config from sphinx.directives.code import CodeBlock, LiteralInclude from sphinx.environment import BuildEnvironment from sphinx.roles import XRefRole from sphinx.util.typing import ExtensionMetadata, OptionSpec from sphinx_substitution_extensions.shared import ( CONTENT_SUBSTITUTION_OPTION_NAME, NO_CONTENT_SUBSTITUTION_OPTION_NAME, NO_PATH_SUBSTITUTION_OPTION_NAME, NO_SUBSTITUTION_OPTION_NAME, PATH_SUBSTITUTION_OPTION_NAME, SUBSTITUTION_OPTION_NAME, ) @beartype def _get_delimiter_pairs( env: BuildEnvironment, config: Config, ) -> set[tuple[str, str]]: """ Get the delimiter pairs for substitution. """ markdown_suffixes = { key.lstrip(".") for key, value in config.source_suffix.items() if value == "markdown" } # Use `| |` on reST as it is the default substitution syntax. # Use `| |` on MyST for backwards compatibility as this is what we # originally shipped with. delimiter_pairs = {("|", "|")} parser_supported_formats = set(env.parser.supported) if parser_supported_formats.intersection(markdown_suffixes): opening_delimiter, closing_delimiter = config.myst_sub_delimiters new_delimiter_pair = ( opening_delimiter + opening_delimiter, closing_delimiter + closing_delimiter, ) delimiter_pairs = {*delimiter_pairs, new_delimiter_pair} return delimiter_pairs @beartype def _get_substitution_defs( env: BuildEnvironment, config: Config, substitution_defs: dict[str, substitution_definition], ) -> dict[str, str]: """ Get the substitution definitions from the environment. """ markdown_suffixes = { key.lstrip(".") for key, value in config.source_suffix.items() if value == "markdown" } parser_supported_formats = set(env.parser.supported) if parser_supported_formats.intersection(markdown_suffixes): if "substitution" in config.myst_enable_extensions: return dict(config.myst_substitutions) else: return { key: value.astext() for key, value in substitution_defs.items() } return {} @beartype def _apply_substitutions( text: str, substitution_defs: dict[str, str], delimiter_pairs: set[tuple[str, str]], ) -> str: """ Apply substitutions to text using the given delimiter pairs. """ new_text = text for name, replacement in substitution_defs.items(): for delimiter_pair in delimiter_pairs: opening_delimiter, closing_delimiter = delimiter_pair new_text = new_text.replace( f"{opening_delimiter}{name}{closing_delimiter}", replacement, ) return new_text @beartype def _should_apply_substitutions( options: dict[str, Any], config: Config, yes_flag: str, no_flag: str, ) -> bool: """ Whether substitutions should be applied based on flags and configuration. """ if no_flag in options: return False if yes_flag in options: return True return bool(config.substitutions_default_enabled) @beartype def _process_node( node: Node, substitution_defs: dict[str, str], delimiter_pairs: set[tuple[str, str]], ) -> None: """ Recursively process nodes to apply substitutions. """ if isinstance(node, Element): new_text = _apply_substitutions( text=node.rawsource, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) node.rawsource = new_text first_child = node.children[0] if isinstance(first_child, Text): node.replace(old=first_child, new=Text(data=new_text)) for child in node.children: _process_node( node=child, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) @beartype class SubstitutionCodeBlock(CodeBlock): """ Similar to CodeBlock but replaces placeholders with variables. """ option_spec: ClassVar[OptionSpec] = CodeBlock.option_spec option_spec[SUBSTITUTION_OPTION_NAME] = directives.flag option_spec[NO_SUBSTITUTION_OPTION_NAME] = directives.flag def run(self) -> list[Node]: """ Replace placeholders with given variables. """ new_content = StringList() existing_content = self.content substitution_defs = _get_substitution_defs( env=self.env, config=self.config, substitution_defs=self.state.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, ) should_apply_substitutions = _should_apply_substitutions( options=self.options, config=self.config, yes_flag=SUBSTITUTION_OPTION_NAME, no_flag=NO_SUBSTITUTION_OPTION_NAME, ) for item in existing_content: new_item = item if should_apply_substitutions: new_item = _apply_substitutions( text=item, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) new_item_string_list = StringList(initlist=[new_item]) new_content.extend(other=new_item_string_list) self.content = new_content return super().run() @beartype class SubstitutionCodeRole: """ Custom role for substitution code. """ options: ClassVar[dict[str, Any]] = { "class": directives.class_option, "language": directives.unchanged, } def __call__( # pylint: disable=dangerous-default-value self, typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner | MockInliner, # We allow mutable defaults as the Sphinx implementation requires it. options: dict[Any, Any] = {}, # noqa: B006 content: list[str] = [], # noqa: B006 ) -> tuple[list[Node], list[system_message]]: """ Replace placeholders with given variables. """ settings = inliner.document.settings env = settings.env substitution_defs = _get_substitution_defs( env=env, config=env.config, substitution_defs=inliner.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=env.config, ) text = _apply_substitutions( text=text, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) rawtext = _apply_substitutions( text=rawtext, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) # ``types-docutils`` says that ``code_role`` requires an ``Inliner`` # for ``inliner``. # # We can remove this when # https://github.com/executablebooks/MyST-Parser/issues/1017 # is resolved by typing ``inliner`` as ``Inliner``. if isinstance(inliner, MockInliner): new_inliner = Inliner() new_inliner.document = inliner.document inliner = new_inliner return code_role( role=typ, rawtext=rawtext, text=text, lineno=lineno, inliner=inliner, options=options, content=content, ) @beartype class SubstitutionLiteralInclude(LiteralInclude): """ Similar to LiteralInclude but replaces placeholders with variables. """ option_spec: ClassVar[OptionSpec] = LiteralInclude.option_spec.copy() option_spec[CONTENT_SUBSTITUTION_OPTION_NAME] = directives.flag option_spec[PATH_SUBSTITUTION_OPTION_NAME] = directives.flag option_spec[NO_CONTENT_SUBSTITUTION_OPTION_NAME] = directives.flag option_spec[NO_PATH_SUBSTITUTION_OPTION_NAME] = directives.flag def run(self) -> list[Node]: """ Replace placeholders with given variables in the file path and/or included file content. """ should_apply_path_substitutions = _should_apply_substitutions( options=self.options, config=self.config, yes_flag=PATH_SUBSTITUTION_OPTION_NAME, no_flag=NO_PATH_SUBSTITUTION_OPTION_NAME, ) if should_apply_path_substitutions: substitution_defs = _get_substitution_defs( env=self.env, config=self.config, substitution_defs=self.state.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, ) for argument_index, argument in enumerate(iterable=self.arguments): self.arguments[argument_index] = _apply_substitutions( text=argument, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) nodes_list = super().run() should_apply_content_substitutions = _should_apply_substitutions( options=self.options, config=self.config, yes_flag=CONTENT_SUBSTITUTION_OPTION_NAME, no_flag=NO_CONTENT_SUBSTITUTION_OPTION_NAME, ) if should_apply_content_substitutions: substitution_defs = _get_substitution_defs( env=self.env, config=self.config, substitution_defs=self.state.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, ) for node in nodes_list: _process_node( node=node, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) return nodes_list @beartype class SubstitutionImage(Image): """ Similar to Image but replaces placeholders with variables in the path. """ _new_option_spec = Image.option_spec.copy() if Image.option_spec else {} _new_option_spec[PATH_SUBSTITUTION_OPTION_NAME] = directives.flag _new_option_spec[NO_PATH_SUBSTITUTION_OPTION_NAME] = directives.flag option_spec: ClassVar[OptionSpec | None] = _new_option_spec def run(self) -> list[Node]: """ Replace placeholders with given variables in the image path. """ env = self.state.document.settings.env config = env.config should_apply_path_substitutions = _should_apply_substitutions( options=self.options, config=config, yes_flag=PATH_SUBSTITUTION_OPTION_NAME, no_flag=NO_PATH_SUBSTITUTION_OPTION_NAME, ) if should_apply_path_substitutions: substitution_defs = _get_substitution_defs( env=env, config=config, substitution_defs=self.state.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=config, ) for argument_index, argument in enumerate(iterable=self.arguments): self.arguments[argument_index] = _apply_substitutions( text=argument, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) return list(super().run()) @beartype class SubstitutionXRefRole(XRefRole): """ Custom role for XRefs. """ def create_xref_node(self) -> tuple[list[Node], list[system_message]]: """Override parent method to set classes. This is a bit of a hack because it assumes that the role name will be `substitution-` and that we want to remove the `substitution-`. """ for index, class_name in enumerate(iterable=self.classes): self.classes[index] = class_name.replace("substitution-", "") return super().create_xref_node() def process_link( self, env: BuildEnvironment, refnode: Element, # We allow a boolean-typed positional argument as we are matching the # method signature of the parent class. has_explicit_title: bool, # noqa: FBT001 title: str, target: str, ) -> tuple[str, str]: """ Override parent method to replace placeholders with given variables. """ assert isinstance(env, BuildEnvironment) substitution_defs = _get_substitution_defs( env=env, config=env.config, substitution_defs=self.inliner.document.substitution_defs, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=env.config, ) title = _apply_substitutions( text=title, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) target = _apply_substitutions( text=target, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) # Use the default implementation to process the link # as it handles whitespace in target text. return super().process_link( env=env, refnode=refnode, has_explicit_title=has_explicit_title, title=title, target=target, ) @beartype def setup(app: Sphinx) -> ExtensionMetadata: """ Add the custom directives to Sphinx. """ app.add_config_value(name="substitutions", default=[], rebuild="html") app.add_config_value( name="substitutions_default_enabled", default=False, rebuild="html", ) directives.register_directive( name="code-block", directive=SubstitutionCodeBlock, ) directives.register_directive( name="literalinclude", directive=SubstitutionLiteralInclude, ) directives.register_directive( name="image", directive=SubstitutionImage, ) app.add_role(name="substitution-code", role=SubstitutionCodeRole()) substitution_download_role = SubstitutionXRefRole( nodeclass=addnodes.download_reference, ) app.add_role(name="substitution-download", role=substitution_download_role) return { "parallel_read_safe": True, "version": version(distribution_name="sphinx-substitution-extensions"), } adamtheturtle-sphinx-substitution-extensions-a8f3746/src/sphinx_substitution_extensions/py.typed000066400000000000000000000000001510663116100340770ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/src/sphinx_substitution_extensions/shared.py000066400000000000000000000010221510663116100342250ustar00rootroot00000000000000""" Constants and functions shared between modules. """ # This is hardcoded in doc8 as a valid option so be wary that changing this # may break doc8 linting. # See https://github.com/PyCQA/doc8/pull/34. SUBSTITUTION_OPTION_NAME = "substitutions" CONTENT_SUBSTITUTION_OPTION_NAME = "content-substitutions" PATH_SUBSTITUTION_OPTION_NAME = "path-substitutions" NO_SUBSTITUTION_OPTION_NAME = "nosubstitutions" NO_CONTENT_SUBSTITUTION_OPTION_NAME = "nocontent-substitutions" NO_PATH_SUBSTITUTION_OPTION_NAME = "nopath-substitutions" spelling_private_dict.txt000066400000000000000000000000001510663116100374340ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/src/sphinx_substitution_extensionsadamtheturtle-sphinx-substitution-extensions-a8f3746/tests/000077500000000000000000000000001510663116100243415ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-a8f3746/tests/__init__.py000066400000000000000000000000561510663116100264530ustar00rootroot00000000000000""" Tests for the substitution extension. """ adamtheturtle-sphinx-substitution-extensions-a8f3746/tests/conftest.py000066400000000000000000000007251510663116100265440ustar00rootroot00000000000000""" Configuration for pytest. """ import pytest from beartype import beartype pytest_plugins = "sphinx.testing.fixtures" # pylint: disable=invalid-name def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: """ Apply the beartype decorator to all collected test functions. """ for item in items: # All our tests are functions, for now assert isinstance(item, pytest.Function) item.obj = beartype(obj=item.obj) adamtheturtle-sphinx-substitution-extensions-a8f3746/tests/test_substitution_extensions.py000066400000000000000000002067351510663116100330220ustar00rootroot00000000000000""" Tests for Sphinx extensions. """ from collections.abc import Callable from importlib.metadata import version from pathlib import Path from textwrap import dedent from sphinx.testing.util import SphinxTestApp import sphinx_substitution_extensions def test_setup( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ Test that the setup function returns the expected metadata. """ source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() app = make_app( srcdir=source_directory, ) setup_result = sphinx_substitution_extensions.setup(app=app) pkg_version = version(distribution_name="sphinx-substitution-extensions") assert setup_result == { "parallel_read_safe": True, "version": pkg_version, } def test_no_substitution_code_block( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``code-block`` directive does not replace placeholders. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. code-block:: shell $ PRE-|a|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() app_expected = make_app( srcdir=source_directory, exception_on_warning=True, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_code_block( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``code-block`` directive replaces the placeholders defined in ``conf.py`` as specified. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. code-block:: shell :substitutions: $ PRE-|a|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. code-block:: shell $ PRE-example_substitution-POST """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_code_block_case_preserving( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``code-block`` directive respects the original case of replacements. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |aBcD_eFgH| replace:: example_substitution .. code-block:: shell :substitutions: $ PRE-|aBcD_eFgH|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. code-block:: shell $ PRE-example_substitution-POST """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_enabled( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is set to True in conf.py, code blocks should apply substitutions by default without needing the ``:substitutions:`` flag. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. code-block:: shell $ PRE-|a|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. code-block:: shell $ PRE-example_substitution-POST """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_disabled_with_flag( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is True but a code block has the ``:nosubstitutions:`` flag, substitutions should not be applied. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. code-block:: shell :nosubstitutions: $ PRE-|a|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() app_expected = make_app( srcdir=source_directory, exception_on_warning=True, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_inline( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``substitution-code`` role replaces the placeholders defined in ``conf.py`` as specified. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |a| replace:: example_substitution Example :substitution-code:`PRE-|a|-POST` """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ Example :code:`PRE-example_substitution-POST` """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_inline_case_preserving( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``substitution-code`` role respects the original case of replacements. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. |aBcD_eFgH| replace:: example_substitution Example :substitution-code:`PRE-|aBcD_eFgH|-POST` """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ Example :code:`PRE-example_substitution-POST` """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_download( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``substitution-download`` role replaces the placeholders defined in ``conf.py`` as specified in both the download text and the download target. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() # Importantly we have a non-space whitespace character in the target name. downloadable_file = ( source_directory / "tgt_pre-example_substitution-tgt_post .py" ) downloadable_file.write_text(data="Sample") source_file_content = dedent( # Importantly we have a substitution in the download text and the # target. text="""\ .. |a| replace:: example_substitution :substitution-download:`txt_pre-|a|-txt_post ` """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ :download:`txt_pre-example_substitution-txt_post ` """, # noqa: E501 ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_no_substitution_literal_include( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive does not replace placeholders. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: example.txt """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() app_expected = make_app( srcdir=source_directory, exception_on_warning=True, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_literal_include( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive replaces the placeholders defined in ``conf.py`` as specified when the `:content-substitutions:` flag is set. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: example.txt :content-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() include_file.write_text( data="Content with example_substitution placeholder" ) equivalent_source = dedent( text="""\ .. literalinclude:: example.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_literal_include_multiple( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive replaces multiple placeholders. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="PRE-|a|-MID-|b|-POST") source_file_content = dedent( text="""\ .. |a| replace:: first_substitution .. |b| replace:: second_substitution .. literalinclude:: example.txt :content-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() include_file.write_text( data="PRE-first_substitution-MID-second_substitution-POST", ) equivalent_source = dedent( text="""\ .. literalinclude:: example.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_literal_include_with_caption( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive works with captions. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: example.txt :caption: Example caption :content-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() include_file.write_text( data="Content with example_substitution placeholder" ) equivalent_source = dedent( text="""\ .. literalinclude:: example.txt :caption: Example caption """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_literal_include_in_rest_example( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive works inside rest-example. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. rest-example:: .. literalinclude:: example.txt :content-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, warningiserror=True, confoverrides={ "extensions": [ "sphinx_substitution_extensions", "sphinx_toolbox.rest_example", ], }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() assert "example_substitution" in content_html def test_substitution_literal_include_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive replaces placeholders in the file path when the `:path-substitutions:` flag is set. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() # Create a file with substitution in the name include_file = source_directory / "example_substitution.txt" include_file.write_text(data="File content") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: |a|.txt :path-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() # Compare with directly using the filename equivalent_source = dedent( text="""\ .. literalinclude:: example_substitution.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_literal_include_both_path_and_content( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``literalinclude`` directive can use both path and content substitutions at the same time. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() # Create a file with substitution in the name and content include_file = source_directory / "example_substitution.txt" include_file.write_text(data="Content with |b| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. |b| replace:: test_value .. literalinclude:: |a|.txt :path-substitutions: :content-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() # Create equivalent file with substituted content include_file.write_text(data="Content with test_value placeholder") equivalent_source = dedent( text="""\ .. literalinclude:: example_substitution.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_literal_include_content( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is True, ``literalinclude`` should apply content substitutions by default without requiring the ``:content- substitutions:`` flag. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: example.txt """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() include_file.write_text( data="Content with example_substitution placeholder" ) equivalent_source = dedent( text="""\ .. literalinclude:: example.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_literal_include_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is True, ``literalinclude`` should apply path substitutions by default without requiring the ``:path- substitutions:`` flag. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example_substitution.txt" include_file.write_text(data="File content") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: |a|.txt """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. literalinclude:: example_substitution.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_literal_include_disabled_content( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is True but ``literalinclude`` has the ``:nocontent-substitutions:`` flag, content substitutions should not be applied. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" include_file.write_text(data="Content with |a| placeholder") source_file_content = dedent( text="""\ .. |a| replace:: example_substitution .. literalinclude:: example.txt :nocontent-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. literalinclude:: example.txt """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_literal_include_disabled_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """When ``substitutions_default_enabled`` is True but ``literalinclude`` has the ``:nopath-substitutions:`` flag, path substitutions should not be applied. Note: This test uses MyST format with custom delimiters because the `|` character cannot be used in Windows file paths. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() # Use custom delimiters [[a]] instead of |a| because | is not allowed # in Windows file paths include_file = source_directory / "[[a]].txt" include_file.write_text(data="File content") index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{literalinclude} [[a]].txt :nopath-substitutions: ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, "myst_sub_delimiters": ("[", "]"), "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{literalinclude} [[a]].txt ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html class TestMyst: """ Tests for MyST documents. """ @staticmethod def test_myst_substitutions_ignored_given_rst_definition( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ MyST substitutions are ignored in rST documents with a rST substitution definition. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. |a| replace:: rst_prolog_substitution .. code-block:: shell :substitutions: $ PRE-|a|-POST """, ) source_file.write_text(data=index_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "myst_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. code-block:: shell $ PRE-rst_prolog_substitution-POST """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "index.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_myst_substitutions_ignored_without_rst_definition( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ MyST substitutions are ignored in rST documents without a rST substitution definition. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() source_file_content = dedent( text="""\ .. code-block:: shell :substitutions: $ PRE-|a|-POST """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "myst_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. code-block:: shell $ PRE-|a|-POST """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "index.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_myst_substitutions( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ MyST substitutions are respected in MyST documents. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{code-block} $ PRE-example_substitution-POST ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html def test_no_substitution_image( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """The ``image`` directive does not replace custom placeholders by default. Note: reST by default processes |substitutions| in image paths, but our extension adds the ability to use custom delimiters like {{var}}. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) source_file_content = dedent( text="""\ .. |a| replace:: test_image .. image:: test_image.png """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() app_expected = make_app( srcdir=source_directory, exception_on_warning=True, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() # The behavior should be the same with or without our extension # when not using :path-substitutions: assert content_html == expected_content_html def test_substitution_image_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``image`` directive replaces placeholders in the file path when the ``:path-substitutions:`` flag is set. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() # Create a simple image file with substitution in the name image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) source_file_content = dedent( text="""\ .. |a| replace:: test_image .. image:: |a|.png :path-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() # Compare with directly using the filename equivalent_source = dedent( text="""\ .. image:: test_image.png """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_image_path_multiple( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``image`` directive replaces multiple placeholders in the file path. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() # Create an image file with multiple substitutions in the name image_file = source_directory / "pre_test_mid_image_post.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) source_file_content = dedent( text="""\ .. |a| replace:: test .. |b| replace:: image .. image:: pre_|a|_mid_|b|_post.png :path-substitutions: """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() # Compare with directly using the filename equivalent_source = dedent( text="""\ .. image:: pre_test_mid_image_post.png """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_substitution_image_with_options( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``image`` directive works with standard image options. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) source_file_content = dedent( text="""\ .. |a| replace:: test_image .. image:: |a|.png :path-substitutions: :alt: Test image alt text :width: 100px """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. image:: test_image.png :alt: Test image alt text :width: 100px """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_image_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ When ``substitutions_default_enabled`` is True, ``image`` should apply path substitutions by default without requiring the ``:path-substitutions:`` flag. """ source_directory = tmp_path / "source" source_directory.mkdir() source_file = source_directory / "index.rst" (source_directory / "conf.py").touch() image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) source_file_content = dedent( text="""\ .. |a| replace:: test_image .. image:: |a|.png """, ) source_file.write_text(data=source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["sphinx_substitution_extensions"], "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "index.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ .. image:: test_image.png """, ) source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = (app_expected.outdir / "index.html").read_text() assert content_html == expected_content_html def test_default_substitutions_image_disabled_path( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """When ``substitutions_default_enabled`` is True but ``image`` has the ``:nopath-substitutions:`` flag, path substitutions should not be applied. Note: This test uses MyST format with custom delimiters because the `|` character cannot be used in Windows file paths. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() # Create an image file with the literal [[a]] in the filename image_file = source_directory / "[[a]].png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{image} [[a]].png :nopath-substitutions: ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, "myst_sub_delimiters": ("[", "]"), "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{image} [[a]].png ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, freshenv=True, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html class TestImageMyst: """ Tests for image directive with MyST documents. """ @staticmethod def test_myst_substitutions_image( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ MyST substitutions are respected in image paths in MyST documents. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() # Create an image file image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{image} |a|.png :path-substitutions: ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "test_image", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{image} test_image.png ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_myst_substitutions_image_default_delimiters( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The default MyST substitution delimiters {{}} are respected for images. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() image_file = source_directory / "test_image.png" png_data = ( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00" b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx" b"\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" ) image_file.write_bytes(data=png_data) index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{image} {{a}}.png :path-substitutions: ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "test_image", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{image} test_image.png ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_myst_substitutions_not_enabled( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ MyST substitutions are not respected in MyST documents when ``myst_enable_extensions`` does not contain ``substitutions``. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_substitutions": { "a": "example_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{code-block} $ PRE-|a|-POST ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_myst_substitutions_custom_markdown_suffix( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ Custom markdown suffixes are respected in MyST documents. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.txt" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, "source_suffix": { ".rst": "restructuredtext", ".txt": "markdown", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{code-block} $ PRE-example_substitution-POST ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": ["myst_parser"], "source_suffix": { ".rst": "restructuredtext", ".txt": "markdown", }, }, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_default_myst_sub_delimiters_code_block( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The default MyST substitution delimiters are respected. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{code-block} :substitutions: $ PRE-{{a}}-POST ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{code-block} $ PRE-example_substitution-POST ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_custom_myst_sub_delimiters_code_block( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ Custom MyST substitution delimiters are respected. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title ```{code-block} :substitutions: $ PRE-[[a]]-POST ``` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, "myst_sub_delimiters": ("[", "]"), }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title ```{code-block} $ PRE-example_substitution-POST ``` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_substitution_code_role( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``substitution-code`` role replaces the placeholders defined in ``conf.py`` as specified. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title Example {substitution-code}`PRE-|a|-POST` """, ) index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title Example {code}`PRE-example_substitution-POST` """, ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html @staticmethod def test_substitution_download( tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """ The ``substitution-download`` role replaces the placeholders defined in ``conf.py`` as specified. """ source_directory = tmp_path / "source" source_directory.mkdir() index_source_file = source_directory / "index.rst" markdown_source_file = source_directory / "markdown_document.md" (source_directory / "conf.py").touch() index_source_file_content = dedent( text="""\ .. toctree:: markdown_document """, ) markdown_source_file_content = dedent( text="""\ # Title {substitution-download}`txt_pre-|a|-txt_post ` """, ) # Importantly we have a non-space whitespace character in the target # name. downloadable_file = ( source_directory / "tgt_pre-example_substitution-tgt_post .py" ) downloadable_file.write_text(data="Sample") index_source_file.write_text(data=index_source_file_content) markdown_source_file.write_text(data=markdown_source_file_content) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "a": "example_substitution", }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() equivalent_source = dedent( text="""\ # Title {download}`txt_pre-example_substitution-txt_post ` """, # noqa: E501 ) markdown_source_file.write_text(data=equivalent_source) app_expected = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["myst_parser"]}, ) app_expected.build() assert app_expected.statuscode == 0 expected_content_html = ( app_expected.outdir / "markdown_document.html" ).read_text() assert content_html == expected_content_html