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
+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>