Skip to content

references ¤

Cross-references module.

Classes:

Functions:

  • fix_ref

    Return a repl function for re.sub.

  • fix_refs

    Fix all references in the given HTML text.

  • relative_url

    Compute the relative path from URL A to URL B.

Attributes:

AUTOREF_RE module-attribute ¤

AUTOREF_RE = compile(
    "<autoref (?P<attrs>.*?)>(?P<title>.*?)</autoref>",
    flags=DOTALL,
)

The autoref HTML tag regular expression.

A regular expression to match mkdocs-autorefs' special reference markers in the on_post_page hook.

AUTO_REF_RE module-attribute ¤

AUTO_REF_RE = compile(
    f"<span data-(?P<kind>autorefs-(?:identifier|optional|optional-hover))=(?P<identifier>{_ATTR_VALUE})(?: class=(?P<class>{_ATTR_VALUE}))?(?P<attrs> [^<>]+)?>(?P<title>.*?)</span>",
    flags=DOTALL,
)

Deprecated. Use AUTOREF_RE instead.

AnchorScannerTreeProcessor ¤

AnchorScannerTreeProcessor(
    plugin: AutorefsPlugin, md: Markdown | None = None
)

Bases: Treeprocessor

Tree processor to scan and register HTML anchors.

Parameters:

  • plugin (AutorefsPlugin) –

    A reference to the autorefs plugin, to use its register_anchor method.

Source code in src/mkdocs_autorefs/references.py
427
428
429
430
431
432
433
434
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

AutorefsExtension ¤

AutorefsExtension(
    plugin: AutorefsPlugin | None = None, **kwargs: Any
)

Bases: Extension

Markdown extension that transforms unresolved references into auto-references.

Auto-references are then resolved later by the MkDocs plugin.

