Skip to content
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
12 changes: 11 additions & 1 deletion docs/contributors/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Guidelines for Contributors and Development

General information are available in the [Contributing Guide](../../CONTRIBUTING.md).
General information is available in the [Contributing Guide](../../CONTRIBUTING.md).

Here are some tips to help during development.

Expand All @@ -21,6 +21,16 @@ Here are some tips to help during development.
- [mxGraph integration](mxgraph-integration.md)
- [mxGraph version bump](mxgraph-version-bump.md)

## [Architecture Decision Records](./adr/)
**Purpose:** Document significant architectural decisions made in this project.

ADRs capture the context, decision, and consequences of important technical choices. They serve as a historical record to help current and future team members understand why certain approaches were chosen.

**When to read:** When you need to understand the rationale behind a system's design or when proposing changes to existing architecture.

**Existing ADRs:**
- [001 — BPMN Extensions Management](./adr/001-bpmn-extensions-management.md)

## Misc
- [Documentation guidelines](documentation-guidelines.md)
- [For the maintainers](maintainers.md): in particular, how to release a new version.
Expand Down
315 changes: 315 additions & 0 deletions docs/contributors/adr/001-bpmn-extensions-management.md

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions src/component/extension/bpmn-in-color/parsing-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copyright 2026 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 { Edge } from '../../../model/bpmn/internal/edge/edge';
import type Shape from '../../../model/bpmn/internal/shape/Shape';
import type { BPMNEdge, BPMNLabel, BPMNShape } from '../../../model/bpmn/json/bpmndi';
import type { ParsingExtensionPoint } from '../extension-points';

import './types';

function setColorExtensionsOnShape(shape: Shape, bpmnShape: BPMNShape): void {
const fillColor = bpmnShape['background-color'] ?? bpmnShape.fill;
fillColor && (shape.extensions.fillColor = fillColor);
const strokeColor = bpmnShape['border-color'] ?? bpmnShape.stroke;
strokeColor && (shape.extensions.strokeColor = strokeColor);
}

function setColorExtensionsOnLabel(label: { extensions: { color?: string } } | undefined, bpmnLabel: string | BPMNLabel | undefined): void {
if (label && bpmnLabel && typeof bpmnLabel === 'object' && bpmnLabel.color) {
label.extensions.color = bpmnLabel.color;
}
}

function setColorExtensionsOnEdge(edge: Edge, bpmnEdge: BPMNEdge): void {
const strokeColor = bpmnEdge['border-color'] ?? bpmnEdge.stroke;
strokeColor && (edge.extensions.strokeColor = strokeColor);
}

export const bpmnInColorParsingExtension: ParsingExtensionPoint = {
onShapeDeserialized(shape: Shape, bpmnShape: BPMNShape): void {
setColorExtensionsOnShape(shape, bpmnShape);
setColorExtensionsOnLabel(shape.label, bpmnShape.BPMNLabel);
},
onEdgeDeserialized(edge: Edge, bpmnEdge: BPMNEdge): void {
setColorExtensionsOnEdge(edge, bpmnEdge);
setColorExtensionsOnLabel(edge.label, bpmnEdge.BPMNLabel);
},
hasLabelExtensionData(bpmnLabel: unknown): boolean {
return typeof bpmnLabel === 'object' && bpmnLabel !== null && 'color' in bpmnLabel;
},
};
53 changes: 53 additions & 0 deletions src/component/extension/bpmn-in-color/style-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Copyright 2026 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 { Edge } from '../../../model/bpmn/internal/edge/edge';
import type Label from '../../../model/bpmn/internal/Label';
import type Shape from '../../../model/bpmn/internal/shape/Shape';
import type { StyleExtensionPoint } from '../extension-points';

import './types';

import { ShapeUtil } from '../../../model/bpmn/internal';
import { mxConstants } from '../../mxgraph/initializer';

function enrichLabelStyle(label: Label | undefined, styleValues: Map<string, string | number>): void {
const color = label?.extensions.color;
color && styleValues.set(mxConstants.STYLE_FONTCOLOR, color);
}

