Glossar: Sprungleiste, Abkürzungen ausschreiben, CLAUDE.md-Regel

Sprungleiste (A B C ... Z) im Glossar — wie im Stichwortverzeichnis:
ein Klick je Buchstabe springt zur '## <Buchstabe>'-Überschrift.
Eingebaut in erzeuge_glossar_04.py als {=html}-Rohblock (nur HTML,
im PDF bedeutungslos).

Abkürzungen beim ersten Mal ausschreiben: 'VaR' → 'VaR (Value at Risk)',
'EVPI' → 'EVPI (Expected Value of Perfect Information)' als Link-Text
bei der ersten Nennung je Datei; danach nur die Abkürzung. 19 Abkürzungen
erfasst (VaR, CVaR, KKT, NLP, MILP, EVPI, LP, QP, ...).

CLAUDE.md: Regel 'Glossar-Links: Fachbegriffe im Fließtext automatisch
verlinkt' dokumentiert — resolve_glossar(), LINK_TEXTE,
ABBREV_ERSTE_NENNUNG, _NICHT_AUTO_LINKEN, _ZU_KURZ_ABK, .glossar-link,
{gl:}-Marke, Sprungleiste, {#gloss:<slug>}-Anchor.
This commit is contained in:
dschlueter 2026-09-10 17:52:49 +02:00
commit 0e2c011732
47 changed files with 545 additions and 561 deletions

View file

@ -5,6 +5,11 @@ Jeder Eintrag nennt am Ende den Abschnitt, in dem der Begriff eingeführt wird
dorthin führt in der Online-Fassung direkt zur Erklärung im Zusammenhang. Wer einen Begriff an
*allen* Fundstellen sucht, benutzt das Stichwortverzeichnis.
```{=html}
<nav class="register-sprungleiste"><a href="#a">A</a> <a href="#b">B</a> <a href="#c">C</a> <a href="#d">D</a> <a href="#e">E</a> <a href="#f">F</a> <a href="#g">G</a> <a href="#h">H</a> <a href="#i">I</a> <a href="#j">J</a> <a href="#k">K</a> <a href="#l">L</a> <a href="#m">M</a> <a href="#n">N</a> <a href="#o">O</a> <a href="#p">P</a> <a href="#q">Q</a> <a href="#r">R</a> <a href="#s">S</a> <a href="#t">T</a> <a href="#u">U</a> <a href="#v">V</a> <a href="#w">W</a> <a href="#y">Y</a> <a href="#z">Z</a></nav>
```
## A
::: {#gloss:absolutbetrag}

View file

@ -157,4 +157,4 @@ Details zu den Lernpfaden stehen in [`00_Vorwort_und_Lesehilfe.md`](00_Vorwort_u
---
*Autor / Herausgeber: Dieter Schlüter · Stand: 10. September 2026 v17.38*
*Autor / Herausgeber: Dieter Schlüter · Stand: 10. September 2026 v17.51*

View file

@ -1114,7 +1114,7 @@ _GLOSSAR_GL_RE = re.compile(r'\{gl:([^}]+)\}')
def resolve_glossar(text: str, fuer_seite: bool = False) -> str:
"""Verlinkt Glossar-Begriffe im Fließtext automatisch (erste Nennung je
Begriff im uebergebenen Text) und loest {gl:}-Marken auf."""
from glossar_eintraege_04 import LINK_TEXTE
from glossar_eintraege_04 import LINK_TEXTE, ABBREV_ERSTE_NENNUNG
# {gl:Begriff}-Marken zuerst: erzwingen einen Link auch fuer Begriffe,
# die nicht auto-linkbar sind. 'Begriff' ist der Anzeigetext im Link;
@ -1165,13 +1165,15 @@ def resolve_glossar(text: str, fuer_seite: bool = False) -> str:
last = 0
for m in _GLOSSAR_TOKEN_RE.finditer(line):
seg = line[last:m.start()]
seg, t = _glossar_in_segment(seg, texte, seen, fuer_seite)
seg, t = _glossar_in_segment(seg, texte, seen, fuer_seite,
ABBREV_ERSTE_NENNUNG)
treffer += t
new_parts.append(seg)
new_parts.append(m.group(0)) # Token unverändert
last = m.end()
rest = line[last:]
rest, t = _glossar_in_segment(rest, texte, seen, fuer_seite)
rest, t = _glossar_in_segment(rest, texte, seen, fuer_seite,
ABBREV_ERSTE_NENNUNG)
treffer += t
new_parts.append(rest)
lines[i] = ''.join(new_parts)
@ -1188,17 +1190,22 @@ def _glossar_slug_fuer(begriff: str) -> str:
def _glossar_in_segment(segment: str, texte: list[tuple[str, str]],
seen: set[str], fuer_seite: bool) -> tuple[str, int]:
seen: set[str], fuer_seite: bool,
abbrev_erste: dict[str, str] | None = None
) -> tuple[str, int]:
"""Sucht im Fließtext-Segment nach Glossar-Begriffen und ersetzt das
erste Vorkommen je Begriff durch einen Link.
Bei Abkürzungen (abbrev_erste) wird die ERSTE Nennung ausgeschrieben:
'VaR' -> 'VaR (Value at Risk)' als Link-Text. Danach nur noch 'VaR'.
Alle Link-Texte werden im Original-Segment gesucht (mit finditer, also
alle Vorkommen), dann nach Position sortiert und nicht-ueberlappend
ersetzt laengste zuerst bei gleicher Position. So wird 'CP-SAT' als
Ganzes erkannt, bevor 'CP' (als Teilstring darin) ein eigenes Match
erzeugt. Wird ein Match wegen Ueberlappung übersprungen, wird das
naechste Vorkommen desselben Begriffs genommen (falls nicht ueberlappend).
erzeugt.
"""
if abbrev_erste is None:
abbrev_erste = {}
matches: list[tuple[int, int, str, str]] = []
for link_text, slug in texte:
if link_text in seen:
@ -1229,7 +1236,9 @@ def _glossar_in_segment(segment: str, texte: list[tuple[str, str]],
result.append(segment[last_end:start])
ziel = (f'anhang-glossar.md#gloss:{slug}' if fuer_seite
else f'#gloss:{slug}')
result.append(f'[{link_text}]({ziel}){{.glossar-link}}')
# Erste Nennung einer Abkürzung: ausgeschrieben als Link-Text.
anzeige = abbrev_erste.get(link_text, link_text)
result.append(f'[{anzeige}]({ziel}){{.glossar-link}}')
seen.add(link_text)
treffer += 1
last_end = end

View file

@ -180,7 +180,16 @@ def baue() -> str:
break_long_words=False,
break_on_hyphens=False))
zeilen = [KOPFZEILE, "", absatz(EINLEITUNG), ""]
# Sprungleiste A B C ... — wie im Stichwortverzeichnis: ein Klick je
# Buchstabe springt zur Ueberschrift '## <Buchstabe>' (Pandoc erzeugt daraus
# id="a", id="b", ...). Nur im HTML (roh-Block); im PDF meaningless.
buchstaben = sorted({anfangsbuchstabe(n) for n, _, _, _ in EINTRAEGE})
sprungleiste = ("```{=html}\n"
'<nav class="register-sprungleiste">'
+ " ".join(f'<a href="#{b.lower()}">{b}</a>' for b in buchstaben)
+ "</nav>\n```\n")
zeilen = [KOPFZEILE, "", absatz(EINLEITUNG), "", sprungleiste, ""]
letzter = None
for name, idx, text, ziel in sorted(EINTRAEGE,
key=lambda e: sortierschluessel(e[0])):

View file

@ -1535,6 +1535,31 @@ _NICHT_AUTO_LINKEN = frozenset({
_ZU_KURZ_ABK = frozenset({"OR"})
# ABBREV_ERSTE_NENNUNG: Abkürzung -> Anzeige-Text für die erste Nennung.
# 'VaR' -> 'VaR (Value at Risk)', 'EVPI' -> 'EVPI (Expected Value ...)'.
# Wird beim ersten Vorkommen der Abkürzung im Fließtext als Link-Text
# verwendet; danach nur noch die Abkürzung. Siehe resolve_glossar().
ABBREV_ERSTE_NENNUNG: dict[str, str] = {}
def _erste_nennung(name: str, abk: str) -> str:
"""Baut die erste-Nennung-Anzeige aus dem Glossar-Namen.
'Value at Risk (VaR)' + 'VaR' -> 'VaR (Value at Risk)'
'EVPI (Expected Value ...)' + 'EVPI' -> 'EVPI (Expected Value ...)' (=name)
"""
m = _re.search(r"\(([^)]+)\)\s*$", name)
if not m:
return name
in_paren = m.group(1).strip()
before = name[: m.start()].strip()
# Fall 1: Abkürzung in der Klammer -> 'Abk (Voller Name)'.
if in_paren == abk:
return f"{abk} ({before})"
# Fall 2: Abkürzung vor der Klammer -> Name ist schon 'Abk (Voller Name)'.
return name
LINK_TEXTE: dict[str, str] = {}
for _name, _idx, _text, _ziel in EINTRAEGE:
if _name in _NICHT_AUTO_LINKEN:
@ -1546,3 +1571,5 @@ for _name, _idx, _text, _ziel in EINTRAEGE:
_abk = _abkuerzung_aus_name(_name)
if _abk and _abk not in _ZU_KURZ_ABK:
LINK_TEXTE[_abk] = _slug
# Erste Nennung: ausgeschrieben ('VaR (Value at Risk)').
ABBREV_ERSTE_NENNUNG[_abk] = _erste_nennung(_name, _abk)