Skip to content

API reference

Auto-generated from the package's docstrings.

Plugin

mkdocs_source_links.plugin

MkDocs plugin entry point.

SourceLinksPlugin

Bases: BasePlugin

MkDocs plugin that rewrites parent-directory inline and reference markdown links to forge URLs.

During mkdocs build, complete inline [text](../path) links and [ref]: ../path reference definitions in each page's markdown are replaced with git-forge blob/tree/view URLs. Source files on disk are not modified.

Attributes:

Name Type Description
config_scheme PlainConfigSchema

Plugin configuration schema. Supports enabled (turn rewriting on or off), branch (override the git branch used in forge URLs), forge (override forge autodetection: one of github, gitlab, bitbucket, gitea, azure), pin (branch, commit, or tag — embed HEAD SHA or an exact tag name when set), warn_on_missing (warn when a ../ link target does not exist), and log_rewrites (opt-in INFO logging of successful rewrites: false, summary, or verbose).

Notes

Requires repo_url in mkdocs.yml. Pages without a backing file (virtual pages) are left unchanged. The git view ref is resolved once per build in on_config, which MkDocs always runs before on_page_markdown. _view_ref holds a placeholder until on_config runs.

Source code in src/mkdocs_source_links/plugin.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
class SourceLinksPlugin(BasePlugin):
    """MkDocs plugin that rewrites parent-directory inline and reference markdown links to
    forge URLs.

    During ``mkdocs build``, complete inline ``[text](../path)`` links and ``[ref]: ../path``
    reference definitions in each page's markdown are replaced with git-forge blob/tree/view URLs.
    Source files on disk are not modified.

    Attributes
    ----------
    config_scheme : PlainConfigSchema
        Plugin configuration schema. Supports ``enabled`` (turn rewriting on or off),
        ``branch`` (override the git branch used in forge URLs), ``forge`` (override forge
        autodetection: one of ``github``, ``gitlab``, ``bitbucket``, ``gitea``, ``azure``),
        ``pin`` (``branch``, ``commit``, or ``tag`` — embed HEAD SHA or an exact tag name when
        set), ``warn_on_missing`` (warn when a ``../`` link target does not exist), and
        ``log_rewrites`` (opt-in INFO logging of successful rewrites: ``false``, ``summary``, or
        ``verbose``).

    Notes
    -----
    Requires ``repo_url`` in ``mkdocs.yml``. Pages without a backing file (virtual pages) are left
    unchanged. The git view ref is resolved once per build in ``on_config``, which MkDocs always
    runs before ``on_page_markdown``. ``_view_ref`` holds a placeholder until ``on_config`` runs.
    """

    config_scheme: PlainConfigSchema = (
        ("enabled", config_options.Type(bool, default=True)),
        ("pin", config_options.Choice(("branch", "commit", "tag"), default="branch")),
        ("branch", config_options.Optional(config_options.Type(str))),
        ("forge", config_options.Optional(config_options.Choice(SUPPORTED_FORGES))),
        ("warn_on_missing", config_options.Type(bool, default=True)),
        ("log_rewrites", config_options.Choice((False, "summary", "verbose"), default=False)),
    )

    def __init__(self) -> None:
        super().__init__()
        self._view_ref = ViewRef("", "branch")
        self._rewrite_total = 0
        self._rewrite_by_page: dict[str, int] = {}
        self._warned_unknown_forge = False

    def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
        """Resolve the git view ref once per build and cache it.

        Resolving here (rather than per page) avoids running ``git`` for every page when
        ``pin: commit`` or ``pin: tag`` is set.

        Parameters
        ----------
        config : MkDocsConfig
            MkDocs site configuration.

        Returns
        -------
        MkDocsConfig
            The unmodified configuration.
        """
        self._rewrite_total = 0
        self._rewrite_by_page = {}
        self._warned_unknown_forge = False
        branch = resolve_branch(
            plugin_branch=self.config.get("branch"),
            extra=config.extra or {},
            edit_uri=config.edit_uri,
        )
        if not self.config.get("enabled", True):
            self._view_ref = ViewRef(branch, "branch")
            return config
        if not config.repo_url:
            self._view_ref = ViewRef(branch, "branch")
            return config
        resolved = resolve_view_ref(
            pin=self.config.get("pin", "branch"),
            repo_root=Path(config.config_file_path).parent,
            branch=branch,
        )
        self._view_ref = resolved.view_ref
        if resolved.used_fallback:
            log.warning(
                "pin %r could not be resolved via git; using branch %r in forge URLs",
                resolved.requested_pin,
                resolved.view_ref.ref,
            )
        return config

    def on_page_markdown(  # pylint: disable=too-many-locals
        self,
        markdown: str,
        /,
        *,
        page: Page,
        config: MkDocsConfig,
        files: Files,
    ) -> str:
        """Rewrite inline ``[text](../…)`` links and ``[ref]: ../…`` definitions to forge URLs.

        Parameters
        ----------
        markdown : str
            Markdown source for the page after metadata has been stripped.
        page : Page
            MkDocs page whose ``file.abs_src_path`` locates the doc in the repo.
        config : MkDocsConfig
            MkDocs site configuration; ``repo_url`` must be set for any
            rewriting to occur.
        files : Files
            MkDocs file collection (required by the hook signature; unused).

        Returns
        -------
        str
            Markdown with matching inline and reference links rewritten, or the original
            ``markdown`` when the plugin is disabled, ``repo_url`` is missing, or the page has no
            backing file.
        """
        _ = files
        if not self.config.get("enabled", True):
            return markdown
        if not config.repo_url:
            return markdown
        if page.file.abs_src_path is None:
            return markdown

        report_missing: Callable[[str], None] | None = None
        if self.config.get("warn_on_missing", True):
            src_path = page.file.src_path
            warned_targets: set[str] = set()

            def _warn(target: str) -> None:
                # Warn once per distinct target on a page; a target repeated in several links
                # would otherwise emit a duplicate warning for each occurrence.
                if target in warned_targets:
                    return
                warned_targets.add(target)
                log.warning("Link target does not exist: %s (in %s)", target, src_path)

            report_missing = _warn

        log_mode = self.config.get("log_rewrites", False)
        report_rewrite: Callable[[], None] | None = None
        report_skipped_shared_label: Callable[[str], None] | None = None
        page_rewrites = 0
        if log_mode:

            def _count_rewrite() -> None:
                nonlocal page_rewrites
                page_rewrites += 1

            report_rewrite = _count_rewrite

            if log_mode == "verbose":
                src_path = page.file.src_path

                def _log_skipped_shared_label(label: str) -> None:
                    log.info(
                        "%s: skipped [%s] reference definition (image reference label)",
                        src_path,
                        label,
                    )

                report_skipped_shared_label = _log_skipped_shared_label

        report_unknown_forge: Callable[[], None] | None = None
        if self.config.get("forge") is None:

            def _warn_unknown_forge() -> None:
                if self._warned_unknown_forge:
                    return
                self._warned_unknown_forge = True
                log.warning(
                    "Could not detect git forge from repo_url %r; set forge: in mkdocs.yml "
                    "to rewrite ../ links",
                    config.repo_url,
                )

            report_unknown_forge = _warn_unknown_forge

        result = rewrite_repo_parent_links(
            markdown,
            page_abs_path=Path(page.file.abs_src_path),
            repo_root=Path(config.config_file_path).parent,
            repo_url=config.repo_url,
            view_ref=self._view_ref,
            forge=self.config.get("forge"),
            report_missing=report_missing,
            report_rewrite=report_rewrite,
            report_unknown_forge=report_unknown_forge,
            report_skipped_shared_label=report_skipped_shared_label,
        )
        if page_rewrites:
            src_path = page.file.src_path
            self._rewrite_total += page_rewrites
            self._rewrite_by_page[src_path] = self._rewrite_by_page.get(src_path, 0) + page_rewrites
        return result

    def on_post_build(
        self,
        *,
        config: MkDocsConfig,
        **kwargs: object,
    ) -> None:
        """Log rewrite statistics when ``log_rewrites`` is enabled.

        Parameters
        ----------
        config : MkDocsConfig
            MkDocs site configuration; ``repo_url`` must be set for rewrite statistics.
        **kwargs : object
            Additional keyword arguments passed by MkDocs (unused; reserved for hook
            compatibility).
        """
        _ = kwargs
        if not self.config.get("enabled", True):
            return
        if not config.repo_url:
            return
        log_mode = self.config.get("log_rewrites", False)
        if not log_mode:
            return

        if log_mode == "verbose":
            for src_path in sorted(self._rewrite_by_page):
                count = self._rewrite_by_page[src_path]
                log.info(
                    "%s: rewrote %d %s",
                    src_path,
                    count,
                    _plural(count, "link"),
                )

        page_count = len(self._rewrite_by_page)
        log.info(
            "Rewrote %d %s across %d %s",
            self._rewrite_total,
            _plural(self._rewrite_total, "link"),
            page_count,
            _plural(page_count, "page"),
        )

