Files
Projektwissen/scripts/vorschau.py
T
DanielandClaude Opus 5.5 19947a845f BPMN-Vorschau im Browser, offline, auf Basis von bpmn-js
- werkzeuge/vorschau/vorschau.html: Anzeige per Drag & Drop oder
  Dateiauswahl, Zoom, Einpassen, Fehlermeldungen; funktioniert ohne
  Python direkt aus dem Dateisystem.
- scripts/vorschau.py: erzeugt aus .bpmn oder Arbeitsschritt-.md eine
  in sich geschlossene Seite und öffnet sie; --stdin als Filter, etwa
  für einen Gitea-Renderer. Diagramm wird mit maskiertem '<' eingebettet,
  die Bibliothek unverändert und nur auf <script/</script geprüft.
- bpmn-js 18.29.1 (navigated viewer + CSS + LICENSE) lokal eingebettet,
  npm-Integrität geprüft; lädt nichts aus dem Netz. Wasserzeichen bleibt
  laut Lizenzauflage sichtbar.
- tests/test_vorschau.py: 11 Tests; Skill sn-bpmn, Anleitung, AGENTS.md,
  README, Leitfäden, Fallstricke und STATUS ergänzt.
- Tests: python -m pytest -q -> 55 bestanden. Darstellung mit Edge
  headless geprüft (beide Beispiele, leere Seite, kaputte Datei).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 13:32:14 +02:00

126 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.
Aufrufe (aus dem Workspace-Root):
python scripts/vorschau.py prozesse/antrag-pruefen/antrag-pruefen.bpmn
python scripts/vorschau.py entwuerfe/antrag-pruefen.md # .md wird vorher umgewandelt
python scripts/vorschau.py <datei> --ausgabe vorschau.html --nicht-oeffnen
python scripts/vorschau.py --stdin < datei.bpmn > vorschau.html # Filter, z. B. als Gitea-Renderer
Ohne Python: werkzeuge/vorschau/vorschau.html direkt im Browser öffnen und eine .bpmn hineinziehen.
Exit-Code: 0 = in Ordnung, 1 = Datei fehlt oder lässt sich nicht umwandeln.
"""
from __future__ import annotations
import argparse
import json
import sys
import tempfile
import webbrowser
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
VORSCHAU = ROOT / "werkzeuge" / "vorschau"
VORLAGE = VORSCHAU / "vorschau.html"
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="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>':
("script", "bpmn-js/bpmn-navigated-viewer.production.min.js"),
}
PLATZHALTER_DIAGRAMM = "const EINGEBETTET = null;"
PLATZHALTER_NAME = "const DATEINAME = null;"
# Sequenzen, die einen eingebetteten <script>- bzw. <style>-Block vorzeitig beenden oder den HTML-Parser in
# den Sonderzustand für "<!--" im Skript bringen würden. Die Bibliothek wird unverändert eingebettet und deshalb
# nur geprüft; nach einem Update von bpmn-js schlägt baue_html() sonst laut fehl, statt eine kaputte Seite zu liefern.
VERBOTEN = {"script": ("</script", "<script"), "style": ("</style",)}
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")
def baue_html(bpmn_xml: str, name: str | None = None) -> str:
"""Eigenständige Vorschau-Seite mit eingebetteter Bibliothek und eingebettetem Diagramm."""
html = VORLAGE.read_text(encoding="utf-8")
for fundstelle, (art, datei) in EINBETTUNGEN.items():
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")
for sequenz in VERBOTEN[art]:
if sequenz in inhalt.lower():
raise RuntimeError(f"{datei} enthält '{sequenz}' und kann nicht sicher eingebettet werden")
html = html.replace(fundstelle, f"<{art}>\n{inhalt}\n</{art}>")
for platzhalter in (PLATZHALTER_DIAGRAMM, PLATZHALTER_NAME):
if platzhalter not in html:
raise RuntimeError(f"{VORLAGE.name}: Platzhalter '{platzhalter}' fehlt")
html = html.replace(PLATZHALTER_DIAGRAMM, f"const EINGEBETTET = {_js_wert(bpmn_xml)};")
return html.replace(PLATZHALTER_NAME, f"const DATEINAME = {_js_wert(name)};")
def lies_diagramm(pfad: Path) -> str:
"""BPMN-XML aus einer .bpmn lesen oder aus einer Arbeitsschritt-.md erzeugen."""
if pfad.suffix.lower() == ".md":
sys.path.insert(0, str(SKILL))
import md_to_bpmn # noqa: PLC0415 (liegt im Skill-Ordner, nicht als Paket installiert)
meta, steps, order, externals = md_to_bpmn.parse_markdown(pfad.read_text(encoding="utf-8"))
return md_to_bpmn.prettify(md_to_bpmn.build_bpmn(meta, steps, order, externals)).decode("utf-8")
return pfad.read_text(encoding="utf-8")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("datei", nargs="?", help=".bpmn oder Arbeitsschritt-.md")
ap.add_argument("--ausgabe", help="HTML-Datei (Standard: temporäre Datei)")
ap.add_argument("--nicht-oeffnen", action="store_true", help="nur erzeugen, keinen Browser öffnen")
ap.add_argument("--stdin", action="store_true", help="BPMN-XML von stdin lesen, HTML nach stdout schreiben")
args = ap.parse_args()
# Windows-Konsolen und Pipes laufen oft mit cp1252; Meldungen mit Umlauten wären sonst verstümmelt.
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if args.stdin:
xml = sys.stdin.buffer.read().decode("utf-8")
sys.stdout.buffer.write(baue_html(xml).encode("utf-8"))
return 0
if not args.datei:
ap.error("Datei angeben oder --stdin verwenden")
pfad = Path(args.datei)
if not pfad.is_file():
print(f"Datei nicht gefunden: {pfad}")
return 1
try:
xml = lies_diagramm(pfad)
except ValueError as fehler:
print(f"{pfad} lässt sich nicht umwandeln: {fehler}")
return 1
if args.ausgabe:
ziel = Path(args.ausgabe)
else:
ziel = Path(tempfile.gettempdir()) / "bpmn-vorschau" / f"{pfad.stem}.html"
ziel.parent.mkdir(exist_ok=True)
ziel.write_text(baue_html(xml, pfad.name), encoding="utf-8")
print(f"Vorschau: {ziel}")
if not args.nicht_oeffnen:
webbrowser.open(ziel.resolve().as_uri())
return 0
if __name__ == "__main__":
raise SystemExit(main())