@waron97/prbot 3.1.3 → 3.2.0
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/README.md +50 -11
- package/agrippa-pb.md +350 -0
- package/package.json +7 -3
- package/src/agrippa/commands/clone.js +29 -13
- package/src/agrippa/commands/clonePb.js +107 -0
- package/src/agrippa/commands/diff.js +94 -18
- package/src/agrippa/commands/init.js +66 -12
- package/src/agrippa/commands/initPhase.js +16 -17
- package/src/agrippa/commands/pb.js +279 -0
- package/src/agrippa/commands/pull.js +119 -13
- package/src/agrippa/commands/pullPb.js +54 -0
- package/src/agrippa/commands/push.js +112 -47
- package/src/agrippa/commands/pushPb.js +87 -0
- package/src/agrippa/commands/repair.js +3 -1
- package/src/agrippa/index.js +138 -14
- package/src/agrippa/lib/api.js +17 -3
- package/src/agrippa/lib/checksum.js +3 -1
- package/src/agrippa/lib/config.js +2 -2
- package/src/agrippa/lib/pbApi.js +71 -0
- package/src/agrippa/lib/pbEdit.js +467 -0
- package/src/agrippa/lib/pbLayout.js +283 -0
- package/src/agrippa/lib/pbModel.js +698 -0
- package/src/agrippa/lib/pbPreview.js +151 -0
- package/src/agrippa/lib/pbProject.js +390 -0
- package/src/agrippa/lib/pbWorkspace.js +30 -0
- package/src/agrippa/lib/workspace.js +23 -3
- package/src/commands/autopr.js +5 -2
- package/src/commands/changelog.js +7 -2
- package/src/commands/export.js +3 -3
- package/src/commands/exportEmailTemplates.js +25 -15
- package/src/commands/exportImperex.js +4 -4
- package/src/commands/exportLrp.js +10 -7
- package/src/commands/exportPb.js +4 -5
- package/src/commands/exportWorkflow.js +27 -14
- package/src/commands/init.js +7 -0
- package/src/commands/routine.js +7 -5
- package/src/index.js +24 -7
- package/src/lib/premigrate.js +3 -3
- package/src/lib/updateCheck.js +5 -2
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
// Process-builder model: BPMN XML <-> editable graph model.
|
|
2
|
+
//
|
|
3
|
+
// The <process> logic subtree and the <bpmndi> diagram are both parsed into a
|
|
4
|
+
// structured model and rebuilt from it (the <definitions> namespace wrapper is
|
|
5
|
+
// carried in the manifest). The 0-loss bar is semantic (behavioral): two
|
|
6
|
+
// processes are equal iff their *normalized* <process> trees deep-equal
|
|
7
|
+
// (whitespace-, attribute-order-, and sibling-order-insensitive, CDATA + attr
|
|
8
|
+
// values exact) and their diagrams match structurally. See compareProcess /
|
|
9
|
+
// compareDiagram.
|
|
10
|
+
|
|
11
|
+
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
|
|
12
|
+
|
|
13
|
+
const XML_OPTS = {
|
|
14
|
+
ignoreAttributes: false,
|
|
15
|
+
attributeNamePrefix: '@_',
|
|
16
|
+
preserveOrder: true,
|
|
17
|
+
cdataPropName: '__cdata',
|
|
18
|
+
parseTagValue: false,
|
|
19
|
+
trimValues: false,
|
|
20
|
+
suppressEmptyNode: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parser = new XMLParser(XML_OPTS);
|
|
24
|
+
// format:false is required — formatting injects indentation *inside* CDATA,
|
|
25
|
+
// corrupting script bodies. built_page is machine-reassembled, never hand-edited.
|
|
26
|
+
const builder = new XMLBuilder({ ...XML_OPTS, format: false });
|
|
27
|
+
|
|
28
|
+
// ---------- preserveOrder tree helpers ----------
|
|
29
|
+
|
|
30
|
+
function tagOf(node) {
|
|
31
|
+
return Object.keys(node).find((k) => k !== ':@' && k !== '#text' && k !== '__cdata');
|
|
32
|
+
}
|
|
33
|
+
function attrsOf(node) {
|
|
34
|
+
return node[':@'] || {};
|
|
35
|
+
}
|
|
36
|
+
function cdataOf(node, tag) {
|
|
37
|
+
// node[tag] is an array of child nodes; find the __cdata holder
|
|
38
|
+
const kids = node[tag];
|
|
39
|
+
if (!Array.isArray(kids)) return null;
|
|
40
|
+
const holder = kids.find((k) => Object.prototype.hasOwnProperty.call(k, '__cdata'));
|
|
41
|
+
if (!holder) return null;
|
|
42
|
+
const inner = holder.__cdata.find((k) => Object.prototype.hasOwnProperty.call(k, '#text'));
|
|
43
|
+
return inner ? inner['#text'] : '';
|
|
44
|
+
}
|
|
45
|
+
function textOf(node, tag) {
|
|
46
|
+
const kids = node[tag];
|
|
47
|
+
if (!Array.isArray(kids)) return null;
|
|
48
|
+
const t = kids
|
|
49
|
+
.filter((k) => Object.prototype.hasOwnProperty.call(k, '#text'))
|
|
50
|
+
.map((k) => k['#text'])
|
|
51
|
+
.join('');
|
|
52
|
+
return t;
|
|
53
|
+
}
|
|
54
|
+
function isWhitespace(s) {
|
|
55
|
+
return typeof s === 'string' && s.trim() === '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build a preserveOrder element node from {tag, attrs, children}
|
|
59
|
+
function el(tag, attrs, children) {
|
|
60
|
+
const node = { [tag]: children || [] };
|
|
61
|
+
if (attrs && Object.keys(attrs).length) node[':@'] = attrs;
|
|
62
|
+
return node;
|
|
63
|
+
}
|
|
64
|
+
function cdataChild(text) {
|
|
65
|
+
return { __cdata: [{ '#text': text }] };
|
|
66
|
+
}
|
|
67
|
+
function textChild(text) {
|
|
68
|
+
return { '#text': text };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------- node classification ----------
|
|
72
|
+
|
|
73
|
+
// Container nodes hold their own nested flow elements (and may loop).
|
|
74
|
+
// `transaction` is Activiti's transactional subprocess — modeled like subProcess.
|
|
75
|
+
const CONTAINER_TAGS = new Set(['subProcess', 'transaction']);
|
|
76
|
+
|
|
77
|
+
const NODE_TAGS = new Set([
|
|
78
|
+
'startEvent',
|
|
79
|
+
'endEvent',
|
|
80
|
+
'scriptTask',
|
|
81
|
+
'serviceTask',
|
|
82
|
+
'userTask',
|
|
83
|
+
'exclusiveGateway',
|
|
84
|
+
'subProcess',
|
|
85
|
+
'transaction',
|
|
86
|
+
'boundaryEvent',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const PROMOTED_ATTRS = {
|
|
90
|
+
scriptTask: ['@_id', '@_name'],
|
|
91
|
+
serviceTask: ['@_id', '@_name', '@_activiti:class'],
|
|
92
|
+
userTask: ['@_id', '@_name', '@_activiti:formKey'],
|
|
93
|
+
exclusiveGateway: ['@_id', '@_name'],
|
|
94
|
+
subProcess: ['@_id', '@_name'],
|
|
95
|
+
boundaryEvent: ['@_id', '@_name', '@_attachedToRef'],
|
|
96
|
+
startEvent: ['@_id', '@_name'],
|
|
97
|
+
endEvent: ['@_id', '@_name'],
|
|
98
|
+
process_root: ['@_id', '@_name', '@_isExecutable'],
|
|
99
|
+
association: ['@_id', '@_sourceRef', '@_targetRef'],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Collect the non-promoted attributes into a plain map (prefix stripped),
|
|
103
|
+
// so rare attrs (@_default, @_activiti:async, scriptFormat, ...) survive.
|
|
104
|
+
function extraAttrs(attrs, type) {
|
|
105
|
+
const promoted = new Set(PROMOTED_ATTRS[type] || ['@_id', '@_name']);
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
108
|
+
if (promoted.has(k)) continue;
|
|
109
|
+
out[k.replace(/^@_/, '')] = v;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseServiceFields(node) {
|
|
115
|
+
const ext = node.serviceTask?.find((k) => tagOf(k) === 'extensionElements');
|
|
116
|
+
if (!ext) return [];
|
|
117
|
+
const fields = [];
|
|
118
|
+
for (const f of ext.extensionElements) {
|
|
119
|
+
if (tagOf(f) !== 'activiti:field') continue;
|
|
120
|
+
const name = attrsOf(f)['@_name'];
|
|
121
|
+
const strChild = f['activiti:field'].find((k) => tagOf(k) === 'activiti:string');
|
|
122
|
+
const exprChild = f['activiti:field'].find((k) => tagOf(k) === 'activiti:expression');
|
|
123
|
+
if (strChild) {
|
|
124
|
+
fields.push({ name, string: cdataOf(strChild, 'activiti:string') ?? '' });
|
|
125
|
+
} else if (exprChild) {
|
|
126
|
+
fields.push({ name, expression: cdataOf(exprChild, 'activiti:expression') ?? '' });
|
|
127
|
+
} else {
|
|
128
|
+
fields.push({ name }); // empty <activiti:field/>
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return fields;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// All attributes of a node as a prefix-stripped map (nothing promoted).
|
|
135
|
+
function allAttrs(node) {
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [k, v] of Object.entries(attrsOf(node))) out[k.replace(/^@_/, '')] = v;
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseMultiInstance(node, tag) {
|
|
142
|
+
const mi = node[tag]?.find((k) => tagOf(k) === 'multiInstanceLoopCharacteristics');
|
|
143
|
+
if (!mi) return undefined;
|
|
144
|
+
const out = { attrs: allAttrs(mi) };
|
|
145
|
+
const card = mi.multiInstanceLoopCharacteristics.find((k) => tagOf(k) === 'loopCardinality');
|
|
146
|
+
const comp = mi.multiInstanceLoopCharacteristics.find(
|
|
147
|
+
(k) => tagOf(k) === 'completionCondition'
|
|
148
|
+
);
|
|
149
|
+
if (card)
|
|
150
|
+
out.loopCardinality = { value: textOf(card, 'loopCardinality'), attrs: allAttrs(card) };
|
|
151
|
+
if (comp)
|
|
152
|
+
out.completionCondition = {
|
|
153
|
+
value: textOf(comp, 'completionCondition'),
|
|
154
|
+
attrs: allAttrs(comp),
|
|
155
|
+
};
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseErrorEventDef(node, type) {
|
|
160
|
+
const eed = node[type]?.find((k) => tagOf(k) === 'errorEventDefinition');
|
|
161
|
+
if (!eed) return undefined;
|
|
162
|
+
return { ...extraAttrs(attrsOf(eed), type) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Parse the children of a <process> or <subProcess> element array into model.
|
|
166
|
+
function parseFlowChildren(children, parentId, model) {
|
|
167
|
+
for (const child of children) {
|
|
168
|
+
const tag = tagOf(child);
|
|
169
|
+
if (!tag || tag === '#text') continue;
|
|
170
|
+
const attrs = attrsOf(child);
|
|
171
|
+
|
|
172
|
+
if (tag === 'documentation') {
|
|
173
|
+
if (parentId === null) model.process.documentation = textOf(child, tag);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (tag === 'error') {
|
|
177
|
+
model.errors.push({ id: attrs['@_id'], name: attrs['@_name'] });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (tag === 'textAnnotation') {
|
|
181
|
+
const txtEl = child.textAnnotation.find((k) => tagOf(k) === 'text');
|
|
182
|
+
model.annotations.push({
|
|
183
|
+
id: attrs['@_id'],
|
|
184
|
+
attrs: extraAttrs(attrs, 'textAnnotation'),
|
|
185
|
+
text: txtEl ? textOf(txtEl, 'text') : '',
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (tag === 'association') {
|
|
190
|
+
model.associations.push({
|
|
191
|
+
id: attrs['@_id'],
|
|
192
|
+
sourceRef: attrs['@_sourceRef'],
|
|
193
|
+
targetRef: attrs['@_targetRef'],
|
|
194
|
+
attrs: extraAttrs({ ...attrs }, 'association'),
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (tag === 'sequenceFlow') {
|
|
199
|
+
const cond = child.sequenceFlow?.find((k) => tagOf(k) === 'conditionExpression');
|
|
200
|
+
const doc = child.sequenceFlow?.find((k) => tagOf(k) === 'documentation');
|
|
201
|
+
const edge = {
|
|
202
|
+
id: attrs['@_id'],
|
|
203
|
+
source: attrs['@_sourceRef'],
|
|
204
|
+
target: attrs['@_targetRef'],
|
|
205
|
+
};
|
|
206
|
+
if (attrs['@_name'] !== undefined) edge.name = attrs['@_name'];
|
|
207
|
+
if (parentId) edge.parent = parentId;
|
|
208
|
+
if (cond) {
|
|
209
|
+
edge.condition =
|
|
210
|
+
cdataOf(cond, 'conditionExpression') ?? textOf(cond, 'conditionExpression');
|
|
211
|
+
const xsi = attrsOf(cond)['@_xsi:type'];
|
|
212
|
+
if (xsi) edge.conditionType = xsi;
|
|
213
|
+
}
|
|
214
|
+
if (doc) edge.documentation = textOf(doc, 'documentation');
|
|
215
|
+
model.edges.push(edge);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (NODE_TAGS.has(tag)) {
|
|
219
|
+
const n = { id: attrs['@_id'], type: tag };
|
|
220
|
+
if (attrs['@_name'] !== undefined) n.name = attrs['@_name'];
|
|
221
|
+
if (parentId) n.parent = parentId;
|
|
222
|
+
const extra = extraAttrs(attrs, tag);
|
|
223
|
+
if (Object.keys(extra).length) n.attrs = extra;
|
|
224
|
+
|
|
225
|
+
const docEl = child[tag]?.find((k) => tagOf(k) === 'documentation');
|
|
226
|
+
if (docEl) n.documentation = textOf(docEl, 'documentation');
|
|
227
|
+
|
|
228
|
+
if (tag === 'scriptTask') {
|
|
229
|
+
const scriptEl = child.scriptTask?.find((k) => tagOf(k) === 'script');
|
|
230
|
+
n.script = scriptEl ? (cdataOf(scriptEl, 'script') ?? '') : '';
|
|
231
|
+
} else if (tag === 'serviceTask') {
|
|
232
|
+
if (attrs['@_activiti:class'] !== undefined) n.class = attrs['@_activiti:class'];
|
|
233
|
+
n.fields = parseServiceFields(child);
|
|
234
|
+
} else if (tag === 'userTask') {
|
|
235
|
+
if (attrs['@_activiti:formKey'] !== undefined)
|
|
236
|
+
n.formKey = attrs['@_activiti:formKey'];
|
|
237
|
+
} else if (tag === 'boundaryEvent') {
|
|
238
|
+
if (attrs['@_attachedToRef'] !== undefined)
|
|
239
|
+
n.attachedToRef = attrs['@_attachedToRef'];
|
|
240
|
+
const eed = parseErrorEventDef(child, tag);
|
|
241
|
+
if (eed) n.errorEventDefinition = eed;
|
|
242
|
+
} else if (tag === 'endEvent') {
|
|
243
|
+
const eed = parseErrorEventDef(child, tag);
|
|
244
|
+
if (eed) n.errorEventDefinition = eed;
|
|
245
|
+
} else if (CONTAINER_TAGS.has(tag)) {
|
|
246
|
+
const mi = parseMultiInstance(child, tag);
|
|
247
|
+
if (mi) n.multiInstance = mi;
|
|
248
|
+
}
|
|
249
|
+
model.nodes.push(n);
|
|
250
|
+
|
|
251
|
+
if (CONTAINER_TAGS.has(tag)) {
|
|
252
|
+
parseFlowChildren(child[tag], n.id, model);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------- public: parse built_page -> { ns, processAttrs, model, diagramXml } ----------
|
|
259
|
+
|
|
260
|
+
function parseProcess(builtPage) {
|
|
261
|
+
const tree = parser.parse(builtPage);
|
|
262
|
+
const defNode = tree.find((n) => tagOf(n) === 'definitions');
|
|
263
|
+
if (!defNode) throw new Error('built_page has no <definitions> root');
|
|
264
|
+
const ns = attrsOf(defNode);
|
|
265
|
+
const procNode = defNode.definitions.find((n) => tagOf(n) === 'process');
|
|
266
|
+
if (!procNode) throw new Error('built_page has no <process>');
|
|
267
|
+
const procAttrs = attrsOf(procNode);
|
|
268
|
+
|
|
269
|
+
const model = {
|
|
270
|
+
process: (() => {
|
|
271
|
+
const p = {
|
|
272
|
+
id: procAttrs['@_id'],
|
|
273
|
+
name: procAttrs['@_name'],
|
|
274
|
+
isExecutable: procAttrs['@_isExecutable'],
|
|
275
|
+
documentation: null,
|
|
276
|
+
};
|
|
277
|
+
const extra = extraAttrs(procAttrs, 'process_root');
|
|
278
|
+
if (Object.keys(extra).length) p.attrs = extra;
|
|
279
|
+
return p;
|
|
280
|
+
})(),
|
|
281
|
+
errors: [],
|
|
282
|
+
nodes: [],
|
|
283
|
+
edges: [],
|
|
284
|
+
annotations: [],
|
|
285
|
+
associations: [],
|
|
286
|
+
};
|
|
287
|
+
parseFlowChildren(procNode.process, null, model);
|
|
288
|
+
|
|
289
|
+
return { ns, model, diagram: parseDiagram(builtPage) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------- diagram (bpmndi) parse / build ----------
|
|
293
|
+
//
|
|
294
|
+
// Parsed into a structured object so node bounds and edge waypoints can be
|
|
295
|
+
// surfaced (and edited) in structure.yaml. Coordinates are kept as numbers.
|
|
296
|
+
|
|
297
|
+
function num(v) {
|
|
298
|
+
const n = Number(v);
|
|
299
|
+
return Number.isNaN(n) ? v : n;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseLabel(labelNode) {
|
|
303
|
+
const b = labelNode['bpmndi:BPMNLabel'].find((k) => tagOf(k) === 'omgdc:Bounds');
|
|
304
|
+
return { attrs: allAttrs(labelNode), bounds: b ? mapBounds(allAttrs(b)) : null };
|
|
305
|
+
}
|
|
306
|
+
function mapBounds(a) {
|
|
307
|
+
const out = {};
|
|
308
|
+
for (const [k, v] of Object.entries(a)) out[k] = num(v);
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseDiagram(builtPage) {
|
|
313
|
+
const tree = parser.parse(builtPage);
|
|
314
|
+
const defNode = tree.find((n) => tagOf(n) === 'definitions');
|
|
315
|
+
const dia = defNode.definitions.find((n) => tagOf(n) === 'bpmndi:BPMNDiagram');
|
|
316
|
+
if (!dia) return null;
|
|
317
|
+
const plane = dia['bpmndi:BPMNDiagram'].find((n) => tagOf(n) === 'bpmndi:BPMNPlane');
|
|
318
|
+
|
|
319
|
+
const shapes = [];
|
|
320
|
+
const edges = [];
|
|
321
|
+
for (const c of plane['bpmndi:BPMNPlane']) {
|
|
322
|
+
const tg = tagOf(c);
|
|
323
|
+
if (tg === 'bpmndi:BPMNShape') {
|
|
324
|
+
const bounds = c['bpmndi:BPMNShape'].find((x) => tagOf(x) === 'omgdc:Bounds');
|
|
325
|
+
const label = c['bpmndi:BPMNShape'].find((x) => tagOf(x) === 'bpmndi:BPMNLabel');
|
|
326
|
+
shapes.push({
|
|
327
|
+
attrs: allAttrs(c),
|
|
328
|
+
bounds: bounds ? mapBounds(allAttrs(bounds)) : null,
|
|
329
|
+
label: label ? parseLabel(label) : null,
|
|
330
|
+
});
|
|
331
|
+
} else if (tg === 'bpmndi:BPMNEdge') {
|
|
332
|
+
const wps = c['bpmndi:BPMNEdge']
|
|
333
|
+
.filter((x) => tagOf(x) === 'omgdi:waypoint')
|
|
334
|
+
.map((w) => mapBounds(allAttrs(w)));
|
|
335
|
+
const label = c['bpmndi:BPMNEdge'].find((x) => tagOf(x) === 'bpmndi:BPMNLabel');
|
|
336
|
+
edges.push({
|
|
337
|
+
attrs: allAttrs(c),
|
|
338
|
+
waypoints: wps,
|
|
339
|
+
label: label ? parseLabel(label) : null,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { attrs: allAttrs(dia), plane: { attrs: allAttrs(plane) }, shapes, edges };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function boundsAttrs(b) {
|
|
347
|
+
return {
|
|
348
|
+
'@_x': String(b.x),
|
|
349
|
+
'@_y': String(b.y),
|
|
350
|
+
'@_width': String(b.width),
|
|
351
|
+
'@_height': String(b.height),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Rebuild <bpmndi> purely from structure.yaml geometry (model graph + geo maps).
|
|
356
|
+
// DI element ids are derived (`<id>_di`); the manifest is never consulted. Labels
|
|
357
|
+
// are omitted (renderers auto-place them); only positions, sizes, waypoints and
|
|
358
|
+
// subprocess expand-state are authoritative — see compareDiagram.
|
|
359
|
+
function buildDiagram(model, geo) {
|
|
360
|
+
if (!geo) return null;
|
|
361
|
+
const shapeEls = [];
|
|
362
|
+
const pushShape = (id) => {
|
|
363
|
+
const b = geo.bounds[id];
|
|
364
|
+
if (!b) return;
|
|
365
|
+
const attrs = { '@_id': `${id}_di`, '@_bpmnElement': id };
|
|
366
|
+
if (geo.expanded[id] !== undefined) attrs['@_isExpanded'] = String(geo.expanded[id]);
|
|
367
|
+
shapeEls.push(el('bpmndi:BPMNShape', attrs, [el('omgdc:Bounds', boundsAttrs(b), [])]));
|
|
368
|
+
};
|
|
369
|
+
for (const n of model.nodes) pushShape(n.id);
|
|
370
|
+
for (const a of model.annotations || []) pushShape(a.id);
|
|
371
|
+
|
|
372
|
+
const edgeEls = [];
|
|
373
|
+
const pushEdge = (id) => {
|
|
374
|
+
const wps = geo.waypoints[id];
|
|
375
|
+
if (!wps) return;
|
|
376
|
+
const pts = wps.map(([x, y]) =>
|
|
377
|
+
el('omgdi:waypoint', { '@_x': String(x), '@_y': String(y) }, [])
|
|
378
|
+
);
|
|
379
|
+
edgeEls.push(el('bpmndi:BPMNEdge', { '@_id': `${id}_di`, '@_bpmnElement': id }, pts));
|
|
380
|
+
};
|
|
381
|
+
for (const e of model.edges) pushEdge(e.id);
|
|
382
|
+
for (const a of model.associations || []) pushEdge(a.id);
|
|
383
|
+
|
|
384
|
+
if (!shapeEls.length && !edgeEls.length) return null;
|
|
385
|
+
const plane = el(
|
|
386
|
+
'bpmndi:BPMNPlane',
|
|
387
|
+
{ '@_id': 'BPMNPlane_1', '@_bpmnElement': model.process.id },
|
|
388
|
+
[...shapeEls, ...edgeEls]
|
|
389
|
+
);
|
|
390
|
+
return builder.build([el('bpmndi:BPMNDiagram', { '@_id': 'BPMNDiagram_1' }, [plane])]).trim();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---------- build: model -> <process> preserveOrder children ----------
|
|
394
|
+
|
|
395
|
+
function buildFlowChildren(model, parentId) {
|
|
396
|
+
const children = [];
|
|
397
|
+
|
|
398
|
+
// documentation (process root only)
|
|
399
|
+
if (parentId === null && model.process.documentation != null) {
|
|
400
|
+
children.push(el('documentation', {}, [textChild(model.process.documentation)]));
|
|
401
|
+
}
|
|
402
|
+
// errors (process root only)
|
|
403
|
+
if (parentId === null) {
|
|
404
|
+
for (const e of model.errors) {
|
|
405
|
+
children.push(el('error', { '@_id': e.id, '@_name': e.name }, []));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const restoreAttr = (extra) => {
|
|
410
|
+
const out = {};
|
|
411
|
+
for (const [k, v] of Object.entries(extra || {})) out[`@_${k}`] = v;
|
|
412
|
+
return out;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const incoming = (id) => model.edges.filter((e) => e.target === id).map((e) => e.id);
|
|
416
|
+
const outgoing = (id) => model.edges.filter((e) => e.source === id).map((e) => e.id);
|
|
417
|
+
|
|
418
|
+
const flowChildren = (id) => {
|
|
419
|
+
const c = [];
|
|
420
|
+
for (const f of incoming(id)) c.push(el('incoming', {}, [textChild(f)]));
|
|
421
|
+
for (const f of outgoing(id)) c.push(el('outgoing', {}, [textChild(f)]));
|
|
422
|
+
return c;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// documentation (any node) + incoming/outgoing
|
|
426
|
+
const baseKids = (n) => {
|
|
427
|
+
const c = [];
|
|
428
|
+
if (n.documentation != null) c.push(el('documentation', {}, [textChild(n.documentation)]));
|
|
429
|
+
c.push(...flowChildren(n.id));
|
|
430
|
+
return c;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
for (const n of model.nodes.filter((x) => (x.parent ?? null) === parentId)) {
|
|
434
|
+
const attrs = { '@_id': n.id };
|
|
435
|
+
if (n.name !== undefined) attrs['@_name'] = n.name;
|
|
436
|
+
|
|
437
|
+
if (n.type === 'scriptTask') {
|
|
438
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
439
|
+
const kids = baseKids(n);
|
|
440
|
+
kids.push(el('script', {}, [cdataChild(n.script ?? '')]));
|
|
441
|
+
children.push(el('scriptTask', attrs, kids));
|
|
442
|
+
} else if (n.type === 'serviceTask') {
|
|
443
|
+
if (n.class !== undefined) attrs['@_activiti:class'] = n.class;
|
|
444
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
445
|
+
const kids = [];
|
|
446
|
+
if (n.fields && n.fields.length) {
|
|
447
|
+
const fieldEls = n.fields.map((f) => {
|
|
448
|
+
let inner = [];
|
|
449
|
+
if (Object.prototype.hasOwnProperty.call(f, 'string')) {
|
|
450
|
+
inner = [el('activiti:string', {}, [cdataChild(f.string)])];
|
|
451
|
+
} else if (Object.prototype.hasOwnProperty.call(f, 'expression')) {
|
|
452
|
+
inner = [el('activiti:expression', {}, [cdataChild(f.expression)])];
|
|
453
|
+
}
|
|
454
|
+
return el('activiti:field', { '@_name': f.name }, inner);
|
|
455
|
+
});
|
|
456
|
+
kids.push(el('extensionElements', {}, fieldEls));
|
|
457
|
+
}
|
|
458
|
+
kids.push(...baseKids(n));
|
|
459
|
+
children.push(el('serviceTask', attrs, kids));
|
|
460
|
+
} else if (n.type === 'userTask') {
|
|
461
|
+
if (n.formKey !== undefined) attrs['@_activiti:formKey'] = n.formKey;
|
|
462
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
463
|
+
children.push(el('userTask', attrs, baseKids(n)));
|
|
464
|
+
} else if (n.type === 'exclusiveGateway') {
|
|
465
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
466
|
+
children.push(el('exclusiveGateway', attrs, baseKids(n)));
|
|
467
|
+
} else if (n.type === 'startEvent') {
|
|
468
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
469
|
+
children.push(el('startEvent', attrs, baseKids(n)));
|
|
470
|
+
} else if (n.type === 'endEvent') {
|
|
471
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
472
|
+
const kids = baseKids(n);
|
|
473
|
+
if (n.errorEventDefinition) {
|
|
474
|
+
kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
|
|
475
|
+
}
|
|
476
|
+
children.push(el('endEvent', attrs, kids));
|
|
477
|
+
} else if (n.type === 'boundaryEvent') {
|
|
478
|
+
if (n.attachedToRef !== undefined) attrs['@_attachedToRef'] = n.attachedToRef;
|
|
479
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
480
|
+
const kids = baseKids(n);
|
|
481
|
+
if (n.errorEventDefinition) {
|
|
482
|
+
kids.push(el('errorEventDefinition', restoreAttr(n.errorEventDefinition), []));
|
|
483
|
+
}
|
|
484
|
+
children.push(el('boundaryEvent', attrs, kids));
|
|
485
|
+
} else if (CONTAINER_TAGS.has(n.type)) {
|
|
486
|
+
Object.assign(attrs, restoreAttr(n.attrs));
|
|
487
|
+
const kids = baseKids(n);
|
|
488
|
+
if (n.multiInstance) {
|
|
489
|
+
const mi = n.multiInstance;
|
|
490
|
+
const miKids = [];
|
|
491
|
+
if (mi.loopCardinality) {
|
|
492
|
+
const lc = mi.loopCardinality;
|
|
493
|
+
miKids.push(
|
|
494
|
+
el(
|
|
495
|
+
'loopCardinality',
|
|
496
|
+
restoreAttr(lc.attrs),
|
|
497
|
+
lc.value != null ? [textChild(lc.value)] : []
|
|
498
|
+
)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
if (mi.completionCondition) {
|
|
502
|
+
const cc = mi.completionCondition;
|
|
503
|
+
miKids.push(
|
|
504
|
+
el(
|
|
505
|
+
'completionCondition',
|
|
506
|
+
restoreAttr(cc.attrs),
|
|
507
|
+
cc.value != null ? [textChild(cc.value)] : []
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
kids.push(el('multiInstanceLoopCharacteristics', restoreAttr(mi.attrs), miKids));
|
|
512
|
+
}
|
|
513
|
+
kids.push(...buildFlowChildren(model, n.id)); // nested
|
|
514
|
+
children.push(el(n.type, attrs, kids)); // subProcess | transaction
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// edges scoped to this parent
|
|
519
|
+
for (const e of model.edges.filter((x) => (x.parent ?? null) === parentId)) {
|
|
520
|
+
const attrs = { '@_id': e.id, '@_sourceRef': e.source, '@_targetRef': e.target };
|
|
521
|
+
if (e.name !== undefined) attrs['@_name'] = e.name;
|
|
522
|
+
const kids = [];
|
|
523
|
+
if (e.documentation != null)
|
|
524
|
+
kids.push(el('documentation', {}, [textChild(e.documentation)]));
|
|
525
|
+
if (e.condition != null) {
|
|
526
|
+
const cAttrs = e.conditionType ? { '@_xsi:type': e.conditionType } : {};
|
|
527
|
+
kids.push(el('conditionExpression', cAttrs, [cdataChild(e.condition)]));
|
|
528
|
+
}
|
|
529
|
+
children.push(el('sequenceFlow', attrs, kids));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// annotations + associations (process root only)
|
|
533
|
+
if (parentId === null) {
|
|
534
|
+
for (const a of model.annotations) {
|
|
535
|
+
children.push(
|
|
536
|
+
el('textAnnotation', { '@_id': a.id, ...restoreAttr(a.attrs) }, [
|
|
537
|
+
el('text', {}, [textChild(a.text ?? '')]),
|
|
538
|
+
])
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
for (const a of model.associations) {
|
|
542
|
+
children.push(
|
|
543
|
+
el(
|
|
544
|
+
'association',
|
|
545
|
+
{
|
|
546
|
+
'@_id': a.id,
|
|
547
|
+
'@_sourceRef': a.sourceRef,
|
|
548
|
+
'@_targetRef': a.targetRef,
|
|
549
|
+
...restoreAttr(a.attrs),
|
|
550
|
+
},
|
|
551
|
+
[]
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return children;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Build the full built_page string from { ns, model, geo }. The <bpmndi> block
|
|
561
|
+
// is regenerated solely from `geo` (structure.yaml geometry) — never a manifest.
|
|
562
|
+
function buildProcess({ ns, model, geo }) {
|
|
563
|
+
const diagramXml = buildDiagram(model, geo);
|
|
564
|
+
const procAttrs = { '@_id': model.process.id };
|
|
565
|
+
if (model.process.name !== undefined) procAttrs['@_name'] = model.process.name;
|
|
566
|
+
if (model.process.isExecutable !== undefined)
|
|
567
|
+
procAttrs['@_isExecutable'] = model.process.isExecutable;
|
|
568
|
+
for (const [k, v] of Object.entries(model.process.attrs || {})) procAttrs[`@_${k}`] = v;
|
|
569
|
+
|
|
570
|
+
const procNode = el('process', procAttrs, buildFlowChildren(model, null));
|
|
571
|
+
const procXml = builder.build([procNode]).trim();
|
|
572
|
+
|
|
573
|
+
const defAttrs = Object.entries(ns)
|
|
574
|
+
.map(([k, v]) => `${k.replace(/^@_/, '')}="${v}"`)
|
|
575
|
+
.join(' ');
|
|
576
|
+
|
|
577
|
+
// Whitespace between elements is insignificant in XML. The <process> is
|
|
578
|
+
// compact (format:false) to keep CDATA exact; the diagram is regenerated.
|
|
579
|
+
const parts = ['<?xml version="1.0" encoding="UTF-8"?>', `<definitions ${defAttrs}>`, procXml];
|
|
580
|
+
if (diagramXml) parts.push(diagramXml);
|
|
581
|
+
parts.push('</definitions>');
|
|
582
|
+
return parts.join('\n');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------- normalize + compare (the 0-loss gate) ----------
|
|
586
|
+
|
|
587
|
+
// Recursively canonicalize a preserveOrder tree: drop whitespace text, sort
|
|
588
|
+
// attributes, sort id-bearing / text-only siblings by a stable key. Returns a
|
|
589
|
+
// plain comparable object. CDATA and attribute values are kept exact.
|
|
590
|
+
function canon(node) {
|
|
591
|
+
const tag = tagOf(node);
|
|
592
|
+
if (tag === undefined) {
|
|
593
|
+
if (Object.prototype.hasOwnProperty.call(node, '__cdata')) {
|
|
594
|
+
const inner = node.__cdata.map((k) => k['#text'] ?? '').join('');
|
|
595
|
+
return { cdata: inner };
|
|
596
|
+
}
|
|
597
|
+
if (Object.prototype.hasOwnProperty.call(node, '#text')) {
|
|
598
|
+
if (isWhitespace(node['#text'])) return null;
|
|
599
|
+
return { text: node['#text'] };
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
const attrs = {};
|
|
604
|
+
for (const [k, v] of Object.entries(attrsOf(node))) attrs[k] = v;
|
|
605
|
+
const rawKids = Array.isArray(node[tag]) ? node[tag] : [];
|
|
606
|
+
const kids = rawKids.map(canon).filter((x) => x !== null);
|
|
607
|
+
kids.sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
|
|
608
|
+
return { tag, attrs, kids };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function keyOf(c) {
|
|
612
|
+
if (c.cdata !== undefined) return '~cdata:' + c.cdata;
|
|
613
|
+
if (c.text !== undefined) return '~text:' + c.text;
|
|
614
|
+
const a = c.attrs || {};
|
|
615
|
+
const id = a['@_id'] || a['@_sourceRef'] + '>' + a['@_targetRef'] || '';
|
|
616
|
+
// for incoming/outgoing (no attrs, text child), include child text
|
|
617
|
+
const childKey = (c.kids || []).map((k) => k.text || k.cdata || k.tag || '').join(',');
|
|
618
|
+
return `${c.tag}|${id}|${childKey}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function normalizeProcessTree(builtPage) {
|
|
622
|
+
const tree = parser.parse(builtPage);
|
|
623
|
+
const defNode = tree.find((n) => tagOf(n) === 'definitions');
|
|
624
|
+
const procNode = defNode.definitions.find((n) => tagOf(n) === 'process');
|
|
625
|
+
return canon(procNode);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Deep structural comparison; returns first diff path or null if equal.
|
|
629
|
+
function diff(a, b, path = '$') {
|
|
630
|
+
if (typeof a !== typeof b) return `${path}: type ${typeof a} != ${typeof b}`;
|
|
631
|
+
if (a === null || typeof a !== 'object') {
|
|
632
|
+
return a === b ? null : `${path}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`;
|
|
633
|
+
}
|
|
634
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
635
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return `${path}: array mismatch`;
|
|
636
|
+
if (a.length !== b.length) return `${path}: length ${a.length} != ${b.length}`;
|
|
637
|
+
for (let i = 0; i < a.length; i++) {
|
|
638
|
+
const d = diff(a[i], b[i], `${path}[${i}]`);
|
|
639
|
+
if (d) return d;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
644
|
+
for (const k of keys) {
|
|
645
|
+
const d = diff(a[k], b[k], `${path}.${k}`);
|
|
646
|
+
if (d) return d;
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Compare two built_page strings at the semantic <process> level.
|
|
652
|
+
function compareProcess(builtPageA, builtPageB) {
|
|
653
|
+
return diff(normalizeProcessTree(builtPageA), normalizeProcessTree(builtPageB));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Reduce a parsed diagram to its *semantic* geometry, keyed by bpmnElement:
|
|
657
|
+
// per-shape bounds (+ subprocess isExpanded), per-edge ordered waypoints.
|
|
658
|
+
// Arbitrary DI element ids, label boxes, and plane/diagram ids are ignored —
|
|
659
|
+
// they are regenerated, not preserved (structure.yaml is the source of truth).
|
|
660
|
+
function diagramGeometry(builtPage) {
|
|
661
|
+
const d = parseDiagram(builtPage);
|
|
662
|
+
if (!d) return null;
|
|
663
|
+
const shapes = {};
|
|
664
|
+
const edges = {};
|
|
665
|
+
for (const s of d.shapes) {
|
|
666
|
+
const be = s.attrs.bpmnElement;
|
|
667
|
+
if (be == null || !s.bounds) continue;
|
|
668
|
+
const g = {
|
|
669
|
+
x: Number(s.bounds.x),
|
|
670
|
+
y: Number(s.bounds.y),
|
|
671
|
+
width: Number(s.bounds.width),
|
|
672
|
+
height: Number(s.bounds.height),
|
|
673
|
+
};
|
|
674
|
+
if (s.attrs.isExpanded !== undefined) g.isExpanded = String(s.attrs.isExpanded);
|
|
675
|
+
shapes[be] = g;
|
|
676
|
+
}
|
|
677
|
+
for (const e of d.edges) {
|
|
678
|
+
const be = e.attrs.bpmnElement;
|
|
679
|
+
if (be == null) continue;
|
|
680
|
+
edges[be] = (e.waypoints || []).map((w) => [Number(w.x), Number(w.y)]);
|
|
681
|
+
}
|
|
682
|
+
return { shapes, edges };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Compare the bpmndi diagrams of two built_page strings, by geometry only.
|
|
686
|
+
function compareDiagram(builtPageA, builtPageB) {
|
|
687
|
+
return diff(diagramGeometry(builtPageA), diagramGeometry(builtPageB));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export {
|
|
691
|
+
parseProcess,
|
|
692
|
+
buildProcess,
|
|
693
|
+
parseDiagram,
|
|
694
|
+
normalizeProcessTree,
|
|
695
|
+
compareProcess,
|
|
696
|
+
compareDiagram,
|
|
697
|
+
diff,
|
|
698
|
+
};
|