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>
This commit is contained in:
2026-09-23 13:32:14 +02:00
co-authored by Claude Opus 5.5
parent f582effe16
commit 19947a845f
18 changed files with 1977 additions and 7 deletions
+9 -1
View File
@@ -45,7 +45,15 @@ in PICTURE geladen werden.
Um zu prüfen, ob `md_to_bpmn.py` selbst korrekt gerechnet hat (Struktur,
Anschlusspunkte, Layout) – unabhängig davon, was ein Import-Werkzeug daraus
macht – eignet sich ein originalgetreuer BPMN-Viewer:
macht – eignet sich ein originalgetreuer BPMN-Viewer. Im Repository liegt
einer, offline und ohne Datenübertragung:
```bash
python scripts/vorschau.py entwuerfe/<name>.md # oder eine .bpmn
```
Ohne Python: `werkzeuge/vorschau/vorschau.html` im Browser öffnen und die
`.bpmn` hineinziehen (Details: `werkzeuge/vorschau/README.md`). Alternativ:
- **`demo.bpmn.io`** – im Browser, kein Install nötig: `.bpmn`-Datei per
Drag & Drop reinziehen.
+4
View File
@@ -47,6 +47,10 @@ schreibst oder bearbeitest.
Ergebnis kurz melden (Anzahl Schritte/Kanten/Lanes, Pfad der Datei) und
anbieten, das Modell im Repository abzulegen (Schritt 5).
Vorschau im Browser, offline: `python scripts/vorschau.py entwuerfe/<name>.bpmn`
(geht auch direkt mit der `.md`, schon vor der Freigabe in Schritt 3).
Beim Anbieten der Vorschau dazusagen, dass PICTURE abweichen kann.
5. **Ablegen (nur auf Wunsch).** Ablauf und Regeln: `prozesse/README.md`
(ADR 0003). Kurz: Branch `feature/<issue-nr>-<name>` von `main`, Ordner
`prozesse/<name>/` mit `<name>.md` und Steckbrief `README.md` aus
+1
View File
@@ -39,6 +39,7 @@
"Bash(python scripts/setup_workspace.py:*)",
"Bash(python scripts/pruefe_umlaute.py:*)",
"Bash(python scripts/pruefe_prozesse.py:*)",
"Bash(python scripts/vorschau.py:*)",
"Bash(python -m pytest:*)"
]
}
+3
View File
@@ -11,3 +11,6 @@
*.pdf binary
*.docx binary
*.vsdx binary
# Eingebettete Fremdbibliothek (werkzeuge/vorschau/README.md): Bytes unverändert lassen, keine Diffs
werkzeuge/vorschau/bpmn-js/** binary
+3 -1
View File
@@ -51,7 +51,8 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
- Rollen und Verantwortliche in Prozessmodellen als Funktion benennen („Sachbearbeitung Flurbereinigung"),
nie als Person.
- Keine Python-Abhängigkeiten außer der Standardbibliothek und `pytest` ohne Issue-Absprache und Eintrag in
`fallstricke.md`.
`fallstricke.md`. Einzige eingebettete Fremdbibliothek ist bpmn-js unter `werkzeuge/vorschau/bpmn-js/` –
nicht von Hand ändern, Aktualisierung nach `werkzeuge/vorschau/README.md`.
- Kein Umbau „nebenbei". Strukturänderungen nur über eigenes Issue.
### Allgemein
@@ -86,6 +87,7 @@ Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gel
- Tests: `python -m pytest -q`
- Umlaute prüfen: `python scripts/pruefe_umlaute.py`
- Prozessmodelle prüfen bzw. alle `.bpmn` neu erzeugen: `python scripts/pruefe_prozesse.py [--erzeugen]`
- Diagramm im Browser ansehen (offline): `python scripts/vorschau.py <datei.bpmn|datei.md>`
- Workspace einrichten oder aktualisieren: `python scripts/setup_workspace.py` (mit `--pull` für fast-forward)
- Entwurf umwandeln: `python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md entwuerfe/<name>.bpmn`
+4 -2
View File
@@ -58,7 +58,8 @@ Im Ordner `AG_Prozesse` Claude Code starten und zum Beispiel sagen:
> *„Modelliere den Prozess aus `referenz/antragspruefung.docx` als BPMN."*
Der Skill `sn-bpmn` erzeugt daraus einen Entwurf `entwuerfe/<name>.md`, nennt die getroffenen Annahmen und wartet.
Du prüfst und änderst den Entwurf fachlich, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
Du prüfst und änderst den Entwurf fachlich — als Diagramm ansehen geht jederzeit mit
`python scripts/vorschau.py entwuerfe/<name>.md` (offline im Browser) —, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
Import in PICTURE. Syntax des Entwurfs: [`.agents/skills/sn-bpmn/Anleitung.md`](.agents/skills/sn-bpmn/Anleitung.md).
`referenz/` und `entwuerfe/` werden nicht versioniert — Quelldokumente bleiben auf deinem Rechner.
@@ -97,7 +98,8 @@ Die vollständige, für Agenten verbindliche Fassung steht in [`AGENTS.md`](AGEN
| [`AGENTS.md`](AGENTS.md) / `CLAUDE.md` | Anweisungen für Agenten (kanonisch ist `AGENTS.md`) |
| [`workspace.toml`](workspace.toml) | Gitea-Organisation und Verzeichnis weiterer Repositories |
| [`prozesse/`](prozesse/README.md) | Versionierte Prozessmodelle, ein Ordner je Prozess (ADR 0003) |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py` |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py`, `pruefe_prozesse.py`, `vorschau.py` |
| [`werkzeuge/vorschau/`](werkzeuge/vorschau/README.md) | BPMN-Vorschau im Browser (bpmn-js, offline); `vorschau.html` auch ohne Python |
| `.agents/skills/` | Skills, gespiegelt nach `.claude/skills/`; `sn-bpmn` enthält den Konverter |
| `.claude/commands/` | Slash-Befehle für Claude Code (`/hallo`) |
| `docs/status/` | **Aktueller Stand** — immer zuerst lesen |
+4 -1
View File
@@ -32,6 +32,8 @@ flowchart TD
- **Der Agent wartet nach dem Entwurf.** Er wandelt erst um, wenn du ausdrücklich zustimmst. Das ist Absicht: die
fachliche Richtigkeit verantwortest du, nicht der Agent.
- **Ansehen statt nur lesen.** `python scripts/vorschau.py entwuerfe/<name>.md` zeigt den Entwurf als Diagramm im
Browser, offline. Ohne Python: `werkzeuge/vorschau/vorschau.html` öffnen und die `.bpmn` hineinziehen.
- **Annahmen ernst nehmen.** Der Agent nennt, wo er Schrittgrenzen, Verzweigungen oder Verantwortliche geraten hat.
Genau dort liegen die Fehler.
- **Rollen statt Personen.** Verantwortliche immer als Funktion („Sachbearbeitung"), nie mit Namen.
@@ -129,7 +131,8 @@ kleine PRs machen.
**Review**
- [ ] Fachlich richtig? (BPMN-Regeln, Begriffe; bei Modellen: Diff der `.md` lesen, nicht der `.bpmn`)
- [ ] Fachlich richtig? (BPMN-Regeln, Begriffe; bei Modellen: Diff der `.md` lesen, nicht der `.bpmn`,
und das Diagramm mit `python scripts/vorschau.py` ansehen)
- [ ] Tests vorhanden und sinnvoll (nicht nur angepasst)?
- [ ] Umfang überschaubar (< 400 Zeilen)?
- [ ] Nach dem Merge: `STATUS.md`
+2 -2
View File
@@ -116,8 +116,8 @@ Die Auto-Memory ist lokal und wird nicht geteilt; Teilbares gehört nach `docs/`
| Python | `ms-python.python` | Syntax, Interpreterwahl, Debugger |
| Markdown Preview Mermaid Support | `bierner.markdown-mermaid` | zeigt Mermaid-Diagramme in der Vorschau |
Zum Ansehen einer `.bpmn` ohne PICTURE eignet sich der Camunda Modeler (Desktop, offline) oder `demo.bpmn.io`,
siehe `.agents/skills/sn-bpmn/Anleitung.md` Abschnitt „Diagramm vorab prüfen“.
Zum Ansehen einer `.bpmn` ohne PICTURE ist keine Erweiterung nötig: `python scripts/vorschau.py <datei>` öffnet
sie im Browser, offline (`werkzeuge/vorschau/README.md`).
### 4.2 Persönliche Einstellungen
+4
View File
@@ -28,6 +28,10 @@ Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern i
## Erledigt
- 23.09.2026 **BPMN-Vorschau** im Browser, offline: `scripts/vorschau.py` und `werkzeuge/vorschau/vorschau.html`
auf Basis von bpmn-js 18.29.1 (lokal eingebettet, MIT mit Wasserzeichen-Auflage). Anzeige direkt in Gitea wäre
Sache der Server-Administration (externer Renderer), Issue-Entwurf liegt vor.
- 23.09.2026 **Ablage der Prozessmodelle** in `prozesse/<name>/` mit Steckbrief, `.md` und erzeugter `.bpmn`
(ADR 0003). `scripts/pruefe_prozesse.py` prüft Struktur, Status und Aktualität der `.bpmn`, auch als Test.
Die in `Anleitung.md` genannten, aber nie committeten Beispiele liegen jetzt unter
+7
View File
@@ -5,6 +5,13 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
- **Das bpmn.io-Wasserzeichen ist Lizenzauflage.** bpmn-js steht unter MIT, verlangt aber, dass das Wasserzeichen
unten rechts sichtbar bleibt und nicht überdeckt wird. In `werkzeuge/vorschau/vorschau.html` deshalb nichts in die
untere rechte Ecke der Zeichenfläche legen. (bpmn-js `LICENSE`, 23.09.2026)
- **Diagramme nie roh in ein `<script>` schreiben.** Eine Beschriftung mit `</script>` würde den Skriptblock beenden.
`scripts/vorschau.py` maskiert jedes `<` im Diagramm als `\u003c`; die Bibliothek selbst wird nicht verändert,
sondern nur auf `<script`/`</script` geprüft. (23.09.2026)
- **Jede Änderung an der Ausgabe von `md_to_bpmn.py` macht die abgelegten `.bpmn` rot.** Der Test in
`tests/test_pruefe_prozesse.py` vergleicht jede `.bpmn` unter `prozesse/` und in `beispiele/` byte-genau mit der
Umwandlung ihrer `.md`. Nach einer Konverteränderung `python scripts/pruefe_prozesse.py --erzeugen` ausführen und
+125
View File
@@ -0,0 +1,125 @@
#!/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())
+124
View File
@@ -0,0 +1,124 @@
"""Tests für scripts/vorschau.py und die eingebettete bpmn-js-Vorschau – reines Python/pytest, ohne Browser."""
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import vorschau # noqa: E402
BEISPIEL_MD = ROOT / ".agents" / "skills" / "sn-bpmn" / "beispiele" / "beispiel_prozess.md"
BEISPIEL_BPMN = BEISPIEL_MD.with_suffix(".bpmn")
def test_vorlage_verweist_nur_auf_vorhandene_lokale_dateien():
"""Prüft, dass vorschau.html nur Dateien unter werkzeuge/vorschau/ einbindet, die es gibt, und nichts aus dem Netz."""
html = vorschau.VORLAGE.read_text(encoding="utf-8")
verweise = re.findall(r'(?:src|href)="([^"]+)"', html)
assert verweise, "keine eingebundenen Dateien gefunden"
for verweis in verweise:
assert not re.match(r"^[a-z]+:", verweis), f"externer Verweis: {verweis}"
assert (vorschau.VORSCHAU / verweis).is_file(), f"fehlt: {verweis}"
def test_lizenz_von_bpmn_js_liegt_bei():
"""Prüft, dass die Lizenz von bpmn-js neben der eingebetteten Bibliothek liegt (MIT-Auflage)."""
lizenz = (vorschau.VORSCHAU / "bpmn-js" / "LICENSE").read_text(encoding="utf-8")
assert "watermark" in lizenz
def test_erzeugte_seite_ist_in_sich_geschlossen():
"""Prüft, dass die erzeugte Seite Bibliothek und Stile eingebettet hat und keine Datei mehr nachlädt."""
html = vorschau.baue_html(BEISPIEL_BPMN.read_text(encoding="utf-8"), BEISPIEL_BPMN.name)
assert not re.search(r"<script[^>]*\bsrc=", html)
assert "<link" not in html
assert "BpmnJS" in html
assert "const EINGEBETTET = null;" not in html
assert '"beispiel_prozess.bpmn"' in html
def test_diagramm_wird_als_javascript_zeichenkette_eingebettet():
"""Prüft, dass der Inhalt der .bpmn verlustfrei als JSON-Zeichenkette mit maskiertem '<' in der Seite landet."""
xml = '<definitions id="x">Ä&amp;</definitions>'
html = vorschau.baue_html(xml, "x.bpmn")
wert = re.search(r"const EINGEBETTET = (.*);", html).group(1)
assert "<" not in wert
assert json.loads(wert) == xml
def test_script_ende_im_diagramm_bricht_die_seite_nicht_auf():
"""Prüft, dass '</script>' in einer Beschriftung nicht als Ende des Skriptblocks wirkt."""
html = vorschau.baue_html('<task name="</script><script>alert(1)</script>"/>', "x.bpmn")
assert "alert(1)" in html
assert "<script>alert(1)" not in html
assert html.lower().count("</script") == html.lower().count("<script")
def test_bibliothek_mit_script_ende_wird_abgelehnt(tmp_path, monkeypatch):
"""Prüft, dass eine (etwa nach einem Update) nicht sicher einbettbare Bibliothek einen Fehler auslöst."""
kopie = tmp_path / "vorschau"
shutil.copytree(vorschau.VORSCHAU, kopie)
js = kopie / "bpmn-js" / "bpmn-navigated-viewer.production.min.js"
js.write_text(js.read_text(encoding="utf-8") + '\nvar x = "</script>";', encoding="utf-8")
monkeypatch.setattr(vorschau, "VORSCHAU", kopie)
monkeypatch.setattr(vorschau, "VORLAGE", kopie / "vorschau.html")
with pytest.raises(RuntimeError, match="</script"):
vorschau.baue_html("<x/>")
def test_md_wird_vor_der_vorschau_umgewandelt():
"""Prüft, dass eine Arbeitsschritt-.md dasselbe XML liefert wie ihre erzeugte .bpmn."""
erwartet = BEISPIEL_BPMN.read_text(encoding="utf-8").replace("\r\n", "\n")
assert vorschau.lies_diagramm(BEISPIEL_MD).replace("\r\n", "\n") == erwartet
def test_ausgabe_in_datei_ohne_browser(tmp_path):
"""Prüft, dass --ausgabe mit --nicht-oeffnen eine Seite schreibt und Exit-Code 0 liefert."""
ziel = tmp_path / "v.html"
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(BEISPIEL_MD),
"--ausgabe", str(ziel), "--nicht-oeffnen"],
capture_output=True, text=True, encoding="utf-8",
)
assert ergebnis.returncode == 0, ergebnis.stdout + ergebnis.stderr
assert "BpmnJS" in ziel.read_text(encoding="utf-8")
def test_stdin_modus_liest_xml_und_schreibt_html():
"""Prüft, dass --stdin als Filter arbeitet (Grundlage für einen Gitea-Renderer)."""
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), "--stdin"],
input=BEISPIEL_BPMN.read_bytes(), capture_output=True,
)
assert ergebnis.returncode == 0, ergebnis.stderr.decode("utf-8", "replace")
html = ergebnis.stdout.decode("utf-8")
assert html.startswith("<!DOCTYPE html>") and "Antrag erfassen" in html
def test_fehlende_datei_gibt_exit_code_1(tmp_path):
"""Prüft, dass eine nicht vorhandene Eingabedatei sauber mit Exit-Code 1 endet."""
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(tmp_path / "gibt-es-nicht.bpmn"),
"--nicht-oeffnen"],
capture_output=True, text=True, encoding="utf-8",
)
assert ergebnis.returncode == 1
assert "nicht gefunden" in ergebnis.stdout
def test_nicht_umwandelbare_md_gibt_exit_code_1(tmp_path):
"""Prüft, dass eine .md mit Syntaxfehler als Meldung statt als Absturz endet."""
md = tmp_path / "kaputt.md"
md.write_text("S1 Start: A\nS1 Ende: B\n", encoding="utf-8")
ergebnis = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(md), "--nicht-oeffnen"],
capture_output=True, text=True, encoding="utf-8",
)
assert ergebnis.returncode == 1
assert "lässt sich nicht umwandeln" in ergebnis.stdout
+49
View File
@@ -0,0 +1,49 @@
# BPMN-Vorschau
Zeigt eine `.bpmn`-Datei als Diagramm im Browser — offline, ohne PICTURE, ohne dass die Datei den Rechner verlässt.
Grundlage ist [bpmn-js](https://github.com/bpmn-io/bpmn-js), die Bibliothek hinter bpmn.io, lokal eingebettet.
## Benutzen
**Mit Python** (aus dem Workspace-Root) — öffnet die Vorschau direkt im Browser, auch für einen Entwurf:
```bash
python scripts/vorschau.py prozesse/antrag-pruefen/antrag-pruefen.bpmn
python scripts/vorschau.py entwuerfe/antrag-pruefen.md # .md wird vorher umgewandelt
```
**Ohne Python:** `werkzeuge/vorschau/vorschau.html` im Browser öffnen (Doppelklick) und eine `.bpmn` hineinziehen
oder über „Datei öffnen …“ wählen. Das funktioniert auch mit einer aus Gitea heruntergeladenen Datei.
Mausrad zoomt, Ziehen verschiebt, „Einpassen“ zeigt das ganze Diagramm.
## Was die Vorschau zeigt — und was nicht
Sie zeigt, was **in der Datei steht**: Elemente, Lanes, Kanten mit genau den Anschlusspunkten, die `md_to_bpmn.py`
berechnet hat. Damit eignet sie sich, um Entwürfe vor der Freigabe und Modelländerungen im Review anzusehen.
PICTURE kann dieselbe Datei anders darstellen (eigene Schriften, eigene Behandlung von Anschlusspunkten). Der Import
in PICTURE bleibt deshalb der letzte Nachweis.
## Eingebettete Bibliothek
| | |
|---|---|
| Paket | `bpmn-js` 18.29.1 (npm), Datei `dist/bpmn-navigated-viewer.production.min.js` und `dist/assets/*.css` |
| Quelle | `https://registry.npmjs.org/bpmn-js/-/bpmn-js-18.29.1.tgz` |
| Integrität (npm) | `sha512-7M3O9LeHbV2ZW9xLXap8CMmIjR+iXWqiXrz7Rt1hE+DqLGhULuwOIgTsQGtXPGVUCrnrk4L38NVlc5R8thOHJw==` |
| Lizenz | MIT mit Auflage, siehe `bpmn-js/LICENSE` |
**Lizenzauflage:** Das bpmn.io-Wasserzeichen unten rechts im Diagramm darf nicht entfernt, verändert oder von
anderen Elementen überdeckt werden. Bedienelemente deshalb nie in die untere rechte Ecke der Zeichenfläche legen.
Die Bibliothek lädt nichts aus dem Netz nach; die Adressen darin sind XML-Namensräume und der Link des Wasserzeichens.
Das Font-Paket `bpmn-font` wird nur vom Editor gebraucht und fehlt bewusst.
## Aktualisieren
1. Neue Version bei npm nachsehen, Tarball laden und die `integrity` aus `https://registry.npmjs.org/bpmn-js/<version>`
gegen die Prüfsumme des geladenen Archivs prüfen.
2. Die vier Dateien in `bpmn-js/` ersetzen (`LICENSE` auf geänderte Auflagen lesen).
3. `python -m pytest -q` — `scripts/vorschau.py` lehnt eine Bibliothek ab, die `<script`/`</script` enthält und damit
nicht sicher eingebettet werden kann.
4. Eine Vorschau erzeugen und im Browser ansehen; Tabelle oben nachziehen. Eigener Pull Request.
+23
View File
@@ -0,0 +1,23 @@
Copyright (c) 2014-present Camunda Services GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The source code responsible for displaying the bpmn.io project watermark that
links back to https://bpmn.io as part of rendered diagrams MUST NOT be
removed or changed. When this software is being used in a website or application,
the watermark must stay fully visible and not visually overlapped by other elements.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
+160
View File
@@ -0,0 +1,160 @@
.bjs-container {
--bjs-font-family: Arial, sans-serif;
--color-grey-225-10-15: hsl(225, 10%, 15%);
--color-grey-225-10-35: hsl(225, 10%, 35%);
--color-grey-225-10-55: hsl(225, 10%, 55%);
--color-grey-225-10-75: hsl(225, 10%, 75%);
--color-grey-225-10-80: hsl(225, 10%, 80%);
--color-grey-225-10-85: hsl(225, 10%, 85%);
--color-grey-225-10-90: hsl(225, 10%, 90%);
--color-grey-225-10-95: hsl(225, 10%, 95%);
--color-grey-225-10-97: hsl(225, 10%, 97%);
--color-blue-205-100-40: hsl(205, 100%, 40%);
--color-blue-205-100-45: hsl(205, 100%, 45%);
--color-blue-205-100-45-opacity-30: hsla(205, 100%, 45%, 30%);
--color-blue-205-100-50: hsl(205, 100%, 50%);
--color-blue-205-100-95: hsl(205, 100%, 95%);
--color-green-150-86-44: hsl(150, 86%, 44%);
--color-red-360-100-40: hsl(360, 100%, 40%);
--color-red-360-100-45: hsl(360, 100%, 45%);
--color-red-360-100-92: hsl(360, 100%, 92%);
--color-red-360-100-97: hsl(360, 100%, 97%);
--color-white: hsl(0, 0%, 100%);
--color-black: hsl(0, 0%, 0%);
--color-black-opacity-05: hsla(0, 0%, 0%, 5%);
--color-black-opacity-10: hsla(0, 0%, 0%, 10%);
--accent-color: var(--color-blue-205-100-40);
--breadcrumbs-font-family: var(--bjs-font-family);
--breadcrumbs-item-color: var(--accent-color);
--breadcrumbs-arrow-color: var(--color-black);
--breadcrumbs-background-color: var(--color-grey-225-10-97);
--breadcrumbs-border-color: var(--color-grey-225-10-75);
--breadcrumbs-focus-outline-color: var(--accent-color);
--drilldown-fill-color: var(--color-white);
--drilldown-background-color: var(--accent-color);
--drilldown-focus-outline-color: var(--accent-color);
}
.bjs-breadcrumbs {
position: absolute;
display: none;
flex-wrap: wrap;
align-items: center;
top: 20px;
left: 20px;
padding: 5px 10px;
margin: 0px;
font-family: var(--breadcrumbs-font-family);
font-size: 16px;
line-height: normal;
background-color: var(--breadcrumbs-background-color);
border: solid 1px var(--breadcrumbs-border-color);
border-radius: 2px;
row-gap: 5px;
}
.bjs-breadcrumbs-shown .bjs-breadcrumbs {
display: flex;
}
.djs-palette-shown .bjs-breadcrumbs {
left: 90px;
}
.djs-palette-shown.djs-palette-two-column .bjs-breadcrumbs {
left: 140px;
}
.bjs-breadcrumbs li {
display: inline-flex;
align-items: center;
}
.bjs-breadcrumbs a.bjs-crumb {
cursor: pointer;
color: var(--breadcrumbs-item-color);
text-decoration: none;
}
.bjs-breadcrumbs a.bjs-crumb:focus-visible {
outline: solid 1px var(--breadcrumbs-focus-outline-color);
outline-offset: 1px;
}
.bjs-breadcrumbs li:not(:first-child)::before {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" /><path d="M0 0h24v24H0z" fill="none" /></svg>');
padding: 0 8px;
color: var(--breadcrumbs-arrow-color);
height: 1em;
}
.bjs-breadcrumbs .bjs-crumb {
display: inline-block;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bjs-drilldown {
width: 20px;
height: 20px;
padding: 0px;
margin-left: -20px;
cursor: pointer;
border: none;
border-radius: 2px;
fill: var(--drilldown-fill-color);
background-color: var(--drilldown-background-color);
}
.bjs-drilldown:focus-visible {
outline: solid 1px var(--drilldown-focus-outline-color);
outline-offset: 1px;
}
.bjs-drilldown-empty {
display: none;
}
.selected .bjs-drilldown-empty {
display: inherit;
}
[data-popup="align-elements"] .djs-popup-results {
display: flex;
}
[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] {
border-left: 1px solid var(--popup-border-color);
}
[data-popup="align-elements"] [data-group="align"] {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
[data-popup="align-elements"] .djs-popup-body .entry {
padding: 6px 8px;
}
[data-popup="align-elements"] .djs-popup-body .entry:not(:first-child) {
margin-top: 0;
}
[data-popup="align-elements"] .djs-popup-entry-icon {
display: block;
margin: 0;
height: 20px;
width: 20px;
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BPMN-Vorschau</title>
<!-- bpmn-js (bpmn.io), lokal eingebettet – siehe README.md. Nichts wird aus dem Netz geladen. -->
<link rel="stylesheet" href="bpmn-js/diagram-js.css">
<link rel="stylesheet" href="bpmn-js/bpmn-js.css">
<style>
:root {
--hintergrund: #ffffff;
--leiste: #f3f5f7;
--rand: #d0d7de;
--text: #1f2328;
--gedaempft: #59636e;
--akzent: #0b6bcb;
--fehler: #b42318;
--warnung: #9a6700;
}
html, body { height: 100%; margin: 0; }
body {
display: flex; flex-direction: column;
font-family: "Segoe UI", system-ui, sans-serif; font-size: 14px;
color: var(--text); background: var(--hintergrund);
}
header {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding: 8px 16px; background: var(--leiste); border-bottom: 1px solid var(--rand);
}
header h1 { font-size: 15px; font-weight: 600; margin: 0 12px 0 0; }
button, label.knopf {
font: inherit; padding: 4px 10px; border: 1px solid var(--rand); border-radius: 6px;
background: #fff; color: var(--text); cursor: pointer;
}
button:hover, label.knopf:hover { border-color: var(--akzent); }
button:disabled { opacity: .5; cursor: default; }
#datei { display: none; }
#dateiname { color: var(--gedaempft); overflow-wrap: anywhere; }
#meldung { flex-basis: 100%; margin: 0; }
#meldung:empty { display: none; }
#meldung.fehler { color: var(--fehler); }
#meldung.warnung { color: var(--warnung); }
/* Zeichenfläche: das bpmn.io-Wasserzeichen unten rechts muss laut Lizenz sichtbar und unverdeckt bleiben. */
main { position: relative; flex: 1; min-height: 0; }
#flaeche { position: absolute; inset: 0; }
#hinweis {
position: absolute; inset: 24px 24px 64px 24px; display: flex; align-items: center; justify-content: center;
text-align: center; color: var(--gedaempft); border: 2px dashed var(--rand); border-radius: 12px;
padding: 16px; pointer-events: none;
}
main.ziehen #hinweis { border-color: var(--akzent); color: var(--akzent); }
main.geladen #hinweis { display: none; }
</style>
</head>
<body>
<header>
<h1>BPMN-Vorschau</h1>
<label class="knopf" for="datei">Datei öffnen …</label>
<input type="file" id="datei" accept=".bpmn,.xml">
<button type="button" id="einpassen" disabled>Einpassen</button>
<button type="button" id="groesser" disabled title="Vergrößern">+</button>
<button type="button" id="kleiner" disabled title="Verkleinern">−</button>
<span id="dateiname"></span>
<p id="meldung" role="status"></p>
</header>
<main id="bereich">
<div id="flaeche"></div>
<div id="hinweis">.bpmn-Datei hierher ziehen oder über „Datei öffnen …“ wählen.<br>
Die Datei wird nur in diesem Browser angezeigt und nirgendwohin übertragen.</div>
</main>
<script src="bpmn-js/bpmn-navigated-viewer.production.min.js"></script>
<script>
// Wird von scripts/vorschau.py durch den Inhalt einer .bpmn und ihren Namen ersetzt.
const EINGEBETTET = null;
const DATEINAME = null;
const bereich = document.getElementById('bereich');
const meldung = document.getElementById('meldung');
const knoepfe = ['einpassen', 'groesser', 'kleiner'].map(id => document.getElementById(id));
const viewer = new BpmnJS({ container: '#flaeche' });
const canvas = () => viewer.get('canvas');
function melde(text, art) {
meldung.textContent = text || '';
meldung.className = art || '';
}
async function zeige(xml, name) {
melde('');
try {
const { warnings } = await viewer.importXML(xml);
canvas().zoom('fit-viewport', 'auto');
bereich.classList.add('geladen');
knoepfe.forEach(k => { k.disabled = false; });
document.getElementById('dateiname').textContent = name || '';
document.title = name ? `${name} – BPMN-Vorschau` : 'BPMN-Vorschau';
if (warnings.length) {
melde(`${warnings.length} Warnung(en) beim Einlesen: ` + warnings.map(w => w.message).join(' | '), 'warnung');
}
} catch (fehler) {
bereich.classList.remove('geladen');
knoepfe.forEach(k => { k.disabled = true; });
melde(`Die Datei ${name || ''} lässt sich nicht als BPMN lesen: ${fehler.message}`, 'fehler');
}
}
function lies(datei) {
if (!datei) return;
datei.text().then(xml => zeige(xml, datei.name));
}
document.getElementById('datei').addEventListener('change', e => lies(e.target.files[0]));
document.getElementById('einpassen').addEventListener('click', () => canvas().zoom('fit-viewport', 'auto'));
document.getElementById('groesser').addEventListener('click', () => canvas().zoom(canvas().zoom() * 1.25));
document.getElementById('kleiner').addEventListener('click', () => canvas().zoom(canvas().zoom() / 1.25));
['dragenter', 'dragover'].forEach(art => document.addEventListener(art, e => {
e.preventDefault(); bereich.classList.add('ziehen');
}));
['dragleave', 'drop'].forEach(art => document.addEventListener(art, e => {
e.preventDefault(); bereich.classList.remove('ziehen');
}));
document.addEventListener('drop', e => lies(e.dataTransfer.files[0]));
if (EINGEBETTET) zeige(EINGEBETTET, DATEINAME);
</script>
</body>
</html>