Skip to content

Commit

Permalink
feat(parser): let configure XML attribute processing (#2704)
Browse files Browse the repository at this point in the history
Remove the 'entities' dependency.
By default, there is no need to be able to decode all entities as most
of the time, only 'basic' entities are generally used in BPMN sources.
Instead, `bpmn-visualization` now provides a small set of entities
transform.

If there is a need to transform more entities, a new Parser option is
now available to let application perform more processing.
  • Loading branch information
tbouffard authored May 25, 2023
1 parent 0ed2ad1 commit 7f5ac76
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 18 deletions.
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

0 comments on commit 7f5ac76

Please sign in to comment.