on_config

on_config(config)

Resolve the git view ref once per build and cache it.

Resolving here (rather than per page) avoids running git for every page when pin: commit or pin: tag is set.

Parameters:

Name Type Description Default
config MkDocsConfig

MkDocs site configuration.

required

Returns:

Type Description
MkDocsConfig

The unmodified configuration.

Source code in src/mkdocs_source_links/plugin.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
    """Resolve the git view ref once per build and cache it.

    Resolving here (rather than per page) avoids running ``git`` for every page when
    ``pin: commit`` or ``pin: tag`` is set.

    Parameters
    ----------
    config : MkDocsConfig
        MkDocs site configuration.

    Returns
    -------
    MkDocsConfig
        The unmodified configuration.
    """
    self._rewrite_total = 0
    self._rewrite_by_page = {}
    self._warned_unknown_forge = False
    branch = resolve_branch(
        plugin_branch=self.config.get("branch"),
        extra=config.extra or {},
        edit_uri=config.edit_uri,
    )
    if not self.config.get("enabled", True):
        self._view_ref = ViewRef(branch, "branch")
        return config
    if not config.repo_url:
        self._view_ref = ViewRef(branch, "branch")
        return config
    resolved = resolve_view_ref(
        pin=self.config.get("pin", "branch"),
        repo_root=Path(config.config_file_path).parent,
        branch=branch,
    )
    self._view_ref = resolved.view_ref
    if resolved.used_fallback:
        log.warning(
            "pin %r could not be resolved via git; using branch %r in forge URLs",
            resolved.requested_pin,
            resolved.view_ref.ref,
        )
    return config

