@formicoidea/labre-framework-bpmn 0.32.0 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +202 -6
- package/dist/actions.js +427 -43
- package/dist/background.d.ts +2 -0
- package/dist/background.js +158 -0
- package/dist/commands-manifest.d.ts +18 -0
- package/dist/commands-manifest.js +226 -0
- package/dist/commands.js +496 -5
- package/dist/consts.d.ts +195 -4
- package/dist/consts.js +230 -4
- package/dist/element-renderer.d.ts +10 -4
- package/dist/element-renderer.js +14 -55
- package/dist/element-view.d.ts +119 -8
- package/dist/element-view.js +274 -30
- package/dist/export.d.ts +277 -0
- package/dist/export.js +1802 -0
- package/dist/facts.d.ts +48 -0
- package/dist/facts.js +127 -0
- package/dist/import.d.ts +69 -0
- package/dist/import.js +1476 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +47 -0
- package/dist/interchange.d.ts +109 -0
- package/dist/interchange.js +191 -0
- package/dist/morph.d.ts +61 -0
- package/dist/morph.js +118 -0
- package/dist/node/node-renderer.d.ts +0 -9
- package/dist/node/node-renderer.js +294 -17
- package/dist/pool-hit.d.ts +98 -0
- package/dist/pool-hit.js +130 -0
- package/dist/presets.d.ts +168 -0
- package/dist/presets.js +327 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +189 -0
- package/dist/roles.d.ts +96 -0
- package/dist/roles.js +410 -0
- package/dist/rules.d.ts +199 -0
- package/dist/rules.js +1539 -0
- package/dist/templates/index.js +116 -9
- package/dist/toolbar/bpmn-senior-button.js +8 -2
- package/dist/toolbar/config.d.ts +28 -2
- package/dist/toolbar/config.js +93 -4
- package/dist/toolbar/icons.d.ts +67 -0
- package/dist/toolbar/icons.js +141 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/translations.d.ts +3 -1
- package/dist/translations.js +38 -3
- package/dist/view.d.ts +6 -2
- package/dist/view.js +68 -5
- package/package.json +6 -2
package/dist/import.js
ADDED
|
@@ -0,0 +1,1476 @@
|
|
|
1
|
+
import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
3
|
+
import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
|
|
4
|
+
import { BPMN_FORMAT_ID, BPMN_NS, BPMN_OWN_DECLARATIONS, BPMN_SCOPE, BPMN_XML_OF_KIND, escapeAttr, escapeText, } from './export.js';
|
|
5
|
+
import { bpmnNodeProps } from './presets.js';
|
|
6
|
+
import { BPMN_ROLE } from './roles.js';
|
|
7
|
+
/**
|
|
8
|
+
* The remarks whose wording is FIXED, as `[key, English]` pairs.
|
|
9
|
+
*
|
|
10
|
+
* A reader is a pure function of text (`docs/adr/0012`, P3) and has no `std`,
|
|
11
|
+
* so it cannot ask the host's catalogue for anything: it declares the key on
|
|
12
|
+
* the note and `reportInterchangeImport` resolves it when it draws the report
|
|
13
|
+
* (`InterchangeNote.messageKey`). The English string stays here and stays the
|
|
14
|
+
* fallback, so the console table and a playground with no catalogue read
|
|
15
|
+
* exactly what they read before.
|
|
16
|
+
*
|
|
17
|
+
* These three and no others, and the line is not arbitrary: every remaining
|
|
18
|
+
* remark NAMES something out of the file — `<boundaryEvent>`, an id, a count of
|
|
19
|
+
* lanes — and the seam has neither interpolation nor pluralisation. A key for
|
|
20
|
+
* one of those would be a sentence with holes in it that a translator cannot
|
|
21
|
+
* see the shape of, which is the same refusal the interchange count labels
|
|
22
|
+
* already make (`interchange-import.ts`).
|
|
23
|
+
*
|
|
24
|
+
* They are contributed to the manifest by `./translations.ts`, with the
|
|
25
|
+
* framework, because they ship in the BPMN bundle rather than in core.
|
|
26
|
+
*/
|
|
27
|
+
export const BPMN_IMPORT_REMARKS = {
|
|
28
|
+
inventedPool: [
|
|
29
|
+
'com.labre.bpmn.import.remark.invented-pool',
|
|
30
|
+
"This file names no participant, so its process was drawn in a pool of Labre's own. The pool is not the file's: exporting writes the process back without one.",
|
|
31
|
+
],
|
|
32
|
+
laneGap: [
|
|
33
|
+
'com.labre.bpmn.import.remark.lane-gap',
|
|
34
|
+
'The lanes of this pool are drawn with a gap or an overlap between them. Labre lays its bands end to end, so their heights were kept in proportion and the space between them was closed.',
|
|
35
|
+
],
|
|
36
|
+
mustUnderstand: [
|
|
37
|
+
'com.labre.bpmn.import.remark.must-understand',
|
|
38
|
+
'The file declares an extension that it says MUST be understood to read the model correctly. Labre does not understand it: the import went ahead, and this reading of the process may be wrong.',
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A BPMN 2.0 interchange document, read as a board — the inverse of `export.ts`
|
|
43
|
+
* on the vocabulary Labre draws, and an honest accounting of everything else
|
|
44
|
+
* (`docs/adr/0012`, D1–D6).
|
|
45
|
+
*
|
|
46
|
+
* ## Pure by construction, like its mirror
|
|
47
|
+
*
|
|
48
|
+
* A string in, element PROPS out. No `BlockStdScope`, no surface, no store, no
|
|
49
|
+
* clock, no randomness — this function has no surface to add anything to, and
|
|
50
|
+
* giving it one would cost exactly the property that lets one implementation
|
|
51
|
+
* serve an editor command and a labre-mcp tool (ADR 0012, P3). The caller does
|
|
52
|
+
* the writing.
|
|
53
|
+
*
|
|
54
|
+
* ## What the caller owes, and it is one thing
|
|
55
|
+
*
|
|
56
|
+
* `surface.addElement` mints its own nanoid and ignores any id it is handed —
|
|
57
|
+
* surface identity is Labre's and never the file's (D3) — so a connector's
|
|
58
|
+
* `source` / `target` below name the **source file's** ids rather than surface
|
|
59
|
+
* ones. The caller creates the elements, folds the array into a map from each
|
|
60
|
+
* element's `interchange.bpmn.id` to the id the surface minted for it, and
|
|
61
|
+
* rewrites the two endpoints. Every element carries that id, so the map is a
|
|
62
|
+
* fold over the very array this returned; nothing else is needed to finish it.
|
|
63
|
+
*
|
|
64
|
+
* ## Three states, and no fourth
|
|
65
|
+
*
|
|
66
|
+
* Every node of the file is **mapped** (there is a Labre artefact: drawn,
|
|
67
|
+
* editable, re-emitted from the drawing), **carried** (no artefact: kept
|
|
68
|
+
* verbatim in `interchange.bpmn` on the nearest mapped element, invisible on
|
|
69
|
+
* the canvas) or **quarantined** (kept, and deliberately never written back,
|
|
70
|
+
* because re-emitting it would produce a file that contradicts the drawing).
|
|
71
|
+
* Nothing is dropped in silence, and the report says which happened to what.
|
|
72
|
+
*
|
|
73
|
+
* What Labre judges is what Labre can DRAW: a carried element is on no canvas,
|
|
74
|
+
* so no validation rule sees it and no audit counts it. That is correct, and it
|
|
75
|
+
* is the sentence the report has to be read with.
|
|
76
|
+
*
|
|
77
|
+
* ## Reading XML by hand, and why
|
|
78
|
+
*
|
|
79
|
+
* The tree is walked child by child rather than through
|
|
80
|
+
* `getElementsByTagNameNS`, which happy-dom does not implement for a parsed XML
|
|
81
|
+
* document: it answers an empty list for every namespaced query, so a unit
|
|
82
|
+
* suite built on it would pass by asserting nothing. The same environment
|
|
83
|
+
* decodes neither numeric character references nor `'` in an attribute
|
|
84
|
+
* value, which is why the escaped-label round trip is pinned in chromium
|
|
85
|
+
* (`integration-test/src/__tests__/edgeless/bpmn.spec.ts`) and not in the unit
|
|
86
|
+
* spec. Both traps are `export.unit.spec.ts`'s findings, inherited rather than
|
|
87
|
+
* rediscovered.
|
|
88
|
+
*
|
|
89
|
+
* ## Failure is an exception, not a report
|
|
90
|
+
*
|
|
91
|
+
* A source that is not a readable BPMN document — malformed XML, a root that is
|
|
92
|
+
* not `definitions`, or a `definitions` whose only root is a choreography or a
|
|
93
|
+
* conversation (D1's one refusal) — makes this function THROW. The report's
|
|
94
|
+
* five note kinds are a closed list and not one of them can say "this is not a
|
|
95
|
+
* file I can read"; a result of three zeroes and no elements would claim an
|
|
96
|
+
* empty process where there was none. The command layer catches it and tells
|
|
97
|
+
* the user which of the three it was.
|
|
98
|
+
*
|
|
99
|
+
* ## The loss table
|
|
100
|
+
*
|
|
101
|
+
* Every semantic capability owes one (ADR 0012), and this is `.bpmn`'s. What is
|
|
102
|
+
* INVISIBLE is not what is LOST, and the distinction is the deliverable: a
|
|
103
|
+
* carried fragment is in the document and out of the picture, and only the
|
|
104
|
+
* bottom rows are gone for good.
|
|
105
|
+
*
|
|
106
|
+
* | what | state | after a round trip |
|
|
107
|
+
* | ----------------------------------------------------------- | ----------- | --------------------------------------------------- |
|
|
108
|
+
* | the 17 kinds, pools, flat lanes, the 3 edge roles, the DI | mapped | drawn, and written back from the drawing |
|
|
109
|
+
* | element ids (participants, flow nodes, flows, lanes) | mapped | given back verbatim — the fixed point (D3) |
|
|
110
|
+
* | `documentation`, `ioSpecification`, `conditionExpression`, … | carried | invisible on the canvas, written back in its XSD slot |
|
|
111
|
+
* | `process/@isExecutable="true"` | carried | written back; the `false` this writer mints is overridden |
|
|
112
|
+
* | a flow onto a carried node (a boundary event's error path) | carried | never drawn loose, and re-emitted beside the node it runs to |
|
|
113
|
+
* | a `BPMNShape` drawing an element the file does not declare | carried | kept under the id it names, and put back on the plane |
|
|
114
|
+
* | loop / multi-instance / compensation markers | carried | a plain task on the canvas, still marked in the file |
|
|
115
|
+
* | Analytic elements (boundary, inclusive, event-based, …) | carried | not drawn; re-emitted in the flow-element slot it came out of |
|
|
116
|
+
* | `camunda:` / `zeebe:` / `signavio:` extensions | carried | re-emitted verbatim, declarations included |
|
|
117
|
+
* | the file's own prefix for a namespace (`bpmn2:`, `semantic:`) | carried | re-declared on `definitions`, so the fragments under it parse |
|
|
118
|
+
* | a colour set in bpmn.io (`bioc:`, `color:`) | quarantined | imports grey; the colour is kept and not written back |
|
|
119
|
+
* | the body of an expanded sub-process | quarantined | drawn collapsed; the body and its DI survive in the document |
|
|
120
|
+
* | lane nesting (`childLaneSet`) | quarantined | flat lanes with joined names; the nesting survives |
|
|
121
|
+
* | `definitions`-level `<import>` | quarantined | single-file import only |
|
|
122
|
+
* | an edge's explicit `di:waypoint` routing | **lost** | re-routed from the two ends, and it says so |
|
|
123
|
+
* | a CARRIED shape's position, once the drawing has moved | **lost** | the fragment is verbatim, so it keeps the file's own coordinates while the rest is translated to the plane origin. The export warns; nothing is lost, something is displaced |
|
|
124
|
+
* | an `xmlns:` binding one of Labre's own four prefixes elsewhere | **lost on export** | kept in the document; not written back, because it would rebind the prefix every `dc:Bounds` in the file is under. The fragments carried under it ARE written, and are then read under Labre's binding — reinterpreted rather than lost, which is the worse of the two, so the export warns about both halves |
|
|
125
|
+
* | an `xmlns:` declared on anything but `definitions` | **lost** | the reader carries declarations off `definitions` only, so a fragment relying on one scoped to its own ancestor comes back unparseable. bpmn.io and Camunda both hoist to `definitions`, so this is theoretical on a real file |
|
|
126
|
+
* | two pools disagreeing about one `definitions` attribute | **lost on export** | one value can be written and the last wins; matter carried from the other file is read under it. Both are in the document and the export warns |
|
|
127
|
+
* | a carried element whose id another carried element already claimed | **lost on export** | the first is written and the rest are not — a BPMN id is unique across a document. They stay in the document; the export names them where the two disagree, and writes an exact duplicate once in silence |
|
|
128
|
+
* | the file's `definitions/@id`, `@targetNamespace`, `@exporter` | **lost** | Labre writes its own |
|
|
129
|
+
* | the file's `process/@id`, where a participant names one | **lost** | re-minted from the participant's id, which IS kept |
|
|
130
|
+
* | `laneSet/@id`, `collaboration/@id` and `@name`, `BPMNDiagram/@id`, `BPMNPlane/@id`, every `BPMNShape/@id` and `BPMNEdge/@id`, the folded `dataObject/@id` | **lost** | re-derived from the id its element settled on, which is what makes the fixed point a fixed point (D3) |
|
|
131
|
+
* | a carried fragment's SLOT inside its parent | **lost, and re-derived** | the scope records the parent, not the slot; the writer places it from the XSD sequence, which is legal but need not be where the file had it |
|
|
132
|
+
* | a gap or an overlap between two lane bands | **lost** | lanes are weights: Labre lays its bands end to end |
|
|
133
|
+
* | the plane offset (§12.3) | **lost** | shape exact, origin at (0, 0) — the export's doing |
|
|
134
|
+
* | surface identity across a re-import | **lost** | a new board beside the old one, never a merge |
|
|
135
|
+
*
|
|
136
|
+
* The row that used to be owed is owed no longer: **the carried payload IS
|
|
137
|
+
* re-emitted on export**, into the element its scope names and the slot the
|
|
138
|
+
* XSD puts it in, with the namespace declarations it needs. The property that
|
|
139
|
+
* makes the claim checkable is the mirror of D3's: read a foreign file, write
|
|
140
|
+
* it, read it again, and the carried payloads are identical — a fixed point on
|
|
141
|
+
* matter this library does not understand. `import.unit.spec.ts` pins it.
|
|
142
|
+
*
|
|
143
|
+
* Quarantined material is the deliberate exception and always was: it is kept
|
|
144
|
+
* in the document and never written back, so a second read of an exported file
|
|
145
|
+
* holds none of it. That is not a gap in the round trip, it is D5.
|
|
146
|
+
*/
|
|
147
|
+
/* ── The inverse of the export's tables ───────────────────────────────── */
|
|
148
|
+
/** The key {@link BPMN_KIND_OF_XML} is read by: an element and its trigger. */
|
|
149
|
+
function xmlKindKey(element, eventDefinition) {
|
|
150
|
+
return `${element}#${eventDefinition ?? ''}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `startEvent` + `messageEventDefinition` → `startEventMessage`, and the
|
|
154
|
+
* sixteen other answers.
|
|
155
|
+
*
|
|
156
|
+
* DERIVED from {@link BPMN_XML_OF_KIND} rather than typed out a second time,
|
|
157
|
+
* which is the only arrangement in which the two directions cannot drift: a
|
|
158
|
+
* kind added to the pack gains its reading the moment it gains its writing, and
|
|
159
|
+
* an element name corrected in the table is corrected in both. The inverse of
|
|
160
|
+
* that table is not a function in general — `startEvent` alone is four kinds —
|
|
161
|
+
* so the key is the PAIR (element name, event definition), which is exactly
|
|
162
|
+
* what the table tells them apart by.
|
|
163
|
+
*
|
|
164
|
+
* That the pair is injective — seventeen kinds, seventeen distinct keys — is a
|
|
165
|
+
* property of the table rather than of this code, so the spec asserts it
|
|
166
|
+
* instead of this line assuming it.
|
|
167
|
+
*/
|
|
168
|
+
export const BPMN_KIND_OF_XML = new Map(Object.entries(BPMN_XML_OF_KIND).map(([kind, mapping]) => [
|
|
169
|
+
xmlKindKey(mapping.element, mapping.eventDefinition),
|
|
170
|
+
kind,
|
|
171
|
+
]));
|
|
172
|
+
/**
|
|
173
|
+
* `.bpmn`'s scope vocabulary — where a carried fragment came off (D2, as
|
|
174
|
+
* amended in #157), and where the writer puts it back.
|
|
175
|
+
*
|
|
176
|
+
* Declared in `export.ts` and re-exported here, for the reason
|
|
177
|
+
* {@link BPMN_FORMAT_ID} is: the reader files a fragment under a scope and the
|
|
178
|
+
* writer looks it up under one, and a table written twice is a table that
|
|
179
|
+
* drifts. See its doc comment there for what a scope means.
|
|
180
|
+
*/
|
|
181
|
+
export { BPMN_SCOPE };
|
|
182
|
+
/**
|
|
183
|
+
* The three edge elements, and the role each one IS.
|
|
184
|
+
*
|
|
185
|
+
* A role is a statement (`docs/adr/0010`), and an imported edge makes the same
|
|
186
|
+
* statement a drawn one does: the file said `messageFlow`, so the arrow says
|
|
187
|
+
* "sends a message to". Nothing here invents a role for an untyped edge —
|
|
188
|
+
* there is no such thing in BPMN, where every connecting object is one of
|
|
189
|
+
* these three.
|
|
190
|
+
*/
|
|
191
|
+
const EDGE_ROLE_OF_ELEMENT = {
|
|
192
|
+
sequenceFlow: BPMN_ROLE.sequenceFlow,
|
|
193
|
+
messageFlow: BPMN_ROLE.messageFlow,
|
|
194
|
+
association: BPMN_ROLE.association,
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* The colour extensions D5 quarantines (case 1), by namespace and by prefix.
|
|
198
|
+
*
|
|
199
|
+
* bpmn.io's own `bioc:` and the OMG's non-normative colour extension. They
|
|
200
|
+
* collide with `strokeColor` / `fillColor`, which Labre owns and the user can
|
|
201
|
+
* change from the shape toolbar — so writing a stale `bioc:fill` back beside a
|
|
202
|
+
* recoloured shape would produce a file that disagrees with itself. Adopting
|
|
203
|
+
* them as real colours would lift the case out of quarantine in both directions
|
|
204
|
+
* at once, and is a chantier of its own (ADR 0012, non-goals).
|
|
205
|
+
*/
|
|
206
|
+
const COLOUR_NS = new Set([
|
|
207
|
+
'http://bpmn.io/schema/bpmn/biocolor/1.0',
|
|
208
|
+
'http://www.omg.org/spec/BPMN/non-normative/color/1.0',
|
|
209
|
+
]);
|
|
210
|
+
const COLOUR_PREFIX = new Set(['bioc', 'color']);
|
|
211
|
+
/** The four reasons a fragment is kept and not written back (D5). Closed. */
|
|
212
|
+
export const BPMN_QUARANTINE_REASON = {
|
|
213
|
+
colour: 'A vendor colour extension. Labre owns the stroke and the fill of the shape ' +
|
|
214
|
+
'it was on, so writing it back beside a recoloured artefact would produce a ' +
|
|
215
|
+
'file that disagrees with itself.',
|
|
216
|
+
expanded: 'The body of an expanded sub-process. Labre draws the collapsed form, and a ' +
|
|
217
|
+
'body written under a shape flagged collapsed is a model and a diagram that ' +
|
|
218
|
+
'contradict each other. It stays in the document.',
|
|
219
|
+
nestedLanes: 'A nested lane set. A Labre pool is one flat list of bands, so the leaves ' +
|
|
220
|
+
'were imported carrying their whole path as a name; writing the nesting back ' +
|
|
221
|
+
'alongside them would describe the pool twice.',
|
|
222
|
+
imported: 'A `definitions`-level <import>. Labre reads one file (§15.3.1 asks for a ' +
|
|
223
|
+
'self-contained set), so writing this back would claim a resolution of ' +
|
|
224
|
+
'another document that never happened.',
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* The children of an activity that are NOT its body.
|
|
228
|
+
*
|
|
229
|
+
* They describe the activity itself — its documentation, its extensions, its
|
|
230
|
+
* loop marker, its data plumbing — so they are carried and written back like
|
|
231
|
+
* any other unmodelled child. Everything else inside an activity the diagram
|
|
232
|
+
* flags `isExpanded="true"` is the flow drawn INSIDE it, which is the thing
|
|
233
|
+
* D5 case 2 is about.
|
|
234
|
+
*/
|
|
235
|
+
const NOT_A_BODY = new Set([
|
|
236
|
+
'documentation',
|
|
237
|
+
'extensionElements',
|
|
238
|
+
'incoming',
|
|
239
|
+
'outgoing',
|
|
240
|
+
'ioSpecification',
|
|
241
|
+
'property',
|
|
242
|
+
'dataInputAssociation',
|
|
243
|
+
'dataOutputAssociation',
|
|
244
|
+
'multiInstanceLoopCharacteristics',
|
|
245
|
+
'standardLoopCharacteristics',
|
|
246
|
+
]);
|
|
247
|
+
/* ── Reading the DOM by hand ──────────────────────────────────────────── */
|
|
248
|
+
/** The `Node.nodeType`s a fragment can be made of, spelled rather than numbered. */
|
|
249
|
+
const ELEMENT_NODE = 1;
|
|
250
|
+
const TEXT_NODE = 3;
|
|
251
|
+
const CDATA_NODE = 4;
|
|
252
|
+
const PI_NODE = 7;
|
|
253
|
+
const COMMENT_NODE = 8;
|
|
254
|
+
/** The element children of `node`, in document order. */
|
|
255
|
+
function childrenOf(node) {
|
|
256
|
+
return Array.from(node.children);
|
|
257
|
+
}
|
|
258
|
+
/** Every element under `node`, at any depth, in document order. */
|
|
259
|
+
function descendantsOf(node) {
|
|
260
|
+
return childrenOf(node).flatMap(child => [child, ...descendantsOf(child)]);
|
|
261
|
+
}
|
|
262
|
+
/** Is this element in the BPMN MODEL namespace, whatever prefix it wears? */
|
|
263
|
+
function isModel(element) {
|
|
264
|
+
return element.namespaceURI === BPMN_NS.model;
|
|
265
|
+
}
|
|
266
|
+
/** MODEL-namespaced children with this local name, in document order. */
|
|
267
|
+
function modelChildren(parent, local) {
|
|
268
|
+
return childrenOf(parent).filter(child => isModel(child) && child.localName === local);
|
|
269
|
+
}
|
|
270
|
+
/** The first MODEL child with this local name, if any. */
|
|
271
|
+
function modelChild(parent, local) {
|
|
272
|
+
return modelChildren(parent, local)[0];
|
|
273
|
+
}
|
|
274
|
+
/** An attribute, or `undefined` — never `null`, which reads as a value. */
|
|
275
|
+
function attrOf(element, name) {
|
|
276
|
+
const value = element.getAttribute(name);
|
|
277
|
+
return value === null ? undefined : value;
|
|
278
|
+
}
|
|
279
|
+
/** The prefix of a qualified name, `''` when it has none. */
|
|
280
|
+
function prefixOf(qualified) {
|
|
281
|
+
const colon = qualified.indexOf(':');
|
|
282
|
+
return colon < 0 ? '' : qualified.slice(0, colon);
|
|
283
|
+
}
|
|
284
|
+
/** Is this attribute one of the colour extensions D5 quarantines? */
|
|
285
|
+
function isColourAttr(attr) {
|
|
286
|
+
return ((attr.namespaceURI !== null && COLOUR_NS.has(attr.namespaceURI)) ||
|
|
287
|
+
COLOUR_PREFIX.has(prefixOf(attr.name)));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* One element, back as XML text — the verbatim form D1 promises for anything
|
|
291
|
+
* carried or quarantined.
|
|
292
|
+
*
|
|
293
|
+
* Serialized here rather than through `outerHTML`, for two halves of one
|
|
294
|
+
* reason: `outerHTML` is an HTML serializer in several DOM implementations
|
|
295
|
+
* (void elements, lower-cased names, attribute newlines left raw), and what is
|
|
296
|
+
* stored here is a document VALUE that a later export has to be able to write
|
|
297
|
+
* back character for character. Prefixes are kept exactly as the file spelled
|
|
298
|
+
* them, because a `camunda:properties` fragment only means anything under the
|
|
299
|
+
* declaration its `definitions` carried — which is why those declarations are
|
|
300
|
+
* carried too (see the residue, below).
|
|
301
|
+
*
|
|
302
|
+
* Escaping is `export.ts`'s own, so a fragment stored by this reader and a name
|
|
303
|
+
* written by that writer treat a newline in an attribute value the same way
|
|
304
|
+
* (XML 1.0 §3.3.3 — only a character reference survives normalization).
|
|
305
|
+
*/
|
|
306
|
+
function fragmentOf(element) {
|
|
307
|
+
const attrs = Array.from(element.attributes)
|
|
308
|
+
.map(attr => ` ${attr.name}="${escapeAttr(attr.value)}"`)
|
|
309
|
+
.join('');
|
|
310
|
+
const parts = [];
|
|
311
|
+
for (const child of Array.from(element.childNodes)) {
|
|
312
|
+
if (child.nodeType === ELEMENT_NODE) {
|
|
313
|
+
parts.push(fragmentOf(child));
|
|
314
|
+
}
|
|
315
|
+
else if (child.nodeType === TEXT_NODE || child.nodeType === CDATA_NODE) {
|
|
316
|
+
parts.push(escapeText(child.nodeValue ?? ''));
|
|
317
|
+
}
|
|
318
|
+
else if (child.nodeType === COMMENT_NODE) {
|
|
319
|
+
// A comment inside a vendor extension is documentation somebody wrote by
|
|
320
|
+
// hand, and dropping it while promising the fragment back "character for
|
|
321
|
+
// character" would make that promise false in the one place a human
|
|
322
|
+
// would notice.
|
|
323
|
+
parts.push(`<!--${child.nodeValue ?? ''}-->`);
|
|
324
|
+
}
|
|
325
|
+
else if (child.nodeType === PI_NODE) {
|
|
326
|
+
const instruction = child;
|
|
327
|
+
parts.push(`<?${instruction.target} ${instruction.data}?>`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const name = element.nodeName;
|
|
331
|
+
if (parts.length === 0)
|
|
332
|
+
return `<${name}${attrs} />`;
|
|
333
|
+
return `<${name}${attrs}>${parts.join('')}</${name}>`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The document, or an exception naming what is wrong with it.
|
|
337
|
+
*
|
|
338
|
+
* `DOMParser` reports a malformed document as a `parsererror` element rather
|
|
339
|
+
* than by throwing, which is a well-formedness answer nobody asked for in the
|
|
340
|
+
* shape of a document — so it is turned into the exception the caller can
|
|
341
|
+
* actually act on.
|
|
342
|
+
*/
|
|
343
|
+
function parseDefinitions(source) {
|
|
344
|
+
const doc = new DOMParser().parseFromString(source, 'application/xml');
|
|
345
|
+
const error = doc.querySelector('parsererror');
|
|
346
|
+
if (error) {
|
|
347
|
+
throw new Error(`This file is not well-formed XML, so no BPMN can be read out of it: ` +
|
|
348
|
+
`${(error.textContent ?? '').trim().slice(0, 200)}`);
|
|
349
|
+
}
|
|
350
|
+
const root = doc.documentElement;
|
|
351
|
+
if (!root || root.localName !== 'definitions') {
|
|
352
|
+
throw new Error(`A BPMN file opens on <definitions>; this one opens on <${root?.localName ?? 'nothing'}>.`);
|
|
353
|
+
}
|
|
354
|
+
// The NAMESPACE, not the element name — `<definitions>` is also the root of a
|
|
355
|
+
// DMN decision model, and of anything else built on the same OMG scaffolding.
|
|
356
|
+
// Without this a `.dmn` imports as an empty board, which is exactly the
|
|
357
|
+
// "three zeroes claiming an empty process" this reader refuses to return.
|
|
358
|
+
if (root.namespaceURI !== BPMN_NS.model) {
|
|
359
|
+
throw new Error(`This <definitions> is in "${root.namespaceURI ?? 'no namespace'}", not ` +
|
|
360
|
+
`in BPMN 2.0's ("${BPMN_NS.model}"). A DMN decision model and a BPMN ` +
|
|
361
|
+
`process open on the same element name and are not the same file.`);
|
|
362
|
+
}
|
|
363
|
+
return root;
|
|
364
|
+
}
|
|
365
|
+
/** A `dc:Bounds` child, as four numbers — `null` when it has none, or junk. */
|
|
366
|
+
function boundsOf(shape) {
|
|
367
|
+
const box = childrenOf(shape).find(child => child.namespaceURI === BPMN_NS.dc && child.localName === 'Bounds');
|
|
368
|
+
if (!box)
|
|
369
|
+
return null;
|
|
370
|
+
const numbers = ['x', 'y', 'width', 'height'].map(name => Number(attrOf(box, name) ?? Number.NaN));
|
|
371
|
+
if (numbers.some(value => !Number.isFinite(value)))
|
|
372
|
+
return null;
|
|
373
|
+
return { x: numbers[0], y: numbers[1], w: numbers[2], h: numbers[3] };
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Every `BPMNShape` and `BPMNEdge` of the document, keyed by what it draws.
|
|
377
|
+
*
|
|
378
|
+
* All planes of all diagrams, flattened, first occurrence winning:
|
|
379
|
+
* `bpmnElement` is an id and an id is document-unique, so two shapes for one
|
|
380
|
+
* element is a contradiction in the source rather than a case to model. A file
|
|
381
|
+
* carrying a second diagram for an expanded sub-process therefore contributes
|
|
382
|
+
* its shapes here, and the sub-process body is quarantined all the same — the
|
|
383
|
+
* DI follows whatever it describes.
|
|
384
|
+
*
|
|
385
|
+
* The INDEX is what makes the round trip a fixed point: the exporter writes
|
|
386
|
+
* shapes in document order, so reading them back in plane order is what lets
|
|
387
|
+
* the export after an import land on the bytes of the export before it.
|
|
388
|
+
*/
|
|
389
|
+
function diagramIndex(definitions) {
|
|
390
|
+
const shapes = new Map();
|
|
391
|
+
const edges = new Map();
|
|
392
|
+
let index = 0;
|
|
393
|
+
for (const diagram of childrenOf(definitions)) {
|
|
394
|
+
if (diagram.namespaceURI !== BPMN_NS.bpmndi ||
|
|
395
|
+
diagram.localName !== 'BPMNDiagram') {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
for (const plane of childrenOf(diagram)) {
|
|
399
|
+
if (plane.namespaceURI !== BPMN_NS.bpmndi ||
|
|
400
|
+
plane.localName !== 'BPMNPlane') {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
for (const child of childrenOf(plane)) {
|
|
404
|
+
if (child.namespaceURI !== BPMN_NS.bpmndi)
|
|
405
|
+
continue;
|
|
406
|
+
const target = attrOf(child, 'bpmnElement');
|
|
407
|
+
if (target === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
if (child.localName === 'BPMNShape' && !shapes.has(target)) {
|
|
410
|
+
shapes.set(target, {
|
|
411
|
+
bounds: boundsOf(child),
|
|
412
|
+
index: index++,
|
|
413
|
+
element: child,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
else if (child.localName === 'BPMNEdge' && !edges.has(target)) {
|
|
417
|
+
edges.set(target, {
|
|
418
|
+
waypoints: childrenOf(child).filter(point => point.namespaceURI === BPMN_NS.di &&
|
|
419
|
+
point.localName === 'waypoint').length,
|
|
420
|
+
index: index++,
|
|
421
|
+
element: child,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { shapes, edges };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The attributes the reader UNDERSTANDS, per element — and therefore the ones
|
|
431
|
+
* it does not carry, because they are already in the drawing.
|
|
432
|
+
*
|
|
433
|
+
* The default row is the honest one: an `id` and a `name` are the model
|
|
434
|
+
* everywhere in this format, and everything else on an element Labre draws is
|
|
435
|
+
* something Labre does not model and must not lose.
|
|
436
|
+
*/
|
|
437
|
+
const READ_ATTRS = {
|
|
438
|
+
definitions: ['id', 'name', 'targetNamespace', 'exporter', 'exporterVersion'],
|
|
439
|
+
participant: ['id', 'name', 'processRef'],
|
|
440
|
+
process: ['id', 'name', 'isExecutable'],
|
|
441
|
+
lane: ['id', 'name'],
|
|
442
|
+
laneSet: ['id', 'name'],
|
|
443
|
+
textAnnotation: ['id', 'textFormat'],
|
|
444
|
+
group: ['id', 'categoryValueRef'],
|
|
445
|
+
dataObjectReference: ['id', 'name', 'dataObjectRef'],
|
|
446
|
+
sequenceFlow: ['id', 'name', 'sourceRef', 'targetRef'],
|
|
447
|
+
messageFlow: ['id', 'name', 'sourceRef', 'targetRef'],
|
|
448
|
+
// `associationDirection` is read only when it says what the exporter would
|
|
449
|
+
// write anyway; see the edge reader for the other values.
|
|
450
|
+
association: ['id', 'name', 'sourceRef', 'targetRef', 'associationDirection'],
|
|
451
|
+
'': ['id', 'name'],
|
|
452
|
+
};
|
|
453
|
+
/** What a `BPMNShape` says that the drawing already carries. */
|
|
454
|
+
const READ_SHAPE_ATTRS = [
|
|
455
|
+
'id',
|
|
456
|
+
'bpmnElement',
|
|
457
|
+
'isExpanded',
|
|
458
|
+
'isMarkerVisible',
|
|
459
|
+
'isHorizontal',
|
|
460
|
+
];
|
|
461
|
+
/** How far off to the side an undrawn artefact is swept, and on what grid. */
|
|
462
|
+
const SWEEP_GAP = 160;
|
|
463
|
+
const SWEEP_STEP = 200;
|
|
464
|
+
const SWEEP_COLUMNS = 4;
|
|
465
|
+
/** The margin between a minted pool's plot and the work inside it. */
|
|
466
|
+
const MINTED_POOL_PADDING = 40;
|
|
467
|
+
/** The connector a role is drawn as — the styles the tools arm, one table. */
|
|
468
|
+
function connectorProps(role) {
|
|
469
|
+
const base = { type: 'connector', role, mode: ConnectorMode.Orthogonal };
|
|
470
|
+
if (role === BPMN_ROLE.messageFlow) {
|
|
471
|
+
return {
|
|
472
|
+
...base,
|
|
473
|
+
stroke: MESSAGE_STROKE,
|
|
474
|
+
strokeWidth: MESSAGE_WIDTH,
|
|
475
|
+
strokeStyle: StrokeStyle.Dash,
|
|
476
|
+
frontEndpointStyle: PointStyle.Circle,
|
|
477
|
+
rearEndpointStyle: PointStyle.Arrow,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (role === BPMN_ROLE.association) {
|
|
481
|
+
return {
|
|
482
|
+
...base,
|
|
483
|
+
stroke: ASSOCIATION_STROKE,
|
|
484
|
+
strokeWidth: ASSOCIATION_WIDTH,
|
|
485
|
+
strokeStyle: StrokeStyle.Dash,
|
|
486
|
+
// No head at either end: an association claims no direction, and an
|
|
487
|
+
// arrowhead would be the picture claiming one (`docs/adr/0010`).
|
|
488
|
+
frontEndpointStyle: PointStyle.None,
|
|
489
|
+
rearEndpointStyle: PointStyle.None,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
...base,
|
|
494
|
+
stroke: SEQUENCE_STROKE,
|
|
495
|
+
strokeWidth: SEQUENCE_WIDTH,
|
|
496
|
+
strokeStyle: StrokeStyle.Solid,
|
|
497
|
+
frontEndpointStyle: PointStyle.None,
|
|
498
|
+
rearEndpointStyle: PointStyle.Triangle,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The version of the format this file declares (ADR 0012, P2 as amended).
|
|
503
|
+
*
|
|
504
|
+
* Always `2.0` and never the namespace URI itself: `parseDefinitions` has
|
|
505
|
+
* already refused anything that is not in BPMN 2.0's MODEL namespace, so there
|
|
506
|
+
* is no case in which a foreign URI could leak out of here into a UI that would
|
|
507
|
+
* render it as a version.
|
|
508
|
+
*/
|
|
509
|
+
function sourceVersionOf(definitions) {
|
|
510
|
+
const version = '2.0';
|
|
511
|
+
const exporter = attrOf(definitions, 'exporter');
|
|
512
|
+
if (exporter === undefined)
|
|
513
|
+
return version;
|
|
514
|
+
const exporterVersion = attrOf(definitions, 'exporterVersion');
|
|
515
|
+
return `${version} (${[exporter, exporterVersion].filter(Boolean).join(' ')})`;
|
|
516
|
+
}
|
|
517
|
+
/** `[x,y,w,h]` back off a draft that already has one. */
|
|
518
|
+
function boundOf(draft) {
|
|
519
|
+
return Bound.deserialize(String(draft.props.xywh));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Where the file placed nothing (D4).
|
|
523
|
+
*
|
|
524
|
+
* A shape with no `dc:Bounds` is still imported — it is in the model, and a
|
|
525
|
+
* model element the reader can draw is not a thing to drop — but its position
|
|
526
|
+
* is Labre's and the report says so. Swept onto a grid to the RIGHT of
|
|
527
|
+
* everything the file did place, in document order, so the same file always
|
|
528
|
+
* lands the same board and nothing an author drew is covered by something they
|
|
529
|
+
* did not.
|
|
530
|
+
*/
|
|
531
|
+
function layOutTheUndrawn(drafts, minted) {
|
|
532
|
+
const placed = drafts.filter(draft => draft.needsLayout === undefined &&
|
|
533
|
+
draft !== minted &&
|
|
534
|
+
draft.kind !== 'edge');
|
|
535
|
+
const boxes = placed.map(boundOf);
|
|
536
|
+
const maxX = boxes.length > 0 ? Math.max(...boxes.map(box => box.x + box.w)) : 0;
|
|
537
|
+
const minY = boxes.length > 0 ? Math.min(...boxes.map(box => box.y)) : 0;
|
|
538
|
+
let seat = 0;
|
|
539
|
+
for (const draft of drafts) {
|
|
540
|
+
if (!draft.needsLayout)
|
|
541
|
+
continue;
|
|
542
|
+
const column = seat % SWEEP_COLUMNS;
|
|
543
|
+
const row = Math.floor(seat / SWEEP_COLUMNS);
|
|
544
|
+
seat++;
|
|
545
|
+
draft.props.xywh = new Bound(maxX + SWEEP_GAP + column * SWEEP_STEP, minY + row * SWEEP_STEP, draft.needsLayout.w, draft.needsLayout.h).serialize();
|
|
546
|
+
}
|
|
547
|
+
// The pool minted for a file that had no participant (D6) is sized LAST, to
|
|
548
|
+
// hold everything: a pool's plot is what decides which artefacts are in it,
|
|
549
|
+
// and an artefact drawn outside every plot would be exported back into a
|
|
550
|
+
// process of its own.
|
|
551
|
+
if (!minted)
|
|
552
|
+
return;
|
|
553
|
+
const inside = drafts
|
|
554
|
+
.filter(draft => draft !== minted && draft.kind === 'node')
|
|
555
|
+
.map(boundOf);
|
|
556
|
+
if (inside.length === 0)
|
|
557
|
+
return;
|
|
558
|
+
const left = Math.min(...inside.map(box => box.x)) - MINTED_POOL_PADDING;
|
|
559
|
+
const top = Math.min(...inside.map(box => box.y)) - MINTED_POOL_PADDING;
|
|
560
|
+
const right = Math.max(...inside.map(box => box.x + box.w)) + MINTED_POOL_PADDING;
|
|
561
|
+
const bottom = Math.max(...inside.map(box => box.y + box.h)) + MINTED_POOL_PADDING;
|
|
562
|
+
minted.props.xywh = new Bound(
|
|
563
|
+
// The name band is drawn INSIDE the frame and is not part of the plot, so
|
|
564
|
+
// the frame starts a band's width further left than the work does.
|
|
565
|
+
left - POOL_BAND_WIDTH, top, right - left + POOL_BAND_WIDTH, bottom - top).serialize();
|
|
566
|
+
}
|
|
567
|
+
/* ── The reader ───────────────────────────────────────────────────────── */
|
|
568
|
+
/**
|
|
569
|
+
* Read a BPMN 2.0 interchange document as element props plus a report.
|
|
570
|
+
*
|
|
571
|
+
* See the module comment for the contract, and `docs/adr/0012` D1–D6 for why it
|
|
572
|
+
* is this contract and not a shorter one.
|
|
573
|
+
*/
|
|
574
|
+
export function importBpmnXml(source, context = {}) {
|
|
575
|
+
// The caller's name has no landing place in an array of elements: what a
|
|
576
|
+
// board is CALLED is the document's, not any element's. A caller that wants
|
|
577
|
+
// to name the doc after the file reads `collaboration/@name` itself.
|
|
578
|
+
void context;
|
|
579
|
+
const definitions = parseDefinitions(source);
|
|
580
|
+
const { shapes, edges: diEdges } = diagramIndex(definitions);
|
|
581
|
+
const notes = [];
|
|
582
|
+
const note = (entry) => notes.push(entry);
|
|
583
|
+
let carried = 0;
|
|
584
|
+
let quarantined = 0;
|
|
585
|
+
let explicitRoutes = 0;
|
|
586
|
+
const drafts = [];
|
|
587
|
+
const seenSourceIds = new Set();
|
|
588
|
+
/** Source ids that became an artefact a flow may attach to: pools and nodes. */
|
|
589
|
+
const mappedSourceIds = new Set();
|
|
590
|
+
/** Source ids kept verbatim on some element instead: never a connector end. */
|
|
591
|
+
const carriedSourceIds = new Set();
|
|
592
|
+
/** Every source id whose diagram element this reader consumed or kept. */
|
|
593
|
+
const drawnSourceIds = new Set();
|
|
594
|
+
/**
|
|
595
|
+
* Source ids whose diagram element is QUARANTINED, and must therefore not be
|
|
596
|
+
* picked up by the orphan sweep at the end.
|
|
597
|
+
*
|
|
598
|
+
* Quarantine means kept and deliberately not written back (D5), so a shape
|
|
599
|
+
* that escaped into the carried column would be re-emitted by the writer and
|
|
600
|
+
* the quarantine would mean nothing. It is a third answer to "was this
|
|
601
|
+
* diagram element accounted for", beside drawn and carried, and it is exactly
|
|
602
|
+
* as good an answer as either.
|
|
603
|
+
*/
|
|
604
|
+
const quarantinedSourceIds = new Set();
|
|
605
|
+
/* ── Roots ─────────────────────────────────────────────────────────── */
|
|
606
|
+
const roots = childrenOf(definitions).filter(isModel);
|
|
607
|
+
const collaborations = roots.filter(root => root.localName === 'collaboration');
|
|
608
|
+
const processes = roots.filter(root => root.localName === 'process');
|
|
609
|
+
// D1's one refusal, and it is at the document level: half a choreography is
|
|
610
|
+
// not a smaller choreography, and a conversation is a different picture of a
|
|
611
|
+
// different thing. Declined by name, with no partial import.
|
|
612
|
+
if (processes.length === 0 && collaborations.length === 0) {
|
|
613
|
+
const declined = roots.find(root => ['choreography', 'globalChoreographyTask', 'conversation'].includes(root.localName));
|
|
614
|
+
if (declined) {
|
|
615
|
+
throw new Error(`This file is a BPMN ${declined.localName}, which Labre does not draw. ` +
|
|
616
|
+
`Only a process or a collaboration can be imported.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/** `categoryValue` id → the label a `group` pointing at it wears. */
|
|
620
|
+
const categoryValues = new Map();
|
|
621
|
+
for (const root of roots) {
|
|
622
|
+
if (root.localName !== 'category')
|
|
623
|
+
continue;
|
|
624
|
+
for (const value of modelChildren(root, 'categoryValue')) {
|
|
625
|
+
const id = attrOf(value, 'id');
|
|
626
|
+
if (id !== undefined)
|
|
627
|
+
categoryValues.set(id, attrOf(value, 'value') ?? '');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/* ── The four ways something is kept ───────────────────────────────── */
|
|
631
|
+
/** Records a source id, and names the SECOND element to claim it (D3). */
|
|
632
|
+
const claim = (sourceId, element) => {
|
|
633
|
+
if (sourceId === undefined)
|
|
634
|
+
return;
|
|
635
|
+
if (seenSourceIds.has(sourceId)) {
|
|
636
|
+
note({
|
|
637
|
+
kind: 'substituted-id',
|
|
638
|
+
sourceId,
|
|
639
|
+
element,
|
|
640
|
+
message: `Two elements in this file share the id "${sourceId}", which BPMN ` +
|
|
641
|
+
`requires to be unique across a document. Both were imported; the ` +
|
|
642
|
+
`second will be written back under an id Labre mints.`,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
seenSourceIds.add(sourceId);
|
|
646
|
+
};
|
|
647
|
+
/** One attribute, kept under the scope of the element that carried it. */
|
|
648
|
+
const carryAttr = (payload, scope, name, value) => {
|
|
649
|
+
payload.attrs = {
|
|
650
|
+
...payload.attrs,
|
|
651
|
+
[scope]: { ...payload.attrs?.[scope], [name]: value },
|
|
652
|
+
};
|
|
653
|
+
carried++;
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* One fragment, kept under the scope of the element it was a CHILD of.
|
|
657
|
+
*
|
|
658
|
+
* `announce: false` for a fragment whose own note is written at the call site
|
|
659
|
+
* — a flow onto a carried node is carried for a REASON, and two notes about
|
|
660
|
+
* one flow, one of them generic, is a worse report than one that is precise.
|
|
661
|
+
*/
|
|
662
|
+
const carryChild = (payload, scope, child, sourceId, announce = true) => {
|
|
663
|
+
payload.children = {
|
|
664
|
+
...payload.children,
|
|
665
|
+
[scope]: [...(payload.children?.[scope] ?? []), fragmentOf(child)],
|
|
666
|
+
};
|
|
667
|
+
carried++;
|
|
668
|
+
if (!announce)
|
|
669
|
+
return;
|
|
670
|
+
note({
|
|
671
|
+
kind: 'carried',
|
|
672
|
+
element: child.nodeName,
|
|
673
|
+
sourceId,
|
|
674
|
+
message: `<${child.nodeName}> has no Labre artefact, so it is kept verbatim on ` +
|
|
675
|
+
`the nearest element that has one. It is not drawn, and no validation ` +
|
|
676
|
+
`rule sees it.`,
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
/** One diagram fragment, kept under the scope of what it DRAWS. */
|
|
680
|
+
const carryDi = (payload, scope, fragment) => {
|
|
681
|
+
payload.di = {
|
|
682
|
+
...payload.di,
|
|
683
|
+
[scope]: [...(payload.di?.[scope] ?? []), fragment],
|
|
684
|
+
};
|
|
685
|
+
carried++;
|
|
686
|
+
};
|
|
687
|
+
const quarantine = (payload, fragment, reason, entry) => {
|
|
688
|
+
payload.quarantined = [
|
|
689
|
+
...(payload.quarantined ?? []),
|
|
690
|
+
{ fragment, reason },
|
|
691
|
+
];
|
|
692
|
+
quarantined++;
|
|
693
|
+
note({ kind: 'quarantined', ...entry, message: reason });
|
|
694
|
+
};
|
|
695
|
+
/** Every attribute the reader does not model — colours quarantined (D5). */
|
|
696
|
+
const sortAttributes = (element, payload, scope, sourceId, understood = READ_ATTRS[element.localName] ??
|
|
697
|
+
READ_ATTRS['']) => {
|
|
698
|
+
for (const attr of Array.from(element.attributes)) {
|
|
699
|
+
if (attr.name === 'xmlns' || attr.name.startsWith('xmlns:'))
|
|
700
|
+
continue;
|
|
701
|
+
if (understood.includes(attr.name))
|
|
702
|
+
continue;
|
|
703
|
+
if (isColourAttr(attr)) {
|
|
704
|
+
quarantine(payload, `${attr.name}="${escapeAttr(attr.value)}"`, BPMN_QUARANTINE_REASON.colour, { sourceId, element: attr.name });
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
carryAttr(payload, scope, attr.name, attr.value);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
/** The DI element's own extras: colours quarantined, the rest kept as `di`. */
|
|
711
|
+
const sortShapeExtras = (shape, payload, sourceId) => {
|
|
712
|
+
if (!shape)
|
|
713
|
+
return;
|
|
714
|
+
sortAttributes(shape, payload, BPMN_SCOPE.shape, sourceId, READ_SHAPE_ATTRS);
|
|
715
|
+
for (const child of childrenOf(shape)) {
|
|
716
|
+
// The bounds and the waypoints ARE the drawing, and the drawing is what
|
|
717
|
+
// was mapped. A `BPMNLabel` and anything else is kept as diagram matter.
|
|
718
|
+
if (child.namespaceURI === BPMN_NS.dc && child.localName === 'Bounds') {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (child.namespaceURI === BPMN_NS.di && child.localName === 'waypoint') {
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
carryDi(payload, BPMN_SCOPE.shape, fragmentOf(child));
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
/* ── Pools, from participants ──────────────────────────────────────── */
|
|
728
|
+
const processById = new Map();
|
|
729
|
+
for (const process of processes) {
|
|
730
|
+
const id = attrOf(process, 'id');
|
|
731
|
+
if (id !== undefined)
|
|
732
|
+
processById.set(id, process);
|
|
733
|
+
}
|
|
734
|
+
/** The process a participant named → the pool that draws the pair. */
|
|
735
|
+
const poolOfProcess = new Map();
|
|
736
|
+
const participants = collaborations.flatMap(collaboration => modelChildren(collaboration, 'participant'));
|
|
737
|
+
for (const participant of participants) {
|
|
738
|
+
const sourceId = attrOf(participant, 'id');
|
|
739
|
+
claim(sourceId, 'participant');
|
|
740
|
+
if (sourceId !== undefined) {
|
|
741
|
+
// A pool is an end a message flow may legally attach to (§10.6).
|
|
742
|
+
mappedSourceIds.add(sourceId);
|
|
743
|
+
drawnSourceIds.add(sourceId);
|
|
744
|
+
}
|
|
745
|
+
const shape = sourceId === undefined ? undefined : shapes.get(sourceId);
|
|
746
|
+
const payload = {};
|
|
747
|
+
// The PARTICIPANT's id, because the participant is what the pool draws and
|
|
748
|
+
// what a `BPMNShape` points at (D3). The process behind it is re-minted
|
|
749
|
+
// from this one on export — that is the one id of the pair the round trip
|
|
750
|
+
// does not keep, and it is in the loss table.
|
|
751
|
+
if (sourceId !== undefined)
|
|
752
|
+
payload.id = sourceId;
|
|
753
|
+
const ref = attrOf(participant, 'processRef');
|
|
754
|
+
const process = ref === undefined ? undefined : processById.get(ref);
|
|
755
|
+
sortAttributes(participant, payload, BPMN_SCOPE.self, sourceId);
|
|
756
|
+
sortShapeExtras(shape?.element, payload, sourceId);
|
|
757
|
+
// Labre draws ONE thing where the format writes two, so the process's own
|
|
758
|
+
// foreign matter rides on the pool that stands for it — under its own
|
|
759
|
+
// scope, because it is a different source element with its own attributes.
|
|
760
|
+
if (process) {
|
|
761
|
+
sortAttributes(process, payload, BPMN_SCOPE.process, attrOf(process, 'id'));
|
|
762
|
+
// A model downgrade if it were dropped: the writer emits
|
|
763
|
+
// `isExecutable="false"` for every process it writes, so a file that says
|
|
764
|
+
// `true` is saying something Labre does not model and must not lose.
|
|
765
|
+
const executable = attrOf(process, 'isExecutable');
|
|
766
|
+
if (executable !== undefined && executable !== 'false') {
|
|
767
|
+
carryAttr(payload, BPMN_SCOPE.process, 'isExecutable', executable);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const bounds = shape?.bounds ?? null;
|
|
771
|
+
const draft = {
|
|
772
|
+
props: {
|
|
773
|
+
type: 'bpmnPool',
|
|
774
|
+
// The FRAME the flow objects are drawn in, and a role of its own: a
|
|
775
|
+
// rule written on the artefacts must never fall on the pool.
|
|
776
|
+
role: BPMN_ROLE.pool,
|
|
777
|
+
// `''` and not `undefined`: the model's own default is "Pool", and a
|
|
778
|
+
// participant the file left unnamed must not acquire a name here.
|
|
779
|
+
name: attrOf(participant, 'name') ?? '',
|
|
780
|
+
xywh: (bounds
|
|
781
|
+
? new Bound(bounds.x, bounds.y, bounds.w, bounds.h)
|
|
782
|
+
: new Bound(0, 0, POOL_REF_WIDTH, POOL_REF_HEIGHT)).serialize(),
|
|
783
|
+
},
|
|
784
|
+
payload,
|
|
785
|
+
order: shape?.index ?? Number.POSITIVE_INFINITY,
|
|
786
|
+
kind: 'pool',
|
|
787
|
+
...(bounds
|
|
788
|
+
? {}
|
|
789
|
+
: { needsLayout: { w: POOL_REF_WIDTH, h: POOL_REF_HEIGHT } }),
|
|
790
|
+
};
|
|
791
|
+
drafts.push(draft);
|
|
792
|
+
if (process)
|
|
793
|
+
poolOfProcess.set(process, draft);
|
|
794
|
+
if (!bounds) {
|
|
795
|
+
note({
|
|
796
|
+
kind: 'invented-layout',
|
|
797
|
+
sourceId,
|
|
798
|
+
element: 'participant',
|
|
799
|
+
message: `The participant "${draft.props.name || 'unnamed'}" arrived with no ` +
|
|
800
|
+
`diagram, so Labre placed its pool beside the drawing.`,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* A pool for a file that named no participant (D6).
|
|
806
|
+
*
|
|
807
|
+
* A bare `process` is exactly what a poolless Labre board exports as, and it
|
|
808
|
+
* is what a good half of the single-participant files in the wild are. It
|
|
809
|
+
* gets a pool minted for it — the framework's background element, and the
|
|
810
|
+
* only thing there is for the document's residue to ride on — and that pool
|
|
811
|
+
* SAYS it stands for a process, which is what tells the exporter to give the
|
|
812
|
+
* poolless form back rather than invent a collaboration nobody drew.
|
|
813
|
+
*/
|
|
814
|
+
const bareProcess = participants.length === 0 ? processes[0] : undefined;
|
|
815
|
+
let mintedPool;
|
|
816
|
+
if (bareProcess) {
|
|
817
|
+
const sourceId = attrOf(bareProcess, 'id');
|
|
818
|
+
claim(sourceId, 'process');
|
|
819
|
+
if (sourceId !== undefined) {
|
|
820
|
+
mappedSourceIds.add(sourceId);
|
|
821
|
+
drawnSourceIds.add(sourceId);
|
|
822
|
+
}
|
|
823
|
+
const payload = { element: 'process' };
|
|
824
|
+
if (sourceId !== undefined)
|
|
825
|
+
payload.id = sourceId;
|
|
826
|
+
// `@self` and not `@process`: this pool IS the process, which is exactly
|
|
827
|
+
// what `element: 'process'` says.
|
|
828
|
+
sortAttributes(bareProcess, payload, BPMN_SCOPE.self, sourceId);
|
|
829
|
+
const bareExecutable = attrOf(bareProcess, 'isExecutable');
|
|
830
|
+
if (bareExecutable !== undefined && bareExecutable !== 'false') {
|
|
831
|
+
carryAttr(payload, BPMN_SCOPE.self, 'isExecutable', bareExecutable);
|
|
832
|
+
}
|
|
833
|
+
mintedPool = {
|
|
834
|
+
props: {
|
|
835
|
+
type: 'bpmnPool',
|
|
836
|
+
role: BPMN_ROLE.pool,
|
|
837
|
+
name: attrOf(bareProcess, 'name') ?? '',
|
|
838
|
+
xywh: new Bound(0, 0, POOL_REF_WIDTH, POOL_REF_HEIGHT).serialize(),
|
|
839
|
+
},
|
|
840
|
+
payload,
|
|
841
|
+
// Behind everything: it is a frame the file never drew.
|
|
842
|
+
order: -1,
|
|
843
|
+
kind: 'pool',
|
|
844
|
+
};
|
|
845
|
+
drafts.push(mintedPool);
|
|
846
|
+
poolOfProcess.set(bareProcess, mintedPool);
|
|
847
|
+
note({
|
|
848
|
+
kind: 'invented-layout',
|
|
849
|
+
sourceId,
|
|
850
|
+
element: 'process',
|
|
851
|
+
messageKey: BPMN_IMPORT_REMARKS.inventedPool[0],
|
|
852
|
+
message: BPMN_IMPORT_REMARKS.inventedPool[1],
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Where the document's own residue rides (D6): the first pool there is.
|
|
857
|
+
*
|
|
858
|
+
* A stated asymmetry rather than an oversight — delete that pool and the
|
|
859
|
+
* file's document-scope residue goes with it. Accepted: it is one value that
|
|
860
|
+
* copy-pastes, undoes and syncs with something the user can see, and an
|
|
861
|
+
* architect who has deleted the only pool of an imported process has deleted
|
|
862
|
+
* the process.
|
|
863
|
+
*/
|
|
864
|
+
const residence = () => drafts.find(draft => draft.kind === 'pool');
|
|
865
|
+
/* ── Lanes ─────────────────────────────────────────────────────────── */
|
|
866
|
+
const laneBands = new Map();
|
|
867
|
+
for (const [process, pool] of poolOfProcess) {
|
|
868
|
+
const laneSet = modelChild(process, 'laneSet');
|
|
869
|
+
if (!laneSet)
|
|
870
|
+
continue;
|
|
871
|
+
sortAttributes(laneSet, pool.payload, BPMN_SCOPE.laneSet, attrOf(laneSet, 'id'));
|
|
872
|
+
const bands = [];
|
|
873
|
+
/**
|
|
874
|
+
* Walks a lane set, flattening a nested one onto its leaves (D5 case 3).
|
|
875
|
+
*
|
|
876
|
+
* `pool.lanes` is ONE flat list of bands over one plot: there is no gesture
|
|
877
|
+
* that puts a lane inside a lane, and inventing a containment model is a
|
|
878
|
+
* bigger decision than an importer gets to take. So the leaves are what
|
|
879
|
+
* land, named by their whole path ("Sales / Back office") so nothing about
|
|
880
|
+
* the original is unreadable, and the `childLaneSet` is quarantined —
|
|
881
|
+
* written back beside the flat set, it would describe the pool twice.
|
|
882
|
+
*/
|
|
883
|
+
const walkLanes = (set, path) => {
|
|
884
|
+
for (const lane of modelChildren(set, 'lane')) {
|
|
885
|
+
const sourceId = attrOf(lane, 'id');
|
|
886
|
+
claim(sourceId, 'lane');
|
|
887
|
+
const name = attrOf(lane, 'name') ?? '';
|
|
888
|
+
const nested = modelChild(lane, 'childLaneSet');
|
|
889
|
+
if (nested) {
|
|
890
|
+
quarantine(pool.payload, fragmentOf(nested), BPMN_QUARANTINE_REASON.nestedLanes, { sourceId, element: 'childLaneSet' });
|
|
891
|
+
// A lane holding a child set is not a band Labre paints — its LEAVES
|
|
892
|
+
// are — so the shape drawing it describes a subdivision the flat pool
|
|
893
|
+
// does not have. Accounted for by the quarantine, and kept out of the
|
|
894
|
+
// orphan sweep, or the writer would put a stray band back.
|
|
895
|
+
if (sourceId !== undefined)
|
|
896
|
+
quarantinedSourceIds.add(sourceId);
|
|
897
|
+
walkLanes(nested, [...path, name]);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const rect = sourceId === undefined
|
|
901
|
+
? null
|
|
902
|
+
: (shapes.get(sourceId)?.bounds ?? null);
|
|
903
|
+
// The file's id, verbatim: a lane has no interchange payload of its
|
|
904
|
+
// own, and this prop IS where its identity is kept (D3). The exporter
|
|
905
|
+
// writes it back unprefixed for exactly that reason. It is also this
|
|
906
|
+
// lane's SCOPE, so two lanes carrying one foreign attribute keep two
|
|
907
|
+
// values.
|
|
908
|
+
const laneId = sourceId ?? `lane-${bands.length + 1}`;
|
|
909
|
+
// A lane is drawn — its band is the pool's own subdivision — so its
|
|
910
|
+
// shape is consumed rather than orphaned, but it is not something a
|
|
911
|
+
// flow may attach to.
|
|
912
|
+
if (sourceId !== undefined)
|
|
913
|
+
drawnSourceIds.add(sourceId);
|
|
914
|
+
bands.push({
|
|
915
|
+
lane: {
|
|
916
|
+
id: laneId,
|
|
917
|
+
name: [...path, name].filter(Boolean).join(' / '),
|
|
918
|
+
// A relative WEIGHT, and the band's drawn height is the truest one
|
|
919
|
+
// there is: the plot is shared in proportion, so two bands that
|
|
920
|
+
// were 120 and 240 units tall come back as a third and two thirds,
|
|
921
|
+
// whatever the pool is resized to afterwards. Filled in below,
|
|
922
|
+
// because a set in which only SOME bands were drawn cannot mix the
|
|
923
|
+
// two kinds of number.
|
|
924
|
+
size: rect && rect.h > 0 ? rect.h : 1,
|
|
925
|
+
},
|
|
926
|
+
rect,
|
|
927
|
+
refs: modelChildren(lane, 'flowNodeRef')
|
|
928
|
+
.map(ref => (ref.textContent ?? '').trim())
|
|
929
|
+
.filter(Boolean),
|
|
930
|
+
});
|
|
931
|
+
// Everything else about the lane rides on the pool, which is the
|
|
932
|
+
// nearest thing that HAS a payload — under the lane's own scope.
|
|
933
|
+
sortAttributes(lane, pool.payload, laneId, sourceId);
|
|
934
|
+
for (const child of childrenOf(lane)) {
|
|
935
|
+
if (isModel(child) && child.localName === 'flowNodeRef')
|
|
936
|
+
continue;
|
|
937
|
+
carryChild(pool.payload, laneId, child, sourceId);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
walkLanes(laneSet, []);
|
|
942
|
+
if (bands.length === 0)
|
|
943
|
+
continue;
|
|
944
|
+
// Top to bottom, which is the order a pool paints its bands in. Sorted by
|
|
945
|
+
// the DRAWING when the drawing says (D4: the file's diagram wins at
|
|
946
|
+
// import), and left in document order when it does not.
|
|
947
|
+
const allDrawn = bands.every(band => band.rect !== null);
|
|
948
|
+
if (allDrawn) {
|
|
949
|
+
bands.sort((a, b) => a.rect.y - b.rect.y);
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
// A drawn height and the fallback `1` are not the same KIND of number: a
|
|
953
|
+
// band of 200 beside a band of 1 paints a hairline nobody drew. So a set
|
|
954
|
+
// that is not wholly drawn is split equally, and — like every other
|
|
955
|
+
// position this reader invents (D4) — it says so.
|
|
956
|
+
for (const band of bands)
|
|
957
|
+
band.lane.size = 1;
|
|
958
|
+
note({
|
|
959
|
+
kind: 'invented-layout',
|
|
960
|
+
sourceId: attrOf(laneSet, 'id'),
|
|
961
|
+
element: 'laneSet',
|
|
962
|
+
message: `${bands.length === 1 ? 'This lane' : `Some of these ${bands.length} lanes`} ` +
|
|
963
|
+
`arrived with no diagram, so Labre split the pool into equal bands. ` +
|
|
964
|
+
`The proportions are Labre's and not the file's.`,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
// Bands that do not tile the pool are still only WEIGHTS here — Labre lays
|
|
968
|
+
// them end to end — so a file that drew a gap or an overlap between two
|
|
969
|
+
// lanes comes back with the gap closed. That changes the picture, so it is
|
|
970
|
+
// said once rather than discovered.
|
|
971
|
+
if (allDrawn && bands.length > 1) {
|
|
972
|
+
const gap = bands.slice(1).some((band, index) => {
|
|
973
|
+
const above = bands[index].rect;
|
|
974
|
+
return Math.abs(band.rect.y - (above.y + above.h)) > 0.5;
|
|
975
|
+
});
|
|
976
|
+
if (gap) {
|
|
977
|
+
note({
|
|
978
|
+
kind: 'invented-layout',
|
|
979
|
+
sourceId: attrOf(laneSet, 'id'),
|
|
980
|
+
element: 'laneSet',
|
|
981
|
+
messageKey: BPMN_IMPORT_REMARKS.laneGap[0],
|
|
982
|
+
message: BPMN_IMPORT_REMARKS.laneGap[1],
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
pool.props.lanes = bands.map(band => band.lane);
|
|
987
|
+
laneBands.set(pool, bands);
|
|
988
|
+
}
|
|
989
|
+
/* ── Flow nodes, data references, artifacts ────────────────────────── */
|
|
990
|
+
/** Source id → the box it was drawn in, for the lane membership check. */
|
|
991
|
+
const nodeBounds = new Map();
|
|
992
|
+
/** The `dataObject`s a `dataObjectReference` folds in (§10.4.1). */
|
|
993
|
+
const foldedDataObjects = new Set();
|
|
994
|
+
for (const process of processes) {
|
|
995
|
+
for (const reference of modelChildren(process, 'dataObjectReference')) {
|
|
996
|
+
const ref = attrOf(reference, 'dataObjectRef');
|
|
997
|
+
if (ref !== undefined)
|
|
998
|
+
foldedDataObjects.add(ref);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* One semantic element of a scope: mapped, or carried on `host` under
|
|
1003
|
+
* `hostScope` — which is the element it was a child of, because that is where
|
|
1004
|
+
* an exporter has to put it back.
|
|
1005
|
+
*/
|
|
1006
|
+
const readNode = (element, host, hostScope) => {
|
|
1007
|
+
const sourceId = attrOf(element, 'id');
|
|
1008
|
+
const local = element.localName;
|
|
1009
|
+
// The `dataObject` behind a reference is folded INTO the reference — DI
|
|
1010
|
+
// attaches to the reference, and the exporter writes the object back out
|
|
1011
|
+
// of the drawing — so it is neither mapped nor carried.
|
|
1012
|
+
if (local === 'dataObject' &&
|
|
1013
|
+
sourceId !== undefined &&
|
|
1014
|
+
foldedDataObjects.has(sourceId)) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
// What TRIGGERS an event, in either of the two forms §10.5.2 allows: the
|
|
1018
|
+
// definition written inside the event, or a reference to one declared at
|
|
1019
|
+
// root scope (Table 10.82). Both are read, because both say the same thing
|
|
1020
|
+
// — and an event whose trigger is named by reference must never come back
|
|
1021
|
+
// as the None event the spec says an event with no definition is.
|
|
1022
|
+
const trigger = childrenOf(element).find(child => isModel(child) && child.localName.endsWith('EventDefinition'));
|
|
1023
|
+
const triggerRef = modelChild(element, 'eventDefinitionRef');
|
|
1024
|
+
const referenced = (() => {
|
|
1025
|
+
const ref = triggerRef?.textContent?.trim();
|
|
1026
|
+
if (!ref)
|
|
1027
|
+
return undefined;
|
|
1028
|
+
// A QName, resolved by id within this one file — which is what every tool
|
|
1029
|
+
// does with the unprefixed form this format writes everywhere else.
|
|
1030
|
+
return roots.find(root => attrOf(root, 'id') === ref.split(':').pop() &&
|
|
1031
|
+
root.localName.endsWith('EventDefinition'));
|
|
1032
|
+
})();
|
|
1033
|
+
const kind = BPMN_KIND_OF_XML.get(xmlKindKey(local, (trigger ?? referenced)?.localName));
|
|
1034
|
+
// A trigger we could not read is not a trigger we may drop: the event goes
|
|
1035
|
+
// whole into the carried branch below rather than onto the canvas claiming
|
|
1036
|
+
// something the file did not say.
|
|
1037
|
+
if (kind === undefined && triggerRef !== undefined) {
|
|
1038
|
+
note({
|
|
1039
|
+
kind: 'warning',
|
|
1040
|
+
sourceId,
|
|
1041
|
+
element: local,
|
|
1042
|
+
message: `<${local}> names its trigger by reference to ` +
|
|
1043
|
+
`"${triggerRef.textContent?.trim() ?? ''}", which Labre does not ` +
|
|
1044
|
+
`draw. The event was kept whole rather than drawn as a plain one.`,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
if (kind === undefined) {
|
|
1048
|
+
// CARRIED, and standing on its own: an Analytic or executable flow node —
|
|
1049
|
+
// a boundary event, an inclusive gateway, a transaction — that Labre has
|
|
1050
|
+
// no artefact for. It rides on the pool of the process it was written
|
|
1051
|
+
// in, which is the nearest mapped element there is, and its DI rides with
|
|
1052
|
+
// it so that whatever writes it back can draw it where it was.
|
|
1053
|
+
if (!host)
|
|
1054
|
+
return;
|
|
1055
|
+
carryChild(host.payload, hostScope, element, sourceId);
|
|
1056
|
+
if (sourceId !== undefined)
|
|
1057
|
+
carriedSourceIds.add(sourceId);
|
|
1058
|
+
const shape = sourceId === undefined ? undefined : shapes.get(sourceId);
|
|
1059
|
+
if (shape) {
|
|
1060
|
+
// Keyed by what it DRAWS, which is the carried element itself — the
|
|
1061
|
+
// only way a writer can pair the two back up.
|
|
1062
|
+
carryDi(host.payload, sourceId ?? hostScope, fragmentOf(shape.element));
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
claim(sourceId, local);
|
|
1067
|
+
if (sourceId !== undefined) {
|
|
1068
|
+
mappedSourceIds.add(sourceId);
|
|
1069
|
+
drawnSourceIds.add(sourceId);
|
|
1070
|
+
}
|
|
1071
|
+
const payload = {};
|
|
1072
|
+
if (sourceId !== undefined)
|
|
1073
|
+
payload.id = sourceId;
|
|
1074
|
+
const shape = sourceId === undefined ? undefined : shapes.get(sourceId);
|
|
1075
|
+
const bounds = shape?.bounds ?? null;
|
|
1076
|
+
sortAttributes(element, payload, BPMN_SCOPE.self, sourceId);
|
|
1077
|
+
sortShapeExtras(shape?.element, payload, sourceId);
|
|
1078
|
+
// The label, from wherever this kind keeps it: an annotation's is a child
|
|
1079
|
+
// element, a group's is the value of the category it points at, everything
|
|
1080
|
+
// else's is its own `name`.
|
|
1081
|
+
let text = attrOf(element, 'name') ?? '';
|
|
1082
|
+
if (local === 'textAnnotation') {
|
|
1083
|
+
text = modelChild(element, 'text')?.textContent?.trim() ?? '';
|
|
1084
|
+
}
|
|
1085
|
+
else if (local === 'group') {
|
|
1086
|
+
const ref = attrOf(element, 'categoryValueRef');
|
|
1087
|
+
text = (ref !== undefined ? categoryValues.get(ref) : undefined) ?? '';
|
|
1088
|
+
}
|
|
1089
|
+
// D5 case 2: an activity the DIAGRAM says is expanded holds a flow drawn
|
|
1090
|
+
// inside it, and the pack draws the collapsed form only.
|
|
1091
|
+
const expanded = shape !== undefined && attrOf(shape.element, 'isExpanded') === 'true';
|
|
1092
|
+
for (const child of childrenOf(element)) {
|
|
1093
|
+
// The trigger IS the kind — in either of its two forms — and the
|
|
1094
|
+
// annotation's text IS the label: children that were read, not carried.
|
|
1095
|
+
if (child === trigger || child === triggerRef)
|
|
1096
|
+
continue;
|
|
1097
|
+
if (isModel(child) &&
|
|
1098
|
+
local === 'textAnnotation' &&
|
|
1099
|
+
child.localName === 'text') {
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (expanded && isModel(child) && !NOT_A_BODY.has(child.localName)) {
|
|
1103
|
+
quarantine(payload, fragmentOf(child), BPMN_QUARANTINE_REASON.expanded, { sourceId, element: child.nodeName });
|
|
1104
|
+
// The body's diagram goes with the body, all the way down. A shape left
|
|
1105
|
+
// behind here is an ORPHAN — nothing declares what it draws any more —
|
|
1106
|
+
// so the residue sweep at the end of this reader would pick it up and
|
|
1107
|
+
// carry it, and the writer would then draw it: the quarantine defeated
|
|
1108
|
+
// by its own leftovers, which is the "absent from the re-export" half
|
|
1109
|
+
// of D5 that nothing could fail on until re-emission landed.
|
|
1110
|
+
for (const held of [child, ...descendantsOf(child)]) {
|
|
1111
|
+
const inner = attrOf(held, 'id');
|
|
1112
|
+
if (inner === undefined)
|
|
1113
|
+
continue;
|
|
1114
|
+
quarantinedSourceIds.add(inner);
|
|
1115
|
+
const drawn = shapes.get(inner)?.element ?? diEdges.get(inner)?.element;
|
|
1116
|
+
if (!drawn)
|
|
1117
|
+
continue;
|
|
1118
|
+
payload.quarantined = [
|
|
1119
|
+
...(payload.quarantined ?? []),
|
|
1120
|
+
{
|
|
1121
|
+
fragment: fragmentOf(drawn),
|
|
1122
|
+
reason: BPMN_QUARANTINE_REASON.expanded,
|
|
1123
|
+
},
|
|
1124
|
+
];
|
|
1125
|
+
}
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
carryChild(payload, BPMN_SCOPE.self, child, sourceId);
|
|
1129
|
+
}
|
|
1130
|
+
const size = NODE_SIZE[kind];
|
|
1131
|
+
drafts.push({
|
|
1132
|
+
props: bpmnNodeProps(kind, {
|
|
1133
|
+
xywh: (bounds
|
|
1134
|
+
? new Bound(bounds.x, bounds.y, bounds.w, bounds.h)
|
|
1135
|
+
: new Bound(0, 0, size.w, size.h)).serialize(),
|
|
1136
|
+
text: text || undefined,
|
|
1137
|
+
// The file's own box, which this reader may not rewrite — so the label
|
|
1138
|
+
// is fitted to it rather than painted past it (#184). A shape the file
|
|
1139
|
+
// did not draw is laid out at the pack's own size below, and wants the
|
|
1140
|
+
// pack's own type: no fit, and an element identical to a drawn one.
|
|
1141
|
+
...(bounds ? { fitLabel: true } : {}),
|
|
1142
|
+
}),
|
|
1143
|
+
payload,
|
|
1144
|
+
order: shape?.index ?? Number.POSITIVE_INFINITY,
|
|
1145
|
+
kind: 'node',
|
|
1146
|
+
...(bounds ? {} : { needsLayout: size }),
|
|
1147
|
+
});
|
|
1148
|
+
if (sourceId !== undefined && bounds)
|
|
1149
|
+
nodeBounds.set(sourceId, bounds);
|
|
1150
|
+
if (!bounds) {
|
|
1151
|
+
note({
|
|
1152
|
+
kind: 'invented-layout',
|
|
1153
|
+
sourceId,
|
|
1154
|
+
element: local,
|
|
1155
|
+
message: `<${local}> arrived with no diagram, so Labre placed it beside the ` +
|
|
1156
|
+
`drawing. Its position is Labre's and not the file's.`,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
/**
|
|
1161
|
+
* The edges, held back until every node of the document has been read.
|
|
1162
|
+
*
|
|
1163
|
+
* A flow may name an end declared further down the file, so whether both of
|
|
1164
|
+
* its ends were MAPPED is not knowable while the walk is still going. It has
|
|
1165
|
+
* to be knowable: a flow onto a carried node — a boundary event's error path,
|
|
1166
|
+
* which is the commonest Analytic construct there is — must not become a
|
|
1167
|
+
* connector with a dead end, drawn on the canvas, attached to nothing and
|
|
1168
|
+
* dropped by the next export. See {@link readEdges}.
|
|
1169
|
+
*/
|
|
1170
|
+
const pendingEdges = [];
|
|
1171
|
+
/** `true` when this element WAS an edge, whatever became of it. */
|
|
1172
|
+
const collectEdge = (element, host, hostScope) => {
|
|
1173
|
+
if (EDGE_ROLE_OF_ELEMENT[element.localName] === undefined)
|
|
1174
|
+
return false;
|
|
1175
|
+
pendingEdges.push({ element, host, hostScope });
|
|
1176
|
+
return true;
|
|
1177
|
+
};
|
|
1178
|
+
/** Every flow, once the whole document is known. */
|
|
1179
|
+
const readEdges = () => {
|
|
1180
|
+
for (const { element, host, hostScope } of pendingEdges) {
|
|
1181
|
+
const local = element.localName;
|
|
1182
|
+
const role = EDGE_ROLE_OF_ELEMENT[local];
|
|
1183
|
+
const sourceId = attrOf(element, 'id');
|
|
1184
|
+
const from = attrOf(element, 'sourceRef');
|
|
1185
|
+
const to = attrOf(element, 'targetRef');
|
|
1186
|
+
if (!from || !to) {
|
|
1187
|
+
note({
|
|
1188
|
+
kind: 'warning',
|
|
1189
|
+
sourceId,
|
|
1190
|
+
element: local,
|
|
1191
|
+
message: `<${local}> names only one of its two ends, so there is no arrow ` +
|
|
1192
|
+
`to draw between them. It was left out.`,
|
|
1193
|
+
});
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
// An end on something Labre did not draw. Carried whole, beside the node
|
|
1197
|
+
// it points at and under the same scope, so the pair travels together and
|
|
1198
|
+
// re-emits together — never a live connector with a dead end, which would
|
|
1199
|
+
// be the fourth state D1 says does not exist.
|
|
1200
|
+
const dangling = [from, to].filter(end => !mappedSourceIds.has(end));
|
|
1201
|
+
if (dangling.length > 0) {
|
|
1202
|
+
if (!host)
|
|
1203
|
+
continue;
|
|
1204
|
+
carryChild(host.payload, hostScope, element, sourceId, false);
|
|
1205
|
+
if (sourceId !== undefined)
|
|
1206
|
+
carriedSourceIds.add(sourceId);
|
|
1207
|
+
const edgeDi = sourceId === undefined ? undefined : diEdges.get(sourceId);
|
|
1208
|
+
if (edgeDi) {
|
|
1209
|
+
carryDi(host.payload, sourceId ?? hostScope, fragmentOf(edgeDi.element));
|
|
1210
|
+
}
|
|
1211
|
+
note({
|
|
1212
|
+
kind: 'warning',
|
|
1213
|
+
sourceId,
|
|
1214
|
+
element: local,
|
|
1215
|
+
message: `<${local}> runs to ${dangling.map(end => `"${end}"`).join(' and ')}, ` +
|
|
1216
|
+
`which ${dangling.length === 1 ? 'is' : 'are'} not drawn on this ` +
|
|
1217
|
+
`canvas. The flow is kept whole beside ` +
|
|
1218
|
+
`${dangling.length === 1 ? 'it' : 'them'} rather than drawn with a ` +
|
|
1219
|
+
`loose end.`,
|
|
1220
|
+
});
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
claim(sourceId, local);
|
|
1224
|
+
if (sourceId !== undefined)
|
|
1225
|
+
drawnSourceIds.add(sourceId);
|
|
1226
|
+
const payload = {};
|
|
1227
|
+
if (sourceId !== undefined)
|
|
1228
|
+
payload.id = sourceId;
|
|
1229
|
+
sortAttributes(element, payload, BPMN_SCOPE.self, sourceId);
|
|
1230
|
+
// The exporter writes `associationDirection="None"` on every association
|
|
1231
|
+
// — the role is declared without a direction — so only another value is
|
|
1232
|
+
// something the model does not hold.
|
|
1233
|
+
const direction = attrOf(element, 'associationDirection');
|
|
1234
|
+
if (local === 'association' &&
|
|
1235
|
+
direction !== undefined &&
|
|
1236
|
+
direction !== 'None') {
|
|
1237
|
+
carryAttr(payload, BPMN_SCOPE.self, 'associationDirection', direction);
|
|
1238
|
+
}
|
|
1239
|
+
for (const child of childrenOf(element)) {
|
|
1240
|
+
carryChild(payload, BPMN_SCOPE.self, child, sourceId);
|
|
1241
|
+
}
|
|
1242
|
+
const di = sourceId === undefined ? undefined : diEdges.get(sourceId);
|
|
1243
|
+
sortShapeExtras(di?.element, payload, sourceId);
|
|
1244
|
+
if (di && di.waypoints > 2)
|
|
1245
|
+
explicitRoutes++;
|
|
1246
|
+
const name = attrOf(element, 'name');
|
|
1247
|
+
drafts.push({
|
|
1248
|
+
props: {
|
|
1249
|
+
...connectorProps(role),
|
|
1250
|
+
// The SOURCE FILE's ids — the caller remaps them onto the ones the
|
|
1251
|
+
// surface minted. See the module comment.
|
|
1252
|
+
source: { id: from, position: [0.5, 0.5] },
|
|
1253
|
+
target: { id: to, position: [0.5, 0.5] },
|
|
1254
|
+
...(name ? { text: name } : {}),
|
|
1255
|
+
},
|
|
1256
|
+
payload,
|
|
1257
|
+
order: di?.index ?? Number.POSITIVE_INFINITY,
|
|
1258
|
+
kind: 'edge',
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
/* ── Walking the document ──────────────────────────────────────────── */
|
|
1263
|
+
for (const collaboration of collaborations) {
|
|
1264
|
+
const host = residence();
|
|
1265
|
+
const scope = BPMN_SCOPE.collaboration;
|
|
1266
|
+
if (host) {
|
|
1267
|
+
sortAttributes(collaboration, host.payload, scope, attrOf(collaboration, 'id'));
|
|
1268
|
+
}
|
|
1269
|
+
for (const child of childrenOf(collaboration)) {
|
|
1270
|
+
if (child.localName === 'participant' && isModel(child))
|
|
1271
|
+
continue;
|
|
1272
|
+
if (isModel(child) && collectEdge(child, host, scope))
|
|
1273
|
+
continue;
|
|
1274
|
+
if (isModel(child)) {
|
|
1275
|
+
readNode(child, host, scope);
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (host) {
|
|
1279
|
+
carryChild(host.payload, scope, child, attrOf(collaboration, 'id'));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
for (const process of processes) {
|
|
1284
|
+
const pool = poolOfProcess.get(process);
|
|
1285
|
+
const host = pool ?? residence();
|
|
1286
|
+
// `@self` when the pool IS this process (a file with no participant),
|
|
1287
|
+
// `@process` when the pool is a participant standing in front of it.
|
|
1288
|
+
const scope = pool && pool === mintedPool ? BPMN_SCOPE.self : BPMN_SCOPE.process;
|
|
1289
|
+
for (const child of childrenOf(process)) {
|
|
1290
|
+
if (child.localName === 'laneSet' && isModel(child))
|
|
1291
|
+
continue;
|
|
1292
|
+
if (isModel(child) && collectEdge(child, host, scope))
|
|
1293
|
+
continue;
|
|
1294
|
+
if (isModel(child)) {
|
|
1295
|
+
readNode(child, host, scope);
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (host)
|
|
1299
|
+
carryChild(host.payload, scope, child, attrOf(process, 'id'));
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
readEdges();
|
|
1303
|
+
/* ── The document's own residue (D6) ───────────────────────────────── */
|
|
1304
|
+
const host = residence();
|
|
1305
|
+
const residue = [];
|
|
1306
|
+
for (const root of childrenOf(definitions)) {
|
|
1307
|
+
if (root.namespaceURI === BPMN_NS.bpmndi)
|
|
1308
|
+
continue;
|
|
1309
|
+
if (isModel(root) &&
|
|
1310
|
+
['collaboration', 'process', 'category'].includes(root.localName)) {
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
residue.push(root);
|
|
1314
|
+
}
|
|
1315
|
+
if (host) {
|
|
1316
|
+
// `definitions`' own foreign attributes, and every namespace declaration
|
|
1317
|
+
// this library is not going to write for itself: a carried `camunda:`
|
|
1318
|
+
// fragment means nothing without the declaration it was written under, and
|
|
1319
|
+
// neither does a `bpmn2:boundaryEvent`.
|
|
1320
|
+
//
|
|
1321
|
+
// The test is the PAIR and not the URI. `xmlns:bpmn2` and `xmlns:bpmn` name
|
|
1322
|
+
// the same namespace and are not interchangeable to a fragment stored
|
|
1323
|
+
// verbatim: dropping the file's prefix would leave every carried fragment
|
|
1324
|
+
// in this document unreadable, which is what the writer half found. What is
|
|
1325
|
+
// dropped is only an exact match of a declaration `export.ts` makes anyway
|
|
1326
|
+
// — which is what keeps a Labre file's payload empty.
|
|
1327
|
+
for (const attr of Array.from(definitions.attributes)) {
|
|
1328
|
+
if (attr.name === 'xmlns' || attr.name.startsWith('xmlns:')) {
|
|
1329
|
+
if (BPMN_OWN_DECLARATIONS[attr.name] !== attr.value) {
|
|
1330
|
+
carryAttr(host.payload, BPMN_SCOPE.definitions, attr.name, attr.value);
|
|
1331
|
+
}
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (READ_ATTRS.definitions.includes(attr.name))
|
|
1335
|
+
continue;
|
|
1336
|
+
carryAttr(host.payload, BPMN_SCOPE.definitions, attr.name, attr.value);
|
|
1337
|
+
}
|
|
1338
|
+
for (const root of residue) {
|
|
1339
|
+
// D5 case 4: §15.3.1 wants the file set self-contained and v1 reads one
|
|
1340
|
+
// file, so writing an `<import>` back would claim a resolution we never
|
|
1341
|
+
// made.
|
|
1342
|
+
if (isModel(root) && root.localName === 'import') {
|
|
1343
|
+
quarantine(host.payload, fragmentOf(root), BPMN_QUARANTINE_REASON.imported, { element: 'import' });
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
// Not quarantine: the file telling us our reading of it may be wrong,
|
|
1347
|
+
// which is a thing to say out loud and not a thing to withhold.
|
|
1348
|
+
if (isModel(root) &&
|
|
1349
|
+
root.localName === 'extension' &&
|
|
1350
|
+
attrOf(root, 'mustUnderstand') === 'true') {
|
|
1351
|
+
note({
|
|
1352
|
+
kind: 'warning',
|
|
1353
|
+
element: 'extension',
|
|
1354
|
+
messageKey: BPMN_IMPORT_REMARKS.mustUnderstand[0],
|
|
1355
|
+
message: BPMN_IMPORT_REMARKS.mustUnderstand[1],
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
carryChild(host.payload, BPMN_SCOPE.definitions, root, attrOf(definitions, 'id'));
|
|
1359
|
+
}
|
|
1360
|
+
// A `BPMNShape` or `BPMNEdge` that draws an element the file never
|
|
1361
|
+
// declares. It is broken in the source — nothing can resolve it — but it is
|
|
1362
|
+
// still a node of the file, and D1 has no state for "quietly forgotten":
|
|
1363
|
+
// kept under the id it names, and named in the report so a reader can go
|
|
1364
|
+
// and look.
|
|
1365
|
+
for (const [target, shape] of [
|
|
1366
|
+
...[...shapes].map(([id, entry]) => [id, entry.element]),
|
|
1367
|
+
...[...diEdges].map(([id, entry]) => [id, entry.element]),
|
|
1368
|
+
]) {
|
|
1369
|
+
if (drawnSourceIds.has(target) ||
|
|
1370
|
+
carriedSourceIds.has(target) ||
|
|
1371
|
+
quarantinedSourceIds.has(target)) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
carryDi(host.payload, target, fragmentOf(shape));
|
|
1375
|
+
note({
|
|
1376
|
+
kind: 'warning',
|
|
1377
|
+
sourceId: target,
|
|
1378
|
+
element: shape.localName,
|
|
1379
|
+
message: `The diagram draws "${target}", which the file does not declare. ` +
|
|
1380
|
+
`The shape is kept, and nothing is drawn for it.`,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
else if (residue.length > 0) {
|
|
1385
|
+
// Nothing was drawn, so there is nothing for the residue to ride on. Said
|
|
1386
|
+
// rather than swallowed: D2's carrier is an element, and a file with no
|
|
1387
|
+
// process and no participant has none.
|
|
1388
|
+
note({
|
|
1389
|
+
kind: 'warning',
|
|
1390
|
+
message: `This file declares ${residue.length} root ` +
|
|
1391
|
+
`${residue.length === 1 ? 'element' : 'elements'} and no process to ` +
|
|
1392
|
+
`draw, so there was no artefact for ` +
|
|
1393
|
+
`${residue.length === 1 ? 'it' : 'them'} to be kept on. Nothing was ` +
|
|
1394
|
+
`imported.`,
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
/* ── Lane membership: checked against the drawing, never stored (D3) ─ */
|
|
1398
|
+
for (const bands of laneBands.values()) {
|
|
1399
|
+
if (!bands.every(band => band.rect !== null))
|
|
1400
|
+
continue;
|
|
1401
|
+
for (const band of bands) {
|
|
1402
|
+
for (const ref of band.refs) {
|
|
1403
|
+
const box = nodeBounds.get(ref);
|
|
1404
|
+
if (!box)
|
|
1405
|
+
continue;
|
|
1406
|
+
const centre = box.y + box.h / 2;
|
|
1407
|
+
const drawnIn = bands.find(candidate => candidate.rect !== null &&
|
|
1408
|
+
centre >= candidate.rect.y &&
|
|
1409
|
+
centre <= candidate.rect.y + candidate.rect.h);
|
|
1410
|
+
if (!drawnIn || drawnIn === band)
|
|
1411
|
+
continue;
|
|
1412
|
+
note({
|
|
1413
|
+
kind: 'warning',
|
|
1414
|
+
sourceId: ref,
|
|
1415
|
+
element: 'flowNodeRef',
|
|
1416
|
+
message: `The file lists this artefact in the lane "${band.lane.name}" and ` +
|
|
1417
|
+
`draws it in "${drawnIn.lane.name}". Labre reads the drawing: a ` +
|
|
1418
|
+
`lane holds what is drawn inside it.`,
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
/* ── What the file did not place (D4) ──────────────────────────────── */
|
|
1424
|
+
layOutTheUndrawn(drafts, mintedPool);
|
|
1425
|
+
if (explicitRoutes > 0) {
|
|
1426
|
+
note({
|
|
1427
|
+
kind: 'invented-layout',
|
|
1428
|
+
message: `${explicitRoutes} ${explicitRoutes === 1 ? 'flow carries' : 'flows carry'} ` +
|
|
1429
|
+
`an explicit routing in the file. Labre routes a flow between its two ` +
|
|
1430
|
+
`ends and re-routes it whenever they move, so ` +
|
|
1431
|
+
`${explicitRoutes === 1 ? 'its bend points are' : 'their bend points are'} ` +
|
|
1432
|
+
`not kept.`,
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
/* ── Out ───────────────────────────────────────────────────────────── */
|
|
1436
|
+
// The DRAWING's order is the board's order: the exporter writes its shapes in
|
|
1437
|
+
// document order, so reading them back in plane order is what lets an export
|
|
1438
|
+
// after an import land on the bytes of the export before it. Anything undrawn
|
|
1439
|
+
// keeps its document order, after everything drawn.
|
|
1440
|
+
const ordered = drafts
|
|
1441
|
+
.map((draft, index) => ({ draft, index }))
|
|
1442
|
+
.sort((a, b) => a.draft.order === b.draft.order
|
|
1443
|
+
? a.index - b.index
|
|
1444
|
+
: a.draft.order - b.draft.order)
|
|
1445
|
+
.map(entry => entry.draft);
|
|
1446
|
+
const elements = ordered.map(draft => {
|
|
1447
|
+
const payload = draft.payload;
|
|
1448
|
+
const empty = payload.id === undefined &&
|
|
1449
|
+
payload.element === undefined &&
|
|
1450
|
+
payload.attrs === undefined &&
|
|
1451
|
+
payload.children === undefined &&
|
|
1452
|
+
payload.di === undefined &&
|
|
1453
|
+
payload.quarantined === undefined;
|
|
1454
|
+
// Written as ONE whole blob, or not written at all: the Y.Map entry is the
|
|
1455
|
+
// entire record, so a partial update is a last-write-wins overwrite of
|
|
1456
|
+
// everything, and an element that carried nothing must keep no key (D2).
|
|
1457
|
+
return empty
|
|
1458
|
+
? draft.props
|
|
1459
|
+
: { ...draft.props, interchange: { [BPMN_FORMAT_ID]: payload } };
|
|
1460
|
+
});
|
|
1461
|
+
const lanes = ordered.reduce((total, draft) => total + (Array.isArray(draft.props.lanes) ? draft.props.lanes.length : 0), 0);
|
|
1462
|
+
const sourceVersion = sourceVersionOf(definitions);
|
|
1463
|
+
return {
|
|
1464
|
+
elements,
|
|
1465
|
+
report: {
|
|
1466
|
+
// Everything that became a drawn, editable artefact: the pools, the flow
|
|
1467
|
+
// objects, the arrows — and the LANES, which are drawn and editable and
|
|
1468
|
+
// are not elements of their own.
|
|
1469
|
+
mapped: ordered.length + lanes,
|
|
1470
|
+
carried,
|
|
1471
|
+
quarantined,
|
|
1472
|
+
notes,
|
|
1473
|
+
...(sourceVersion !== undefined ? { sourceVersion } : {}),
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
}
|