pax_global_header00006660000000000000000000000064150130772540014516gustar00rootroot0000000000000052 comment=ca304f019fc0e79a604074d37699b55f77df4ae9 mkdocs-autorefs-1.4.2/000077500000000000000000000000001501307725400146305ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/.copier-answers.yml000066400000000000000000000012101501307725400203640ustar00rootroot00000000000000# Changes here will be overwritten by Copier. _commit: 1.8.4 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothée Mazzucotelli author_username: pawamoy copyright_date: '2019' copyright_holder: Timothée Mazzucotelli copyright_holder_email: dev@pawamoy.fr copyright_license: ISC insiders: false project_description: Automatically link across pages in MkDocs. project_name: mkdocs-autorefs python_package_command_line_name: '' python_package_distribution_name: mkdocs-autorefs python_package_import_name: mkdocs_autorefs repository_name: autorefs repository_namespace: mkdocstrings repository_provider: github.com mkdocs-autorefs-1.4.2/.envrc000066400000000000000000000000211501307725400157370ustar00rootroot00000000000000PATH_add scripts mkdocs-autorefs-1.4.2/.github/000077500000000000000000000000001501307725400161705ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/.github/FUNDING.yml000066400000000000000000000000371501307725400200050ustar00rootroot00000000000000github: pawamoy polar: pawamoy mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001501307725400203535ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/1-bug.md000066400000000000000000000027251501307725400216160ustar00rootroot00000000000000--- name: Bug report about: Create a bug report to help us improve. title: "bug: " labels: unconfirmed assignees: [pawamoy] --- ### Description of the bug ### To Reproduce ``` WRITE MRE / INSTRUCTIONS HERE ``` ### Full traceback
Full traceback ```python PASTE TRACEBACK HERE ```
### Expected behavior ### Environment information ```bash python -m mkdocs_autorefs._internal.debug # | xclip -selection clipboard ``` PASTE MARKDOWN OUTPUT HERE ### Additional context mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/2-feature.md000066400000000000000000000012131501307725400224640ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project. title: "feature: " labels: feature assignees: pawamoy --- ### Is your feature request related to a problem? Please describe. ### Describe the solution you'd like ### Describe alternatives you've considered ### Additional context mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/3-docs.md000066400000000000000000000011311501307725400217610ustar00rootroot00000000000000--- name: Documentation update about: Point at unclear, missing or outdated documentation. title: "docs: " labels: docs assignees: pawamoy --- ### Is something unclear, missing or outdated in our documentation? ### Relevant code snippets ### Link to the relevant documentation section mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/4-change.md000066400000000000000000000011261501307725400222630ustar00rootroot00000000000000--- name: Change request about: Suggest any other kind of change for this project. title: "change: " assignees: pawamoy --- ### Is your change request related to a problem? Please describe. ### Describe the solution you'd like ### Describe alternatives you've considered ### Additional context mkdocs-autorefs-1.4.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003321501307725400223410ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: I have a question / I need help url: https://github.com/mkdocstrings/autorefs/discussions/new?category=q-a about: Ask and answer questions in the Discussions tab. mkdocs-autorefs-1.4.2/.github/workflows/000077500000000000000000000000001501307725400202255ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/.github/workflows/ci.yml000066400000000000000000000046561501307725400213560ustar00rootroot00000000000000name: ci on: push: pull_request: branches: - main defaults: run: shell: bash env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 PYTHON_VERSIONS: "" jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Setup uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml - name: Install dependencies run: make setup - name: Check if the documentation builds correctly run: make check-docs - name: Check the code quality run: make check-quality - name: Check if the code is correctly typed run: make check-types - name: Check for breaking changes in the API run: make check-api - name: Store objects inventory for tests uses: actions/upload-artifact@v4 with: name: objects.inv path: site/objects.inv tests: needs: - quality strategy: matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" resolution: - highest - lowest-direct exclude: - os: macos-latest resolution: lowest-direct - os: windows-latest resolution: lowest-direct runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.14' }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Setup uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml cache-suffix: ${{ matrix.resolution }} - name: Install dependencies env: UV_RESOLUTION: ${{ matrix.resolution }} run: make setup - name: Download objects inventory uses: actions/download-artifact@v4 with: name: objects.inv path: site/ - name: Run the test suite run: make test mkdocs-autorefs-1.4.2/.github/workflows/release.yml000066400000000000000000000012141501307725400223660ustar00rootroot00000000000000name: release on: push permissions: contents: write jobs: release: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Setup uv uses: astral-sh/setup-uv@v5 - name: Prepare release notes run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release uses: softprops/action-gh-release@v2 with: body_path: release-notes.md mkdocs-autorefs-1.4.2/.gitignore000066400000000000000000000003311501307725400166150ustar00rootroot00000000000000# editors .idea/ .vscode/ # python *.egg-info/ *.py[cod] .venv/ .venvs/ /build/ /dist/ # tools .coverage* /.pdm-build/ /htmlcov/ /site/ uv.lock # cache .cache/ .pytest_cache/ .mypy_cache/ .ruff_cache/ __pycache__/ mkdocs-autorefs-1.4.2/CHANGELOG.md000066400000000000000000000400111501307725400164350ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [1.4.2](https://github.com/mkdocstrings/autorefs/releases/tag/1.4.2) - 2025-05-20 [Compare with 1.4.1](https://github.com/mkdocstrings/autorefs/compare/1.4.1...1.4.2) ### Build - Exclude mypy cache from dists ([5e77f7f](https://github.com/mkdocstrings/autorefs/commit/5e77f7fdbd79d4aca6473e9224270752ed9b9165) by Timothée Mazzucotelli). [Issue-71](https://github.com/mkdocstrings/autorefs/issues/71) ## [1.4.1](https://github.com/mkdocstrings/autorefs/releases/tag/1.4.1) - 2025-03-08 [Compare with 1.4.0](https://github.com/mkdocstrings/autorefs/compare/1.4.0...1.4.1) ### Code Refactoring - Store parent pages *and parent sections* in backlink breadcrumbs ([67955ce](https://github.com/mkdocstrings/autorefs/commit/67955ce5bf6b1b2cbea9c78b459e980f17bececc) by Timothée Mazzucotelli). - Ignore Markdown anchors when setting backlink metadata on autorefs ([3ac4797](https://github.com/mkdocstrings/autorefs/commit/3ac47979c0371ba53e623284c76bb29985ee7037) by Timothée Mazzucotelli). - Handle absence of `#` when computing relative URLs ([ca6461e](https://github.com/mkdocstrings/autorefs/commit/ca6461ebdb006897b012d1b92692ffffe9445ed2) by Timothée Mazzucotelli). ## [1.4.0](https://github.com/mkdocstrings/autorefs/releases/tag/1.4.0) - 2025-02-24 [Compare with 1.3.1](https://github.com/mkdocstrings/autorefs/compare/1.3.1...1.4.0) ### Features - Add backlinks feature ([5a3b387](https://github.com/mkdocstrings/autorefs/commit/5a3b38753c68cabd047fd062afba66417ccd124e) by Timothée Mazzucotelli). [PR-65](https://github.com/mkdocstrings/autorefs/pull/65), [Issue-mkdocstrings-723](https://github.com/mkdocstrings/mkdocstrings/issues/723), [Issue-mkdocstrings-python-153](https://github.com/mkdocstrings/python/issues/153) - Add `strip_title_tags` option ([00ce203](https://github.com/mkdocstrings/autorefs/commit/00ce2031a1a648c7d6f682ff7e94138c73957b20) and [b21aefd](https://github.com/mkdocstrings/autorefs/commit/b21aefd79b7f53c1b153be635cf4a8ccf1fcdb2f) by Timothée Mazzucotelli). [Issue-33](https://github.com/mkdocstrings/autorefs/issues/33) - Add `link_titles` option and adapt related logic ([e3b602e](https://github.com/mkdocstrings/autorefs/commit/e3b602e60a5836e3ef41433d5490202a9656f603) by Timothée Mazzucotelli). [Issue-33](https://github.com/mkdocstrings/autorefs/issues/33), [Issue-62](https://github.com/mkdocstrings/autorefs/issues/62) ### Code Refactoring - Move code to internal folder, expose public API in top-level module, document all public objects ([9615d13](https://github.com/mkdocstrings/autorefs/commit/9615d13e2f85640ebb1c6c055d41f068752884b2) by Timothée Mazzucotelli). - Store actual page instance instead of URL in plugin's `current_page` attribute ([8023588](https://github.com/mkdocstrings/autorefs/commit/8023588ee38dc86299010979b05873dfd6b5039a) and [2009f85](https://github.com/mkdocstrings/autorefs/commit/2009f85eb10abc14b35c74a969c84744ee1f98ed) by Timothée Mazzucotelli). - Use the `on_env` hook to fix cross-references ([70fec3e](https://github.com/mkdocstrings/autorefs/commit/70fec3e270e2d8f95213d63b8a99962b9d30569c) by Timothée Mazzucotelli). [Discussion-mkdocs-3917](https://github.com/mkdocs/mkdocs/discussions/3917) - Record heading titles alongside URLs ([791782e](https://github.com/mkdocstrings/autorefs/commit/791782eef8f85aad84c39bf4f82286613f055322) by Timothée Mazzucotelli). [Issue-33](https://github.com/mkdocstrings/autorefs/issues/33) ## [1.3.1](https://github.com/mkdocstrings/autorefs/releases/tag/1.3.1) - 2025-02-11 [Compare with 1.3.0](https://github.com/mkdocstrings/autorefs/compare/1.3.0...1.3.1) ### Bug Fixes - Always resolve secondary URLs to closest (don't log warnings) ([243ad35](https://github.com/mkdocstrings/autorefs/commit/243ad35f193b48216b531333e0d91ab2fa0a0db4) by Timothée Mazzucotelli). [Issue-52](https://github.com/mkdocstrings/autorefs/issues/52) ## [1.3.0](https://github.com/mkdocstrings/autorefs/releases/tag/1.3.0) - 2025-01-12 [Compare with 1.2.0](https://github.com/mkdocstrings/autorefs/compare/1.2.0...1.3.0) ### Build - Drop support for Python 3.8 ([ee3eaad](https://github.com/mkdocstrings/autorefs/commit/ee3eaadac59331b883f83c6cd9aa0ac4ea3707b5) by Timothée Mazzucotelli). ### Features - Handle inline references with markup within them ([54a02a7](https://github.com/mkdocstrings/autorefs/commit/54a02a7a61cdeaed0df3f98f49be4c36d07c0b8e) by Timothée Mazzucotelli). [Follow-up-of-issue-58](https://github.com/mkdocstrings/autorefs/issues/58) - Separate URLs in two groups, primary and secondary ([559c723](https://github.com/mkdocstrings/autorefs/commit/559c723203d3f73040b1005ab3762a4a7bf8e133) by Timothée Mazzucotelli). [Related-to-issue-61](https://github.com/mkdocstrings/autorefs/issues/61) ### Bug Fixes - Fallback to slugified title as id for non-exact, non-code references (`[Hello World][]` -> `[hello-world][]`) ([13428f1](https://github.com/mkdocstrings/autorefs/commit/13428f15d72d3fd473dcd16da94abcc3c32465e9) by Timothée Mazzucotelli). [Issue-58](https://github.com/mkdocstrings/autorefs/issues/58) ### Code Refactoring - Deprecate fallback mechanism ([5e89cd8](https://github.com/mkdocstrings/autorefs/commit/5e89cd89f56f611666d84f95c4de0e184aa98437) by Timothée Mazzucotelli). [Issue-61](https://github.com/mkdocstrings/autorefs/issues/61) - Log a debug message for unresolved optional references ([9e990d7](https://github.com/mkdocstrings/autorefs/commit/9e990d79a348f06c3d031c6759814acc32449e15) by Timothée Mazzucotelli). ## [1.2.0](https://github.com/mkdocstrings/autorefs/releases/tag/1.2.0) - 2024-09-01 [Compare with 1.1.0](https://github.com/mkdocstrings/autorefs/compare/1.1.0...1.2.0) ### Features - Provide hook interface, use it to expand identifiers, attach additional context to references, and give more context around unmapped identifiers ([fb8df98](https://github.com/mkdocstrings/autorefs/commit/fb8df98fc8f9fb1b3accb8a305cc90b3a3507d86) by Timothée Mazzucotelli). [Issue-54](https://github.com/mkdocstrings/autorefs/issues/54), [PR-mkdocstrings#666](https://github.com/mkdocstrings/mkdocstrings/pull/666) - Add option to resolve autorefs to closest URLs when multiple ones are found ([2916eb2](https://github.com/mkdocstrings/autorefs/commit/2916eb27dec89287dcaa1aefb4e9532156b66e30) by Timothée Mazzucotelli). [Issue-52](https://github.com/mkdocstrings/autorefs/issues/52) ### Bug Fixes - Don't ignore identifiers containing spaces and slashes ([b36a0d1](https://github.com/mkdocstrings/autorefs/commit/b36a0d1c4b0f5a6441ee6a2de7409942a8702bd8) by Timothée Mazzucotelli). [Issue-55](https://github.com/mkdocstrings/autorefs/issues/55) ### Code Refactoring - Emit deprecation warnings when old-style spans are found ([4f2be46](https://github.com/mkdocstrings/autorefs/commit/4f2be4633eec42c8e8582804741548a8e5602727) by Timothée Mazzucotelli). - Use `%s` formatting instead of f-strings in log messages ([0cedf9d](https://github.com/mkdocstrings/autorefs/commit/0cedf9d82ede8ba10dc8e100d7d1e5ce488fca34) by Timothée Mazzucotelli). ## [1.1.0](https://github.com/mkdocstrings/autorefs/releases/tag/1.1.0) - 2024-08-20 [Compare with 1.0.1](https://github.com/mkdocstrings/autorefs/compare/1.0.1...1.1.0) ### Deprecations - `AUTO_REF_RE` is renamed `AUTOREF_RE` (and updated for an improved version of `fix_refs`) - `AutoRefInlineProcessor` is renamed `AutorefsInlineProcessor` ### Features - Warn when multiple URLs are found for the same identifier ([c630354](https://github.com/mkdocstrings/autorefs/commit/c6303542018ca835f6941c070accb582f851f6b1) by Markus B). [Issue-35](https://github.com/mkdocstrings/autorefs/issues/35), [PR-50](https://github.com/mkdocstrings/autorefs/pull/50), Co-authored-by: Timothée Mazzucotelli ### Bug Fixes - Only log "Markdown anchors feature enabled" once ([1c9bda1](https://github.com/mkdocstrings/autorefs/commit/1c9bda1ab4f13c9a5cf5d202de755e5296729654) by Timothée Mazzucotelli). [Issue-44](https://github.com/mkdocstrings/autorefs/issues/44) ### Code Refactoring - Use a custom autoref HTML tag ([e142023](https://github.com/mkdocstrings/autorefs/commit/e14202317dc13dd5eed93b5d7cfd183c87de893f) by Timothée Mazzucotelli). [PR-48](https://github.com/mkdocstrings/autorefs/pull/48) - Rename AutoRefInlineProcessor to AutorefsInlineProcessor ([ffcaa01](https://github.com/mkdocstrings/autorefs/commit/ffcaa0178b642e423acdc66d35f1e6b207099dc7) by Timothée Mazzucotelli). - Attach name to processors for easier retrieval ([036b825](https://github.com/mkdocstrings/autorefs/commit/036b825c7994b2586564e8707fbc0b3627c29569) by Timothée Mazzucotelli). ## [1.0.1](https://github.com/mkdocstrings/autorefs/releases/tag/1.0.1) - 2024-02-29 [Compare with 1.0.0](https://github.com/mkdocstrings/autorefs/compare/1.0.0...1.0.1) ### Bug Fixes - Don't import `MkDocsConfig` (does not exist on MkDocs 1.3-) ([9c15664](https://github.com/mkdocstrings/autorefs/commit/9c156643ead1dc24f08b8047bd5b2fcd97662783) by Timothée Mazzucotelli). ## [1.0.0](https://github.com/mkdocstrings/autorefs/releases/tag/1.0.0) - 2024-02-27 [Compare with 0.5.0](https://github.com/mkdocstrings/autorefs/compare/0.5.0...1.0.0) ### Features - Add Markdown anchors and aliases ([a215a97](https://github.com/mkdocstrings/autorefs/commit/a215a97a057b54e11ebec8865c64e93429edde63) by Timothée Mazzucotelli). [Replaces-PR-#20](https://github.com/mkdocstrings/autorefs/pull/20), [Related-to-PR-#25](https://github.com/mkdocstrings/autorefs/pull/25), [Related-to-issue-#35](https://github.com/mkdocstrings/autorefs/issues/35), Co-authored-by: Oleh Prypin , Co-authored-by: tvdboom - Preserve HTML data attributes (from spans to anchors) ([0c1781d](https://github.com/mkdocstrings/autorefs/commit/0c1781d7e3d6bffd55802868802bcd1ec9e8bbc7) by Timothée Mazzucotelli). [Issue-#41](https://github.com/mkdocstrings/autorefs/issues/41), [PR-#42](https://github.com/mkdocstrings/autorefs/pull/42), Co-authored-by: Oleh Prypin - Support ``[`identifier`][]`` with pymdownx.inlinehilite enabled ([e7f2228](https://github.com/mkdocstrings/autorefs/commit/e7f222894c70627c70e6a14e453a10a81e3f8957) by Oleh Prypin). [Issue-#34](https://github.com/mkdocstrings/autorefs/issues/34), [PR-#40](https://github.com/mkdocstrings/autorefs/pull/40), Co-authored-by: Timothée Mazzucotelli ### Bug Fixes - Recognize links with multi-line text ([225a6f2](https://github.com/mkdocstrings/autorefs/commit/225a6f275069bcdfb3411e80d4a7fa645b857b88) by Oleh Prypin). [Issue #31](https://github.com/mkdocstrings/autorefs/issues/31), [PR #32](https://github.com/mkdocstrings/autorefs/pull/32) ## [0.5.0](https://github.com/mkdocstrings/autorefs/releases/tag/0.5.0) - 2023-08-02 [Compare with 0.4.1](https://github.com/mkdocstrings/autorefs/compare/0.4.1...0.5.0) ### Breaking Changes - Drop support for Python 3.7 ### Build - Migrate to pdm-backend ([48b92fb](https://github.com/mkdocstrings/autorefs/commit/48b92fb2c12e97242007e5fbbc1b18a36b7f29b6) by Michał Górny). ### Bug Fixes - Stop using deprecated `warning_filter` ([7721103](https://github.com/mkdocstrings/autorefs/commit/77211035bb10b8e55f595eb7d0392344669ffdec) by Kyle King). [PR #30](https://github.com/mkdocstrings/autorefs/pull/30) ### Code Refactoring - Use new MkDocs plugin logger if available ([ca8d758](https://github.com/mkdocstrings/autorefs/commit/ca8d75805ac289e9a5a8123565aa7833b34bd214) by Timothée Mazzucotelli). ## [0.4.1](https://github.com/mkdocstrings/autorefs/releases/tag/0.4.1) - 2022-03-07 [Compare with 0.4.0](https://github.com/mkdocstrings/autorefs/compare/0.4.0...0.4.1) ### Bug Fixes - Fix packaging (missing `__init__` module) ([de0670b](https://github.com/mkdocstrings/autorefs/commit/de0670b77be84529c9c1ef37cad2a85ef8ec3cab) by Timothée Mazzucotelli). [Issue #17](https://github.com/mkdocstrings/autorefs/issues/17), [issue mkdocstrings/mkdocstrings#398](https://github.com/mkdocstrings/mkdocstrings/issues/398), [PR #18](https://github.com/mkdocstrings/autorefs/pull/18) ## [0.4.0](https://github.com/mkdocstrings/autorefs/releases/tag/0.4.0) - 2022-03-07 [Compare with 0.3.1](https://github.com/mkdocstrings/autorefs/compare/0.3.1...0.4.0) ### Features - Add HTML classes to references: `autorefs` always, and `autorefs-internal` or `autorefs-external` depending on the link ([39db59d](https://github.com/mkdocstrings/autorefs/commit/39db59d802a59d1af93d24520b1e219eeec780e4) by Timothée Mazzucotelli). [PR #16](https://github.com/mkdocstrings/autorefs/pull/16) ### Bug Fixes - Don't compute relative URLs of already relative ones ([f6b861c](https://github.com/mkdocstrings/autorefs/commit/f6b861c0e4a95c406ea3552fc93f889c3006e1a9) by Timothée Mazzucotelli). [PR #15](https://github.com/mkdocstrings/autorefs/pull/15) ## [0.3.1](https://github.com/mkdocstrings/autorefs/releases/tag/0.3.1) - 2021-12-27 [Compare with 0.3.0](https://github.com/mkdocstrings/autorefs/compare/0.3.0...0.3.1) ### Code Refactoring - Support fallback method returning multiple identifiers ([0d2b411](https://github.com/mkdocstrings/autorefs/commit/0d2b411030d23cf65c834c6a881ec8d0efddee8c) by Timothée Mazzucotelli). [Issue #11](https://github.com/mkdocstrings/autorefs/issues/11), [PR #12](https://github.com/mkdocstrings/autorefs/pull/12) and [mkdocstrings#350](https://github.com/mkdocstrings/mkdocstrings/pull/350) ## [0.3.0](https://github.com/mkdocstrings/autorefs/releases/tag/0.3.0) - 2021-07-24 [Compare with 0.2.1](https://github.com/mkdocstrings/autorefs/compare/0.2.1...0.3.0) ### Features - Add optional-hover ref type ([0288bdd](https://github.com/mkdocstrings/autorefs/commit/0288bdd34f779d73d3da19cfe2a89254fd3c4942) by Brian Koropoff). [PR #10](https://github.com/mkdocstrings/autorefs/pull/10) ## [0.2.1](https://github.com/mkdocstrings/autorefs/releases/tag/0.2.1) - 2021-05-07 [Compare with 0.2.0](https://github.com/mkdocstrings/autorefs/compare/0.2.0...0.2.1) ### Bug Fixes - Prevent error during parallel installations ([c90e399](https://github.com/mkdocstrings/autorefs/commit/c90e399213dec3435bf5dd0a0e5035ba586076fd) by Timothée Mazzucotelli). [PR #9](https://github.com/mkdocstrings/autorefs/pull/9) ## [0.2.0](https://github.com/mkdocstrings/autorefs/releases/tag/0.2.0) - 2021-05-03 [Compare with 0.1.1](https://github.com/mkdocstrings/autorefs/compare/0.1.1...0.2.0) ### Features - Allow registering absolute URLs for autorefs ([621686b](https://github.com/mkdocstrings/autorefs/commit/621686b4b36b8d24df80035095700f6a4f96567c) by Oleh Prypin). [PR #8](https://github.com/mkdocstrings/autorefs/pull/8) - Allow external tools to insert references that are OK to skip ([7619c28](https://github.com/mkdocstrings/autorefs/commit/7619c2835a63b54b1f5e9e11c5f320c04e3579ac) by Oleh Prypin). [PR #7](https://github.com/mkdocstrings/autorefs/pull/7) - Allow `[``identifier``][]`, understood as `[``identifier``][identifier]` ([2d3182d](https://github.com/mkdocstrings/autorefs/commit/2d3182db54dc33e75914e9c509bbf849842eb70a) by Oleh Prypin). [PR #5](https://github.com/mkdocstrings/autorefs/pull/5) ## [0.1.1](https://github.com/mkdocstrings/autorefs/releases/tag/0.1.1) - 2021-02-28 [Compare with 0.1.0](https://github.com/mkdocstrings/autorefs/compare/0.1.0...0.1.1) ### Packaging - Remove unused dependencies ([9c6a8e6](https://github.com/mkdocstrings/autorefs/commit/9c6a8e610f52d471fefa02baa4aef2773bdb59c0) by Oleh Prypin). ## [0.1.0](https://github.com/mkdocstrings/autorefs/releases/tag/0.1.0) - 2021-02-17 [Compare with first commit](https://github.com/mkdocstrings/autorefs/compare/fe6faa5d5a7a901605ec8ab98df09dc95067f6a8...0.1.0) ### Features - Split out "mkdocs-autorefs" plugin from "mkdocstrings" ([fe6faa5](https://github.com/mkdocstrings/autorefs/commit/fe6faa5d5a7a901605ec8ab98df09dc95067f6a8) by Oleh Prypin). mkdocs-autorefs-1.4.2/CODE_OF_CONDUCT.md000066400000000000000000000125361501307725400174360ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dev@pawamoy.fr. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations mkdocs-autorefs-1.4.2/CONTRIBUTING.md000066400000000000000000000100771501307725400170660ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Environment setup Nothing easier! Fork and clone the repository, then: ```bash cd mkdocs-autorefs make setup ``` > NOTE: If it fails for some reason, you'll need to install [uv](https://github.com/astral-sh/uv) manually. > > You can install it with: > > ```bash > curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > > Now you can try running `make setup` again, or simply `uv sync`. You now have the dependencies installed. Run `make help` to see all the available actions! ## Tasks The entry-point to run commands and tasks is the `make` Python script, located in the `scripts` directory. Try running `make` to show the available commands and tasks. The *commands* do not need the Python dependencies to be installed, while the *tasks* do. The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. ## Development As usual: 1. create a new branch: `git switch -c feature-or-bugfix-name` 1. edit the code and/or the documentation **Before committing:** 1. run `make format` to auto-format the code 1. run `make check` to check everything (fix any warning) 1. run `make test` to run the tests (fix any issue) 1. if you updated the documentation or the project dependencies: 1. run `make docs` 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) If you are unsure about how to fix or ignore a warning, just let the continuous integration fail, and we will help you during review. Don't bother updating the changelog, we will take care of this. ## Commit message convention Commit messages must follow our convention based on the [Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): ``` [(scope)]: Subject [Body] ``` **Subject and body must be valid Markdown.** Subject must have proper casing (uppercase for first letter if it makes sense), but no dot at the end, and no punctuation in general. Scope and body are optional. Type can be: - `build`: About packaging, building wheels, etc. - `chore`: About packaging or repo/files management. - `ci`: About Continuous Integration. - `deps`: Dependencies update. - `docs`: About documentation. - `feat`: New feature. - `fix`: Bug fix. - `perf`: About performance. - `refactor`: Changes that are not features or bug fixes. - `style`: A change in code style/format. - `tests`: About tests. If you write a body, please add trailers at the end (for example issues and PR references, or co-authors), without relying on GitHub's flavored Markdown: ``` Body. Issue #10: https://github.com/namespace/project/issues/10 Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15 ``` These "trailers" must appear at the end of the body, without any blank lines between them. The trailer title can contain any character except colons `:`. We expect a full URI for each trailer, not just GitHub autolinks (for example, full GitHub URLs for commits and issues, not the hash or the #issue-number). We do not enforce a line length on commit messages summary and body, but please avoid very long summaries, and very long lines in the body, unless they are part of code blocks that must not be wrapped. ## Pull requests guidelines Link to any related issue in the Pull Request message. During the review, we recommend using fixups: ```bash # SHA is the SHA of the commit you want to fix git commit --fixup=SHA ``` Once all the changes are approved, you can squash your commits: ```bash git rebase -i --autosquash main ``` And force-push: ```bash git push -f ``` If this seems all too complicated, you can push or force-push each new commit, and we will squash them ourselves if needed, before merging. mkdocs-autorefs-1.4.2/LICENSE000066400000000000000000000014221501307725400156340ustar00rootroot00000000000000ISC License Copyright (c) 2019, Oleh Prypin Copyright (c) 2019, Timothée Mazzucotelli Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. mkdocs-autorefs-1.4.2/Makefile000066400000000000000000000007601501307725400162730ustar00rootroot00000000000000# If you have `direnv` loaded in your shell, and allow it in the repository, # the `make` command will point at the `scripts/make` shell script. # This Makefile is just here to allow auto-completion in the terminal. actions = \ allrun \ changelog \ check \ check-api \ check-docs \ check-quality \ check-types \ clean \ coverage \ docs \ docs-deploy \ format \ help \ multirun \ release \ run \ setup \ test \ vscode .PHONY: $(actions) $(actions): @python scripts/make "$@" mkdocs-autorefs-1.4.2/README.md000066400000000000000000000277041501307725400161210ustar00rootroot00000000000000# mkdocs-autorefs [![ci](https://github.com/mkdocstrings/autorefs/workflows/ci/badge.svg)](https://github.com/mkdocstrings/autorefs/actions?query=workflow%3Aci) [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/autorefs/) [![pypi version](https://img.shields.io/pypi/v/mkdocs-autorefs.svg)](https://pypi.org/project/mkdocs-autorefs/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocs-autorefs.svg)](https://anaconda.org/conda-forge/mkdocs-autorefs) [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/autorefs) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#autorefs:gitter.im) Automatically link across pages in MkDocs. ## Installation ```bash pip install mkdocs-autorefs ``` ## Usage ```yaml # mkdocs.yml plugins: - search - autorefs ``` In one of your Markdown files (e.g. `doc1.md`) create some headings: ```markdown ## Hello, world! ## Another heading Link to [Hello, World!](#hello-world) on the same page. ``` This is a [*normal* link to an anchor](https://www.mkdocs.org/user-guide/writing-your-docs/#linking-to-pages). MkDocs generates anchors for each heading, and they can always be used to link to something, either within the same page (as shown here) or by specifying the path of the other page. But with this plugin, you can **link to a heading from any other page** on the site *without* needing to know the path of either of the pages, just the heading title itself. Let's create another Markdown page to try this, `subdir/doc2.md`: ```markdown We can [link to that heading][hello-world] from another page too. This works the same as [a normal link to that heading](../doc1.md#hello-world). ``` Linking to a heading without needing to know the destination page can be useful if specifying that path is cumbersome, e.g. when the pages have deeply nested paths, are far apart, or are moved around frequently. ### Non-unique headings When linking to a heading that appears several times throughout the site, this plugin will log a warning message stating that multiple URLs were found and that headings should be made unique, and will resolve the link using the first found URL. To prevent getting warnings, use [Markdown anchors](#markdown-anchors) to add unique aliases to your headings, and use these aliases when referencing the headings. If you cannot use Markdown anchors, for example because you inject the same generated contents in multiple locations (for example mkdocstrings' API documentation), then you can try to alleviate the warnings by enabling the `resolve_closest` option: ```yaml plugins: - autorefs: resolve_closest: true ``` When `resolve_closest` is enabled, and multiple URLs are found for the same identifier, the plugin will try to resolve to the one that is "closest" to the current page (the page containing the link). By closest, we mean: - URLs that are relative to the current page's URL, climbing up parents - if multiple URLs are relative to it, use the one at the shortest distance if possible. If multiple relative URLs are at the same distance, the first of these URLs will be used. If no URL is relative to the current page's URL, the first URL of all found URLs will be used. Examples: Current page | Candidate URLs | Relative URLs | Winner ------------ | -------------- | ------------- | ------ ` ` | `x/#b`, `#b` | `#b` | `#b` (only one relative) `a/` | `b/c/#d`, `c/#d` | none | `b/c/#d` (no relative, use first one, even if longer distance) `a/b/` | `x/#e`, `a/c/#e`, `a/d/#e` | `a/c/#e`, `a/d/#e` (relative to parent `a/`) | `a/c/#e` (same distance, use first one) `a/b/` | `x/#e`, `a/c/d/#e`, `a/c/#e` | `a/c/d/#e`, `a/c/#e` (relative to parent `a/`) | `a/c/#e` (shortest distance) `a/b/c/` | `x/#e`, `a/#e`, `a/b/#e`, `a/b/c/d/#e`, `a/b/c/#e` | `a/b/c/d/#e`, `a/b/c/#e` | `a/b/c/#e` (shortest distance) ### Markdown anchors The autorefs plugin offers a feature called "Markdown anchors". Such anchors can be added anywhere in a document, and linked to from any other place. The syntax is: ```md [](){ #id-of-the-anchor } ``` If you look closely, it starts with the usual syntax for a link, `[]()`, except both the text value and URL of the link are empty. Then we see `{ #id-of-the-anchor }`, which is the syntax supported by the [`attr_list`](https://python-markdown.github.io/extensions/attr_list/) extension. It sets an HTML id to the anchor element. The autorefs plugin simply gives a meaning to such anchors with ids. Note that raw HTML anchors like `` are not supported. The `attr_list` extension must be enabled for the Markdown anchors feature to work: ```yaml # mkdocs.yml plugins: - search - autorefs markdown_extensions: - attr_list ``` Now, you can add anchors to documents: ```md Somewhere in a document. [](){ #foobar-paragraph } Paragraph about foobar. ``` ...making it possible to link to this anchor with our automatic links: ```md In any document. Check out the [paragraph about foobar][foobar-paragraph]. ``` If you add a Markdown anchor right above a heading, this anchor will redirect to the heading itself: ```md [](){ #foobar } ## A verbose title about foobar ``` Linking to the `foobar` anchor will bring you directly to the heading, not the anchor itself, so the URL will show `#a-verbose-title-about-foobar` instead of `#foobar`. These anchors therefore act as "aliases" for headings. It is possible to define multiple aliases per heading: ```md [](){ #contributing } [](){ #development-setup } ## How to contribute to the project? ``` Such aliases are especially useful when the same headings appear in several different pages. Without aliases, linking to the heading is undefined behavior (it could lead to any one of the headings). With unique aliases above headings, you can make sure to link to the right heading. For example, consider the following setup. You have one document per operating system describing how to install a project with the OS package manager or from sources: ```tree docs/ install/ arch.md debian.md gentoo.md ``` Each page has: ```md ## Install with package manager ... ## Install from sources ... ``` You don't want to change headings and make them redundant, like `## Arch: Install with package manager` and `## Debian: Install with package manager` just to be able to reference the right one with autorefs. Instead you can do this: ```md [](){ #arch-install-pkg } ## Install with package manager ... [](){ #arch-install-src } ## Install from sources ... ``` ...changing `arch` by `debian`, `gentoo`, etc. in the other pages. --- You can also change the actual identifier of a heading, thanks again to the `attr_list` Markdown extension: ```md ## Install from sources { #arch-install-src } ... ``` ...though note that this will impact the URL anchor too (and therefore the permalink to the heading). ### Link titles When rendering cross-references, the autorefs plugin sets `title` HTML attributes on links. These titles are displayed as tooltips when hovering on links. For mandatory cross-references (user-written ones), the original title of the target section is used as tooltip, for example: `Original title`. For optional cross-references (typically rendered by mkdocstrings handlers), the identifier is appended to the original title, for example: `Original title (package.module.function)`. This is useful to indicate the fully qualified name of API objects. Since the presence of titles prevents [the instant preview feature of Material for MkDocs][instant-preview] from working, the autorefs plugin will detect when this theme and feature are used, and only set titles on *external* links (for which instant previews cannot work). If you want to force autorefs to always set titles, never set titles, or only set titles on external links, you can use the `link_titles` option: ```yaml plugins: - autorefs: link_titles: external # link_titles: true # link_titles: false # link_titles: auto # default ``` [instant-preview]: https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#instant-previews By default, HTML tags are only preserved in titles if the current theme in use is Material for MkDocs and its `content.tooltips` feature is enabled. If your chosen theme does support HTML tags in titles, you can prevent tags stripping with the `strip_title_tags` option: ```yaml plugins: - autorefs: strip_title_tags: false # strip_title_tags: true # strip_title_tags: auto # default ``` ### Backlinks The autorefs plugin supports recording backlinks, that other plugins or systems can then use to render backlinks into pages. For example, when linking from page `foo/`, section `Section` to a heading with identifier `heading` thanks to a cross-reference `[Some heading][heading]`, the plugin will record that `foo/#section` references `heading`. ```md # Page foo This is page foo. ## Section This section references [some heading][heading]. ``` The `record_backlinks` attribute of the autorefs plugin must be set to true before Markdown is rendered to HTML to enable backlinks recording. This is typically done in an `on_config` MkDocs hook: ```python from mkdocs.config.defaults import MkDocsConfig def on_config(config: MkDocsConfig) -> MkDocsConfig | None: config.plugins["autorefs"].record_backlinks = True return config ``` Note that for backlinks to be recorded with accurate URLs, headings must have HTML IDs, meaning either the `toc` extension must be enabled, or the `attr_list` extension must be enabled *and* authors must add IDs to the relevant headings, with the `## Heading { #heading-id }` syntax. Other plugins or systems integrating with the autorefs plugin can then retrieve backlinks for a specific identifier: ```python backlinks = autorefs_plugin.get_backlinks("heading") ``` The `get_backlinks` method returns a map of backlink types to sets of backlinks. A backlink is a tuple of navigation breadcrumbs, each breadcrumb having a title and URL. ```python print(backlinks) # { # "referenced-by": { # Backlink( # crumbs=( # BacklinkCrumb(title="Foo", url="foo/"), # BacklinkCrumb(title="Section", url="foo/#section"), # ), # ), # } ``` The default backlink type is `referenced-by`, but can be customized by other plugins or systems thanks to the `backlink-type` HTML data attribute on `autoref` elements. Such plugins and systems can also specify the anchor on the current page to use for the backlink with the `backlink-anchor` HTML data attribute on `autoref` elements. ```html ``` This feature is typically designed for use in [mkdocstrings](https://mkdocstrings.github.io/) handlers, though is not exclusive to mkdocstrings: it can be used by any other plugin or even author hooks. Such a hook is provided as an example here: ```python def on_env(env, /, *, config, files): regex = r"" def repl(match: Match) -> str: identifier = match.group(1) backlinks = config.plugin["autorefs"].get_backlinks(identifier, from_url=file.page.url) if not backlinks: return "" return "".join(_render_backlinks(backlinks)) for file in files: if file.page and file.page.content: file.page.content = re.sub(regex, repl, file.page.content) return env def _render_backlinks(backlinks): yield "
" for backlink_type, backlink_list in backlinks: yield f"{verbose_type[backlink_type]}:" yield "
    " for backlink in sorted(backlink_list, key: lambda b: b.crumbs): yield "
  • " for crumb in backlink.crumbs: if crumb.url and crumb.title: yield f'{crumb.title}' elif crumb.title: yield f"{crumb.title}" yield "
  • " yield "