on_page_markdown

on_page_markdown(markdown, /, *, page, config, files)

Rewrite inline [text](../…) links and [ref]: ../… definitions to forge URLs.

Parameters:

Name Type Description Default
markdown str

Markdown source for the page after metadata has been stripped.

required
page Page

MkDocs page whose file.abs_src_path locates the doc in the repo.

required
config MkDocsConfig

MkDocs site configuration; repo_url must be set for any rewriting to occur.

required
files Files

MkDocs file collection (required by the hook signature; unused).

required

Returns:

Type Description
str

Markdown with matching inline and reference links rewritten, or the original markdown when the plugin is disabled, repo_url is missing, or the page has no backing file.

Source code in src/mkdocs_source_links/plugin.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def on_page_markdown(  # pylint: disable=too-many-locals
    self,
    markdown: str,
    /,
    *,
    page: Page,
    config: MkDocsConfig,
    files: Files,
) -> str:
    """Rewrite inline ``[text](../…)`` links and ``[ref]: ../…`` definitions to forge URLs.

    Parameters
    ----------
    markdown : str
        Markdown source for the page after metadata has been stripped.
    page : Page
        MkDocs page whose ``file.abs_src_path`` locates the doc in the repo.
    config : MkDocsConfig
        MkDocs site configuration; ``repo_url`` must be set for any
        rewriting to occur.
    files : Files
        MkDocs file collection (required by the hook signature; unused).

    Returns
    -------
    str
        Markdown with matching inline and reference links rewritten, or the original
        ``markdown`` when the plugin is disabled, ``repo_url`` is missing, or the page has no
        backing file.
    """
    _ = files
    if not self.config.get("enabled", True):
        return markdown
    if not config.repo_url:
        return markdown
    if page.file.abs_src_path is None:
        return markdown

    report_missing: Callable[[str], None] | None = None
    if self.config.get("warn_on_missing", True):
        src_path = page.file.src_path
        warned_targets: set[str] = set()

        def _warn(target: str) -> None:
            # Warn once per distinct target on a page; a target repeated in several links
            # would otherwise emit a duplicate warning for each occurrence.
            if target in warned_targets:
                return
            warned_targets.add(target)
            log.warning("Link target does not exist: %s (in %s)", target, src_path)

        report_missing = _warn

    log_mode = self.config.get("log_rewrites", False)
    report_rewrite: Callable[[], None] | None = None
    report_skipped_shared_label: Callable[[str], None] | None = None
    page_rewrites = 0
    if log_mode:

        def _count_rewrite() -> None:
            nonlocal page_rewrites
            page_rewrites += 1

        report_rewrite = _count_rewrite

        if log_mode == "verbose":
            src_path = page.file.src_path

            def _log_skipped_shared_label(label: str) -> None:
                log.info(
                    "%s: skipped [%s] reference definition (image reference label)",
                    src_path,
                    label,
                )

            report_skipped_shared_label = _log_skipped_shared_label

    report_unknown_forge: Callable[[], None] | None = None
    if self.config.get("forge") is None:

        def _warn_unknown_forge() -> None:
            if self._warned_unknown_forge:
                return
            self._warned_unknown_forge = True
            log.warning(
                "Could not detect git forge from repo_url %r; set forge: in mkdocs.yml "
                "to rewrite ../ links",
                config.repo_url,
            )

        report_unknown_forge = _warn_unknown_forge

    result = rewrite_repo_parent_links(
        markdown,
        page_abs_path=Path(page.file.abs_src_path),
        repo_root=Path(config.config_file_path).parent,
        repo_url=config.repo_url,
        view_ref=self._view_ref,
        forge=self.config.get("forge"),
        report_missing=report_missing,
        report_rewrite=report_rewrite,
        report_unknown_forge=report_unknown_forge,
        report_skipped_shared_label=report_skipped_shared_label,
    )
    if page_rewrites:
        src_path = page.file.src_path
        self._rewrite_total += page_rewrites
        self._rewrite_by_page[src_path] = self._rewrite_by_page.get(src_path, 0) + page_rewrites
    return result

on_post_build

on_post_build(*, config, **kwargs)

Log rewrite statistics when log_rewrites is enabled.

Parameters:

