Statische Website-Assets in die Quelle web_04/ holen
site.css, site.js, icons.svg, plotly.min.js und die 22 KaTeX-Dateien lagen in
OR_HTML_04/, obwohl kein Skript sie je geschrieben hat. Die Falle daran: Wer
eine CSS-Regel suchte, suchte sie in den Quellen und fand nichts - genau das
ist beim Einruecken des ZIP-Menuepunkts passiert. Und wer OR_HTML_04/ geloescht
und neu gebaut haette, haette eine Website ohne Stil, ohne Symbole und ohne
Formelsatz bekommen.
Jetzt liegen sie in web_04/, dessen Aufbau (assets/, katex/) das Ziel spiegelt.
spiegle_statische_assets() kopiert sie bei JEDEM Lauf.
kopiere_plotly_bibliothek() fuellt die Quelle statt des Ziels.
_lade_icon_sprite_inline() und die beiden KaTeX-Pruefungen lesen die Quelle,
haengen also nicht mehr vom eigenen Ergebnis ab.
Zwei Waechter, weil genau diese Verwechslung schon vorgekommen ist:
* Wurde die Kopie in OR_HTML_04/ von Hand geaendert (Inhalt weicht ab UND
Zeitstempel ist neuer), bricht der Bau ab und nennt den mv-Befehl, der es
richtigstellt - kein stilles Ueberschreiben.
* pruefe_assets() liest die href=/src=-Literale aus dem Quelltext des
Bauskripts und verlangt fuer jedes einen Erzeuger: entweder web_04/ oder die
Liste ERZEUGTE_ASSETS (highlight.css, search-index.js, programme.js).
Probe: rm -rf OR_HTML_04 && --html baut alle 213 Dateien wieder auf,
Dateiliste identisch zur Sicherung.
Fund dabei: Das Stichwortverzeichnis war nicht byte-reproduzierbar
ziel_links() sortierte nach (seite, kontext) - und kontext ist der
Kapiteltitel, fuer alle Marken einer Datei also derselbe. Bei zwei Fundstellen
im selben Kapitel war der Schluessel gleich, und die Reihenfolge fiel auf die
eines set() zurueck, also auf den je Prozess zufaelligen PYTHONHASHSEED. Zwei
Laeufe erzeugten unterschiedliche Bytes ohne Quellaenderung.
Derselbe Fehler war auch sichtbar: vier {idx:Branch-and-Bound} in Kapitel 6
ergaben vier optisch identische Links nebeneinander; zehn Registereintraege
waren betroffen. Behoben durch einen Link je Kapitel (erste Fundstelle in
Dokumentreihenfolge, dict statt set) - das macht die Sortierung zugleich
eindeutig.
Gegenprobe: drei Laeufe mit PYTHONHASHSEED=random liefern dieselbe Pruefsumme.
219 Fachbegriffe und 328 Indexmarken unveraendert.
Nachtrag zum vorigen Commit: highlight.css gehoert NICHT zu den Handdateien -
erzeuge_highlight_css() erzeugt sie aus pandoc --print-highlight-style. Die
Notiz in PROGRESS.md ist korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6de2802fc
commit
567bfa799a
30 changed files with 4686 additions and 22 deletions
|
|
@ -25,6 +25,9 @@ Dieses Verzeichnis ist die Wurzel; alle Skripte leiten ihre Pfade daraus ab
|
||||||
```
|
```
|
||||||
Operations_Research_mit_Python_Version_04/ Quelle: 37 Kapiteldateien + Build-Skripte
|
Operations_Research_mit_Python_Version_04/ Quelle: 37 Kapiteldateien + Build-Skripte
|
||||||
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
bilder_04/ Quelle: Diagramme + erzeuge_*.py-Generatoren
|
||||||
|
web_04/ Quelle: site.css, site.js, icons.svg,
|
||||||
|
plotly.min.js, katex/ - die statischen
|
||||||
|
Bestandteile der Website
|
||||||
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
Operations_Research_mit_Python_Version_04.md generiert: Gesamtdokument
|
||||||
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
Operations_Research_mit_Python_Version_04.pdf generiert: PDF (xelatex)
|
||||||
OR_HTML_04/ generiert: Mehrseiten-Website
|
OR_HTML_04/ generiert: Mehrseiten-Website
|
||||||
|
|
@ -39,7 +42,11 @@ LICENSE, LICENSE-TEXT.md MIT fuer Code, CC BY-SA 4.0 fuer
|
||||||
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
pandoc-defaults-basis.yaml, pandoc/, pandoc-defaults-buch.yaml PDF-Konfiguration
|
||||||
```
|
```
|
||||||
|
|
||||||
Nur die als **Quelle** markierten Verzeichnisse werden von Hand bearbeitet.
|
Nur die als **Quelle** markierten Verzeichnisse werden von Hand bearbeitet. Seit dem
|
||||||
|
Umzug nach `web_04/` stimmt dieser Satz auch: Vorher lagen `site.css`, `site.js`,
|
||||||
|
`icons.svg` und `katex/` mitten im erzeugten `OR_HTML_04/`. `--check` bewacht das jetzt
|
||||||
|
(`pruefe_assets()`), und der Bau bricht ab, wenn jemand die Kopie statt der Quelle
|
||||||
|
bearbeitet hat.
|
||||||
|
|
||||||
Dieses Verzeichnis ist seit dem 08.09.2026 ein **eigenes Git-Repository** (`main`). Das
|
Dieses Verzeichnis ist seit dem 08.09.2026 ein **eigenes Git-Repository** (`main`). Das
|
||||||
übergeordnete `OR_mit_Python/` ist nur noch das Archiv der Historie bis zur Trennung und
|
übergeordnete `OR_mit_Python/` ist nur noch das Archiv der Historie bis zur Trennung und
|
||||||
|
|
|
||||||
2
OR_HTML_04/stichwortverzeichnis.html
generated
2
OR_HTML_04/stichwortverzeichnis.html
generated
File diff suppressed because one or more lines are too long
|
|
@ -32,6 +32,7 @@ import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
@ -71,6 +72,19 @@ BILDER_VERZ = os.path.join(BASIS, BILDER_NAME)
|
||||||
HTML_NAME = f"OR_HTML_{VERSION}"
|
HTML_NAME = f"OR_HTML_{VERSION}"
|
||||||
HTML_VERZ = os.path.join(BASIS, HTML_NAME)
|
HTML_VERZ = os.path.join(BASIS, HTML_NAME)
|
||||||
|
|
||||||
|
# Statische Bestandteile der Website, die kein Skript erzeugt: das von Hand
|
||||||
|
# gepflegte Stylesheet und Skript, die Symbolsammlung, die mitgelieferte
|
||||||
|
# Plotly- und KaTeX-Bibliothek. Der Aufbau von web_04/ spiegelt das Ziel
|
||||||
|
# (assets/, katex/), damit spiegle_statische_assets() ein stumpfer
|
||||||
|
# Baumdurchlauf bleibt.
|
||||||
|
#
|
||||||
|
# Sie lagen frueher in OR_HTML_04/ selbst - also mitten im erzeugten
|
||||||
|
# Verzeichnis. Das war eine Falle: Wer eine CSS-Regel suchte, suchte sie in den
|
||||||
|
# Quellen und fand nichts; wer OR_HTML_04/ loeschte und neu baute, bekam eine
|
||||||
|
# Website ohne Stil.
|
||||||
|
WEB_NAME = f"web_{VERSION}"
|
||||||
|
WEB_VERZ = os.path.join(BASIS, WEB_NAME)
|
||||||
|
|
||||||
# Generierte Jupyter-Notebooks (eines je Kapitel).
|
# Generierte Jupyter-Notebooks (eines je Kapitel).
|
||||||
NOTEBOOK_NAME = f"Notebooks_{VERSION}"
|
NOTEBOOK_NAME = f"Notebooks_{VERSION}"
|
||||||
NOTEBOOK_VERZ = os.path.join(BASIS, NOTEBOOK_NAME)
|
NOTEBOOK_VERZ = os.path.join(BASIS, NOTEBOOK_NAME)
|
||||||
|
|
@ -374,6 +388,49 @@ def pruefe_dateien() -> list[str]:
|
||||||
f"folgt aber '{soll}' - die Lesekette ueberspringt eine Datei.")
|
f"folgt aber '{soll}' - die Lesekette ueberspringt eine Datei.")
|
||||||
fehlend.append(name)
|
fehlend.append(name)
|
||||||
|
|
||||||
|
fehlend.extend(pruefe_assets())
|
||||||
|
return fehlend
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = {
|
||||||
|
"assets/highlight.css", # erzeuge_highlight_css() (pandoc)
|
||||||
|
"assets/search-index.js", # erzeuge_suchindex()
|
||||||
|
"assets/programme.js", # baue_programme_seite()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pruefe_assets() -> list[str]:
|
||||||
|
"""Jede Datei, die der Seitenkopf per href=/src= einbindet, braucht einen
|
||||||
|
Erzeuger: entweder ein Skript (ERZEUGTE_ASSETS) oder die Quelle web_04/.
|
||||||
|
|
||||||
|
Ohne diese Pruefung entsteht beim naechsten Eintrag im Seitenkopf wieder
|
||||||
|
eine Datei ohne Herkunft - genau der Zustand, aus dem site.css, site.js,
|
||||||
|
icons.svg und katex/ gerade befreit wurden."""
|
||||||
|
# Gelesen wird der Quelltext dieses Skripts, nicht eine gerenderte Seite:
|
||||||
|
# Die Verweise stehen als Literale in mehreren Bausteinen (Seitenkopf,
|
||||||
|
# KATEX_KOPF, PLOTLY_SKRIPT), und eine Textsuche findet sie alle, ohne
|
||||||
|
# dass dafuer eine Seite gebaut werden muss.
|
||||||
|
with open(os.path.abspath(__file__), encoding="utf-8") as f:
|
||||||
|
eigener_quelltext = f.read()
|
||||||
|
verweise = sorted(set(re.findall(r'(?:href|src)="((?:assets|katex)/[^"]+)"',
|
||||||
|
eigener_quelltext)))
|
||||||
|
fehlend = []
|
||||||
|
for rel in verweise:
|
||||||
|
if rel in ERZEUGTE_ASSETS:
|
||||||
|
continue
|
||||||
|
if os.path.exists(os.path.join(WEB_VERZ, rel)):
|
||||||
|
continue
|
||||||
|
print(f"FEHLER: Der Seitenkopf bindet '{rel}' ein, aber niemand erzeugt "
|
||||||
|
f"die Datei - sie fehlt in {WEB_NAME}/ und in ERZEUGTE_ASSETS.")
|
||||||
|
fehlend.append(rel)
|
||||||
|
# icons.svg wird nicht verlinkt, sondern inline eingebettet (ICON_SPRITE_INLINE)
|
||||||
|
# und taucht deshalb nicht in `verweise` auf - hier separat geprueft.
|
||||||
|
if not os.path.exists(os.path.join(WEB_VERZ, "assets", "icons.svg")):
|
||||||
|
print(f"FEHLER: {WEB_NAME}/assets/icons.svg fehlt - die Seiten haetten "
|
||||||
|
"keine Symbole.")
|
||||||
|
fehlend.append("assets/icons.svg")
|
||||||
return fehlend
|
return fehlend
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1364,7 +1421,7 @@ def baue_gesamtdokument(html_verz: str) -> None:
|
||||||
"--standalone", "--wrap=none", "--toc", "--toc-depth=3",
|
"--standalone", "--wrap=none", "--toc", "--toc-depth=3",
|
||||||
"-M", f"pagetitle={SITE_TITEL} - Version {VERSION}",
|
"-M", f"pagetitle={SITE_TITEL} - Version {VERSION}",
|
||||||
"-M", "lang=de", "--highlight-style=breezedark", "-o", ziel]
|
"-M", "lang=de", "--highlight-style=breezedark", "-o", ziel]
|
||||||
if os.path.exists(os.path.join(html_verz, "katex")):
|
if os.path.exists(os.path.join(WEB_VERZ, "katex")):
|
||||||
befehl.insert(-2, "--katex=katex/")
|
befehl.insert(-2, "--katex=katex/")
|
||||||
if os.path.exists(stil):
|
if os.path.exists(stil):
|
||||||
befehl.extend(["-H", stil])
|
befehl.extend(["-H", stil])
|
||||||
|
|
@ -1431,10 +1488,14 @@ def icon(name: str, klasse: str = "icon") -> str:
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=1)
|
@functools.lru_cache(maxsize=1)
|
||||||
def _lade_icon_sprite_inline() -> str:
|
def _lade_icon_sprite_inline() -> str:
|
||||||
"""Liest assets/icons.svg und liefert nur die <symbol>-Definitionen
|
"""Liest web_04/assets/icons.svg und liefert nur die <symbol>-Definitionen
|
||||||
(ohne die aeussere <svg>-Huelle) zum Einbetten in jede Seite - siehe
|
(ohne die aeussere <svg>-Huelle) zum Einbetten in jede Seite - siehe
|
||||||
icon()."""
|
icon().
|
||||||
pfad = os.path.join(HTML_VERZ, "assets", "icons.svg")
|
|
||||||
|
Gelesen wird die QUELLE, nicht die Kopie in OR_HTML_04/: Sonst haenge der
|
||||||
|
Bau von seinem eigenen Ergebnis ab und schluege bei leerem Zielverzeichnis
|
||||||
|
fehl."""
|
||||||
|
pfad = os.path.join(WEB_VERZ, "assets", "icons.svg")
|
||||||
with open(pfad, encoding="utf-8") as f:
|
with open(pfad, encoding="utf-8") as f:
|
||||||
inhalt = f.read()
|
inhalt = f.read()
|
||||||
innen = re.search(r"<svg[^>]*>(.*)</svg>", inhalt, re.DOTALL)
|
innen = re.search(r"<svg[^>]*>(.*)</svg>", inhalt, re.DOTALL)
|
||||||
|
|
@ -1490,6 +1551,50 @@ def spiegle_bilder(html_verz: str) -> None:
|
||||||
print(f"Bilder gespiegelt: {kopiert} Datei(en) nach {HTML_NAME}/{BILDER_NAME}/")
|
print(f"Bilder gespiegelt: {kopiert} Datei(en) nach {HTML_NAME}/{BILDER_NAME}/")
|
||||||
|
|
||||||
|
|
||||||
|
def spiegle_statische_assets(html_verz: str) -> None:
|
||||||
|
"""Kopiert web_04/ nach OR_HTML_04/ - bei JEDEM Lauf, nicht nur wenn das
|
||||||
|
Ziel fehlt. Erst dadurch ist OR_HTML_04/ vollstaendig wiederherstellbar:
|
||||||
|
loeschen, neu bauen, fertig.
|
||||||
|
|
||||||
|
Der Waechter: Weicht die Zieldatei von der Quelle ab und ist sie neuer,
|
||||||
|
hat jemand die Kopie bearbeitet statt des Originals. Dann bricht der Bau
|
||||||
|
ab, statt die Arbeit still zu ueberschreiben - derselbe Fehler ist in
|
||||||
|
diesem Projekt schon vorgekommen."""
|
||||||
|
if not os.path.isdir(WEB_VERZ):
|
||||||
|
print(f"WARNUNG: Quellverzeichnis der Website-Assets fehlt: {WEB_VERZ}")
|
||||||
|
return
|
||||||
|
kopiert = 0
|
||||||
|
for wurzel, verzeichnisse, dateien in os.walk(WEB_VERZ):
|
||||||
|
verzeichnisse[:] = [v for v in verzeichnisse if not v.startswith(".")]
|
||||||
|
rel = os.path.relpath(wurzel, WEB_VERZ)
|
||||||
|
ziel_verz = html_verz if rel == "." else os.path.join(html_verz, rel)
|
||||||
|
os.makedirs(ziel_verz, exist_ok=True)
|
||||||
|
for name in sorted(dateien):
|
||||||
|
quelle = os.path.join(wurzel, name)
|
||||||
|
ziel = os.path.join(ziel_verz, name)
|
||||||
|
with open(quelle, "rb") as f:
|
||||||
|
daten = f.read()
|
||||||
|
if os.path.exists(ziel):
|
||||||
|
with open(ziel, "rb") as f:
|
||||||
|
vorhanden = f.read()
|
||||||
|
if vorhanden == daten:
|
||||||
|
continue
|
||||||
|
if os.path.getmtime(ziel) > os.path.getmtime(quelle):
|
||||||
|
rel_ziel = os.path.relpath(ziel, BASIS)
|
||||||
|
rel_quelle = os.path.relpath(quelle, BASIS)
|
||||||
|
raise SystemExit(
|
||||||
|
f"FEHLER: {rel_ziel} wurde geaendert, ist aber eine Kopie.\n"
|
||||||
|
f" Die Aenderung gehoert in die Quelle:\n"
|
||||||
|
f" mv {rel_ziel} {rel_quelle}")
|
||||||
|
with open(ziel, "wb") as f:
|
||||||
|
f.write(daten)
|
||||||
|
shutil.copystat(quelle, ziel)
|
||||||
|
kopiert += 1
|
||||||
|
gesamt = sum(len(d) for _, _, d in os.walk(WEB_VERZ))
|
||||||
|
print(f"Statische Assets: {gesamt} Datei(en) aus {WEB_NAME}/, "
|
||||||
|
f"davon {kopiert} neu geschrieben.")
|
||||||
|
|
||||||
|
|
||||||
def kopiere_pdf(html_verz: str) -> bool:
|
def kopiere_pdf(html_verz: str) -> bool:
|
||||||
"""Kopiert das gebaute PDF nach OR_HTML_04/, damit der Download-Link in der
|
"""Kopiert das gebaute PDF nach OR_HTML_04/, damit der Download-Link in der
|
||||||
Sidebar funktioniert, sobald der Ordner auf einen Webserver kopiert wird.
|
Sidebar funktioniert, sobald der Ordner auf einen Webserver kopiert wird.
|
||||||
|
|
@ -1651,7 +1756,7 @@ PANDOC_HTML_BASIS = ["-f", "markdown+autolink_bare_uris", "-t", "html5",
|
||||||
|
|
||||||
def _pandoc_fragment(text: str, html_verz: str) -> str:
|
def _pandoc_fragment(text: str, html_verz: str) -> str:
|
||||||
befehl = ["pandoc"] + PANDOC_HTML_BASIS
|
befehl = ["pandoc"] + PANDOC_HTML_BASIS
|
||||||
if os.path.exists(os.path.join(html_verz, "katex")):
|
if os.path.exists(os.path.join(WEB_VERZ, "katex")):
|
||||||
befehl.append("--katex=katex/")
|
befehl.append("--katex=katex/")
|
||||||
ergebnis = subprocess.run(befehl, cwd=BASIS, input=text,
|
ergebnis = subprocess.run(befehl, cwd=BASIS, input=text,
|
||||||
capture_output=True, text=True, check=True)
|
capture_output=True, text=True, check=True)
|
||||||
|
|
@ -1725,14 +1830,20 @@ def resolve_plotly(text: str, fuer_html: bool) -> tuple[str, bool]:
|
||||||
return PLOTLY_RE.sub(ersetze, text), eingebettet
|
return PLOTLY_RE.sub(ersetze, text), eingebettet
|
||||||
|
|
||||||
|
|
||||||
def kopiere_plotly_bibliothek(html_verz: str) -> bool:
|
def kopiere_plotly_bibliothek() -> bool:
|
||||||
"""Kopiert die von plotly mitgelieferte plotly.min.js nach assets/.
|
"""Holt die von plotly mitgelieferte plotly.min.js in die QUELLE
|
||||||
|
web_04/assets/ - von dort verteilt spiegle_statische_assets() sie.
|
||||||
|
|
||||||
Bewusst lokal statt per CDN: Die Website soll komplett offline
|
Bewusst lokal statt per CDN: Die Website soll komplett offline
|
||||||
funktionieren - genau wie die bereits lokal eingebundenen KaTeX-Assets."""
|
funktionieren - genau wie die bereits lokal eingebundenen KaTeX-Assets.
|
||||||
ziel = os.path.join(html_verz, "assets", "plotly.min.js")
|
|
||||||
|
Nur wenn die Datei fehlt: Sie ist mit 4,8 MB versioniert und aendert sich
|
||||||
|
nur, wenn plotly aktualisiert wird. Wer sie auffrischen will, loescht sie
|
||||||
|
und baut neu."""
|
||||||
|
ziel = os.path.join(WEB_VERZ, "assets", "plotly.min.js")
|
||||||
if os.path.exists(ziel):
|
if os.path.exists(ziel):
|
||||||
return True
|
return True
|
||||||
|
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
||||||
try:
|
try:
|
||||||
import plotly
|
import plotly
|
||||||
quelle = os.path.join(os.path.dirname(plotly.__file__),
|
quelle = os.path.join(os.path.dirname(plotly.__file__),
|
||||||
|
|
@ -2265,18 +2376,33 @@ def baue_stichwortverzeichnis(html_verz: str, struktur: list[dict],
|
||||||
haupt_name, _, unter_name = eintrag["roh"].partition("!")
|
haupt_name, _, unter_name = eintrag["roh"].partition("!")
|
||||||
haupt_name = haupt_name.rsplit("@", 1)[-1].strip()
|
haupt_name = haupt_name.rsplit("@", 1)[-1].strip()
|
||||||
ziel = (eintrag["seite"], eintrag["anker"], eintrag["kontext"])
|
ziel = (eintrag["seite"], eintrag["anker"], eintrag["kontext"])
|
||||||
knoten = haupt.setdefault(haupt_name, {"ziele": set(), "unter": {}})
|
knoten = haupt.setdefault(haupt_name, {"ziele": {}, "unter": {}})
|
||||||
if unter_name:
|
if unter_name:
|
||||||
unter_name = unter_name.rsplit("@", 1)[-1].strip()
|
unter_name = unter_name.rsplit("@", 1)[-1].strip()
|
||||||
knoten["unter"].setdefault(unter_name, set()).add(ziel)
|
knoten["unter"].setdefault(unter_name, {})[ziel] = None
|
||||||
else:
|
else:
|
||||||
knoten["ziele"].add(ziel)
|
knoten["ziele"][ziel] = None
|
||||||
|
|
||||||
def ziel_links(ziele: set) -> str:
|
def ziel_links(ziele: dict) -> str:
|
||||||
eintraege = sorted(ziele, key=lambda z: (z[0], z[2]))
|
# Ein Link je Kapitel, nicht je Fundstelle. 'kontext' ist der
|
||||||
|
# Kapiteltitel und damit fuer alle Marken einer Datei gleich - vier
|
||||||
|
# {idx:Branch-and-Bound} in Kapitel 6 ergaben vier optisch identische
|
||||||
|
# Links nebeneinander. Behalten wird die ERSTE Fundstelle; die Marken
|
||||||
|
# kommen in Dokumentreihenfolge an, weil finditer() den Text linear
|
||||||
|
# durchlaeuft und dict die Einfuegereihenfolge bewahrt.
|
||||||
|
#
|
||||||
|
# Die Deduplizierung macht die anschliessende Sortierung zugleich
|
||||||
|
# eindeutig. Vorher war sie es nicht: Der Schluessel (seite, kontext)
|
||||||
|
# war fuer zwei Fundstellen desselben Kapitels identisch, und die
|
||||||
|
# Reihenfolge fiel auf die eines set() zurueck - also auf den je Prozess
|
||||||
|
# zufaelligen PYTHONHASHSEED. Zwei Laeufe erzeugten unterschiedliche
|
||||||
|
# Bytes, ohne dass sich eine Quelle geaendert haette.
|
||||||
|
je_kapitel: dict[tuple[str, str], str] = {}
|
||||||
|
for seite, anker, kontext in ziele:
|
||||||
|
je_kapitel.setdefault((seite, kontext), anker)
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
f'<a href="{seite}{"#" + anker if anker else ""}">{kontext}</a>'
|
f'<a href="{seite}{"#" + anker if anker else ""}">{kontext}</a>'
|
||||||
for seite, anker, kontext in eintraege)
|
for (seite, kontext), anker in sorted(je_kapitel.items()))
|
||||||
|
|
||||||
namen = sorted(haupt.keys(), key=_sortierschluessel)
|
namen = sorted(haupt.keys(), key=_sortierschluessel)
|
||||||
sprungleiste = " ".join(
|
sprungleiste = " ".join(
|
||||||
|
|
@ -2346,12 +2472,13 @@ def baue_html_website() -> None:
|
||||||
os.makedirs(html_verz, exist_ok=True)
|
os.makedirs(html_verz, exist_ok=True)
|
||||||
|
|
||||||
datei_seite, labels, label_seite, struktur, stichwortregister = baue_seiten_registry()
|
datei_seite, labels, label_seite, struktur, stichwortregister = baue_seiten_registry()
|
||||||
|
kopiere_plotly_bibliothek()
|
||||||
|
spiegle_statische_assets(html_verz)
|
||||||
spiegle_bilder(html_verz)
|
spiegle_bilder(html_verz)
|
||||||
erzeuge_highlight_css(html_verz)
|
erzeuge_highlight_css(html_verz)
|
||||||
pdf_verfuegbar = kopiere_pdf(html_verz)
|
pdf_verfuegbar = kopiere_pdf(html_verz)
|
||||||
notizbuecher = baue_notebooks(struktur)
|
notizbuecher = baue_notebooks(struktur)
|
||||||
spiegle_notebooks(html_verz, notizbuecher)
|
spiegle_notebooks(html_verz, notizbuecher)
|
||||||
kopiere_plotly_bibliothek(html_verz)
|
|
||||||
suchindex: list[dict] = []
|
suchindex: list[dict] = []
|
||||||
baue_kapitel_seiten(html_verz, datei_seite, labels, label_seite, struktur,
|
baue_kapitel_seiten(html_verz, datei_seite, labels, label_seite, struktur,
|
||||||
suchindex, pdf_verfuegbar, notizbuecher)
|
suchindex, pdf_verfuegbar, notizbuecher)
|
||||||
|
|
|
||||||
52
PROGRESS.md
52
PROGRESS.md
|
|
@ -2024,14 +2024,60 @@ Archivs — so können Link und Datei nicht auseinanderlaufen. Das Glossar ersch
|
||||||
dasselbe leisten, und unten in der Gruppe „Anhänge", weil es ein Anhang ist.
|
dasselbe leisten, und unten in der Gruppe „Anhänge", weil es ein Anhang ist.
|
||||||
|
|
||||||
**Nebenbefund: `OR_HTML_04/assets/site.css` ist eine Quelle, keine erzeugte Datei.** Der
|
**Nebenbefund: `OR_HTML_04/assets/site.css` ist eine Quelle, keine erzeugte Datei.** Der
|
||||||
Build schreibt sie nicht (Zeitstempel vom 06.09., während `programme.js` bei jedem Lauf
|
Build schreibt sie nicht. Dasselbe gilt für `site.js`, `icons.svg`, `plotly.min.js` und
|
||||||
neu entsteht). Dasselbe gilt für `site.js`, `icons.svg`, `highlight.css` und
|
`katex/`. Sie liegen im als „generiert" beschriebenen Verzeichnis, sind aber von Hand
|
||||||
`plotly.min.js`. Sie liegen im als „generiert" beschriebenen Verzeichnis, sind aber von Hand
|
|
||||||
gepflegt bzw. mitgeliefert — wer `OR_HTML_04/` löscht und neu baut, verliert sie.
|
gepflegt bzw. mitgeliefert — wer `OR_HTML_04/` löscht und neu baut, verliert sie.
|
||||||
|
(Nachtrag: Der nächste Abschnitt räumt das auf. **Und `highlight.css` gehört nicht in diese
|
||||||
|
Liste** — die erzeugt `erzeuge_highlight_css()` aus `pandoc --print-highlight-style`; die
|
||||||
|
erste Fassung dieses Absatzes zählte sie fälschlich mit.)
|
||||||
|
|
||||||
Die alte `anhang-glossar-literatur.html` musste von Hand entfernt werden; der Build räumt
|
Die alte `anhang-glossar-literatur.html` musste von Hand entfernt werden; der Build räumt
|
||||||
verwaiste Seiten nicht ab.
|
verwaiste Seiten nicht ab.
|
||||||
|
|
||||||
|
### ✅ `web_04/` — die statischen Assets bekommen eine Quelle
|
||||||
|
|
||||||
|
Vier Dateien und die 22 KaTeX-Dateien lagen im erzeugten Verzeichnis, obwohl kein Skript sie
|
||||||
|
je geschrieben hat. Die Falle daran: Wer eine CSS-Regel suchte, suchte sie in den Quellen und
|
||||||
|
fand nichts — genau das ist beim Einrücken des ZIP-Menüpunkts passiert. Und wer `OR_HTML_04/`
|
||||||
|
gelöscht und neu gebaut hätte, hätte eine Website ohne Stil, ohne Symbole und ohne
|
||||||
|
Formelsatz bekommen.
|
||||||
|
|
||||||
|
Jetzt liegen sie in `web_04/`, dessen Aufbau (`assets/`, `katex/`) das Ziel spiegelt.
|
||||||
|
`spiegle_statische_assets()` kopiert sie bei **jedem** Lauf; `kopiere_plotly_bibliothek()`
|
||||||
|
füllt die Quelle statt des Ziels; `_lade_icon_sprite_inline()` liest die Quelle, hängt also
|
||||||
|
nicht mehr vom eigenen Ergebnis ab.
|
||||||
|
|
||||||
|
**Zwei Wächter**, weil genau diese Verwechslung schon vorgekommen ist:
|
||||||
|
|
||||||
|
* Wurde die Kopie in `OR_HTML_04/` von Hand geändert (Inhalt weicht ab *und* Zeitstempel ist
|
||||||
|
neuer), **bricht der Bau ab** und nennt den `mv`-Befehl, der es richtigstellt. Kein
|
||||||
|
stilles Überschreiben.
|
||||||
|
* `pruefe_assets()` liest die `href=`/`src=`-Literale aus dem Quelltext des Bauskripts und
|
||||||
|
verlangt für jedes einen Erzeuger — entweder `web_04/` oder die Liste `ERZEUGTE_ASSETS`
|
||||||
|
(`highlight.css`, `search-index.js`, `programme.js`). Ein künftiger Eintrag im Seitenkopf
|
||||||
|
ohne Datei dahinter fällt sofort auf.
|
||||||
|
|
||||||
|
Die Probe aufs Exempel: `rm -rf OR_HTML_04 && --html` baut alle 213 Dateien wieder auf,
|
||||||
|
Dateiliste identisch zur Sicherung.
|
||||||
|
|
||||||
|
### 🐛 Fund: Das Stichwortverzeichnis war nicht byte-reproduzierbar
|
||||||
|
|
||||||
|
Beim Gegenprüfen der Neubau-Probe unterschied sich `stichwortverzeichnis.html` zwischen zwei
|
||||||
|
Läufen, ohne dass sich eine Quelle geändert hatte. Ursache in `ziel_links()`: sortiert wurde
|
||||||
|
nach `(seite, kontext)` — und `kontext` ist der **Kapiteltitel**, für alle Marken einer Datei
|
||||||
|
also derselbe. Bei zwei Fundstellen im selben Kapitel war der Schlüssel gleich, und die
|
||||||
|
Reihenfolge fiel auf die eines `set()` zurück, also auf den je Prozess zufälligen
|
||||||
|
`PYTHONHASHSEED`.
|
||||||
|
|
||||||
|
Derselbe Konstruktionsfehler war auch sichtbar: Vier `{idx:Branch-and-Bound}` in Kapitel 6
|
||||||
|
ergaben **vier optisch identische Links** nebeneinander — zehn Einträge im Register waren
|
||||||
|
davon betroffen. Behoben durch einen Link je Kapitel (die erste Fundstelle in
|
||||||
|
Dokumentreihenfolge, `dict` statt `set`). Damit ist die Sortierung zugleich eindeutig.
|
||||||
|
|
||||||
|
Gegenprobe: drei Läufe mit `PYTHONHASHSEED=random` liefern dieselbe Prüfsumme
|
||||||
|
`3da5b9b2…`. Keine Hauptzeile trägt mehr einen doppelten Link; die 219 Fachbegriffe und 328
|
||||||
|
Indexmarken bleiben unverändert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Commit-Historie des V04-Strangs
|
## 8. Commit-Historie des V04-Strangs
|
||||||
|
|
|
||||||
41
web_04/assets/icons.svg
Normal file
41
web_04/assets/icons.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
||||||
|
<symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
|
<circle cx="12" cy="12" r="4.5"/>
|
||||||
|
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
|
||||||
|
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
|
||||||
|
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
|
||||||
|
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="15 4 7 12 15 20"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 4 17 12 9 20"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="4 13 9.5 18.5 20 6"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
|
||||||
|
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
|
||||||
|
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3 KiB |
3882
web_04/assets/plotly.min.js
vendored
Normal file
3882
web_04/assets/plotly.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
348
web_04/assets/site.css
Normal file
348
web_04/assets/site.css
Normal file
|
|
@ -0,0 +1,348 @@
|
||||||
|
/* site.css — Design-System der Mehrseiten-Website "Operations Research mit
|
||||||
|
Python". Struktur (CSS-Variablen, .dark-Umschaltung, kuehle Graustufen)
|
||||||
|
nach dem Vorbild https://jamulix.de/sorting/, Farben aus der bestehenden
|
||||||
|
Buchpalette (Titelseite/PDF: Indigo/Cyan/Violett/Gruen/Amber). */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f6fa;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface2: #eef0f5;
|
||||||
|
--border: #d9dce6;
|
||||||
|
--text: #1a1a2e;
|
||||||
|
--muted: #5c6380;
|
||||||
|
--accent: #4338ca;
|
||||||
|
--accent-text: #ffffff;
|
||||||
|
--cyan: #0891b2;
|
||||||
|
--violett: #7c3aed;
|
||||||
|
--emerald: #059669;
|
||||||
|
--amber: #b45309;
|
||||||
|
--rose: #e11d48;
|
||||||
|
--tint-indigo: #eef2ff;
|
||||||
|
--tint-violett: #f5f3ff;
|
||||||
|
--tint-cyan: #ecfeff;
|
||||||
|
--tint-emerald: #ecfdf5;
|
||||||
|
--tint-amber: #fffbeb;
|
||||||
|
--code-bg: #1e293b;
|
||||||
|
--code-text: #e6edf3;
|
||||||
|
--shadow: 0 2px 10px rgba(26, 26, 46, .07);
|
||||||
|
--shadow-lg: 0 8px 30px rgba(26, 26, 46, .12);
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--bg: #0e0f1a;
|
||||||
|
--surface: #161826;
|
||||||
|
--surface2: #1e2133;
|
||||||
|
--border: #2b2f45;
|
||||||
|
--text: #e7e9f5;
|
||||||
|
--muted: #9aa1c4;
|
||||||
|
--accent: #818cf8;
|
||||||
|
--accent-text: #0e0f1a;
|
||||||
|
--cyan: #22d3ee;
|
||||||
|
--violett: #c4b5fd;
|
||||||
|
--emerald: #34d399;
|
||||||
|
--amber: #fbbf24;
|
||||||
|
--rose: #fb7185;
|
||||||
|
--tint-indigo: #1b1f3a;
|
||||||
|
--tint-violett: #241f3d;
|
||||||
|
--tint-cyan: #132833;
|
||||||
|
--tint-emerald: #10291f;
|
||||||
|
--tint-amber: #2c2410;
|
||||||
|
--code-bg: #0b0d14;
|
||||||
|
--code-text: #dbe4f0;
|
||||||
|
--shadow: 0 2px 10px rgba(0, 0, 0, .35);
|
||||||
|
--shadow-lg: 0 8px 30px rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg: #0e0f1a;
|
||||||
|
--surface: #161826;
|
||||||
|
--surface2: #1e2133;
|
||||||
|
--border: #2b2f45;
|
||||||
|
--text: #e7e9f5;
|
||||||
|
--muted: #9aa1c4;
|
||||||
|
--accent: #818cf8;
|
||||||
|
--accent-text: #0e0f1a;
|
||||||
|
--cyan: #22d3ee;
|
||||||
|
--violett: #c4b5fd;
|
||||||
|
--emerald: #34d399;
|
||||||
|
--amber: #fbbf24;
|
||||||
|
--rose: #fb7185;
|
||||||
|
--tint-indigo: #1b1f3a;
|
||||||
|
--tint-violett: #241f3d;
|
||||||
|
--tint-cyan: #132833;
|
||||||
|
--tint-emerald: #10291f;
|
||||||
|
--tint-amber: #2c2410;
|
||||||
|
--code-bg: #0b0d14;
|
||||||
|
--code-text: #dbe4f0;
|
||||||
|
--shadow: 0 2px 10px rgba(0, 0, 0, .35);
|
||||||
|
--shadow-lg: 0 8px 30px rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
body {
|
||||||
|
margin: 0; background: var(--bg); color: var(--text);
|
||||||
|
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "DejaVu Sans", Arial, sans-serif;
|
||||||
|
line-height: 1.7; font-size: 16px; -webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
.icon { width: 1.15em; height: 1.15em; flex: none; vertical-align: -.2em; }
|
||||||
|
|
||||||
|
/* --- Kopfzeile --- */
|
||||||
|
.site-header {
|
||||||
|
position: sticky; top: 0; z-index: 30; display: flex; align-items: center;
|
||||||
|
gap: .75rem; padding: .6rem 1rem; background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--border); box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
.icon-btn {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 2.2rem; height: 2.2rem; border-radius: 8px; border: 1px solid transparent;
|
||||||
|
background: transparent; color: var(--text); cursor: pointer;
|
||||||
|
}
|
||||||
|
.icon-btn:hover { background: var(--surface2); border-color: var(--border); }
|
||||||
|
.brand {
|
||||||
|
display: flex; align-items: center; gap: .5rem; color: var(--accent);
|
||||||
|
font-weight: 700; text-decoration: none; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.brand span { display: none; }
|
||||||
|
@media (min-width: 900px) { .brand span { display: inline; } }
|
||||||
|
.site-search { position: relative; flex: 1; max-width: 420px; margin: 0 auto; }
|
||||||
|
.site-search input {
|
||||||
|
width: 100%; padding: .5rem .8rem .5rem 2.2rem; border-radius: 20px;
|
||||||
|
border: 1px solid var(--border); background: var(--surface2); color: var(--text);
|
||||||
|
font-size: .92rem;
|
||||||
|
}
|
||||||
|
.site-search input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.such-icon { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); color: var(--muted); }
|
||||||
|
.suche-ergebnisse {
|
||||||
|
position: absolute; top: calc(100% + .4rem); left: 0; right: 0; max-height: 60vh;
|
||||||
|
overflow-y: auto; background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius); box-shadow: var(--shadow-lg); z-index: 40;
|
||||||
|
}
|
||||||
|
.suche-ergebnisse a {
|
||||||
|
display: block; padding: .55rem .9rem; color: var(--text); text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.suche-ergebnisse a:last-child { border-bottom: none; }
|
||||||
|
.suche-ergebnisse a:hover, .suche-ergebnisse a.aktiv { background: var(--tint-indigo); }
|
||||||
|
.suche-ergebnisse small { display: block; color: var(--muted); font-weight: 400; }
|
||||||
|
.suche-leer { padding: .8rem .9rem; color: var(--muted); }
|
||||||
|
#theme-toggle .icon-moon { display: none; }
|
||||||
|
:root[data-theme="dark"] #theme-toggle .icon-sun { display: none; }
|
||||||
|
:root[data-theme="dark"] #theme-toggle .icon-moon { display: inline; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) #theme-toggle .icon-sun { display: none; }
|
||||||
|
:root:not([data-theme="light"]) #theme-toggle .icon-moon { display: inline; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Layout: Sidebar + Inhalt --- */
|
||||||
|
.site-body { display: flex; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
|
||||||
|
.sidebar {
|
||||||
|
position: sticky; top: 3.6rem; width: 290px; flex: none;
|
||||||
|
height: calc(100vh - 3.6rem); overflow-y: auto; border-right: 1px solid var(--border);
|
||||||
|
background: var(--surface); padding: 1rem 0;
|
||||||
|
}
|
||||||
|
.sidebar-gruppe { border-bottom: 1px solid var(--border); }
|
||||||
|
.sidebar-gruppe summary {
|
||||||
|
cursor: pointer; padding: .6rem 1rem; font-weight: 700; color: var(--accent);
|
||||||
|
list-style: none; display: flex; align-items: center; justify-content: space-between;
|
||||||
|
}
|
||||||
|
.sidebar-gruppe summary::-webkit-details-marker { display: none; }
|
||||||
|
.sidebar-gruppe summary::after { content: "▾"; color: var(--muted); font-size: .8em; }
|
||||||
|
.sidebar-gruppe[open] summary::after { content: "▴"; }
|
||||||
|
.sidebar ul { list-style: none; margin: 0; padding: 0 .5rem .5rem; }
|
||||||
|
.sidebar li a {
|
||||||
|
display: flex; align-items: center; gap: .5rem; padding: .4rem .6rem;
|
||||||
|
border-radius: 8px; color: var(--text); text-decoration: none; font-size: .92rem;
|
||||||
|
}
|
||||||
|
.sidebar li a:hover { background: var(--tint-indigo); }
|
||||||
|
.sidebar li.aktiv a { background: var(--accent); color: var(--accent-text); font-weight: 600; }
|
||||||
|
.fortschritt-haken { width: 1em; height: 1em; color: var(--emerald); visibility: hidden; }
|
||||||
|
.fortschritt-haken .icon { width: 100%; height: 100%; }
|
||||||
|
li[data-erledigt="1"] .fortschritt-haken { visibility: visible; }
|
||||||
|
.sidebar-extra { border-top: 2px solid var(--border); margin-top: .5rem; padding-top: .8rem !important; }
|
||||||
|
/* Unterpunkt in der festen Menueliste (Download-ZIP unter Notebooks): eingerueckt und etwas kleiner, ohne echte Listenverschachtelung. */
|
||||||
|
.sidebar-unterpunkt a { padding-left: 1.9rem !important; font-size: .92em; }
|
||||||
|
.sidebar-overlay { display: none; }
|
||||||
|
|
||||||
|
.content { flex: 1; min-width: 0; padding: 1.5rem 2rem 5rem; max-width: 900px; margin: 0 auto; }
|
||||||
|
|
||||||
|
/* --- Breadcrumb, Prev/Next --- */
|
||||||
|
.breadcrumb { font-size: .85rem; color: var(--muted); margin-bottom: .8rem; }
|
||||||
|
.breadcrumb a { color: var(--muted); }
|
||||||
|
.prev-next { display: flex; justify-content: space-between; gap: 1rem; margin: 1.2rem 0; }
|
||||||
|
.prev-next-knopf {
|
||||||
|
display: flex; align-items: center; gap: .6rem; flex: 1; max-width: 48%;
|
||||||
|
padding: .7rem 1rem; border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
background: var(--surface); color: var(--text); text-decoration: none; font-size: .9rem;
|
||||||
|
box-shadow: var(--shadow); transition: border-color .15s, transform .15s;
|
||||||
|
}
|
||||||
|
.prev-next-knopf:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||||||
|
.prev-next-next { margin-left: auto; text-align: right; flex-direction: row-reverse; }
|
||||||
|
.prev-next-knopf small { display: block; color: var(--muted); font-weight: 400; }
|
||||||
|
.prev-next-leer { flex: 1; max-width: 48%; }
|
||||||
|
|
||||||
|
.fortschritt-knopf {
|
||||||
|
display: inline-flex; align-items: center; gap: .5rem; margin: 1rem 0;
|
||||||
|
padding: .55rem 1rem; border-radius: 20px; border: 1px solid var(--border);
|
||||||
|
background: var(--surface2); color: var(--text); cursor: pointer; font: inherit; font-size: .88rem;
|
||||||
|
}
|
||||||
|
.fortschritt-knopf .icon { color: var(--muted); }
|
||||||
|
.fortschritt-knopf.erledigt { background: var(--tint-emerald); border-color: var(--emerald); color: var(--emerald); }
|
||||||
|
.fortschritt-knopf.erledigt .icon { color: var(--emerald); }
|
||||||
|
|
||||||
|
.site-footer { text-align: center; padding: 1.5rem; color: var(--muted); font-size: .85rem; border-top: 1px solid var(--border); }
|
||||||
|
.site-footer a { color: var(--muted); }
|
||||||
|
|
||||||
|
/* --- Typografie im Artikel (angelehnt an bilder/or_html_style.html) --- */
|
||||||
|
article h1, article h2, article h3, article h4, article h5 {
|
||||||
|
line-height: 1.25; font-weight: 700; margin-top: 2.1em; margin-bottom: .6em; scroll-margin-top: 4.5rem;
|
||||||
|
}
|
||||||
|
article h1 { color: var(--accent); font-size: 1.9rem; border-bottom: 3px solid var(--accent); padding-bottom: .25em; }
|
||||||
|
article h2 { color: var(--accent); font-size: 1.45rem; border-bottom: 2px solid var(--border); padding-bottom: .2em; }
|
||||||
|
article h3 { color: var(--cyan); font-size: 1.18rem; }
|
||||||
|
article h4 { color: var(--violett); font-size: 1.05rem; }
|
||||||
|
article h2[id$="-lernziele"]::before { content: "🎯 "; }
|
||||||
|
article h2[id$="-uebungsaufgaben"]::before { content: "📝 "; }
|
||||||
|
article h2[id$="-selbsttest"]::before { content: "✅ "; }
|
||||||
|
article h2[id$="-zusammenfassung"]::before { content: "📌 "; }
|
||||||
|
article a { color: var(--accent); text-decoration: none; }
|
||||||
|
article a:hover { text-decoration: underline; }
|
||||||
|
article a[target="_blank"]::after { content: " \2197"; font-size: .82em; color: var(--cyan); text-decoration: none; }
|
||||||
|
article p { margin: .75em 0; }
|
||||||
|
article hr { border: 0; border-top: 1px solid var(--border); margin: 2.2em 0; }
|
||||||
|
article figure { margin: 2em 0; text-align: center; }
|
||||||
|
article img { max-width: 100%; height: auto; display: block; margin: 0 auto; border-radius: 10px; }
|
||||||
|
article figure img { box-shadow: var(--shadow-lg); }
|
||||||
|
article figcaption { color: var(--muted); font-size: .9rem; font-style: italic; margin-top: .6em; }
|
||||||
|
article table { border-collapse: collapse; width: 100%; margin: 1.4em 0; font-size: .94rem; box-shadow: var(--shadow); border-radius: 8px; overflow: hidden; }
|
||||||
|
article th { background: var(--accent); color: var(--accent-text); font-weight: 700; text-align: left; }
|
||||||
|
article th, article td { padding: .55em .75em; border: 1px solid var(--border); vertical-align: top; }
|
||||||
|
article tbody tr:nth-child(even) { background: var(--tint-indigo); }
|
||||||
|
article code { font-family: "DejaVu Sans Mono", Consolas, Menlo, monospace; font-size: .9em; background: var(--tint-violett); color: var(--violett); padding: .12em .4em; border-radius: 5px; }
|
||||||
|
article pre { background: var(--code-bg); color: var(--code-text); padding: 1.1em 1.2em; border-radius: 10px; overflow-x: auto; line-height: 1.5; font-size: .85rem; box-shadow: var(--shadow-lg); }
|
||||||
|
article pre code { background: none; color: inherit; padding: 0; font-size: inherit; }
|
||||||
|
article div.sourceCode { position: relative; }
|
||||||
|
article div.sourceCode pre.sourceCode { padding-top: 2.6em; }
|
||||||
|
article blockquote { border-left: 4px solid var(--violett); background: var(--tint-violett); margin: 1.4em 0; padding: .6em 1.2em; border-radius: 0 8px 8px 0; }
|
||||||
|
article ul, article ol { padding-left: 1.4em; }
|
||||||
|
article li { margin: .3em 0; }
|
||||||
|
.katex-display { overflow-x: auto; overflow-y: hidden; padding: .4em 0; }
|
||||||
|
|
||||||
|
/* --- Code-Buttons (Kopieren / Herunterladen) --- */
|
||||||
|
.code-btn-leiste { position: absolute; top: .55em; right: .55em; z-index: 2; display: flex; gap: .4em; }
|
||||||
|
.code-btn, .download-link {
|
||||||
|
display: inline-flex; align-items: center; gap: .35em; background: rgba(255,255,255,.08);
|
||||||
|
color: #cbd5e1; border: 1px solid rgba(255,255,255,.18); border-radius: 6px;
|
||||||
|
padding: .3em .65em; font: inherit; font-size: .78rem; cursor: pointer;
|
||||||
|
text-decoration: none !important; transition: background .15s, color .15s;
|
||||||
|
}
|
||||||
|
.code-btn:hover, .download-link:hover { background: rgba(255,255,255,.18); color: #fff; }
|
||||||
|
.code-btn.copied { background: var(--emerald); color: #fff; border-color: var(--emerald); }
|
||||||
|
.download-link { position: static; margin-left: .6em; }
|
||||||
|
|
||||||
|
/* --- Karten (Kapitel auf einen Blick, Lernpfade, Kapitelraster) --- */
|
||||||
|
.card { border-radius: var(--radius); padding: 1.1em 1.4em; margin: 1.6em 0; box-shadow: var(--shadow); }
|
||||||
|
.card p, .card blockquote { margin: .5em 0 !important; border: none !important; background: none !important; padding: 0 !important; }
|
||||||
|
.card-blick { background: var(--tint-indigo); border: 1px solid var(--accent); }
|
||||||
|
.card-blick blockquote { border-left: 3px solid var(--accent); padding-left: .8em !important; }
|
||||||
|
|
||||||
|
/* --- Didaktische Karten der Version 04 ---------------------------------
|
||||||
|
Jede Kartenart traegt eine eigene Leitfarbe aus der Buchpalette, damit
|
||||||
|
Kursteilnehmer beim Blaettern sofort erkennen, was sie vor sich haben:
|
||||||
|
gruen = sofort ausprobieren, cyan = Formel im Klartext, violett = Excel,
|
||||||
|
amber = Selbstkontrolle, rose = hier steckt ein Fehler drin. Die
|
||||||
|
Leitfarbe liegt links als 5px-Balken, damit sie auch im Ausdruck und in
|
||||||
|
der Druckvorschau erkennbar bleibt. */
|
||||||
|
.card-schnellstart,
|
||||||
|
.card-formel,
|
||||||
|
.card-excel,
|
||||||
|
.card-quiz,
|
||||||
|
.card-denkfehler { border: 1px solid var(--border); border-left-width: 5px; }
|
||||||
|
|
||||||
|
.card-schnellstart { background: var(--tint-emerald); border-left-color: var(--emerald); }
|
||||||
|
.card-formel { background: var(--tint-cyan); border-left-color: var(--cyan); }
|
||||||
|
.card-excel { background: var(--tint-violett); border-left-color: var(--violett); }
|
||||||
|
.card-quiz { background: var(--tint-amber); border-left-color: var(--amber); }
|
||||||
|
.card-denkfehler { background: var(--tint-amber); border-left-color: var(--rose); }
|
||||||
|
|
||||||
|
/* Die Karten enthalten haeufig Tabellen (Formel-Uebersetzer) und
|
||||||
|
Codebloecke (Schnellstart, Denkfehler). Beide sollen sich klar vom
|
||||||
|
getoenten Kartengrund abheben statt darin zu verschwimmen. */
|
||||||
|
.card-schnellstart pre,
|
||||||
|
.card-denkfehler pre { margin: .8em 0 !important; }
|
||||||
|
.card-formel table { margin: .6em 0; background: var(--surface); }
|
||||||
|
.card-formel td:first-child { white-space: nowrap; }
|
||||||
|
|
||||||
|
/* --- Interaktive Plotly-Figuren ---------------------------------------
|
||||||
|
Stehen im Text unmittelbar hinter der statischen Fassung (die im PDF
|
||||||
|
gedruckt wird). Auf schmalen Displays darf die Figur horizontal
|
||||||
|
scrollen, statt die Seite selbst breit zu machen. */
|
||||||
|
.plotly-figur { margin: 1.4em 0; background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius); padding: .6em; overflow-x: auto; }
|
||||||
|
.plotly-figur .plotly-graph-div { min-width: 320px; }
|
||||||
|
|
||||||
|
/* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik,
|
||||||
|
rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die
|
||||||
|
Tabelle sonst aus dem Viewport druecken. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.card-formel td:first-child { white-space: normal; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.kartenraster { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1rem; margin: 1rem 0 2rem; }
|
||||||
|
.karte { display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.1rem; box-shadow: var(--shadow); text-decoration: none; color: var(--text); }
|
||||||
|
.karte:hover { border-color: var(--accent); transform: translateY(-2px); transition: transform .15s, border-color .15s; }
|
||||||
|
.karte h3 { margin: .3em 0; color: var(--text); font-size: 1.05rem; }
|
||||||
|
.karte p { color: var(--muted); font-size: .88rem; margin: .4em 0; }
|
||||||
|
.karte-nummer { font-size: .78rem; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .03em; }
|
||||||
|
.karte-zeit { font-size: .8rem; color: var(--muted); }
|
||||||
|
.karte-pfad { background: var(--tint-violett); border-color: var(--violett); }
|
||||||
|
.karte-einstieg { background: var(--tint-cyan); border-color: var(--cyan); }
|
||||||
|
.kapitelgruppe { margin: 2.5rem 0; }
|
||||||
|
.kapitelgruppe h2 { color: var(--accent); border-bottom: 2px solid var(--border); padding-bottom: .3em; }
|
||||||
|
.lernpfade h2 { color: var(--violett); }
|
||||||
|
|
||||||
|
/* --- Landing-Page Hero --- */
|
||||||
|
.hero { margin: 1rem 0 2.5rem; }
|
||||||
|
.hero-kicker { color: var(--cyan); letter-spacing: .12em; font-weight: 700; font-size: .85rem; }
|
||||||
|
.hero-text h1 { font-size: 2.1rem; color: var(--accent); margin: .2em 0; }
|
||||||
|
.hero-untertitel { color: var(--muted); font-size: 1.05rem; }
|
||||||
|
.hero-badges { display: flex; gap: .6rem; margin-top: 1rem; flex-wrap: wrap; }
|
||||||
|
.hero-kompendium { color: var(--muted); margin-top: 1.4rem; max-width: 640px; }
|
||||||
|
.hero-autor { color: var(--muted); font-size: .92rem; line-height: 1.5; }
|
||||||
|
.badge { padding: .35em .9em; border-radius: 20px; color: #fff; font-weight: 600; font-size: .85rem; }
|
||||||
|
.badge-indigo { background: var(--accent); }
|
||||||
|
.badge-cyan { background: var(--cyan); }
|
||||||
|
.badge-violett { background: var(--violett); }
|
||||||
|
.badge-emerald { background: var(--emerald); }
|
||||||
|
|
||||||
|
/* --- Programme-Seite --- */
|
||||||
|
.programmgruppe { margin: 2rem 0; }
|
||||||
|
.programmliste { list-style: none; padding: 0; }
|
||||||
|
.programmliste li { display: flex; align-items: center; justify-content: space-between; padding: .5em .8em; border: 1px solid var(--border); border-radius: 8px; margin: .4em 0; background: var(--surface); }
|
||||||
|
|
||||||
|
/* --- Stichwortverzeichnis --- */
|
||||||
|
.register-sprungleiste { display: flex; flex-wrap: wrap; gap: .3rem; margin: 1.2rem 0 1.8rem; position: sticky; top: 3.6rem; background: var(--bg); padding: .5rem 0; z-index: 5; }
|
||||||
|
.register-sprungleiste a { display: inline-flex; align-items: center; justify-content: center; width: 1.8rem; height: 1.8rem; border-radius: 6px; background: var(--surface2); color: var(--accent); text-decoration: none; font-weight: 700; font-size: .85rem; }
|
||||||
|
.register-sprungleiste a:hover { background: var(--tint-indigo); }
|
||||||
|
.register-buchstabe { color: var(--accent); border-bottom: 2px solid var(--border); padding-bottom: .15em; margin-top: 2em; font-size: 1.3rem; }
|
||||||
|
.register-eintrag { padding: .55em 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.register-begriff { font-weight: 700; }
|
||||||
|
.register-eintrag a { font-size: .92rem; }
|
||||||
|
.register-unter { list-style: none; margin: .4em 0 0; padding-left: 1.4em; }
|
||||||
|
.register-unter li { margin: .3em 0; color: var(--muted); font-size: .92rem; }
|
||||||
|
.register-unter a { font-size: inherit; }
|
||||||
|
|
||||||
|
/* --- Mobile --- */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.sidebar {
|
||||||
|
position: fixed; top: 0; left: 0; height: 100vh; z-index: 50; transform: translateX(-105%);
|
||||||
|
transition: transform .2s ease; box-shadow: var(--shadow-lg); padding-top: 3.6rem;
|
||||||
|
}
|
||||||
|
.sidebar.offen { transform: translateX(0); }
|
||||||
|
.sidebar-overlay.offen { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 40; }
|
||||||
|
.content { padding: 1.2rem 1rem 4rem; }
|
||||||
|
.site-search { max-width: none; }
|
||||||
|
}
|
||||||
211
web_04/assets/site.js
Normal file
211
web_04/assets/site.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// site.js — Interaktivität der Mehrseiten-Website "Operations Research mit
|
||||||
|
// Python": Dark-Mode-Toggle, mobile Sidebar, Volltextsuche, Lernfortschritt,
|
||||||
|
// Code-Kopieren/Herunterladen. Reines Vanilla-JS, keine Abhaengigkeiten.
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var LS_THEME = "or-theme";
|
||||||
|
var LS_FORTSCHRITT = "or-fortschritt";
|
||||||
|
|
||||||
|
function ladeFortschritt() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem(LS_FORTSCHRITT) || "{}");
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function speichereFortschritt(daten) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LS_FORTSCHRITT, JSON.stringify(daten));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dark-Mode-Toggle -------------------------------------------------
|
||||||
|
function initTheme() {
|
||||||
|
var btn = document.getElementById("theme-toggle");
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
var aktuell = document.documentElement.getAttribute("data-theme");
|
||||||
|
var dunkelSystem = window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
var istDunkel = aktuell ? aktuell === "dark" : dunkelSystem;
|
||||||
|
var neu = istDunkel ? "light" : "dark";
|
||||||
|
document.documentElement.setAttribute("data-theme", neu);
|
||||||
|
try { localStorage.setItem(LS_THEME, neu); } catch (e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mobile Sidebar -----------------------------------------------------
|
||||||
|
function initSidebar() {
|
||||||
|
var sidebar = document.getElementById("sidebar");
|
||||||
|
var overlay = document.getElementById("sidebar-overlay");
|
||||||
|
var knopf = document.getElementById("sidebar-toggle");
|
||||||
|
if (!sidebar || !overlay || !knopf) return;
|
||||||
|
|
||||||
|
function schliessen() {
|
||||||
|
sidebar.classList.remove("offen");
|
||||||
|
overlay.classList.remove("offen");
|
||||||
|
overlay.hidden = true;
|
||||||
|
}
|
||||||
|
knopf.addEventListener("click", function () {
|
||||||
|
var offen = sidebar.classList.toggle("offen");
|
||||||
|
overlay.hidden = !offen;
|
||||||
|
overlay.classList.toggle("offen", offen);
|
||||||
|
});
|
||||||
|
overlay.addEventListener("click", schliessen);
|
||||||
|
sidebar.querySelectorAll("a").forEach(function (a) {
|
||||||
|
a.addEventListener("click", schliessen);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktuelle Kapitelgruppe aufklappen, falls noch nicht offen.
|
||||||
|
var aktiv = sidebar.querySelector("li.aktiv");
|
||||||
|
if (aktiv) {
|
||||||
|
var details = aktiv.closest("details");
|
||||||
|
if (details) details.open = true;
|
||||||
|
// Nur den Sidebar-eigenen Scrollbereich verschieben, NICHT
|
||||||
|
// aktiv.scrollIntoView() verwenden: das wuerde auch das ganze Fenster
|
||||||
|
// scrollen und einen #anker-Sprung aus der URL (z.B. von einem
|
||||||
|
// Querverweis) wieder zunichtemachen.
|
||||||
|
sidebar.scrollTop = Math.max(0, aktiv.offsetTop - sidebar.clientHeight / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lernfortschritt ------------------------------------------------
|
||||||
|
function initFortschritt() {
|
||||||
|
var daten = ladeFortschritt();
|
||||||
|
document.querySelectorAll('.sidebar li[data-kapitel]').forEach(function (li) {
|
||||||
|
if (daten[li.dataset.kapitel]) li.dataset.erledigt = "1";
|
||||||
|
});
|
||||||
|
var knopf = document.querySelector(".fortschritt-knopf");
|
||||||
|
if (!knopf) return;
|
||||||
|
var kapitel = knopf.dataset.kapitel;
|
||||||
|
function aktualisiereKnopf() {
|
||||||
|
var erledigt = !!daten[kapitel];
|
||||||
|
knopf.classList.toggle("erledigt", erledigt);
|
||||||
|
knopf.querySelector("span").textContent =
|
||||||
|
erledigt ? "Als gelesen markiert" : "Als gelesen markieren";
|
||||||
|
var eintrag = document.querySelector(
|
||||||
|
'.sidebar li[data-kapitel="' + kapitel + '"]');
|
||||||
|
if (eintrag) eintrag.dataset.erledigt = erledigt ? "1" : "0";
|
||||||
|
}
|
||||||
|
aktualisiereKnopf();
|
||||||
|
knopf.addEventListener("click", function () {
|
||||||
|
daten[kapitel] = !daten[kapitel];
|
||||||
|
speichereFortschritt(daten);
|
||||||
|
aktualisiereKnopf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Suche ------------------------------------------------------------
|
||||||
|
function initSuche() {
|
||||||
|
var eingabe = document.getElementById("suche-eingabe");
|
||||||
|
var ergebnisBox = document.getElementById("suche-ergebnisse");
|
||||||
|
if (!eingabe || !ergebnisBox) return;
|
||||||
|
// window.OR_SEARCH_INDEX kommt aus assets/search-index.js (als normales
|
||||||
|
// <script> geladen, nicht per fetch()) - fetch() auf lokale Dateien
|
||||||
|
// scheitert unter file:// (Seite ohne Webserver geoeffnet) an CORS.
|
||||||
|
var index = window.OR_SEARCH_INDEX || [];
|
||||||
|
|
||||||
|
function anzeigen(treffer) {
|
||||||
|
if (!treffer.length) {
|
||||||
|
ergebnisBox.innerHTML = '<p class="suche-leer">Keine Treffer.</p>';
|
||||||
|
ergebnisBox.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ergebnisBox.innerHTML = treffer.slice(0, 20).map(function (t) {
|
||||||
|
var href = t.seite + (t.anker ? "#" + t.anker : "");
|
||||||
|
var snippet = t.text ? "<small>" + t.text + "</small>" : "";
|
||||||
|
return '<a href="' + href + '">' + t.titel + snippet + "</a>";
|
||||||
|
}).join("");
|
||||||
|
ergebnisBox.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eingabe.addEventListener("input", function () {
|
||||||
|
var q = eingabe.value.trim().toLowerCase();
|
||||||
|
if (q.length < 2) { ergebnisBox.hidden = true; return; }
|
||||||
|
var treffer = index.filter(function (e) {
|
||||||
|
return (e.titel && e.titel.toLowerCase().indexOf(q) !== -1) ||
|
||||||
|
(e.text && e.text.toLowerCase().indexOf(q) !== -1);
|
||||||
|
});
|
||||||
|
anzeigen(treffer);
|
||||||
|
});
|
||||||
|
document.addEventListener("click", function (ev) {
|
||||||
|
if (!ergebnisBox.contains(ev.target) && ev.target !== eingabe) {
|
||||||
|
ergebnisBox.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
eingabe.addEventListener("keydown", function (ev) {
|
||||||
|
if (ev.key === "Escape") { ergebnisBox.hidden = true; eingabe.blur(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Code: Kopieren-Button (+ Herunterladen, falls Programmdatei bekannt) --
|
||||||
|
function initCodeButtons() {
|
||||||
|
// window.OR_PROGRAMME kommt aus assets/programme.js (normales <script>,
|
||||||
|
// kein fetch() - siehe initSuche()).
|
||||||
|
var programmListe = window.OR_PROGRAMME || [];
|
||||||
|
|
||||||
|
document.querySelectorAll("div.sourceCode").forEach(function (box) {
|
||||||
|
var code = box.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
var leiste = document.createElement("div");
|
||||||
|
leiste.className = "code-btn-leiste";
|
||||||
|
|
||||||
|
var kopieren = document.createElement("button");
|
||||||
|
kopieren.type = "button";
|
||||||
|
kopieren.className = "code-btn";
|
||||||
|
kopieren.setAttribute("aria-label", "Code kopieren");
|
||||||
|
kopieren.innerHTML =
|
||||||
|
'<svg class="icon"><use href="#icon-copy"></use></svg><span>Kopieren</span>';
|
||||||
|
kopieren.addEventListener("click", function () {
|
||||||
|
navigator.clipboard.writeText(code.innerText).then(function () {
|
||||||
|
var label = kopieren.querySelector("span");
|
||||||
|
var original = label.textContent;
|
||||||
|
kopieren.classList.add("copied");
|
||||||
|
label.textContent = "Kopiert!";
|
||||||
|
setTimeout(function () {
|
||||||
|
kopieren.classList.remove("copied");
|
||||||
|
label.textContent = original;
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
leiste.appendChild(kopieren);
|
||||||
|
box.appendChild(leiste);
|
||||||
|
|
||||||
|
// Erste Codezeile "# Name.py" (nach Shebang) -> Download-Button, wenn
|
||||||
|
// eine passende Datei unter programme/ existiert (Programme.py werden
|
||||||
|
// beim Bau nach OR_HTML/programme/ kopiert, siehe build_version_03.py).
|
||||||
|
var text = code.innerText;
|
||||||
|
var treffer = text.match(/^#\s*([A-Za-z0-9_]+\.py)\s*$/m);
|
||||||
|
if (treffer && programmListe.indexOf(treffer[1]) !== -1) {
|
||||||
|
var link = document.createElement("a");
|
||||||
|
link.className = "code-btn";
|
||||||
|
link.href = "programme/" + treffer[1];
|
||||||
|
link.setAttribute("download", "");
|
||||||
|
link.innerHTML =
|
||||||
|
'<svg class="icon"><use href="#icon-download"></use></svg><span>Herunterladen</span>';
|
||||||
|
leiste.appendChild(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
initTheme();
|
||||||
|
initSidebar();
|
||||||
|
initFortschritt();
|
||||||
|
initSuche();
|
||||||
|
initCodeButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bilder ohne width/height-Attribut (siehe Kapitelseiten) reflowen den
|
||||||
|
// Text erst, wenn sie fertig geladen sind - das passiert erst NACH
|
||||||
|
// DOMContentLoaded. Ein #anker-Sprung aus der URL (Querverweis, Such-
|
||||||
|
// treffer, Stichwortverzeichnis) kann dadurch zu weit oben landen. Nach
|
||||||
|
// vollstaendigem Laden (inkl. Bilder) hier zur Sicherheit erneut hinspringen.
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
if (!location.hash) return;
|
||||||
|
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
||||||
|
if (ziel) ziel.scrollIntoView({ behavior: "instant", block: "start" });
|
||||||
|
});
|
||||||
|
})();
|
||||||
BIN
web_04/katex/fonts/KaTeX_AMS-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_AMS-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Caligraphic-Bold.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Caligraphic-Bold.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Caligraphic-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Caligraphic-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Fraktur-Bold.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Fraktur-Bold.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Fraktur-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Fraktur-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Main-Bold.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Main-Bold.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Main-BoldItalic.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Main-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Main-Italic.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Main-Italic.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Main-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Main-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Math-BoldItalic.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Math-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Math-Italic.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Math-Italic.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_SansSerif-Bold.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_SansSerif-Bold.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_SansSerif-Italic.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_SansSerif-Italic.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_SansSerif-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_SansSerif-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Script-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Script-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Size1-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Size1-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Size2-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Size2-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Size3-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Size3-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Size4-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Size4-Regular.woff2
Normal file
Binary file not shown.
BIN
web_04/katex/fonts/KaTeX_Typewriter-Regular.woff2
Normal file
BIN
web_04/katex/fonts/KaTeX_Typewriter-Regular.woff2
Normal file
Binary file not shown.
1
web_04/katex/katex.min.css
vendored
Normal file
1
web_04/katex/katex.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web_04/katex/katex.min.js
vendored
Normal file
1
web_04/katex/katex.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue