Titeltext: eine Quelle fuer PDF-Titelseite und Startseite

Die Startseite zeigte einen anderen Titeltext als das PDF. Der Text stand
VIERMAL im Repository: NEUER_TITEL.md, die Konstanten in
bilder_04/erzeuge_titelseite.py, der Titelblock des Vorworts - und noch einmal
fest einprogrammiert im Hero-Block von baue_landingpage().

Die Ursache ist messbar: Die drei Commits, die den Titel ueberarbeitet haben
(ca5621f, 199dd3b, 1659fb9 im Ursprungs-Repository), fassten jedes Mal
dieselben drei Dateien an. build_version_04.py war in keinem davon, und
OR_HTML_04/index.html taucht in ihren Dateilisten gar nicht auf. Online stand
deshalb:

  Unterzeile  "Von den mathematischen Grundlagen zur quantitativen
              Portfolio-Optimierung an den Finanzmaerkten."
  Kompendium  "Ein vollstaendiges Kompendium mit Algorithmen ..."
  Anwendungen fehlte ganz

Behandlung wie bei den neun Nummern-Funden zuvor: nicht die Kopie korrigieren,
sondern die Kopie abschaffen. Das neue titeltexte_04.py zerlegt NEUER_TITEL.md
in Titel, Untertitel, Kompendium- und Anwendungen-Absatz; PDF-Titelseite und
Startseite lesen dieselbe Funktion. Der Autor bleibt bewusst Konstante im
Generator - die Vorlage beschreibt das Buch, nicht sein Impressum.

Der entscheidende Beleg: Die neu erzeugte titelseite.svg ist BYTE-IDENTISCH
(97fb40d7...) zur vorherigen - der Umbau hat am PDF nachweislich nichts
veraendert. Auch nicht am Umbruch: Die frueher handgesetzten drei Zeilen
entstehen jetzt automatisch aus _umbrechen(), und zwar exakt so. Ein
Textvergleich ueber das SVG war uebrigens nicht moeglich - es enthaelt kein
einziges <text>-Element, matplotlib zeichnet alle 726 Buchstaben als Kurven.

Das Vorwort bleibt von Hand gepflegter Buchtext, wird aber von
pruefe_titeltexte() bei jedem --check gegen die Quelle gestellt. Gleich beim
ersten Lauf fand es einen echten Unterschied: Das Vorwort setzte einen
Geviertstrich, die Vorlage einen Halbgeviertstrich. Im Deutschen ist der
Halbgeviertstrich der richtige - das Vorwort folgt jetzt der Quelle.

Veroeffentlicht: 5 Dateien uebertragen, alle vierzehn HTTP-Proben richtig.
Gegenprobe vom Server: alle drei Absaetze der Startseite sind zeichengenau
identisch mit NEUER_TITEL.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 19:39:49 +02:00
commit 022bd4eaca
15 changed files with 250 additions and 37 deletions

View file

@ -23,7 +23,7 @@ und Matrixmethoden sowie HiGHS, SciPy, Google OR-Tools und CVXPY.
Von **Personaleinsatz-, Schicht- und Vertretungsplänen, Fahrtrouten, Logistik und
Maschinen- und Netzwerkauslastung** über **Energieverteilung und Ressourcenplanung** bis zur
**Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python
**Portfoliooptimierung und besseren Entscheidungen an den Finanzmärkten** mit Python
modellieren, optimieren und belegbar fundierte Entscheidungen treffen.
Autor / Herausgeber: Dieter Schlüter

View file

