Markdown: Nummerierte Kapitel

  Ralf Hersel   Lesezeit: 3 Minuten Auf Mastodon ansehen

Markdown unterstützt keine automatische Generierung von Kapitelnummern. Mit einem Shell-Skript kann man diese Funktion ergänzen.

markdown: nummerierte kapitel

Ihr alle liebt Markdown und verwendet es, oder, oder? Nun, ich wette, dass 99.9 % aller Computernutzer auf dieser Welt noch nie etwas von Markdown gehört haben. Genauso, wie 90 % meiner Mitarbeitenden noch nie etwas von einem Texteditor gehört haben. Ich erlebe das jeden Tag und wundere mich. Ja, wischen können sie alle.

Markdown ist das neue Word oder LibreOffice. Wer sich auf die Inhalte konzentrieren möchte, will nicht mit der Formatierung kämpfen. Europäische Wissenschaftler haben herausgefunden, dass 30 % der Arbeitszeit in Büros mit Formatierung verschwendet werden. Doch das Markdown-Format hat zwei Probleme:

  1. Es ist nicht standardisiert.
  2. Es reitet auf dem schmalen Grat zwischen Einfachheit und praktischer Nützlichkeit.

In diesem Artikel geht es um den zweiten Punkt. Für einfache Texte, ist Markdown völlig ausreichend. Bei längeren Texten, die Anforderungen an die Struktur stellen, wird es schwierig. Mit der Markdown-Syntax ist es nicht möglich nummernhaltige Kapitel zu erstellen. Was das ist, werdet ihr jetzt sehen.

Hier ist ein einfaches Beispiel mit Kapiteln (Headings), geschrieben in Markdown-Editor Apostrophe.

Links seht ihr den Markdown-Quelltext und rechts das gerenderte Ergebnis. Soweit, so gut. Nun möchtet ihr eine Kapitelnummerierung hinzufügen. Ihr könntet per Hand die Nummern (1, 1.1, 1.2, 2, 2.1, usw.) vor die Kapitelüberschriften schreiben. Das ist eine schlechte Idee, weil diese manuellen Einträge nicht angepasst werden, sobald ihr neue Kapitel einfügt oder die Kapitel umsortiert. Markdown kann das nicht.

Nach einer Recherche im Internet stiess ich auf das Projekt enumerate_markdown. Das Ding kann man über Python-PIP installieren. Macht es nicht, es funktioniert nicht. Doch damit war die Idee für diesen Artikel geboren. Wenn Markdown keine automatische Kapitelnummerierung kann, dann machen wir es selbst. Das soll so gehen: Man schreibt einen Markdown-Text, ähnlich, wie ihr ihn im Screenshot seht. Selbstverständlich lohnt sich das nur für längere Texte (Diplomarbeiten, Projektberichte, Dokumentationen usw.), bei denen ihr die Kapitelnummerierung nicht manuell verwalten könnt. Stattdessen ruft ihr ein kleines Programm auf, welches die Nummerierung (in place) für euch erledigt.

Zuerst wollte ich ein kleines Python-Skript schreiben, das diese Aufgabe übernimmt. Das kann doch nicht so schwierig sein. Ist es auch nicht, aber ich hatte keine Zeit zum Programmieren, weil ich diesen Artikel schreiben wollte. Ausserdem hielt ich ein Bash-Skript besser geeignet als ein Python-Programm. Also habe ich meinen KI-Agenten OpenCode mit dem Modell GLM-5.3-Flash-free im Build-Modus gestartet. Das Prompten war einfach, weil sich die Anforderungen genau beschreiben lassen. Es hat mich knapp 17'000 Token (nicht) gekostet, bis das Skript fertig war. Hier ist es:

#!/usr/bin/env bash
#
# mdenum.sh - Add continuous hierarchical numbering to markdown headings.
#
# Usage (from any directory; file paths are resolved relative to the
# current working directory, so the script can be installed in a PATH
# directory such as ~/.local/bin and called as `mdenum.sh`):
#   mdenum.sh input.md [output.md]
#
# If no output file is given, the input file is overwritten in place with
# the numbered version. With an explicit output file, the input file is
# left untouched and the result is written to the output file.
#
# Behavior:
#   - Headings are lines starting with 1-6 '#' characters (ATX headings).
#   - Each heading gets a hierarchical number based on its level:
#       #  Foo   -> 1 Foo
#       ## Bar   -> 1.1 Bar
#       ## Baz   -> 1.2 Baz
#       #  Qux   -> 2 Qux
#   - An existing leading numbering (e.g. "1.", "2.3.", "1.2.3 ") in the
#     heading text is removed before the new number is written, so running
#     the script repeatedly does not accumulate numbers.
#   - Lines inside fenced code blocks (``` or ~~~) are left untouched.

set -euo pipefail

usage() {
    echo "Usage: $(basename "$0") input.md [output.md]" >&2
    exit 1
}