export const bpmnInColorStyleExtension: StyleExtensionPoint = {
enrichShapeStyle(shape: Shape, styleValues: Map<string, string | number>): void {
const extensions = shape.extensions;
const fillColor = extensions.fillColor;
if (fillColor) {
styleValues.set(mxConstants.STYLE_FILLCOLOR, fillColor);
if (ShapeUtil.isPoolOrLane(shape.bpmnElement.kind)) {
styleValues.set(mxConstants.STYLE_SWIMLANE_FILLCOLOR, fillColor);
}
}
extensions.strokeColor && styleValues.set(mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
enrichLabelStyle(shape.label, styleValues);
},
enrichEdgeStyle(edge: Edge, styleValues: Map<string, string | number>): void {
const extensions = edge.extensions;
extensions.strokeColor && styleValues.set(mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
enrichLabelStyle(edge.label, styleValues);
},
enrichMessageFlowIconStyle(edge: Edge, styleValues: Map<string, string | number>): void {
edge.extensions.strokeColor && styleValues.set(mxConstants.STYLE_STROKECOLOR, edge.extensions.strokeColor);
},
};
69 changes: 69 additions & 0 deletions src/component/extension/bpmn-in-color/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright 2026 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.
*/

// Marks this file as an ES module so the `declare module` blocks below are treated as module augmentation
// (with relative paths) rather than ambient module declarations. Without this marker, TypeScript reports
// "TS2436: Ambient module declaration cannot specify relative module names."
//
// `export {}` is preferred over an empty `import type {} from '...'` because it survives
// `verbatimModuleSyntax: true`, which would otherwise erase empty type-only imports and break compilation.
export {};

// Extend BPMN JSON diagram types with color properties from the BPMN in Color specification.
declare module '../../../model/bpmn/json/bpmndi' {
interface BPMNShape {
/** Fill color of the shape, as defined by the BPMN in Color specification. */
'background-color'?: string;
/** Fill color of the shape, as defined by the bpmn.io color specification. Used as fallback when {@link background-color} is not set. */
fill?: string;
/** Stroke (border) color of the shape, as defined by the BPMN in Color specification. */
'border-color'?: string;
/** Stroke (border) color of the shape, as defined by the bpmn.io color specification. Used as fallback when {@link border-color} is not set. */
stroke?: string;
}

interface BPMNEdge {
/** Stroke color of the edge, as defined by the BPMN in Color specification. */
'border-color'?: string;
/** Stroke color of the edge, as defined by the bpmn.io color specification. Used as fallback when {@link border-color} is not set. */
stroke?: string;
}

interface BPMNLabel {
/** Font color of the label, as defined by the BPMN in Color and in the bpmn.io color specifications. */
color?: string;
}
}

// Extend internal model types with color properties to model data coming from the BPMN in Color specification.
declare module '../../../model/bpmn/internal/types' {
interface ShapeExtensions {
/** Fill color of the shape, as a CSS color string. */
fillColor?: string;
/** Stroke (border) color of the shape, as a CSS color string. */
strokeColor?: string;
}

interface EdgeExtensions {
/** Stroke color of the edge, as a CSS color string. */
strokeColor?: string;
}

interface LabelExtensions {
/** Font color of the label, as a CSS color string. */
color?: string;
}
}
91 changes: 91 additions & 0 deletions src/component/extension/extension-points.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2026 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 { Edge } from '../../model/bpmn/internal/edge/edge';
import type Shape from '../../model/bpmn/internal/shape/Shape';
import type { BPMNEdge, BPMNShape } from '../../model/bpmn/json/bpmndi';

/**
* Extension point called during BPMN parsing to allow custom processing of deserialized elements.
*
* @since 0.48.0
* @internal
*/
export interface ParsingExtensionPoint {
/**
* Called after a shape has been deserialized from the BPMN XML.
*
* @param shape The internal shape model built from the BPMN semantic and diagram data.
* @param bpmnShape The raw BPMN diagram shape from the XML.
*/
onShapeDeserialized?(shape: Shape, bpmnShape: BPMNShape): void;

/**
* Called after an edge has been deserialized from the BPMN XML.
*
* @param edge The internal edge model built from the BPMN semantic and diagram data.
* @param bpmnEdge The raw BPMN diagram edge from the XML.
*/
onEdgeDeserialized?(edge: Edge, bpmnEdge: BPMNEdge): void;

// We have to rework label handling — currently uses hasLabelExtensionData which is a workaround to ensure
// Labels are created when extensions need them (e.g., color). Find a more generic approach.
// Current issue: labels set to a shape or edge are immutable, so extensions cannot add a label if not already present. hasLabelExtensionData is used to check if a label should be created for the shape or edge during deserialization.
// See ADR for more details
/**
* Returns `true` if the given BPMN label data requires a label to be created during deserialization.
*
* This is a workaround: labels are immutable once set on a shape or edge, so extensions that need a label
* (e.g., to apply color) must signal that requirement here so the label is created upfront.
*
* @param bpmnLabel The raw label data from the BPMN diagram.
*/
hasLabelExtensionData?(bpmnLabel: unknown): boolean;
}

/**
* Extension point for enriching the computed style of BPMN elements before they are rendered.
*
* @since 0.48.0
* @internal
*/
export interface StyleExtensionPoint {
/**
* Enriches the style values computed for a shape before rendering.
*
* @param shape The internal shape model.
* @param styleValues The mutable map of style key/value pairs to enrich.
*/
enrichShapeStyle?(shape: Shape, styleValues: Map<string, string | number>): void;

/**
* Enriches the style values computed for an edge before rendering.
*
* @param edge The internal edge model.
* @param styleValues The mutable map of style key/value pairs to enrich.
*/
enrichEdgeStyle?(edge: Edge, styleValues: Map<string, string | number>): void;

/**
* Enriches the style values computed for the message flow icon of an edge before rendering.
*
* The message flow icon is a distinct visual element painted on a message flow edge.
*
* @param edge The internal edge model carrying the message flow icon.
* @param styleValues The mutable map of style key/value pairs to enrich.
*/
enrichMessageFlowIconStyle?(edge: Edge, styleValues: Map<string, string | number>): void;
}
43 changes: 15 additions & 28 deletions src/component/mxgraph/renderer/StyleComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import type Bounds from '../../../model/bpmn/internal/Bounds';
import type { Edge } from '../../../model/bpmn/internal/edge/edge';
import type { Font } from '../../../model/bpmn/internal/Label';
import type { StyleExtensionPoint } from '../../extension/extension-points';
import type { RendererOptions } from '../../options';

import { MessageVisibleKind, ShapeBpmnCallActivityKind, ShapeBpmnElementKind, ShapeBpmnMarkerKind, ShapeUtil } from '../../../model/bpmn/internal';
Expand All @@ -31,19 +32,23 @@ import {
ShapeBpmnStartEvent,
ShapeBpmnSubProcess,
} from '../../../model/bpmn/internal/shape/ShapeBpmnElement';
import { bpmnInColorStyleExtension } from '../../extension/bpmn-in-color/style-extension';
import { mxConstants } from '../initializer';
import { BpmnStyleIdentifier } from '../style';

/**
* @internal
*/
export default class StyleComputer {
private readonly ignoreBpmnColors: boolean;
private readonly ignoreLabelStyles: boolean;
private readonly styleExtensions: StyleExtensionPoint[];

constructor(options?: RendererOptions) {
this.ignoreBpmnColors = options?.ignoreBpmnColors ?? true;
this.ignoreLabelStyles = options?.ignoreLabelStyles ?? false;
// The extension list is hardcoded on purpose: the extension mechanism is currently introduced internally
// only; external injection (a public `bpmnExtensions` option) is deferred. See ADR 001 in
// docs/contributors/adr/, section "Refactoring scope vs. follow-up work".
this.styleExtensions = (options?.ignoreBpmnColors ?? true) ? [] : [bpmnInColorStyleExtension];
}

computeStyle(bpmnCell: Shape | Edge, labelBounds: Bounds): string {
Expand Down Expand Up @@ -82,28 +87,15 @@ export default class StyleComputer {
styleValues.set(BpmnStyleIdentifier.EVENT_BASED_GATEWAY_KIND, String(bpmnElement.gatewayKind));
}

if (!this.ignoreBpmnColors) {
const extensions = shape.extensions;
const fillColor = extensions.fillColor;
if (fillColor) {
styleValues.set(mxConstants.STYLE_FILLCOLOR, fillColor);
if (ShapeUtil.isPoolOrLane(bpmnElement.kind)) {
styleValues.set(mxConstants.STYLE_SWIMLANE_FILLCOLOR, fillColor);
}
}
extensions.strokeColor && styleValues.set(mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
}
for (const extension of this.styleExtensions) extension.enrichShapeStyle?.(shape, styleValues);

return styleValues;
}

private computeEdgeStyleValues(edge: Edge): Map<string, string | number> {
const styleValues = new Map<string, string | number>();

if (!this.ignoreBpmnColors) {
const extensions = edge.extensions;
extensions.strokeColor && styleValues.set(mxConstants.STYLE_STROKECOLOR, extensions.strokeColor);
}
for (const extension of this.styleExtensions) extension.enrichEdgeStyle?.(edge, styleValues);

return styleValues;
}
Expand All @@ -118,22 +110,17 @@ export default class StyleComputer {
styleValues.set(mxConstants.STYLE_FONTSTYLE, getFontStyleValue(font));
}

if (!this.ignoreBpmnColors) {
const extensions = bpmnCell.label?.extensions;
extensions?.color && styleValues.set(mxConstants.STYLE_FONTCOLOR, extensions.color);
}

return styleValues;
}

computeMessageFlowIconStyle(edge: Edge): string {
const styleValues: [string, string][] = [];
styleValues.push(['shape', BpmnStyleIdentifier.MESSAGE_FLOW_ICON], [BpmnStyleIdentifier.IS_INITIATING, String(edge.messageVisibleKind === MessageVisibleKind.INITIATING)]);
if (!this.ignoreBpmnColors) {
edge.extensions.strokeColor && styleValues.push([mxConstants.STYLE_STROKECOLOR, edge.extensions.strokeColor]);
}
const styleValues = new Map<string, string | number>();
styleValues.set('shape', BpmnStyleIdentifier.MESSAGE_FLOW_ICON);
styleValues.set(BpmnStyleIdentifier.IS_INITIATING, String(edge.messageVisibleKind === MessageVisibleKind.INITIATING));

for (const extension of this.styleExtensions) extension.enrichMessageFlowIconStyle?.(edge, styleValues);

return toArrayOfMxGraphStyleEntries(styleValues).join(';');
return toArrayOfMxGraphStyleEntries([...styleValues]).join(';');
}
}

Expand Down
Loading