pax_global_header00006660000000000000000000000064145701237340014520gustar00rootroot0000000000000052 comment=08c8945d83de49b81050db12e09b97042056a243 mkdocs-autorefs-1.0.1/000077500000000000000000000000001457012373400146255ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/.copier-answers.yml000066400000000000000000000012201457012373400203620ustar00rootroot00000000000000# Changes here will be overwritten by Copier _commit: 1.2.3 _src_path: gh:pawamoy/copier-pdm 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 License 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.0.1/.github/000077500000000000000000000000001457012373400161655ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457012373400203505ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000027021457012373400230430ustar00rootroot00000000000000--- 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.debug # | xclip -selection clipboard ``` PASTE OUTPUT HERE ### Additional context mkdocs-autorefs-1.0.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003321457012373400223360ustar00rootroot00000000000000blank_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.0.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012131457012373400240720ustar00rootroot00000000000000--- 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.0.1/.github/workflows/000077500000000000000000000000001457012373400202225ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/.github/workflows/ci.yml000066400000000000000000000035041457012373400213420ustar00rootroot00000000000000name: 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 jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up PDM uses: pdm-project/setup-pdm@v4 with: python-version: "3.8" - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-quality - name: Install dependencies run: pdm install -G ci-quality - name: Check if the documentation builds correctly run: pdm run duty check-docs - name: Check the code quality run: pdm run duty check-quality - name: Check if the code is correctly typed run: pdm run duty check-types - name: Check for vulnerabilities in dependencies run: pdm run duty check-dependencies - name: Check for breaking changes in the API run: pdm run duty check-api tests: strategy: max-parallel: 4 matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.12' }} steps: - name: Checkout uses: actions/checkout@v4 - name: Set up PDM uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} allow-python-prereleases: true - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-tests - name: Install dependencies run: pdm install --no-editable -G ci-tests - name: Run the test suite run: pdm run duty test mkdocs-autorefs-1.0.1/.github/workflows/release.yml000066400000000000000000000011521457012373400223640ustar00rootroot00000000000000name: 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 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Setup Python uses: actions/setup-python@v4 - name: Install git-changelog run: pip install git-changelog - name: Prepare release notes run: git-changelog --release-notes > release-notes.md - name: Create release uses: softprops/action-gh-release@v1 with: body_path: release-notes.md mkdocs-autorefs-1.0.1/.gitignore000066400000000000000000000003301457012373400166110ustar00rootroot00000000000000.idea/ .vscode/ __pycache__/ *.py[cod] dist/ *.egg-info/ build/ htmlcov/ .coverage* pip-wheel-metadata/ .pytest_cache/ .python-version site/ pdm.lock pdm.toml .pdm-plugins/ .pdm-python __pypackages__/ .venv/ .cache/ mkdocs-autorefs-1.0.1/.gitpod.dockerfile000066400000000000000000000001741457012373400202240ustar00rootroot00000000000000FROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ pipx install pdm; \ pipx ensurepath mkdocs-autorefs-1.0.1/.gitpod.yml000066400000000000000000000002171457012373400167140ustar00rootroot00000000000000vscode: extensions: - ms-python.python image: file: .gitpod.dockerfile ports: - port: 8000 onOpen: notify tasks: - init: make setup mkdocs-autorefs-1.0.1/CHANGELOG.md000066400000000000000000000167711457012373400164520ustar00rootroot00000000000000# 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.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.0.1/CODE_OF_CONDUCT.md000066400000000000000000000125501457012373400174270ustar00rootroot00000000000000# 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.0.1/CONTRIBUTING.md000066400000000000000000000102431457012373400170560ustar00rootroot00000000000000# 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 > [PDM](https://github.com/pdm-project/pdm) > manually. > > You can install it with: > > ```bash > python3 -m pip install --user pipx > pipx install pdm > ``` > > Now you can try running `make setup` again, > or simply `pdm install`. You now have the dependencies installed. Run `make help` to see all the available actions! ## Tasks This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task on multiple Python versions, you run the task directly with `pdm run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-pdm/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.0.1/LICENSE000066400000000000000000000014221457012373400156310ustar00rootroot00000000000000ISC 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.0.1/Makefile000066400000000000000000000017541457012373400162740ustar00rootroot00000000000000.DEFAULT_GOAL := help SHELL := bash DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 export PDM_MULTIRUN_USE_VENVS ?= $(if $(shell pdm config python.use_venv | grep True),1,0) args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files docs_args = host port release_args = version test_args = cleancov match BASIC_DUTIES = \ changelog \ check-api \ check-dependencies \ clean \ coverage \ docs \ docs-deploy \ format \ release \ vscode QUALITY_DUTIES = \ check-quality \ check-docs \ check-types \ test .PHONY: help help: @$(DUTY) --list .PHONY: lock lock: @pdm lock -G:all .PHONY: setup setup: @bash scripts/setup.sh .PHONY: check check: @pdm multirun duty check-quality check-types check-docs @$(DUTY) check-dependencies check-api .PHONY: $(BASIC_DUTIES) $(BASIC_DUTIES): @$(DUTY) $@ $(call args,$@) .PHONY: $(QUALITY_DUTIES) $(QUALITY_DUTIES): @pdm multirun duty $@ $(call args,$@) mkdocs-autorefs-1.0.1/README.md000066400000000000000000000121711457012373400161060ustar00rootroot00000000000000# 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%20material-blue.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-blue.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 With `pip`: ```bash python3 -m 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. And the issue is somewhat exacerbated by the fact that [MkDocs supports only *relative* links between pages](https://github.com/mkdocs/mkdocs/issues/1592). Note that this plugin's behavior is undefined when trying to link to a heading title that appears several times throughout the site. Currently it arbitrarily chooses one of the pages. In such cases, use [Markdown anchors](#markdown-anchors) to add unique aliases to your headings. ### 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-pararaph]. ``` 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. mkdocs-autorefs-1.0.1/config/000077500000000000000000000000001457012373400160725ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/config/black.toml000066400000000000000000000000721457012373400200420ustar00rootroot00000000000000[tool.black] line-length = 120 exclude = "tests/fixtures" mkdocs-autorefs-1.0.1/config/coverage.ini000066400000000000000000000005351457012373400203710ustar00rootroot00000000000000[coverage:run] branch = true parallel = true source = src/ tests/ [coverage:paths] equivalent = src/ __pypackages__/ [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.0.1/config/git-changelog.toml000066400000000000000000000003121457012373400214730ustar00rootroot00000000000000bump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" mkdocs-autorefs-1.0.1/config/mypy.ini000066400000000000000000000001621457012373400175700ustar00rootroot00000000000000[mypy] ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true mkdocs-autorefs-1.0.1/config/pytest.ini000066400000000000000000000005661457012373400201320ustar00rootroot00000000000000[pytest] norecursedirs = .git .tox .env dist build python_files = test_*.py *_test.py tests.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.0.1/config/ruff.toml000066400000000000000000000041721457012373400177350ustar00rootroot00000000000000target-version = "py38" line-length = 120 exclude = [ "fixtures", "site", ] 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 ] [per-file-ignores] "src/*/cli.py" = [ "T201", # Print statement ] "src/*/debug.py" = [ "T201", # Print statement ] "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 ] [flake8-quotes] docstring-quotes = "double" [flake8-tidy-imports] ban-relative-imports = "all" [isort] known-first-party = ["mkdocs_autorefs"] [pydocstyle] convention = "google" [format] docstring-code-format = true docstring-code-line-length = 80 mkdocs-autorefs-1.0.1/config/vscode/000077500000000000000000000000001457012373400173555ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/config/vscode/launch.json000066400000000000000000000015731457012373400215300ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false }, { "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.0.1/config/vscode/settings.json000066400000000000000000000030061457012373400221070ustar00rootroot00000000000000{ "files.watcherExclude": { "**/__pypackages__/**": true, "**/.venv*/**": true, "**/venv*/**": true }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, "python.autoComplete.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "python.analysis.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "black-formatter.args": [ "--config=config/black.toml" ], "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.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.0.1/config/vscode/tasks.json000066400000000000000000000044261457012373400214030ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "changelog", "type": "shell", "command": "pdm run duty changelog" }, { "label": "check", "type": "shell", "command": "pdm run duty check" }, { "label": "check-quality", "type": "shell", "command": "pdm run duty check-quality" }, { "label": "check-types", "type": "shell", "command": "pdm run duty check-types" }, { "label": "check-docs", "type": "shell", "command": "pdm run duty check-docs" }, { "label": "check-dependencies", "type": "shell", "command": "pdm run duty check-dependencies" }, { "label": "check-api", "type": "shell", "command": "pdm run duty check-api" }, { "label": "clean", "type": "shell", "command": "pdm run duty clean" }, { "label": "docs", "type": "shell", "command": "pdm run duty docs" }, { "label": "docs-deploy", "type": "shell", "command": "pdm run duty docs-deploy" }, { "label": "format", "type": "shell", "command": "pdm run duty format" }, { "label": "lock", "type": "shell", "command": "pdm lock -G:all" }, { "label": "release", "type": "shell", "command": "pdm run duty release ${input:version}" }, { "label": "setup", "type": "shell", "command": "bash scripts/setup.sh" }, { "label": "test", "type": "shell", "command": "pdm run duty test coverage", "group": "test" }, { "label": "vscode", "type": "shell", "command": "pdm run duty vscode" } ], "inputs": [ { "id": "version", "type": "promptString", "description": "Version" } ] }mkdocs-autorefs-1.0.1/docs/000077500000000000000000000000001457012373400155555ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/docs/changelog.md000066400000000000000000000000261457012373400200240ustar00rootroot00000000000000--8<-- "CHANGELOG.md" mkdocs-autorefs-1.0.1/docs/code_of_conduct.md000066400000000000000000000000341457012373400212110ustar00rootroot00000000000000--8<-- "CODE_OF_CONDUCT.md" mkdocs-autorefs-1.0.1/docs/contributing.md000066400000000000000000000000311457012373400206000ustar00rootroot00000000000000--8<-- "CONTRIBUTING.md" mkdocs-autorefs-1.0.1/docs/credits.md000066400000000000000000000002011457012373400175250ustar00rootroot00000000000000--- hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` mkdocs-autorefs-1.0.1/docs/css/000077500000000000000000000000001457012373400163455ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/docs/css/material.css000066400000000000000000000001311457012373400206500ustar00rootroot00000000000000/* More space at the bottom of the page. */ .md-main__inner { margin-bottom: 1.5rem; } mkdocs-autorefs-1.0.1/docs/css/mkdocstrings.css000066400000000000000000000021171457012373400215670ustar00rootroot00000000000000/* 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: var(--md-typeset-a-color); } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); }mkdocs-autorefs-1.0.1/docs/index.md000066400000000000000000000000231457012373400172010ustar00rootroot00000000000000--8<-- "README.md" mkdocs-autorefs-1.0.1/docs/license.md000066400000000000000000000000441457012373400175170ustar00rootroot00000000000000# License ``` --8<-- "LICENSE" ``` mkdocs-autorefs-1.0.1/duties.py000066400000000000000000000205221457012373400164750ustar00rootroot00000000000000"""Development tasks.""" from __future__ import annotations import os import sys from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Iterator from duty import duty from duty.callables import coverage, lazy, mkdocs, mypy, pytest, ruff, safety if TYPE_CHECKING: 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("PDM_MULTIRUN", "0") == "1" def pyprefix(title: str) -> str: # noqa: D103 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]: # noqa: D103 if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: yield True finally: os.environ.pop("MATERIAL_INSIDERS") else: yield False @duty def changelog(ctx: Context) -> None: """Update the changelog in-place with latest commits. Parameters: ctx: The context instance (passed automatically). """ from git_changelog.cli import main as git_changelog ctx.run(git_changelog, args=[[]], title="Updating changelog") @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) def check(ctx: Context) -> None: # noqa: ARG001 """Check it all! Parameters: ctx: The context instance (passed automatically). """ @duty def check_quality(ctx: Context) -> None: """Check the code quality. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), command=f"ruff check --config config/ruff.toml {PY_SRC}", ) @duty def check_dependencies(ctx: Context) -> None: """Check for vulnerabilities in dependencies. Parameters: ctx: The context instance (passed automatically). """ # retrieve the list of dependencies requirements = ctx.run( ["pdm", "export", "-f", "requirements", "--without-hashes"], title="Exporting dependencies as requirements", allow_overrides=False, ) ctx.run( safety.check(requirements), title="Checking dependencies", command="pdm export -f requirements --without-hashes | safety check --stdin", ) @duty def check_docs(ctx: Context) -> None: """Check if the documentation builds correctly. Parameters: ctx: The context instance (passed automatically). """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with material_insiders(): ctx.run( mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), command="mkdocs build -vs", ) @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed. Parameters: ctx: The context instance (passed automatically). """ ctx.run( mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @duty def check_api(ctx: Context) -> None: """Check for API breaking changes. Parameters: ctx: The context instance (passed automatically). """ from griffe.cli import check as g_check griffe_check = lazy(g_check, name="griffe.check") ctx.run( griffe_check("mkdocs_autorefs", search_paths=["src"], color=True), title="Checking for API breaking changes", command="griffe check -ssrc mkdocs_autorefs", nofail=True, ) @duty(silent=True) def clean(ctx: Context) -> None: """Delete temporary files. Parameters: ctx: The context instance (passed automatically). """ ctx.run("rm -rf .coverage*") ctx.run("rm -rf .mypy_cache") ctx.run("rm -rf .pytest_cache") ctx.run("rm -rf build") ctx.run("rm -rf dist") ctx.run("rm -rf htmlcov") ctx.run("rm -rf pip-wheel-metadata") ctx.run("rm -rf site") ctx.run("find . -type d -name __pycache__ | xargs rm -rf") ctx.run("find . -name '*.rej' -delete") @duty def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( mkdocs.serve(dev_addr=f"{host}:{port}"), title="Serving documentation", capture=False, ) @duty def docs_deploy(ctx: Context) -> None: """Deploy the documentation on GitHub pages. Parameters: ctx: The context instance (passed automatically). """ 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(mkdocs.gh_deploy(), title="Deploying documentation") @duty def format(ctx: Context) -> None: """Run formatting tools on the code. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) ctx.run(ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") @duty(post=["docs-deploy"]) def release(ctx: Context, version: str) -> None: """Release a new Python package. Parameters: ctx: The context instance (passed automatically). version: The new version number to use. """ 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) ctx.run("pdm build", title="Building dist/wheel", pty=PTY) ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) @duty(silent=True, aliases=["coverage"]) def cov(ctx: Context) -> None: """Report coverage as text and HTML. Parameters: ctx: The context instance (passed automatically). """ ctx.run(coverage.combine, nofail=True) ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(coverage.html(rcfile="config/coverage.ini")) @duty def test(ctx: Context, match: str = "") -> None: """Run the test suite. Parameters: ctx: The context instance (passed automatically). cleancov: Whether to remove the `.coverage` file before running the tests. 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( pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), title=pyprefix("Running tests"), command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) @duty def vscode(ctx: Context) -> None: """Configure VSCode. This task will overwrite the following files, so make sure to back them up: - `.vscode/launch.json` - `.vscode/settings.json` - `.vscode/tasks.json` Parameters: ctx: The context instance (passed automatically). """ def update_config(filename: str) -> None: source_file = Path("config", "vscode", filename) target_file = Path(".vscode", filename) target_file.parent.mkdir(exist_ok=True) target_file.write_text(source_file.read_text()) for filename in ("launch.json", "settings.json", "tasks.json"): ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") mkdocs-autorefs-1.0.1/mkdocs.yml000066400000000000000000000072551457012373400166410ustar00rootroot00000000000000site_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 # defer to gen-files + literate-nav - API reference: - mkdocs-autorefs: reference/ - 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.indexes - 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 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: - autorefs - search - markdown-exec - gen-files: scripts: - scripts/gen_ref_nav.py - literate-nav: nav_file: SUMMARY.md - coverage - mkdocstrings: handlers: python: import: - https://docs.python.org/3/objects.inv - https://www.mkdocs.org/objects.inv - https://python-markdown.github.io/objects.inv paths: [src] options: 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_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - git-committers: enabled: !ENV [DEPLOY, false] repository: mkdocstrings/autorefs - 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/ mkdocs-autorefs-1.0.1/pyproject.toml000066400000000000000000000053721457012373400175500ustar00rootroot00000000000000[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 = {text = "ISC"} readme = "README.md" requires-python = ">=3.8" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc"] dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: ISC License (ISCL)", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "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.plugin:AutorefsPlugin" [tool.pdm] version = {source = "scm"} plugins = [ "pdm-multirun", ] [tool.pdm.build] package-dir = "src" editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.10"] ci-quality = ["mkdocs-autorefs[duty,docs,quality,typing,security]"] ci-tests = ["mkdocs-autorefs[duty,tests]"] docs = [ "black>=23.9", "markdown-callouts>=0.3", "markdown-exec>=1.7", "mkdocs>=1.5", "mkdocs-coverage>=1.0", "mkdocs-gen-files>=0.5", "mkdocs-git-committers-plugin-2>=1.2", "mkdocs-literate-nav>=0.6", "mkdocs-material>=9.4", "mkdocs-minify-plugin>=0.7", "mkdocstrings[python]>=0.23", "tomli>=2.0; python_version < '3.11'", ] maintain = [ "black>=23.9", "blacken-docs>=1.16", "git-changelog>=2.3", ] quality = [ "ruff>=0.0", ] tests = [ "pygments>=2.16", "pymdown-extensions>=10.0", "pytest>=7.4", "pytest-cov>=4.1", "pytest-randomly>=3.15", "pytest-xdist>=3.3", ] typing = [ "mypy>=1.5", "types-markdown>=3.5", "types-pyyaml>=6.0", ] security = [ "safety>=2.3", ] mkdocs-autorefs-1.0.1/scripts/000077500000000000000000000000001457012373400163145ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/scripts/gen_credits.py000066400000000000000000000114701457012373400211570ustar00rootroot00000000000000"""Script to generate the project's credits.""" from __future__ import annotations import os import re import sys from importlib.metadata import PackageNotFoundError, metadata from itertools import chain from pathlib import Path from textwrap import dedent from typing import Mapping, cast from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment # TODO: Remove once support for Python 3.10 is dropped. 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"] pdm = pyproject["tool"]["pdm"] with project_dir.joinpath("pdm.lock").open("rb") as lock_file: lock_data = tomllib.load(lock_file) lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] regex = re.compile(r"(?P[\w.-]+)(?P.*)$") def _get_license(pkg_name: str) -> str: try: data = metadata(pkg_name) except PackageNotFoundError: return "?" license_name = cast(dict, data).get("License", "").strip() multiple_lines = bool(license_name.count("\n")) # TODO: Remove author logic once all my packages licenses are fixed. author = "" if multiple_lines or not license_name or license_name == "UNKNOWN": for header, value in cast(dict, data).items(): if header == "Classifier" and value.startswith("License ::"): license_name = value.rsplit("::", 1)[1].strip() elif header == "Author-email": author = value if license_name == "Other/Proprietary License" and "pawamoy" in author: license_name = "ISC" return license_name or "?" def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: deps = {} for dep in base_deps: parsed = regex.match(dep).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name not in lock_pkgs: continue deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True while again: again = False for pkg_name in lock_pkgs: if pkg_name in deps: for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]: deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True return deps def _render_credits() -> str: dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] prod_dependencies = _get_deps( chain( # type: ignore[arg-type] project.get("dependencies", []), chain(*project.get("optional-dependencies", {}).values()), ), ) template_data = { "project_name": project_name, "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), "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/) | [`pdm`](https://pdm.fming.dev/) | [`copier-pdm`](https://github.com/pawamoy/copier-pdm) {% macro dep_line(dep) -%} [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in prod_dependencies -%} {{ dep_line(dep) }} {% endfor %} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in dev_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% 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.0.1/scripts/gen_ref_nav.py000066400000000000000000000021631457012373400211410ustar00rootroot00000000000000"""Generate the code reference pages and navigation.""" from pathlib import Path import mkdocs_gen_files nav = mkdocs_gen_files.Nav() mod_symbol = '' src = Path(__file__).parent.parent / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") elif parts[-1].startswith("_"): continue nav_parts = [f"{mod_symbol} {part}" for part in parts] nav[tuple(nav_parts)] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) mkdocs-autorefs-1.0.1/scripts/setup.sh000077500000000000000000000012171457012373400200140ustar00rootroot00000000000000#!/usr/bin/env bash set -e if ! command -v pdm &>/dev/null; then if ! command -v pipx &>/dev/null; then python3 -m pip install --user pipx fi pipx install pdm fi if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then pdm install --plugins fi if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then if [ "${PDM_MULTIRUN_USE_VENVS}" -eq "1" ]; then for version in ${PDM_MULTIRUN_VERSIONS}; do if ! pdm venv --path "${version}" &>/dev/null; then pdm venv create --name "${version}" "${version}" fi done fi pdm multirun -v pdm install -G:all else pdm install -G:all fi mkdocs-autorefs-1.0.1/src/000077500000000000000000000000001457012373400154145ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/src/mkdocs_autorefs/000077500000000000000000000000001457012373400206045ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/src/mkdocs_autorefs/__init__.py000066400000000000000000000002111457012373400227070ustar00rootroot00000000000000"""mkdocs-autorefs package. Automatically link across pages in MkDocs. """ from __future__ import annotations __all__: list[str] = [] mkdocs-autorefs-1.0.1/src/mkdocs_autorefs/debug.py000066400000000000000000000052341457012373400222500ustar00rootroot00000000000000"""Debugging utilities.""" from __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.""" 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, 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}") 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.0.1/src/mkdocs_autorefs/plugin.py000066400000000000000000000213001457012373400224500ustar00rootroot00000000000000"""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. It stores them during the [`on_page_content` event hook](https://www.mkdocs.org/user-guide/plugins/#on_page_content). Just before writing the final HTML to the disc, during the [`on_post_page` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_page), this plugin searches for references of the form `[identifier][]` or `[title][identifier]` that were not resolved, and fixes them using the previously stored identifier-URL mapping. """ from __future__ import annotations import contextlib import functools import logging from typing import TYPE_CHECKING, Any, Callable, Sequence from urllib.parse import urlsplit from mkdocs.plugins import BasePlugin from mkdocs.structure.pages import Page from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url if TYPE_CHECKING: from mkdocs.config.defaults import MkDocsConfig from mkdocs.structure.pages import Page 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 AutorefsPlugin(BasePlugin): """An `mkdocs` plugin. This plugin defines the following event hooks: - `on_config` - `on_page_content` - `on_post_page` 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 current_page: str | None = None def __init__(self) -> None: """Initialize the object.""" super().__init__() self._url_map: dict[str, str] = {} self._abs_url_map: dict[str, str] = {} self.get_fallback_anchor: Callable[[str], tuple[str, ...]] | None = None def register_anchor(self, page: str, identifier: str, anchor: str | None = None) -> None: """Register that an anchor corresponding to an identifier was encountered when rendering the page. Arguments: page: The relative URL of the current page. Examples: `'foo/bar/'`, `'foo/index.html'` identifier: The HTML anchor (without '#') as a string. """ self._url_map[identifier] = f"{page}#{anchor or identifier}" 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 def _get_item_url( self, identifier: str, fallback: Callable[[str], Sequence[str]] | None = None, ) -> str: try: return self._url_map[identifier] except KeyError: 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._url_map[identifier] = url return url raise def get_item_url( self, identifier: str, from_url: str | None = None, fallback: Callable[[str], Sequence[str]] | None = None, ) -> str: """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. fallback: An optional function to suggest alternative anchors to try on failure. Returns: A site-relative URL. """ url = self._get_item_url(identifier, fallback) if from_url is not None: parsed = urlsplit(url) if not parsed.scheme and not parsed.netloc: return relative_url(from_url, url) return url 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.references.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)) 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. """ self.current_page = page.url 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. """ if self.scan_toc: log.debug(f"Mapping identifiers to URLs for page {page.file.src_path}") for item in page.toc.items: self.map_urls(page.url, item) return html def map_urls(self, base_url: str, anchor: AnchorLink) -> None: """Recurse on every anchor to map its ID to its absolute URL. This method populates `self.url_map` by side-effect. Arguments: base_url: The base URL to use as a prefix for each anchor's relative URL. anchor: The anchor to process and to recurse on. """ self.register_anchor(base_url, anchor.id) for child in anchor.children: self.map_urls(base_url, child) def on_post_page(self, output: str, page: Page, **kwargs: Any) -> str: # noqa: ARG002 """Fix cross-references. Hook for the [`on_post_page` event](https://www.mkdocs.org/user-guide/plugins/#on_post_page). 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, but try to be smart and ignore identifiers that do not look legitimate (sometimes documentation can contain strings matching our [`AUTO_REF_RE`][mkdocs_autorefs.references.AUTO_REF_RE] regular expression that did not intend to reference anything). We currently ignore references when their identifier contains a space or a slash. Arguments: output: HTML converted from Markdown. page: The related MkDocs page instance. kwargs: Additional arguments passed by MkDocs. Returns: Modified HTML. """ log.debug(f"Fixing references in page {page.file.src_path}") url_mapper = functools.partial(self.get_item_url, from_url=page.url, fallback=self.get_fallback_anchor) fixed_output, unmapped = fix_refs(output, url_mapper) if unmapped and log.isEnabledFor(logging.WARNING): for ref in unmapped: log.warning(f"{page.file.src_path}: Could not find cross-reference target '[{ref}]'") return fixed_output mkdocs-autorefs-1.0.1/src/mkdocs_autorefs/py.typed000066400000000000000000000000001457012373400222710ustar00rootroot00000000000000mkdocs-autorefs-1.0.1/src/mkdocs_autorefs/references.py000066400000000000000000000315321457012373400233030ustar00rootroot00000000000000"""Cross-references module.""" from __future__ import annotations import logging import re from html import escape, unescape from typing import TYPE_CHECKING, Any, Callable, ClassVar, Match from urllib.parse import urlsplit from xml.etree.ElementTree import Element import markupsafe from markdown.core import Markdown from markdown.extensions import Extension from markdown.inlinepatterns import REFERENCE_RE, ReferenceInlineProcessor from markdown.treeprocessors import Treeprocessor from markdown.util import HTML_PLACEHOLDER_RE, INLINE_PLACEHOLDER_RE if TYPE_CHECKING: from markdown import Markdown from mkdocs_autorefs.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] _ATTR_VALUE = r'"[^"<>]+"|[^"<> ]+' # Possibly with double quotes around AUTO_REF_RE = re.compile( rf"autorefs-(?:identifier|optional|optional-hover))=(?P{_ATTR_VALUE})" rf"(?: class=(?P{_ATTR_VALUE}))?(?P [^<>]+)?>(?P.*?)</span>", flags=re.DOTALL, ) """A regular expression to match mkdocs-autorefs' special reference markers in the [`on_post_page` hook][mkdocs_autorefs.plugin.AutorefsPlugin.on_post_page]. """ class AutoRefInlineProcessor(ReferenceInlineProcessor): """A Markdown extension.""" def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 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, end, handled = self.evalId(data, index, text) if not handled or identifier is None: return None, None, None if re.search(r"[/ \x00-\x1f]", identifier): # Do nothing if the matched reference contains: # - a space, slash or control character (considered unintended); # - specifically \x01 is used by Python-Markdown HTML stash when there's inline formatting, # but references with Markdown formatting are not possible anyway. return None, m.start(0), end return self._make_tag(identifier, text), m.start(0), end def evalId(self, data: str, index: int, text: str) -> tuple[str | None, int, bool]: # noqa: N802 (parent's casing) """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 end position, and whether it matched. """ m = self.RE_LINK.match(data, pos=index) if not m: return None, index, False identifier = m.group(1) if not identifier: identifier = text # Allow the entire content to be one placeholder, with the intent of catching things like [`Foo`][]. # It doesn't catch [*Foo*][] though, just due to the priority order. # https://github.com/Python-Markdown/markdown/blob/1858c1b601ead62ed49646ae0d99298f41b1a271/markdown/inlinepatterns.py#L78 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": identifier = "".join(el.itertext()) # 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 = markupsafe.Markup(html).striptags() self.md.htmlStash.rawHtmlBlocks[stash_index] = escape(identifier) end = m.end(0) return identifier, end, True def _make_tag(self, identifier: str, text: str) -> 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("span") el.set("data-autorefs-identifier", identifier) el.text = text return el 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, anchor = url_b.split("#", 1) 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], str], unmapped: list[str]) -> 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.plugin.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) if title == identifier: return f"[{identifier}][]" return f"[{title}][{identifier}]" 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 def fix_refs(html: str, url_mapper: Callable[[str], str]) -> tuple[str, list[str]]: """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.plugin.AutorefsPlugin.get_item_url][]. Returns: The fixed HTML. """ unmapped: list[str] = [] html = AUTO_REF_RE.sub(fix_ref(url_mapper, unmapped), html) return html, unmapped class AnchorScannerTreeProcessor(Treeprocessor): """Tree processor to scan and register HTML anchors.""" _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: # noqa: D102 if self.plugin.current_page is not None: pending_anchors = _PendingAnchors(self.plugin, self.plugin.current_page) self._scan_anchors(root, pending_anchors) pending_anchors.flush() def _scan_anchors(self, parent: Element, pending_anchors: _PendingAnchors) -> 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() 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) # 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. pending_anchors.flush(el.get("id")) else: # But if it's some other interruption, flush anchors anyway as non-aliases. pending_anchors.flush() # Recurse into sub-elements, in a *separate* context. self.run(el) class _PendingAnchors: """A collection of HTML anchors that may or may not become aliased to an upcoming heading.""" def __init__(self, plugin: AutorefsPlugin, current_page: str): self.plugin = plugin self.current_page = current_page self.anchors: list[str] = [] def append(self, anchor: str) -> None: self.anchors.append(anchor) def flush(self, alias_to: str | None = None) -> None: for anchor in self.anchors: self.plugin.register_anchor(self.current_page, anchor, alias_to) self.anchors.clear() class AutorefsExtension(Extension): """Extension that inserts auto-references in Markdown.""" 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.extensions.Extension]. """ super().__init__(**kwargs) self.plugin = plugin def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name) """Register the extension. Add an instance of our [`AutoRefInlineProcessor`][mkdocs_autorefs.references.AutoRefInlineProcessor] to the Markdown parser. Also optionally add an instance of our [`AnchorScannerTreeProcessor`][mkdocs_autorefs.references.AnchorScannerTreeProcessor] 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( AutoRefInlineProcessor(md), "mkdocs-autorefs", priority=168, # Right after markdown.inlinepatterns.ReferenceInlineProcessor ) if self.plugin is not None and self.plugin.scan_toc and "attr_list" in md.treeprocessors: log.debug("Enabling Markdown anchors feature") md.treeprocessors.register( AnchorScannerTreeProcessor(self.plugin, md), "mkdocs-autorefs-anchors-scanner", priority=0, ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.0.1/tests/������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14570123734�0015767�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.0.1/tests/__init__.py�������������������������������������������������������������0000664�0000000�0000000�00000000055�14570123734�0020100�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the mkdocs-autorefs package.""" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.0.1/tests/conftest.py�������������������������������������������������������������0000664�0000000�0000000�00000000057�14570123734�0020170�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Configuration for the pytest test suite.""" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.0.1/tests/test_plugin.py����������������������������������������������������������0000664�0000000�0000000�00000005031�14570123734�0020675�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the plugin module.""" from __future__ import annotations import pytest from mkdocs_autorefs.plugin import AutorefsPlugin def test_url_registration() -> None: """Check that URLs can be registered, then obtained.""" plugin = AutorefsPlugin() plugin.register_anchor(identifier="foo", page="foo1.html") plugin.register_url(identifier="bar", url="https://example.org/bar.html") assert plugin.get_item_url("foo") == "foo1.html#foo" assert plugin.get_item_url("bar") == "https://example.org/bar.html" 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="foo1.html") 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" assert plugin.get_item_url("bar", from_url="a/b.html") == "https://example.org/bar.html" with pytest.raises(KeyError): plugin.get_item_url("baz", from_url="a/b.html") 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="foo1.html") 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" # as expected, baz is now known as foo1.html#foo assert plugin.get_item_url("baz", fallback=lambda _: ("bar",)) == "foo1.html#foo" # unknown identifiers correctly fallback: qux -> https://example.org/bar.html assert plugin.get_item_url("qux", fallback=lambda _: ("bar",)) == "https://example.org/bar.html" 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="foo/bar/baz.html") for _ in range(2): assert ( plugin.get_item_url("hello", from_url="baz/bar/foo.html", fallback=lambda _: ("foo.bar.baz",)) == "../../foo/bar/baz.html#foo.bar.baz" ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mkdocs-autorefs-1.0.1/tests/test_references.py������������������������������������������������������0000664�0000000�0000000�00000026766�14570123734�0021542�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the references module.""" from __future__ import annotations from textwrap import dedent from typing import Mapping import markdown import pytest from mkdocs_autorefs.plugin import AutorefsPlugin from mkdocs_autorefs.references import AutorefsExtension, fix_refs, relative_url @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: dict[str, str], source: str, output: str, unmapped: list[str] | None = None, from_url: str = "page.html", extensions: Mapping = {}, ) -> 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. """ md = markdown.Markdown(extensions=[AutorefsExtension(), *extensions], extension_configs=extensions) content = md.convert(source) def url_mapper(identifier: str) -> str: return relative_url(from_url, url_map[identifier]) actual_output, actual_unmapped = fix_refs(content, url_mapper) 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 not fixed.""" run_references_test( url_map={"Foo bar": "foo.html#Foo bar"}, source="This [Foo bar][].", output="<p>This [Foo bar][].</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"], ) 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>][Foo]</p>", unmapped=["Foo"], ) 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*"], ) 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][]</p>", unmapped=["*Foo-bar*"], ) def test_ignore_reference_with_special_char() -> None: """Check that references are not considered if there is a space character inside.""" run_references_test( url_map={"a b": "foo.html#Foo"}, source="This [*a b*][].", output="<p>This [<em>a b</em>][].</p>", ) def test_custom_required_reference() -> None: """Check that external HTML-based references are expanded or reported missing.""" url_map = {"ok": "ok.html#ok"} source = "<span data-autorefs-identifier=bar>foo</span> <span data-autorefs-identifier=ok>ok</span>" output, unmapped = fix_refs(source, url_map.__getitem__) assert output == '[foo][bar] <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' assert unmapped == ["bar"] def test_custom_optional_reference() -> None: """Check that optional HTML-based references are expanded and never reported missing.""" url_map = {"ok": "ok.html#ok"} source = '<span data-autorefs-optional="bar">foo</span> <span data-autorefs-optional=ok>ok</span>' output, unmapped = fix_refs(source, url_map.__getitem__) assert output == 'foo <a class="autorefs autorefs-internal" href="ok.html#ok">ok</a>' assert unmapped == [] def test_custom_optional_hover_reference() -> None: """Check that optional-hover HTML-based references are expanded and never reported missing.""" url_map = {"ok": "ok.html#ok"} source = '<span data-autorefs-optional-hover="bar">foo</span> <span data-autorefs-optional-hover=ok>ok</span>' output, unmapped = fix_refs(source, url_map.__getitem__) assert ( output == '<span title="bar">foo</span> <a class="autorefs autorefs-internal" title="ok" href="ok.html#ok">ok</a>' ) assert unmapped == [] def test_external_references() -> None: """Check that external references are marked as such.""" url_map = {"example": "https://example.com"} source = '<span data-autorefs-optional="example">example</span>' output, unmapped = fix_refs(source, url_map.__getitem__) assert output == '<a class="autorefs autorefs-external" href="https://example.com">example</a>' assert unmapped == [] 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 = "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} [](){#alias10} """, ), ) assert plugin._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", } 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 = "page" md.convert( dedent( """ [](){#alias1} !!! note ## Heading foo [](){#alias2} ## Heading bar [](){#alias3} ## Heading baz """, ), ) assert plugin._url_map == { "alias1": "page#alias1", "alias2": "page#heading-bar", "alias3": "page#alias3", } def test_keep_data_attributes() -> None: """Keep HTML data attributes from autorefs spans.""" url_map = {"example": "https://e.com"} source = '<span data-autorefs-optional="example" class="hi ho" data-foo data-bar="0">e</span>' output, _ = fix_refs(source, url_map.__getitem__) assert output == '<a class="autorefs autorefs-external hi ho" href="https://e.com" data-foo data-bar="0">e</a>' ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������