Name Type Description Default
config MkDocsConfig

MkDocs site configuration; repo_url must be set for rewrite statistics.

required
**kwargs object

Additional keyword arguments passed by MkDocs (unused; reserved for hook compatibility).

{}
Source code in src/mkdocs_source_links/plugin.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def on_post_build(
    self,
    *,
    config: MkDocsConfig,
    **kwargs: object,
) -> None:
    """Log rewrite statistics when ``log_rewrites`` is enabled.

    Parameters
    ----------
    config : MkDocsConfig
        MkDocs site configuration; ``repo_url`` must be set for rewrite statistics.
    **kwargs : object
        Additional keyword arguments passed by MkDocs (unused; reserved for hook
        compatibility).
    """
    _ = kwargs
    if not self.config.get("enabled", True):
        return
    if not config.repo_url:
        return
    log_mode = self.config.get("log_rewrites", False)
    if not log_mode:
        return

    if log_mode == "verbose":
        for src_path in sorted(self._rewrite_by_page):
            count = self._rewrite_by_page[src_path]
            log.info(
                "%s: rewrote %d %s",
                src_path,
                count,
                _plural(count, "link"),
            )

    page_count = len(self._rewrite_by_page)
    log.info(
        "Rewrote %d %s across %d %s",
        self._rewrite_total,
        _plural(self._rewrite_total, "link"),
        page_count,
        _plural(page_count, "page"),
    )

mkdocs_source_links.rewrite

Rewrite [text](../path) inline links and [ref]: ../path definitions to forge view URLs.

:func:rewrite_repo_parent_links collects image-reference labels, rewrites inline links, then reference definitions. The markdown parsing primitives live in the internal _scan and _fences modules, and ../ path resolution lives in the internal _paths module.

repo_relative_path

repo_relative_path(*, page_abs_path, href, repo_root)

Resolve a ../ link from a doc page to a repo-root-relative POSIX path.

The returned path reflects the link text (lexical path). Symlink targets are not followed when building the forge URL path segment, but resolve() is still used to verify the target lies inside repo_root.

Parameters:

Name Type Description Default
page_abs_path Path

Absolute path to the page's markdown file on disk.

required
href str

Link target as written in the markdown; only ../ targets are resolved.

required
repo_root Path

Repository root used to resolve the target and build the relative path.

required

Returns:

Type Description
str | None

Repo-root-relative POSIX path, or None if href is not a ../ link or resolves outside repo_root.

Source code in src/mkdocs_source_links/_paths.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def repo_relative_path(*, page_abs_path: Path, href: str, repo_root: Path) -> str | None:
    """Resolve a ``../`` link from a doc page to a repo-root-relative POSIX path.

    The returned path reflects the link text (lexical path). Symlink targets are not followed
    when building the forge URL path segment, but ``resolve()`` is still used to verify the
    target lies inside ``repo_root``.

    Parameters
    ----------
    page_abs_path : Path
        Absolute path to the page's markdown file on disk.
    href : str
        Link target as written in the markdown; only ``../`` targets are resolved.
    repo_root : Path
        Repository root used to resolve the target and build the relative path.

    Returns
    -------
    str | None
        Repo-root-relative POSIX path, or ``None`` if ``href`` is not a ``../`` link or resolves
        outside ``repo_root``.
    """
    resolved = resolve_parent_href(page_abs_path=page_abs_path, href=href, repo_root=repo_root)
    return resolved[0] if resolved is not None else None
rewrite_repo_parent_links(
    markdown,
    *,
    page_abs_path,
    repo_root,
    repo_url,
    view_ref,
    forge=None,
    report_missing=None,
    report_rewrite=None,
    report_unknown_forge=None,
    report_skipped_shared_label=None,
)

Replace complete inline [text](../…) links and [ref]: ../… definitions with forge URLs.

Inline rewrites require a bracket-balanced [label](../path) pair; lonely ](../path) suffixes in prose are left unchanged. Only links whose target resolves to an existing file or directory inside repo_root are rewritten. Unsupported hosts, paths outside the repo, and missing targets are left unchanged.

Parameters:

Name Type Description Default
markdown str

Markdown source text for a single documentation page.

required
page_abs_path Path

Absolute path to the page's markdown file on disk.

required
repo_root Path

Repository root used to resolve ../ targets and to build repo-relative paths for the forge URL.

required
repo_url str

Forge repository URL from mkdocs.yml (for example a GitHub URL).

required
view_ref ViewRef

Git branch name, tag name, or commit SHA and its kind (branch, commit, or tag) for forge view URLs.

required
forge str | None

Explicit forge name; when None the forge is autodetected from repo_url.

None
report_missing Callable[[str], None] | None

Optional callback invoked with the link target (as written in the markdown) for each ../ link that resolves inside repo_root but points at a path that does not exist on disk. Links outside the repo are not reported.

None
report_rewrite Callable[[], None] | None

Optional callback invoked once for each successfully rewritten inline link or reference definition.

None
report_unknown_forge Callable[[], None] | None

