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:

  • AUTO_REF_RE

    A regular expression to match mkdocs-autorefs' special reference markers

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,
)

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

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
229
230
231
232
233
234
235
236
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

AutoRefInlineProcessor ¤

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

Bases: ReferenceInlineProcessor

A Markdown extension.

Methods:

  • evalId

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

  • handleMatch

    Handle an element that matched.

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

AutorefsExtension ¤

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

Bases: Extension

Extension that inserts auto-references in Markdown.

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
294
295
296
297
298
299
300
301
302
303
304
305
306
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 AutoRefInlineProcessor 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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
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,
        )

fix_ref ¤

fix_ref(
    url_mapper: Callable[[str], str], unmapped: list[str]
) -> 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
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
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

fix_refs ¤

fix_refs(
    html: str, url_mapper: Callable[[str], str]
) -> tuple[str, list[str]]

Fix all references in the given HTML text.

Parameters:

Returns:

Source code in src/mkdocs_autorefs/references.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
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

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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}"