pax_global_header00006660000000000000000000000064152622126160014515gustar00rootroot0000000000000052 comment=219c7b162c4f7ff3c646e60b5a0fc12f933828a1 adamtheturtle-sphinx-substitution-extensions-219c7b1/000077500000000000000000000000001526221261600231705ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/.git_archival.txt000066400000000000000000000000331526221261600264370ustar00rootroot00000000000000ref-names: tag: 2026.10.09 adamtheturtle-sphinx-substitution-extensions-219c7b1/.gitattributes000066400000000000000000000000631526221261600260620ustar00rootroot00000000000000.git_archival.txt export-subst * text=auto eol=lf adamtheturtle-sphinx-substitution-extensions-219c7b1/.github/000077500000000000000000000000001526221261600245305ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/.github/dependabot.yml000066400000000000000000000004751526221261600273660ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: / schedule: interval: daily - package-ecosystem: pre-commit directory: / schedule: interval: daily adamtheturtle-sphinx-substitution-extensions-219c7b1/.github/workflows/000077500000000000000000000000001526221261600265655ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/.github/workflows/autofix.yml000066400000000000000000000013151526221261600307670ustar00rootroot00000000000000--- name: autofix.ci on: pull_request: branches: [main] push: branches: [main] permissions: contents: read jobs: autofix: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 fetch-tags: true persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Run fixers uses: j178/prek-action@v3.0.1 with: prek-version: 0.4.11 extra-args: >- --all-files --hook-stage pre-commit --no-fail-fast --verbose env: UV_NO_CACHE: '1' UV_PYTHON: 3.13 - uses: autofix-ci/action@v1.3.4 if: always() adamtheturtle-sphinx-substitution-extensions-219c7b1/.github/workflows/ci.yml000066400000000000000000000034471526221261600277130ustar00rootroot00000000000000--- 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 * * * permissions: {} jobs: build: strategy: matrix: python-version: ['3.11', '3.12', '3.13', '3.14'] uv-resolution: [highest, lowest-direct] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v7 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@v10.2.0 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' - name: Freeze for debugging run: | uv pip freeze - name: Build sample run: | uv run --group=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 --group=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 --group=dev pytest -s -vvv . --cov --cov-config=pyproject.toml env: UV_PYTHON: ${{ matrix.python-version }} UV_RESOLUTION: ${{ matrix.uv-resolution }} completion-ci: needs: build runs-on: ubuntu-latest if: ${{ !cancelled() }} 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-219c7b1/.github/workflows/dependabot-merge.yml000066400000000000000000000007001526221261600325070ustar00rootroot00000000000000--- name: Dependabot auto-merge on: pull_request permissions: contents: write pull-requests: write jobs: dependabot: runs-on: ubuntu-latest if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - 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-219c7b1/.github/workflows/lint.yml000066400000000000000000000030741526221261600302620ustar00rootroot00000000000000--- name: Lint 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 * * * workflow_dispatch: {} permissions: {} jobs: build: strategy: matrix: python-version: ['3.13'] platform: [ubuntu-latest, windows-latest] hook-stage: [pre-commit, pre-push, manual] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v7 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@v10.2.0 with: enable-cache: true cache-dependency-glob: '**/pyproject.toml' - name: Lint uses: j178/prek-action@v3.0.1 with: prek-version: 0.4.11 extra-args: >- --all-files --hook-stage ${{ matrix.hook-stage }} --verbose env: UV_NO_CACHE: '1' UV_PYTHON: ${{ matrix.python-version }} # UV_RESOLUTION is intentionally not set here. prek uses uv to # install hooks, and uv inherits UV_RESOLUTION. With lowest-direct, # this causes hook dependencies to resolve to ancient versions that # don't build on modern Python. completion-lint: needs: build runs-on: ubuntu-latest if: ${{ !cancelled() }} 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-219c7b1/.github/workflows/release.yml000066400000000000000000000063151526221261600307350ustar00rootroot00000000000000--- 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@v7 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@v10.2.0 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: Assemble native Markdown release notes env: RELEASE: ${{ steps.calver.outputs.release }} run: | uv run --group=release towncrier build --yes --version "$RELEASE" cp "changelog/$RELEASE.md" release-notes.md - uses: stefanzweifel/git-auto-commit-action@v7 id: commit with: commit_message: Bump CHANGELOG file_pattern: CHANGELOG.md changelog newsfragments # Error if there are no changes. skip_dirty_check: true - name: Bump version and push tag id: tag_version uses: mathieudutour/github-tag-action@v7 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 }} bodyFile: release-notes.md - name: Build a binary wheel and a source tarball env: NEW_TAG: ${{ steps.tag_version.outputs.new_tag }} run: | git fetch --tags git checkout "$NEW_TAG" uv build --sdist --wheel --out-dir dist/ uv run --group=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-219c7b1/.gitignore000066400000000000000000000026041526221261600251620ustar00rootroot00000000000000# 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 # Vale styles downloaded by ``vale sync`` styles/ adamtheturtle-sphinx-substitution-extensions-219c7b1/.prettierrc000066400000000000000000000002331526221261600253520ustar00rootroot00000000000000{ "overrides": [ { "files": ["*.yaml", "*.yml"], "options": { "singleQuote": true, "printWidth": 100 } } ] } adamtheturtle-sphinx-substitution-extensions-219c7b1/.vale.ini000066400000000000000000000010671526221261600247020ustar00rootroot00000000000000StylesPath = styles MinAlertLevel = error Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.29.0/ai-tells.zip [*.{rst,md}] BasedOnStyles = ai-tells, snapper snapper.SemanticLineBreaks = error # These rules misclassify established technical, example, or release-note prose # in this repository. All other ai-tells rules remain enforced. ai-tells.ColonUsage = NO ai-tells.ContrastiveFormulas = NO ai-tells.FillerPhrases = NO ai-tells.FormalTransitions = NO ai-tells.Metacommentary = NO ai-tells.OverusedVocabulary = NO ai-tells.VerbTricolon = NO adamtheturtle-sphinx-substitution-extensions-219c7b1/.vscode/000077500000000000000000000000001526221261600245315ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/.vscode/extensions.json000066400000000000000000000001341526221261600276210ustar00rootroot00000000000000{ "recommendations": [ "charliermarsh.ruff", "ms-python.python" ] } adamtheturtle-sphinx-substitution-extensions-219c7b1/.vscode/settings.json000066400000000000000000000005371526221261600272710ustar00rootroot00000000000000{ "[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-219c7b1/CHANGELOG.md000066400000000000000000000001261526221261600250000ustar00rootroot00000000000000# Changelog Release notes are stored as [one Markdown file per version](changelog/). adamtheturtle-sphinx-substitution-extensions-219c7b1/CONTRIBUTING.rst000066400000000000000000000047621526221261600256420ustar00rootroot00000000000000Contributing ============ 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 . --group 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 prek install Linting ------- Run lint tools either by committing, or with: .. code-block:: shell prek run --all-files --hook-stage pre-commit --verbose prek run --all-files --hook-stage pre-push --verbose prek run --all-files --hook-stage manual --verbose The development dependencies include `doc8-custom-ignores`_. The plugin is discovered automatically by doc8 and reads additional ignores from ``pyproject.toml``: .. code-block:: toml [tool.doc8] ignore-messages = [ """Error in "include" directive: unknown option: "content-substitutions".""", """Error in "include" directive: unknown option: "path-substitutions".""", ] This ignores diagnostics for ``include``'s ``:content-substitutions:`` and ``:path-substitutions:`` options. Plain docutils does not know about those options, but this extension adds them when Sphinx builds the documentation. Keep these as exact-message ignores so other ``D000`` diagnostics remain visible. .. _Homebrew: https://brew.sh .. _doc8-custom-ignores: https://adamtheturtle.github.io/doc8-custom-ignores/ 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 Release notes ------------- Write user-facing changes as Markdown in ``newsfragments/.change.md``. Towncrier writes one Markdown file per version, used directly for GitHub release notes. Invalid fragment names fail release assembly. adamtheturtle-sphinx-substitution-extensions-219c7b1/LICENSE000066400000000000000000000020551526221261600241770ustar00rootroot00000000000000MIT 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-219c7b1/README.rst000066400000000000000000000257511526221261600246710ustar00rootroot00000000000000|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>` External hyperlinks ~~~~~~~~~~~~~~~~~~~ Enable substitutions in external hyperlink targets in ``conf.py``: .. code-block:: python """Configuration for Sphinx.""" substitutions_hyperlink_targets_enabled = True Then substitutions are applied to hyperlink targets: .. code-block:: rst Download version |release| from the tarball_. .. _tarball: https://example.com/releases/v|release|.tar.gz The setting enables hyperlink-target substitutions throughout the project, but only targets containing a defined substitution are changed. To limit a substitution to one page, define it in that page instead of in ``rst_prolog``: .. code-block:: rst .. |tarball-release| replace:: 0.8.5 Download the tarball_. .. _tarball: https://example.com/releases/v|tarball-release|.tar.gz To limit the substitution to one link on that page, use a unique substitution name, such as ``tarball-release`` above, only in that link's target. Other hyperlink targets are left unchanged. ``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: ``include`` ~~~~~~~~~~~ This adds ``:content-substitutions:`` and ``:path-substitutions:`` options to docutils' built-in `include`_ directive. Replace substitutions in the included source content before it is parsed: .. code-block:: rst .. include:: path/to/file.rst :content-substitutions: Replace substitutions in the file path: .. code-block:: rst .. include:: 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``. Substitutions can also be defined or overridden for an individual Markdown document in its frontmatter: .. code-block:: markdown --- myst: substitutions: release: "0.2" author: name: Talya --- ```{code-block} shell :substitutions: echo "|author.name| released version |release|" ``` Enabling substitutions by default ---------------------------------- By default, you need to explicitly add the ``:substitutions:`` flag to ``code-block`` directives, ``:content-substitutions:`` or ``:path-substitutions:`` flags to ``literalinclude`` and ``include`` directives, and ``:path-substitutions:`` to ``image`` 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 - All ``include`` directives will have both content and path substitutions applied automatically - All ``image`` directives will have 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: .. include:: path/to/|literal|_file.txt :nocontent-substitutions: :nopath-substitutions: .. image:: path/to/|literal|_diagram.png :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: ``` ``include`` ~~~~~~~~~~~ This adds a ``:path-substitutions:`` option to docutils' built-in `include`_ directive. Replace substitutions in the file path: .. code-block:: markdown ```{include} 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 ``` Nested substitutions ~~~~~~~~~~~~~~~~~~~~~ ``myst_substitutions`` supports nested dictionaries and lists, which are flattened using dot notation. **Important:** Substitution keys cannot contain dots (``.``), as dots are reserved for nested access notation. For example, ``{"key.with.dots": "value"}`` raises an exception. Nested dictionaries: .. code-block:: python """Configuration for Sphinx.""" myst_substitutions = { "app": { "name": "MyApp", "version": "1.0.0", }, } Usage in Markdown: .. code-block:: markdown ```{code-block} bash :substitutions: echo "Application: |app.name| version |app.version|" ``` Lists with index access: .. code-block:: python """Configuration for Sphinx.""" myst_substitutions = { "platforms": ["Linux", "Windows", "macOS"], } Usage: .. code-block:: markdown ```{code-block} bash :substitutions: echo "First platform: |platforms.0|" echo "Second platform: |platforms.1|" ``` Nested lists of dictionaries: .. code-block:: python """Configuration for Sphinx.""" myst_substitutions = { "releases": [ {"version": "1.0", "codename": "Alpha"}, {"version": "2.0", "codename": "Beta"}, ], } Usage: .. code-block:: markdown ```{code-block} bash :substitutions: echo "First release: |releases.0.version| (|releases.0.codename|)" echo "Second release: |releases.1.version| (|releases.1.codename|)" ``` Complex nested structures: .. code-block:: python """Configuration for Sphinx.""" myst_substitutions = { "project": { "name": "MyProject", "contributors": [ {"name": "Alice", "role": "dev"}, {"name": "Bob", "role": "docs"}, ], }, } Usage: .. code-block:: markdown ```{code-block} bash :substitutions: echo "Project: |project.name|" echo "Developer: |project.contributors.0.name|" echo "Documentation: |project.contributors.1.name|" ``` 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 .. _include: https://docutils.sourceforge.io/docs/ref/rst/directives.html#including-an-external-document-fragment .. _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.11 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/000077500000000000000000000000001526221261600251175ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2018.11.12.0.md000066400000000000000000000000761526221261600266350ustar00rootroot00000000000000# 2018.11.12.0 - Initial release with `substitution-prompt`. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2018.11.12.2.md000066400000000000000000000000731526221261600266340ustar00rootroot00000000000000# 2018.11.12.2 - Add `substitution-code-block` directive. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2018.11.12.3.md000066400000000000000000000000731526221261600266350ustar00rootroot00000000000000# 2018.11.12.3 - Make `substitution` a list, not a tuple. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2019.04.04.0.md000066400000000000000000000000501526221261600266310ustar00rootroot00000000000000# 2019.04.04.0 - Support Sphinx 2.0.0. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2019.04.04.1.md000066400000000000000000000000171526221261600266350ustar00rootroot00000000000000# 2019.04.04.1 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2019.06.15.0.md000066400000000000000000000000171526221261600266400ustar00rootroot00000000000000# 2019.06.15.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2019.12.28.0.md000066400000000000000000000000171526221261600266410ustar00rootroot00000000000000# 2019.12.28.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2019.12.28.1.md000066400000000000000000000000171526221261600266420ustar00rootroot00000000000000# 2019.12.28.1 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.02.21.0.md000066400000000000000000000000171526221261600266210ustar00rootroot00000000000000# 2020.02.21.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.04.05.0.md000066400000000000000000000000171526221261600266250ustar00rootroot00000000000000# 2020.04.05.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.05.23.0.md000066400000000000000000000003651526221261600266340ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.05.27.0.md000066400000000000000000000001701526221261600266320ustar00rootroot00000000000000# 2020.05.27.0 - Breaking change: Use `:substitutions:` option on `code-block` or `prompt` rather than new directives. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.05.30.0.md000066400000000000000000000000171526221261600266240ustar00rootroot00000000000000# 2020.05.30.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.07.04.0.md000066400000000000000000000001661526221261600266340ustar00rootroot00000000000000# 2020.07.04.0 - Ensure non-lower-case replacements can also be substituted. Thanks go to @Julian for this change. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.07.04.1.md000066400000000000000000000001631526221261600266320ustar00rootroot00000000000000# 2020.07.04.1 - Ensure non-lower-case replacements can also be substituted in the inline substitution code role. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2020.09.30.0.md000066400000000000000000000000171526221261600266300ustar00rootroot00000000000000# 2020.09.30.0 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2022.02.16.md000066400000000000000000000002531526221261600264730ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2024.02.24.1.md000066400000000000000000000001451526221261600266330ustar00rootroot00000000000000# 2024.02.24.1 - Add support for MyST. Thanks to Václav Votípka (@eNcacz) for the contribution. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2024.02.24.md000066400000000000000000000006431526221261600264770ustar00rootroot00000000000000# 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 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2024.02.25.md000066400000000000000000000000621526221261600264730ustar00rootroot00000000000000# 2024.02.25 - Add `substitution-download` role. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2024.08.06.md000066400000000000000000000003611526221261600265020ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2024.10.17.md000066400000000000000000000002541526221261600264760ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.01.02.md000066400000000000000000000002251526221261600264670ustar00rootroot00000000000000# 2025.01.02 - Supports situations where there is no source file name available to the extension, such as when using `sphinx_toolbox.rest_example`. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.02.19.md000066400000000000000000000002461526221261600265030ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.03.03.md000066400000000000000000000000551526221261600264730ustar00rootroot00000000000000# 2025.03.03 - Add support for Python 3.10. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.04.03.md000066400000000000000000000000151526221261600264700ustar00rootroot00000000000000# 2025.04.03 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.06.06.md000066400000000000000000000000151526221261600264750ustar00rootroot00000000000000# 2025.06.06 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.10.24.md000066400000000000000000000000151526221261600264700ustar00rootroot00000000000000# 2025.10.24 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.11.17.md000066400000000000000000000024701526221261600265020ustar00rootroot00000000000000# 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. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2025.12.15.md000066400000000000000000000000151526221261600264720ustar00rootroot00000000000000# 2025.12.15 adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.01.12.md000066400000000000000000000002211526221261600264650ustar00rootroot00000000000000# 2026.01.12 - Fix bug where `SubstitutionXRefRole` removed all occurrences of "substitution-" from CSS class names instead of only the prefix. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.06.17.md000066400000000000000000000002401526221261600265000ustar00rootroot00000000000000# 2026.06.17 - Drop Python 3.10 support (requires Python \>=3.11). - Pin Sphinx to \<9 in dev dependencies for sphinx_toolbox compatibility in sample project. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.08.02.md000066400000000000000000000003231526221261600264760ustar00rootroot00000000000000# 2026.08.02 - Add content substitutions to the `include` directive with `:content-substitutions:` and `:nocontent-substitutions:` options. - Add an option to apply substitutions to external hyperlink targets. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.08.05.md000066400000000000000000000000461526221261600265030ustar00rootroot00000000000000# 2026.08.05 No significant changes. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.08.13.1.md000066400000000000000000000000501526221261600266340ustar00rootroot00000000000000# 2026.08.13.1 No significant changes. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.08.13.md000066400000000000000000000000461526221261600265020ustar00rootroot00000000000000# 2026.08.13 No significant changes. adamtheturtle-sphinx-substitution-extensions-219c7b1/changelog/2026.10.09.md000066400000000000000000000034131526221261600265010ustar00rootroot00000000000000# 2026.10.09 (2026-10-09) ## Changes - Support MyST substitutions defined in document frontmatter. ([#1554](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1554)) - Preserve Sphinx's tracking of files consumed by the `include` directive. ([#1632](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1632)) - Restore Sphinx's source-directory-relative handling of slash-prefixed `include` paths. ([#1633](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1633)) - Restore Sphinx's `include-read` event for the `include` directive and compose listener changes with content substitutions, including for parsed and literal include content, while preserving nested includes' substitution settings. ([#1634](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1634)) - Avoid shared mutable defaults in the substitution code role. ([#1717](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1717)) - Validate equivalence-case TOML values without untyped pass-through helpers. ([#1718](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1718)) - Type MyST parsing contexts as docutils and MyST context classes instead of arbitrary objects. ([#1722](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1722)) - Type substitution option collections by the names they contain. ([#1723](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/1723)) - Publish native Markdown release notes and use the same notes in documentation where available. - Use a backported recursive type alias to enable runtime type checking of nested substitution flattening while retaining Python 3.11 support. Require Beartype 0.23.0rc2 and typing-extensions 4.6.0 or newer. adamtheturtle-sphinx-substitution-extensions-219c7b1/newsfragments/000077500000000000000000000000001526221261600260535ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/newsfragments/.gitkeep000066400000000000000000000000001526221261600274720ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/newsfragments/README.md000066400000000000000000000006731526221261600273400ustar00rootroot00000000000000# Release notes Add user-facing changes in `newsfragments/..md`, using an issue or pull request number. The available types are `change`. Use `+name..md` for changes without an issue number. Write GitHub-compatible Markdown, including inline code, explicit links, and lists. Towncrier writes one Markdown file per version and rejects invalid fragment names. GitHub publishes that file directly as the release description. adamtheturtle-sphinx-substitution-extensions-219c7b1/prek.toml000066400000000000000000000275241526221261600250400ustar00rootroot00000000000000# Configuration file for `prek`, a git hook framework written in Rust. # See https://prek.j178.dev for more information. #:schema https://www.schemastore.org/prek.json fail_fast = true default_install_hook_types = [ "pre-commit", "pre-push" ] [[repos]] repo = "meta" hooks = [ { id = "check-useless-excludes", stages = ["pre-commit"] } ] [[repos]] 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"] } ] [[repos]] 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"] } ] [[repos]] repo = "local" hooks = [ { id = "actionlint", name = "actionlint", entry = "uv run --group=dev actionlint", language = "python", pass_filenames = false, types_or = ["yaml"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "pydocstringformatter", name = "pydocstringformatter", entry = "uv run --group=dev pydocstringformatter", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "shellcheck", name = "shellcheck", entry = "uv run --group=dev shellcheck --shell=bash", language = "python", types_or = ["shell"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "shellcheck-docs", name = "shellcheck-docs", entry = 'uv run --group=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.11.7"], stages = ["pre-commit"] }, { id = "shfmt", name = "shfmt", entry = "uv run --group=dev shfmt --write --space-redirects --indent=4", language = "python", types_or = ["shell"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "shfmt-docs", name = "shfmt-docs", entry = 'uv run --group=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.11.7"], stages = ["pre-commit"] }, { id = "mypy", name = "mypy", stages = ["pre-push"], entry = "uv run --group=dev -m mypy", language = "python", types_or = [ "python", "toml" ], pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "mypy-docs", name = "mypy-docs", stages = ["pre-push"], entry = 'uv run --group=dev doccmd --no-write-to-file --language=python --command="mypy"', language = "python", types_or = [ "markdown", "rst" ] }, { id = "check-manifest", name = "check-manifest", stages = ["pre-push"], entry = "uv run --group=dev -m check_manifest", language = "python", pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "pyright", name = "pyright", stages = ["pre-push"], entry = "uv run --group=dev -m pyright .", language = "python", types_or = [ "python", "toml" ], pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "pyright-docs", name = "pyright-docs", stages = ["pre-push"], entry = 'uv run --group=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"], entry = "uv run --group=dev -m pyright --ignoreexternal --verifytypes sphinx_substitution_extensions", language = "python", pass_filenames = false, types_or = ["python"], additional_dependencies = ["uv==0.11.7"] }, { id = "ty", name = "ty", stages = ["pre-push"], entry = "uv run --group=dev ty check", language = "python", types_or = [ "python", "toml" ], pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "ty-docs", name = "ty-docs", stages = ["pre-push"], entry = 'uv run --group=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="ty check"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"] }, { id = "vulture", name = "vulture", entry = "uv run --group=dev -m vulture .", language = "python", types_or = ["python"], pass_filenames = false, additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "vulture-docs", name = "vulture docs", entry = 'uv run --group=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="vulture"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "pyroma", name = "pyroma", entry = "uv run --group=dev -m pyroma --min 10 .", language = "python", pass_filenames = false, types_or = ["toml"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "deptry", name = "deptry", entry = "uv run --group=dev -m deptry src/", language = "python", pass_filenames = false, additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "pylint", name = "pylint", entry = "uv run --group=dev -m pylint src/ tests/", language = "python", stages = ["manual"], pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "ruff-check-fix", name = "Ruff check fix", entry = "uv run --group=dev -m ruff check --fix", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "ruff-check-fix-docs", name = "Ruff check fix docs", entry = 'uv run --group=dev doccmd --language=python --command="ruff check --fix"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "ruff-format-fix", name = "Ruff format", entry = "uv run --group=dev -m ruff format", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "ruff-format-fix-docs", name = "Ruff format docs", entry = 'uv run --group=dev doccmd --language=python --no-pad-file --command="ruff format"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "strict-kwargs-fix", name = "strict-kwargs", entry = "uv run --group=dev strict-kwargs check --diff", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"], require_serial = true }, { id = "no-defaults", name = "no-defaults", entry = "uv run --group=dev no-defaults", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"], require_serial = true }, { id = "vale-sync", name = "vale sync", # The Python Vale wrapper may fail transiently while downloading its binary; see # https://github.com/daniperez/vale-python-package/issues/77 entry = "uv run --group=dev vale sync", language = "python", pass_filenames = false, files = '(\.(rst|md)$|^\.vale\.ini$)', additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "snapper", name = "snapper", entry = "uv run --group=dev snapper --native --max-width 0 --in-place", language = "python", types_or = [ "rst", "markdown" ], require_serial = true, additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "vale", name = "vale", entry = "uv run --group=dev vale", language = "python", types_or = [ "rst", "markdown" ], require_serial = true, additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "doc8", name = "doc8", entry = "uv run --group=dev -m doc8", language = "python", types_or = ["rst"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "interrogate", name = "interrogate", entry = "uv run --group=dev -m interrogate", language = "python", types_or = ["python"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "interrogate-docs", name = "interrogate docs", entry = 'uv run --group=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="interrogate"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "pyproject-fmt-fix", name = "pyproject-fmt", entry = "uv run --group=dev pyproject-fmt", language = "python", types_or = ["toml"], files = "pyproject.toml", stages = ["pre-commit"] }, { id = "yamlfix", name = "yamlfix", entry = "uv run --group=dev yamlfix", language = "python", types_or = ["yaml"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "zizmor", name = "zizmor", entry = "uv run --group=dev zizmor --strict-collection .github", language = "python", pass_filenames = false, types_or = ["yaml"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "sphinx-lint", name = "sphinx-lint", entry = "uv run --group=dev sphinx-lint --enable=all --disable=line-too-long", language = "python", types_or = ["rst"], additional_dependencies = ["uv==0.11.7"], stages = ["pre-commit"] }, { id = "pyrefly", name = "pyrefly", stages = ["pre-push"], entry = "uv run --group=dev pyrefly check", language = "python", types_or = [ "python", "toml" ], pass_filenames = false, additional_dependencies = ["uv==0.11.7"] }, { id = "pyrefly-docs", name = "pyrefly-docs", stages = ["pre-push"], entry = 'uv run --group=dev doccmd --no-write-to-file --example-workers 0 --language=python --command="pyrefly check"', language = "python", types_or = [ "markdown", "rst" ], additional_dependencies = ["uv==0.11.7"] } ] adamtheturtle-sphinx-substitution-extensions-219c7b1/pyproject.toml000066400000000000000000000315171526221261600261130ustar00rootroot00000000000000[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.11" 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.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] dynamic = [ "version", ] dependencies = [ "beartype>=0.23.0rc2", "docutils>=0.19", "myst-parser>=4.0.0", "sphinx>=8.1.0", "typing-extensions>=4.6.0", ] urls.Source = "https://github.com/adamtheturtle/sphinx-substitution-extensions" [dependency-groups] dev = [ "actionlint-py==1.7.12.25", "beartype==0.23.0rc2", "check-manifest==0.51", "deptry==0.25.1", "doc8==2.0.0", "doc8-custom-ignores==2026.7.29.1", "doccmd==2026.9.16.1", "interrogate==1.7.0", "mypy==2.4.0", "mypy-pytest-parametrize==2026.10.5.1", "mypy-strict-kwargs==2026.8.25.1", "no-defaults==2026.9.1", "prek==0.5.5", "pydocstringformatter==1.0.0", "pylint[spelling]==4.1.2", "pyproject-fmt==2.30.1", "pyrefly==1.3.2", "pyright==1.1.414", "pyroma==5.0.1", "pytest==9.1.1", "pytest-beartype==0.3.0", "pytest-cov==7.1.0", "ruff==0.16.10", # 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==4.2.0", "snapper-fmt==0.11.9", "sphinx>=9,<10", "sphinx-lint==1.0.2", "sphinx-toolbox==4.3.1", "strict-kwargs==2026.8.28.post2", "towncrier==26.9.0", "ty==0.0.84", "types-docutils==0.23.0.20260923", "vale==3.22.0.0", "vulture==2.16", "yamlfix==1.19.1", "zizmor==1.30.1", ] release = [ "check-wheel-contents==0.6.3", "towncrier==26.9.0" ] [tool.setuptools] packages.find.where = [ "src", ] package-data.sphinx_substitution_extensions = [ "py.typed", ] zip-safe = false [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.extend-select = [ "ANN", "PGH003", "PYI" ] lint.ignore = [ # Ruff warns that this conflicts with the formatter. "COM812", # This project does not use per-file copyright notices. "CPY001", # Allow our chosen docstring line-style - pydocstringformatter handles formatting # but doesn't enforce D205 (blank line after summary) or D212 (summary on first line). "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", ] lint.explicit-preview-rules = true lint.per-file-ignores."doccmd_*.py" = [ # Allow our chosen docstring line-style - pydocstringformatter handles # formatting but docstrings in docs may not match this style. "D200", ] # 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.external = [ "NOD" ] lint.flake8-tidy-imports.banned-api."operator.attrgetter".msg = "operator.attrgetter is banned: use a lambda instead." lint.flake8-tidy-imports.banned-api."typing.cast".msg = """\ typing.cast is banned: use explicit type narrowing or a typed variable instead.\ """ lint.pydocstyle.convention = "google" lint.preview = true [tool.pylint] # 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" "MESSAGES CONTROL".disable = [ # Too difficult to please "duplicate-code", # Let flake8 handle long lines "line-too-long", "locally-disabled", "missing-return-type-doc", # We don't need everything to be documented because of mypy "missing-type-doc", # Let ruff find protected member access. "protected-access", "too-few-public-methods", "too-many-arguments", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-positional-arguments", "too-many-return-statements", # Let ruff deal with sorting "ungrouped-imports", # Let ruff handle unused imports "unused-import", # Let ruff handle imports "wrong-import-order", ] # 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. "MESSAGES CONTROL".enable = [ "bad-inline-option", "deprecated-pragma", "file-ignored", "spelling", "use-symbolic-message-instead", "useless-suppression", ] DEPRECATED_BUILTINS.bad-functions = [ # Use Pylint until Ruff can ban bare builtin calls, or until custom rules # make this removable: # https://github.com/astral-sh/ruff/issues/10079 # https://github.com/astral-sh/ruff/issues/970 "filter", "getattr", "hasattr", "map", "setattr", ] # Allow the body of an if to be on the same line as the test if there is no # else. FORMAT.single-line-if-stmt = false # Return non-zero exit code if useless-suppression is emitted. MAIN.fail-on = [ "useless-suppression", ] # Use multiple processes to speed up Pylint. MASTER.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. MASTER.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", ] # Pickle collected data for later comparisons. MASTER.persistent = true # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. MASTER.unsafe-load-any-extension = false # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. SPELLING.spelling-dict = "en_US" # A path to a file that contains private dictionary; one word per line. SPELLING.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.spelling-store-unknown-words = "no" [tool.interrogate] fail-under = 100 verbose = 2 omit-covered-files = true [tool.check-manifest] ignore = [ "*.enc", ".checkmake-config.ini", ".git_archival.txt", ".prettierrc", ".vale.ini", ".vscode/*.json", ".yamlfmt", "changelog", "CHANGELOG.md", "changelog/**", "ci", "ci/**", "CODE_OF_CONDUCT.rst", "CONTRIBUTING.rst", "docs", "docs/**", "LICENSE", "lint.mk", "Makefile", "newsfragments", "newsfragments/**", "prek.toml", "readthedocs.yaml", "sample", "sample/**", "spelling_private_dict.txt", "styles/snapper/*.yml", "tests", "tests-pylintrc", "tests/**", "zizmor.yml", ] [tool.vulture] # Duplicate some of .gitignore exclude = [ ".venv" ] # Ideally we would limit the paths to the source code where we want to ignore names, # but Vulture does not enable this. ignore_names = [ # Sphinx "autoclass_content", "autoclass_content", "autodoc_member_order", "copybutton_exclude", "extensions", # pytest fixtures - we name fixtures like this for this purpose "fixture_*", "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", # pytest configuration "pytest_collect_file", "pytest_plugins", "rst_prolog", "setup", "source_suffix", "spelling_word_list_filename", "substitutions_default_enabled", "substitutions_hyperlink_targets_enabled", "templates_path", "warning_is_error", ] [tool.pyproject-fmt] indent = 4 keep_full_version = true max_supported_python = "3.14" [tool.mypy] files = [ "." ] exclude = [ "build" ] exclude_gitignore = true follow_untyped_imports = true strict = true plugins = [ "mypy_pytest_parametrize", "mypy_strict_kwargs", ] num_workers = "auto" [tool.pyrefly] errors.non-exhaustive-match = "error" enabled-ignores = [ "pyrefly" ] preset = "all" [tool.pyright] typeCheckingMode = "strict" enableTypeIgnoreComments = false reportUnnecessaryTypeIgnoreComment = true [tool.ty] rules.all = "error" terminal.error-on-warning = true analysis.respect-type-ignore-comments = false analysis.strict-equality-semantics = true analysis.strict-generic-narrowing = true [tool.pytest] # Source functions already use @beartype. Enable beartype_packages for # sphinx_substitution_extensions once package import checking preserves # branch coverage: https://github.com/beartype/beartype/issues/706 beartype_test_fixtures = true beartype_tests = true log_cli = true xfail_strict = true [tool.coverage] run.source = [ "src/", "tests" ] run.branch = true report.fail_under = 100 report.exclude_also = [ "if TYPE_CHECKING:", ] report.show_missing = true [tool.towncrier] # Write the same native Markdown file used by documentation and GitHub releases. directory = "newsfragments" filename = "changelog/{version}.md" title_format = "# {version} ({project_date})" issue_format = "[#{issue}](https://github.com/adamtheturtle/sphinx-substitution-extensions/issues/{issue})" single_file = false # Reject invalid fragment names instead of silently losing release entries. ignore = [ ".gitkeep" ] type = [ { directory = "change", name = "Changes", showcontent = true } ] [tool.mypy_strict_kwargs] # The runtime parameter is named ``role``, while types-docutils names it # ``role_name``. A positional argument is the only spelling compatible with # both signatures. ignore_names = [ "docutils.parsers.rst.roles.code_role" ] [tool.strict_kwargs] ignore_names = [ "docutils.parsers.rst.roles.code_role" ] [tool.pydocstringformatter] write = true split-summary-body = false max-line-length = 75 linewrap-full-docstring = true [tool.doc8] max_line_length = 2000 ignore-messages = [ '''Error in "include" directive: unknown option: "content-substitutions".''', '''Error in "include" directive: unknown option: "path-substitutions".''', ] ignore_path = [ "./.eggs", "./docs/build", "./docs/build/spelling/output.txt", "./node_modules", "./sample/build", "./src/*.egg-info/", "./src/*/_setuptools_scm_version.txt", ] [tool.yamlfix] section_whitelines = 1 whitelines = 1 [tool.no_defaults] private_only = true per_file_enforcement."tests/**" = "all" adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/000077500000000000000000000000001526221261600244515ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/000077500000000000000000000000001526221261600257515ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/Eleanor.txt000066400000000000000000000000461526221261600300770ustar00rootroot00000000000000This is a downloadable text document. adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/Eleanor_diagram.png000066400000000000000000000102161526221261600315300ustar00rootroot00000000000000‰PNG  IHDR�,bÕr•UIDATxœíÝ{xSåÀñ÷$iÚR¤nŽíATP¹ ¡ˆP)"ÃY´Tª°y)ÒQZŸú ¢>*¬ Üª °¢S[ ¨¨Âä& <ʘ”)åoˆâ…zKšd#KzIišœß9ßÏ_iH““÷¼ç›7iéÑ<� ,‘ÞÁ Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †Mé’¦i‘ÞÀì<�ÒVXÄÐé Ë땒ÑÞÀŒþÜå|¥K¬°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆ¡ë?‘ŒPÉHìxÑ%½¼—û\5xèèÛ3ût)ÞUÒì�¨i5NGÊM·vó—ö¼ë�k2F5߃Âð–)Ø¢ìÓ‹_‰È#VWVÌ“Û"ùÚ¡í.ìÎm€ñ,S;QVúìŒúþ»§sÔøÉõèõå�ýOO_^Vö§¿d ÍÊUJeöé’:"kßû;ËËJo¾ë�¤”Ôcß÷äÄû*+Êc[ÄÝ9ëñsóÛÌ>]’®N½ [÷´Ì¿‡ˆŽm‘9î¡Â‡'%_;Ô»¬ó{ˆc?}ê¡ÊËJ[·m·û­7–¼ó‘÷A½wØ£ß�ÛsyJêG»Þ¹>gLÉ{;Kv¿;dd¶÷Ÿ`x|†ejË�>ddöÔ¢—î�óÄÓ“Ç)¥6,/yßęϭ^óìSÞÛÔ8çüê×3ŠWýí‰ÂÂY“”RKò§]‘vÃÌ嫯H»aé#Ó•RNGõi×ÖÊ+¡s×#Ÿò}é÷K™ÞÈõ3Ÿ{µßà´ªŠrïm|w¸=ÎêªÁ7gN_¶rÑ´ C2s¦/[éû'+,S¨q:&gþÙ{ùÖû'vNìí½¼gû6_Jª**Ü.W渇¶¯[óî¶Í'Ž{¯÷¸ÝW ¿E)õ»vÊ�Ÿ¼ò£�;îœ5O)•œš¾ü±‡O¾îY¬=“¯¬ëÑ]5.kÔ/3Íï!>ڹ㎙�)¥zºÚb±zoã»ÃÀíÑ,–NÝ{Z¬V[TÔ…Ý{jKuUeó t‡`™ú3,W�kÒâ磢£=n÷¾÷vZ¬Ö¹÷ä^>8mÈÈœM/,ó}o\«Vgž‘;ð|ÀV›M³Ô¹Zÿdïîwõ}é÷5N§÷z�ÛãQ¿;¬u{,Ö“]‹ŠŽ©çAaHìoSëriŸÿlÙ ”zÿ­7V=³@)õé?èŸ:Ôé¨v:ª½· ŒB÷¤äozM)õïM¯ý¡orýq¢¬´xîÃ7Üv‡ï¿‡èÜë²][7*¥þ³yCà¹Ñ·fÆ Ëto /îÕ{ä}Ût¸`Õ3 FO˜öôäq›V,³ZmcfÌQJ];"kBFzBçnq­â�G”Ýxo?ùÞ÷ú‹Å1±-îœõx}�¨i®ç°Ûî<³k~1:ojAÞ=ëŸ+êœØ;&¶…ßý³=0-ð5M¼o=8U½,È»7}ôí:w=°wÏ’G¦Í\¾:Ò[å=U½ãÀ 6$3{ÑŒ‰ö蘧3wÊìHot�1+,>t Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †®ÿâ(€òè/¬°ˆ¡Ó$ò­‹=Ï™—#ºQ0‚…&Ï¡ºÛD¶Z M˜=A/£(B‚`á¬æÍY½ã#[h"‚…FΘ&8E¶pÖ‚›(¡þ�lá,,44Ešùç}” Á#X¨{r„ñWÈ‚A°PÛ´ˆÐoQ‘-Ô�`áŒÙ �ßöÔÉf@‡ôÛ}n"ˆ`™�þ£ ÿ-DØ,ó’Y[‹fB°LGô‘/zãÑtËDŒt´é¹ xËŒzxõy¡.ËàÌpH›á9‹`“ �a>e"XFÃqËÁ2TFÃð–�*FÆ$–`tŠ�2‚%©bÜ̉` CªC3#Xb�*ÆKïèà ‚¥_¤ŠÑ†‚¥G¤Š‘Gí‡ÿ�AWH•°t‹`…à?¯Õ%øŽjÊN áÜÀ™V“Ô3/ƒœ‘¤JÿÎn5}n �­–ë¤J o_´SÈMd±ÂjòÖöBZÿK(©’«Qûî,æêÇ +|è”°ÚŠ,VX¡Äÿ! | %UFÕàžmpn QXa5/Rel,¸ÂŒVs½Ò’*³©k�3BˆVˆ1;M‹ÕV°Â ÝPrÔ14�-4t:Ž þ¢0BEŸ Ì 3Ï K¤7 ñÖüçGzŒìÞ_ß«Ä:6uj¤7ÁÈÎÕëð²Â Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á J^B^ÓÇzËü-M¿èÁs»w\¸0eñâ� ¾°gO=·œ·}{£î¹±·7‚>˶8P¼{÷ÚQ£6ßvÛÚQ£–¾ÿþ¶ƒëº1Á -‚Õ8y yëf®[pÝ‚G<úákú®\;ymÁ�‚‚´‚>ÿÁoEæ½¼!ƒ£ÜñÔð§B»ÿ~;vLOI‰�‰QJÅÇÄLKI™jYÔ>?ßwïåÙo¾Yîp +.ö^3éõׯ-,L-*úü§Ÿ¼=jE°ÇåpÅ�7öµ±9Å9«&¬:}eµ«]¯vw¯¿;yTò«½Zë7¦æ¥ÚãìcV�iäBwö=Ú£Mß—=Û´)9z´Ö[N4(În_�™©”rÔÔô:ÿü�ÙÙY½{OÜ´©ÁÛ£V«q<OÒˆ$¥Ôy çU•U�¾VS=Òz(¥¯O<´ó�ÿ·¸uw6J„Ò©;û]ç<©¦]×µ«Rê†nÝv}õU÷GmVãX£¬±ñ±§¿øy–jM³žþÂm;3R•¥•.§«‘]ëÒºõß|ãûrÏ7ßtmÝúÌè”VU9]þ;ýÔ9=IìVkƒ·G­Vãhÿ×Ò“3ÏåÞ·yßɹ»fÏE.RJŶŠ=RrD)õÞËïi?‡Íãö°Ú2€»û÷Ÿ²ysYÕÉõuiUÕÔ-[îéß_)Õ*:ºä»ï”R/}ø¡o–œÚç'Ãär»7ò‰RêÕ�?pÁ ÞòÎü,…-ÚöÁ??غ`kl|lFA†Rjxþð%£—´lݲå¬Ñ'_N•R�úuZ4bQîŠÜHo/šäªN�—•]·ti´Õêp¹nOJر£RêÑÔÔ¬—_þM\\ï¶mí¶ÓGVrûö/¼ðâˆÑ6Ûš}ûþ¾cG|LÌéé Þž�T+ͣ˜{?�rªú¼„¼üC¿üÄGÖ©êõ9œâNUß>?ÿ‹¼ü6_8OU¯Ã¹Á[Bb¬�¸¼B˜ Z^éÁ Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †®ÿ¦;Â@Ÿ Ì 3Ï VXÄÐé K"ß+?C æC3!X¡P²enL€fE°šmd)—ɰÇÀ`5óø’-`/‡ Á Ë(“-ƒbφÁ ãX“-£`WF ÁŠÄ S.±Øw‘E°"7ôdKö—¬Hï²¥{ì#ý XºÀ!¡Cì"XúÂA¢ìÝ"XzÄÃÈ£VK¿È£ ?KïÈà ‚%åbÔEo<šŽ`™—¬ƒ_ÖÖ¢™,³Óô¿…‚ýFAŸ[…"XM |ß[—àï“N¡. !+W=Í ò~HêgkàßaJÞ¾h§„ç©B0Xa!”«­ZYõ#©BðXa!2«-:…³À !^pù-²oFªpÖXa!| .R…&b……Я¶¯'U ‚…&Ï¡ºóäÃo~"$š¥\,©Ð– þ9B…• ÎY"½,~J(†Çs(Ò›`dš–éM@ÃXaƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ?‘Œ ´hÑ¥oßžš¦9�5óæMêÓ§'‡ðã¬9ü|ŽåHþM÷sÏ½äØ±½J©½{K²³ÇïÚµVñoºsÖ�c……ƹä’.Ÿ}ö¥7aÆ]Ó«×nºéºììq'NT´lÙ¢°pŽÍfÍÍ�ðã�Çìvûòåó£¢lcÇN9rä¨Ãáœ;÷Á¾}{.X°¤°ð%MÓòóóöïÿÔwyðàì Ô�`¡q¶nýWbb7¥Tuµã–[Ò¯¹æÊ[o½'##=3sxqñªxØjµÞxcZFFzQÑËS¦<îtÖŒ›•””øÅ‡ÓÓsöìÙ0cFÁ§Ÿ¾ýõ×GfÍzrãÆm¾Ë â-¡zxKèý ËãñÄÇ·*(˜š�ðû–-»••ý×b±´m›tðàÛÑÑöêjGÇŽ4M;xðm»=Êår�8QѽûàN�Ú{ïä믿-)Ùš“3¾´ôøwd¦¤ Èʺßw9‚ÏŽ·„R°ÂBPìö¨mÛ^ü¿©c³Z,–ÀÏ}\.—÷«ÕNMMÍÆ�Ëbb¢Ýn÷öíïZ­Ö%K{ë­�óç?ûüókμ\T4—��úñk hªAƒú­\¹^)µråú?þñò¾}׬y])µxñŠ éßÿ²Õ«7)¥6lØ6{ö“¥¥Ç¼©_¿K‹‹ç­[÷†ïòúõo²'Ð Þ  «Ÿ^søð·99ãËË+ãâb çTTTæäŒ÷¾y\¶ìñãÇËssó***m6Û¢Eù;¶Ÿ3gáŠÿt»Ý997WVVù.ßuרÈ=?~J(Á@Á2<~­AÞƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ¿é.æoº# 8U½Î±Â +,b°Â Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á ¤ø:Qá71šþIEND®B`‚adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/Talya.txt000066400000000000000000000000371526221261600275640ustar00rootroot00000000000000This is a text file for Talya. adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/Talya_diagram.png000066400000000000000000000102161526221261600312150ustar00rootroot00000000000000‰PNG  IHDR�,bÕr•UIDATxœíÝ{xSåÀñ÷$iÚR¤nŽíATP¹ ¡ˆP)"ÃY´Tª°y)ÒQZŸú ¢>*¬ Üª °¢S[ ¨¨Âä& <ʘ”)åoˆâ…zKšd#KzIišœß9ßÏ_iH““÷¼ç›7iéÑ<� ,‘ÞÁ Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †Mé’¦i‘ÞÀì<�ÒVXÄÐé Ë땒ÑÞÀŒþÜå|¥K¬°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆA°ˆ¡ë?‘ŒPÉHìxÑ%½¼—û\5xèèÛ3ût)ÞUÒì�¨i5NGÊM·vó—ö¼ë�k2F5߃Âð–)Ø¢ìÓ‹_‰È#VWVÌ“Û"ùÚ¡í.ìÎm€ñ,S;QVúìŒúþ»§sÔøÉõèõå�ýOO_^Vö§¿d ÍÊUJeöé’:"kßû;ËËJo¾ë�¤”Ôcß÷äÄû*+Êc[ÄÝ9ëñsóÛÌ>]’®N½ [÷´Ì¿‡ˆŽm‘9î¡Â‡'%_;Ô»¬ó{ˆc?}ê¡ÊËJ[·m·û­7–¼ó‘÷A½wØ£ß�ÛsyJêG»Þ¹>gLÉ{;Kv¿;dd¶÷Ÿ`x|†ejË�>ddöÔ¢—î�óÄÓ“Ç)¥6,/yßęϭ^óìSÞÛÔ8çüê×3ŠWýí‰ÂÂY“”RKò§]‘vÃÌ嫯H»aé#Ó•RNGõi×ÖÊ+¡s×#Ÿò}é÷K™ÞÈõ3Ÿ{µßà´ªŠrïm|w¸=ÎêªÁ7gN_¶rÑ´ C2s¦/[éû'+,S¨q:&gþÙ{ùÖû'vNìí½¼gû6_Jª**Ü.W渇¶¯[óî¶Í'Ž{¯÷¸ÝW ¿E)õ»vÊ�Ÿ¼ò£�;îœ5O)•œš¾ü±‡O¾îY¬=“¯¬ëÑ]5.kÔ/3Íï!>ڹ㎙�)¥zºÚb±zoã»ÃÀíÑ,–NÝ{Z¬V[TÔ…Ý{jKuUeó t‡`™ú3,W�kÒâ磢£=n÷¾÷vZ¬Ö¹÷ä^>8mÈÈœM/,ó}o\«Vgž‘;ð|ÀV›M³Ô¹Zÿdïîwõ}é÷5N§÷z�ÛãQ¿;¬u{,Ö“]‹ŠŽ©çAaHìoSëriŸÿlÙ ”zÿ­7V=³@)õé?èŸ:Ôé¨v:ª½· ŒB÷¤äozM)õïM¯ý¡orýq¢¬´xîÃ7Üv‡ï¿‡èÜë²][7*¥þ³yCà¹Ñ·fÆ Ëto /îÕ{ä}Ût¸`Õ3 FO˜öôäq›V,³ZmcfÌQJ];"kBFzBçnq­â�G”Ýxo?ùÞ÷ú‹Å1±-îœõx}�¨i®ç°Ûî<³k~1:ojAÞ=ëŸ+êœØ;&¶…ßý³=0-ð5M¼o=8U½,È»7}ôí:w=°wÏ’G¦Í\¾:Ò[å=U½ãÀ 6$3{ÑŒ‰ö蘧3wÊìHot�1+,>t Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †®ÿâ(€òè/¬°ˆ¡Ó$ò­‹=Ï™—#ºQ0‚…&Ï¡ºÛD¶Z M˜=A/£(B‚`á¬æÍY½ã#[h"‚…FΘ&8E¶pÖ‚›(¡þ�lá,,44Ešùç}” Á#X¨{r„ñWÈ‚A°PÛ´ˆÐoQ‘-Ô�`áŒÙ �ßöÔÉf@‡ôÛ}n"ˆ`™�þ£ ÿ-DØ,ó’Y[‹fB°LGô‘/zãÑtËDŒt´é¹ xËŒzxõy¡.ËàÌpH›á9‹`“ �a>e"XFÃqËÁ2TFÃð–�*FÆ$–`tŠ�2‚%©bÜ̉` CªC3#Xb�*ÆKïèà ‚¥_¤ŠÑ†‚¥G¤Š‘Gí‡ÿ�AWH•°t‹`…à?¯Õ%øŽjÊN áÜÀ™V“Ô3/ƒœ‘¤JÿÎn5}n �­–ë¤J o_´SÈMd±ÂjòÖöBZÿK(©’«Qûî,æêÇ +|è”°ÚŠ,VX¡Äÿ! | %UFÕàžmpn QXa5/Rel,¸ÂŒVs½Ò’*³©k�3BˆVˆ1;M‹ÕV°Â ÝPrÔ14�-4t:Ž þ¢0BEŸ Ì 3Ï K¤7 ñÖüçGzŒìÞ_ß«Ä:6uj¤7ÁÈÎÕëð²Â Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á J^B^ÓÇzËü-M¿èÁs»w\¸0eñâ� ¾°gO=·œ·}{£î¹±·7‚>˶8P¼{÷ÚQ£6ßvÛÚQ£–¾ÿþ¶ƒëº1Á -‚Õ8y yëf®[pÝ‚G<úákú®\;ymÁ�‚‚´‚>ÿÁoEæ½¼!ƒ£ÜñÔð§B»ÿ~;vLOI‰�‰QJÅÇÄLKI™jYÔ>?ßwïåÙo¾Yîp +.ö^3éõׯ-,L-*úü§Ÿ¼=jE°ÇåpÅ�7öµ±9Å9«&¬:}eµ«]¯vw¯¿;yTò«½Zë7¦æ¥ÚãìcV�iäBwö=Ú£Mß—=Û´)9z´Ö[N4(În_�™©”rÔÔô:ÿü�ÙÙY½{OÜ´©ÁÛ£V«q<OÒˆ$¥Ôy çU•U�¾VS=Òz(¥¯O<´ó�ÿ·¸uw6J„Ò©;û]ç<©¦]×µ«Rê†nÝv}õU÷GmVãX£¬±ñ±§¿øy–jM³žþÂm;3R•¥•.§«‘]ëÒºõß|ãûrÏ7ßtmÝúÌè”VU9]þ;ýÔ9=IìVkƒ·G­Vãhÿ×Ò“3ÏåÞ·yßɹ»fÏE.RJŶŠ=RrD)õÞËïi?‡Íãö°Ú2€»û÷Ÿ²ysYÕÉõuiUÕÔ-[îéß_)Õ*:ºä»ï”R/}ø¡o–œÚç'Ãär»7ò‰RêÕ�?pÁ ÞòÎü,…-ÚöÁ??غ`kl|lFA†Rjxþð%£—´lݲå¬Ñ'_N•R�úuZ4bQîŠÜHo/šäªN�—•]·ti´Õêp¹nOJر£RêÑÔÔ¬—_þM\\ï¶mí¶ÓGVrûö/¼ðâˆÑ6Ûš}ûþ¾cG|LÌéé Þž�T+ͣ˜{?�rªú¼„¼üC¿üÄGÖ©êõ9œâNUß>?ÿ‹¼ü6_8OU¯Ã¹Á[Bb¬�¸¼B˜ Z^éÁ Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á †®ÿ¦;Â@Ÿ Ì 3Ï VXÄÐé K"ß+?C æC3!X¡P²enL€fE°šmd)—ɰÇÀ`5óø’-`/‡ Á Ë(“-ƒbφÁ ãX“-£`WF ÁŠÄ S.±Øw‘E°"7ôdKö—¬Hï²¥{ì#ý XºÀ!¡Cì"XúÂA¢ìÝ"XzÄÃÈ£VK¿È£ ?KïÈà ‚%åbÔEo<šŽ`™—¬ƒ_ÖÖ¢™,³Óô¿…‚ýFAŸ[…"XM |ß[—àï“N¡. !+W=Í ò~HêgkàßaJÞ¾h§„ç©B0Xa!”«­ZYõ#©BðXa!2«-:…³À !^pù-²oFªpÖXa!| .R…&b……Я¶¯'U ‚…&Ï¡ºóäÃo~"$š¥\,©Ð– þ9B…• ÎY"½,~J(†Çs(Ò›`dš–éM@ÃXaƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ?‘Œ ´hÑ¥oßžš¦9�5óæMêÓ§'‡ðã¬9ü|ŽåHþM÷sÏ½äØ±½J©½{K²³ÇïÚµVñoºsÖ�c……ƹä’.Ÿ}ö¥7aÆ]Ó«×nºéºììq'NT´lÙ¢°pŽÍfÍÍ�ðã�Çìvûòåó£¢lcÇN9rä¨Ãáœ;÷Á¾}{.X°¤°ð%MÓòóóöïÿÔwyðàì Ô�`¡q¶nýWbb7¥Tuµã–[Ò¯¹æÊ[o½'##=3sxqñªxØjµÞxcZFFzQÑËS¦<îtÖŒ›•””øÅ‡ÓÓsöìÙ0cFÁ§Ÿ¾ýõ×GfÍzrãÆm¾Ë â-¡zxKèý ËãñÄÇ·*(˜š�ðû–-»••ý×b±´m›tðàÛÑÑöêjGÇŽ4M;xðm»=Êår�8QѽûàN�Ú{ïä믿-)Ùš“3¾´ôøwd¦¤ Èʺßw9‚ÏŽ·„R°ÂBPìö¨mÛ^ü¿©c³Z,–ÀÏ}\.—÷«ÕNMMÍÆ�Ëbb¢Ýn÷öíïZ­Ö%K{ë­�óç?ûüókμ\T4—��úñk hªAƒú­\¹^)µråú?þñò¾}׬y])µxñŠ éßÿ²Õ«7)¥6lØ6{ö“¥¥Ç¼©_¿K‹‹ç­[÷†ïòúõo²'Ð Þ  «Ÿ^søð·99ãËË+ãâb çTTTæäŒ÷¾y\¶ìñãÇËssó***m6Û¢Eù;¶Ÿ3gáŠÿt»Ý997WVVù.ßuרÈ=?~J(Á@Á2<~­AÞƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ`ƒ¿é.æoº# 8U½Î±Â +,b°Â Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á Á ¤ø:Qá71šþIEND®B`‚adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/__init__.py000066400000000000000000000000251526221261600300570ustar00rootroot00000000000000"""Documentation.""" adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/conf.py000066400000000000000000000005151526221261600272510ustar00rootroot00000000000000"""Sample ``conf.py``.""" extensions = [ "myst_parser", "sphinx_substitution_extensions", "sphinx_toolbox.rest_example", ] rst_prolog = """ .. |author| replace:: Eleanor .. |MixedCaseReplacement| replace:: UnusedReplacement """ myst_enable_extensions = ["substitution"] myst_substitutions = { "author": "Talya", } adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/five.rst000066400000000000000000000000261526221261600274320ustar00rootroot00000000000000======= Five ======= adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/four.rst000066400000000000000000000000261526221261600274540ustar00rootroot00000000000000======= Four ======= adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/index.rst000066400000000000000000000035051526221261600276150ustar00rootroot00000000000000Samples for substitution directives and roles ============================================= Configuration ------------- .. literalinclude:: conf.py :language: python ``code-block`` -------------- .. rest-example:: .. code-block:: shell echo "The author is |author|" .. code-block:: shell :substitutions: echo "The author is |author|" Inline ``:code:`` ----------------- .. rest-example:: :code:`echo "The author is |author|"` :substitution-code:`echo "The author is |author|"` Inline ``:download:`` --------------------- .. rest-example:: .. We cannot use the substitution in the download target, because the download directive will error if the file does not exist. :download:`Script by |author| <../source/Eleanor.txt>`. :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: ``include`` ----------- Content substitutions ~~~~~~~~~~~~~~~~~~~~~ .. include:: sample_content_include.txt :content-substitutions: Path substitutions ~~~~~~~~~~~~~~~~~~ .. include:: |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-219c7b1/sample/source/markdown_sample.md000066400000000000000000000050021526221261600314530ustar00rootroot00000000000000Samples 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: ``` ``include`` ----------- ### Path substitutions ```{code-block} markdown ```{include} {{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-219c7b1/sample/source/one.rst000066400000000000000000000000211526221261600272550ustar00rootroot00000000000000===== One ===== adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/sample_content_include.txt000066400000000000000000000000611526221261600332250ustar00rootroot00000000000000.. code-block:: text The author is |author|. adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/sample_image.png000066400000000000000000000001031526221261600310740ustar00rootroot00000000000000‰PNG  IHDRĉ IDATxœc -´IEND®B`‚adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/sample_include.txt000066400000000000000000000001201526221261600314670ustar00rootroot00000000000000This is a sample file for demonstrating literalinclude. The author is |author|. adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/three.rst000066400000000000000000000000271526221261600276110ustar00rootroot00000000000000======= Three ======= adamtheturtle-sphinx-substitution-extensions-219c7b1/sample/source/two.rst000066400000000000000000000000211526221261600273050ustar00rootroot00000000000000===== Two ===== adamtheturtle-sphinx-substitution-extensions-219c7b1/spelling_private_dict.txt000066400000000000000000000003431526221261600303030ustar00rootroot00000000000000admin beartype changelog conf hardcoded inline linters pragma py pyrefly pyright pytest reportUnknownArgumentType reportUnknownMemberType reportUnknownParameterType reportUnknownVariableType rst str tuple ty untyped whitespace adamtheturtle-sphinx-substitution-extensions-219c7b1/src/000077500000000000000000000000001526221261600237575ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/src/sphinx_substitution_extensions/000077500000000000000000000000001526221261600324035ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/src/sphinx_substitution_extensions/__init__.py000066400000000000000000000707521526221261600345270ustar00rootroot00000000000000"""Custom Sphinx extensions.""" from collections.abc import Callable, Collection from importlib.metadata import version from pathlib import Path from typing import TYPE_CHECKING, ClassVar, TypeAlias from unittest.mock import patch from beartype import beartype from docutils.nodes import ( Element, Node, Text, document, reference, substitution_definition, system_message, ) from docutils.nodes import target as target_node from docutils.parsers.rst import directives from docutils.parsers.rst.directives.images import Image from docutils.parsers.rst.directives.misc import Include as DocutilsInclude from docutils.parsers.rst.roles import code_role from docutils.parsers.rst.states import Inliner, RSTState from docutils.statemachine import StringList from myst_parser.config.main import MdParserConfig from myst_parser.mdit_to_docutils.base import DocutilsRenderer from myst_parser.mocking import MockInliner, MockState from sphinx import addnodes from sphinx.application import Sphinx from sphinx.config import Config from sphinx.directives.code import CodeBlock, LiteralInclude from sphinx.directives.other import Include from sphinx.environment import BuildEnvironment from sphinx.errors import SphinxError from sphinx.roles import XRefRole from sphinx.util.typing import ExtensionMetadata from typing_extensions import TypeAliasType, override 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, ) # Keep the legacy static alias to preserve complete type information. if TYPE_CHECKING: SubstitutionValue: TypeAlias = ( str | int | float | list["SubstitutionValue"] | dict[str, "SubstitutionValue"] ) else: SubstitutionValue = TypeAliasType( "SubstitutionValue", str | int | float | list["SubstitutionValue"] | dict[str, "SubstitutionValue"], ) Substitutions: TypeAlias = dict[str, SubstitutionValue] # Avoid Any-valued converter results until our minimum Sphinx includes # https://github.com/sphinx-doc/sphinx/pull/14672. OptionSpec: TypeAlias = dict[str, Callable[[str], object]] @beartype def _delimiter_pair(*, value: object) -> tuple[str, str]: """Return a validated pair of string delimiters.""" assert isinstance(value, (list, tuple)) assert isinstance(value[0], str) assert isinstance(value[1], str) assert value[2:] == value[:0] return value[0], value[1] @beartype def _get_myst_config( *, context: Inliner | MockInliner | MockState | RSTState ) -> MdParserConfig | None: """Get the effective MyST configuration from a parsing context.""" if isinstance(context, (MockInliner, MockState)): # MyST merges front matter into a document-local configuration before # it creates these parsing contexts. The Sphinx configuration only # contains the global ``conf.py`` values. # https://github.com/executablebooks/MyST-Parser/issues/680 tracks # substitutions inside directives, but its proposed PR registers them # only after directives have run: # https://github.com/executablebooks/MyST-Parser/pull/966 renderer = vars(context)["_renderer"] assert isinstance(renderer, DocutilsRenderer) return renderer.md_config return None @beartype def _validate_substitution_key(*, key: str) -> None: """Validate that a substitution key does not contain dots. Dots are reserved for nested access notation in flattened keys (e.g., |a.b.c| for nested dictionaries or |items.0| for lists). Allowing dots in user-defined keys would create ambiguity between a literal key name and a path to a nested value. A :class:`~sphinx.errors.SphinxError` is raised if the key contains a dot. """ if "." in key: message = ( f"Substitution key {key!r} contains a dot ('.'). " "Dots are reserved for nested access notation " "(e.g., |a.b.c| for nested dictionaries or |items.0| for lists)." ) raise SphinxError(message) @beartype def _flatten_substitutions( *, substitutions: Substitutions, ) -> dict[str, str]: """Flatten nested substitutions dictionary using dot notation. Recursively processes nested dictionaries and lists, converting them to a flat dictionary where keys represent the path to each value using dot notation. For example:: {'a': {'b': {'c': 'value'}}} -> {'a.b.c': 'value'} {'items': [{'name': 'a'}]} -> {'items.0.name': 'a'} A :class:`~sphinx.errors.SphinxError` is raised if any key in the nested structure contains a dot. """ result: dict[str, str] = {} stack: list[tuple[str, SubstitutionValue]] = [("", substitutions)] while len(stack) > 0: current_key, current_value = stack.pop() match current_value: case dict(): for key, value in current_value.items(): _validate_substitution_key(key=key) new_key = ( f"{current_key}.{key}" if current_key != "" else key ) stack.append((new_key, value)) case list(): for idx, item in enumerate(iterable=current_value): new_key = ( f"{current_key}.{idx}" if current_key != "" else str(object=idx) ) stack.append((new_key, item)) case _: result[current_key] = str(object=current_value) return result @beartype def _get_delimiter_pairs( *, env: BuildEnvironment, config: Config, myst_config: MdParserConfig | None, ) -> 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 len(parser_supported_formats.intersection(markdown_suffixes)) > 0: if myst_config is None: configured_delimiters: object = config.myst_sub_delimiters else: configured_delimiters = myst_config.sub_delimiters opening_delimiter, closing_delimiter = _delimiter_pair( value=configured_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], myst_config: MdParserConfig | None, ) -> 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 len(parser_supported_formats.intersection(markdown_suffixes)) > 0: if myst_config is None: enable_extensions = config.myst_enable_extensions substitutions = dict(config.myst_substitutions) else: enable_extensions = myst_config.enable_extensions substitutions = dict(myst_config.substitutions) if "substitution" in enable_extensions: return _flatten_substitutions( substitutions=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: Collection[str], 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: Element, source_path: Path | None, substitution_defs: dict[str, str], delimiter_pairs: set[tuple[str, str]], ) -> None: """Recursively process nodes to apply substitutions.""" if ( source_path is not None and node.source is not None and Path(node.source).resolve() != source_path ): return node.rawsource = _apply_substitutions( text=node.rawsource, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) for child in list(node.children): if isinstance(child, Text): node.replace( old=child, new=Text( data=_apply_substitutions( text=child.astext(), substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ), ), ) else: assert isinstance(child, Element) _process_node( node=child, source_path=source_path, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) @beartype def _substitute_hyperlink_targets( app: Sphinx, doctree: document, ) -> None: """Replace placeholders in hyperlink targets.""" if not app.config.substitutions_hyperlink_targets_enabled: return substitution_defs = _get_substitution_defs( env=app.env, config=app.config, substitution_defs=doctree.substitution_defs, myst_config=None, ) delimiter_pairs = _get_delimiter_pairs( env=app.env, config=app.config, myst_config=None, ) for node in doctree.findall(): if not isinstance(node, (reference, target_node)): continue refuri = node.attributes.get("refuri") if isinstance(refuri, str): node["refuri"] = _apply_substitutions( text=refuri, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) @beartype class SubstitutionCodeBlock(CodeBlock): """Similar to CodeBlock but replaces placeholders with variables.""" option_spec: ClassVar[OptionSpec] = OptionSpec(CodeBlock.option_spec) option_spec[SUBSTITUTION_OPTION_NAME] = directives.flag option_spec[NO_SUBSTITUTION_OPTION_NAME] = directives.flag @override def run(self) -> list[Node]: """Replace placeholders with given variables.""" new_content = StringList() existing_content = self.content myst_config = _get_myst_config(context=self.state) substitution_defs = _get_substitution_defs( env=self.env, config=self.config, substitution_defs=self.state.document.substitution_defs, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, myst_config=myst_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, object]] = { "class": directives.class_option, "language": directives.unchanged, } def __call__( self, typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner | MockInliner, *, options: dict[str, object] | None = None, content: list[str] | None = None, ) -> tuple[list[Node], list[system_message]]: """Replace placeholders with given variables.""" settings = inliner.document.settings env: object = settings.env assert isinstance(env, BuildEnvironment) myst_config = _get_myst_config(context=inliner) substitution_defs = _get_substitution_defs( env=env, config=env.config, substitution_defs=inliner.document.substitution_defs, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=env.config, myst_config=myst_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( typ, rawtext=rawtext, text=text, lineno=lineno, inliner=inliner, options={} if options is None else options, content=[] if content is None else content, ) @beartype class SubstitutionLiteralInclude(LiteralInclude): """ Similar to LiteralInclude but replaces placeholders with variables. """ option_spec: ClassVar[OptionSpec] = OptionSpec(LiteralInclude.option_spec) 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 @override def run(self) -> list[Node]: """ Replace placeholders with given variables in the file path and/or included file content. """ myst_config = _get_myst_config(context=self.state) 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, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, myst_config=myst_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, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=self.env, config=self.config, myst_config=myst_config, ) for node in nodes_list: assert isinstance(node, Element) _process_node( node=node, source_path=None, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) return nodes_list @beartype class SubstitutionInclude(Include): """ Similar to Include but replaces placeholders with variables in the path. """ option_spec: ClassVar[OptionSpec | None] = { **(Include.option_spec if Include.option_spec is not None else {}), CONTENT_SUBSTITUTION_OPTION_NAME: directives.flag, PATH_SUBSTITUTION_OPTION_NAME: directives.flag, NO_CONTENT_SUBSTITUTION_OPTION_NAME: directives.flag, NO_PATH_SUBSTITUTION_OPTION_NAME: directives.flag, } def _run_with_include_read( self, *, env: BuildEnvironment, substitution_defs: dict[str, str], delimiter_pairs: set[tuple[str, str]], ) -> list[Node]: """Apply substitutions after existing include-read listeners.""" include_read_emitted = False _relative_path, absolute_path = env.relfn2path( filename=self.arguments[0], ) current_include_path = Path(absolute_path).resolve() def substitute_include_content( _app: Sphinx, relative_path: Path, _parent_docname: str, content: list[str], ) -> None: """Substitute content changed by earlier listeners.""" included_path = (env.srcdir / relative_path).resolve() if included_path != current_include_path: return nonlocal include_read_emitted include_read_emitted = True content[0] = _apply_substitutions( text=content[0], substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) listener_id = env.events.connect( name="include-read", callback=substitute_include_content, priority=999, ) try: nodes_list = list(super().run()) finally: env.events.disconnect(listener_id=listener_id) if not include_read_emitted: for node in nodes_list: assert isinstance(node, Element) _process_node( node=node, source_path=current_include_path, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) return nodes_list @override def run(self) -> list[Node]: """Replace placeholders in the path and/or included content.""" env: object = self.state.document.settings.env if env is None: return list(DocutilsInclude.run(self=self)) assert isinstance(env, BuildEnvironment) config: Config = env.config myst_config = _get_myst_config(context=self.state) 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, ) should_apply_content_substitutions = _should_apply_substitutions( options=self.options, config=config, yes_flag=CONTENT_SUBSTITUTION_OPTION_NAME, no_flag=NO_CONTENT_SUBSTITUTION_OPTION_NAME, ) if not ( should_apply_path_substitutions or should_apply_content_substitutions ): return list(super().run()) substitution_defs = _get_substitution_defs( env=env, config=config, substitution_defs=self.state.document.substitution_defs, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=config, myst_config=myst_config, ) if should_apply_path_substitutions: for argument_index, argument in enumerate(iterable=self.arguments): argument_text: str = argument self.arguments[argument_index] = _apply_substitutions( text=argument_text, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) if not should_apply_content_substitutions: return list(super().run()) if "include-read" in env.events.listeners: return self._run_with_include_read( env=env, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) original_insert_input = self.state_machine.insert_input def insert_substituted_input( input_lines: list[str], source: str, ) -> None: """Insert included lines after applying substitutions.""" substituted_lines = [ _apply_substitutions( text=line, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) for line in input_lines ] original_insert_input( input_lines=substituted_lines, source=source, ) # ``Include.run`` only queues these lines. Nested includes are parsed # after it returns, when this patch has already been removed. with patch.object( target=self.state_machine, attribute="insert_input", new=insert_substituted_input, ): nodes_list = list(super().run()) for node in nodes_list: assert isinstance(node, Element) _process_node( node=node, source_path=None, 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. """ option_spec: ClassVar[OptionSpec | None] = { **(Image.option_spec if Image.option_spec is not None else {}), PATH_SUBSTITUTION_OPTION_NAME: directives.flag, NO_PATH_SUBSTITUTION_OPTION_NAME: directives.flag, } @override def run(self) -> list[Node]: """Replace placeholders with given variables in the image path.""" env: object = self.state.document.settings.env assert isinstance(env, BuildEnvironment) config: Config = env.config myst_config = _get_myst_config(context=self.state) 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, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=config, myst_config=myst_config, ) for argument_index, argument in enumerate(iterable=self.arguments): argument_text: str = argument self.arguments[argument_index] = _apply_substitutions( text=argument_text, substitution_defs=substitution_defs, delimiter_pairs=delimiter_pairs, ) return list(super().run()) @beartype class SubstitutionXRefRole(XRefRole): """Custom role for XRefs.""" @override 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.removeprefix("substitution-") return super().create_xref_node() @override def process_link( self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str, ) -> tuple[str, str]: """ Override parent method to replace placeholders with given variables. """ assert isinstance(env, BuildEnvironment) myst_config = _get_myst_config(context=self.inliner) substitution_defs = _get_substitution_defs( env=env, config=env.config, substitution_defs=self.inliner.document.substitution_defs, myst_config=myst_config, ) delimiter_pairs = _get_delimiter_pairs( env=env, config=env.config, myst_config=myst_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_hyperlink_targets_enabled", default=False, 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="include", directive=SubstitutionInclude, ) 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) _ = app.connect( event="doctree-read", callback=_substitute_hyperlink_targets, ) return { "parallel_read_safe": True, "version": version(distribution_name="sphinx-substitution-extensions"), } adamtheturtle-sphinx-substitution-extensions-219c7b1/src/sphinx_substitution_extensions/py.typed000066400000000000000000000000001526221261600340700ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/src/sphinx_substitution_extensions/shared.py000066400000000000000000000010201526221261600342140ustar00rootroot00000000000000"""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.txt000066400000000000000000000000001526221261600374250ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/src/sphinx_substitution_extensionsadamtheturtle-sphinx-substitution-extensions-219c7b1/styles/000077500000000000000000000000001526221261600245135ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/styles/snapper/000077500000000000000000000000001526221261600261635ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/styles/snapper/LongProseLine.yml000066400000000000000000000006701526221261600314310ustar00rootroot00000000000000--- # Vale style rule: flag prose lines that are suspiciously long. # Likely indicates wrapped text that should use semantic line breaks. extends: existence message: Prose line exceeds 120 characters. Consider running 'snapper' to break at sentence boundaries. level: suggestion scope: paragraph # This catches lines > 120 chars that contain a period followed by a space. # Pure code or URLs are less likely to match. tokens: ['.{120,}'] adamtheturtle-sphinx-substitution-extensions-219c7b1/styles/snapper/SemanticLineBreaks.yml000066400000000000000000000006641526221261600324170ustar00rootroot00000000000000--- # Vale style rule: flag lines containing multiple sentences. # These should be broken at sentence boundaries. extends: existence message: Line contains multiple sentences. Run 'snapper --check' or reformat with 'snapper --in-place'. level: warning scope: paragraph # Match a sentence-ending punctuation followed by a space and a capital letter # on the same line (indicating two sentences on one line). tokens: ['[.!?]\\s+[A-Z]'] adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/000077500000000000000000000000001526221261600243325ustar00rootroot00000000000000adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/__init__.py000066400000000000000000000000541526221261600264420ustar00rootroot00000000000000"""Tests for the substitution extension.""" adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/conftest.py000066400000000000000000000001541526221261600265310ustar00rootroot00000000000000"""Configuration for pytest.""" pytest_plugins = "sphinx.testing.fixtures" # pylint: disable=invalid-name adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/equivalence_cases.toml000066400000000000000000001520151526221261600307120ustar00rootroot00000000000000# Declarative Sphinx build-equivalence cases. # Each case builds `actual`, builds its hand-expanded `expected` form, # and compares the selected HTML output byte-for-byte. schema_version = 1 [[cases]] id = "custom_myst_sub_delimiters_code_block" description = "Custom MyST substitution delimiters are respected." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } "myst_sub_delimiters" = ["[", "]"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-[[a]]-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-example_substitution-POST ``` ''' [[cases]] id = "default_myst_sub_delimiters_code_block" description = "The default MyST substitution delimiters are respected." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-{{a}}-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-example_substitution-POST ``` ''' [[cases]] id = "default_substitution_include_content" description = "Replace include content when substitutions are enabled by\ndefault." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "example.txt" = ''' Included literal content:: Content with |name| placeholder ''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Included literal content:: Content with example placeholder ''' "index.rst" = ''' .. include:: example.txt ''' [[cases]] id = "default_substitution_include_disabled_content" description = "Respect ``:nocontent-substitutions:`` when defaults are enabled." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "example.txt" = ''' Included literal content:: Content with |name| placeholder ''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt :nocontent-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Included literal content:: Content with |name| placeholder ''' "index.rst" = ''' .. include:: example.txt ''' [[cases]] id = "default_substitutions_disabled_with_flag" description = "When ``substitutions_default_enabled`` is True but a code block has\nthe\n``:nosubstitutions:`` flag, substitutions should not be applied." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. code-block:: shell :nosubstitutions: $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-|a|-POST ''' [[cases]] id = "default_substitutions_enabled" description = "When ``substitutions_default_enabled`` is set to True in conf.py,\ncode\nblocks should apply substitutions by default without needing the\n``:substitutions:`` flag." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. code-block:: shell $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-example_substitution-POST ''' [[cases]] id = "default_substitutions_image_disabled_path" description = "When ``substitutions_default_enabled`` is True but ``image`` has the\n``:nopath-substitutions:`` flag, path substitutions should not be\napplied.\n\nNote: This test uses MyST format with custom delimiters because the `|`\ncharacter cannot be used in Windows file paths." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } "myst_sub_delimiters" = ["[", "]"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} [[a]].png :nopath-substitutions: ``` ''' [cases.actual.binary_files] "[[a]].png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} [[a]].png ``` ''' [cases.expected.binary_files] "[[a]].png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "default_substitutions_image_path" description = "When ``substitutions_default_enabled`` is True, ``image`` should\napply path\nsubstitutions by default without requiring the ``:path-substitutions:``\nflag." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test_image .. image:: |a|.png ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. image:: test_image.png ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "default_substitutions_literal_include_content" description = "When ``substitutions_default_enabled`` is True, ``literalinclude``\nshould\napply content substitutions by default without requiring the\n``:content-\nsubstitutions:`` flag." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example_substitution placeholder''' "index.rst" = ''' .. literalinclude:: example.txt ''' [[cases]] id = "default_substitutions_literal_include_disabled_content" description = "When ``substitutions_default_enabled`` is True but\n``literalinclude`` has\nthe ``:nocontent-substitutions:`` flag, content substitutions should\nnot be\napplied." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt :nocontent-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. literalinclude:: example.txt ''' [[cases]] id = "default_substitutions_literal_include_disabled_path" description = "When ``substitutions_default_enabled`` is True but\n``literalinclude``\nhas the ``:nopath-substitutions:`` flag, path substitutions should not\nbe\napplied.\n\nNote: This test uses MyST format with custom delimiters because the `|`\ncharacter cannot be used in Windows file paths." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } "myst_sub_delimiters" = ["[", "]"] "substitutions_default_enabled" = true [cases.actual.files] "[[a]].txt" = ''' File content''' "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{literalinclude} [[a]].txt :nopath-substitutions: ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "[[a]].txt" = ''' File content''' "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{literalinclude} [[a]].txt ``` ''' [[cases]] id = "default_substitutions_literal_include_path" description = "When ``substitutions_default_enabled`` is True, ``literalinclude``\nshould\napply path substitutions by default without requiring the ``:path-\nsubstitutions:`` flag." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "substitutions_default_enabled" = true [cases.actual.files] "conf.py" = "" "example_substitution.txt" = ''' File content''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: |a|.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example_substitution.txt" = ''' File content''' "index.rst" = ''' .. literalinclude:: example_substitution.txt ''' [[cases]] id = "include_path_is_relative_to_source_directory[literal]" description = "Resolve slash-prefixed include paths from Sphinx's source root." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "fragments/example.txt" = ''' Included content''' "index.rst" = ''' .. |name| replace:: example .. include:: /fragments/example.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "fragments/example.txt" = ''' Included content''' "index.rst" = ''' .. include:: /fragments/example.txt ''' [[cases]] id = "include_path_is_relative_to_source_directory[substitution]" description = "Resolve slash-prefixed include paths from Sphinx's source root." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "fragments/example.txt" = ''' Included content''' "index.rst" = ''' .. |name| replace:: example .. include:: /fragments/|name|.txt :path-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "fragments/example.txt" = ''' Included content''' "index.rst" = ''' .. include:: /fragments/example.txt ''' [[cases]] id = "myst_deep_nesting" description = "MyST deeply nested substitutions (5 levels) are flattened\ncorrectly." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "level1" = { "level2" = { "level3" = { "level4" = { "level5" = "deep_value" } } } } } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|level1.level2.level3.level4.level5|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-deep_value-POST ``` ''' [[cases]] id = "myst_empty_containers" description = "MyST empty containers (lists and dictionaries) do not create\nkeys\nand do not break the build." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "empty_list" = [], "empty_dict" = { }, "mixed" = { "empty_nested_list" = [], "empty_nested_dict" = { }, "normal" = "value" }, "normal" = "normal_value" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|normal|-POST ``` ```{code-block} :substitutions: $ PRE-|mixed.normal|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-normal_value-POST ``` ```{code-block} $ PRE-value-POST ``` ''' [[cases]] id = "myst_frontmatter_substitutions" description = "MyST front matter substitutions are respected in code\nblocks." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "global" = "global_value" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' --- myst: substitutions: local: nested: local_value --- # Title ```{code-block} shell :substitutions: $ PRE-|global|-|local.nested|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} shell $ PRE-global_value-local_value-POST ``` ''' [[cases]] id = "myst_mixed_types_in_list" description = "MyST mixed types in lists are converted to strings\ncorrectly." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "mixed" = [1, "text", 3.14, -42, 0] } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|mixed.0|-POST ``` ```{code-block} :substitutions: $ PRE-|mixed.1|-POST ``` ```{code-block} :substitutions: $ PRE-|mixed.2|-POST ``` ```{code-block} :substitutions: $ PRE-|mixed.3|-POST ``` ```{code-block} :substitutions: $ PRE-|mixed.4|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-1-POST ``` ```{code-block} $ PRE-text-POST ``` ```{code-block} $ PRE-3.14-POST ``` ```{code-block} $ PRE--42-POST ``` ```{code-block} $ PRE-0-POST ``` ''' [[cases]] id = "myst_nested_lists" description = "MyST nested lists are flattened with multi-index notation." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "matrix" = [[1, 2], [3, 4]] } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|matrix.0.0|-POST ``` ```{code-block} :substitutions: $ PRE-|matrix.0.1|-POST ``` ```{code-block} :substitutions: $ PRE-|matrix.1.0|-POST ``` ```{code-block} :substitutions: $ PRE-|matrix.1.1|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-1-POST ``` ```{code-block} $ PRE-2-POST ``` ```{code-block} $ PRE-3-POST ``` ```{code-block} $ PRE-4-POST ``` ''' [[cases]] id = "myst_nested_substitutions" description = "MyST nested substitutions are flattened and applied\ncorrectly." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "value_a", "b" = { "c" = "value_b_c", "d" = { "e" = "value_b_d_e" } } } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` ```{code-block} :substitutions: $ PRE-|b.c|-POST ``` ```{code-block} :substitutions: $ PRE-|b.d.e|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-value_a-POST ``` ```{code-block} $ PRE-value_b_c-POST ``` ```{code-block} $ PRE-value_b_d_e-POST ``` ''' [[cases]] id = "myst_nested_substitutions_with_lists" description = "MyST nested substitutions with lists are flattened and\napplied correctly." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "items" = ["first", "second"], "nested" = [{ "name" = "a", "value" = "1" }, { "name" = "b", "value" = "2" }] } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|items.0|-POST ``` ```{code-block} :substitutions: $ PRE-|items.1|-POST ``` ```{code-block} :substitutions: $ PRE-|nested.0.name|-POST ``` ```{code-block} :substitutions: $ PRE-|nested.1.value|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-first-POST ``` ```{code-block} $ PRE-second-POST ``` ```{code-block} $ PRE-a-POST ``` ```{code-block} $ PRE-2-POST ``` ''' [[cases]] id = "myst_substitution_code_role" description = "The ``substitution-code`` role replaces the placeholders defined\nin\n``conf.py`` as specified." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title Example {substitution-code}`PRE-|a|-POST` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title Example {code}`PRE-example_substitution-POST` ''' [[cases]] id = "myst_substitution_download" description = "The ``substitution-download`` role replaces the placeholders\ndefined in\n``conf.py`` as specified." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title {substitution-download}`txt_pre-|a|-txt_post ` ''' "tgt_pre-example_substitution-tgt_post .py" = ''' Sample''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title {download}`txt_pre-example_substitution-txt_post ` ''' "tgt_pre-example_substitution-tgt_post .py" = ''' Sample''' [[cases]] id = "myst_substitution_hyperlink_target" description = "Replace global MyST substitutions in external hyperlink targets." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "ver" = "0.8.5" } "substitutions_hyperlink_targets_enabled" = true [cases.actual.files] "conf.py" = "" "index.md" = ''' ```{eval-rst} Download the tarball_ .. _tarball: https://example.com/releases/v|ver|.tar.gz ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.md" = ''' ```{eval-rst} Download the tarball_ .. _tarball: https://example.com/releases/v0.8.5.tar.gz ``` ''' [[cases]] id = "myst_substitutions" description = "MyST substitutions are respected in MyST documents." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-example_substitution-POST ``` ''' [[cases]] id = "myst_substitutions_custom_markdown_suffix" description = "Custom markdown suffixes are respected in MyST documents." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "example_substitution" } "source_suffix" = { ".rst" = "restructuredtext", ".txt" = "markdown" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.txt" = ''' # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] "source_suffix" = { ".rst" = "restructuredtext", ".txt" = "markdown" } [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.txt" = ''' # Title ```{code-block} $ PRE-example_substitution-POST ``` ''' [[cases]] id = "myst_substitutions_ignored_given_rst_definition" description = "MyST substitutions are ignored in rST documents with a rST\nsubstitution\ndefinition." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "myst_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: rst_prolog_substitution .. code-block:: shell :substitutions: $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-rst_prolog_substitution-POST ''' [[cases]] id = "myst_substitutions_ignored_without_rst_definition" description = "MyST substitutions are ignored in rST documents without a rST\nsubstitution definition." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "myst_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell :substitutions: $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-|a|-POST ''' [[cases]] id = "myst_substitutions_image" description = "MyST substitutions are respected in image paths in MyST\ndocuments." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "test_image" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} |a|.png :path-substitutions: ``` ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} test_image.png ``` ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "myst_substitutions_image_default_delimiters" description = "The default MyST substitution delimiters {{}} are respected for\nimages." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_enable_extensions" = ["substitution"] "myst_substitutions" = { "a" = "test_image" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} {{a}}.png :path-substitutions: ``` ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{image} test_image.png ``` ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "myst_substitutions_not_enabled" description = "MyST substitutions are not respected in MyST documents when\n``myst_enable_extensions`` does not contain ``substitutions``." output = "markdown_document.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["myst_parser", "sphinx_substitution_extensions"] "myst_substitutions" = { "a" = "example_substitution" } [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} :substitutions: $ PRE-|a|-POST ``` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "extensions" = ["myst_parser"] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. toctree:: markdown_document ''' "markdown_document.md" = ''' # Title ```{code-block} $ PRE-|a|-POST ``` ''' [[cases]] id = "no_substitution_code_block" description = "The ``code-block`` directive does not replace placeholders." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. code-block:: shell $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. code-block:: shell $ PRE-|a|-POST ''' [[cases]] id = "no_substitution_hyperlink_target" description = "Leave placeholders in hyperlink targets unchanged by default." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |ver| replace:: 0.8.5 Download the tarball_ .. _tarball: https://example.com/releases/v|ver|.tar.gz ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. |ver| replace:: 0.8.5 Download the tarball_ .. _tarball: https://example.com/releases/v|ver|.tar.gz ''' [[cases]] id = "no_substitution_image" description = "The ``image`` directive does not replace custom placeholders by\ndefault.\n\nNote: reST by default processes |substitutions| in image paths, but\nour extension adds the ability to use custom delimiters like {{var}}." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test_image .. image:: test_image.png ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test_image .. image:: test_image.png ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "no_substitution_literal_include" description = "The ``literalinclude`` directive does not replace placeholders." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt ''' [[cases]] id = "rst_substitution_key_with_dot_does_not_raise_error" description = "Substitution names with dots do not raise SphinxError in reST.\n\nThe dot validation applies only to flattened MyST substitutions, so\na refactoring that leaked it into the reST path would be caught here." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |key.with.dots| replace:: example_substitution .. code-block:: shell :substitutions: $ PRE-|key.with.dots|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-example_substitution-POST ''' [[cases]] id = "substitution_code_block" description = "The ``code-block`` directive replaces the placeholders defined in\n``conf.py`` as specified." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. code-block:: shell :substitutions: $ PRE-|a|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-example_substitution-POST ''' [[cases]] id = "substitution_code_block_case_preserving" description = "The ``code-block`` directive respects the original case of\nreplacements." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |aBcD_eFgH| replace:: example_substitution .. code-block:: shell :substitutions: $ PRE-|aBcD_eFgH|-POST ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. code-block:: shell $ PRE-example_substitution-POST ''' [[cases]] id = "substitution_download" description = "The ``substitution-download`` role replaces the placeholders defined\nin\n``conf.py`` as specified in both the download text and the download\ntarget." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution :substitution-download:`txt_pre-|a|-txt_post ` ''' "tgt_pre-example_substitution-tgt_post .py" = ''' Sample''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' :download:`txt_pre-example_substitution-txt_post ` ''' "tgt_pre-example_substitution-tgt_post .py" = ''' Sample''' [[cases]] id = "substitution_hyperlink_target" description = "Replace placeholders in external hyperlink targets." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "rst_prolog" = ".. |ver| replace:: 0.8.5" "substitutions_hyperlink_targets_enabled" = true [cases.actual.files] "conf.py" = "" "index.rst" = ''' Download the tarball_ .. _tarball: https://example.com/releases/v|ver|.tar.gz See the `internal section`_. Internal section ---------------- ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' Download the tarball_ .. _tarball: https://example.com/releases/v0.8.5.tar.gz See the `internal section`_. Internal section ---------------- ''' [[cases]] id = "substitution_image_path" description = "The ``image`` directive replaces placeholders in the file path when\nthe\n``:path-substitutions:`` flag is set." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test_image .. image:: |a|.png :path-substitutions: ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. image:: test_image.png ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "substitution_image_path_multiple" description = "The ``image`` directive replaces multiple placeholders in the file\npath." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test .. |b| replace:: image .. image:: pre_|a|_mid_|b|_post.png :path-substitutions: ''' [cases.actual.binary_files] "pre_test_mid_image_post.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. image:: pre_test_mid_image_post.png ''' [cases.expected.binary_files] "pre_test_mid_image_post.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "substitution_image_with_options" description = "The ``image`` directive works with standard image options." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: test_image .. image:: |a|.png :path-substitutions: :alt: Test image alt text :width: 100px ''' [cases.actual.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. image:: test_image.png :alt: Test image alt text :width: 100px ''' [cases.expected.binary_files] "test_image.png" = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" [[cases]] id = "substitution_include_content" description = "Replace placeholders in included source content when requested." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |SRC_VERSION| replace:: 4.0 .. |NEW_VERSION| replace:: 4.1 .. include:: upgrade-guide.txt :content-substitutions: ''' "upgrade-guide.txt" = ''' .. code-block:: shell upgrade --from |SRC_VERSION| --to |NEW_VERSION| ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. include:: upgrade-guide.txt ''' "upgrade-guide.txt" = ''' .. code-block:: shell upgrade --from 4.0 --to 4.1 ''' [[cases]] id = "substitution_include_content_does_not_leak_to_nested_includes" description = "Apply content substitutions only to the include that requests\nthem." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |name| replace:: example .. include:: outer.txt :content-substitutions: ''' "nested-default.txt" = ''' .. code-block:: text Default nested content with |name| placeholder ''' "nested-disabled.txt" = ''' .. code-block:: text Disabled nested content with |name| placeholder ''' "outer.txt" = ''' .. include:: nested-default.txt .. include:: nested-disabled.txt :nocontent-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. include:: outer.txt ''' "nested-default.txt" = ''' .. code-block:: text Default nested content with |name| placeholder ''' "nested-disabled.txt" = ''' .. code-block:: text Disabled nested content with |name| placeholder ''' "outer.txt" = ''' .. include:: nested-default.txt .. include:: nested-disabled.txt ''' [[cases]] id = "substitution_include_literal_content" description = "Replace placeholders when ``include`` returns a literal node." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |name| placeholder''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt :literal: :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example placeholder''' "index.rst" = ''' .. include:: example.txt :literal: ''' [[cases]] id = "substitution_include_literal_content_with_include_read" description = "Replace placeholders in returned literal nodes with an include-read listener." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = ''' def on_include_read(app, relative_path, parent_docname, content): pass def setup(app): app.connect("include-read", on_include_read) ''' "example.txt" = ''' Content with |name| placeholder''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt :literal: :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example placeholder''' "index.rst" = ''' .. include:: example.txt :literal: ''' [[cases]] id = "substitution_include_code_content_with_include_read" description = "Replace placeholders in returned code nodes with an include-read listener." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = ''' def on_include_read(app, relative_path, parent_docname, content): pass def setup(app): app.connect("include-read", on_include_read) ''' "example.txt" = ''' Content with |name| placeholder''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt :code: text :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example placeholder''' "index.rst" = ''' .. include:: example.txt :code: text ''' [[cases]] id = "substitution_include_parser_content_with_include_read" description = "Replace placeholders in returned parser nodes with an include-read listener." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = ''' def on_include_read(app, relative_path, parent_docname, content): pass def setup(app): app.connect("include-read", on_include_read) ''' "example.txt" = ''' Content with |name| placeholder''' "index.rst" = ''' .. |name| replace:: example .. include:: example.txt :parser: rst :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example placeholder''' "index.rst" = ''' .. include:: example.txt :parser: rst ''' [[cases]] id = "substitution_parser_include_with_nested_include_read" description = "Apply substitutions only to outer parser nodes when a nested include emits include-read." output = "guide/index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] "root_doc" = "guide/index" [cases.actual.files] "conf.py" = ''' def on_include_read(app, relative_path, parent_docname, content): pass def setup(app): app.connect("include-read", on_include_read) ''' "guide/index.rst" = ''' .. |name| replace:: example .. include:: outer.txt :parser: rst :content-substitutions: ''' "guide/nested.txt" = ''' .. code-block:: text Nested |name| content ''' "guide/outer.txt" = ''' Outer |name| content .. include:: nested.txt ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] "root_doc" = "guide/index" [cases.expected.files] "conf.py" = "" "guide/index.rst" = ''' .. include:: outer.txt :parser: rst ''' "guide/nested.txt" = ''' .. code-block:: text Nested |name| content ''' "guide/outer.txt" = ''' Outer example content .. include:: nested.txt ''' [[cases]] id = "substitution_include_path_and_content" description = "Replace placeholders in an include path and its source content." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |document| replace:: upgrade-guide .. |version| replace:: 4.1 .. include:: |document|.txt :path-substitutions: :content-substitutions: ''' "upgrade-guide.txt" = ''' .. code-block:: text Upgrade to |version|. ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' .. include:: upgrade-guide.txt ''' "upgrade-guide.txt" = ''' .. code-block:: text Upgrade to 4.1. ''' [[cases]] id = "substitution_inline" description = "The ``substitution-code`` role replaces the placeholders defined in\n``conf.py`` as specified." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |a| replace:: example_substitution Example :substitution-code:`PRE-|a|-POST` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' Example :code:`PRE-example_substitution-POST` ''' [[cases]] id = "substitution_inline_case_preserving" description = "The ``substitution-code`` role respects the original case of\nreplacements." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "index.rst" = ''' .. |aBcD_eFgH| replace:: example_substitution Example :substitution-code:`PRE-|aBcD_eFgH|-POST` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "index.rst" = ''' Example :code:`PRE-example_substitution-POST` ''' [[cases]] id = "substitution_literal_include" description = "The ``literalinclude`` directive replaces the placeholders defined\nin\n``conf.py`` as specified when the `:content-substitutions:` flag is\nset." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example_substitution placeholder''' "index.rst" = ''' .. literalinclude:: example.txt ''' [[cases]] id = "substitution_literal_include_both_path_and_content" description = "The ``literalinclude`` directive can use both path and content\nsubstitutions at the same time." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example_substitution.txt" = ''' Content with |b| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. |b| replace:: test_value .. literalinclude:: |a|.txt :path-substitutions: :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example_substitution.txt" = ''' Content with test_value placeholder''' "index.rst" = ''' .. literalinclude:: example_substitution.txt ''' [[cases]] id = "substitution_literal_include_empty_file" description = "The ``literalinclude`` directive handles empty files without\ncrashing." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "empty.txt" = "" "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: empty.txt :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "empty.txt" = "" "index.rst" = ''' .. literalinclude:: empty.txt ''' [[cases]] id = "substitution_literal_include_multiple" description = "The ``literalinclude`` directive replaces multiple placeholders." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example.txt" = ''' PRE-|a|-MID-|b|-POST''' "index.rst" = ''' .. |a| replace:: first_substitution .. |b| replace:: second_substitution .. literalinclude:: example.txt :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' PRE-first_substitution-MID-second_substitution-POST''' "index.rst" = ''' .. literalinclude:: example.txt ''' [[cases]] id = "substitution_literal_include_path" description = "The ``literalinclude`` directive replaces placeholders in the file\npath\nwhen the `:path-substitutions:` flag is set." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example_substitution.txt" = ''' File content''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: |a|.txt :path-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example_substitution.txt" = ''' File content''' "index.rst" = ''' .. literalinclude:: example_substitution.txt ''' [[cases]] id = "substitution_literal_include_with_caption" description = "The ``literalinclude`` directive works with captions." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = "" "example.txt" = ''' Content with |a| placeholder''' "index.rst" = ''' .. |a| replace:: example_substitution .. literalinclude:: example.txt :caption: Example caption :content-substitutions: ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = "" "example.txt" = ''' Content with example_substitution placeholder''' "index.rst" = ''' .. literalinclude:: example.txt :caption: Example caption ''' [[cases]] id = "xref_role_class_prefix_removal" description = "The ``SubstitutionXRefRole`` should only remove the \"substitution-\"\nprefix\nfrom CSS classes, not all occurrences of \"substitution-\" in the class\nname." output = "index.html" [cases.actual] exception_on_warning = true [cases.actual.confoverrides] "extensions" = ["sphinx_substitution_extensions"] [cases.actual.files] "conf.py" = ''' from sphinx import addnodes from sphinx_substitution_extensions import SubstitutionXRefRole def setup(app): role = SubstitutionXRefRole( nodeclass=addnodes.download_reference, ) app.add_role("substitution-my-substitution-download", role) ''' "example.py" = ''' Sample''' "index.rst" = ''' :substitution-my-substitution-download:`Download ` ''' [cases.expected] exception_on_warning = true [cases.expected.confoverrides] [cases.expected.files] "conf.py" = ''' from sphinx import addnodes from sphinx.roles import XRefRole def setup(app): role = XRefRole(nodeclass=addnodes.download_reference) app.add_role("my-substitution-download", role) ''' "example.py" = ''' Sample''' "index.rst" = ''' :my-substitution-download:`Download ` ''' adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/test_equivalence_cases.py000066400000000000000000000267431526221261600314360ustar00rootroot00000000000000"""Run declarative Sphinx build-equivalence cases.""" import base64 import re import tomllib from collections.abc import Callable from dataclasses import dataclass from pathlib import Path, PurePosixPath import pytest from beartype.door import TypeHint from sphinx.testing.util import SphinxTestApp from typing_extensions import TypeIs @dataclass(frozen=True, slots=True) class Build: """Inputs for one side of an equivalence case.""" files: dict[str, str] binary_files: dict[str, str] confoverrides: dict[str, object] exception_on_warning: bool @dataclass(frozen=True, slots=True) class Case: """Two Sphinx builds whose selected HTML output must match.""" id: str description: str output: str actual: Build expected: Build def _is_mapping(value: object, /) -> TypeIs[dict[str, object]]: """Return whether a parser value is a string-keyed mapping.""" return TypeHint(hint=dict[str, object]).is_bearable(obj=value) def _is_list(value: object, /) -> TypeIs[list[object]]: """Return whether a parser value is a list.""" return isinstance(value, list) def _mapping(value: object, *, context: str) -> dict[str, object]: """Validate and narrow a TOML table.""" if not _is_mapping(value): msg = f"{context} must be a table" raise TypeError(msg) return value def _string(value: object, *, context: str) -> str: """Validate and narrow a TOML string.""" if not isinstance(value, str): msg = f"{context} must be a string" raise TypeError(msg) return value def _string_mapping(value: object, *, context: str) -> dict[str, str]: """Validate and narrow a table whose values are strings.""" table = _mapping(value=value, context=context) return { key: _string(value=item, context=f"{context}.{key}") for key, item in table.items() } def _check_keys(data: dict[str, object], *, context: str) -> None: """Reject unknown keys left after parsing a table.""" if len(data) > 0: unknown_keys = ", ".join(sorted(data)) msg = f"Unknown keys in {context}: {unknown_keys}" raise ValueError(msg) def _parse_build(value: object, *, context: str) -> Build: """Parse one build table.""" data = _mapping(value=value, context=context) files = _string_mapping( value=data.pop("files", {}), context=f"{context}.files", ) binary_files = _string_mapping( value=data.pop("binary_files", {}), context=f"{context}.binary_files", ) confoverrides = _mapping( value=data.pop("confoverrides", {}), context=f"{context}.confoverrides", ) exception_on_warning = data.pop("exception_on_warning", False) if not isinstance(exception_on_warning, bool): msg = f"{context}.exception_on_warning must be a boolean" raise TypeError(msg) _check_keys(data=data, context=context) return Build( files=files, binary_files=binary_files, confoverrides=confoverrides, exception_on_warning=exception_on_warning, ) def _parse_case(value: object, *, index: int) -> Case: """Parse one equivalence case table.""" context = f"cases[{index}]" data = _mapping(value=value, context=context) case = Case( id=_string(value=data.pop("id", None), context=f"{context}.id"), description=_string( value=data.pop("description", None), context=f"{context}.description", ), output=_string( value=data.pop("output", None), context=f"{context}.output", ), actual=_parse_build( value=data.pop("actual", None), context=f"{context}.actual", ), expected=_parse_build( value=data.pop("expected", None), context=f"{context}.expected", ), ) _check_keys(data=data, context=context) return case def _load_cases(*, cases_path: Path) -> list[Case]: """Load and validate all equivalence cases.""" with cases_path.open(mode="rb") as cases_file: data = _mapping(value=tomllib.load(cases_file), context="root") schema_version = data.pop("schema_version", None) if schema_version != 1: msg = f"Unsupported schema version: {schema_version!r}" raise ValueError(msg) raw_cases = data.pop("cases", None) if not _is_list(raw_cases): msg = "cases must be an array of tables" raise TypeError(msg) _check_keys(data=data, context="root") cases = [ _parse_case(value=raw_case, index=index) for index, raw_case in enumerate(iterable=raw_cases) ] ids = [case.id for case in cases] if len(ids) != len(set(ids)): msg = "Equivalence case IDs must be unique" raise ValueError(msg) return cases def _destination(*, source_directory: Path, relative_path: str) -> Path: """Resolve and validate a case file path.""" path = PurePosixPath(relative_path) if path.is_absolute() or ".." in path.parts: msg = ( "Case file path must stay within its source directory: " f"{relative_path}" ) raise ValueError(msg) return source_directory.joinpath(*path.parts) def _write_build(*, source_directory: Path, build: Build) -> None: """Materialize one build's files.""" overlap = build.files.keys() & build.binary_files.keys() if len(overlap) > 0: msg = f"Files cannot be both text and binary: {sorted(overlap)}" raise ValueError(msg) for relative_path, content in build.files.items(): destination = _destination( source_directory=source_directory, relative_path=relative_path, ) destination.parent.mkdir(parents=True, exist_ok=True) _ = destination.write_text(data=content) for relative_path, content in build.binary_files.items(): destination = _destination( source_directory=source_directory, relative_path=relative_path, ) destination.parent.mkdir(parents=True, exist_ok=True) _ = destination.write_bytes( data=base64.b64decode(s=content, validate=True), ) def _build_html( *, source_directory: Path, build: Build, output: str, make_app: Callable[..., SphinxTestApp], ) -> str: """Build one side and return its selected HTML output.""" _write_build(source_directory=source_directory, build=build) app = make_app( srcdir=source_directory, exception_on_warning=build.exception_on_warning, confoverrides=build.confoverrides, ) app.build() assert app.statuscode == 0 content = (app.outdir / output).read_text() app.cleanup() return content @pytest.mark.parametrize( argnames="case", argvalues=_load_cases( cases_path=Path(__file__).with_name(name="equivalence_cases.toml"), ), ids=lambda case: case.id, ) def test_equivalent_builds( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], case: Case, ) -> None: """Match an extension build to its hand-expanded equivalent.""" actual_html = _build_html( source_directory=tmp_path / "actual", build=case.actual, output=case.output, make_app=make_app, ) expected_html = _build_html( source_directory=tmp_path / "expected", build=case.expected, output=case.output, make_app=make_app, ) assert actual_html == expected_html, case.description @pytest.mark.parametrize( argnames=("manifest", "error", "message"), argvalues=[ ( "schema_version = 2\ncases = []\n", ValueError, "Unsupported schema version", ), ( "schema_version = 1\ncases = {}\n", TypeError, "cases must be an array", ), ( "schema_version = 1\ncases = []\nextra = 1\n", ValueError, "Unknown keys in root", ), ( "schema_version = 1\ncases = [1]\n", TypeError, "cases[0] must be a table", ), ( ( 'schema_version = 1\ncases = [{id = 1, description = "", ' 'output = "", actual = {}, expected = {}}]\n' ), TypeError, "cases[0].id must be a string", ), ( ( 'schema_version = 1\ncases = [{id = "one", description = "", ' 'output = "", actual = {files = 1}, expected = {}}]\n' ), TypeError, "cases[0].actual.files must be a table", ), ( ( 'schema_version = 1\ncases = [{id = "one", description = "", ' 'output = "", actual = {files = {index = 1}}, ' "expected = {}}]\n" ), TypeError, "cases[0].actual.files.index must be a string", ), ( ( 'schema_version = 1\ncases = [{id = "one", description = "", ' 'output = "", actual = {exception_on_warning = "yes"}, ' "expected = {}}]\n" ), TypeError, "cases[0].actual.exception_on_warning must be a boolean", ), ( ( 'schema_version = 1\ncases = [{id = "one", description = "", ' 'output = "", actual = {}, expected = {}, extra = 1}]\n' ), ValueError, "Unknown keys in cases[0]", ), ( ( 'schema_version = 1\ncases = [{id = "one", description = "", ' 'output = "", actual = {}, expected = {}}, ' '{id = "one", description = "", output = "", actual = {}, ' "expected = {}}]\n" ), ValueError, "Equivalence case IDs must be unique", ), ], ids=[ "schema-version", "cases-type", "unknown-root-key", "case-type", "case-id-type", "files-type", "file-content-type", "warning-setting-type", "unknown-case-key", "duplicate-case-id", ], ) def test_invalid_manifest( *, tmp_path: Path, manifest: str, error: type[Exception], message: str, ) -> None: """Reject malformed equivalence cases with a useful error.""" cases_path = tmp_path / "equivalence_cases.toml" _ = cases_path.write_text(data=manifest) with pytest.raises( expected_exception=error, match=re.escape(pattern=message), ): _ = _load_cases(cases_path=cases_path) @pytest.mark.parametrize( argnames="relative_path", argvalues=["/escape", "../escape"], ) def test_destination_rejects_unsafe_path( *, tmp_path: Path, relative_path: str, ) -> None: """Keep generated files inside their build directory.""" with pytest.raises( expected_exception=ValueError, match="must stay within its source directory", ): _ = _destination( source_directory=tmp_path, relative_path=relative_path, ) def test_write_build_rejects_overlapping_files(tmp_path: Path) -> None: """Reject a file declared as both text and binary.""" build = Build( files={"index.rst": "text"}, binary_files={"index.rst": "dGV4dA=="}, confoverrides={}, exception_on_warning=False, ) with pytest.raises( expected_exception=ValueError, match="Files cannot be both text and binary", ): _write_build(source_directory=tmp_path, build=build) adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/test_myst.py000066400000000000000000000142521526221261600267430ustar00rootroot00000000000000"""Tests for MyST documents.""" from collections.abc import Callable from pathlib import Path from textwrap import dedent import pytest from beartype.roar import BeartypeCallHintParamViolation from sphinx.errors import SphinxError from sphinx.testing.util import SphinxTestApp def test_myst_unsupported_substitution_value_raises_error( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Reject unsupported substitution values.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "index.md").write_text( data=dedent( text="""\ # Title ```{code-block} :substitutions: |unsupported| ``` """, ), ) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": {"unsupported": None}, }, ) with pytest.raises( expected_exception=BeartypeCallHintParamViolation, match="substitutions", ): app.build() def test_myst_invalid_substitution_access( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """MyST invalid substitution access does not break the build.""" 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-|items.99|-POST ``` ```{code-block} :substitutions: $ PRE-|nonexistent.key|-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=False, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_substitutions": { "items": ["a", "b"], }, }, ) app.build() assert app.statuscode == 0 content_html = (app.outdir / "markdown_document.html").read_text() app.cleanup() expected_text_in_html = [ "$ PRE-|items.99|-POST", "$ PRE-|nonexistent.key|-POST", ] for text in expected_text_in_html: assert text in content_html def test_myst_substitution_key_with_dot_raises_error( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """MyST substitution keys containing dots raise SphinxError. Dots are reserved for nested access notation. """ 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: |key.with.dots| ``` """, ) _ = 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": { "key.with.dots": "value", }, }, ) with pytest.raises( expected_exception=SphinxError, match=r"Substitution key 'key\.with\.dots' contains a dot", ): app.build() def test_myst_nested_substitution_key_with_dot_raises_error( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """MyST nested substitution keys containing dots raise SphinxError. Dots are reserved for nested access notation. """ 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: |parent.key.with.dots| ``` """, ) _ = 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": { "parent": { "key.with.dots": "value", }, }, }, ) with pytest.raises( expected_exception=SphinxError, match=r"Substitution key 'key\.with\.dots' contains a dot", ): app.build() adamtheturtle-sphinx-substitution-extensions-219c7b1/tests/test_substitution_extensions.py000066400000000000000000000224411526221261600330010ustar00rootroot00000000000000"""Tests for Sphinx extensions.""" from collections.abc import Callable from importlib.metadata import version from pathlib import Path from textwrap import dedent from docutils import core from docutils.parsers.rst import directives from sphinx.application import Sphinx 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_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_no_substitution_include( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Leave an ``include`` path unchanged by default.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "example.txt").write_text(data="Included content") _ = (source_directory / "index.rst").write_text( data=".. include:: example.txt\n", ) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 assert "Included content" in (app.outdir / "index.html").read_text() def test_include_fragment_is_not_reported_as_unreferenced( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Register included documents with the Sphinx environment.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "fragment.rst").write_text(data="Included content") _ = (source_directory / "index.rst").write_text( data=".. include:: fragment.rst\n", ) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 def test_include_without_sphinx_environment(tmp_path: Path) -> None: """Support documents which have no Sphinx environment.""" source_file = tmp_path / "index.rst" _ = source_file.write_text(data=".. include:: included.rst\n") _ = (tmp_path / "included.rst").write_text(data="Included content") directives.register_directive( name="include", directive=sphinx_substitution_extensions.SubstitutionInclude, ) document = core.publish_doctree( source=source_file.read_text(), source_path=source_file.as_posix(), settings_overrides={"env": None}, ) assert "Included content" in document.astext() def test_substitution_include_path( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Replace placeholders in an ``include`` path when requested.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "example.txt").write_text(data="Included content") _ = (source_directory / "index.rst").write_text( data=dedent( text="""\ .. |name| replace:: example .. include:: |name|.txt :path-substitutions: """, ), ) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) app.build() assert app.statuscode == 0 assert "Included content" in (app.outdir / "index.html").read_text() def test_include_read_event_with_content_substitutions( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Emit include-read and then substitute the listener's content.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() include_file = source_directory / "example.txt" _ = include_file.write_text( data="Included |name| content", ) source_file = source_directory / "index.rst" _ = source_file.write_text( data=dedent( text="""\ .. |name| replace:: original .. include:: example.txt :content-substitutions: """, ), ) observed_content: list[str] = [] def on_include_read( _app: Sphinx, _relative_path: Path, _parent_docname: str, content: list[str], ) -> None: """Record and transform the included source.""" observed_content.append(content[0]) content[0] = content[0].replace("Included", "Observed") app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={"extensions": ["sphinx_substitution_extensions"]}, ) _ = app.connect(event="include-read", callback=on_include_read) app.build() assert observed_content == ["Included |name| content"] content_html = (app.outdir / "index.html").read_text() app.cleanup() _ = include_file.write_text(data="Observed original content") _ = source_file.write_text(data=".. include:: example.txt\n") 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_substitution_include_path( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Replace ``include`` path placeholders when defaults are enabled.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "example.txt").write_text(data="Included content") _ = (source_directory / "index.rst").write_text( data=dedent( text="""\ .. |name| replace:: example .. include:: |name|.txt """, ), ) 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 assert "Included content" in (app.outdir / "index.html").read_text() def test_default_substitution_include_disabled( *, tmp_path: Path, make_app: Callable[..., SphinxTestApp], ) -> None: """Respect ``:nopath-substitutions:`` when defaults are enabled.""" source_directory = tmp_path / "source" source_directory.mkdir() (source_directory / "conf.py").touch() _ = (source_directory / "[[name]].txt").write_text(data="Included content") _ = (source_directory / "index.rst").write_text( data=dedent( text="""\ .. toctree:: document """, ), ) _ = (source_directory / "document.md").write_text( data=dedent( text="""\ # Document ```{include} [[name]].txt :nopath-substitutions: ``` """, ), ) app = make_app( srcdir=source_directory, exception_on_warning=True, confoverrides={ "extensions": [ "myst_parser", "sphinx_substitution_extensions", ], "myst_enable_extensions": ["substitution"], "myst_sub_delimiters": ("[", "]"), "myst_substitutions": {"name": "example"}, "substitutions_default_enabled": True, }, ) app.build() assert app.statuscode == 0 assert "Included content" in (app.outdir / "document.html").read_text() adamtheturtle-sphinx-substitution-extensions-219c7b1/zizmor.yml000066400000000000000000000003051526221261600252430ustar00rootroot00000000000000--- rules: unpinned-uses: disable: true cache-poisoning: disable: true dependabot-cooldown: disable: true superfluous-actions: disable: true artipacked: disable: true