@ -37,6 +37,8 @@ import subprocess
import sys
import zipfile
from titeltexte_04 import lies_titeltexte, _saeubern as _saeubere_titeltext
HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER)
@ -389,9 +391,77 @@ def pruefe_dateien() -> list[str]:
fehlend.append(name)
fehlend.extend(pruefe_assets())
fehlend.extend(pruefe_titeltexte())
return fehlend
# Die Datei, die den Titeltext ein viertes Mal traegt: als Buchtext, den ein
# Leser im Vorwort sieht. Erzeugt wird er dort NICHT - er steht mit
# Hervorhebungen im Fliesstext und gehoert dem Autor. Aber er wird geprueft.
VORWORT_DATEI = "00_Vorwort_und_Lesehilfe.md"
def pruefe_titeltexte() -> list[str]:
"""Vergleicht den Titelblock des Vorworts mit NEUER_TITEL.md.
PDF-Titelseite und Startseite lesen die Quelle direkt und koennen deshalb
gar nicht abweichen. Das Vorwort ist die dritte Stelle - und genau eine
solche ungeprueft mitgefuehrte Kopie hat dazu gefuehrt, dass die Startseite
monatelang einen anderen Untertitel zeigte als das PDF."""
quelle = lies_titeltexte()
pfad = os.path.join(HIER, VORWORT_DATEI)
if not os.path.exists(pfad):
return []
with open(pfad, encoding="utf-8") as f:
zeilen = f.read().split("\n")
# Gesucht werden die '#'- und '##'-Ueberschrift und die beiden Absaetze
# danach. Der '{-}'-Zusatz (Pandoc: nicht nummerieren) gehoert nicht zum
# Text.
def ohne_marke(text: str) -> str:
return re.sub(r"\s*\{-\}\s*$", "", text)
gefunden: dict[str, tuple[int, str]] = {}
absaetze: list[tuple[int, str]] = []
block: list[str] = []
block_start = 0
for nr, zeile in enumerate(zeilen, start=1):
if zeile.startswith("# ") and "titel" not in gefunden:
gefunden["titel"] = (nr, _saeubere_titeltext(ohne_marke(zeile[2:])))
elif zeile.startswith("## ") and "untertitel" not in gefunden:
gefunden["untertitel"] = (nr, _saeubere_titeltext(ohne_marke(zeile[3:])))
elif zeile.strip() and not zeile.startswith("#") and "untertitel" in gefunden:
if not block:
block_start = nr
block.append(zeile)
else:
if block:
absaetze.append((block_start, _saeubere_titeltext(" ".join(block))))
block = []
if len(absaetze) == 2:
break
if block:
absaetze.append((block_start, _saeubere_titeltext(" ".join(block))))
for name, eintrag in zip(("kompendium", "anwendungen"), absaetze):
gefunden[name] = eintrag
fehler = []
for name in ("titel", "untertitel", "kompendium", "anwendungen"):
if name not in gefunden:
print(f"FEHLER: {VORWORT_DATEI}: Der Titelblock enthaelt kein "
f"'{name}' - erwartet werden '#', '##' und zwei Absaetze.")
fehler.append(f"{VORWORT_DATEI}:{name}")
continue
nr, ist = gefunden[name]
if ist != quelle[name]:
print(f"FEHLER: {VORWORT_DATEI}:{nr}: '{name}' weicht von der "
f"Titelquelle ab.")
print(f" Vorwort: {ist}")
print(f" Quelle: {quelle[name]}")
fehler.append(f"{VORWORT_DATEI}:{nr}")
return fehler
# Dateien, die der Bau selbst in OR_HTML_04/assets/ erzeugt. Alles andere, was
# der Seitenkopf einbindet, muss aus der Quelle web_04/ kommen.
ERZEUGTE_ASSETS = {
@ -2172,22 +2242,25 @@ def baue_landingpage(html_verz: str, struktur: list[dict], labels: dict,
f'<section class="kapitelgruppe"><h2>{titel}</h2>'
f'<div class="kartenraster">{karten}</div></section>')
# Dieselbe Quelle wie die Titelseite des PDFs (NEUER_TITEL.md). Frueher
# standen die drei Absaetze hier fest im Quelltext - und wichen deshalb
# vom PDF ab, sobald der Titel ueberarbeitet wurde.
titeltexte = lies_titeltexte()
inhalt = f"""
<section class="hero">
<div class="hero-text">
<p class="hero-kicker">KOMPENDIUM</p>
<h1>{SITE_TITEL}</h1>
<p class="hero-untertitel">Von den mathematischen Grundlagen zur quantitativen
Portfolio-Optimierung an den Finanzmärkten.</p>
<p class="hero-untertitel">{titeltexte["untertitel"]}</p>
<div class="hero-badges">
<span class="badge badge-indigo">HiGHS</span>
<span class="badge badge-emerald">SciPy</span>
<span class="badge badge-cyan">Google OR-Tools</span>
<span class="badge badge-violett">CVXPY</span>
</div>
<p class="hero-kompendium"><em>Ein vollständiges Kompendium mit Algorithmen, Constraint
Programming, Vektor- und Matrixmethoden sowie modernen Python-Solvern (HiGHS, SciPy,
Google OR-Tools, CVXPY)</em></p>
<p class="hero-kompendium"><em>{titeltexte["kompendium"]}</em></p>
<p class="hero-anwendungen">{titeltexte["anwendungen"]}</p>
<p class="hero-autor">Autor / Herausgeber: Dieter Schlüter<br>
&lt;dieter(dot)schlueter(atsign)linix(dot)de&gt;<br>
Stand: {aktuelles_datum()}</p>

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
# titeltexte_04.py
#
# Liest den Titeltext des Buchs aus seiner EINEN Quelle:
# Kritik_und_Verbesserungsvorschlaege/NEUER_TITEL.md
#
# Warum es dieses Modul gibt: Der Titeltext stand viermal im Repository - in
# NEUER_TITEL.md, in den Konstanten von bilder_04/erzeuge_titelseite.py (PDF),
# im Vorwort und fest einprogrammiert im Hero-Block von baue_landingpage()
# (Website). Drei Ueberarbeitungen des Titels zogen die ersten drei nach und
# liessen die vierte stehen: Die Startseite zeigte monatelang einen anderen
# Untertitel als das PDF, und der Anwendungen-Absatz fehlte dort ganz.
# Aufgefallen ist es niemandem, weil nichts die Fassungen verglichen hat.
#
# Jetzt lesen PDF-Titelseite und Startseite denselben Text ueber
# lies_titeltexte(); das Vorwort bleibt von Hand gepflegter Buchtext, wird aber
# von pruefe_dateien() gegen diese Quelle geprueft.
import os
import re
HIER = os.path.dirname(os.path.abspath(__file__))
BASIS = os.path.dirname(HIER)
TITEL_QUELLE = os.path.join(BASIS, "Kritik_und_Verbesserungsvorschlaege",
"NEUER_TITEL.md")
def _saeubern(text: str) -> str:
"""Entfernt Markdown-Hervorhebungen und zieht Zeilenumbrueche zu einfachen
Leerzeichen zusammen. Die Vorlage setzt die Fettungen anders als das
Vorwort - fuer den Vergleich zaehlt allein der Wortlaut."""
return " ".join(text.replace("**", "").replace("*", "").split())
def lies_titeltexte(pfad: str = TITEL_QUELLE) -> dict[str, str]:
"""Zerlegt NEUER_TITEL.md in seine vier Bestandteile.
Aufbau der Vorlage:
# <Titel>
### <Untertitel>
**<Kompendium-Absatz>**
<Anwendungen-Absatz>
Fehlt ein Teil, wird abgebrochen statt ein halb gefuelltes Titelblatt zu
erzeugen - ein Titelblatt ohne Untertitel faellt im PDF erst auf, wenn es
gedruckt ist."""
if not os.path.exists(pfad):
raise SystemExit(f"FEHLER: Die Quelle des Titeltexts fehlt: {pfad}")
with open(pfad, encoding="utf-8") as f:
inhalt = f.read()
bloecke = [b.strip() for b in re.split(r"\n\s*\n", inhalt) if b.strip()]
texte: dict[str, str] = {}
absaetze: list[str] = []
for block in bloecke:
if block.startswith("### "):
texte.setdefault("untertitel", _saeubern(block[4:]))
elif block.startswith("# "):
texte.setdefault("titel", _saeubern(block[2:]))
elif not block.startswith("#"):
absaetze.append(_saeubern(block))
if len(absaetze) >= 2:
texte["kompendium"], texte["anwendungen"] = absaetze[0], absaetze[1]
fehlend = [name for name in ("titel", "untertitel", "kompendium", "anwendungen")
if not texte.get(name)]
if fehlend:
raise SystemExit(
f"FEHLER: In {os.path.relpath(pfad, BASIS)} fehlt: "
f"{', '.join(fehlend)}.\n"
f" Erwartet werden eine '#'-Ueberschrift (Titel), eine "
f"'###'-Ueberschrift\n"
f" (Untertitel) und zwei Absaetze (Kompendium, Anwendungen).")
return texte
if __name__ == "__main__":
for name, text in lies_titeltexte().items():
print(f"{name}:\n {text}\n")