Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): let configure XML attribute processing #2704

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
},
"dependencies": {
"@typed-mxgraph/typed-mxgraph": "~1.0.7",
"entities": "~4.3.1",
"fast-xml-parser": "4.2.2",
"lodash-es": "~4.17.21",
"mxgraph": "4.2.2",
Expand Down
7 changes: 5 additions & 2 deletions src/component/BpmnVisualization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import GraphConfigurator from './mxgraph/GraphConfigurator';
import { newBpmnRenderer } from './mxgraph/BpmnRenderer';
import { newBpmnParser } from './parser/BpmnParser';
import type { BpmnGraph } from './mxgraph/BpmnGraph';
import type { GlobalOptions, LoadOptions } from './options';
import type { GlobalOptions, LoadOptions, ParserOptions } from './options';
import type { BpmnElementsRegistry } from './registry';
import { newBpmnElementsRegistry } from './registry/bpmn-elements-registry';
import { BpmnModelRegistry } from './registry/bpmn-model-registry';
Expand Down Expand Up @@ -67,6 +67,8 @@ export class BpmnVisualization {

private readonly bpmnModelRegistry: BpmnModelRegistry;

private readonly parserOptions: ParserOptions;

constructor(options: GlobalOptions) {
// mxgraph configuration
const configurator = new GraphConfigurator(htmlElement(options?.container));
Expand All @@ -75,6 +77,7 @@ export class BpmnVisualization {
this.navigation = new Navigation(this.graph);
this.bpmnModelRegistry = new BpmnModelRegistry();
this.bpmnElementsRegistry = newBpmnElementsRegistry(this.bpmnModelRegistry, this.graph);
this.parserOptions = options?.parser;
}

/**
Expand All @@ -84,7 +87,7 @@ export class BpmnVisualization {
* @throws `Error` when loading fails. This is generally due to a parsing error caused by a malformed BPMN content
*/
load(xml: string, options?: LoadOptions): void {
const bpmnModel = newBpmnParser().parse(xml);
const bpmnModel = newBpmnParser(this.parserOptions).parse(xml);
const renderedModel = this.bpmnModelRegistry.load(bpmnModel, options?.modelFilter);
newBpmnRenderer(this.graph).render(renderedModel, options?.fit);
}
Expand Down
25 changes: 25 additions & 0 deletions src/component/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface GlobalOptions {
container: string | HTMLElement;
/** Configure the BPMN diagram navigation (panning and zoom). */
navigation?: NavigationConfiguration;
/** Configure the BPMN parser. */
parser?: ParserOptions;
}

/**
Expand Down Expand Up @@ -155,3 +157,26 @@ export enum ZoomType {
In = 'in',
Out = 'out',
}

/**
* Configure the BPMN parser.
* @category Initialization & Configuration
*/
export type ParserOptions = {
/**
* Apply additional processing to the XML attributes in the BPMN source.
*
* When defined, this function is called after the `bpmn-visualization` attribute processing.
* You can use it to perform extra entities decoding. This can be done by using libraries like {@link https://www.npmjs.com/package/entities}.
* ```ts
* import { decodeXML } from 'entities';
* const parserOptions: ParserOptions = {
* parser: {
* additionalXmlAttributeProcessor: (val: string) => { return decodeXML(val) }
* }
* }
* ```
* @param val the value of the 'name' attribute to be processed.
*/
additionalXmlAttributeProcessor?: (val: string) => string;
};
5 changes: 3 additions & 2 deletions src/component/parser/BpmnParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import BpmnXmlParser from './xml/BpmnXmlParser';
import type BpmnJsonParser from './json/BpmnJsonParser';
import { newBpmnJsonParser } from './json/BpmnJsonParser';
import { ParsingMessageCollector } from './parsing-messages';
import type { ParserOptions } from '../options';

/**
* @internal
Expand All @@ -35,6 +36,6 @@ class BpmnParser {
/**
* @internal
*/
export function newBpmnParser(): BpmnParser {
return new BpmnParser(newBpmnJsonParser(new ParsingMessageCollector()), new BpmnXmlParser());
export function newBpmnParser(options?: ParserOptions): BpmnParser {
return new BpmnParser(newBpmnJsonParser(new ParsingMessageCollector()), new BpmnXmlParser(options));
}
36 changes: 33 additions & 3 deletions src/component/parser/xml/BpmnXmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,47 @@ limitations under the License.
*/

import { XMLParser, type X2jOptions } from 'fast-xml-parser';
import { decodeXML } from 'entities';
import type { BpmnJsonModel } from '../../../model/bpmn/json/BPMN20';
import type { ParserOptions } from '../../options';

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: '"' },
];

/**
* @internal
*/
export type XmlParserOptions = Pick<ParserOptions, 'additionalXmlAttributeProcessor'>;

/**
* Parse bpmn xml source
* @internal
*/
export default class BpmnXmlParser {
private x2jOptions: Partial<X2jOptions> = {
private readonly x2jOptions: Partial<X2jOptions> = {
attributeNamePrefix: '', // default to '@_'
removeNSPrefix: true,
ignoreAttributes: false,
parseAttributeValue: true, // ensure numbers are parsed as number, not as string
// entities management
processEntities: false, // If you don't have entities in your XML document then it is recommended to disable it for better performance.
attributeValueProcessor: (_name: string, val: string) => {
return decodeXML(val);
return this.processAttribute(val);
},
};
private xmlParser: XMLParser = new XMLParser(this.x2jOptions);

constructor(private options?: XmlParserOptions) {}

parse(xml: string): BpmnJsonModel {
let model: BpmnJsonModel;
try {
Expand All @@ -52,4 +72,14 @@ export default class BpmnXmlParser {
}
return model;
}

private processAttribute(val: string): string {
for (const replacement of entitiesReplacements) {
val = val.replace(replacement.regex, replacement.val);
}
if (this.options?.additionalXmlAttributeProcessor) {
val = this.options.additionalXmlAttributeProcessor(val);
}
return val;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<semantic:startEvent name="Start Event &#10;(Main) with &unknown; entity" id="_93c466ab-b271-4376-a427-f4c353d55ce8">
<semantic:outgoing>_e16564d7-0c4c-413e-95f6-f668a3f851fb</semantic:outgoing>
</semantic:startEvent>
<semantic:task completionQuantity="1" isForCompensation="false" startQuantity="1" name="Task 1" id="_ec59e164-68b4-4f94-98de-ffb1c58a84af &#9824;">
<semantic:task completionQuantity="1" isForCompensation="false" startQuantity="1" name="Task 1" id="&lt;_ec59e164-68b4-4f94-98de-ffb1c58a84af&gt;">
<semantic:incoming>_e16564d7-0c4c-413e-95f6-f668a3f851fb</semantic:incoming>
<semantic:outgoing>_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599</semantic:outgoing>
</semantic:task>
Expand All @@ -19,8 +19,8 @@
<semantic:endEvent name="End Event" id="_a47df184-085b-49f7-bb82-031c84625821">
<semantic:incoming>_8e8fe679-eb3b-4c43-a4d6-891e7087ff80</semantic:incoming>
</semantic:endEvent>
<semantic:sequenceFlow sourceRef="_93c466ab-b271-4376-a427-f4c353d55ce8" targetRef="_ec59e164-68b4-4f94-98de-ffb1c58a84af" name="" id="_e16564d7-0c4c-413e-95f6-f668a3f851fb"/>
<semantic:sequenceFlow sourceRef="_ec59e164-68b4-4f94-98de-ffb1c58a84af" targetRef="_820c21c0-45f3-473b-813f-06381cc637cd" name="" id="_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599"/>
<semantic:sequenceFlow sourceRef="_93c466ab-b271-4376-a427-f4c353d55ce8" targetRef="&lt;_ec59e164-68b4-4f94-98de-ffb1c58a84af&gt;" name="" id="_e16564d7-0c4c-413e-95f6-f668a3f851fb"/>
<semantic:sequenceFlow sourceRef="&lt;_ec59e164-68b4-4f94-98de-ffb1c58a84af&gt;" targetRef="_820c21c0-45f3-473b-813f-06381cc637cd" name="" id="_d77dd5ec-e4e7-420e-bbe7-8ac9cd1df599"/>
<semantic:sequenceFlow sourceRef="_820c21c0-45f3-473b-813f-06381cc637cd" targetRef="_e70a6fcb-913c-4a7b-a65d-e83adc73d69c" name="" id="_2aa47410-1b0e-4f8b-ad54-d6f798080cb4"/>
<semantic:sequenceFlow sourceRef="_e70a6fcb-913c-4a7b-a65d-e83adc73d69c" targetRef="_a47df184-085b-49f7-bb82-031c84625821" name="" id="_8e8fe679-eb3b-4c43-a4d6-891e7087ff80"/>
</semantic:process>
Expand All @@ -32,7 +32,7 @@
<dc:Bounds height="12.804751171875008" width="94.93333333333335" x="153.67766754457273" y="371.3333333333333"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_ec59e164-68b4-4f94-98de-ffb1c58a84af" id="S1373649849859__ec59e164-68b4-4f94-98de-ffb1c58a84af">
<bpmndi:BPMNShape bpmnElement="&lt;_ec59e164-68b4-4f94-98de-ffb1c58a84af&gt;" id="S1373649849859__ec59e164-68b4-4f94-98de-ffb1c58a84af">
<dc:Bounds height="68.0" width="83.0" x="258.0" y="317.0"/>
<bpmndi:BPMNLabel labelStyle="LS1373649849858">
<dc:Bounds height="12.804751171875008" width="72.48293963254594" x="263.3333333333333" y="344.5818763825664"/>
Expand Down
61 changes: 61 additions & 0 deletions test/integration/mxGraph.model.parsing.entities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Copyright 2023 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 { BpmnVisualization } from '@lib/component/BpmnVisualization';
import { readFileSync } from '@test/shared/file-helper';

const additionalXmlAttributeProcessor = (val: string): string => {
val = val.replace(/&#174;/g, '®');
val = val.replace(/&#9824;/g, '♠');
val = val.replace(/&#x00D8;/g, 'Ø');
val = val.replace(/&#10741;/g, '⧵');
return val;
};

describe('From BPMN diagram with entities in attributes', () => {
describe.each([false, true])('XML parser with additional attribute processor: %s', (useAdditionalXmlAttributeProcessor: boolean) => {
const bpmnVisualization = useAdditionalXmlAttributeProcessor
? new BpmnVisualization({ container: null, parser: { additionalXmlAttributeProcessor } })
: new BpmnVisualization(null);
bpmnVisualization.load(readFileSync('../fixtures/bpmn/xml-parsing/special/start-tasks-end_entities_in_attributes.bpmn'));

const expectElementLabel = (id: string): jest.JestMatchers<string> => {
const model = bpmnVisualization.graph.getModel();
const cell = model.getCell(id);
expect(cell).toBeDefined();
// eslint-disable-next-line jest/valid-expect -- util function
return expect(String(cell.getValue()));
};

test('start event', () => {
expectElementLabel('StartEvent_1').toBe(useAdditionalXmlAttributeProcessor ? '®Start Event 1 &reg;\nbuilt with ♠' : '&#174;Start Event 1 &reg;\nbuilt with &#9824;');
});
test('task', () => {
expectElementLabel('Activity_1').toBe(useAdditionalXmlAttributeProcessor ? 'Task 1&nbsp;or task 2&#x2215;3⧵4' : 'Task 1&nbsp;or task 2&#x2215;3&#10741;4');
});
test('end event', () => {
expectElementLabel('EndEvent_1').toBe(
useAdditionalXmlAttributeProcessor ? '&unknown; End Event & 1/2\\3 Ø \n &yen; / &#165;' : '&unknown; End Event & 1/2\\3 &#x00D8; \n &yen; / &#165;',
);
});
test('sequence flow 1', () => {
expectElementLabel('Flow_1').toBe('<Sequence> Flow 1&2');
});
test('sequence flow 2', () => {
expectElementLabel('Flow_2').toBe('Sequence \'Flow" 2');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ describe('Special parsing cases', () => {
expect(json).toMatchObject({
definitions: {
process: {
startEvent: { name: '®Start Event 1 &reg;\nbuilt with ' },
task: { name: 'Task 1&nbsp;or task 2∕3⧵4' },
endEvent: { name: '&unknown; End Event & 1/2\\3 Ø \n &yen; / ¥' },
startEvent: { name: '&#174;Start Event 1 &reg;\nbuilt with &#9824;' },
task: { name: 'Task 1&nbsp;or task 2&#x2215;3&#10741;4' },
endEvent: { name: '&unknown; End Event & 1/2\\3 &#x00D8; \n &yen; / &#165;' },
sequenceFlow: [{ name: '<Sequence> Flow 1&2' }, { name: 'Sequence \'Flow" 2' }],
},
BPMNDiagram: expect.anything(),
Expand Down
2 changes: 1 addition & 1 deletion test/unit/component/parser/xml/BpmnXmlParser.miwg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe('parse bpmn as xml for MIWG', () => {
definitions: {
process: {
startEvent: { name: 'Start Event \n(Main) with &unknown; entity' },
task: [{ id: '_ec59e164-68b4-4f94-98de-ffb1c58a84af' }, expect.anything(), expect.anything()],
task: [{ id: '<_ec59e164-68b4-4f94-98de-ffb1c58a84af>' }, expect.anything(), expect.anything()],
},
BPMNDiagram: expect.anything(),
},
Expand Down