-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathBpmnXmlParser.ts
More file actions
124 lines (105 loc) · 5.05 KB
/
BpmnXmlParser.ts
File metadata and controls
124 lines (105 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/*
Copyright 2020 Bonitasoft S.A.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import type { BpmnJsonModel } from '../../../model/bpmn/json/bpmn20';
import type { ParserOptions } from '../../options';
import { XMLParser, type X2jOptions } from 'fast-xml-parser';
type Replacement = {
regex: RegExp;
val: string;
};
const entitiesReplacements: Replacement[] = [
{ regex: /&(amp|#38|#x26);/g, val: '&' },
{ regex: /&(apos|#39|#x27);/g, val: "'" },
{ regex: /&#(xa|xA|10);/g, val: '\n' },
{ regex: /&(gt|#62|#x3e|#x3E);/g, val: '>' },
{ regex: /&(lt|#60|#x3c|#x3C);/g, val: '<' },
{ regex: /&(quot|#34|#x22);/g, val: '"' },
];
const nodesWithNumericAttributes = new Set(
['BPMNShape.Bounds', 'BPMNShape.BPMNLabel.Bounds', 'BPMNEdge.BPMNLabel.Bounds', 'BPMNEdge.waypoint'].map(element => `definitions.BPMNDiagram.BPMNPlane.${element}`),
);
const numericAttributes = new Set(['x', 'y', 'width', 'height']);
const isNumeric = (attributeName: string, nodePath: string): boolean => {
return nodesWithNumericAttributes.has(nodePath) && numericAttributes.has(attributeName);
};
/**
* @internal
*/
export type XmlParserOptions = Pick<ParserOptions, 'additionalXmlAttributeProcessor'>;
/**
* Parse bpmn xml source
* @internal
*/
export default class BpmnXmlParser {
private readonly x2jOptions: Partial<X2jOptions> = {
attributeNamePrefix: '', // default to '@_'
removeNSPrefix: true,
ignoreAttributes: false,
/**
* Ensure numbers and booleans are parsed with their related type and not as string.
* See https://github.com/NaturalIntelligence/fast-xml-parser/blob/v4.3.4/docs/v4/2.XMLparseOptions.md#parseattributevalue
*/
parseAttributeValue: true,
/**
* Entities management. The recommendation is: "If you don't have entities in your XML document then it is recommended to disable it for better performance."
* See https://github.com/NaturalIntelligence/fast-xml-parser/blob/v4.3.4/docs/v4/2.XMLparseOptions.md#processentities
*/
processEntities: false,
// Use Matcher object (jPath: false) to get paths without namespace prefixes via toString('.', false).
// With jPath: true (default), toString() includes namespace prefixes (e.g. "bpmn:definitions.bpmndi:BPMNDiagram")
// which don't match our expected paths in nodesWithNumericAttributes.
jPath: false,
// See https://github.com/NaturalIntelligence/fast-xml-parser/blob/v5.5.7/docs/v4%2C%20v5/2.XMLparseOptions.md#attributevalueprocessor
attributeValueProcessor: (attributeName: string, attributeValue: string, nodePathOrMatcher: unknown): unknown => {
// nodePathOrMatcher is a Matcher instance (jPath: false). Get path without namespace prefixes.
const nodePath =
typeof nodePathOrMatcher === 'object' && nodePathOrMatcher !== null
? String((nodePathOrMatcher as { toString(separator?: string, includeNs?: boolean): string }).toString('.', false))
: String(nodePathOrMatcher);
if (isNumeric(attributeName, nodePath)) {
// The strnum lib used by fast-xml-parser is not able to parse all numbers
// The only available options are https://github.com/NaturalIntelligence/fast-xml-parser/blob/v4.3.4/docs/v4/2.XMLparseOptions.md#numberparseoptions
// This is a fix for https://github.com/process-analytics/bpmn-visualization-js/issues/2857
return Number(attributeValue);
}
return this.processAttribute(attributeValue);
},
};
private readonly xmlParser: XMLParser = new XMLParser(this.x2jOptions);
constructor(private readonly options?: XmlParserOptions) {}
parse(xml: string): BpmnJsonModel {
let model: BpmnJsonModel;
try {
model = this.xmlParser.parse(xml);
} catch {
throw new Error('XML parsing failed. Invalid BPMN source.');
}
if (!model.definitions) {
// We currently don't validate the xml, so we don't detect xml validation error
// if 'definitions' is undefined, there is an Error later in the parsing code without explicit information
// So for now, throw a generic error that better explains the problem.
// See https://github.com/process-analytics/bpmn-visualization-js/issues/21 for improvement
throw new Error(`XML parsing failed. Unable to retrieve 'definitions' from the BPMN source.`);
}
return model;
}
private processAttribute(value: string): string {
for (const replacement of entitiesReplacements) {
value = value.replace(replacement.regex, replacement.val);
}
if (this.options?.additionalXmlAttributeProcessor) {
value = this.options.additionalXmlAttributeProcessor(value);
}
return value;
}
}