├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock ├── src └── autolink_references │ ├── __about__.py │ ├── __init__.py │ └── main.py └── tests └── test_parser.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: 9 | - '3.x' # the latest available python version 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-python@v5 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - uses: sksat/setup-rye@v0.10.0 16 | - run: make install 17 | - run: make test 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | pypi-publish: 9 | name: upload release to PyPI 10 | 11 | runs-on: ubuntu-latest 12 | # Specifying a GitHub environment is optional, but strongly encouraged 13 | environment: release 14 | permissions: 15 | # IMPORTANT: this permission is mandatory for trusted publishing 16 | id-token: write 17 | 18 | steps: 19 | # retrieve your distributions here 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Python 3.11 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.11" 26 | 27 | - uses: sksat/setup-rye@v0.10.0 28 | 29 | - name: Build package distributions 30 | run: make build 31 | 32 | - name: Publish package distributions to PyPI 33 | uses: pypa/gh-action-pypi-publish@release/v1 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/venv,macos,python,pycharm+all 3 | # Edit at https://www.gitignore.io/?templates=venv,macos,python,pycharm+all 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### PyCharm+all ### 34 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 35 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 36 | 37 | # User-specific stuff 38 | .idea/**/workspace.xml 39 | .idea/**/tasks.xml 40 | .idea/**/usage.statistics.xml 41 | .idea/**/dictionaries 42 | .idea/**/shelf 43 | 44 | # Generated files 45 | .idea/**/contentModel.xml 46 | 47 | # Sensitive or high-churn files 48 | .idea/**/dataSources/ 49 | .idea/**/dataSources.ids 50 | .idea/**/dataSources.local.xml 51 | .idea/**/sqlDataSources.xml 52 | .idea/**/dynamic.xml 53 | .idea/**/uiDesigner.xml 54 | .idea/**/dbnavigator.xml 55 | 56 | # Gradle 57 | .idea/**/gradle.xml 58 | .idea/**/libraries 59 | 60 | # Gradle and Maven with auto-import 61 | # When using Gradle or Maven with auto-import, you should exclude module files, 62 | # since they will be recreated, and may cause churn. Uncomment if using 63 | # auto-import. 64 | # .idea/modules.xml 65 | # .idea/*.iml 66 | # .idea/modules 67 | 68 | # CMake 69 | cmake-build-*/ 70 | 71 | # Mongo Explorer plugin 72 | .idea/**/mongoSettings.xml 73 | 74 | # File-based project format 75 | *.iws 76 | 77 | # IntelliJ 78 | out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Cursive Clojure plugin 87 | .idea/replstate.xml 88 | 89 | # Crashlytics plugin (for Android Studio and IntelliJ) 90 | com_crashlytics_export_strings.xml 91 | crashlytics.properties 92 | crashlytics-build.properties 93 | fabric.properties 94 | 95 | # Editor-based Rest Client 96 | .idea/httpRequests 97 | 98 | # Android studio 3.1+ serialized cache file 99 | .idea/caches/build_file_checksums.ser 100 | 101 | ### PyCharm+all Patch ### 102 | # Ignores the whole .idea folder and all .iml files 103 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 104 | 105 | .idea/ 106 | 107 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 108 | 109 | *.iml 110 | modules.xml 111 | .idea/misc.xml 112 | *.ipr 113 | 114 | # Sonarlint plugin 115 | .idea/sonarlint 116 | 117 | ### Python ### 118 | # Byte-compiled / optimized / DLL files 119 | __pycache__/ 120 | *.py[cod] 121 | *$py.class 122 | 123 | # C extensions 124 | *.so 125 | 126 | # Distribution / packaging 127 | .Python 128 | build/ 129 | develop-eggs/ 130 | dist/ 131 | downloads/ 132 | eggs/ 133 | .eggs/ 134 | lib/ 135 | lib64/ 136 | parts/ 137 | sdist/ 138 | var/ 139 | wheels/ 140 | pip-wheel-metadata/ 141 | share/python-wheels/ 142 | *.egg-info/ 143 | .installed.cfg 144 | *.egg 145 | MANIFEST 146 | 147 | # PyInstaller 148 | # Usually these files are written by a python script from a template 149 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 150 | *.manifest 151 | *.spec 152 | 153 | # Installer logs 154 | pip-log.txt 155 | pip-delete-this-directory.txt 156 | 157 | # Unit test / coverage reports 158 | htmlcov/ 159 | .tox/ 160 | .nox/ 161 | .coverage 162 | .coverage.* 163 | .cache 164 | nosetests.xml 165 | coverage.xml 166 | *.cover 167 | .hypothesis/ 168 | .pytest_cache/ 169 | 170 | # Translations 171 | *.mo 172 | *.pot 173 | 174 | # Django stuff: 175 | *.log 176 | local_settings.py 177 | db.sqlite3 178 | 179 | # Flask stuff: 180 | instance/ 181 | .webassets-cache 182 | 183 | # Scrapy stuff: 184 | .scrapy 185 | 186 | # Sphinx documentation 187 | docs/_build/ 188 | 189 | # PyBuilder 190 | target/ 191 | 192 | # Jupyter Notebook 193 | .ipynb_checkpoints 194 | 195 | # IPython 196 | profile_default/ 197 | ipython_config.py 198 | 199 | # pyenv 200 | .python-version 201 | 202 | # celery beat schedule file 203 | celerybeat-schedule 204 | 205 | # SageMath parsed files 206 | *.sage.py 207 | 208 | # Environments 209 | .env 210 | .venv 211 | env/ 212 | venv/ 213 | ENV/ 214 | env.bak/ 215 | venv.bak/ 216 | 217 | # Spyder project settings 218 | .spyderproject 219 | .spyproject 220 | 221 | # Rope project settings 222 | .ropeproject 223 | 224 | # VSCode project settings 225 | .vscode/ 226 | 227 | # mkdocs documentation 228 | /site 229 | 230 | # mypy 231 | .mypy_cache/ 232 | .dmypy.json 233 | dmypy.json 234 | 235 | # Pyre type checker 236 | .pyre/ 237 | 238 | ### Python Patch ### 239 | .venv/ 240 | 241 | ### venv ### 242 | # Virtualenv 243 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 244 | [Bb]in 245 | [Ii]nclude 246 | [Ll]ib 247 | [Ll]ib64 248 | [Ll]ocal 249 | [Ss]cripts 250 | pyvenv.cfg 251 | pip-selfcheck.json 252 | 253 | # End of https://www.gitignore.io/api/venv,macos,python,pycharm+all 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Saurabh Kumar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build 2 | 3 | clean: clean-build clean-pyc 4 | 5 | clean-build: 6 | rm -fr build/ 7 | rm -fr dist/ 8 | rm -fr src/*.egg-info 9 | 10 | clean-pyc: 11 | find . -name '*.pyc' -exec rm -f {} + 12 | find . -name '*.pyo' -exec rm -f {} + 13 | find . -name '*~' -exec rm -f {} + 14 | 15 | test: 16 | rye run pytest tests/ 17 | 18 | release: build 19 | rye publish 20 | 21 | release-test: build 22 | rye publish --repository testpypi --repository-url https://test.pypi.org/legacy/ 23 | 24 | build: clean 25 | rye build 26 | 27 | install: 28 | rye sync 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autolink References (MkDocs Plugin) 2 | 3 | [![PyPI - Version](https://img.shields.io/pypi/v/autolink-references-mkdocs-plugin)](https://pypi.org/project/autolink-references-mkdocs-plugin/) 4 | 5 | 6 | This [mkdocs plugin](http://www.mkdocs.org/user-guide/plugins/) 7 | look in each MkDocs article for the presence of a reference to tickets from issues 8 | trackers like Jira, Linear, etc and convert them to links that point to respective 9 | platforms: 10 | 11 | 12 | ## Getting started 13 | To install it, using `pip`: 14 | 15 | ``` 16 | pip install autolink-references-mkdocs-plugin 17 | ``` 18 | 19 | Edit your `mkdocs.yml` file and add these few lines of code: 20 | 21 | ```yaml 22 | plugins: 23 | - autolink_references: 24 | autolinks: 25 | - reference_prefix: AF- 26 | target_url: https://linear.com/AF- 27 | - reference_prefix: PROJ- 28 | target_url: https://jiracloud.com/PROJ- 29 | ``` 30 | 31 | - __reference_prefix__: This prefix appended by a number will generate a link any time it is found in a page. 32 | - __target_url__: The URL must contain `` for the reference number. 33 | 34 | ### An example 35 | 36 | For example, you could edit the `docs/index.md` file and insert the ticket references like this: 37 | 38 | ````markdown 39 | 40 | Changelog: 41 | 42 | - AF-100: add new feature. 43 | 44 | ```` 45 | 46 | This will generate pre-processed to: 47 | 48 | ``` 49 | Changelog: 50 | 51 | - [AF-100](https://linear.com/AF-100): add new feature. 52 | 53 | ``` 54 | 55 | ## Changelog 56 | 57 | ### 0.2.2 (2023-12-28) 58 | 59 | - Allow extended set for and ignore ref style links, already linked items, and attr_list cases with '#' before the ref 60 | 61 | ### 0.2.0 62 | - Ignore already linked references. 63 | - Converts text `[AF-100]` to a linked version and removes the brackets `AF-100` 64 | 65 | ## License 66 | 67 | MIT 68 | 69 | Built with ❤️ by [Saurabh Kumar](https://saurabh-kumar.com?ref=autolink-references-mkdocs-plugin) 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "autolink-references-mkdocs-plugin" 3 | description = "This plugin allows to configure your own autolink references for non-GitHub URLs." 4 | authors = [ 5 | { name = "Saurabh Kumar", email = "autolink-references-mkdocs-plugin@saurabh-kumar.com" } 6 | ] 7 | readme = "README.md" 8 | license = "MIT" 9 | requires-python = ">= 3.8" 10 | dynamic = ["version"] 11 | dependencies = ["mkdocs"] 12 | 13 | keywords = [ 14 | 'autolinks', 15 | 'mkdocs', 16 | 'regex', 17 | ] 18 | 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 3.8', 23 | 'Programming Language :: Python :: 3.9', 24 | 'Programming Language :: Python :: 3.10', 25 | 'Programming Language :: Python :: 3.11', 26 | 'Programming Language :: Python :: 3.12', 27 | 'Intended Audience :: Developers', 28 | 'Intended Audience :: System Administrators', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: OS Independent', 31 | 'Topic :: Utilities', 32 | ] 33 | 34 | [project.entry-points."mkdocs.plugins"] 35 | "autolink_references" = "autolink_references:AutolinkReference" 36 | 37 | [project.urls] 38 | Documentation = "https://github.com/theskumar/autolink-references-mkdocs-plugin/tree/main#readme" 39 | Source = "https://github.com/theskumar/autolink-references-mkdocs-plugin" 40 | Tracker = "https://github.com/theskumar/autolink-references-mkdocs-plugin/issues" 41 | 42 | [build-system] 43 | requires = ["hatchling"] 44 | build-backend = "hatchling.build" 45 | 46 | [tool.rye] 47 | managed = true 48 | dev-dependencies = [ 49 | "pytest~=7.3.2", 50 | "ruff~=0.0.272", 51 | "hatch~=1.7.0", 52 | ] 53 | 54 | [tool.hatch.metadata] 55 | allow-direct-references = true 56 | 57 | [tool.hatch.build.targets.sdist] 58 | include = [ 59 | "/src", 60 | ] 61 | 62 | [tool.hatch.build.targets.wheel] 63 | packages = ["src/autolink_references"] 64 | 65 | [tool.hatch.version] 66 | path = "src/autolink_references/__about__.py" 67 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | anyio==4.2.0 11 | certifi==2023.11.17 12 | click==8.1.7 13 | distlib==0.3.8 14 | editables==0.5 15 | filelock==3.13.1 16 | ghp-import==2.1.0 17 | h11==0.14.0 18 | hatch==1.7.0 19 | hatchling==1.21.0 20 | httpcore==1.0.2 21 | httpx==0.26.0 22 | hyperlink==21.0.0 23 | idna==3.6 24 | importlib-metadata==7.0.0 25 | iniconfig==2.0.0 26 | jaraco-classes==3.3.0 27 | jinja2==3.1.2 28 | keyring==24.3.0 29 | markdown==3.5.1 30 | markdown-it-py==3.0.0 31 | markupsafe==2.1.3 32 | mdurl==0.1.2 33 | mergedeep==1.3.4 34 | mkdocs==1.5.3 35 | more-itertools==10.1.0 36 | packaging==23.2 37 | pathspec==0.12.1 38 | pexpect==4.9.0 39 | platformdirs==4.1.0 40 | pluggy==1.3.0 41 | ptyprocess==0.7.0 42 | pygments==2.17.2 43 | pyperclip==1.8.2 44 | pytest==7.3.2 45 | python-dateutil==2.8.2 46 | pyyaml==6.0.1 47 | pyyaml-env-tag==0.1 48 | rich==13.7.0 49 | ruff==0.0.292 50 | shellingham==1.5.4 51 | six==1.16.0 52 | sniffio==1.3.0 53 | tomli-w==1.0.0 54 | tomlkit==0.12.3 55 | trove-classifiers==2023.11.29 56 | userpath==1.9.1 57 | virtualenv==20.25.0 58 | watchdog==3.0.0 59 | zipp==3.17.0 60 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | click==8.1.7 11 | ghp-import==2.1.0 12 | jinja2==3.1.2 13 | markdown==3.5.1 14 | markupsafe==2.1.3 15 | mergedeep==1.3.4 16 | mkdocs==1.5.3 17 | packaging==23.2 18 | pathspec==0.12.1 19 | platformdirs==4.1.0 20 | python-dateutil==2.8.2 21 | pyyaml==6.0.1 22 | pyyaml-env-tag==0.1 23 | six==1.16.0 24 | watchdog==3.0.0 25 | -------------------------------------------------------------------------------- /src/autolink_references/__about__.py: -------------------------------------------------------------------------------- 1 | VERSION = "0.2.2" 2 | -------------------------------------------------------------------------------- /src/autolink_references/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import AutolinkReference 2 | 3 | __all__ = ["AutolinkReference"] 4 | -------------------------------------------------------------------------------- /src/autolink_references/main.py: -------------------------------------------------------------------------------- 1 | import re 2 | from mkdocs.plugins import BasePlugin 3 | from mkdocs.config import config_options 4 | 5 | 6 | def replace_autolink_references(markdown, ref_prefix, target_url): 7 | if "" not in ref_prefix: 8 | ref_prefix = ref_prefix + "" 9 | find_regex = re.compile( 10 | r"(?", r"(?P[-\w]+)") 11 | ) 12 | linked_ref = rf"[{ref_prefix}](" + target_url + r")" 13 | replace_text = linked_ref.replace(r"", r"\g") 14 | markdown = re.sub(find_regex, replace_text, markdown, re.IGNORECASE) 15 | return markdown 16 | 17 | 18 | class AutoLinkOption(config_options.OptionallyRequired): 19 | def run_validation(self, values): 20 | if not isinstance(values, list): 21 | raise config_options.ValidationError("Expected a list of autolinks.") 22 | 23 | for autolink in values: 24 | if "reference_prefix" not in autolink: 25 | raise config_options.ValidationError( 26 | "Expected a 'reference_prefix' in autolinks." 27 | ) 28 | if "target_url" not in autolink: 29 | raise config_options.ValidationError( 30 | "Expected a 'target_url' in autolinks." 31 | ) 32 | if "" not in autolink["target_url"]: 33 | raise config_options.ValidationError("Missing '' in 'target_url'.") 34 | 35 | return values 36 | 37 | 38 | class AutolinkReference(BasePlugin): 39 | 40 | config_scheme = (("autolinks", AutoLinkOption(required=True)),) 41 | 42 | def on_page_markdown(self, markdown, **kwargs): 43 | """ 44 | Takes an article written in markdown and looks for the 45 | presence of a ticket reference and replaces it with autual link 46 | to the ticket. 47 | 48 | :param markdown: Original article in markdown format 49 | :param kwargs: Other parameters (won't be used here) 50 | :return: Modified markdown 51 | """ 52 | for autolink in self.config["autolinks"]: 53 | markdown = replace_autolink_references( 54 | markdown, autolink["reference_prefix"], autolink["target_url"] 55 | ) 56 | 57 | return markdown 58 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from autolink_references.main import replace_autolink_references as autolink 4 | 5 | simple_replace = [ 6 | ("TAG-", "http://gh/", "TAG-123", "[TAG-123](http://gh/123)"), 7 | ("TAG-", "http://gh/", "x TAG-123", "x [TAG-123](http://gh/123)"), 8 | ("TAG-", "http://gh/", "TAG-123 x", "[TAG-123](http://gh/123) x"), 9 | ("TAG-", "http://gh/", "x TAG-123 y", "x [TAG-123](http://gh/123) y"), 10 | ("TAG-", "http://gh/", "x TAG-123 y", "x [TAG-123](http://gh/123) y"), 11 | ("TAG-", "http://gh/TAG-", "(TAG-123)", "([TAG-123](http://gh/TAG-123))"), 12 | ("TAG-", "http://forgot-num/", "TAG-543", "[TAG-543](http://forgot-num/543)"), 13 | ( 14 | "TAG-", 15 | "http://gh/TAG-", 16 | "(TAG-12_3-4)", 17 | "([TAG-12_3-4](http://gh/TAG-12_3-4))", 18 | ), 19 | ( 20 | "TAG-", 21 | "http://gh/", 22 | "x TAG-123 y TAG-456 z", 23 | "x [TAG-123](http://gh/123) y [TAG-456](http://gh/456) z", 24 | ), 25 | ( 26 | "TAG-", 27 | "http://gh/TAG-", 28 | "TAG-Ab123dD", 29 | "[TAG-Ab123dD](http://gh/TAG-Ab123dD)", 30 | ), 31 | ] 32 | 33 | ignore_already_linked = [ 34 | ( 35 | "TAG-", 36 | "http://gh/", 37 | "[TAG-789](http://gh/789)", 38 | "[TAG-789](http://gh/789)", 39 | ), 40 | ( 41 | "TAG-", 42 | "http://gh/TAG-", 43 | "[TAG-789](http://gh/TAG-789)", 44 | "[TAG-789](http://gh/TAG-789)", 45 | ), 46 | ] 47 | 48 | # This test cases address #4. Reference style links should be ignored. 49 | ignore_ref_links = [ 50 | ("TAG-", "http://gh/", "[TAG-456]", "[TAG-456]"), 51 | ("TAG-", "http://gh/", "[TAG-456][test456]", "[TAG-456][test456]"), 52 | ("TAG-", "http://gh/", "[TAG-456] [tag456]", "[TAG-456] [tag456]"), 53 | ( 54 | "TAG-", 55 | "http://gh/TAG-", 56 | "[tag456]: http://gh/TAG-456", 57 | "[tag456]: http://gh/TAG-456", 58 | ), 59 | ] 60 | 61 | 62 | @pytest.mark.parametrize( 63 | "ref_prefix, target_url, test_input, expected", 64 | simple_replace + ignore_already_linked + ignore_ref_links, 65 | ) 66 | def test_parser(ref_prefix, target_url, test_input, expected): 67 | assert autolink(test_input, ref_prefix, target_url) == expected 68 | 69 | 70 | # This test address #5. It currently only checks for '#' before the link 71 | def test_with_attr_list(): 72 | text = "## Feature 1 { #F-001 .class-feature }" 73 | ref_prefix = "F-" 74 | target_url = "http://gh/" 75 | assert autolink(text, ref_prefix, target_url) == text 76 | --------------------------------------------------------------------------------