clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

feat: replace browser BPMN with bounded edge SVG

Dami committed 2d08813
Render BPMN 2.0 diagram interchange data directly in a dedicated edge Worker with bounded XML parsing, core BPMN shapes, labels, flows, and deterministic SVG. Remove BPMN from Browser Run and the bpmn-js client package, update catalog and normalization identity, and cover the production fixture and hostile XML inputs.
commit
2d0881334ce2af1bd0c0266c610fcf32c6a42989
parent
820567b5b7b420c083fc3c256988b49b666a1dbe
author
Dami <dami.etoile@gmail.com>
26 files changed, +218 -921
MTHIRD_PARTY_NOTICES.md1-
Mapps/editor/test/svg-contract.test.js1+
Dlicenses/bpmn-license.txt23-
Mrenderers/browser-run/src/contracts.ts3+-
Mrenderers/browser-run/src/executor.ts2+-
Mrenderers/browser-run/test/contracts.test.ts6+-
Mrenderers/browser-run/test/executor.test.ts6+-
Mrenderers/browser-run/test/worker.test.ts8+-
Mrenderers/catalog/src/catalog.ts6+-
Mrenderers/catalog/test/catalog.test.ts6+
Drenderers/client/bpmn/.gitignore2-
Drenderers/client/bpmn/package-lock.json730------------------------------
Drenderers/client/bpmn/package.json17-
Drenderers/client/bpmn/scripts/build.mjs22-
Drenderers/client/bpmn/src/frame.js63---
Drenderers/client/bpmn/unit/_headers12-
Drenderers/client/bpmn/unit/index.html14-
Drenderers/client/bpmn/unit/worker.js3-
Drenderers/client/bpmn/wrangler.jsonc16-
Arenderers/edge/config/units/bpmn.jsonc9+
Arenderers/edge/src/adapters/bpmn.ts123+++++
Arenderers/edge/src/units/bpmn.ts14+
Arenderers/edge/test/bpmn.test.ts28+
Arenderers/edge/test/fixtures/bpmn/core.bpmn15+
Mrenderers/scripts/smoke-units.mjs7+-
Mshared/svg/index.js2+-
M THIRD_PARTY_NOTICES.md +0 -1
@@ -41,7 +41,6 @@ DiagramZip product, repository, or hosted service.
4141 | Renderer paths or deployable unit | Third-party material | License and effective boundary | Detailed record or full text |4242 | --- | --- | --- | --- |4343 | `renderers/client/mermaid/` | Mermaid 11.17.0 and its emitted browser dependencies | Mermaid is MIT; browser bundle is delivered to recipients | [Mermaid text](licenses/mermaid-license.txt), package lock |44-| `renderers/client/bpmn/` | bpmn-js 18.25.1 | Custom bpmn.io MIT-like terms, including its watermark condition; browser bundle is delivered to recipients | [bpmn.io terms](licenses/bpmn-license.txt) |4544 | `renderers/client/diagramsnet/` | diagrams.net 29.6.1 export runtime, shapes, stencils, images, and math assets | Application code is Apache-2.0; visual assets retain the additional diagrams.net terms. The built client publishes both beside the bundle | [renderer notice](renderers/client/diagramsnet/README.md), nested `vendor/assets/*/LICENSE` files |4645 | `renderers/client/tikz/` | `@planktimerr/tikzjax` 1.0.63 plus TeX/TikZ assets | The deployable browser unit is GPL-3.0-or-later; bundled TeX/TikZ material is LPPL-1.3c. Corresponding source and terms are published on the renderer origin | [renderer notice](renderers/client/tikz/README.md), `vendor-licenses/` |4746 | `renderers/python/wireviz-translator/` | Vendored WireViz release source and PyYAML | The in-process combined Worker unit is GPL-3.0-only; PyYAML is MIT. Separate services reached over HTTP or a service binding are outside this Python program | [unit scope](renderers/python/wireviz-translator/README.md), [notices](renderers/python/wireviz-translator/THIRD_PARTY_NOTICES.md) |
M apps/editor/test/svg-contract.test.js +1 -0
D licenses/bpmn-license.txt +0 -23
M renderers/browser-run/src/contracts.ts +1 -2
M renderers/browser-run/src/executor.ts +1 -1
M renderers/browser-run/test/contracts.test.ts +3 -3
M renderers/browser-run/test/executor.test.ts +3 -3
@@ -7,11 +7,11 @@ describe('browser page executor', () => {
77 const fakeWindow = {88 addEventListener: (_type: string, listener: (event: MessageEvent) => void) => listeners.add(listener),99 removeEventListener: (_type: string, listener: (event: MessageEvent) => void) => listeners.delete(listener),10- postMessage: (data: unknown) => queueMicrotask(() => listeners.forEach(listener => listener({ source: globalThis, data: { channel: 'diagram.zip:renderer:v1', type: 'result', requestId: (data as any).requestId, ok: true, svg: '<svg/>', version: 'v', build: 'b', pipeline: ['bpmn'] } } as unknown as MessageEvent))),10+ postMessage: (data: unknown) => queueMicrotask(() => listeners.forEach(listener => listener({ source: globalThis, data: { channel: 'diagram.zip:renderer:v1', type: 'result', requestId: (data as any).requestId, ok: true, svg: '<svg/>', version: 'v', build: 'b', pipeline: ['mermaid'] } } as unknown as MessageEvent))),1111 }1212 const oldMethods = { addEventListener: (globalThis as any).addEventListener, removeEventListener: (globalThis as any).removeEventListener, postMessage: (globalThis as any).postMessage }1313 Object.assign(globalThis, fakeWindow)14- await expect(browserHarness({ engine: 'bpmn', requestId: 'r', source: '<xml/>' })).resolves.toMatchObject({ ok: true, svg: '<svg/>' })14+ await expect(browserHarness({ engine: 'mermaid', requestId: 'r', source: 'graph TD' })).resolves.toMatchObject({ ok: true, svg: '<svg/>' })1515 Object.assign(globalThis, oldMethods)1616 })1717
@@ -33,6 +33,6 @@ describe('browser page executor', () => {
3333 async goto() {},3434 async evaluate<T>(): Promise<T> { return { ok: true, svg: 'x'.repeat(4_194_305), version: 'v', build: 'b', pipeline: [] } as T },3535 }36- await expect(createPageExecutor(page).render({ engine: 'bpmn', requestId: 'r', source: '' })).rejects.toThrow(/output is too large/)36+ await expect(createPageExecutor(page).render({ engine: 'mermaid', requestId: 'r', source: '' })).rejects.toThrow(/output is too large/)3737 })3838 })
M renderers/browser-run/test/worker.test.ts +4 -4
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
22 vi.mock('cloudflare:workers', () => ({ DurableObject: class {} }))33 import worker, { responseFromRenderResult } from '../src/index'44 5-function environment(response = { ok: true, svg: '<svg/>', version: 'v', build: 'b', pipeline: ['bpmn'] }) {5+function environment(response = { ok: true, svg: '<svg/>', version: 'v', build: 'b', pipeline: ['mermaid'] }) {66 const forwarded: Request[] = []77 const stub = { fetch: vi.fn(async (request: Request) => { forwarded.push(request); return Response.json(response) }) }88 return {
@@ -26,13 +26,13 @@ describe('private Worker boundary', () => {
2626 2727 it('requires auth, validates JSON, and forwards the exact render contract to the DO', async () => {2828 const setup = environment()29- const body = JSON.stringify({ engine: 'bpmn', requestId: 'request-1', source: '<definitions/>' })29+ const body = JSON.stringify({ engine: 'mermaid', requestId: 'request-1', source: 'graph TD' })3030 expect((await worker.fetch(new Request('https://bridge.example/render', { method: 'POST', body }), setup.env)).status).toBe(401)3131 const request = new Request('https://bridge.example/render', { method: 'POST', body, headers: { Authorization: 'Bearer secret' } })3232 expect((await worker.fetch(request, setup.env)).status).toBe(200)33- expect(setup.env.BROWSER_SESSIONS.getByName).toHaveBeenCalledWith('renderer:bpmn')33+ expect(setup.env.BROWSER_SESSIONS.getByName).toHaveBeenCalledWith('renderer:mermaid')3434 expect(setup.forwarded).toHaveLength(1)35- expect(await setup.forwarded[0]!.json()).toEqual({ engine: 'bpmn', requestId: 'request-1', source: '<definitions/>' })35+ expect(await setup.forwarded[0]!.json()).toEqual({ engine: 'mermaid', requestId: 'request-1', source: 'graph TD' })3636 })3737 3838 it('maps a frame failure to a non-success structured error', async () => {
M renderers/catalog/src/catalog.ts +3 -3
M renderers/catalog/test/catalog.test.ts +6 -0
D renderers/client/bpmn/.gitignore +0 -2
@@ -1,2 +0,0 @@
1-node_modules/2-dist/
D renderers/client/bpmn/package-lock.json +0 -730
@@ -1,730 +0,0 @@
1-{2- "name": "@diagramzip/renderer-bpmn",3- "version": "1.0.0",4- "lockfileVersion": 3,5- "requires": true,6- "packages": {7- "": {8- "name": "@diagramzip/renderer-bpmn",9- "version": "1.0.0",10- "dependencies": {11- "bpmn-js": "18.25.1"12- },13- "devDependencies": {14- "esbuild": "0.28.1"15- },16- "engines": {17- "node": ">=24"18- }19- },20- "node_modules/@bpmn-io/diagram-js-ui": {21- "version": "0.2.4",22- "resolved": "https://registry.npmjs.org/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.4.tgz",23- "integrity": "sha512-I678ZGtoH7z7FgzFhzzppM9ThsJ3Lh83kW3GjhjG3xyYciIPYHMaHK1MABP8F69H9pFZpViM6t4qUo8dwrVz0w==",24- "license": "MIT",25- "dependencies": {26- "htm": "^3.1.1",27- "preact": "^10.29.2"28- }29- },30- "node_modules/@esbuild/aix-ppc64": {31- "version": "0.28.1",32- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",33- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",34- "cpu": [35- "ppc64"36- ],37- "dev": true,38- "license": "MIT",39- "optional": true,40- "os": [41- "aix"42- ],43- "engines": {44- "node": ">=18"45- }46- },47- "node_modules/@esbuild/android-arm": {48- "version": "0.28.1",49- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",50- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",51- "cpu": [52- "arm"53- ],54- "dev": true,55- "license": "MIT",56- "optional": true,57- "os": [58- "android"59- ],60- "engines": {61- "node": ">=18"62- }63- },64- "node_modules/@esbuild/android-arm64": {65- "version": "0.28.1",66- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",67- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",68- "cpu": [69- "arm64"70- ],71- "dev": true,72- "license": "MIT",73- "optional": true,74- "os": [75- "android"76- ],77- "engines": {78- "node": ">=18"79- }80- },81- "node_modules/@esbuild/android-x64": {82- "version": "0.28.1",83- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",84- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",85- "cpu": [86- "x64"87- ],88- "dev": true,89- "license": "MIT",90- "optional": true,91- "os": [92- "android"93- ],94- "engines": {95- "node": ">=18"96- }97- },98- "node_modules/@esbuild/darwin-arm64": {99- "version": "0.28.1",100- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",101- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",102- "cpu": [103- "arm64"104- ],105- "dev": true,106- "license": "MIT",107- "optional": true,108- "os": [109- "darwin"110- ],111- "engines": {112- "node": ">=18"113- }114- },115- "node_modules/@esbuild/darwin-x64": {116- "version": "0.28.1",117- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",118- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",119- "cpu": [120- "x64"121- ],122- "dev": true,123- "license": "MIT",124- "optional": true,125- "os": [126- "darwin"127- ],128- "engines": {129- "node": ">=18"130- }131- },132- "node_modules/@esbuild/freebsd-arm64": {133- "version": "0.28.1",134- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",135- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",136- "cpu": [137- "arm64"138- ],139- "dev": true,140- "license": "MIT",141- "optional": true,142- "os": [143- "freebsd"144- ],145- "engines": {146- "node": ">=18"147- }148- },149- "node_modules/@esbuild/freebsd-x64": {150- "version": "0.28.1",151- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",152- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",153- "cpu": [154- "x64"155- ],156- "dev": true,157- "license": "MIT",158- "optional": true,159- "os": [160- "freebsd"161- ],162- "engines": {163- "node": ">=18"164- }165- },166- "node_modules/@esbuild/linux-arm": {167- "version": "0.28.1",168- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",169- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",170- "cpu": [171- "arm"172- ],173- "dev": true,174- "license": "MIT",175- "optional": true,176- "os": [177- "linux"178- ],179- "engines": {180- "node": ">=18"181- }182- },183- "node_modules/@esbuild/linux-arm64": {184- "version": "0.28.1",185- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",186- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",187- "cpu": [188- "arm64"189- ],190- "dev": true,191- "license": "MIT",192- "optional": true,193- "os": [194- "linux"195- ],196- "engines": {197- "node": ">=18"198- }199- },200- "node_modules/@esbuild/linux-ia32": {201- "version": "0.28.1",202- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",203- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",204- "cpu": [205- "ia32"206- ],207- "dev": true,208- "license": "MIT",209- "optional": true,210- "os": [211- "linux"212- ],213- "engines": {214- "node": ">=18"215- }216- },217- "node_modules/@esbuild/linux-loong64": {218- "version": "0.28.1",219- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",220- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",221- "cpu": [222- "loong64"223- ],224- "dev": true,225- "license": "MIT",226- "optional": true,227- "os": [228- "linux"229- ],230- "engines": {231- "node": ">=18"232- }233- },234- "node_modules/@esbuild/linux-mips64el": {235- "version": "0.28.1",236- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",237- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",238- "cpu": [239- "mips64el"240- ],241- "dev": true,242- "license": "MIT",243- "optional": true,244- "os": [245- "linux"246- ],247- "engines": {248- "node": ">=18"249- }250- },251- "node_modules/@esbuild/linux-ppc64": {252- "version": "0.28.1",253- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",254- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",255- "cpu": [256- "ppc64"257- ],258- "dev": true,259- "license": "MIT",260- "optional": true,261- "os": [262- "linux"263- ],264- "engines": {265- "node": ">=18"266- }267- },268- "node_modules/@esbuild/linux-riscv64": {269- "version": "0.28.1",270- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",271- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",272- "cpu": [273- "riscv64"274- ],275- "dev": true,276- "license": "MIT",277- "optional": true,278- "os": [279- "linux"280- ],281- "engines": {282- "node": ">=18"283- }284- },285- "node_modules/@esbuild/linux-s390x": {286- "version": "0.28.1",287- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",288- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",289- "cpu": [290- "s390x"291- ],292- "dev": true,293- "license": "MIT",294- "optional": true,295- "os": [296- "linux"297- ],298- "engines": {299- "node": ">=18"300- }301- },302- "node_modules/@esbuild/linux-x64": {303- "version": "0.28.1",304- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",305- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",306- "cpu": [307- "x64"308- ],309- "dev": true,310- "license": "MIT",311- "optional": true,312- "os": [313- "linux"314- ],315- "engines": {316- "node": ">=18"317- }318- },319- "node_modules/@esbuild/netbsd-arm64": {320- "version": "0.28.1",321- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",322- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",323- "cpu": [324- "arm64"325- ],326- "dev": true,327- "license": "MIT",328- "optional": true,329- "os": [330- "netbsd"331- ],332- "engines": {333- "node": ">=18"334- }335- },336- "node_modules/@esbuild/netbsd-x64": {337- "version": "0.28.1",338- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",339- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",340- "cpu": [341- "x64"342- ],343- "dev": true,344- "license": "MIT",345- "optional": true,346- "os": [347- "netbsd"348- ],349- "engines": {350- "node": ">=18"351- }352- },353- "node_modules/@esbuild/openbsd-arm64": {354- "version": "0.28.1",355- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",356- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",357- "cpu": [358- "arm64"359- ],360- "dev": true,361- "license": "MIT",362- "optional": true,363- "os": [364- "openbsd"365- ],366- "engines": {367- "node": ">=18"368- }369- },370- "node_modules/@esbuild/openbsd-x64": {371- "version": "0.28.1",372- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",373- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",374- "cpu": [375- "x64"376- ],377- "dev": true,378- "license": "MIT",379- "optional": true,380- "os": [381- "openbsd"382- ],383- "engines": {384- "node": ">=18"385- }386- },387- "node_modules/@esbuild/openharmony-arm64": {388- "version": "0.28.1",389- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",390- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",391- "cpu": [392- "arm64"393- ],394- "dev": true,395- "license": "MIT",396- "optional": true,397- "os": [398- "openharmony"399- ],400- "engines": {401- "node": ">=18"402- }403- },404- "node_modules/@esbuild/sunos-x64": {405- "version": "0.28.1",406- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",407- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",408- "cpu": [409- "x64"410- ],411- "dev": true,412- "license": "MIT",413- "optional": true,414- "os": [415- "sunos"416- ],417- "engines": {418- "node": ">=18"419- }420- },421- "node_modules/@esbuild/win32-arm64": {422- "version": "0.28.1",423- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",424- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",425- "cpu": [426- "arm64"427- ],428- "dev": true,429- "license": "MIT",430- "optional": true,431- "os": [432- "win32"433- ],434- "engines": {435- "node": ">=18"436- }437- },438- "node_modules/@esbuild/win32-ia32": {439- "version": "0.28.1",440- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",441- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",442- "cpu": [443- "ia32"444- ],445- "dev": true,446- "license": "MIT",447- "optional": true,448- "os": [449- "win32"450- ],451- "engines": {452- "node": ">=18"453- }454- },455- "node_modules/@esbuild/win32-x64": {456- "version": "0.28.1",457- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",458- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",459- "cpu": [460- "x64"461- ],462- "dev": true,463- "license": "MIT",464- "optional": true,465- "os": [466- "win32"467- ],468- "engines": {469- "node": ">=18"470- }471- },472- "node_modules/bpmn-js": {473- "version": "18.25.1",474- "resolved": "https://registry.npmjs.org/bpmn-js/-/bpmn-js-18.25.1.tgz",475- "integrity": "sha512-wT/TjzUQw3o5vU6nI2tytOhOm9atf2tOgCdCKF6sQUvokZ0nOEH7yfE9IvwKXclfdUxvdawgU80h909hvQJ9hw==",476- "license": "SEE LICENSE IN LICENSE",477- "dependencies": {478- "bpmn-moddle": "^10.0.0",479- "diagram-js": "^15.24.0",480- "diagram-js-direct-editing": "^3.5.1",481- "ids": "^3.0.2",482- "inherits-browser": "^0.1.0",483- "min-dash": "^5.1.0",484- "min-dom": "^5.3.0",485- "tiny-svg": "^4.1.4"486- },487- "engines": {488- "node": "*"489- }490- },491- "node_modules/bpmn-moddle": {492- "version": "10.1.0",493- "resolved": "https://registry.npmjs.org/bpmn-moddle/-/bpmn-moddle-10.1.0.tgz",494- "integrity": "sha512-k9Vl97hiFwlr4bJTQ5+pZp21bmrQwil5jnMQ30wF8vney2/WnIcJmWwYzRTAKFVoViTGowtDfbO5rGEF4viO0w==",495- "license": "MIT",496- "dependencies": {497- "min-dash": "^5.1.0",498- "moddle": "^8.2.1",499- "moddle-xml": "^12.1.0"500- },501- "engines": {502- "node": ">= 20.12"503- }504- },505- "node_modules/clsx": {506- "version": "2.1.1",507- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",508- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",509- "license": "MIT",510- "engines": {511- "node": ">=6"512- }513- },514- "node_modules/diagram-js": {515- "version": "15.24.1",516- "resolved": "https://registry.npmjs.org/diagram-js/-/diagram-js-15.24.1.tgz",517- "integrity": "sha512-0zlPffk8pGdbUmt0RQH32N7dIbgYuLwkFlZTCi28kRSkb37GdEjONjXKs+NluZvWeLsY1hqSyq4le19ZeFE19Q==",518- "license": "MIT",519- "dependencies": {520- "@bpmn-io/diagram-js-ui": "^0.2.4",521- "clsx": "^2.1.1",522- "didi": "^11.0.0",523- "inherits-browser": "^0.1.0",524- "min-dash": "^5.1.0",525- "min-dom": "^5.3.0",526- "object-refs": "^0.4.0",527- "path-intersection": "^4.1.0",528- "tiny-svg": "^4.1.4"529- },530- "engines": {531- "node": "*"532- }533- },534- "node_modules/diagram-js-direct-editing": {535- "version": "3.5.1",536- "resolved": "https://registry.npmjs.org/diagram-js-direct-editing/-/diagram-js-direct-editing-3.5.1.tgz",537- "integrity": "sha512-fEQCQ/x5oKVV0m7Nmgv7i85Vhw+kJkKNXF8zcV5Vm0KwF8x/8e3Ax0aJgdXt8/fDPymEkY8HmZweXIipCeeh+g==",538- "license": "MIT",539- "dependencies": {540- "min-dash": "^5.0.0",541- "min-dom": "^5.3.0"542- },543- "engines": {544- "node": "*"545- },546- "peerDependencies": {547- "diagram-js": "*"548- }549- },550- "node_modules/didi": {551- "version": "11.0.0",552- "resolved": "https://registry.npmjs.org/didi/-/didi-11.0.0.tgz",553- "integrity": "sha512-PzCfRzQttvFpVcYMbSF7h8EsWjeJpVjWH4qDhB5LkMi1ILvHq4Ob0vhM2wLFziPkbUBi+PAo7ODbe2sacR7nJQ==",554- "license": "MIT",555- "engines": {556- "node": ">= 20.12"557- }558- },559- "node_modules/domify": {560- "version": "3.0.0",561- "resolved": "https://registry.npmjs.org/domify/-/domify-3.0.0.tgz",562- "integrity": "sha512-bs2yO68JDFOm6rKv8f0EnrM2cENduhRkpqOtt/s5l5JBA/eqGBZCzLPmdYoHtJ6utgLGgcBajFsEQbl12pT0lQ==",563- "license": "MIT",564- "engines": {565- "node": ">=20"566- },567- "funding": {568- "url": "https://github.com/sponsors/sindresorhus"569- }570- },571- "node_modules/esbuild": {572- "version": "0.28.1",573- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",574- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",575- "dev": true,576- "hasInstallScript": true,577- "license": "MIT",578- "bin": {579- "esbuild": "bin/esbuild"580- },581- "engines": {582- "node": ">=18"583- },584- "optionalDependencies": {585- "@esbuild/aix-ppc64": "0.28.1",586- "@esbuild/android-arm": "0.28.1",587- "@esbuild/android-arm64": "0.28.1",588- "@esbuild/android-x64": "0.28.1",589- "@esbuild/darwin-arm64": "0.28.1",590- "@esbuild/darwin-x64": "0.28.1",591- "@esbuild/freebsd-arm64": "0.28.1",592- "@esbuild/freebsd-x64": "0.28.1",593- "@esbuild/linux-arm": "0.28.1",594- "@esbuild/linux-arm64": "0.28.1",595- "@esbuild/linux-ia32": "0.28.1",596- "@esbuild/linux-loong64": "0.28.1",597- "@esbuild/linux-mips64el": "0.28.1",598- "@esbuild/linux-ppc64": "0.28.1",599- "@esbuild/linux-riscv64": "0.28.1",600- "@esbuild/linux-s390x": "0.28.1",601- "@esbuild/linux-x64": "0.28.1",602- "@esbuild/netbsd-arm64": "0.28.1",603- "@esbuild/netbsd-x64": "0.28.1",604- "@esbuild/openbsd-arm64": "0.28.1",605- "@esbuild/openbsd-x64": "0.28.1",606- "@esbuild/openharmony-arm64": "0.28.1",607- "@esbuild/sunos-x64": "0.28.1",608- "@esbuild/win32-arm64": "0.28.1",609- "@esbuild/win32-ia32": "0.28.1",610- "@esbuild/win32-x64": "0.28.1"611- }612- },613- "node_modules/htm": {614- "version": "3.1.1",615- "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",616- "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",617- "license": "Apache-2.0"618- },619- "node_modules/ids": {620- "version": "3.0.2",621- "resolved": "https://registry.npmjs.org/ids/-/ids-3.0.2.tgz",622- "integrity": "sha512-t6YJP4mdC+GHF96Nbis/4FEANhP/8VWmYMvUuYpXvSdrhg5hpIVbq2XZlOA3UWTbtdwPCi0q7jEXOdHkAnqOnw==",623- "license": "MIT",624- "engines": {625- "node": ">= 20.12"626- }627- },628- "node_modules/inherits-browser": {629- "version": "0.1.0",630- "resolved": "https://registry.npmjs.org/inherits-browser/-/inherits-browser-0.1.0.tgz",631- "integrity": "sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==",632- "license": "ISC"633- },634- "node_modules/min-dash": {635- "version": "5.1.0",636- "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.1.0.tgz",637- "integrity": "sha512-HAvN6XzmCQj+js43G9sJRRw8kj/7DvoxN7qhoIN9Xo5ZN1NMljtpyKs/NunXmw0QSWwlgZzNv6tLeEa4xaZ0tg==",638- "license": "MIT"639- },640- "node_modules/min-dom": {641- "version": "5.3.0",642- "resolved": "https://registry.npmjs.org/min-dom/-/min-dom-5.3.0.tgz",643- "integrity": "sha512-0w5FEBgPAyHhmFojW3zxd7we3D+m5XYS3E/06OyvxmbHJoiQVa4Nagj6RWvoAKYRw5xth6cP5TMePc5cR1M9hA==",644- "license": "MIT",645- "dependencies": {646- "domify": "^3.0.0",647- "min-dash": "^5.0.0"648- }649- },650- "node_modules/moddle": {651- "version": "8.2.1",652- "resolved": "https://registry.npmjs.org/moddle/-/moddle-8.2.1.tgz",653- "integrity": "sha512-20qzO/ZpMGO9j9gdQgXk9bfGxb9wA3JKnGth/21YZKODLrZE4RW/uVMIL5pTskYm34Z61DucBEPkq93qByNsBQ==",654- "license": "MIT",655- "dependencies": {656- "min-dash": "^5.1.0"657- }658- },659- "node_modules/moddle-xml": {660- "version": "12.1.0",661- "resolved": "https://registry.npmjs.org/moddle-xml/-/moddle-xml-12.1.0.tgz",662- "integrity": "sha512-+m50j9/MFGhvWbLJoKSSjQj3uv7SYm2w5i2lfv6goWarlWW1Nd0A31NYUXmKuWnEZ8Mm/bH1RZYtuf4x8ptY7g==",663- "license": "MIT",664- "dependencies": {665- "min-dash": "^5.1.0",666- "saxen": "^11.1.0"667- },668- "engines": {669- "node": ">= 18"670- },671- "peerDependencies": {672- "moddle": ">= 6.2.0"673- }674- },675- "node_modules/object-refs": {676- "version": "0.4.0",677- "resolved": "https://registry.npmjs.org/object-refs/-/object-refs-0.4.0.tgz",678- "integrity": "sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==",679- "license": "MIT",680- "engines": {681- "node": "*"682- }683- },684- "node_modules/path-intersection": {685- "version": "4.1.0",686- "resolved": "https://registry.npmjs.org/path-intersection/-/path-intersection-4.1.0.tgz",687- "integrity": "sha512-urUP6WvhnxbHPdHYl6L7Yrc6+1ny6uOFKPCzPxTSUSYGHG0o94RmI7SvMMaScNAM5RtTf08bg4skc6/kjfne3A==",688- "license": "MIT",689- "engines": {690- "node": ">= 14.20"691- }692- },693- "node_modules/preact": {694- "version": "10.29.8",695- "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz",696- "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==",697- "license": "MIT",698- "funding": {699- "type": "opencollective",700- "url": "https://opencollective.com/preact"701- },702- "peerDependencies": {703- "preact-render-to-string": ">=5"704- },705- "peerDependenciesMeta": {706- "preact-render-to-string": {707- "optional": true708- }709- }710- },711- "node_modules/saxen": {712- "version": "11.1.1",713- "resolved": "https://registry.npmjs.org/saxen/-/saxen-11.1.1.tgz",714- "integrity": "sha512-J4BkmJFaM7VgE7pgkFGsNEcqqM3h7+Mz80vfLWFhx7uNOCOXIu6LLjQHYWNejdst3pf/3JUaBIG9+pkk1umlow==",715- "license": "MIT",716- "engines": {717- "node": ">= 20.12"718- }719- },720- "node_modules/tiny-svg": {721- "version": "4.1.4",722- "resolved": "https://registry.npmjs.org/tiny-svg/-/tiny-svg-4.1.4.tgz",723- "integrity": "sha512-cBaEACCbouYrQc9RG+eTXnPYosX1Ijqty/I6DdXovwDd89Pwu4jcmpOR7BuFEF9YCcd7/AWwasE0207WMK7hdw==",724- "license": "MIT",725- "engines": {726- "node": ">= 20"727- }728- }729- }730-}
D renderers/client/bpmn/package.json +0 -17
D renderers/client/bpmn/scripts/build.mjs +0 -22
D renderers/client/bpmn/src/frame.js +0 -63
@@ -1,63 +0,0 @@
1-import BpmnViewer from 'bpmn-js/lib/Viewer'2-3-const CHANNEL = 'diagram.zip:renderer:v1'4-const ENGINE = 'bpmn'5-const VERSION = 'bpmn-js@18.25.1'6-const BUILD = 'bpmn-js-18.25.1-client-unit-1'7-const MAX_SOURCE_LENGTH = 524_2888-9-let queue = Promise.resolve()10-11-async function render(message) {12- if (typeof message.source !== 'string' || message.source.length > MAX_SOURCE_LENGTH) {13- throw new Error('BPMN source must be a string no larger than 512 KiB.')14- }15- const container = document.createElement('div')16- Object.assign(container.style, { width: '1280px', height: '800px' })17- document.body.append(container)18- const viewer = new BpmnViewer({ container })19- try {20- await viewer.importXML(message.source)21- const { svg } = await viewer.saveSVG()22- return svg23- } finally {24- viewer.destroy()25- container.remove()26- }27-}28-29-window.addEventListener('message', event => {30- const message = event.data31- if (32- event.source !== parent33- || message?.channel !== CHANNEL34- || message?.type !== 'render'35- || message?.engine !== ENGINE36- || typeof message?.requestId !== 'string'37- ) return38-39- queue = queue.then(async () => {40- try {41- parent.postMessage({42- channel: CHANNEL,43- type: 'result',44- requestId: message.requestId,45- ok: true,46- svg: await render(message),47- version: VERSION,48- build: BUILD,49- pipeline: [ENGINE],50- }, '*')51- } catch (error) {52- parent.postMessage({53- channel: CHANNEL,54- type: 'result',55- requestId: message.requestId,56- ok: false,57- error: error instanceof Error ? error.message.slice(0, 500) : 'BPMN rendering failed.',58- }, '*')59- }60- })61-})62-63-parent.postMessage({ channel: CHANNEL, type: 'ready', engine: ENGINE, version: VERSION, build: BUILD }, '*')
D renderers/client/bpmn/unit/_headers +0 -12
D renderers/client/bpmn/unit/index.html +0 -14
D renderers/client/bpmn/unit/worker.js +0 -3
D renderers/client/bpmn/wrangler.jsonc +0 -16
A renderers/edge/config/units/bpmn.jsonc +9 -0
A renderers/edge/src/adapters/bpmn.ts +123 -0
@@ -0,0 +1,123 @@
1+import { SaxesParser, type SaxesTagPlain } from 'saxes'2+import { RenderError } from '../runtime/errors'3+import type { EngineId, RendererAdapter, RenderRequest } from '../runtime/types'4+import { edgeResult } from './types'5+6+const VERSION = 'diagramzip-bpmn-svg@1'7+const MAX_SOURCE = 512 * 10248+const MAX_NODES = 20_0009+const MAX_COORD = 32_76810+const MAX_OUTPUT = 4 * 1024 * 102411+12+type Node = { name: string; attrs: Record<string, string>; text: string; children: Node[] }13+type Box = { x: number; y: number; w: number; h: number }14+type Shape = Box & { id: string; type: string; name: string; label?: Box }15+type Edge = { id: string; type: string; points: { x: number; y: number }[]; name: string; label?: Box }16+17+const local = (name: string) => name.includes(':') ? name.slice(name.indexOf(':') + 1) : name18+const esc = (value: string) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&apos;')19+const attr = (node: Node, key: string) => node.attrs[key] ?? node.attrs[Object.keys(node.attrs).find(k => local(k) === key) ?? '']20+const child = (node: Node, name: string) => node.children.find(value => local(value.name) === name)21+22+function fail(message: string, code = 'invalid_source'): never { throw new RenderError(422, code, message) }23+function number(value: string | undefined, name: string): number {24+ if (!value || !/^-?(?:\d+\.?\d*|\.\d+)$/.test(value.trim())) fail(`BPMN ${name} must be a finite number.`)25+ const result = Number(value)26+ if (!Number.isFinite(result) || Math.abs(result) > MAX_COORD) fail(`BPMN ${name} is outside the supported range.`)27+ return result28+}29+function parseXml(source: string): Node {30+ if (new TextEncoder().encode(source).byteLength > MAX_SOURCE) throw new RenderError(413, 'source_too_large', 'BPMN source may not exceed 512 KiB.')31+ if (/<!DOCTYPE|<!ENTITY/i.test(source)) fail('BPMN source may not contain a doctype or entity declaration.')32+ let root: Node | null = null; const stack: Node[] = []; let nodes = 0; let error: unknown33+ const parser = new SaxesParser({ xmlns: false })34+ parser.on('doctype', () => { error = new Error('DOCTYPE is not allowed') })35+ parser.on('processinginstruction', () => { error = new Error('Processing instructions are not allowed') })36+ parser.on('opentag', (tag: SaxesTagPlain) => {37+ if (++nodes > MAX_NODES) { error = new Error('Too many XML nodes'); return }38+ const attrs: Record<string, string> = {}39+ for (const [key, value] of Object.entries(tag.attributes)) attrs[key] = typeof value === 'string' ? value : String((value as { value?: string }).value ?? '')40+ const node: Node = { name: tag.name, attrs, text: '', children: [] }; const parent = stack.at(-1)41+ if (parent) parent.children.push(node); else if (root) error = new Error('Multiple XML roots'); else root = node42+ stack.push(node)43+ })44+ const append = (value: string) => { const node = stack.at(-1); if (node) node.text += value; else if (value.trim()) error = new Error('Text outside root') }45+ parser.on('text', append); parser.on('cdata', append); parser.on('closetag', () => { stack.pop() }); parser.on('error', value => { error = value })46+ try { parser.write(source).close() } catch (value) { error = value }47+ if (error || !root) fail('BPMN source is not valid bounded XML.')48+ return root49+}50+51+function box(node: Node, allowEmpty = false): Box {52+ const x = number(attr(node, 'x'), 'x coordinate'); const y = number(attr(node, 'y'), 'y coordinate')53+ const w = number(attr(node, 'width'), 'width'); const h = number(attr(node, 'height'), 'height')54+ if (allowEmpty ? w < 0 || h < 0 : w <= 0 || h <= 0) fail(`BPMN DI dimensions must be ${allowEmpty ? 'non-negative' : 'positive'}.`)55+ return { x, y, w, h }56+}57+58+export function parseBpmn(source: string): { shapes: Shape[]; edges: Edge[]; width: number; height: number } {59+ const root = parseXml(source); if (local(root.name) !== 'definitions') fail('BPMN source must have a definitions root.')60+ const semantic = new Map<string, { type: string; name: string }>(); const di = new Map<string, Node>(); const labels = new Map<string, Box>()61+ const walk = (node: Node) => {62+ const name = local(node.name); const id = attr(node, 'id')63+ if (id && (name !== 'BPMNShape' && name !== 'BPMNEdge' && name !== 'BPMNLabel')) semantic.set(id, { type: name, name: attr(node, 'name') ?? '' })64+ if (id && (name === 'BPMNShape' || name === 'BPMNEdge')) di.set(id, node)65+ for (const value of node.children) walk(value)66+ }67+ // A second traversal keeps the DI parent id while remaining independent of XML prefixes.68+ const collect = (node: Node, parentDi?: string) => {69+ const name = local(node.name); const id = attr(node, 'id'); const element = attr(node, 'bpmnElement')70+ if (id && (name === 'BPMNShape' || name === 'BPMNEdge')) di.set(id, node)71+ if (name === 'BPMNLabel' && parentDi) { const b = child(node, 'Bounds'); if (b) labels.set(parentDi, box(b, true)) }72+ const next = id && (name === 'BPMNShape' || name === 'BPMNEdge') ? id : parentDi73+ for (const value of node.children) collect(value, next)74+ }75+ walk(root); collect(root)76+ const shapes: Shape[] = []; const edges: Edge[] = []77+ for (const [diId, node] of di) {78+ const element = attr(node, 'bpmnElement'); if (!element) continue79+ const sem = semantic.get(element); if (!sem) continue80+ if (local(node.name) === 'BPMNShape') shapes.push({ ...box(child(node, 'Bounds') ?? fail(`BPMN shape ${diId} has no bounds.`)), id: element, type: sem.type, name: sem.name, label: labels.get(diId) })81+ else {82+ const points = node.children.filter(value => local(value.name) === 'waypoint').map(value => ({ x: number(attr(value, 'x'), 'waypoint x'), y: number(attr(value, 'y'), 'waypoint y') }))83+ if (points.length < 2) fail(`BPMN edge ${diId} needs at least two waypoints.`)84+ edges.push({ id: element, type: sem.type, points, name: sem.name, label: labels.get(diId) })85+ }86+ }87+ if (!shapes.length) fail('BPMN diagram contains no rendered shapes.')88+ const maxX = Math.max(...shapes.map(s => s.x + s.w), ...edges.flatMap(e => e.points.map(p => p.x)), 1)89+ const maxY = Math.max(...shapes.map(s => s.y + s.h), ...edges.flatMap(e => e.points.map(p => p.y)), 1)90+ return { shapes, edges, width: maxX + 20, height: maxY + 20 }91+}92+93+function label(value: string, b: Box, anchor = 'middle'): string {94+ if (!value) return ''95+ const lines = value.split(/\r?\n/).slice(0, 8); const x = anchor === 'start' ? b.x + 4 : b.x + b.w / 296+ return `<text x="${x}" y="${b.y + 14}" text-anchor="${anchor}" font-family="Arial,sans-serif" font-size="12" fill="#111827">${lines.map((line, i) => `<tspan x="${x}" dy="${i ? 14 : 0}">${esc(line.slice(0, 240))}</tspan>`).join('')}</text>`97+}98+function shapeSvg(shape: Shape): string {99+ const { x, y, w, h } = shape; const t = shape.type.toLowerCase(); const stroke = '#334155'; const fill = '#fff'100+ let body: string101+ if (t.includes('event')) body = `<circle cx="${x + w / 2}" cy="${y + h / 2}" r="${Math.min(w, h) / 2 - 2}" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`102+ else if (t.includes('gateway')) body = `<path d="M${x + w / 2} ${y}L${x + w} ${y + h / 2}L${x + w / 2} ${y + h}L${x} ${y + h / 2}Z" fill="${fill}" stroke="${stroke}" stroke-width="2"/>`103+ else if (t === 'participant' || t === 'lane') body = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#f8fafc" stroke="${stroke}"/><path d="M${x + 28} ${y}V${y + h}" stroke="${stroke}"/>`104+ else if (t.includes('dataobject')) body = `<path d="M${x} ${y}H${x + w - 12}L${x + w} ${y + 12}V${y + h}H${x}Z" fill="${fill}" stroke="${stroke}"/><path d="M${x + w - 12} ${y}V${y + 12}H${x + w}" fill="none" stroke="${stroke}"/>`105+ else body = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${t.includes('subprocess') ? 2 : 8}" fill="${fill}" stroke="${stroke}"${t.includes('subprocess') ? ' stroke-dasharray="6 4"' : ''}/>`106+ const b = shape.label ?? { x, y: y + h / 2 - 10, w, h: 20 }107+ return `<g data-bpmn-id="${esc(shape.id)}">${body}${label(shape.name, b, shape.type.toLowerCase() === 'participant' || shape.type.toLowerCase() === 'lane' ? 'start' : 'middle')}</g>`108+}109+export function renderBpmn(source: string): string {110+ const diagram = parseBpmn(source); const defs = '<defs><marker id="bpmn-arrow" markerWidth="10" markerHeight="10" refX="9" refY="4" orient="auto"><path d="M0 0L9 4L0 8Z" fill="#334155"/></marker></defs>'111+ const edges = diagram.edges.map(edge => { const first = edge.points[0]!; return `<g data-bpmn-id="${esc(edge.id)}"><polyline points="${edge.points.map(p => `${p.x},${p.y}`).join(' ')}" fill="none" stroke="#334155" stroke-width="1.5" marker-end="url(#bpmn-arrow)"${edge.type.toLowerCase() === 'association' ? ' stroke-dasharray="5 4"' : ''}/>${label(edge.name, edge.label ?? { x: first.x, y: first.y, w: 80, h: 20 })}</g>` }).join('')112+ const body = `<svg xmlns="http://www.w3.org/2000/svg" width="${diagram.width}" height="${diagram.height}" viewBox="0 0 ${diagram.width} ${diagram.height}">${defs}${edges}${diagram.shapes.map(shapeSvg).join('')}</svg>`113+ if (body.length > MAX_OUTPUT) throw new RenderError(413, 'output_too_large', 'BPMN SVG output exceeds 4 MiB.')114+ return body115+}116+117+export const bpmnAdapter: RendererAdapter = {118+ id: 'bpmn' as EngineId, runtime: 'edge-js', version: VERSION,119+ render(request: RenderRequest, signal: AbortSignal) {120+ if (signal.aborted) throw signal.reason121+ return Promise.resolve(edgeResult('bpmn' as EngineId, VERSION, renderBpmn(request.source)))122+ },123+}
A renderers/edge/src/units/bpmn.ts +14 -0
A renderers/edge/test/bpmn.test.ts +28 -0
@@ -0,0 +1,28 @@
1+import { describe, expect, it } from 'vitest'2+import fixture from './fixtures/bpmn/core.bpmn?raw'3+import productionFixture from '../../../examples/diagrams/example.bpmn?raw'4+import { parseBpmn, renderBpmn } from '../src/adapters/bpmn'5+6+describe('bounded BPMN DI-to-SVG adapter', () => {7+ it('renders core shapes, connections, labels, and dimensions', () => {8+ const parsed = parseBpmn(fixture); expect(parsed.shapes).toHaveLength(5); expect(parsed.edges).toHaveLength(4)9+ const svg = renderBpmn(fixture)10+ expect(svg).toContain('viewBox="0 0 606 121"'); expect(svg).toContain('Review order'); expect(svg).toContain('Approved?')11+ expect(svg).toContain('marker-end="url(#bpmn-arrow)"'); expect(svg).toContain('data-bpmn-id="Gateway"')12+ })13+ it('rejects active XML, malformed DI, and oversized coordinates', () => {14+ expect(() => renderBpmn('<!DOCTYPE definitions [<!ENTITY x "y">]><definitions/>')).toThrow(/doctype|entity/i)15+ expect(() => renderBpmn('<definitions xmlns="x"><process id="p"><task id="t"/></process><bpmndi:BPMNDiagram xmlns:bpmndi="d"><bpmndi:BPMNPlane><bpmndi:BPMNShape bpmnElement="t"/></bpmndi:BPMNPlane></bpmndi:BPMNDiagram></definitions>')).toThrow(/bounds|shapes/i)16+ expect(() => renderBpmn(fixture.replace('x="20"', 'x="999999"'))).toThrow(/range|coordinate/i)17+ expect(() => renderBpmn('x'.repeat(512 * 1024 + 1))).toThrow(/512 KiB/i)18+ })19+ it('escapes labels and does not emit active content', () => {20+ const svg = renderBpmn(fixture.replace('name="Review order"', 'name="&lt;x&gt;&amp;danger&lt;/x&gt;"'))21+ expect(svg).toContain('&lt;x&gt;&amp;danger&lt;/x&gt;'); expect(svg).not.toContain('<x>'); expect(svg).not.toContain('foreignObject')22+ })23+ it('renders the production fixture including empty BPMN label bounds', () => {24+ const svg = renderBpmn(productionFixture)25+ expect(svg).toContain('Examine Situation')26+ expect(svg).toContain('Notification Sent')27+ })28+})
A renderers/edge/test/fixtures/bpmn/core.bpmn +15 -0
@@ -0,0 +1,15 @@
1+<?xml version="1.0" encoding="UTF-8"?>2+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI">3+ <process id="Process_1"><startEvent id="Start" name="Start"/><task id="Task" name="Review order"/><exclusiveGateway id="Gateway" name="Approved?"/><subProcess id="Sub" name="Fulfil"><task id="Inner" name="Pack"/></subProcess><endEvent id="End" name="Done"/><sequenceFlow id="F1" sourceRef="Start" targetRef="Task"/><sequenceFlow id="F2" name="yes" sourceRef="Task" targetRef="Gateway"/><sequenceFlow id="F3" sourceRef="Gateway" targetRef="Sub"/><sequenceFlow id="F4" sourceRef="Sub" targetRef="End"/></process>4+ <bpmndi:BPMNDiagram id="D"><bpmndi:BPMNPlane id="P" bpmnElement="Process_1">5+ <bpmndi:BPMNShape id="S1" bpmnElement="Start"><dc:Bounds x="20" y="50" width="36" height="36"/></bpmndi:BPMNShape>6+ <bpmndi:BPMNShape id="S2" bpmnElement="Task"><dc:Bounds x="100" y="40" width="120" height="56"/></bpmndi:BPMNShape>7+ <bpmndi:BPMNShape id="S3" bpmnElement="Gateway"><dc:Bounds x="270" y="42" width="52" height="52"/><bpmndi:BPMNLabel><dc:Bounds x="250" y="100" width="92" height="20"/></bpmndi:BPMNLabel></bpmndi:BPMNShape>8+ <bpmndi:BPMNShape id="S4" bpmnElement="Sub"><dc:Bounds x="370" y="35" width="130" height="66"/></bpmndi:BPMNShape>9+ <bpmndi:BPMNShape id="S5" bpmnElement="End"><dc:Bounds x="550" y="50" width="36" height="36"/></bpmndi:BPMNShape>10+ <bpmndi:BPMNEdge id="E1" bpmnElement="F1"><di:waypoint x="56" y="68"/><di:waypoint x="100" y="68"/></bpmndi:BPMNEdge>11+ <bpmndi:BPMNEdge id="E2" bpmnElement="F2"><di:waypoint x="220" y="68"/><di:waypoint x="270" y="68"/><bpmndi:BPMNLabel><dc:Bounds x="226" y="78" width="36" height="18"/></bpmndi:BPMNLabel></bpmndi:BPMNEdge>12+ <bpmndi:BPMNEdge id="E3" bpmnElement="F3"><di:waypoint x="322" y="68"/><di:waypoint x="370" y="68"/></bpmndi:BPMNEdge>13+ <bpmndi:BPMNEdge id="E4" bpmnElement="F4"><di:waypoint x="500" y="68"/><di:waypoint x="550" y="68"/></bpmndi:BPMNEdge>14+ </bpmndi:BPMNPlane></bpmndi:BPMNDiagram>15+</definitions>
M renderers/scripts/smoke-units.mjs +6 -1
M shared/svg/index.js +1 -1