Functional Weave
Code in Rust

text.slugify@1.0.0

impl/python.py

3,043 bytes · the Python implementation · view raw

from typing import Dict

# The transliteration table: every letter of the Latin-1 supplement
# (U+00C0-U+00FF) plus the seven letters Windows-1252 adds.
#
# Spelled out character by character rather than left to unicodedata, because
# that is the only way three languages with three different Unicode stacks can
# be pinned to the same output by one vector.
#
# Diacritics are dropped rather than expanded, so "a-umlaut" is "a" and not
# "ae": that is the convention every URL slug in the wild follows. The
# exceptions are the letters that are not accented vowels at all - "sharp s" is
# genuinely two letters, and the ligatures and thorn are letters in their own right.
TRANSLITERATIONS: Dict[str, str] = {
    "À": "a", "Á": "a", "Â": "a", "Ã": "a", "Ä": "a",
    "Å": "a", "Æ": "ae", "Ç": "c", "È": "e", "É": "e",
    "Ê": "e", "Ë": "e", "Ì": "i", "Í": "i", "Î": "i",
    "Ï": "i", "Ð": "d", "Ñ": "n", "Ò": "o", "Ó": "o",
    "Ô": "o", "Õ": "o", "Ö": "o", "Ø": "o", "Ù": "u",
    "Ú": "u", "Û": "u", "Ü": "u", "Ý": "y", "Þ": "th",
    "ß": "ss",
    "à": "a", "á": "a", "â": "a", "ã": "a", "ä": "a",
    "å": "a", "æ": "ae", "ç": "c", "è": "e", "é": "e",
    "ê": "e", "ë": "e", "ì": "i", "í": "i", "î": "i",
    "ï": "i", "ð": "d", "ñ": "n", "ò": "o", "ó": "o",
    "ô": "o", "õ": "o", "ö": "o", "ø": "o", "ù": "u",
    "ú": "u", "û": "u", "ü": "u", "ý": "y", "þ": "th",
    "ÿ": "y",
    "Œ": "oe", "œ": "oe", "Š": "s", "š": "s",
    "Ž": "z", "ž": "z", "Ÿ": "y",
}


def slugify(value: str) -> str:
    """A URL-safe slug: lowercase ASCII letters and digits, single hyphens
    between them, none at either end.

    Anything outside the transliteration table is dropped rather than guessed,
    so a title written entirely in another script slugifies to "". That empty
    string is returned, not raised: the caller knows what its fallback is (an
    id, a date, a hash) and this function does not.
    """
    if not isinstance(value, str):
        raise TypeError("slugify needs a string, received %r" % (value,))

    out = []
    length = 0
    # A pending separator rather than a trailing hyphen plus a strip: it
    # collapses runs and drops the leading and trailing ones in one pass.
    pending_separator = False

    for ch in value:
        code = ord(ch)
        if 0x61 <= code <= 0x7A or 0x30 <= code <= 0x39:
            piece = ch
        elif 0x41 <= code <= 0x5A:
            piece = chr(code + 32)
        else:
            piece = TRANSLITERATIONS.get(ch, "")

        if piece == "":
            pending_separator = length > 0
            continue
        if pending_separator:
            out.append("-")
            length += 1
            pending_separator = False
        out.append(piece)
        length += len(piece)

    return "".join(out)


def slugify_or(value: str, fallback: str) -> str:
    """A slug guaranteed to be non-empty, falling back when nothing survives."""
    slug = slugify(value)
    return slug if slug else fallback