Source code for myst_parser.slugs
"""Slug generation for heading anchors.
This is the single source of truth for heading slugs,
shared by the renderer (``myst_heading_anchors``) and the ``myst-anchors`` CLI.
The functions here are pure and dependency-free (standard library ``re`` only),
so that alternative implementations of MyST can replicate them from this file;
for the same reason, the unit-test corpus is kept as a language-agnostic
data file (``tests/fixtures/slugs.json``).
"""
from __future__ import annotations
import re
from collections.abc import Callable, Container
_GITHUB_CLEAN = re.compile(r"[^\w\u4e00-\u9fff\- ]")
[docs]
def github_slugify(title: str) -> str:
"""Convert a heading title to a GitHub-style slug.
Algorithm: lowercase the title; replace spaces with hyphens ``-``;
remove every character that is not a word character (``\\w``),
a CJK ideograph (U+4E00-U+9FFF), a hyphen or a space.
Leading/trailing whitespace is deliberately *not* stripped first,
so it survives as leading/trailing hyphens
(e.g. ``" a b "`` -> ``"-a-b-"``); see the v0.18.1 changelog.
See https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
and https://gist.github.com/asabaylus/3071099
"""
return _GITHUB_CLEAN.sub("", title.lower().replace(" ", "-"))
_GITLAB_CLEAN = re.compile(r"[^\w\- ]")
_GITLAB_DIGITS = re.compile(r"\d+")
_GITLAB_SQUEEZE = re.compile(r"-{2,}")
[docs]
def gitlab_slugify(title: str) -> str:
"""Convert a heading title to a GitLab-style slug.
Algorithm: strip and lowercase the title; remove every character that is
not a word character (``\\w``), a hyphen or a space; replace spaces with
hyphens; squeeze consecutive hyphens; prefix ``anchor-`` if the result
consists only of digits.
See https://docs.gitlab.com/ee/user/markdown.html#heading-ids-and-links
and ``Gitlab::Utils::Markdown#string_to_anchor``.
"""
slug = _GITLAB_CLEAN.sub("", title.strip().lower()).replace(" ", "-")
slug = _GITLAB_SQUEEZE.sub("-", slug)
if _GITLAB_DIGITS.fullmatch(slug):
slug = f"anchor-{slug}"
return slug
SLUG_PRESETS: dict[str, Callable[[str], str]] = {
"github": github_slugify,
"gitlab": gitlab_slugify,
}
"""Named slugify functions, usable as ``myst_heading_slug_func`` values."""
[docs]
def unique_slug(slug: str, existing: Container[str]) -> str:
"""Suffix ``slug`` with ``-1``, ``-2``, ... until it is not in ``existing``.
The base never changes, so duplicates of ``x`` become
``x, x-1, x-2, ...`` (GitHub behaviour), not ``x, x-1, x-1-2, ...``.
"""
uniq, i = slug, 1
while uniq in existing:
uniq = f"{slug}-{i}"
i += 1
return uniq