Optional callback invoked when a ../ link would be rewritten but the git forge could not be determined from repo_url and no explicit forge was set.

None
report_skipped_shared_label Callable[[str], None] | None

Optional callback invoked when a [label]: ../path definition is skipped because label is also used by an image reference on the page.

None

Returns:

Type Description
str

Markdown with matching inline and reference links rewritten to forge URLs.

Notes

Directory targets use tree URLs; file targets use blob URLs (on forges that distinguish them). URL fragments (#anchor) and link titles are preserved, and angle-bracket destinations ([text](<../x>)) are supported. Inline images (![alt](../path)), including alt text with nested or escaped ] characters, and image reference definitions are left unchanged because forge blob URLs are HTML pages, not raw image assets. Links inside fenced code blocks and inline code spans are left unchanged. Reference-style definitions ([ref]: ../path) are rewritten when not inside a fenced code block and not used as an image reference label.

The report_* callbacks are opt-in: when omitted, the corresponding events (missing targets, rewrites, undetectable forge, skipped shared labels) are silently ignored and only affect the returned markdown. The plugin (:class:mkdocs_source_links.plugin.SourceLinksPlugin) wires these callbacks to MkDocs logging — for example it passes report_unknown_forge only when no explicit forge is configured and warns once per build. Direct API callers that want the same warnings must pass their own callbacks.

Source code in src/mkdocs_source_links/rewrite.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def rewrite_repo_parent_links(  # pylint: disable=too-many-arguments
    markdown: str,
    *,
    page_abs_path: Path,
    repo_root: Path,
    repo_url: str,
    view_ref: ViewRef,
    forge: str | None = None,
    report_missing: Callable[[str], None] | None = None,
    report_rewrite: Callable[[], None] | None = None,
    report_unknown_forge: Callable[[], None] | None = None,
    report_skipped_shared_label: Callable[[str], None] | None = None,
) -> str:
    """Replace complete inline ``[text](../…)`` links and ``[ref]: ../…`` definitions with
    forge URLs.

    Inline rewrites require a bracket-balanced ``[label](../path)`` pair; lonely ``](../path)``
    suffixes in prose are left unchanged. Only links whose target resolves to an existing file or
    directory inside ``repo_root`` are rewritten. Unsupported hosts, paths outside the repo, and
    missing targets are left unchanged.

    Parameters
    ----------
    markdown : str
        Markdown source text for a single documentation page.
    page_abs_path : Path
        Absolute path to the page's markdown file on disk.
    repo_root : Path
        Repository root used to resolve ``../`` targets and to build
        repo-relative paths for the forge URL.
    repo_url : str
        Forge repository URL from ``mkdocs.yml`` (for example a GitHub URL).
    view_ref : ViewRef
        Git branch name, tag name, or commit SHA and its kind (``branch``, ``commit``, or
        ``tag``) for forge view URLs.
    forge : str | None
        Explicit forge name; when ``None`` the forge is autodetected from ``repo_url``.
    report_missing : Callable[[str], None] | None
        Optional callback invoked with the link target (as written in the markdown) for each
        ``../`` link that resolves inside ``repo_root`` but points at a path that does not exist
        on disk. Links outside the repo are not reported.
    report_rewrite : Callable[[], None] | None
        Optional callback invoked once for each successfully rewritten inline link or reference
        definition.
    report_unknown_forge : Callable[[], None] | None
        Optional callback invoked when a ``../`` link would be rewritten but the git forge could
        not be determined from ``repo_url`` and no explicit ``forge`` was set.
    report_skipped_shared_label : Callable[[str], None] | None
        Optional callback invoked when a ``[label]: ../path`` definition is skipped because
        ``label`` is also used by an image reference on the page.

    Returns
    -------
    str
        Markdown with matching inline and reference links rewritten to forge URLs.

    Notes
    -----
    Directory targets use ``tree`` URLs; file targets use ``blob`` URLs (on forges that
    distinguish them). URL fragments (``#anchor``) and link titles are preserved, and
    angle-bracket destinations (``[text](<../x>)``) are supported. Inline images
    (``![alt](../path)``), including alt text with nested or escaped ``]`` characters, and image
    reference definitions are left unchanged because forge blob URLs are HTML pages, not raw image
    assets. Links inside fenced code blocks and inline code spans are left unchanged.
    Reference-style definitions (``[ref]: ../path``) are rewritten when not inside a fenced code
    block and not used as an image reference label.

    The ``report_*`` callbacks are opt-in: when omitted, the corresponding events (missing targets,
    rewrites, undetectable forge, skipped shared labels) are silently ignored and only affect the
    returned markdown. The plugin (:class:`mkdocs_source_links.plugin.SourceLinksPlugin`) wires
    these callbacks to MkDocs logging — for example it passes ``report_unknown_forge`` only when no
    explicit ``forge`` is configured and warns once per build. Direct API callers that want the
    same warnings must pass their own callbacks.
    """

    image_ref_labels = collect_image_reference_labels(markdown)
    ctx = _RewriteContext(
        page_abs_path=page_abs_path,
        repo_root=repo_root,
        repo_url=repo_url,
        view_ref=view_ref,
        resolved_forge=forge or detect_forge(repo_url),
        report_missing=report_missing,
        report_rewrite=report_rewrite,
        report_unknown_forge=report_unknown_forge,
        report_skipped_shared_label=report_skipped_shared_label,
        image_ref_labels=image_ref_labels,
    )

    rewritten = _rewrite_inline_links(markdown, ctx)
    return _rewrite_reference_definitions(rewritten, ctx)

Forge URLs

mkdocs_source_links.urls

Forge view URL builders with host autodetection.

Supports GitHub, GitLab, Bitbucket Cloud, Gitea/Forgejo (incl. Codeberg), and Azure DevOps. Public forge hosts are detected automatically; self-hosted instances on non-obvious domains can be selected with an explicit forge name.

detect_forge

detect_forge(repo_url)

Identify the git forge that hosts a repository URL.

Detection first matches well-known public hosts exactly, then falls back to hostname-label hints so self-hosted instances (for example GitHub Enterprise at github.example.com) are recognized. Ambiguous custom domains return None and should be configured explicitly.

Parameters:

Name Type Description Default
repo_url str

Repository URL from mkdocs.yml (repo_url).

required

Returns:

Type Description
str | None

A forge name from :data:SUPPORTED_FORGES, or None if the host is unknown.

Source code in src/mkdocs_source_links/urls.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def detect_forge(repo_url: str) -> str | None:
    """Identify the git forge that hosts a repository URL.

    Detection first matches well-known public hosts exactly, then falls back to hostname-label
    hints so self-hosted instances (for example GitHub Enterprise at ``github.example.com``) are
    recognized. Ambiguous custom domains return ``None`` and should be configured explicitly.

    Parameters
    ----------
    repo_url : str
        Repository URL from ``mkdocs.yml`` (``repo_url``).

    Returns
    -------
    str | None
        A forge name from :data:`SUPPORTED_FORGES`, or ``None`` if the host is unknown.
    """
    host = (urlsplit(repo_url).hostname or "").lower()
    if not host:
        return None
    if host in _KNOWN_HOSTS:
        return _KNOWN_HOSTS[host]
    if host.endswith(".visualstudio.com"):
        return "azure"
    for needle, forge in _HOST_HINTS:
        if _host_matches_hint(host, needle):
            return forge
    return None

repo_view_url

repo_view_url(
    *,
    repo_url,
    ref,
    ref_kind,
    repo_path,
    is_dir,
    forge=None,
)

Build a git-forge view URL for a path inside the repository.

Parameters:

Name Type Description Default
repo_url str

Forge repository URL from mkdocs.yml.

required
ref str

Git branch name, tag name, or commit SHA to embed in the URL.

required
ref_kind RefKind

Whether ref is a branch name, tag name, or commit SHA. Azure DevOps uses different version prefixes (GB, GT, and GC) depending on this value.

required
repo_path str

Repo-root-relative POSIX path to the target file or directory.

required
is_dir bool

Whether repo_path is a directory or a file. Forges that distinguish the two (GitHub, GitLab) use tree vs blob accordingly.

required
forge str | None

Explicit forge name from :data:SUPPORTED_FORGES. When None, the forge is autodetected from repo_url.

None

Returns:

Type Description
str | None

Forge view URL, or None if the forge could not be determined.

Raises:

Type Description
ValueError

If forge is set to a name that is not in :data:SUPPORTED_FORGES.

Source code in src/mkdocs_source_links/urls.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def repo_view_url(
    *,
    repo_url: str,
    ref: str,
    ref_kind: RefKind,
    repo_path: str,
    is_dir: bool,
    forge: str | None = None,
) -> str | None:
    """Build a git-forge view URL for a path inside the repository.

    Parameters
    ----------
    repo_url : str
        Forge repository URL from ``mkdocs.yml``.
    ref : str
        Git branch name, tag name, or commit SHA to embed in the URL.
    ref_kind : RefKind
        Whether ``ref`` is a branch name, tag name, or commit SHA. Azure DevOps uses different
        version prefixes (``GB``, ``GT``, and ``GC``) depending on this value.
    repo_path : str
        Repo-root-relative POSIX path to the target file or directory.
    is_dir : bool
        Whether ``repo_path`` is a directory or a file. Forges that distinguish the two
        (GitHub, GitLab) use ``tree`` vs ``blob`` accordingly.
    forge : str | None
        Explicit forge name from :data:`SUPPORTED_FORGES`. When ``None``, the forge is
        autodetected from ``repo_url``.

    Returns
    -------
    str | None
        Forge view URL, or ``None`` if the forge could not be determined.

    Raises
    ------
    ValueError
        If ``forge`` is set to a name that is not in :data:`SUPPORTED_FORGES`.
    """
    forge_name = forge or detect_forge(repo_url)
    if forge_name is None:
        return None
    if forge_name not in _BUILDERS:
        msg = f"unsupported forge {forge_name!r}; expected one of {SUPPORTED_FORGES}"
        raise ValueError(msg)
    request = _ForgeRequest(
        base=_normalize_repo_base(repo_url),
        ref=ref,
        ref_kind=ref_kind,
        repo_path=repo_path,
        is_dir=is_dir,
    )
    return _BUILDERS[forge_name](request)

Branch resolution

mkdocs_source_links.branch

Resolve git branch for forge URLs from MkDocs config.

resolve_branch

resolve_branch(*, plugin_branch, extra, edit_uri)

Resolve the git branch name used in forge view URLs.

Branch resolution follows MkDocs and plugin configuration in priority order: explicit plugin branch, then extra.git_branch, then the branch parsed from edit_uri, then main.

Parameters:

Name Type Description Default
plugin_branch str | None

Value of the plugin's branch config option, if set. An empty or whitespace-only value is ignored with a warning, falling back to extra.git_branch, edit_uri, or main.

required
extra Mapping[str, Any]

MkDocs extra mapping; git_branch is consulted when set (not None). A non-string git_branch is coerced with str() and logs a build warning. An empty or whitespace-only git_branch is ignored with a warning, falling back to edit_uri or main.

required
edit_uri str | None

MkDocs edit_uri; the segment after edit/, blob/, or Bitbucket src/ is used as the branch name when present. GitLab-style -/edit/<branch>/… paths are supported.

required

Returns:

Type Description
str

Branch name for forge URLs.

Source code in src/mkdocs_source_links/branch.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def resolve_branch(
    *,
    plugin_branch: str | None,
    extra: Mapping[str, Any],
    edit_uri: str | None,
) -> str:
    """Resolve the git branch name used in forge view URLs.

    Branch resolution follows MkDocs and plugin configuration in priority order: explicit plugin
    ``branch``, then ``extra.git_branch``, then the branch parsed from ``edit_uri``, then ``main``.

    Parameters
    ----------
    plugin_branch : str | None
        Value of the plugin's ``branch`` config option, if set. An empty or whitespace-only value
        is ignored with a warning, falling back to ``extra.git_branch``, ``edit_uri``, or ``main``.
    extra : Mapping[str, Any]
        MkDocs ``extra`` mapping; ``git_branch`` is consulted when set (not ``None``). A non-string
        ``git_branch`` is coerced with ``str()`` and logs a build warning. An empty or
        whitespace-only ``git_branch`` is ignored with a warning, falling back to ``edit_uri`` or
        ``main``.
    edit_uri : str | None
        MkDocs ``edit_uri``; the segment after ``edit/``, ``blob/``, or Bitbucket ``src/`` is used
        as the branch name when present. GitLab-style ``-/edit/<branch>/…`` paths are supported.

    Returns
    -------
    str
        Branch name for forge URLs.
    """
    if plugin_branch is not None:
        if plugin_branch.strip():
            return plugin_branch
        log.warning(
            "plugin 'branch' is set but empty; ignoring it and falling back "
            "to extra.git_branch, edit_uri, or 'main'.",
        )
    if (branch := extra.get("git_branch")) is not None:
        if not isinstance(branch, str):
            log.warning(
                "extra.git_branch should be a string; got %r (%s). Coercing with str().",
                branch,
                type(branch).__name__,
            )
            return str(branch)
        if branch.strip():
            return branch
        log.warning(
            "extra.git_branch is set but empty; ignoring it and falling back "
            "to edit_uri or 'main'.",
        )
    if edit_uri:
        parts = edit_uri.strip("/").split("/")
        if len(parts) >= 3 and parts[0] == "-" and parts[1] in ("edit", "blob"):
            return parts[2]
        if len(parts) >= 2 and parts[0] in ("edit", "blob", "src"):
            return parts[1]
    return "main"

Git ref resolution

mkdocs_source_links.ref

Resolve git ref and ref kind for forge view URLs.

ResolvedViewRef

Bases: NamedTuple

Result of resolving pin to a :class:ViewRef, with fallback diagnostics.

Source code in src/mkdocs_source_links/ref.py
22
23
24
25
26
27
class ResolvedViewRef(NamedTuple):
    """Result of resolving ``pin`` to a :class:`ViewRef`, with fallback diagnostics."""

    view_ref: ViewRef
    used_fallback: bool
    requested_pin: str

ViewRef

Bases: NamedTuple

Git ref and kind for forge view URLs.

Source code in src/mkdocs_source_links/ref.py
15
16
17
18
19
class ViewRef(NamedTuple):
    """Git ref and kind for forge view URLs."""

    ref: str
    kind: RefKind

resolve_view_ref

resolve_view_ref(*, pin, repo_root, branch)

Resolve the git ref and kind used in forge view URLs.

When pin is commit, the current HEAD SHA is resolved via git rev-parse. When pin is tag, an exact tag at HEAD is resolved via git describe --tags --exact-match. If git is unavailable, times out, or lookup fails, the resolved branch is used instead (for commit and tag).

Parameters:

Name Type Description Default
pin str

Pin mode: branch, commit, or tag.

required
repo_root Path

Repository root passed to git -C.

required
branch str

Resolved branch name used when pin is branch or as a fallback for commit and tag.

required

Returns:

Type Description
ResolvedViewRef

Resolved ref and kind for URL building, plus whether a commit/tag pin fell back to the branch.

Raises:

Type Description
ValueError

If pin is not branch, commit, or tag.

Source code in src/mkdocs_source_links/ref.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def resolve_view_ref(*, pin: str, repo_root: Path, branch: str) -> ResolvedViewRef:
    """Resolve the git ref and kind used in forge view URLs.

    When ``pin`` is ``commit``, the current HEAD SHA is resolved via ``git rev-parse``. When
    ``pin`` is ``tag``, an exact tag at ``HEAD`` is resolved via ``git describe --tags
    --exact-match``. If git is unavailable, times out, or lookup fails, the resolved branch is
    used instead (for ``commit`` and ``tag``).

    Parameters
    ----------
    pin : str
        Pin mode: ``branch``, ``commit``, or ``tag``.
    repo_root : Path
        Repository root passed to ``git -C``.
    branch : str
        Resolved branch name used when ``pin`` is ``branch`` or as a fallback for ``commit`` and
        ``tag``.

    Returns
    -------
    ResolvedViewRef
        Resolved ref and kind for URL building, plus whether a ``commit``/``tag`` pin fell back to
        the branch.

    Raises
    ------
    ValueError
        If ``pin`` is not ``branch``, ``commit``, or ``tag``.
    """
    if pin == "branch":
        return ResolvedViewRef(ViewRef(branch, "branch"), used_fallback=False, requested_pin=pin)
    if pin not in ("commit", "tag"):
        msg = f"unsupported pin {pin!r}; expected 'branch', 'commit', or 'tag'"
        raise ValueError(msg)
    if shutil.which("git") is None:
        return ResolvedViewRef(
            ViewRef(branch, "branch"),
            used_fallback=True,
            requested_pin=pin,
        )
    if pin == "tag":
        tag = _git_exact_tag(repo_root)
        if tag is not None:
            return ResolvedViewRef(ViewRef(tag, "tag"), used_fallback=False, requested_pin=pin)
        return ResolvedViewRef(
            ViewRef(branch, "branch"),
            used_fallback=True,
            requested_pin=pin,
        )
    sha = _git_head_sha(repo_root)
    if sha is not None:
        return ResolvedViewRef(ViewRef(sha, "commit"), used_fallback=False, requested_pin=pin)
    return ResolvedViewRef(
        ViewRef(branch, "branch"),
        used_fallback=True,
        requested_pin=pin,
    )

Line anchors

mkdocs_source_links.anchors

Translate canonical line-number URL fragments to forge-specific syntax.

translate_line_fragment

translate_line_fragment(fragment, *, forge)

Translate a canonical #L line fragment to forge-specific syntax.

Canonical input uses #L10 for a single line and #L10-L20 for a range. Non-line fragments (for example #section) are returned unchanged. Azure DevOps does not support hash-based line anchors in view URLs, so line fragments are dropped for that forge.

Parameters:

Name Type Description Default
fragment str

URL fragment from the markdown link, including the leading #, or an empty string.

required
forge str

Forge name from :data:~mkdocs_source_links.urls.SUPPORTED_FORGES.

required

Returns:

Type Description
str

Forge-specific fragment, the original fragment when it is not a line anchor, or an empty string for Azure line anchors.

Source code in src/mkdocs_source_links/anchors.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def translate_line_fragment(fragment: str, *, forge: str) -> str:
    """Translate a canonical ``#L`` line fragment to forge-specific syntax.

    Canonical input uses ``#L10`` for a single line and ``#L10-L20`` for a range. Non-line
    fragments (for example ``#section``) are returned unchanged. Azure DevOps does not support
    hash-based line anchors in view URLs, so line fragments are dropped for that forge.

    Parameters
    ----------
    fragment : str
        URL fragment from the markdown link, including the leading ``#``, or an empty string.
    forge : str
        Forge name from :data:`~mkdocs_source_links.urls.SUPPORTED_FORGES`.

    Returns
    -------
    str
        Forge-specific fragment, the original fragment when it is not a line anchor, or an empty
        string for Azure line anchors.
    """
    if not fragment:
        return fragment
    match = _LINE_FRAGMENT.match(fragment)
    if match is None:
        return fragment
    start = match.group(1)
    end = match.group(2)
    if forge == "azure":
        return ""
    if forge == "gitlab":
        return f"#L{start}-{end}" if end else f"#L{start}"
    if forge == "bitbucket":
        return f"#lines-{start}:{end}" if end else f"#lines-{start}"
    return f"#L{start}-L{end}" if end else f"#L{start}"