[[ $# -ge 1 && $# -le 2 ]] || usage

input=$1
[[ -f $input ]] || { echo "Error: input file not found: $input" >&2; exit 1; }

if [[ $# -eq 2 ]]; then
    output=$2
else
    # No explicit output file: overwrite the input file in place.
    # We write to a temporary file first and then move it over the input
    # file, so the original is never left in a half-written state.
    output=$(mktemp "${input%.md}_numbered.XXXXXX.md")
    trap 'rm -f "$output"' EXIT
fi

awk '
# counters[n] = current number at heading depth n; depth 0 unused.
BEGIN {
    in_fence = 0
    for (i = 0; i <= 6; i++) counters[i] = 0
}

{
    line = $0

    # Toggle fenced code block state (``` or ~~~, optionally indented).
    if (line ~ /^[[:space:]]*(```|~~~)/) {
        in_fence = !in_fence
        print line
        next
    }
    if (in_fence) {
        print line
        next
    }

    # Match ATX headings: up to 6 leading #, then space (or end of line).
    if (match(line, /^(#{1,6})([[:space:]]+|$)/)) {
        hashes = substr(line, 1, RLENGTH)      # "###   "
        hashes = substr(hashes, 1, index(hashes, " ") - 1)  # just the "#"-run
        hashes_len = length(hashes)
        rest = substr(line, RLENGTH + 1)       # heading text after hashes/space

        # Strip an existing leading numbering from the heading text.
        # Matches forms like: "1 ", "1. ", "1.2 ", "1.2.3 ", "1) ", "1.2) "
        sub(/^[0-9]+([.][0-9]+)*[.)]?[[:space:]]+/, "", rest)

        # Build the new hierarchical number.
        counters[hashes_len]++
        for (i = hashes_len + 1; i <= 6; i++) counters[i] = 0
        num = counters[1]
        for (i = 2; i <= hashes_len; i++) num = num "." counters[i]

        print hashes " " num " " rest
        next
    }

    print line
}
' "$input" > "$output"

if [[ $# -eq 2 ]]; then
    echo "Numbered headings written to: $output"
else
    mv "$output" "$input"
    trap - EXIT
    echo "Numbered headings written to: $input (in place)"
fi

Viel länger als das Prompten, hat das Testen und die Überprüfung des Codes auf Bullshit gedauert. Ferner habe ich den Code einer zweiten KI (Copilot) vorgelegt, und die Suche nach Sicherheitslücken gefordert. Copilot strahlte über beide Backen und hat nichts gefunden.

md_enum

Das Skript mdenum.sh (Markdown Enumeration) kann man in ein Verzeichnis ablegen, welches sich im PATH befindet, damit es aus jedem anderen Verzeichnis heraus aufgerufen werden kann. Es wird so aufgerufen:

mdenum.sh deine_markdown_datei.md
oder
mdenum.sh deine_markdown_datei.md die_neue_datei.md

Im ersten Fall wird die md-Datei überschrieben; im zweiten Fall wird der veränderte Inhalt in eine neue Datei geschrieben. So sieht es danach aus:

Aber Ralf, das kann man doch mit nummerierten Listen in Markdown machen. Nein, kann man nicht:

  • Listen sind Listen, aber keine Kapitel. Falls man nummerierte Listen verwenden würde, ergäbe das ein ziemliches Durcheinander im Dokument.
  • Wer Listen als Ersatz für Kapitel verwendet, darf niemals die RETURN-Taste drücken, weil dadurch ein neues Listenelement initiiert wird.

Was auf den ersten Blick gut aussieht, erweist sich bei der konkreten Arbeit als ungeeignet.

Das Skript durchsucht den Markdown-Text nach Headings, die in der üblichen Syntax geschrieben sind, also: # Obst. Dann fügt es Nummerierungen vor dem Heading-Text ein. Die Nummerierung ist hierarchisch und folgt der jeweiligen Ebene des Kapitels. Falls man neue Kapitel hinzufügt, einfügt, oder die Kapitel umsortiert, entfernt das Skript alle Nummerierungen und erstellt neue.

In der praktischen Arbeit, schreibt man ganz normale Markdown-Syntax, ohne manuelle Kapitelnummern einzufügen. Gelegentlich führt man in einem Terminal das Skript aus (mdenum.sh datei.md), falls man die Kapitelnummern aktualisieren möchte.

Das Skript erstellt kein Inhaltsverzeichnis aus den Headings, was sich mit einem zweiten Schritt leicht bewerkstelligen liesse. Wenn ihr Lust habt, könnt ihr mein Skript in die KI eurer Wahl werfen und die Funktion für Inhaltsverzeichnisse hinzufügen.

Titelbild: https://pixabay.com/photos/alice-english-wonderland-rabbit-2902560/

Quellen: im Text

Tags

Markdown, Kapitelnummern, Outline

Es wurden noch keine Kommentare verfasst, sei der erste!