This extension also scans Markdown anchors ([](){#some-id}) to register them with the MkDocs plugin.

Parameters:

  • plugin (AutorefsPlugin | None, default: None ) –

    An optional reference to the autorefs plugin (to pass it to the anchor scanner tree processor).

  • **kwargs (Any, default: {} ) –

    Keyword arguments passed to the base constructor.

Methods:

Source code in src/mkdocs_autorefs/references.py
503
504
505
506
507
508
509
510
511
512
513
514
515
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

extendMarkdown ¤

extendMarkdown(md: Markdown) -> None

Register the extension.

Add an instance of our AutorefsInlineProcessor to the Markdown parser. Also optionally add an instance of our AnchorScannerTreeProcessor to the Markdown parser if a reference to the autorefs plugin was passed to this extension.

Parameters:

  • md (Markdown) –

    A markdown.Markdown instance.

Source code in src/mkdocs_autorefs/references.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def extendMarkdown(self, md: Markdown) -> None:  # noqa: N802 (casing: parent method's name)
    """Register the extension.

    Add an instance of our [`AutorefsInlineProcessor`][mkdocs_autorefs.references.AutorefsInlineProcessor] 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(
        AutorefsInlineProcessor(md),
        AutorefsInlineProcessor.name,
        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_enabling_markdown_anchors()
        md.treeprocessors.register(
            AnchorScannerTreeProcessor(self.plugin, md),
            AnchorScannerTreeProcessor.name,
            priority=0,
        )

AutorefsHookInterface ¤

Bases: ABC

An interface for hooking into how AutoRef handles inline references.

Classes:

  • Context

    The context around an auto-reference.

Methods:

Context dataclass ¤

Context(
    domain: str,
    role: str,
    origin: str,
    filepath: str | Path,
    lineno: int,
)

The context around an auto-reference.

Methods:

  • as_dict

    Convert the context to a dictionary of HTML attributes.

as_dict ¤

as_dict() -> dict[str, str]

Convert the context to a dictionary of HTML attributes.

Source code in src/mkdocs_autorefs/references.py
79
80
81
82
83
84
85
86
87
def as_dict(self) -> dict[str, str]:
    """Convert the context to a dictionary of HTML attributes."""
    return {
        "domain": self.domain,
        "role": self.role,
        "origin": self.origin,
        "filepath": str(self.filepath),
        "lineno": str(self.lineno),
    }

expand_identifier abstractmethod ¤

expand_identifier(identifier: str) -> str

Expand an identifier in a given context.

Parameters:

  • identifier (str) –

    The identifier to expand.

Returns:

  • str

    The expanded identifier.

Source code in src/mkdocs_autorefs/references.py
89
90
91
92
93
94
95
96
97
98
99
@abstractmethod
def expand_identifier(self, identifier: str) -> str:
    """Expand an identifier in a given context.

    Parameters:
        identifier: The identifier to expand.

    Returns:
        The expanded identifier.
    """
    raise NotImplementedError

get_context abstractmethod ¤

get_context() -> Context

Get the current context.

Returns:

Source code in src/mkdocs_autorefs/references.py
101
102
103
104
105
106
107
108
@abstractmethod
def get_context(self) -> AutorefsHookInterface.Context:
    """Get the current context.

    Returns:
        The current context.
    """
    raise NotImplementedError

AutorefsInlineProcessor ¤

AutorefsInlineProcessor(*args: Any, **kwargs: Any)

Bases: ReferenceInlineProcessor

A Markdown extension to handle inline references.

Methods:

  • evalId

    Evaluate the id portion of [ref][id].

  • handleMatch

    Handle an element that matched.

Source code in src/mkdocs_autorefs/references.py
117
118
def __init__(self, *args: Any, **kwargs: Any) -> None:  # noqa: D107
    super().__init__(REFERENCE_RE, *args, **kwargs)

evalId ¤

evalId(
    data: str, index: int, text: str
) -> tuple[str | None, int, bool]

Evaluate the id portion of [ref][id].

If [ref][] use [ref].

Parameters:

  • data (str) –

    The data to evaluate.

  • index (int) –

    The starting position.

  • text (str) –

    The text to use when no identifier.

Returns:

  • tuple[str | None, int, bool]

    A tuple containing the identifier, its end position, and whether it matched.

Source code in src/mkdocs_autorefs/references.py
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
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

handleMatch ¤

handleMatch(
    m: Match[str], data: str
) -> tuple[Element | None, int | None, int | None]

Handle an element that matched.

Parameters:

  • m (Match[str]) –

    The match object.

  • data (str) –

    The matched data.

Returns:

Source code in src/mkdocs_autorefs/references.py
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
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 control characters (from 0 to 31 included).
        # 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

fix_ref ¤

fix_ref(
    url_mapper: Callable[[str], str],
    unmapped: list[tuple[str, Context | None]],
) -> Callable

Return a repl function for 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.

Parameters:

Returns:

Source code in src/mkdocs_autorefs/references.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def fix_ref(
    url_mapper: Callable[[str], str],
    unmapped: list[tuple[str, AutorefsHookInterface.Context | None]],
) -> Callable:
    """Return a `repl` function for [`re.sub`](https://docs.python.org/3/library/re.html#re.sub).

    In our context, we match Markdown references and replace them with HTML links.

    When the matched reference's identifier was not mapped to an URL, we append the identifier to the outer
    `unmapped` list. It generally means the user is trying to cross-reference an object that was not collected
    and rendered, making it impossible to link to it. We catch this exception in the caller to issue a warning.

    Arguments:
        url_mapper: A callable that gets an object's site URL by its identifier,
            such as [mkdocs_autorefs.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:
        title = match["title"]
        attrs = _html_attrs_parser.parse(f"<a {match['attrs']}>")
        identifier: str = attrs["identifier"]
        optional = "optional" in attrs
        hover = "hover" in attrs

        try:
            url = url_mapper(unescape(identifier))
        except KeyError:
            if optional:
                if hover:
                    return f'<span title="{identifier}">{title}</span>'
                return title
            unmapped.append((identifier, attrs.context))
            if title == identifier:
                return f"[{identifier}][]"
            return f"[{title}][{identifier}]"

        parsed = urlsplit(url)
        external = parsed.scheme or parsed.netloc
        classes = (attrs.get("class") or "").strip().split()
        classes = ["autorefs", "autorefs-external" if external else "autorefs-internal", *classes]
        class_attr = " ".join(classes)
        if remaining := attrs.remaining:
            remaining = f" {remaining}"
        if optional and hover:
            return f'<a class="{class_attr}" title="{identifier}" href="{escape(url)}"{remaining}>{title}</a>'
        return f'<a class="{class_attr}" href="{escape(url)}"{remaining}>{title}</a>'

    return inner

fix_refs ¤

fix_refs(
    html: str,
    url_mapper: Callable[[str], str],
    *,
    _legacy_refs: bool = True
) -> tuple[str, list[tuple[str, Context | None]]]

Fix all references in the given HTML text.

Parameters:

Returns:

Source code in src/mkdocs_autorefs/references.py
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
def fix_refs(
    html: str,
    url_mapper: Callable[[str], str],
    *,
    _legacy_refs: bool = True,
) -> tuple[str, list[tuple[str, AutorefsHookInterface.Context | None]]]:
    """Fix all references in the given HTML text.

    Arguments:
        html: The text to fix.
        url_mapper: A callable that gets an object's site URL by its identifier,
            such as [mkdocs_autorefs.plugin.AutorefsPlugin.get_item_url][].

    Returns:
        The fixed HTML, and a list of unmapped identifiers (string and optional context).
    """
    unmapped: list[tuple[str, AutorefsHookInterface.Context | None]] = []
    html = AUTOREF_RE.sub(fix_ref(url_mapper, unmapped), html)

    # YORE: Bump 2: Remove block.
    if _legacy_refs:
        html = AUTO_REF_RE.sub(_legacy_fix_ref(url_mapper, unmapped), html)

    return html, unmapped

relative_url ¤

relative_url(url_a: str, url_b: str) -> str

Compute the relative path from URL A to URL B.

Parameters:

  • url_a (str) –

    URL A.

  • url_b (str) –

    URL B.

Returns:

  • str

    The relative URL to go from A to B.

Source code in src/mkdocs_autorefs/references.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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}"