" yield "
" ``` mkdocs-autorefs-1.4.2/config/000077500000000000000000000000001501307725400160755ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/config/coverage.ini000066400000000000000000000006101501307725400203660ustar00rootroot00000000000000[coverage:run] branch = true parallel = true source = src/ tests/ [coverage:paths] equivalent = src/ .venv/lib/*/site-packages/ .venvs/*/lib/*/site-packages/ [coverage:report] ignore_errors = True precision = 2 omit = src/*/__init__.py src/*/__main__.py tests/__init__.py exclude_lines = pragma: no cover if TYPE_CHECKING [coverage:json] output = htmlcov/coverage.json mkdocs-autorefs-1.4.2/config/git-changelog.toml000066400000000000000000000003401501307725400214770ustar00rootroot00000000000000bump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" versioning = "pep440" mkdocs-autorefs-1.4.2/config/mypy.ini000066400000000000000000000001621501307725400175730ustar00rootroot00000000000000[mypy] ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true mkdocs-autorefs-1.4.2/config/pytest.ini000066400000000000000000000004531501307725400201300ustar00rootroot00000000000000[pytest] python_files = test_*.py addopts = --cov --cov-append --cov-config config/coverage.ini testpaths = tests # action:message_regex:warning_class:module_regex:line filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist mkdocs-autorefs-1.4.2/config/ruff.toml000066400000000000000000000044231501307725400177370ustar00rootroot00000000000000target-version = "py39" line-length = 120 [lint] exclude = [ "tests/fixtures/*.py", ] select = [ "A", "ANN", "ARG", "B", "BLE", "C", "C4", "COM", "D", "DTZ", "E", "ERA", "EXE", "F", "FBT", "G", "I", "ICN", "INP", "ISC", "N", "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", "Q", "RUF", "RSE", "RET", "S", "SIM", "SLF", "T", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", ] ignore = [ "A001", # Variable is shadowing a Python builtin "ANN101", # Missing type annotation for self "ANN102", # Missing type annotation for cls "ANN204", # Missing return type annotation for special method __str__ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ARG005", # Unused lambda argument "C901", # Too complex "D105", # Missing docstring in magic method "D417", # Missing argument description in the docstring "E501", # Line too long "ERA001", # Commented out code "G004", # Logging statement uses f-string "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "SLF001", # Private member accessed "TRY003", # Avoid specifying long messages outside the exception class ] [lint.per-file-ignores] "src/**/cli.py" = [ "T201", # Print statement ] "src/*/debug.py" = [ "T201", # Print statement ] "!src/*/*.py" = [ "D100", # Missing docstring in public module ] "!src/**.py" = [ "D101", # Missing docstring in public class "D103", # Missing docstring in public function ] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement ] "tests/**.py" = [ "ARG005", # Unused lambda argument "FBT001", # Boolean positional arg in function definition "PLR2004", # Magic value used in comparison "S101", # Use of assert detected ] [lint.flake8-quotes] docstring-quotes = "double" [lint.flake8-tidy-imports] ban-relative-imports = "all" [lint.isort] known-first-party = ["mkdocs_autorefs"] [lint.pydocstyle] convention = "google" [format] exclude = [ "tests/fixtures/*.py", ] docstring-code-format = true docstring-code-line-length = 80 mkdocs-autorefs-1.4.2/config/vscode/000077500000000000000000000000001501307725400173605ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/config/vscode/launch.json000066400000000000000000000026731501307725400215350ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false, "args": "${command:pickArgs}" }, { "name": "run", "type": "debugpy", "request": "launch", "module": "mkdocs_autorefs", "console": "integratedTerminal", "justMyCode": false, "args": "${command:pickArgs}" }, { "name": "docs", "type": "debugpy", "request": "launch", "module": "mkdocs", "justMyCode": false, "args": [ "serve", "-v" ] }, { "name": "test", "type": "debugpy", "request": "launch", "module": "pytest", "justMyCode": false, "args": [ "-c=config/pytest.ini", "-vvv", "--no-cov", "--dist=no", "tests", "-k=${input:tests_selection}" ] } ], "inputs": [ { "id": "tests_selection", "type": "promptString", "description": "Tests selection", "default": "" } ] }mkdocs-autorefs-1.4.2/config/vscode/settings.json000066400000000000000000000017041501307725400221150ustar00rootroot00000000000000{ "files.watcherExclude": { "**/.venv*/**": true, "**/.venvs*/**": true, "**/venv*/**": true }, "mypy-type-checker.args": [ "--config-file=config/mypy.ini" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestArgs": [ "--config-file=config/pytest.ini" ], "ruff.enable": true, "ruff.format.args": [ "--config=config/ruff.toml" ], "ruff.lint.args": [ "--config=config/ruff.toml" ], "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, "yaml.customTags": [ "!ENV scalar", "!ENV sequence", "!relative scalar", "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" ] }mkdocs-autorefs-1.4.2/config/vscode/tasks.json000066400000000000000000000046051501307725400214050ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "changelog", "type": "process", "command": "scripts/make", "args": ["changelog"] }, { "label": "check", "type": "process", "command": "scripts/make", "args": ["check"] }, { "label": "check-quality", "type": "process", "command": "scripts/make", "args": ["check-quality"] }, { "label": "check-types", "type": "process", "command": "scripts/make", "args": ["check-types"] }, { "label": "check-docs", "type": "process", "command": "scripts/make", "args": ["check-docs"] }, { "label": "check-api", "type": "process", "command": "scripts/make", "args": ["check-api"] }, { "label": "clean", "type": "process", "command": "scripts/make", "args": ["clean"] }, { "label": "docs", "type": "process", "command": "scripts/make", "args": ["docs"] }, { "label": "docs-deploy", "type": "process", "command": "scripts/make", "args": ["docs-deploy"] }, { "label": "format", "type": "process", "command": "scripts/make", "args": ["format"] }, { "label": "release", "type": "process", "command": "scripts/make", "args": ["release", "${input:version}"] }, { "label": "setup", "type": "process", "command": "scripts/make", "args": ["setup"] }, { "label": "test", "type": "process", "command": "scripts/make", "args": ["test", "coverage"], "group": "test" }, { "label": "vscode", "type": "process", "command": "scripts/make", "args": ["vscode"] } ], "inputs": [ { "id": "version", "type": "promptString", "description": "Version" } ] }mkdocs-autorefs-1.4.2/docs/000077500000000000000000000000001501307725400155605ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/.overrides/000077500000000000000000000000001501307725400176405ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/.overrides/partials/000077500000000000000000000000001501307725400214575ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/.overrides/partials/comments.html000066400000000000000000000041401501307725400241710ustar00rootroot00000000000000 mkdocs-autorefs-1.4.2/docs/.overrides/partials/path-item.html000066400000000000000000000012111501307725400242300ustar00rootroot00000000000000{# Fix breadcrumbs for when mkdocs-section-index is used. #} {# See https://github.com/squidfunk/mkdocs-material/issues/7614. #} {% macro render_content(nav_item) %} {{ nav_item.title }} {% endmacro %} {% macro render(nav_item, ref=nav_item) %} {% if nav_item.is_page %}
  • {{ render_content(ref) }}
  • {% elif nav_item.children %} {{ render(nav_item.children | first, ref) }} {% endif %} {% endmacro %} mkdocs-autorefs-1.4.2/docs/changelog.md000066400000000000000000000000601501307725400200250ustar00rootroot00000000000000--- title: Changelog --- --8<-- "CHANGELOG.md" mkdocs-autorefs-1.4.2/docs/code_of_conduct.md000066400000000000000000000000741501307725400212200ustar00rootroot00000000000000--- title: Code of Conduct --- --8<-- "CODE_OF_CONDUCT.md" mkdocs-autorefs-1.4.2/docs/contributing.md000066400000000000000000000000661501307725400206130ustar00rootroot00000000000000--- title: Contributing --- --8<-- "CONTRIBUTING.md" mkdocs-autorefs-1.4.2/docs/credits.md000066400000000000000000000001351501307725400175360ustar00rootroot00000000000000--- title: Credits hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` mkdocs-autorefs-1.4.2/docs/css/000077500000000000000000000000001501307725400163505ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/css/material.css000066400000000000000000000001311501307725400206530ustar00rootroot00000000000000/* More space at the bottom of the page. */ .md-main__inner { margin-bottom: 1.5rem; } mkdocs-autorefs-1.4.2/docs/css/mkdocstrings.css000066400000000000000000000046151501307725400215770ustar00rootroot00000000000000/* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; border-left: .05rem solid var(--md-typeset-table-color); } /* Mark external links as such. */ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ mask-image: url('data:image/svg+xml,'); -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; vertical-align: middle; position: relative; height: 1em; width: 1em; background-color: currentColor; } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } /* Tree-like output for backlinks. */ .doc-backlink-list { --tree-clr: var(--md-default-fg-color); --tree-font-size: 1rem; --tree-item-height: 1; --tree-offset: 1rem; --tree-thickness: 1px; --tree-style: solid; display: grid; list-style: none !important; } .doc-backlink-list li > span:first-child { text-indent: .3rem; } .doc-backlink-list li { padding-inline-start: var(--tree-offset); border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); position: relative; margin-left: 0 !important; &:last-child { border-color: transparent; } &::before{ content: ''; position: absolute; top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness)); left: calc(var(--tree-thickness) * -1); width: calc(var(--tree-offset) + var(--tree-thickness) * 2); height: calc(var(--tree-item-height) * var(--tree-font-size)); border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr); } &::after{ content: ''; position: absolute; border-radius: 50%; background-color: var(--tree-clr); top: calc(var(--tree-item-height) / 2 * 1rem); left: var(--tree-offset) ; translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1); } } mkdocs-autorefs-1.4.2/docs/index.md000066400000000000000000000000751501307725400172130ustar00rootroot00000000000000--- title: Overview hide: - feedback --- --8<-- "README.md" mkdocs-autorefs-1.4.2/docs/js/000077500000000000000000000000001501307725400161745ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/js/feedback.js000066400000000000000000000007751501307725400202670ustar00rootroot00000000000000const feedback = document.forms.feedback; feedback.hidden = false; feedback.addEventListener("submit", function(ev) { ev.preventDefault(); const commentElement = document.getElementById("feedback"); commentElement.style.display = "block"; feedback.firstElementChild.disabled = true; const data = ev.submitter.getAttribute("data-md-value"); const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']"); if (note) { note.hidden = false; } }) mkdocs-autorefs-1.4.2/docs/license.md000066400000000000000000000001151501307725400175210ustar00rootroot00000000000000--- title: License hide: - feedback --- # License ``` --8<-- "LICENSE" ``` mkdocs-autorefs-1.4.2/docs/reference/000077500000000000000000000000001501307725400175165ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/docs/reference/api.md000066400000000000000000000001071501307725400206070ustar00rootroot00000000000000--- title: API reference hide: - navigation --- # ::: mkdocs_autorefs mkdocs-autorefs-1.4.2/duties.py000066400000000000000000000146111501307725400165020ustar00rootroot00000000000000"""Development tasks.""" from __future__ import annotations import os import re import sys from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING from duty import duty, tools if TYPE_CHECKING: from collections.abc import Iterator from duty.context import Context PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" def pyprefix(title: str) -> str: if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title @contextmanager def material_insiders() -> Iterator[bool]: if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: yield True finally: os.environ.pop("MATERIAL_INSIDERS") else: yield False def _get_changelog_version() -> str: changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") with Path(__file__).parent.joinpath("CHANGELOG.md").open("r", encoding="utf8") as file: return next(filter(bool, map(changelog_version_re.match, file))).group(1) # type: ignore[union-attr] @duty def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. Parameters: bump: Bump option passed to git-changelog. """ ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") ctx.run(tools.yore.check(bump=bump or _get_changelog_version()), title="Checking legacy code") @duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) def check(ctx: Context) -> None: """Check it all!""" @duty def check_quality(ctx: Context) -> None: """Check the code quality.""" ctx.run( tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), ) @duty def check_docs(ctx: Context) -> None: """Check if the documentation builds correctly.""" Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with material_insiders(): ctx.run( tools.mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), ) @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" os.environ["FORCE_COLOR"] = "1" ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), ) @duty def check_api(ctx: Context, *cli_args: str) -> None: """Check for API breaking changes.""" ctx.run( tools.griffe.check("mkdocs_autorefs", search=["src"], color=True).add_args(*cli_args), title="Checking for API breaking changes", nofail=True, ) @duty def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), title="Serving documentation", capture=False, ) @duty def docs_deploy(ctx: Context) -> None: """Deploy the documentation to GitHub pages.""" os.environ["DEPLOY"] = "true" with material_insiders() as insiders: if not insiders: ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") ctx.run(tools.mkdocs.gh_deploy(), title="Deploying documentation") @duty def format(ctx: Context) -> None: """Run formatting tools on the code.""" ctx.run( tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) ctx.run(tools.ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") @duty def build(ctx: Context) -> None: """Build source and wheel distributions.""" ctx.run( tools.build(), title="Building source and wheel distributions", pty=PTY, ) @duty def publish(ctx: Context) -> None: """Publish source and wheel distributions to PyPI.""" if not Path("dist").exists(): ctx.run("false", title="No distribution files found") dists = [str(dist) for dist in Path("dist").iterdir()] ctx.run( tools.twine.upload(*dists, skip_existing=True), title="Publishing source and wheel distributions to PyPI", pty=PTY, ) @duty(post=["build", "publish", "docs-deploy"]) def release(ctx: Context, version: str = "") -> None: """Release a new Python package. Parameters: version: The new version number to use. """ if not (version := (version or input("> Version to release: ")).strip()): ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) @duty(silent=True, aliases=["cov"]) def coverage(ctx: Context) -> None: """Report coverage as text and HTML.""" ctx.run(tools.coverage.combine(), nofail=True) ctx.run(tools.coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) @duty def test(ctx: Context, *cli_args: str, match: str = "") -> None: """Run the test suite. Parameters: match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( tools.pytest( "tests", config_file="config/pytest.ini", select=match, color="yes", ).add_args("-n", "auto", *cli_args), title=pyprefix("Running tests"), ) mkdocs-autorefs-1.4.2/mkdocs.yml000066400000000000000000000103041501307725400166310ustar00rootroot00000000000000site_name: "mkdocs-autorefs" site_description: "Automatically link across pages in MkDocs." site_url: "https://mkdocstrings.github.io/autorefs" repo_url: "https://github.com/mkdocstrings/autorefs" repo_name: "mkdocstrings/autorefs" site_dir: "site" watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocs_autorefs] copyright: Copyright © 2019 Oleh Prypin, Timothée Mazzucotelli edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn nav: - Home: - Overview: index.md - Changelog: changelog.md - Credits: credits.md - License: license.md - API reference: reference/api.md - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md - Coverage report: coverage.md theme: name: material icon: logo: material/currency-sign features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.footer - navigation.instant.preview - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky - navigation.top - search.highlight - search.suggest - toc.follow palette: - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal accent: purple toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: black accent: lime toggle: icon: material/weather-night name: Switch to system preference extra_css: - css/material.css - css/mkdocstrings.css extra_javascript: - js/feedback.js markdown_extensions: - attr_list - admonition - callouts - footnotes - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.magiclink - pymdownx.snippets: base_path: [!relative $config_dir] check_paths: true - pymdownx.superfences - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - toc: permalink: "¤" plugins: - search - autorefs - markdown-exec - section-index - coverage - mkdocstrings: handlers: python: inventories: - https://docs.python.org/3/objects.inv - https://www.mkdocs.org/objects.inv - https://python-markdown.github.io/objects.inv paths: [src] options: backlinks: tree docstring_options: ignore_init_summary: true docstring_section_style: list filters: ["!^_", "^__"] heading_level: 1 inherited_members: true merge_init_into_class: true separate_signature: true show_root_heading: true show_root_full_path: false show_signature_annotations: true show_source: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - llmstxt: full_output: llms-full.txt sections: Usage: - index.md API: - reference/api.md - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true type: timeago - minify: minify_html: !ENV [DEPLOY, false] - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: - typeset extra: social: - icon: fontawesome/brands/github link: https://github.com/mkdocstrings/autorefs - icon: fontawesome/brands/gitter link: https://gitter.im/mkdocstrings/autorefs - icon: fontawesome/brands/python link: https://pypi.org/project/mkdocs-autorefs/ analytics: feedback: title: Was this page helpful? ratings: - icon: material/emoticon-happy-outline name: This page was helpful data: 1 note: Thanks for your feedback! - icon: material/emoticon-sad-outline name: This page could be improved data: 0 note: Let us know how we can improve this page. mkdocs-autorefs-1.4.2/pyproject.toml000066400000000000000000000062331501307725400175500ustar00rootroot00000000000000[build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [project] name = "mkdocs-autorefs" description = "Automatically link across pages in MkDocs." authors = [ {name = "Oleh Prypin", email = "oleh@pryp.in"}, {name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}, ] license = "ISC" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc"] dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ "Markdown>=3.3", "markupsafe>=2.0.1", "mkdocs>=1.1", ] [project.urls] Homepage = "https://mkdocstrings.github.io/autorefs" Documentation = "https://mkdocstrings.github.io/autorefs" Changelog = "https://mkdocstrings.github.io/autorefs/changelog" Repository = "https://github.com/mkdocstrings/autorefs" Issues = "https://github.com/mkdocstrings/autorefs/issues" Discussions = "https://github.com/mkdocstrings/autorefs/discussions" Gitter = "https://gitter.im/mkdocstrings/autorefs" [project.entry-points."mkdocs.plugins"] autorefs = "mkdocs_autorefs:AutorefsPlugin" [tool.pdm.version] source = "call" getter = "scripts.get_version:get_version" [tool.pdm.build] # Include as much as possible in the source distribution, to help redistributors. excludes = ["**/.pytest_cache", "**/.mypy_cache"] source-includes = [ "config", "docs", "scripts", "share", "tests", "duties.py", "mkdocs.yml", "*.md", "LICENSE", ] [tool.pdm.build.wheel-data] # Manual pages can be included in the wheel. # Depending on the installation tool, they will be accessible to users. # pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. data = [ {path = "share/**/*", relative-to = "."}, ] [dependency-groups] maintain = [ "build>=1.2", "git-changelog>=2.5", "twine>=5.1", "yore>=0.3.3", ] ci = [ "duty>=1.6", "mypy>=1.10", "pymdown-extensions>=10.14", "pytest>=8.2", "pytest-cov>=5.0", "pytest-randomly>=3.15", "pytest-xdist>=3.6", "ruff>=0.4", "types-markdown>=3.6", "types-pyyaml>=6.0", ] docs = [ "markdown-callouts>=0.4", "markdown-exec>=1.8", "mkdocs>=1.6", "mkdocs-coverage>=1.0", "mkdocs-git-revision-date-localized-plugin>=1.2", "mkdocs-llmstxt>=0.2", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-section-index>=0.3", "mkdocstrings[python]>=0.29", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] [tool.uv] default-groups = ["maintain", "ci", "docs"] mkdocs-autorefs-1.4.2/scripts/000077500000000000000000000000001501307725400163175ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/scripts/gen_credits.py000066400000000000000000000147741501307725400211740ustar00rootroot00000000000000# Script to generate the project's credits. from __future__ import annotations import os import sys from collections import defaultdict from collections.abc import Iterable from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent from typing import Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment from packaging.requirements import Requirement # YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", ".")) with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] devdeps = [dep for group in pyproject["dependency-groups"].values() for dep in group if not dep.startswith("-e")] PackageMetadata = dict[str, Union[str, Iterable[str]]] Metadata = dict[str, PackageMetadata] def _merge_fields(metadata: dict) -> PackageMetadata: fields = defaultdict(list) for header, value in metadata.items(): fields[header.lower()].append(value.strip()) return { field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0] for field, value in fields.items() } def _norm_name(name: str) -> str: return name.replace("_", "-").replace(".", "-").lower() def _requirements(deps: list[str]) -> dict[str, Requirement]: return {_norm_name((req := Requirement(dep)).name): req for dep in deps} def _extra_marker(req: Requirement) -> str | None: if not req.marker: return None try: return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra") except StopIteration: return None def _get_metadata() -> Metadata: metadata = {} for pkg in distributions(): name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore] metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type] metadata[name]["spec"] = set() metadata[name]["extras"] = set() metadata[name].setdefault("summary", "") _set_license(metadata[name]) return metadata def _set_license(metadata: PackageMetadata) -> None: license_field = metadata.get("license-expression", metadata.get("license", "")) license_name = license_field if isinstance(license_field, str) else " + ".join(license_field) check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n") if check_classifiers: license_names = [] for classifier in metadata["classifier"]: if classifier.startswith("License ::"): license_names.append(classifier.rsplit("::", 1)[1].strip()) license_name = " + ".join(license_names) metadata["license"] = license_name or "?" def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} for dep_name, dep_req in base_deps.items(): if dep_name not in metadata or dep_name == "mkdocs-autorefs": continue metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator] metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator] deps[dep_name] = metadata[dep_name] again = True while again: again = False for pkg_name in metadata: if pkg_name in deps: for pkg_dependency in metadata[pkg_name].get("requires-dist", []): requirement = Requirement(pkg_dependency) dep_name = _norm_name(requirement.name) extra_marker = _extra_marker(requirement) if ( dep_name in metadata and dep_name not in deps and dep_name != project["name"] and (not extra_marker or extra_marker in deps[pkg_name]["extras"]) ): metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator] deps[dep_name] = metadata[dep_name] again = True return deps def _render_credits() -> str: metadata = _get_metadata() dev_dependencies = _get_deps(_requirements(devdeps), metadata) prod_dependencies = _get_deps( _requirements( chain( # type: ignore[arg-type] project.get("dependencies", []), chain(*project.get("optional-dependencies", {}).values()), ), ), metadata, ) template_data = { "project_name": project_name, "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( """ # Credits These projects were used to build *{{ project_name }}*. **Thank you!** [Python](https://www.python.org/) | [uv](https://github.com/astral-sh/uv) | [copier-uv](https://github.com/pawamoy/copier-uv) {% macro dep_line(dep) -%} [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} {% if prod_dependencies -%} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in prod_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% endif -%} {% if dev_dependencies -%} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in dev_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% endif -%} {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} """, ) jinja_env = SandboxedEnvironment(undefined=StrictUndefined) return jinja_env.from_string(template_text).render(**template_data) print(_render_credits()) mkdocs-autorefs-1.4.2/scripts/get_version.py000066400000000000000000000020061501307725400212130ustar00rootroot00000000000000# Get current project version from Git tags or changelog. import re from contextlib import suppress from pathlib import Path from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm _root = Path(__file__).parent.parent _changelog = _root / "CHANGELOG.md" _changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") _default_scm_version = SCMVersion(Version("0.0.0"), None, False, None, None) # noqa: FBT003 def get_version() -> str: scm_version = get_version_from_scm(_root) or _default_scm_version if scm_version.version <= Version("0.1"): # Missing Git tags? with suppress(OSError, StopIteration): # noqa: SIM117 with _changelog.open("r", encoding="utf8") as file: match = next(filter(None, map(_changelog_version_re.match, file))) scm_version = scm_version._replace(version=Version(match.group(1))) return default_version_formatter(scm_version) if __name__ == "__main__": print(get_version()) mkdocs-autorefs-1.4.2/scripts/make000077700000000000000000000000001501307725400204412make.pyustar00rootroot00000000000000mkdocs-autorefs-1.4.2/scripts/make.py000077500000000000000000000141721501307725400176160ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations import os import shutil import subprocess import sys from contextlib import contextmanager from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Iterator PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: """Run a shell command.""" if capture_output: return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 return None @contextmanager def environ(**kwargs: str) -> Iterator[None]: """Temporarily set environment variables.""" original = dict(os.environ) os.environ.update(kwargs) try: yield finally: os.environ.clear() os.environ.update(original) def uv_install(venv: Path) -> None: """Install dependencies using uv.""" with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): if "CI" in os.environ: shell("uv sync --no-editable") else: shell("uv sync") def setup() -> None: """Setup the project.""" if not shutil.which("uv"): raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") print("Installing dependencies (default environment)") default_venv = Path(".venv") if not default_venv.exists(): shell("uv venv") uv_install(default_venv) if PYTHON_VERSIONS: for version in PYTHON_VERSIONS: print(f"\nInstalling dependencies (python{version})") venv_path = Path(f".venvs/{version}") if not venv_path.exists(): shell(f"uv venv --python {version} {venv_path}") with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): uv_install(venv_path) def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in a virtual environment.""" kwargs = {"check": True, **kwargs} uv_run = ["uv", "run", "--no-sync"] if version == "default": with environ(UV_PROJECT_ENVIRONMENT=".venv"): subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 else: with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"): subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 def multirun(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command for all configured Python versions.""" if PYTHON_VERSIONS: for version in PYTHON_VERSIONS: run(version, cmd, *args, **kwargs) else: run("default", cmd, *args, **kwargs) def allrun(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in all virtual environments.""" run("default", cmd, *args, **kwargs) if PYTHON_VERSIONS: multirun(cmd, *args, **kwargs) def clean() -> None: """Delete build artifacts and cache files.""" paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] for path in paths_to_clean: shutil.rmtree(path, ignore_errors=True) cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"} for dirpath in Path(".").rglob("*/"): if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs: shutil.rmtree(dirpath, ignore_errors=True) def vscode() -> None: """Configure VSCode to work on this project.""" shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True) def main() -> int: """Main entry point.""" args = list(sys.argv[1:]) if not args or args[0] == "help": if len(args) > 1: run("default", "duty", "--help", args[1]) else: print( dedent( """ Available commands help Print this help. Add task name to print help. setup Setup all virtual environments (install dependencies). run Run a command in the default virtual environment. multirun Run a command for all configured Python versions. allrun Run a command in all virtual environments. 3.x Run a command in the virtual environment for Python 3.x. clean Delete build artifacts and cache files. vscode Configure VSCode to work on this project. """, ), flush=True, ) if os.path.exists(".venv"): print("\nAvailable tasks", flush=True) run("default", "duty", "--list") return 0 while args: cmd = args.pop(0) if cmd == "run": run("default", *args) return 0 if cmd == "multirun": multirun(*args) return 0 if cmd == "allrun": allrun(*args) return 0 if cmd.startswith("3."): run(cmd, *args) return 0 opts = [] while args and (args[0].startswith("-") or "=" in args[0]): opts.append(args.pop(0)) if cmd == "clean": clean() elif cmd == "setup": setup() elif cmd == "vscode": vscode() elif cmd == "check": multirun("duty", "check-quality", "check-types", "check-docs") run("default", "duty", "check-api") elif cmd in {"check-quality", "check-docs", "check-types", "test"}: multirun("duty", cmd, *opts) else: run("default", "duty", cmd, *opts) return 0 if __name__ == "__main__": try: sys.exit(main()) except subprocess.CalledProcessError as process: if process.output: print(process.output, file=sys.stderr) sys.exit(process.returncode) mkdocs-autorefs-1.4.2/src/000077500000000000000000000000001501307725400154175ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/000077500000000000000000000000001501307725400206075ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/__init__.py000066400000000000000000000015471501307725400227270ustar00rootroot00000000000000"""mkdocs-autorefs package. Automatically link across pages in MkDocs. """ from __future__ import annotations from mkdocs_autorefs._internal.backlinks import Backlink, BacklinkCrumb, BacklinksTreeProcessor from mkdocs_autorefs._internal.plugin import AutorefsConfig, AutorefsPlugin from mkdocs_autorefs._internal.references import ( AUTO_REF_RE, AUTOREF_RE, AnchorScannerTreeProcessor, AutorefsExtension, AutorefsHookInterface, AutorefsInlineProcessor, fix_ref, fix_refs, relative_url, ) __all__: list[str] = [ "AUTOREF_RE", "AUTO_REF_RE", "AnchorScannerTreeProcessor", "AutorefsConfig", "AutorefsExtension", "AutorefsHookInterface", "AutorefsInlineProcessor", "AutorefsPlugin", "Backlink", "BacklinkCrumb", "BacklinksTreeProcessor", "fix_ref", "fix_refs", "relative_url", ] mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/000077500000000000000000000000001501307725400225625ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/__init__.py000066400000000000000000000000001501307725400246610ustar00rootroot00000000000000mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/backlinks.py000066400000000000000000000056111501307725400251000ustar00rootroot00000000000000# Backlinks module. from __future__ import annotations import logging from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar from markdown.core import Markdown from markdown.treeprocessors import Treeprocessor if TYPE_CHECKING: from xml.etree.ElementTree import Element from markdown import Markdown from mkdocs_autorefs._internal.plugin import AutorefsPlugin try: from mkdocs.plugins import get_plugin_logger _log = get_plugin_logger(__name__) except ImportError: # TODO: remove once support for MkDocs <1.5 is dropped _log = logging.getLogger(f"mkdocs.plugins.{__name__}") # type: ignore[assignment] @dataclass(frozen=True, order=True) class BacklinkCrumb: """A navigation breadcrumb for a backlink.""" title: str """The title of the breadcrumb.""" url: str """The URL of the breadcrumb.""" parent: BacklinkCrumb | None = None """The parent breadcrumb.""" def __eq__(self, value: object) -> bool: """Compare URLs for equality.""" if isinstance(value, BacklinkCrumb): return self.url == value.url return False @dataclass(eq=True, frozen=True, order=True) class Backlink: """A backlink (list of breadcrumbs).""" crumbs: tuple[BacklinkCrumb, ...] """The list of breadcrumbs.""" class BacklinksTreeProcessor(Treeprocessor): """Enhance autorefs with `backlink-type` and `backlink-anchor` attributes. These attributes are then used later to register backlinks. """ name: str = "mkdocs-autorefs-backlinks" """The name of the tree processor.""" initial_id: str | None = None """The initial heading ID.""" _htags: ClassVar[set[str]] = {"h1", "h2", "h3", "h4", "h5", "h6"} def __init__(self, plugin: AutorefsPlugin, md: Markdown | None = None) -> None: """Initialize the tree processor. Parameters: plugin: A reference to the autorefs plugin, to use its `register_anchor` method. """ super().__init__(md) self._plugin = plugin self._last_heading_id: str | None = None def run(self, root: Element) -> None: """Run the tree processor. Arguments: root: The root element of the document. """ if self._plugin.current_page is not None: self._last_heading_id = self.initial_id self._enhance_autorefs(root) def _enhance_autorefs(self, parent: Element) -> None: for el in parent: if el.tag in self._htags: self._last_heading_id = el.get("id") elif el.tag == "autoref": if "backlink-type" not in el.attrib: el.set("backlink-type", "referenced-by") if "backlink-anchor" not in el.attrib and self._last_heading_id: el.set("backlink-anchor", self._last_heading_id) else: self._enhance_autorefs(el) mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/debug.py000066400000000000000000000054211501307725400242240ustar00rootroot00000000000000from __future__ import annotations import os import platform import sys from dataclasses import dataclass from importlib import metadata @dataclass class _Variable: """Dataclass describing an environment variable.""" name: str """Variable name.""" value: str """Variable value.""" @dataclass class _Package: """Dataclass describing a Python package.""" name: str """Package name.""" version: str """Package version.""" @dataclass class _Environment: """Dataclass to store environment information.""" interpreter_name: str """Python interpreter name.""" interpreter_version: str """Python interpreter version.""" interpreter_path: str """Path to Python executable.""" platform: str """Operating System.""" packages: list[_Package] """Installed packages.""" variables: list[_Variable] """Environment variables.""" def _interpreter_name_version() -> tuple[str, str]: if hasattr(sys, "implementation"): impl = sys.implementation.version version = f"{impl.major}.{impl.minor}.{impl.micro}" kind = impl.releaselevel if kind != "final": version += kind[0] + str(impl.serial) return sys.implementation.name, version return "", "0.0.0" def _get_version(dist: str = "mkdocs-autorefs") -> str: """Get version of the given distribution. Parameters: dist: A distribution name. Returns: A version number. """ try: return metadata.version(dist) except metadata.PackageNotFoundError: return "0.0.0" def _get_debug_info() -> _Environment: """Get debug/environment information. Returns: Environment information. """ py_name, py_version = _interpreter_name_version() packages = ["mkdocs-autorefs"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MKDOCS_AUTOREFS")]] return _Environment( interpreter_name=py_name, interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) def _print_debug_info() -> None: """Print debug/environment information.""" info = _get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") for var in info.variables: print(f" - `{var.name}`: `{var.value}`") print("- __Installed packages__:") for pkg in info.packages: print(f" - `{pkg.name}` v{pkg.version}") if __name__ == "__main__": _print_debug_info() mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/plugin.py000066400000000000000000000607201501307725400244370ustar00rootroot00000000000000# This module contains the "mkdocs-autorefs" plugin. # # After each page is processed by the Markdown converter, this plugin stores absolute URLs of every HTML anchors # it finds to later be able to fix unresolved references. # # Once every page has been rendered and all identifiers and their URLs collected, # the plugin fixes unresolved references in the HTML content of the pages. from __future__ import annotations import contextlib import functools import logging from collections import defaultdict from pathlib import PurePosixPath as URL # noqa: N814 from typing import TYPE_CHECKING, Any, Callable, Literal from urllib.parse import urlsplit from warnings import warn from mkdocs.config.base import Config from mkdocs.config.config_options import Choice, Type from mkdocs.plugins import BasePlugin, event_priority from mkdocs.structure.pages import Page from mkdocs_autorefs._internal.backlinks import Backlink, BacklinkCrumb from mkdocs_autorefs._internal.references import AutorefsExtension, fix_refs, relative_url if TYPE_CHECKING: from collections.abc import Sequence from jinja2.environment import Environment from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import Files from mkdocs.structure.nav import Section from mkdocs.structure.toc import AnchorLink try: from mkdocs.plugins import get_plugin_logger _log = get_plugin_logger(__name__) except ImportError: # TODO: Remove once support for MkDocs <1.5 is dropped. _log = logging.getLogger(f"mkdocs.plugins.{__name__}") # type: ignore[assignment] class AutorefsConfig(Config): """Configuration options for the `autorefs` plugin.""" resolve_closest: bool = Type(bool, default=False) # type: ignore[assignment] """Whether to resolve an autoref to the closest URL when multiple URLs are found for an identifier. By closest, we mean a combination of "relative to the current page" and "shortest distance from the current page". For example, if you link to identifier `hello` from page `foo/bar/`, and the identifier is found in `foo/`, `foo/baz/` and `foo/bar/baz/qux/` pages, autorefs will resolve to `foo/bar/baz/qux`, which is the only URL relative to `foo/bar/`. If multiple URLs are equally close, autorefs will resolve to the first of these equally close URLs. If autorefs cannot find any URL that is close to the current page, it will log a warning and resolve to the first URL found. When false and multiple URLs are found for an identifier, autorefs will log a warning and resolve to the first URL. """ link_titles: bool | Literal["auto", "external"] = Choice((True, False, "auto", "external"), default="auto") # type: ignore[assignment] """Whether to set titles on links. Such title attributes are displayed as tooltips when hovering over the links. - `"auto"`: autorefs will detect whether the instant preview feature of Material for MkDocs is enabled, and set titles on external links when it is, all links if it is not. - `"external"`: autorefs will set titles on external links only. - `True`: autorefs will set titles on all links. - `False`: autorefs will not set any title attributes on links. Titles are only set when they are different from the link's text. Titles are constructed from the linked heading's original title, optionally appending the identifier for API objects. """ strip_title_tags: bool | Literal["auto"] = Choice((True, False, "auto"), default="auto") # type: ignore[assignment] """Whether to strip HTML tags from link titles. Some themes support HTML in link titles, but others do not. - `"auto"`: strip tags unless the Material for MkDocs theme is detected. """ class AutorefsPlugin(BasePlugin[AutorefsConfig]): """The `autorefs` plugin for `mkdocs`. This plugin defines the following event hooks: - `on_config`, to configure itself - `on_page_markdown`, to set the current page in order for Markdown extension to use it - `on_env`, to apply cross-references once all pages have been rendered Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs` for more information about its plugin system. """ scan_toc: bool = True """Whether to scan the table of contents for identifiers to map to URLs.""" record_backlinks: bool = False """Whether to record backlinks.""" current_page: Page | None = None """The current page being processed.""" # YORE: Bump 2: Remove block. legacy_refs: bool = True """Whether to support legacy references.""" def __init__(self) -> None: """Initialize the object.""" super().__init__() # The plugin uses three URL maps, one for "primary" URLs, one for "secondary" URLs, # and one for "absolute" URLs. # # - A primary URL is an identifier that links to a specific anchor on a page. # - A secondary URL is an alias of an identifier that links to the same anchor as the identifier's primary URL. # Primary URLs with these aliases as identifiers may or may not be rendered later. # - An absolute URL is an identifier that links to an external resource. # These URLs are typically registered by mkdocstrings when loading object inventories. # # For example, mkdocstrings registers a primary URL for each heading rendered in a page. # Then, for each alias of this heading's identifier, it registers a secondary URL. # # We need to keep track of whether an identifier is primary or secondary, # to give it precedence when resolving cross-references. # We wouldn't want to log a warning if there is a single primary URL and one or more secondary URLs, # instead we want to use the primary URL without any warning. # # - A single primary URL mapped to an identifer? Use it. # - Multiple primary URLs mapped to an identifier? Use the first one, or closest one if configured as such. # - No primary URL mapped to an identifier, but a secondary URL mapped? Use it. # - Multiple secondary URLs mapped to an identifier? Use the first one, or closest one if configured as such. # - No secondary URL mapped to an identifier? Try using absolute URLs # (typically registered by loading inventories in mkdocstrings). # # This logic unfolds in `_get_item_url`. self._primary_url_map: dict[str, list[str]] = {} self._secondary_url_map: dict[str, list[str]] = {} self._title_map: dict[str, str] = {} self._breadcrumbs_map: dict[str, BacklinkCrumb] = {} self._abs_url_map: dict[str, str] = {} self._backlinks: dict[str, dict[str, set[str]]] = defaultdict(lambda: defaultdict(set)) # YORE: Bump 2: Remove line. self._get_fallback_anchor: Callable[[str], tuple[str, ...]] | None = None # YORE: Bump 2: Remove line. self._url_to_page: dict[str, Page] = {} self._link_titles: bool | Literal["external"] = True self._strip_title_tags: bool = False # ----------------------------------------------------------------------- # # MkDocs Hooks # # ----------------------------------------------------------------------- # def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Instantiate our Markdown extension. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). In this hook, we instantiate our [`AutorefsExtension`][mkdocs_autorefs.AutorefsExtension] and add it to the list of Markdown extensions used by `mkdocs`. Arguments: config: The MkDocs config object. Returns: The modified config. """ _log.debug("Adding AutorefsExtension to the list") config.markdown_extensions.append(AutorefsExtension(self)) # type: ignore[arg-type] # YORE: Bump 2: Remove block. # mkdocstrings still uses the `page` attribute as a string. # Fortunately, it does so in f-strings, so we can simply patch the `__str__` method # to render the URL. Page.__str__ = lambda page: page.url # type: ignore[method-assign,attr-defined] if self.config.link_titles == "auto": if getattr(config.theme, "name", None) == "material" and "navigation.instant.preview" in config.theme.get( "features", (), ): self._link_titles = "external" else: self._link_titles = True else: self._link_titles = self.config.link_titles if self.config.strip_title_tags == "auto": if getattr(config.theme, "name", None) == "material" and "content.tooltips" in config.theme.get( "features", (), ): self._strip_title_tags = False else: self._strip_title_tags = True else: self._strip_title_tags = self.config.strip_title_tags return config def on_page_markdown(self, markdown: str, page: Page, **kwargs: Any) -> str: # noqa: ARG002 """Remember which page is the current one. Arguments: markdown: Input Markdown. page: The related MkDocs page instance. kwargs: Additional arguments passed by MkDocs. Returns: The same Markdown. We only use this hook to keep a reference to the current page URL, used during Markdown conversion by the anchor scanner tree processor. """ # YORE: Bump 2: Remove line. self._url_to_page[page.url] = page self.current_page = page return markdown def on_page_content(self, html: str, page: Page, **kwargs: Any) -> str: # noqa: ARG002 """Map anchors to URLs. Hook for the [`on_page_content` event](https://www.mkdocs.org/user-guide/plugins/#on_page_content). In this hook, we map the IDs of every anchor found in the table of contents to the anchors absolute URLs. This mapping will be used later to fix unresolved reference of the form `[title][identifier]` or `[identifier][]`. Arguments: html: HTML converted from Markdown. page: The related MkDocs page instance. kwargs: Additional arguments passed by MkDocs. Returns: The same HTML. We only use this hook to map anchors to URLs. """ self.current_page = page # Collect `std`-domain URLs. if self.scan_toc: _log.debug("Mapping identifiers to URLs for page %s", page.file.src_path) for item in page.toc.items: self.map_urls(page, item) return html @event_priority(-50) # Late, after mkdocstrings has finished loading inventories. def on_env(self, env: Environment, /, *, config: MkDocsConfig, files: Files) -> Environment: # noqa: ARG002 """Apply cross-references and collect backlinks. Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). In this hook, we try to fix unresolved references of the form `[title][identifier]` or `[identifier][]`. Doing that allows the user of `autorefs` to cross-reference objects in their documentation strings. It uses the native Markdown syntax so it's easy to remember and use. We log a warning for each reference that we couldn't map to an URL. We also collect backlinks at the same time. We fix cross-refs and collect backlinks in a single pass for performance reasons (we don't want to run the regular expression on each page twice). Arguments: env: The MkDocs environment. config: The MkDocs config object. files: The list of files in the MkDocs project. Returns: The unmodified environment. """ for file in files: if file.page and file.page.content: _log.debug("Applying cross-refs in page %s", file.page.file.src_path) # YORE: Bump 2: Replace `, fallback=self.get_fallback_anchor` with `` within line. url_mapper = functools.partial( self.get_item_url, from_url=file.page.url, fallback=self.get_fallback_anchor, ) backlink_recorder = ( functools.partial(self._record_backlink, page_url=file.page.url) if self.record_backlinks else None ) # YORE: Bump 2: Replace `, _legacy_refs=self.legacy_refs` with `` within line. file.page.content, unmapped = fix_refs( file.page.content, url_mapper, record_backlink=backlink_recorder, link_titles=self._link_titles, strip_title_tags=self._strip_title_tags, _legacy_refs=self.legacy_refs, ) if unmapped and _log.isEnabledFor(logging.WARNING): for ref, context in unmapped: message = f"from {context.filepath}:{context.lineno}: ({context.origin}) " if context else "" _log.warning( f"{file.page.file.src_path}: {message}Could not find cross-reference target '{ref}'", ) return env # ----------------------------------------------------------------------- # # Utilities # # ----------------------------------------------------------------------- # # TODO: Maybe stop exposing this method in the future. def map_urls(self, page: Page, anchor: AnchorLink) -> None: """Recurse on every anchor to map its ID to its absolute URL. This method populates `self._primary_url_map` by side-effect. Arguments: page: The page containing the anchors. anchor: The anchor to process and to recurse on. """ return self._map_urls(page, anchor) def _map_urls(self, page: Page, anchor: AnchorLink, parent: BacklinkCrumb | None = None) -> None: # YORE: Bump 2: Remove block. if isinstance(page, str): try: page = self._url_to_page[page] except KeyError: page = self.current_page self.register_anchor(page, anchor.id, title=anchor.title, primary=True) breadcrumb = self._get_breadcrumb(page, anchor, parent) for child in anchor.children: self._map_urls(page, child, breadcrumb) def _get_breadcrumb( self, page: Page | Section, anchor: AnchorLink | None = None, parent: BacklinkCrumb | None = None, ) -> BacklinkCrumb: parent_breadcrumb = None if page.parent is None else self._get_breadcrumb(page.parent) if parent is None: if isinstance(page, Page): if (parent_url := page.url) not in self._breadcrumbs_map: self._breadcrumbs_map[parent_url] = BacklinkCrumb( title=page.title, url=parent_url, parent=parent_breadcrumb, ) parent = self._breadcrumbs_map[parent_url] else: parent = BacklinkCrumb(title=page.title, url="", parent=parent_breadcrumb) if anchor is None: return parent if (url := f"{page.url}#{anchor.id}") not in self._breadcrumbs_map: # type: ignore[union-attr] # Skip the parent page if the anchor is a top-level heading, to reduce repetition. if anchor.level == 1: parent = parent.parent self._breadcrumbs_map[url] = BacklinkCrumb(title=anchor.title, url=url, parent=parent) return self._breadcrumbs_map[url] def _record_backlink(self, identifier: str, backlink_type: str, backlink_anchor: str, page_url: str) -> None: """Record a backlink. Arguments: identifier: The target identifier. backlink_type: The type of backlink. backlink_anchor: The backlink target anchor. page_url: The URL of the page containing the backlink. """ # When we record backlinks, all identifiers have been registered. # If an identifier is not found in the primary or secondary URL maps, it's an absolute URL, # meaning it comes from an external source (typically an object inventory), # and we don't need to record backlinks for it. if identifier in self._primary_url_map or identifier in self._secondary_url_map: self._backlinks[identifier][backlink_type].add(f"{page_url}#{backlink_anchor}") def get_backlinks(self, *identifiers: str, from_url: str) -> dict[str, set[Backlink]]: """Return the backlinks to an identifier relative to the given URL. Arguments: *identifiers: The identifiers to get backlinks for. from_url: The URL of the page where backlinks are rendered. Returns: A dictionary of backlinks, with the type of reference as key and a set of backlinks as value. Each backlink is a tuple of (URL, title) tuples forming navigation breadcrumbs. """ relative_backlinks: dict[str, set[Backlink]] = defaultdict(set) for identifier in set(identifiers): backlinks = self._backlinks.get(identifier, {}) for backlink_type, backlink_urls in backlinks.items(): for backlink_url in backlink_urls: relative_backlinks[backlink_type].add(self._get_backlink(from_url, backlink_url)) return relative_backlinks def _get_backlink(self, from_url: str, backlink_url: str) -> Backlink: breadcrumbs = [] breadcrumb: BacklinkCrumb | None = self._breadcrumbs_map[backlink_url] while breadcrumb: breadcrumbs.append( BacklinkCrumb( title=breadcrumb.title, url=breadcrumb.url and relative_url(from_url, breadcrumb.url), parent=breadcrumb.parent, ), ) breadcrumb = breadcrumb.parent return Backlink(tuple(reversed(breadcrumbs))) def register_anchor( self, page: Page, identifier: str, anchor: str | None = None, *, title: str | None = None, primary: bool = True, ) -> None: """Register that an anchor corresponding to an identifier was encountered when rendering the page. Arguments: page: The page where the anchor was found. identifier: The identifier to register. anchor: The anchor on the page, without `#`. If not provided, defaults to the identifier. title: The title of the anchor (optional). primary: Whether this anchor is the primary one for the identifier. """ # YORE: Bump 2: Remove block. if isinstance(page, str): try: page = self._url_to_page[page] except KeyError: page = self.current_page url = f"{page.url}#{anchor or identifier}" url_map = self._primary_url_map if primary else self._secondary_url_map if identifier in url_map: if url not in url_map[identifier]: url_map[identifier].append(url) else: url_map[identifier] = [url] if title and url not in self._title_map: self._title_map[url] = title def register_url(self, identifier: str, url: str) -> None: """Register that the identifier should be turned into a link to this URL. Arguments: identifier: The new identifier. url: The absolute URL (including anchor, if needed) where this item can be found. """ self._abs_url_map[identifier] = url @staticmethod def _get_closest_url(from_url: str, urls: list[str], qualifier: str) -> str: """Return the closest URL to the current page. Arguments: from_url: The URL of the base page, from which we link towards the targeted pages. urls: A list of URLs to choose from. qualifier: The type of URLs we are choosing from. Returns: The closest URL to the current page. """ base_url = URL(from_url) while True: if candidates := [url for url in urls if URL(url).is_relative_to(base_url)]: break base_url = base_url.parent if not base_url.name: break if not candidates: _log.warning( "Could not find closest %s URL (from %s, candidates: %s). " "Make sure to use unique headings, identifiers, or Markdown anchors (see our docs).", qualifier, from_url, urls, ) return urls[0] winner = candidates[0] if len(candidates) == 1 else min(candidates, key=lambda c: c.count("/")) _log.debug("Closest URL found: %s (from %s, candidates: %s)", winner, from_url, urls) return winner def _get_urls(self, identifier: str) -> tuple[list[str], str]: try: return self._primary_url_map[identifier], "primary" except KeyError: return self._secondary_url_map[identifier], "secondary" def _get_item_url( self, identifier: str, from_url: str | None = None, # YORE: Bump 2: Remove line. fallback: Callable[[str], Sequence[str]] | None = None, ) -> str: try: urls, qualifier = self._get_urls(identifier) except KeyError: # YORE: Bump 2: Replace block with line 2. if identifier in self._abs_url_map: return self._abs_url_map[identifier] if fallback: new_identifiers = fallback(identifier) for new_identifier in new_identifiers: with contextlib.suppress(KeyError): url = self._get_item_url(new_identifier) self._secondary_url_map[identifier] = [url] return url raise if len(urls) > 1: if (self.config.resolve_closest or qualifier == "secondary") and from_url is not None: return self._get_closest_url(from_url, urls, qualifier) _log.warning( "Multiple %s URLs found for '%s': %s. " "Make sure to use unique headings, identifiers, or Markdown anchors (see our docs).", qualifier, identifier, urls, ) return urls[0] def get_item_url( self, identifier: str, from_url: str | None = None, # YORE: Bump 2: Remove line. fallback: Callable[[str], Sequence[str]] | None = None, ) -> tuple[str, str | None]: """Return a site-relative URL with anchor to the identifier, if it's present anywhere. Arguments: identifier: The anchor (without '#'). from_url: The URL of the base page, from which we link towards the targeted pages. Returns: A site-relative URL. """ # YORE: Bump 2: Replace `, fallback` with `` within line. url = self._get_item_url(identifier, from_url, fallback) title = self._title_map.get(url) or None if from_url is not None: parsed = urlsplit(url) if not parsed.scheme and not parsed.netloc: url = relative_url(from_url, url) return url, title # YORE: Bump 2: Remove block. # ----------------------------------------------------------------------- # # Deprecated API # # ----------------------------------------------------------------------- # @property def get_fallback_anchor(self) -> Callable[[str], tuple[str, ...]] | None: """Fallback anchors getter.""" return self._get_fallback_anchor # YORE: Bump 2: Remove block. @get_fallback_anchor.setter def get_fallback_anchor(self, value: Callable[[str], tuple[str, ...]] | None) -> None: """Fallback anchors setter.""" self._get_fallback_anchor = value if value is not None: warn( "Setting a fallback anchor function is deprecated and will be removed in a future release.", DeprecationWarning, stacklevel=2, ) mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/_internal/references.py000066400000000000000000000631051501307725400252620ustar00rootroot00000000000000# Cross-references module. from __future__ import annotations import logging import re import warnings from abc import ABC, abstractmethod from dataclasses import dataclass from functools import lru_cache from html import escape, unescape from html.parser import HTMLParser from io import StringIO from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal from urllib.parse import urlsplit from xml.etree.ElementTree import Element from markdown.core import Markdown from markdown.extensions import Extension from markdown.extensions.toc import slugify from markdown.inlinepatterns import REFERENCE_RE, ReferenceInlineProcessor from markdown.treeprocessors import Treeprocessor from markdown.util import HTML_PLACEHOLDER_RE, INLINE_PLACEHOLDER_RE from markupsafe import Markup from mkdocs_autorefs._internal.backlinks import BacklinksTreeProcessor if TYPE_CHECKING: from collections.abc import Iterable from pathlib import Path from re import Match from markdown import Markdown from mkdocs_autorefs._internal.plugin import AutorefsPlugin try: from mkdocs.plugins import get_plugin_logger _log = get_plugin_logger(__name__) except ImportError: # TODO: remove once support for MkDocs <1.5 is dropped _log = logging.getLogger(f"mkdocs.plugins.{__name__}") # type: ignore[assignment] AUTOREF_RE = re.compile(r".*?)>(?P.*?)</autoref>", flags=re.DOTALL) """The autoref HTML tag regular expression. A regular expression to match mkdocs-autorefs' special reference markers in the [`on_env` hook][mkdocs_autorefs.AutorefsPlugin.on_env]. """ class AutorefsHookInterface(ABC): """An interface for hooking into how AutoRef handles inline references.""" @dataclass class Context: """The context around an auto-reference.""" domain: str """A domain like `py` or `js`.""" role: str """A role like `class` or `function`.""" origin: str """The origin of an autoref (an object identifier).""" filepath: str | Path """The path to the file containing the autoref.""" lineno: int """The line number in the file containing the autoref.""" def as_dict(self) -> dict[str, str]: """Convert the context to a dictionary of HTML attributes.""" return { "domain": self.domain, "role": self.role, "origin": self.origin, "filepath": str(self.filepath), "lineno": str(self.lineno), } @abstractmethod def expand_identifier(self, identifier: str) -> str: """Expand an identifier in a given context. Parameters: identifier: The identifier to expand. Returns: The expanded identifier. """ raise NotImplementedError @abstractmethod def get_context(self) -> AutorefsHookInterface.Context: """Get the current context. Returns: The current context. """ raise NotImplementedError class AutorefsInlineProcessor(ReferenceInlineProcessor): """A Markdown extension to handle inline references.""" name: str = "mkdocs-autorefs" """The name of the inline processor.""" hook: AutorefsHookInterface | None = None """The hook to use for expanding identifiers or adding context to autorefs.""" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(REFERENCE_RE, *args, **kwargs) # Code based on # https://github.com/Python-Markdown/markdown/blob/8e7528fa5c98bf4652deb13206d6e6241d61630b/markdown/inlinepatterns.py#L780 def handleMatch(self, m: Match[str], data: str) -> tuple[Element | None, int | None, int | None]: # type: ignore[override] # noqa: N802 """Handle an element that matched. Arguments: m: The match object. data: The matched data. Returns: A new element or a tuple. """ text, index, handled = self.getText(data, m.end(0)) if not handled: return None, None, None identifier, slug, end, handled = self._eval_id(data, index, text) if not handled or identifier is None: return None, None, None if slug is None and re.search(r"[\x00-\x1f]", identifier): # Do nothing if the matched reference still contains control characters (from 0 to 31 included) # that weren't unstashed when trying to compute a slug of the title. return None, m.start(0), end return self._make_tag(identifier, text, slug=slug), m.start(0), end def _unstash(self, identifier: str) -> str: stashed_nodes: dict[str, Element | str] = self.md.treeprocessors["inline"].stashed_nodes # type: ignore[attr-defined] def _repl(match: Match) -> str: el = stashed_nodes.get(match[1]) if isinstance(el, Element): return f"`{''.join(el.itertext())}`" if el == "\x0296\x03": return "`" return str(el) return INLINE_PLACEHOLDER_RE.sub(_repl, identifier) def _eval_id(self, data: str, index: int, text: str) -> tuple[str | None, str | None, int, bool]: """Evaluate the id portion of `[ref][id]`. If `[ref][]` use `[ref]`. Arguments: data: The data to evaluate. index: The starting position. text: The text to use when no identifier. Returns: A tuple containing the identifier, its optional slug, its end position, and whether it matched. """ m = self.RE_LINK.match(data, pos=index) if not m: return None, None, index, False if identifier := m.group(1): # An identifier was provided, match it exactly (later). slug = None else: # Only a title was provided, use it as identifier. identifier = text # Catch single stash entries, like the result of [`Foo`][]. if match := INLINE_PLACEHOLDER_RE.fullmatch(identifier): stashed_nodes: dict[str, Element | str] = self.md.treeprocessors["inline"].stashed_nodes # type: ignore[attr-defined] el = stashed_nodes.get(match[1]) if isinstance(el, Element) and el.tag == "code": # The title was wrapped in backticks, we only keep the content, # and tell autorefs to match the identifier exactly. identifier = "".join(el.itertext()) slug = None # Special case: allow pymdownx.inlinehilite raw <code> snippets but strip them back to unhighlighted. if match := HTML_PLACEHOLDER_RE.fullmatch(identifier): stash_index = int(match.group(1)) html = self.md.htmlStash.rawHtmlBlocks[stash_index] identifier = Markup(html).striptags() # noqa: S704 self.md.htmlStash.rawHtmlBlocks[stash_index] = escape(identifier) # In any other case, unstash the title and slugify it. # Examples: ``[`Foo` and `Bar`]``, `[The *Foo*][]`. else: identifier = self._unstash(identifier) slug = slugify(identifier, separator="-") end = m.end(0) return identifier, slug, end, True def _make_tag(self, identifier: str, text: str, *, slug: str | None = None) -> Element: """Create a tag that can be matched by `AUTO_REF_RE`. Arguments: identifier: The identifier to use in the HTML property. text: The text to use in the HTML tag. Returns: A new element. """ el = Element("autoref") if self.hook: identifier = self.hook.expand_identifier(identifier) el.attrib.update(self.hook.get_context().as_dict()) el.set("identifier", identifier) el.text = text if slug: el.attrib["slug"] = slug return el class AnchorScannerTreeProcessor(Treeprocessor): """Tree processor to scan and register HTML anchors.""" name: str = "mkdocs-autorefs-anchors-scanner" """The name of the tree processor.""" _htags: ClassVar[set[str]] = {"h1", "h2", "h3", "h4", "h5", "h6"} def __init__(self, plugin: AutorefsPlugin, md: Markdown | None = None) -> None: """Initialize the tree processor. Parameters: plugin: A reference to the autorefs plugin, to use its `register_anchor` method. """ super().__init__(md) self._plugin = plugin def run(self, root: Element) -> None: """Run the tree processor. Arguments: root: The root element of the tree. """ if self._plugin.current_page is not None: pending_anchors = _PendingAnchors(self._plugin) self._scan_anchors(root, pending_anchors) pending_anchors.flush() def _scan_anchors(self, parent: Element, pending_anchors: _PendingAnchors, last_heading: str | None = None) -> None: for el in parent: if el.tag == "a": # We found an anchor. Record its id if it has one. if anchor_id := el.get("id"): pending_anchors.append(anchor_id) # If the element has text or a link, it's not an alias. # Non-whitespace text after the element interrupts the chain, aliases can't apply. if el.text or el.get("href") or (el.tail and el.tail.strip()): pending_anchors.flush(title=last_heading) elif el.tag == "p": # A `p` tag is a no-op for our purposes, just recurse into it in the context # of the current collection of anchors. self._scan_anchors(el, pending_anchors, last_heading) # Non-whitespace text after the element interrupts the chain, aliases can't apply. if el.tail and el.tail.strip(): pending_anchors.flush() elif el.tag in self._htags: # If the element is a heading, that turns the pending anchors into aliases. last_heading = el.text pending_anchors.flush(el.get("id"), title=last_heading) else: # But if it's some other interruption, flush anchors anyway as non-aliases. pending_anchors.flush(title=last_heading) # Recurse into sub-elements, in a *separate* context. self.run(el) class AutorefsExtension(Extension): """Markdown extension that transforms unresolved references into auto-references. Auto-references are then resolved later by the MkDocs plugin. This extension also scans Markdown anchors (`[](){#some-id}`) to register them with the MkDocs plugin. """ def __init__( self, plugin: AutorefsPlugin | None = None, **kwargs: Any, ) -> None: """Initialize the Markdown extension. Parameters: plugin: An optional reference to the autorefs plugin (to pass it to the anchor scanner tree processor). **kwargs: Keyword arguments passed to the [base constructor][markdown.Extension]. """ super().__init__(**kwargs) self.plugin = plugin """A reference to the autorefs plugin.""" def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name) """Register the extension. Add an instance of our [`AutorefsInlineProcessor`][mkdocs_autorefs.AutorefsInlineProcessor] to the Markdown parser. Also optionally add an instance of our [`AnchorScannerTreeProcessor`][mkdocs_autorefs.AnchorScannerTreeProcessor] and [`BacklinksTreeProcessor`][mkdocs_autorefs.BacklinksTreeProcessor] to the Markdown parser if a reference to the autorefs plugin was passed to this extension. Arguments: md: A `markdown.Markdown` instance. """ md.inlinePatterns.register( AutorefsInlineProcessor(md), AutorefsInlineProcessor.name, priority=168, # Right after markdown.inlinepatterns.ReferenceInlineProcessor ) if self.plugin is not None: # Markdown anchors require the `attr_list` extension. if self.plugin.scan_toc and "attr_list" in md.treeprocessors: _log_enabling_markdown_anchors() md.treeprocessors.register( AnchorScannerTreeProcessor(self.plugin, md), AnchorScannerTreeProcessor.name, priority=0, ) # Backlinks require IDs on headings, which are either set by `toc`, # or manually by the user with `attr_list`. if self.plugin.record_backlinks and ("attr_list" in md.treeprocessors or "toc" in md.treeprocessors): _log_enabling_backlinks() md.treeprocessors.register( BacklinksTreeProcessor(self.plugin, md), BacklinksTreeProcessor.name, priority=0, ) class _PendingAnchors: """A collection of HTML anchors that may or may not become aliased to an upcoming heading.""" def __init__(self, plugin: AutorefsPlugin): self.plugin = plugin self.anchors: list[str] = [] def append(self, anchor: str) -> None: self.anchors.append(anchor) def flush(self, alias_to: str | None = None, title: str | None = None) -> None: if page := self.plugin.current_page: for anchor in self.anchors: self.plugin.register_anchor(page, anchor, alias_to, title=title, primary=True) self.anchors.clear() class _AutorefsAttrs(dict): _handled_attrs: ClassVar[set[str]] = { "identifier", "optional", "hover", # TODO: Remove at some point. "class", "domain", "role", "origin", "filepath", "lineno", "slug", "backlink-type", "backlink-anchor", } @property def context(self) -> AutorefsHookInterface.Context | None: try: return AutorefsHookInterface.Context( domain=self["domain"], role=self["role"], origin=self["origin"], filepath=self["filepath"], lineno=int(self["lineno"]), ) except KeyError: return None @property def remaining(self) -> str: return " ".join(k if v is None else f'{k}="{v}"' for k, v in self.items() if k not in self._handled_attrs) class _HTMLAttrsParser(HTMLParser): def __init__(self): super().__init__() self.attrs = {} def parse(self, html: str) -> _AutorefsAttrs: self.reset() self.attrs.clear() self.feed(html) return _AutorefsAttrs(self.attrs) def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: # noqa: ARG002 self.attrs.update(attrs) class _HTMLTagStripper(HTMLParser): def __init__(self) -> None: super().__init__() self.text = StringIO() def strip(self, html: str) -> str: self.reset() self.text = StringIO() self.feed(html) return self.text.getvalue() def handle_data(self, data: str) -> None: self.text.write(data) def relative_url(url_a: str, url_b: str) -> str: """Compute the relative path from URL A to URL B. Arguments: url_a: URL A. url_b: URL B. Returns: The relative URL to go from A to B. """ parts_a = url_a.split("/") url_b, *rest = url_b.split("#", 1) anchor = rest[0] if rest else "" parts_b = url_b.split("/") # Remove common left parts. while parts_a and parts_b and parts_a[0] == parts_b[0]: parts_a.pop(0) parts_b.pop(0) # Go up as many times as remaining a parts' depth. levels = len(parts_a) - 1 parts_relative = [".."] * levels + parts_b relative = "/".join(parts_relative) return f"{relative}#{anchor}" def fix_ref( url_mapper: Callable[[str], tuple[str, str | None]], unmapped: list[tuple[str, AutorefsHookInterface.Context | None]], record_backlink: Callable[[str, str, str], None] | None = None, *, link_titles: bool | Literal["external"] = True, strip_title_tags: bool = False, ) -> Callable: """Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub). In our context, we match Markdown references and replace them with HTML links. When the matched reference's identifier was not mapped to an URL, we append the identifier to the outer `unmapped` list. It generally means the user is trying to cross-reference an object that was not collected and rendered, making it impossible to link to it. We catch this exception in the caller to issue a warning. Arguments: url_mapper: A callable that gets an object's site URL by its identifier, such as [mkdocs_autorefs.AutorefsPlugin.get_item_url][]. unmapped: A list to store unmapped identifiers. record_backlink: A callable to record backlinks. link_titles: How to set HTML titles on links. Always (`True`), never (`False`), or external-only (`"external"`). strip_title_tags: Whether to strip HTML tags from link titles. Returns: The actual function accepting a [`Match` object](https://docs.python.org/3/library/re.html#match-objects) and returning the replacement strings. """ def inner(match: Match) -> str: title = match["title"] attrs = _html_attrs_parser.parse(f"<a {match['attrs']}>") identifier: str = attrs["identifier"] slug = attrs.get("slug", None) optional = "optional" in attrs identifiers = (identifier, slug) if slug else (identifier,) if ( record_backlink and (backlink_type := attrs.get("backlink-type")) and (backlink_anchor := attrs.get("backlink-anchor")) ): record_backlink(identifier, backlink_type, backlink_anchor) try: url, original_title = _find_url(identifiers, url_mapper) except KeyError: if optional: _log.debug("Unresolved optional cross-reference: %s", identifier) return f'<span title="{identifier}">{title}</span>' unmapped.append((identifier, attrs.context)) if title == identifier: return f"[{identifier}][]" if title == f"<code>{identifier}</code>" and not slug: return f"[<code>{identifier}</code>][]" return f"[{title}][{identifier}]" parsed = urlsplit(url) external = parsed.scheme or parsed.netloc classes = (attrs.get("class") or "").strip().split() classes = ["autorefs", "autorefs-external" if external else "autorefs-internal", *classes] class_attr = " ".join(classes) if remaining := attrs.remaining: remaining = f" {remaining}" title_attr = "" if link_titles is True or (link_titles == "external" and external): if optional: # The `optional` attribute is generally only added by mkdocstrings handlers, # for API objects, meaning we can and should append the full identifier. tooltip = _tooltip(identifier, original_title, strip_tags=strip_title_tags) else: # Autorefs without `optional` are generally user-written ones, # so we should only use the original title. tooltip = original_title or "" if tooltip and tooltip not in f"<code>{title}</code>": title_attr = f' title="{_html_tag_stripper.strip(tooltip) if strip_title_tags else escape(tooltip)}"' return f'<a class="{class_attr}"{title_attr} href="{escape(url)}"{remaining}>{title}</a>' return inner def fix_refs( html: str, url_mapper: Callable[[str], tuple[str, str | None]], *, record_backlink: Callable[[str, str, str], None] | None = None, link_titles: bool | Literal["external"] = True, strip_title_tags: bool = False, # YORE: Bump 2: Remove line. _legacy_refs: bool = True, ) -> tuple[str, list[tuple[str, AutorefsHookInterface.Context | None]]]: """Fix all references in the given HTML text. Arguments: html: The text to fix. url_mapper: A callable that gets an object's site URL by its identifier, such as [mkdocs_autorefs.AutorefsPlugin.get_item_url][]. record_backlink: A callable to record backlinks. link_titles: How to set HTML titles on links. Always (`True`), never (`False`), or external-only (`"external"`). strip_title_tags: Whether to strip HTML tags from link titles. Returns: The fixed HTML, and a list of unmapped identifiers (string and optional context). """ unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] = [] html = AUTOREF_RE.sub( fix_ref(url_mapper, unmapped, record_backlink, link_titles=link_titles, strip_title_tags=strip_title_tags), html, ) # YORE: Bump 2: Remove block. if _legacy_refs: html = AUTO_REF_RE.sub(_legacy_fix_ref(url_mapper, unmapped), html) return html, unmapped _html_attrs_parser = _HTMLAttrsParser() _html_tag_stripper = _HTMLTagStripper() def _find_url( identifiers: Iterable[str], url_mapper: Callable[[str], tuple[str, str | None]], ) -> tuple[str, str | None]: for identifier in identifiers: try: return url_mapper(identifier) except KeyError: pass raise KeyError(f"None of the identifiers {identifiers} were found") def _tooltip(identifier: str, title: str | None, *, strip_tags: bool = False) -> str: if title: # Don't append identifier if it's already in the title. if identifier in title: return title # Append identifier (useful for API objects). if strip_tags: return f"{title} ({identifier})" return f"{title} (<code>{identifier}</code>)" # No title, just return the identifier. if strip_tags: return identifier return f"<code>{identifier}</code>" @lru_cache def _log_enabling_markdown_anchors() -> None: _log.debug("Enabling Markdown anchors feature") @lru_cache def _log_enabling_backlinks() -> None: _log.debug("Enabling backlinks feature") # YORE: Bump 2: Remove block. _ATTR_VALUE = r'"[^"<>]+"|[^"<> ]+' # Possibly with double quotes around AUTO_REF_RE = re.compile( rf"<span data-(?P<kind>autorefs-(?:identifier|optional|optional-hover))=(?P<identifier>{_ATTR_VALUE})" rf"(?: class=(?P<class>{_ATTR_VALUE}))?(?P<attrs> [^<>]+)?>(?P<title>.*?)</span>", flags=re.DOTALL, ) """Deprecated. Use [`AUTOREF_RE`][mkdocs_autorefs.AUTOREF_RE] instead.""" # YORE: Bump 2: Remove block. def __getattr__(name: str) -> Any: if name == "AutoRefInlineProcessor": warnings.warn("AutoRefInlineProcessor was renamed AutorefsInlineProcessor", DeprecationWarning, stacklevel=2) return AutorefsInlineProcessor raise AttributeError(f"module 'mkdocs_autorefs.references' has no attribute {name}") # YORE: Bump 2: Remove block. def _legacy_fix_ref( url_mapper: Callable[[str], tuple[str, str | None]], unmapped: list[tuple[str, AutorefsHookInterface.Context | None]], ) -> Callable: """Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub). In our context, we match Markdown references and replace them with HTML links. When the matched reference's identifier was not mapped to an URL, we append the identifier to the outer `unmapped` list. It generally means the user is trying to cross-reference an object that was not collected and rendered, making it impossible to link to it. We catch this exception in the caller to issue a warning. Arguments: url_mapper: A callable that gets an object's site URL by its identifier, such as [mkdocs_autorefs.AutorefsPlugin.get_item_url][]. unmapped: A list to store unmapped identifiers. Returns: The actual function accepting a [`Match` object](https://docs.python.org/3/library/re.html#match-objects) and returning the replacement strings. """ def inner(match: Match) -> str: identifier = match["identifier"].strip('"') title = match["title"] kind = match["kind"] attrs = match["attrs"] or "" classes = (match["class"] or "").strip('"').split() try: url, _ = url_mapper(unescape(identifier)) except KeyError: if kind == "autorefs-optional": return title if kind == "autorefs-optional-hover": return f'<span title="{identifier}">{title}</span>' unmapped.append((identifier, None)) if title == identifier: return f"[{identifier}][]" return f"[{title}][{identifier}]" warnings.warn( "autorefs `span` elements are deprecated in favor of `autoref` elements: " f'`<span data-autorefs-identifier="{identifier}">...</span>` becomes `<autoref identifer="{identifier}">...</autoref>`', DeprecationWarning, stacklevel=1, ) parsed = urlsplit(url) external = parsed.scheme or parsed.netloc classes = ["autorefs", "autorefs-external" if external else "autorefs-internal", *classes] class_attr = " ".join(classes) if kind == "autorefs-optional-hover": return f'<a class="{class_attr}" title="{identifier}" href="{escape(url)}"{attrs}>{title}</a>' return f'<a class="{class_attr}" href="{escape(url)}"{attrs}>{title}</a>' return inner �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/plugin.py�������������������������������������������������0000664�0000000�0000000�00000000660�15013077254�0022461�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Deprecated. Import from 'mkdocs_autorefs' instead.""" # YORE: Bump 2: Remove file. import warnings from typing import Any from mkdocs_autorefs._internal import plugin def __getattr__(name: str) -> Any: warnings.warn( "Importing from 'mkdocs_autorefs.plugin' is deprecated. Import directly from 'mkdocs_autorefs' instead.", DeprecationWarning, stacklevel=2, ) return getattr(plugin, name) ��������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/py.typed��������������������������������������������������0000664�0000000�0000000�00000000000�15013077254�0022274�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/src/mkdocs_autorefs/references.py���������������������������������������������0000664�0000000�0000000�00000000674�15013077254�0023311�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Deprecated. Import from 'mkdocs_autorefs' instead.""" # YORE: Bump 2: Remove file. import warnings from typing import Any from mkdocs_autorefs._internal import references def __getattr__(name: str) -> Any: warnings.warn( "Importing from 'mkdocs_autorefs.references' is deprecated. Import directly from 'mkdocs_autorefs' instead.", DeprecationWarning, stacklevel=2, ) return getattr(references, name) ��������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�15013077254�0015772�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/__init__.py�������������������������������������������������������������0000664�0000000�0000000�00000000055�15013077254�0020103�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the mkdocs-autorefs package.""" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/conftest.py�������������������������������������������������������������0000664�0000000�0000000�00000000057�15013077254�0020173�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Configuration for the pytest test suite.""" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/helpers.py��������������������������������������������������������������0000664�0000000�0000000�00000001201�15013077254�0020000�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Helper functions for the tests.""" from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.files import File from mkdocs.structure.pages import Page from mkdocs.structure.toc import AnchorLink def create_page(url: str) -> Page: """Create a page with the given URL.""" return Page( title=url, file=File(url, "docs", "site", use_directory_urls=False), config=MkDocsConfig(), ) def create_anchor_link(title: str, anchor_id: str, level: int = 1) -> AnchorLink: """Create an anchor link.""" return AnchorLink( title=title, id=anchor_id, level=level, ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/test_api.py�������������������������������������������������������������0000664�0000000�0000000�00000016264�15013077254�0020165�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for our own API exposition.""" from __future__ import annotations from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING import griffe import pytest from mkdocstrings import Inventory import mkdocs_autorefs if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture(name="loader", scope="module") def _fixture_loader() -> griffe.GriffeLoader: loader = griffe.GriffeLoader() loader.load("mkdocs_autorefs") loader.resolve_aliases() return loader @pytest.fixture(name="internal_api", scope="module") def _fixture_internal_api(loader: griffe.GriffeLoader) -> griffe.Module: return loader.modules_collection["mkdocs_autorefs._internal"] @pytest.fixture(name="public_api", scope="module") def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: return loader.modules_collection["mkdocs_autorefs"] def _yield_public_objects( obj: griffe.Module | griffe.Class, *, modules: bool = False, modulelevel: bool = True, inherited: bool = False, special: bool = False, ) -> Iterator[griffe.Object | griffe.Alias]: for member in obj.all_members.values() if inherited else obj.members.values(): try: if member.is_module: if member.is_alias or not member.is_public: continue if modules: yield member yield from _yield_public_objects( member, # type: ignore[arg-type] modules=modules, modulelevel=modulelevel, inherited=inherited, special=special, ) elif member.is_public and (special or not member.is_special): yield member else: continue if member.is_class and not modulelevel: yield from _yield_public_objects( member, # type: ignore[arg-type] modules=modules, modulelevel=False, inherited=inherited, special=special, ) except (griffe.AliasResolutionError, griffe.CyclicAliasError): continue @pytest.fixture(name="modulelevel_internal_objects", scope="module") def _fixture_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(internal_api, modulelevel=True)) @pytest.fixture(name="internal_objects", scope="module") def _fixture_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(internal_api, modulelevel=False, special=True)) @pytest.fixture(name="public_objects", scope="module") def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) @pytest.fixture(name="inventory", scope="module") def _fixture_inventory() -> Inventory: inventory_file = Path(__file__).parent.parent / "site" / "objects.inv" if not inventory_file.exists(): raise pytest.skip("The objects inventory is not available.") with inventory_file.open("rb") as file: return Inventory.parse_sphinx(file) def test_exposed_objects(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: """All public objects in the internal API are exposed under `mkdocs_autorefs`.""" not_exposed = [ obj.path for obj in modulelevel_internal_objects if obj.name not in mkdocs_autorefs.__all__ or not hasattr(mkdocs_autorefs, obj.name) ] assert not not_exposed, "Objects not exposed:\n" + "\n".join(sorted(not_exposed)) def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: """All internal objects have unique names.""" names_to_paths = defaultdict(list) for obj in modulelevel_internal_objects: names_to_paths[obj.name].append(obj.path) non_unique = [paths for paths in names_to_paths.values() if len(paths) > 1] assert not non_unique, "Non-unique names:\n" + "\n".join(str(paths) for paths in non_unique) def test_single_locations(public_api: griffe.Module) -> None: """All objects have a single public location.""" def _public_path(obj: griffe.Object | griffe.Alias) -> bool: return obj.is_public and (obj.parent is None or _public_path(obj.parent)) multiple_locations = {} for obj_name in mkdocs_autorefs.__all__: obj = public_api[obj_name] if obj.aliases and ( public_aliases := [path for path, alias in obj.aliases.items() if path != obj.path and _public_path(alias)] ): multiple_locations[obj.path] = public_aliases assert not multiple_locations, "Multiple public locations:\n" + "\n".join( f"{path}: {aliases}" for path, aliases in multiple_locations.items() ) def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias]) -> None: """All public objects are added to the inventory.""" ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"} not_in_inventory = [ obj.path for obj in public_objects if obj.name not in ignore_names and obj.path not in inventory ] msg = "Objects not in the inventory (try running `make run mkdocs build`):\n{paths}" assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) def test_inventory_matches_api( inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias], loader: griffe.GriffeLoader, ) -> None: """The inventory doesn't contain any additional Python object.""" not_in_api = [] public_api_paths = {obj.path for obj in public_objects} public_api_paths.add("mkdocs_autorefs") ignore = {"mkdocs_autorefs.plugin", "mkdocs_autorefs.references"} for item in inventory.values(): if ( item.domain == "py" and "(" not in item.name and (item.name == "mkdocs_autorefs" or item.name.startswith("mkdocs_autorefs.")) ): obj = loader.modules_collection[item.name] if ( obj.path not in ignore and obj.path not in public_api_paths and not any(path in public_api_paths for path in obj.aliases) ): not_in_api.append(item.name) msg = "Inventory objects not in public API (try running `make run mkdocs build`):\n{paths}" assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api))) def test_no_module_docstrings_in_internal_api(internal_api: griffe.Module) -> None: """No module docstrings should be written in our internal API. The reasoning is that docstrings are addressed to users of the public API, but internal modules are not exposed to users, so they should not have docstrings. """ def _modules(obj: griffe.Module) -> Iterator[griffe.Module]: for member in obj.modules.values(): yield member yield from _modules(member) for obj in _modules(internal_api): assert not obj.docstring, f"Object {obj.path} has a docstring." ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/test_backlinks.py�������������������������������������������������������0000664�0000000�0000000�00000003660�15013077254�0021351�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the backlinks module.""" from __future__ import annotations from textwrap import dedent from markdown import Markdown from mkdocs_autorefs import AUTOREF_RE, AutorefsExtension, AutorefsPlugin, Backlink, BacklinkCrumb from mkdocs_autorefs._internal.references import _html_attrs_parser from tests.helpers import create_anchor_link, create_page def test_record_backlinks() -> None: """Check that only useful backlinks are recorded.""" plugin = AutorefsPlugin() plugin._record_backlink("foo", "referenced-by", "foo", "foo.html") assert "foo" not in plugin._backlinks plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=True) plugin._record_backlink("foo", "referenced-by", "foo", "foo.html") assert "foo" in plugin._backlinks def test_get_backlinks() -> None: """Check that backlinks can be retrieved.""" plugin = AutorefsPlugin() plugin.record_backlinks = True plugin.map_urls(create_page("foo.html"), create_anchor_link("Foo", "foo")) plugin._primary_url_map["bar"] = ["bar.html#bar"] plugin._record_backlink("bar", "referenced-by", "foo", "foo.html") assert plugin.get_backlinks("bar", from_url="") == { "referenced-by": {Backlink(crumbs=(BacklinkCrumb(title="Foo", url="foo.html#foo", parent=None),))}, } def test_backlinks_treeprocessor() -> None: """Check that the backlinks treeprocessor works.""" plugin = AutorefsPlugin() plugin.record_backlinks = True plugin.current_page = create_page("foo.html") md = Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) html = md.convert( dedent( """ [](){#alias} ## Heading [Foo][foo] """, ), ) match = AUTOREF_RE.search(html) assert match attrs = _html_attrs_parser.parse(f"<a {match['attrs']}>") assert "backlink-type" in attrs assert "backlink-anchor" in attrs ��������������������������������������������������������������������������������mkdocs-autorefs-1.4.2/tests/test_plugin.py����������������������������������������������������������0000664�0000000�0000000�00000021762�15013077254�0020711�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the plugin module.""" from __future__ import annotations import functools from typing import Literal import pytest from mkdocs.config.defaults import MkDocsConfig from mkdocs.theme import Theme from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin, fix_refs from tests.helpers import create_page def test_url_registration() -> None: """Check that URLs can be registered, then obtained.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo", page=create_page("foo1.html"), primary=True) plugin.register_url(identifier="bar", url="https://example.org/bar.html") assert plugin.get_item_url("foo") == ("foo1.html#foo", None) assert plugin.get_item_url("bar") == ("https://example.org/bar.html", None) with pytest.raises(KeyError): plugin.get_item_url("baz") def test_url_registration_with_from_url() -> None: """Check that URLs can be registered, then obtained, relative to a page.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo", page=create_page("foo1.html"), primary=True) plugin.register_url(identifier="bar", url="https://example.org/bar.html") assert plugin.get_item_url("foo", from_url="a/b.html") == ("../foo1.html#foo", None) assert plugin.get_item_url("bar", from_url="a/b.html") == ("https://example.org/bar.html", None) with pytest.raises(KeyError): plugin.get_item_url("baz", from_url="a/b.html") # YORE: Bump 2: Remove block. def test_url_registration_with_fallback() -> None: """Check that URLs can be registered, then obtained through a fallback.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo", page=create_page("foo1.html"), primary=True) plugin.register_url(identifier="bar", url="https://example.org/bar.html") # URL map will be updated with baz -> foo1.html#foo assert plugin.get_item_url("baz", fallback=lambda _: ("foo",)) == ("foo1.html#foo", None) # as expected, baz is now known as foo1.html#foo assert plugin.get_item_url("baz", fallback=lambda _: ("bar",)) == ("foo1.html#foo", None) # unknown identifiers correctly fallback: qux -> https://example.org/bar.html assert plugin.get_item_url("qux", fallback=lambda _: ("bar",)) == ("https://example.org/bar.html", None) with pytest.raises(KeyError): plugin.get_item_url("foobar", fallback=lambda _: ("baaaa",)) with pytest.raises(KeyError): plugin.get_item_url("foobar", fallback=lambda _: ()) def test_dont_make_relative_urls_relative_again() -> None: """Check that URLs are not made relative more than once.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo.bar.baz", page=create_page("foo/bar/baz.html"), primary=True) for _ in range(2): assert plugin.get_item_url("foo.bar.baz", from_url="baz/bar/foo.html") == ( "../../foo/bar/baz.html#foo.bar.baz", None, ) @pytest.mark.parametrize( ("base", "urls", "expected"), [ # One URL is closest. ("", ["x/#b", "#b"], "#b"), # Several URLs are equally close. ("a/b", ["x/#e", "a/c/#e", "a/d/#e"], "a/c/#e"), ("a/b/", ["x/#e", "a/d/#e", "a/c/#e"], "a/d/#e"), # Two close URLs, one is shorter (closer). ("a/b", ["x/#e", "a/c/#e", "a/c/d/#e"], "a/c/#e"), ("a/b/", ["x/#e", "a/c/d/#e", "a/c/#e"], "a/c/#e"), # Deeper-nested URLs. ("a/b/c", ["x/#e", "a/#e", "a/b/#e", "a/b/c/#e", "a/b/c/d/#e"], "a/b/c/#e"), ("a/b/c/", ["x/#e", "a/#e", "a/b/#e", "a/b/c/d/#e", "a/b/c/#e"], "a/b/c/#e"), # No closest URL, use first one even if longer distance. ("a", ["b/c/#d", "c/#d"], "b/c/#d"), ("a/", ["c/#d", "b/c/#d"], "c/#d"), ], ) def test_find_closest_url(base: str, urls: list[str], expected: str) -> None: """Find closest URLs given a list of URLs.""" assert AutorefsPlugin._get_closest_url(base, urls, "test") == expected def test_register_secondary_url() -> None: """Test registering secondary URLs.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=False) assert plugin._secondary_url_map == {"foo": ["foo.html#foo"]} @pytest.mark.parametrize("primary", [True, False]) def test_warn_multiple_urls(caplog: pytest.LogCaptureFixture, primary: bool) -> None: """Warn when multiple URLs are found for the same identifier.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=primary) plugin.register_anchor(identifier="foo", page=create_page("bar.html"), primary=primary) url_mapper = functools.partial(plugin.get_item_url, from_url="/hello") # YORE: Bump 2: Replace `, _legacy_refs=False` with `` within line. fix_refs('<autoref identifier="foo">Foo</autoref>', url_mapper, _legacy_refs=False) qualifier = "primary" if primary else "secondary" assert (f"Multiple {qualifier} URLs found for 'foo': ['foo.html#foo', 'bar.html#foo']" in caplog.text) is primary @pytest.mark.parametrize("primary", [True, False]) def test_use_closest_url(caplog: pytest.LogCaptureFixture, primary: bool) -> None: """Use the closest URL when multiple URLs are found for the same identifier.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.resolve_closest = True plugin.register_anchor(identifier="foo", page=create_page("foo.html"), primary=primary) plugin.register_anchor(identifier="foo", page=create_page("bar.html"), primary=primary) url_mapper = functools.partial(plugin.get_item_url, from_url="/hello") # YORE: Bump 2: Replace `, _legacy_refs=False` with `` within line. fix_refs('<autoref identifier="foo">Foo</autoref>', url_mapper, _legacy_refs=False) qualifier = "primary" if primary else "secondary" assert f"Multiple {qualifier} URLs found for 'foo': ['foo.html#foo', 'bar.html#foo']" not in caplog.text def test_on_config_hook() -> None: """Check that the `on_config` hook runs without issue.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.on_config(config=MkDocsConfig()) def test_auto_link_titles_external() -> None: """Check that `link_titles` are made external when automatic and Material is detected.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.link_titles = "auto" config = MkDocsConfig() config.theme = Theme(name="material", features=["navigation.instant.preview"]) plugin.on_config(config=config) assert plugin._link_titles == "external" def test_auto_link_titles() -> None: """Check that `link_titles` are made true when automatic and Material is not detected.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.link_titles = "auto" config = MkDocsConfig() config.theme = Theme(name="material", features=[]) plugin.on_config(config=config) assert plugin._link_titles is True config.theme = Theme("mkdocs") plugin.on_config(config=config) assert plugin._link_titles is True config.theme = Theme("readthedocs") plugin.on_config(config=config) assert plugin._link_titles is True @pytest.mark.parametrize("link_titles", ["external", True, False]) def test_explicit_link_titles(link_titles: bool | Literal["external"]) -> None: """Check that explicit `link_titles` are kept unchanged.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.link_titles = link_titles plugin.on_config(config=MkDocsConfig()) assert plugin._link_titles is link_titles def test_auto_strip_title_tags_false() -> None: """Check that `strip_title_tags` is made false when Material is detected.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.strip_title_tags = "auto" config = MkDocsConfig() config.theme = Theme(name="material", features=["content.tooltips"]) plugin.on_config(config=config) assert plugin._strip_title_tags is False def test_auto_strip_title_tags_true() -> None: """Check that `strip_title_tags` are made true when automatic and Material is not detected.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.strip_title_tags = "auto" config = MkDocsConfig() config.theme = Theme(name="material", features=[]) plugin.on_config(config=config) assert plugin._strip_title_tags is True config.theme = Theme("mkdocs") plugin.on_config(config=config) assert plugin._strip_title_tags is True config.theme = Theme("readthedocs") plugin.on_config(config=config) assert plugin._strip_title_tags is True @pytest.mark.parametrize("strip_title_tags", [True, False]) def test_explicit_strip_tags(strip_title_tags: bool) -> None: """Check that explicit `_strip_title_tags` are kept unchanged.""" plugin = AutorefsPlugin() plugin.config = AutorefsConfig() plugin.config.strip_title_tags = strip_title_tags plugin.on_config(config=MkDocsConfig()) assert plugin._strip_title_tags is strip_title_tags ��������������mkdocs-autorefs-1.4.2/tests/test_references.py������������������������������������������������������0000664�0000000�0000000�00000044616�15013077254�0021537�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the references module.""" from __future__ import annotations from textwrap import dedent from typing import TYPE_CHECKING, Any import markdown import pytest from mkdocs_autorefs import AutorefsExtension, AutorefsHookInterface, AutorefsPlugin, fix_refs, relative_url from tests.helpers import create_page if TYPE_CHECKING: from collections.abc import Mapping @pytest.mark.parametrize( ("current_url", "to_url", "href_url"), [ ("a/", "a#b", "#b"), ("a/", "a/b#c", "b#c"), ("a/b/", "a/b#c", "#c"), ("a/b/", "a/c#d", "../c#d"), ("a/b/", "a#c", "..#c"), ("a/b/c/", "d#e", "../../../d#e"), ("a/b/", "c/d/#e", "../../c/d/#e"), ("a/index.html", "a/index.html#b", "#b"), ("a/index.html", "a/b.html#c", "b.html#c"), ("a/b.html", "a/b.html#c", "#c"), ("a/b.html", "a/c.html#d", "c.html#d"), ("a/b.html", "a/index.html#c", "index.html#c"), ("a/b/c.html", "d.html#e", "../../d.html#e"), ("a/b.html", "c/d.html#e", "../c/d.html#e"), ("a/b/index.html", "a/b/c/d.html#e", "c/d.html#e"), ("", "#x", "#x"), ("a/", "#x", "../#x"), ("a/b.html", "#x", "../#x"), ("", "a/#x", "a/#x"), ("", "a/b.html#x", "a/b.html#x"), ], ) def test_relative_url(current_url: str, to_url: str, href_url: str) -> None: """Compute relative URLs correctly.""" assert relative_url(current_url, to_url) == href_url def run_references_test( url_map: Mapping[str, str], source: str, output: str, unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] | None = None, from_url: str = "page.html", extensions: Mapping[str, Mapping[str, Any]] | None = None, title_map: Mapping[str, str] | None = None, *, strip_tags: bool = True, ) -> None: """Help running tests about references. Arguments: url_map: The URL mapping. source: The source text. output: The expected output. unmapped: The expected unmapped list. from_url: The source page URL. """ extensions = extensions or {} md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions) content = md.convert(source) title_map = title_map or {} def url_mapper(identifier: str) -> tuple[str, str | None]: return relative_url(from_url, url_map[identifier]), title_map.get(identifier, None) actual_output, actual_unmapped = fix_refs(content, url_mapper, strip_title_tags=strip_tags) assert actual_output == output assert actual_unmapped == (unmapped or []) def test_reference_implicit() -> None: """Check implicit references (identifier only).""" run_references_test( url_map={"Foo": "foo.html#Foo"}, source="This [Foo][].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo">Foo</a>.</p>', ) def test_reference_explicit_with_markdown_text() -> None: """Check explicit references with Markdown formatting.""" run_references_test( url_map={"Foo": "foo.html#Foo"}, source="This [**Foo**][Foo].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><strong>Foo</strong></a>.</p>', ) def test_reference_implicit_with_code() -> None: """Check implicit references (identifier only, wrapped in backticks).""" run_references_test( url_map={"Foo": "foo.html#Foo"}, source="This [`Foo`][].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>Foo</code></a>.</p>', ) def test_reference_implicit_with_code_inlinehilite_plain() -> None: """Check implicit references (identifier in backticks, wrapped by inlinehilite).""" run_references_test( extensions={"pymdownx.inlinehilite": {}}, url_map={"pathlib.Path": "pathlib.html#Path"}, source="This [`pathlib.Path`][].", output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code>pathlib.Path</code></a>.</p>', ) def test_reference_implicit_with_code_inlinehilite_python() -> None: """Check implicit references (identifier in backticks, syntax-highlighted by inlinehilite).""" run_references_test( extensions={"pymdownx.inlinehilite": {"style_plain_text": "python"}, "pymdownx.highlight": {}}, url_map={"pathlib.Path": "pathlib.html#Path"}, source="This [`pathlib.Path`][].", output='<p>This <a class="autorefs autorefs-internal" href="pathlib.html#Path"><code class="highlight">pathlib.Path</code></a>.</p>', ) def test_reference_with_punctuation() -> None: """Check references with punctuation.""" run_references_test( url_map={'Foo&"bar': 'foo.html#Foo&"bar'}, source='This [Foo&"bar][].', output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo&"bar">Foo&"bar</a>.</p>', ) def test_reference_to_relative_path() -> None: """Check references from a page at a nested path.""" run_references_test( from_url="sub/sub/page.html", url_map={"zz": "foo.html#zz"}, source="This [zz][].", output='<p>This <a class="autorefs autorefs-internal" href="../../foo.html#zz">zz</a>.</p>', ) def test_multiline_links() -> None: """Check that links with multiline text are recognized.""" run_references_test( url_map={"foo-bar": "foo.html#bar"}, source="This [Foo\nbar][foo-bar].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo\nbar</a>.</p>', ) def test_no_reference_with_space() -> None: """Check that references with spaces are fixed.""" run_references_test( url_map={"Foo bar": "foo.html#bar"}, source="This [Foo bar][].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#bar">Foo bar</a>.</p>', ) def test_no_reference_inside_markdown() -> None: """Check that references inside code are not fixed.""" run_references_test( url_map={"Foo": "foo.html#Foo"}, source="This `[Foo][]`.", output="<p>This <code>[Foo][]</code>.</p>", ) def test_missing_reference() -> None: """Check that implicit references are correctly seen as unmapped.""" run_references_test( url_map={"NotFoo": "foo.html#NotFoo"}, source="[Foo][]", output="<p>[Foo][]</p>", unmapped=[("Foo", None)], ) def test_missing_reference_with_markdown_text() -> None: """Check unmapped explicit references.""" run_references_test( url_map={"NotFoo": "foo.html#NotFoo"}, source="[`Foo`][Foo]", output="<p>[<code>Foo</code>][]</p>", unmapped=[("Foo", None)], ) def test_missing_reference_with_markdown_id() -> None: """Check unmapped explicit references with Markdown in the identifier.""" run_references_test( url_map={"Foo": "foo.html#Foo", "NotFoo": "foo.html#NotFoo"}, source="[Foo][*NotFoo*]", output="<p>[Foo][*NotFoo*]</p>", unmapped=[("*NotFoo*", None)], ) def test_missing_reference_with_markdown_implicit() -> None: """Check that implicit references are not fixed when the identifier is not the exact one.""" run_references_test( url_map={"Foo-bar": "foo.html#Foo-bar"}, source="[*Foo-bar*][] and [`Foo`-bar][]", output="<p>[<em>Foo-bar</em>][*Foo-bar*] and [<code>Foo</code>-bar][`Foo`-bar]</p>", unmapped=[("*Foo-bar*", None), ("`Foo`-bar", None)], ) def test_reference_with_markup() -> None: """Check that references with markup are resolved (and need escaping to prevent rendering).""" run_references_test( url_map={"*a b*": "foo.html#Foo"}, source="This [*a b*][].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><em>a b</em></a>.</p>', ) run_references_test( url_map={"*a/b*": "foo.html#Foo"}, source="This [`*a/b*`][].", output='<p>This <a class="autorefs autorefs-internal" href="foo.html#Foo"><code>*a/b*</code></a>.</p>', ) # YORE: Bump 2: Remove block. def test_legacy_custom_required_reference() -> None: """Check that external HTML-based references are expanded or reported missing.""" with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): run_references_test( url_map={"ok": "ok.html#ok"}, source="<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>", output='<p>[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', unmapped=[("bar", None)], ) def test_custom_required_reference() -> None: """Check that external HTML-based references are expanded or reported missing.""" run_references_test( url_map={"ok": "ok.html#ok"}, source="<autoref identifier=bar>foo</autoref> <autoref identifier=ok>ok</autoref>", output='<p>[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', unmapped=[("bar", None)], ) # YORE: Bump 2: Remove block. def test_legacy_custom_optional_reference() -> None: """Check that optional HTML-based references are expanded and never reported missing.""" with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): run_references_test( url_map={"ok": "ok.html#ok"}, source='<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>', output='<p>foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', ) def test_custom_optional_reference() -> None: """Check that optional HTML-based references are expanded and never reported missing.""" run_references_test( url_map={"ok": "ok.html#ok"}, source='<autoref optional identifier="foo">bar</autoref> <autoref optional identifier="ok">ok</autoref>', output='<p><span title="foo">bar</span> <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a></p>', ) # YORE: Bump 2: Remove block. def test_legacy_custom_optional_hover_reference() -> None: """Check that optional-hover HTML-based references are expanded and never reported missing.""" with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): run_references_test( url_map={"ok": "ok.html#ok"}, source='<span data-autorefs-optional-hover="bar">foo</span> <span data-autorefs-optional-hover=ok>ok</span>', output='<p><span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a></p>', ) # YORE: Bump 2: Remove block. def test_legacy_external_references() -> None: """Check that external references are marked as such.""" with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): run_references_test( url_map={"example": "https://example.com/#example"}, source='<span data-autorefs-optional="example">example</span>', output='<p><a class="autorefs autorefs-external" href="https://example.com/#example">example</a></p>', ) def test_external_references() -> None: """Check that external references are marked as such.""" run_references_test( url_map={"example": "https://example.com/#example"}, source='<autoref optional identifier="example">example</autoref>', output='<p><a class="autorefs autorefs-external" href="https://example.com/#example">example</a></p>', ) def test_register_markdown_anchors() -> None: """Check that Markdown anchors are registered when enabled.""" plugin = AutorefsPlugin() md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) plugin.current_page = create_page("page") md.convert( dedent( """ [](){#foo} ## Heading foo Paragraph 1. [](){#bar} Paragraph 2. [](){#alias1} [](){#alias2} ## Heading bar [](){#alias3} Text. [](){#alias4} ## Heading baz [](){#alias5} [](){#alias6} Decoy. ## Heading more1 [](){#alias7} [decoy](){#alias8} [](){#alias9} ## Heading more2 {#heading-custom2} [](){#aliasSame} ## Same heading 1 [](){#aliasSame} ## Same heading 2 [](){#alias10} """, ), ) assert plugin._primary_url_map == { "foo": ["page#heading-foo"], "bar": ["page#bar"], "alias1": ["page#heading-bar"], "alias2": ["page#heading-bar"], "alias3": ["page#alias3"], "alias4": ["page#heading-baz"], "alias5": ["page#alias5"], "alias6": ["page#alias6"], "alias7": ["page#alias7"], "alias8": ["page#alias8"], "alias9": ["page#heading-custom2"], "alias10": ["page#alias10"], "aliasSame": ["page#same-heading-1", "page#same-heading-2"], } def test_register_markdown_anchors_with_admonition() -> None: """Check that Markdown anchors are registered inside a nested admonition element.""" plugin = AutorefsPlugin() md = markdown.Markdown(extensions=["attr_list", "toc", "admonition", AutorefsExtension(plugin)]) plugin.current_page = create_page("page") md.convert( dedent( """ [](){#alias1} !!! note ## Heading foo [](){#alias2} ## Heading bar [](){#alias3} ## Heading baz """, ), ) assert plugin._primary_url_map == { "alias1": ["page#alias1"], "alias2": ["page#heading-bar"], "alias3": ["page#alias3"], } # YORE: Bump 2: Remove block. def test_legacy_keep_data_attributes() -> None: """Keep HTML data attributes from autorefs spans.""" with pytest.warns(DeprecationWarning, match="`span` elements are deprecated"): run_references_test( url_map={"example": "https://e.com/#example"}, source='<span data-autorefs-optional="example" class="hi ho" data-foo data-bar="0">e</span>', output='<p><a class="autorefs autorefs-external hi ho" href="https://e.com/#example" data-foo data-bar="0">e</a></p>', ) def test_keep_data_attributes() -> None: """Keep HTML data attributes from autorefs spans.""" run_references_test( url_map={"example": "https://e.com#a"}, source='<autoref optional identifier="example" class="hi ho" data-foo data-bar="0">example</autoref>', output='<p><a class="autorefs autorefs-external hi ho" href="https://e.com#a" data-foo data-bar="0">example</a></p>', ) @pytest.mark.parametrize( ("markdown_ref", "exact_expected"), [ ("[Foo][]", False), ("[\\`Foo][]", False), ("[\\`\\`Foo][]", False), ("[\\`\\`Foo\\`][]", False), ("[Foo\\`][]", False), ("[Foo\\`\\`][]", False), ("[\\`Foo\\`\\`][]", False), ("[`Foo` `Bar`][]", False), ("[Foo][Foo]", True), ("[`Foo`][]", True), ("[`Foo``Bar`][]", True), ("[`Foo```Bar`][]", True), ("[``Foo```Bar``][]", True), ("[``Foo`Bar``][]", True), ("[```Foo``Bar```][]", True), ], ) def test_mark_identifiers_as_exact(markdown_ref: str, exact_expected: bool) -> None: """Mark code and explicit identifiers as exact (no `slug` attribute in autoref elements).""" plugin = AutorefsPlugin() md = markdown.Markdown(extensions=["attr_list", "toc", AutorefsExtension(plugin)]) plugin.current_page = create_page("page") output = md.convert(markdown_ref) if exact_expected: assert "slug=" not in output else: assert "slug=" in output def test_slugified_identifier_fallback() -> None: """Fallback to the slugified identifier when no URL is found.""" run_references_test( url_map={"hello-world": "https://e.com#a"}, source='<autoref identifier="Hello World" slug="hello-world">Hello World</autoref>', output='<p><a class="autorefs autorefs-external" href="https://e.com#a">Hello World</a></p>', ) run_references_test( url_map={"foo-bar": "https://e.com#a"}, source="[*Foo*-bar][]", output='<p><a class="autorefs autorefs-external" href="https://e.com#a"><em>Foo</em>-bar</a></p>', ) run_references_test( url_map={"foo-bar": "https://e.com#a"}, source="[`Foo`-bar][]", output='<p><a class="autorefs autorefs-external" href="https://e.com#a"><code>Foo</code>-bar</a></p>', ) def test_no_fallback_for_exact_identifiers() -> None: """Do not fallback to the slugified identifier for exact identifiers.""" run_references_test( url_map={"hello-world": "https://e.com"}, source='<autoref identifier="Hello World"><code>Hello World</code></autoref>', output="<p>[<code>Hello World</code>][]</p>", unmapped=[("Hello World", None)], ) run_references_test( url_map={"hello-world": "https://e.com"}, source='<autoref identifier="Hello World">Hello World</autoref>', output="<p>[Hello World][]</p>", unmapped=[("Hello World", None)], ) def test_no_fallback_for_provided_identifiers() -> None: """Do not slugify provided identifiers.""" run_references_test( url_map={"hello-world": "foo.html#hello-world"}, source="[Hello][Hello world]", output="<p>[Hello][Hello world]</p>", unmapped=[("Hello world", None)], ) def test_title_use_identifier() -> None: """Check that the identifier is used for the title.""" run_references_test( url_map={"fully.qualified.name": "ok.html#fully.qualified.name"}, source='<autoref optional identifier="fully.qualified.name">name</autoref>', output='<p><a class="autorefs autorefs-internal" title="fully.qualified.name" href="ok.html#fully.qualified.name">name</a></p>', ) def test_title_append_identifier() -> None: """Check that the identifier is appended to the title.""" run_references_test( url_map={"fully.qualified.name": "ok.html#fully.qualified.name"}, title_map={"fully.qualified.name": "Qualified Name"}, source='<autoref optional identifier="fully.qualified.name">name</autoref>', output='<p><a class="autorefs autorefs-internal" title="Qualified Name (fully.qualified.name)" href="ok.html#fully.qualified.name">name</a></p>', ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������