Bootstrap lokal einbetten und Stil der AG anlegen (#27)

- werkzeuge/bootstrap/: Bootstrap 5.3.8 (CSS, JS-Bundle) und Bootstrap
  Icons 1.13.1 (CSS, woff2/woff) byte-gleich aus den npm-Paketen,
  Integrität geprüft; README mit Version, Quelle, Integrität, Lizenz
  und Aktualisierung; Lizenzen (MIT) daneben; .gitattributes: binary,
  die README bleibt Text mit Diff.
- werkzeuge/stil/ag_prozesse.css: eigene kleine Stilschicht (ag-*):
  Seite in voller Höhe, dunkle Navigationsleiste, hellgrauer
  Hintergrund, Karten mit farbigem Rand, Tabellenziffern, Druck.
  Vorbild VLN_SN/EA-Bilanz, nichts übernommen.
- Vorschau: Kopfzeile als dunkle Navigationsleiste mit Icon-Knöpfen;
  JavaScript, IDs und Zeichenfläche (Wasserzeichen) unverändert.
- scripts/vorschau.py bettet Bootstrap, Icons und Stil ein und setzt die
  Icon-Schrift als data:-URI ein (_schriften_einbetten); ein nicht
  einbettbarer Schriftverweis bricht laut ab.
- Tests: drei neue in test_vorschau.py; der Aufbau von
  test_bibliothek_mit_script_ende_wird_abgelehnt kopiert jetzt ganz
  werkzeuge/ (Erwartung unverändert).
- AGENTS.md: eingebettete Fremdbibliotheken sind bpmn-js und Bootstrap.
- Tests: python -m pytest -q -> 256 bestanden, 1 übersprungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 20:11:29 +02:00
co-authored by Claude Opus 5.5
parent 73a7f41492
commit b341a658bc
17 changed files with 248 additions and 58 deletions
+31 -3
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""Zeigt eine BPMN-Datei oder eine Arbeitsschritt-.md als Diagramm im Browser – offline, ohne PICTURE.
Grundlage ist werkzeuge/vorschau/vorschau.html mit der lokal eingebetteten Bibliothek bpmn-js (bpmn.io).
Das Skript erzeugt daraus eine einzelne, in sich geschlossene HTML-Datei (Bibliothek, Stile und Diagramm
eingebettet) und öffnet sie im Standardbrowser. Es wird nichts übertragen; die Seite lädt nichts aus dem Netz.
Grundlage ist werkzeuge/vorschau/vorschau.html mit der lokal eingebetteten Bibliothek bpmn-js (bpmn.io), gestaltet
mit Bootstrap und dem Stil der AG (werkzeuge/bootstrap/, werkzeuge/stil/). Das Skript erzeugt daraus eine einzelne,
in sich geschlossene HTML-Datei (Bibliotheken, Stile, Icon-Schrift und Diagramm eingebettet) und öffnet sie im
Standardbrowser. Es wird nichts übertragen; die Seite lädt nichts aus dem Netz.
Aufrufe (aus dem Workspace-Root):
python scripts/vorschau.py Prozessbibliothek/antrag-pruefen/antrag-pruefen.bpmn
@@ -19,7 +20,9 @@ Exit-Code: 0 = in Ordnung, 1 = Datei fehlt oder lässt sich nicht umwandeln.
from __future__ import annotations
import argparse
import base64
import json
import re
import sys
import tempfile
import webbrowser
@@ -32,6 +35,10 @@ SKILL = ROOT / ".agents" / "skills" / "sn-bpmn"
# Stellen in vorschau.html, die beim Einbetten ersetzt werden. Ein Test prüft, dass es sie gibt.
EINBETTUNGEN = {
'<link rel="stylesheet" href="../bootstrap/bootstrap.min.css">': ("style", "../bootstrap/bootstrap.min.css"),
'<link rel="stylesheet" href="../bootstrap/icons/bootstrap-icons.min.css">':
("style", "../bootstrap/icons/bootstrap-icons.min.css"),
'<link rel="stylesheet" href="../stil/ag_prozesse.css">': ("style", "../stil/ag_prozesse.css"),
'<link rel="stylesheet" href="bpmn-js/diagram-js.css">': ("style", "bpmn-js/diagram-js.css"),
'<link rel="stylesheet" href="bpmn-js/bpmn-js.css">': ("style", "bpmn-js/bpmn-js.css"),
'<script src="bpmn-js/bpmn-navigated-viewer.production.min.js"></script>':
@@ -47,6 +54,25 @@ PLATZHALTER_NAME = "const DATEINAME = null;"
VERBOTEN = {"script": ("</script", "<script"), "style": ("</style",)}
# Die Icon-CSS verweist relativ auf ihre Schrift (url("fonts/bootstrap-icons.woff2?…")). In der erzeugten Einzeldatei
# gäbe es diese Datei nicht: die woff2-Schrift wird als data:-URI eingesetzt, der woff-Rückfall entfällt (#27).
SCHRIFT = re.compile(r'url\("fonts/(?P<datei>[^"?]+)(?:\?[^"]*)?"\)\s*format\("(?P<format>[\w-]+)"\)')
def _schriften_einbetten(inhalt: str, ordner: Path) -> str:
"""Ersetzt in einer CSS die relativen Schriftverweise: woff2 als data:-URI, alle übrigen Formate fallen weg."""
def ersetze(treffer: re.Match) -> str:
if treffer["format"] != "woff2":
return ""
daten = base64.b64encode((ordner / "fonts" / treffer["datei"]).read_bytes()).decode("ascii")
return f'url("data:font/woff2;base64,{daten}") format("woff2")'
ergebnis = SCHRIFT.sub(ersetze, inhalt)
ergebnis = re.sub(r',\s*(?=[;}])', '', ergebnis) # Komma vor dem entfallenen Rückfall
if 'url("fonts/' in ergebnis:
raise RuntimeError("Icon-CSS enthält einen Schriftverweis, der sich nicht einbetten lässt")
return ergebnis
def _js_wert(text: str | None) -> str:
"""Als JavaScript-Literal, sicher innerhalb von <script>: jedes '<' wird als \\u003c geschrieben."""
return json.dumps(text, ensure_ascii=False).replace("<", "\\u003c")
@@ -59,6 +85,8 @@ def baue_html(bpmn_xml: str, name: str | None = None) -> str:
if fundstelle not in html:
raise RuntimeError(f"{VORLAGE.name}: '{fundstelle}' fehlt – Vorlage und scripts/vorschau.py passen nicht zusammen")
inhalt = (VORSCHAU / datei).read_text(encoding="utf-8")
if 'url("fonts/' in inhalt:
inhalt = _schriften_einbetten(inhalt, (VORSCHAU / datei).parent)
for sequenz in VERBOTEN[art]:
if sequenz in inhalt.lower():
raise RuntimeError(f"{datei} enthält '{sequenz}' und kann nicht sicher eingebettet werden")