diff --git a/packages/core/__tests__/serialization/codecs/mxgraph/utils.test.ts b/packages/core/__tests__/serialization/codecs/mxgraph/utils.test.ts new file mode 100644 index 000000000..d428a3cc5 --- /dev/null +++ b/packages/core/__tests__/serialization/codecs/mxgraph/utils.test.ts @@ -0,0 +1,81 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 { describe, expect, test } from '@jest/globals'; +import { convertStyleFromString } from '../../../../src/serialization/codecs/mxGraph/utils'; +import { CellStyle } from '../../../../src'; + +describe('convertStyleFromString', () => { + test('Basic', () => { + // adapted from https://github.com/maxGraph/maxGraph/issues/221 + expect( + convertStyleFromString( + 'rounded=0;whiteSpace=wrap;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=27' + ) + ).toEqual({ + rounded: 0, // FIX should be true + whiteSpace: 'wrap', + fillColor: '#dae8fc', + strokeColor: '#6c8ebf', + fontStyle: 1, + fontSize: 27, + }); + }); + + test('With leading ;', () => { + // To update when implementing https://github.com/maxGraph/maxGraph/issues/154 + expect(convertStyleFromString(';arcSize=4;endSize=5;')).toEqual({ + arcSize: 4, + endSize: 5, + }); + }); + + test('With trailing ;', () => { + // from https://github.com/maxGraph/maxGraph/issues/102#issuecomment-1225577772 + expect( + convertStyleFromString( + 'rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;dashed=1;' + ) + ).toEqual({ + rounded: 0, // FIX should be true + whiteSpace: 'wrap', + html: 1, // custom draw.io + fillColor: '#E6E6E6', + dashed: 1, + }); + }); + + // manage base name style (no = at the begining and in the middle) + test('With base name style', () => { + expect( + convertStyleFromString( + 'rectangle;fontColor=yellow;customRectangle;gradientColor=white;' + ) + ).toEqual({ + baseStyleNames: ['rectangle', 'customRectangle'], + fontColor: 'yellow', + gradientColor: 'white', + }); + }); + + // renamed properties (see migration guide) + test('With renamed properties', () => { + // @ts-ignore + expect(convertStyleFromString('autosize=1')).toEqual({ + autoSize: 1, // FIX should be true + }); + }); +}); diff --git a/packages/core/__tests__/serialization/serialization.xml.mxGraph.test.ts b/packages/core/__tests__/serialization/serialization.xml.mxGraph.test.ts new file mode 100644 index 000000000..4c0929780 --- /dev/null +++ b/packages/core/__tests__/serialization/serialization.xml.mxGraph.test.ts @@ -0,0 +1,138 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 { describe, test } from '@jest/globals'; +import { ModelChecker } from './utils'; +import { + CodecRegistry, + Geometry, + GraphDataModel, + ModelXmlSerializer, + Point, +} from '../../src'; + +describe('import mxGraph model', () => { + test('Model with geometry', () => { + const mxGraphModelAsXml = ` + + + + + + + + + + + + + + + + + + + `; + + const model = new GraphDataModel(); + new ModelXmlSerializer(model).import(mxGraphModelAsXml); + + const modelChecker = new ModelChecker(model); + + modelChecker.checkRootCells(); + modelChecker.checkCellsCount(5); + + modelChecker.expectIsVertex(model.getCell('2'), 'Vertex #2', { + geometry: new Geometry(380, 20, 140, 30), + }); + + modelChecker.expectIsVertex(model.getCell('3'), 'Vertex #3', { + geometry: new Geometry(200, 80, 380, 30), + }); + + const edgeGeometry = new Geometry(); + edgeGeometry.points = [new Point(420, 60)]; + modelChecker.expectIsEdge(model.getCell('7'), 'Edge #7', { + geometry: edgeGeometry, + }); + }); + + test('Model with style', () => { + const xmlWithStyleAttribute = ` + + + + + + +`; + + const model = new GraphDataModel(); + new ModelXmlSerializer(model).import(xmlWithStyleAttribute); + + const modelChecker = new ModelChecker(model); + + modelChecker.checkRootCells(); + modelChecker.checkCellsCount(3); + modelChecker.expectIsVertex(model.getCell('2'), 'Vertex with style', { + style: { + // @ts-ignore FIX should be true + dashed: 1, + fillColor: '#E6E6E6', + html: 1, + // @ts-ignore FIX should be false + rounded: 0, + whiteSpace: 'wrap', + }, + }); + }); +}); + +describe('import model from draw.io', () => { + test('from https://github.com/maxGraph/maxGraph/issues/221', () => { + const model = new GraphDataModel(); + new ModelXmlSerializer(model) + .import(` + + + + + + + +`); + + const modelChecker = new ModelChecker(model); + modelChecker.checkRootCells(); + modelChecker.checkCellsCount(3); + modelChecker.expectIsVertex(model.getCell('2Ija7sB8CSz23ppRtK8d-5'), 'PostgreSQL', { + geometry: new Geometry(650, 430, 210, 80), + style: { + fillColor: '#dae8fc', + fontSize: 27, + fontStyle: 1, + html: 1, + // @ts-ignore FIX should be false + rounded: 0, + strokeColor: '#6c8ebf', + whiteSpace: 'wrap', + }, + }); + }); +}); diff --git a/packages/core/__tests__/serialization/serialization.xml.test.ts b/packages/core/__tests__/serialization/serialization.xml.test.ts index 41aa189c2..c7fa2c64a 100644 --- a/packages/core/__tests__/serialization/serialization.xml.test.ts +++ b/packages/core/__tests__/serialization/serialization.xml.test.ts @@ -15,15 +15,9 @@ limitations under the License. */ import { describe, expect, test } from '@jest/globals'; +import { ModelChecker } from './utils'; import { createGraphWithoutContainer } from '../utils'; -import { - Cell, - type CellStyle, - Geometry, - GraphDataModel, - ModelXmlSerializer, - Point, -} from '../../src'; +import { Cell, Geometry, GraphDataModel, ModelXmlSerializer, Point } from '../../src'; // inspired by VertexMixin.createVertex const newVertex = (id: string, value: string) => { @@ -47,68 +41,6 @@ const getParent = (model: GraphDataModel) => { return model.getRoot()!.getChildAt(0); }; -type ExpectCellProperties = { - geometry?: Geometry; - style?: CellStyle; -}; - -/** - * Utility class to check the model after import. - */ -class ModelChecker { - constructor(private model: GraphDataModel) {} - - checkRootCells() { - const cell0 = this.model.getCell('0'); - expect(cell0).toBeDefined(); - expect(cell0).not.toBeNull(); - expect(cell0?.parent).toBeNull(); - - const cell1 = this.model.getCell('1'); - expect(cell1).toBeDefined(); - expect(cell1).not.toBeNull(); - expect(cell1?.parent).toBe(cell0); - } - - expectIsVertex(cell: Cell | null, value: string, properties?: ExpectCellProperties) { - this.checkCellBaseProperties(cell, value, properties); - if (!cell) return; // cannot occur, this is enforced by checkCellBaseProperties - expect(cell.edge).toEqual(false); - expect(cell.isEdge()).toBeFalsy(); - expect(cell.vertex).toEqual(1); // FIX should be set to true - expect(cell.isVertex()).toBeTruthy(); - } - - expectIsEdge( - cell: Cell | null, - value: string | null = null, - properties?: ExpectCellProperties - ) { - this.checkCellBaseProperties(cell, value, properties); - if (!cell) return; // cannot occur, this is enforced by checkCellBaseProperties - expect(cell.edge).toEqual(1); // FIX should be set to true - expect(cell.isEdge()).toBeTruthy(); - expect(cell.vertex).toEqual(false); - expect(cell.isVertex()).toBeFalsy(); - } - - private checkCellBaseProperties( - cell: Cell | null, - value: string | null, - properties?: ExpectCellProperties - ) { - expect(cell).toBeDefined(); - expect(cell).not.toBeNull(); - if (!cell) return; // cannot occur, see above - - expect(cell.value).toEqual(value); - expect(cell.getParent()?.id).toEqual('1'); // default parent id - - expect(cell.geometry).toEqual(properties?.geometry ?? null); - expect(cell.style).toEqual(properties?.style ?? {}); - } -} - // Adapted from https://github.com/maxGraph/maxGraph/issues/178 const xmlWithSingleVertex = ` @@ -201,6 +133,7 @@ describe('import before the export (reproduce https://github.com/maxGraph/maxGra const modelChecker = new ModelChecker(model); modelChecker.checkRootCells(); + modelChecker.checkCellsCount(3); modelChecker.expectIsVertex(model.getCell('B_#0'), 'rootNode', { geometry: new Geometry(100, 100, 100, 80), @@ -305,6 +238,7 @@ describe('import after export', () => { const modelChecker = new ModelChecker(model); modelChecker.checkRootCells(); + modelChecker.checkCellsCount(3); modelChecker.expectIsVertex(model.getCell('B_#0'), 'rootNode', { geometry: new Geometry(100, 100, 100, 80), diff --git a/packages/core/__tests__/serialization/utils.ts b/packages/core/__tests__/serialization/utils.ts new file mode 100644 index 000000000..c45694fb0 --- /dev/null +++ b/packages/core/__tests__/serialization/utils.ts @@ -0,0 +1,85 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 { expect } from '@jest/globals'; +import type { Cell, CellStyle, Geometry, GraphDataModel } from '../../src'; + +type ExpectCellProperties = { + geometry?: Geometry; + style?: CellStyle; +}; + +/** + * Utility class to check the content of GraphDataModel. + */ +export class ModelChecker { + constructor(private model: GraphDataModel) {} + + checkRootCells() { + const cell0 = this.model.getCell('0'); + expect(cell0).toBeDefined(); + expect(cell0).not.toBeNull(); + expect(cell0?.parent).toBeNull(); + + const cell1 = this.model.getCell('1'); + expect(cell1).toBeDefined(); + expect(cell1).not.toBeNull(); + expect(cell1?.parent).toBe(cell0); + } + + checkCellsCount(count: number) { + const cellIds = Object.getOwnPropertyNames(this.model.cells); + expect(cellIds).toHaveLength(count); + } + + expectIsVertex(cell: Cell | null, value: string, properties?: ExpectCellProperties) { + this.checkCellBaseProperties(cell, value, properties); + if (!cell) return; // cannot occur, this is enforced by checkCellBaseProperties + expect(cell.edge).toEqual(false); + expect(cell.isEdge()).toBeFalsy(); + expect(cell.vertex).toEqual(1); // FIX should be set to true + expect(cell.isVertex()).toBeTruthy(); + } + + expectIsEdge( + cell: Cell | null, + value: string | null = null, + properties?: ExpectCellProperties + ) { + this.checkCellBaseProperties(cell, value, properties); + if (!cell) return; // cannot occur, this is enforced by checkCellBaseProperties + expect(cell.edge).toEqual(1); // FIX should be set to true + expect(cell.isEdge()).toBeTruthy(); + expect(cell.vertex).toEqual(false); + expect(cell.isVertex()).toBeFalsy(); + } + + private checkCellBaseProperties( + cell: Cell | null, + value: string | null, + properties?: ExpectCellProperties + ) { + expect(cell).toBeDefined(); + expect(cell).not.toBeNull(); + if (!cell) return; // cannot occur, see above + + expect(cell.value).toEqual(value); + expect(cell.getParent()?.id).toEqual('1'); // default parent id + + expect(cell.geometry).toEqual(properties?.geometry ?? null); + expect(cell.style).toEqual(properties?.style ?? {}); + } +} diff --git a/packages/core/src/serialization/Codec.ts b/packages/core/src/serialization/Codec.ts index 142ee56c5..f64410f17 100644 --- a/packages/core/src/serialization/Codec.ts +++ b/packages/core/src/serialization/Codec.ts @@ -23,7 +23,6 @@ import Cell from '../view/cell/Cell'; import MaxLog from '../gui/MaxLog'; import { getFunctionName } from '../util/StringUtils'; import { importNode, isNode } from '../util/domUtils'; -import ObjectCodec from './ObjectCodec'; const createXmlDocument = () => { return document.implementation.createDocument('', '', null); @@ -415,35 +414,35 @@ class Codec { * and insertEdge on the parent and terminals, respectively. * Default is `true`. */ - decodeCell(node: Element, restoreStructures = true): Cell { - let cell = null; + decodeCell(node: Element, restoreStructures = true): Cell | null { + if (node?.nodeType !== NODETYPE.ELEMENT) { + return null; + } - if (node != null && node.nodeType === NODETYPE.ELEMENT) { - // Tries to find a codec for the given node name. If that does - // not return a codec then the node is the user object (an XML node - // that contains the mxCell, aka inversion). - let decoder = CodecRegistry.getCodec(node.nodeName); + // Tries to find a codec for the given node name. If that does + // not return a codec then the node is the user object (an XML node + // that contains the mxCell, aka inversion). + let decoder = CodecRegistry.getCodec(node.nodeName); - // Tries to find the codec for the cell inside the user object. - // This assumes all node names inside the user object are either - // not registered or they correspond to a class for cells. - if (!this.isCellCodec(decoder)) { - let child = node.firstChild as Element; + // Tries to find the codec for the cell inside the user object. + // This assumes all node names inside the user object are either + // not registered or they correspond to a class for cells. + if (!this.isCellCodec(decoder)) { + let child = node.firstChild as Element; - while (child != null && !this.isCellCodec(decoder)) { - decoder = CodecRegistry.getCodec(child.nodeName); - child = child.nextSibling as Element; - } + while (child != null && !this.isCellCodec(decoder)) { + decoder = CodecRegistry.getCodec(child.nodeName); + child = child.nextSibling as Element; } + } - if (!this.isCellCodec(decoder)) { - decoder = CodecRegistry.getCodec(Cell); - } - cell = decoder?.decode(this, node); + if (!this.isCellCodec(decoder)) { + decoder = CodecRegistry.getCodec(Cell); + } + const cell = decoder?.decode(this, node); - if (restoreStructures) { - this.insertIntoGraph(cell); - } + if (restoreStructures) { + this.insertIntoGraph(cell); } return cell; } diff --git a/packages/core/src/serialization/CodecRegistry.ts b/packages/core/src/serialization/CodecRegistry.ts index d3f2b6722..1b15b9847 100644 --- a/packages/core/src/serialization/CodecRegistry.ts +++ b/packages/core/src/serialization/CodecRegistry.ts @@ -54,17 +54,18 @@ class CodecRegistry { static aliases: { [key: string]: string | undefined } = {}; /** - * Registers a new codec and associates the name of the template constructor in the codec with the codec object. + * Registers a new codec and associates the name of the codec via {@link ObjectCodec.getName} with the codec object. * * @param codec ObjectCodec to be registered. + * @param registerAlias if `true`, register an alias if the codec name doesn't match the name of the constructor of {@link ObjectCodec.template}. */ - static register(codec: ObjectCodec): ObjectCodec { + static register(codec: ObjectCodec, registerAlias = true): ObjectCodec { if (codec != null) { const name = codec.getName(); CodecRegistry.codecs[name] = codec; const classname: string = codec.template.constructor.name; - if (classname !== name) { + if (registerAlias && classname !== name) { CodecRegistry.addAlias(classname, name); } } @@ -79,38 +80,59 @@ class CodecRegistry { } /** - * Returns a codec that handles objects that are constructed using the given constructor. + * Returns a codec that handles objects that are constructed using the given constructor or a codec registered under the provided name. * - * @param constructor_ JavaScript constructor function. + * When passing a name, the method first check if an alias exists for the name, and if so, it uses it to retrieve the codec. + * + * If there is no registered Codec, the method tries to register a new Codec using the provided constructor. + * + * @param constructorOrName JavaScript constructor function of the Codec or Codec name. */ - static getCodec(constructor_: any): ObjectCodec | null { + static getCodec(constructorOrName: any): ObjectCodec | null { + if (constructorOrName == null) { + return null; + } + let codec = null; - if (constructor_ != null) { - let { name } = constructor_; - const tmp = CodecRegistry.aliases[name]; + // Equivalent of calling import { getFunctionName } from '../util/StringUtils'; + let name = + typeof constructorOrName === 'string' ? constructorOrName : constructorOrName.name; - if (tmp != null) { - name = tmp; - } + const tmp = CodecRegistry.aliases[name]; + if (tmp != null) { + name = tmp; + } - codec = CodecRegistry.codecs[name] ?? null; + codec = CodecRegistry.codecs[name] ?? null; - // Registers a new default codec for the given constructor if no codec has been previously defined. - if (codec == null) { - try { - codec = new ObjectCodec(new constructor_()); - CodecRegistry.register(codec); - } catch (e) { - // ignore - } + // Registers a new default codec for the given constructor if no codec has been previously defined. + if (codec == null) { + try { + codec = new ObjectCodec(new constructorOrName()); + CodecRegistry.register(codec); + } catch (e) { + // ignore } } return codec; } + /** + * First try to get the codec by the name it is registered with. If it doesn't exist, use the alias eventually declared + * to get the codec. + * @param name the name of the codec that is willing to be retrieved. + */ static getCodecByName(name: string) { - return CodecRegistry.codecs[name] ?? null; + let codec = CodecRegistry.codecs[name]; + if (!codec) { + const alias = CodecRegistry.aliases[name]; + if (alias) { + codec = CodecRegistry.codecs[alias]; + } + } + + return codec ?? null; } } diff --git a/packages/core/src/serialization/ObjectCodec.ts b/packages/core/src/serialization/ObjectCodec.ts index 66a1f1287..91d55bf1f 100644 --- a/packages/core/src/serialization/ObjectCodec.ts +++ b/packages/core/src/serialization/ObjectCodec.ts @@ -765,7 +765,6 @@ class ObjectCodec { } if (!this.isExcluded(obj, name, value, false)) { - // MaxLog.debug(mxUtils.getFunctionName(obj.constructor)+'.'+name+'='+value); obj[name] = value; } } @@ -858,7 +857,6 @@ class ObjectCodec { } else { obj.push(value); } - // MaxLog.debug('Decoded '+mxUtils.getFunctionName(obj.constructor)+'.'+fieldname+': '+value); } } diff --git a/packages/core/src/serialization/codecs/ModelCodec.ts b/packages/core/src/serialization/codecs/ModelCodec.ts index ce545e8cd..541bd50e0 100644 --- a/packages/core/src/serialization/codecs/ModelCodec.ts +++ b/packages/core/src/serialization/codecs/ModelCodec.ts @@ -30,8 +30,8 @@ export class ModelCodec extends ObjectCodec { } /** - * Encodes the given {@link GraphDataModel} by writing a (flat) XML sequence of cell nodes as produced by the . - * The sequence is wrapped-up in a node with the name root. + * Encodes the given {@link GraphDataModel} by writing a (flat) XML sequence of cell nodes as produced by the {@link CellCodec}. + * The sequence is wrapped-up in a node with the name `root`. */ encodeObject(enc: any, obj: Cell, node: Element) { const rootNode = enc.document.createElement('root'); diff --git a/packages/core/src/serialization/codecs/index.ts b/packages/core/src/serialization/codecs/index.ts index 257f71799..338b3f140 100644 --- a/packages/core/src/serialization/codecs/index.ts +++ b/packages/core/src/serialization/codecs/index.ts @@ -23,3 +23,5 @@ export * from './ModelCodec'; export * from './RootChangeCodec'; export * from './StylesheetCodec'; export * from './TerminalChangeCodec'; +export * from './mxGraph/mxCellCodec'; +export * from './mxGraph/mxGeometryCodec'; diff --git a/packages/core/src/serialization/codecs/mxGraph/mxCellCodec.ts b/packages/core/src/serialization/codecs/mxGraph/mxCellCodec.ts new file mode 100644 index 000000000..51ba487ae --- /dev/null +++ b/packages/core/src/serialization/codecs/mxGraph/mxCellCodec.ts @@ -0,0 +1,37 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 { convertStyleFromString } from './utils'; +import { CellCodec } from '../CellCodec'; +import type Codec from '../../Codec'; + +/** + * CellCodec to support the legacy `mxGraph` format. + */ +export class mxCellCodec extends CellCodec { + getName(): string { + return 'mxCell'; + } + + decodeAttribute(dec: Codec, attr: any, obj?: any) { + const attributeNodeName = attr.nodeName; + if (obj && attributeNodeName == 'style') { + obj['style'] = convertStyleFromString(attr.value); + } else { + super.decodeAttribute(dec, attr, obj); + } + } +} diff --git a/packages/core/src/serialization/codecs/mxGraph/mxGeometryCodec.ts b/packages/core/src/serialization/codecs/mxGraph/mxGeometryCodec.ts new file mode 100644 index 000000000..28868e5db --- /dev/null +++ b/packages/core/src/serialization/codecs/mxGraph/mxGeometryCodec.ts @@ -0,0 +1,54 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 Codec from '../../Codec'; +import ObjectCodec from '../../ObjectCodec'; +import Geometry from '../../../view/geometry/Geometry'; +import Point from '../../../view/geometry/Point'; + +export class mxGeometryCodec extends ObjectCodec { + getName(): string { + return 'mxGeometry'; + } + + constructor() { + super(new Geometry()); + } + + afterDecode(dec: Codec, node: Element | null, obj?: any): any { + // Convert points to the right form + // input: [ { x: 420, y: 60 }, ... ] + // output: [ Point { _x: 420, _y: 60 }, ... ] + // + // In mxGraph XML, the points are modeled as Object, so there is no way to create an alias to do the decoding with a custom Codec. + // Then, it is easier to convert the values to Point objects after the whole decoding of the geometry + // + // + // + + const originalPoints = (obj as Geometry).points; + if (originalPoints) { + const points: Array = []; + for (const pointInput of originalPoints) { + const rawPoint = pointInput as { x: number; y: number }; + points.push(new Point(rawPoint.x, rawPoint.y)); + } + (obj as Geometry).points = points; + } + + return obj; + } +} diff --git a/packages/core/src/serialization/codecs/mxGraph/utils.ts b/packages/core/src/serialization/codecs/mxGraph/utils.ts new file mode 100644 index 000000000..9807d400d --- /dev/null +++ b/packages/core/src/serialization/codecs/mxGraph/utils.ts @@ -0,0 +1,56 @@ +/* +Copyright 2024-present The maxGraph project Contributors + +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 { isNumeric } from '../../../util/mathUtils'; +import type { CellStyle } from '../../../types'; + +// from mxGraph to maxGraph +const fieldMapping = new Map([['autosize', 'autoSize']]); + +export function convertStyleFromString(input: string) { + const style: CellStyle = {}; + + const elements = input + .split(';') + // filter empty key + .filter(([k]) => k); + for (const element of elements) { + if (!element.includes('=')) { + !style.baseStyleNames && (style.baseStyleNames = []); + style.baseStyleNames.push(element); + } else { + const [key, value] = element.split('='); + // @ts-ignore + style[fieldMapping.get(key) ?? key] = convertToNumericIfNeeded(value); + } + } + + return style; +} + +function convertToNumericIfNeeded(value: string): string | number { + // Adapted from ObjectCodec.convertAttributeFromXml + if (!isNumeric(value)) { + return value; + } + + let numericValue = parseFloat(value); + + if (Number.isNaN(numericValue) || !Number.isFinite(numericValue)) { + numericValue = 0; + } + return numericValue; +} diff --git a/packages/core/src/serialization/register.ts b/packages/core/src/serialization/register.ts index a6fe60157..37fb6f155 100644 --- a/packages/core/src/serialization/register.ts +++ b/packages/core/src/serialization/register.ts @@ -25,6 +25,8 @@ import { GenericChangeCodec, GraphViewCodec, ModelCodec, + mxCellCodec, + mxGeometryCodec, RootChangeCodec, StylesheetCodec, TerminalChangeCodec, @@ -88,6 +90,11 @@ export const registerCoreCodecs = (force = false) => { CodecRegistry.register(new ObjectCodec({})); // Object CodecRegistry.register(new ObjectCodec([])); // Array + // mxGraph support + CodecRegistry.addAlias('mxGraphModel', 'GraphDataModel'); + CodecRegistry.register(new mxCellCodec(), false); + CodecRegistry.register(new mxGeometryCodec(), false); + isCoreCodecsRegistered = true; } }; diff --git a/packages/html/stories/Codec.stories.ts b/packages/html/stories/Codec.stories.ts new file mode 100644 index 000000000..bac1fac16 --- /dev/null +++ b/packages/html/stories/Codec.stories.ts @@ -0,0 +1,179 @@ +/* +Copyright 2024-present The maxGraph project Contributors +Copyright (c) 2006-2013, JGraph Ltd + +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 { + Graph, + InternalEvent, + ModelXmlSerializer, + type PanningHandler, +} from '@maxgraph/core'; +import { globalTypes, globalValues } from './shared/args.js'; + +const xmlModel = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +export default { + title: 'Misc/CodecImport_mxGraph', + argTypes: { + ...globalTypes, + }, + args: { + ...globalValues, + }, +}; + +// This example demonstrates dynamically creating a graph from XML coming from mxGraph, as well as +// changing the default style for edges in-place. +const Template = ({ label, ...args }: Record) => { + const div = document.createElement('div'); + + const container = document.createElement('div'); + container.style.position = 'relative'; + container.style.overflow = 'hidden'; + container.style.width = `${args.width}px`; + container.style.height = `${args.height}px`; + container.style.background = '#eeeeee'; + container.style.border = '1px solid gray'; + div.appendChild(container); + + const graph = new Graph(container); + graph.centerZoom = false; + graph.setTooltips(false); + graph.setEnabled(false); + + // Changes the default style for edges "in-place" + const style = graph.getStylesheet().getDefaultEdgeStyle(); + style.edgeStyle = 'elbowEdgeStyle'; + + // Enables panning with left mouse button + const panningHandler = graph.getPlugin('PanningHandler') as PanningHandler; + panningHandler.useLeftButtonForPanning = true; + panningHandler.ignoreCell = true; + graph.container.style.cursor = 'move'; + graph.setPanning(true); + InternalEvent.disableContextMenu(container); + + new ModelXmlSerializer(graph.getDataModel()).import(xmlModel); + graph.resizeContainer = false; + + // Adds zoom buttons in top, left corner + const buttons = document.createElement('div'); + buttons.style.position = 'absolute'; + buttons.style.overflow = 'visible'; + + const bs = graph.getBorderSizes(); + buttons.style.top = `${container.offsetTop + bs.y}px`; + buttons.style.left = `${container.offsetLeft + bs.x}px`; + + let left = 0; + const bw = 16; + const bh = 16; + + function addButton(label: string, funct: () => void) { + const btn = document.createElement('div'); + const labelNode = btn.ownerDocument.createTextNode(label); + btn.appendChild(labelNode); + + btn.style.position = 'absolute'; + btn.style.backgroundColor = 'transparent'; + btn.style.border = '1px solid gray'; + btn.style.textAlign = 'center'; + btn.style.fontSize = '10px'; + btn.style.cursor = 'pointer'; + btn.style.width = `${bw}px`; + btn.style.height = `${bh}px`; + btn.style.left = `${left}px`; + btn.style.top = '0px'; + + InternalEvent.addListener(btn, 'click', function (evt: Event) { + funct(); + InternalEvent.consume(evt); + }); + + left += bw; + + buttons.appendChild(btn); + } + + addButton('+', function () { + graph.zoomIn(); + }); + + addButton('-', function () { + graph.zoomOut(); + }); + + div.insertBefore(buttons, container); + + return div; +}; + +export const Default = Template.bind({}); diff --git a/packages/html/stories/stashed/Codec.js b/packages/html/stories/stashed/Codec.js deleted file mode 100644 index 9abb922ef..000000000 --- a/packages/html/stories/stashed/Codec.js +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Copyright (c) 2006-2013, JGraph Ltd - * - * Codec - */ - -import React from 'react'; -import mxEvent from '../mxgraph/util/mxEvent'; -import mxGraph from '../mxgraph/view/mxGraph'; -import Codec from '../mxgraph/io/Codec'; -import mxUtils from '../mxgraph/util/mxUtils'; -import mxConstants from '../mxgraph/util/mxConstants'; -import mxEdgeStyle from '../mxgraph/view/mxEdgeStyle'; - - -// Contains a graph description which will be converted -const HTML_TEMPLATE = ` -

Codec

- -This example demonstrates dynamically creating a graph from XML and -encoding the model into XML, as well as changing the default style for -edges in-place. This graph is embedded in the page. -
- <mxGraphModel><root><mxCell id="0"/><mxCell - id="1" parent="0"/><mxCell id="2" vertex="1" parent="1" - value="Interval 1"><mxGeometry x="380" y="0" width="140" - height="30" as="geometry"/></mxCell><mxCell id="3" - vertex="1" parent="1" value="Interval 2"><mxGeometry x="200" - y="80" width="380" height="30" - as="geometry"/></mxCell><mxCell id="4" vertex="1" - parent="1" value="Interval 3"><mxGeometry x="40" y="140" - width="260" height="30" as="geometry"/></mxCell><mxCell - id="5" vertex="1" parent="1" value="Interval 4"><mxGeometry - x="120" y="200" width="240" height="30" - as="geometry"/></mxCell><mxCell id="6" vertex="1" - parent="1" value="Interval 5"><mxGeometry x="420" y="260" - width="80" height="30" as="geometry"/></mxCell><mxCell - id="7" edge="1" source="2" target="3" parent="1" - value="Transfer1"><mxGeometry as="geometry"><Array - as="points"><Object x="420" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="8" edge="1" source="2" target="6" parent="1" - value=""><mxGeometry as="geometry" relative="1" - y="-30"><Array as="points"><Object x="600" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="9" edge="1" source="3" target="4" parent="1" - value="Transfer3"><mxGeometry as="geometry"><Array - as="points"><Object x="260" - y="120"/></Array></mxGeometry></mxCell><mxCell - id="10" edge="1" source="4" target="5" parent="1" - value="Transfer4"><mxGeometry as="geometry"><Array - as="points"><Object x="200" - y="180"/></Array></mxGeometry></mxCell><mxCell - id="11" edge="1" source="4" target="6" parent="1" - value="Transfer5"><mxGeometry as="geometry" relative="1" - y="-10"><Array as="points"><Object x="460" - y="155"/></Array></mxGeometry></mxCell></root></mxGraphModel> -
-This graph is embedded in the page. -
- <mxGraphModel><root><mxCell id="0"/><mxCell - id="1" parent="0"/><mxCell id="2" vertex="1" parent="1" - value="Interval 1"><mxGeometry x="380" y="0" width="140" - height="30" as="geometry"/></mxCell><mxCell id="3" - vertex="1" parent="1" value="Interval 2"><mxGeometry x="200" - y="80" width="380" height="30" - as="geometry"/></mxCell><mxCell id="4" vertex="1" - parent="1" value="Interval 3"><mxGeometry x="40" y="140" - width="260" height="30" as="geometry"/></mxCell><mxCell - id="5" vertex="1" parent="1" value="Interval 4"><mxGeometry - x="120" y="200" width="240" height="30" - as="geometry"/></mxCell><mxCell id="6" vertex="1" - parent="1" value="Interval 5"><mxGeometry x="420" y="260" - width="80" height="30" as="geometry"/></mxCell><mxCell - id="7" edge="1" source="2" target="3" parent="1" - value="Transfer1"><mxGeometry as="geometry"><Array - as="points"><Object x="420" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="8" edge="1" source="2" target="6" parent="1" - value=""><mxGeometry as="geometry" relative="1" - y="-30"><Array as="points"><Object x="600" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="9" edge="1" source="3" target="4" parent="1" - value="Transfer3"><mxGeometry as="geometry"><Array - as="points"><Object x="260" - y="120"/></Array></mxGeometry></mxCell><mxCell - id="10" edge="1" source="4" target="5" parent="1" - value="Transfer4"><mxGeometry as="geometry"><Array - as="points"><Object x="200" - y="180"/></Array></mxGeometry></mxCell><mxCell - id="11" edge="1" source="4" target="6" parent="1" - value="Transfer5"><mxGeometry as="geometry" relative="1" - y="-10"><Array as="points"><Object x="460" - y="155"/></Array></mxGeometry></mxCell></root></mxGraphModel> -
-This graph is embedded in the page. -
- <mxGraphModel><root><mxCell id="0"/><mxCell - id="1" parent="0"/><mxCell id="2" vertex="1" parent="1" - value="Interval 1"><mxGeometry x="380" y="20" width="140" - height="30" as="geometry"/></mxCell><mxCell id="3" - vertex="1" parent="1" value="Interval 2"><mxGeometry x="200" - y="80" width="380" height="30" - as="geometry"/></mxCell><mxCell id="4" vertex="1" - parent="1" value="Interval 3"><mxGeometry x="40" y="140" - width="260" height="30" as="geometry"/></mxCell><mxCell - id="5" vertex="1" parent="1" value="Interval 4"><mxGeometry - x="120" y="200" width="240" height="30" - as="geometry"/></mxCell><mxCell id="6" vertex="1" - parent="1" value="Interval 5"><mxGeometry x="420" y="260" - width="80" height="30" as="geometry"/></mxCell><mxCell - id="7" edge="1" source="2" target="3" parent="1" - value="Transfer1"><mxGeometry as="geometry"><Array - as="points"><Object x="420" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="8" edge="1" source="2" target="6" parent="1" - value="Transfer2"><mxGeometry as="geometry" relative="1" - y="0"><Array as="points"><Object x="600" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="9" edge="1" source="3" target="4" parent="1" - value="Transfer3"><mxGeometry as="geometry"><Array - as="points"><Object x="260" - y="120"/></Array></mxGeometry></mxCell><mxCell - id="10" edge="1" source="4" target="5" parent="1" - value="Transfer4"><mxGeometry as="geometry"><Array - as="points"><Object x="200" - y="180"/></Array></mxGeometry></mxCell><mxCell - id="11" edge="1" source="4" target="6" parent="1" - value="Transfer5"><mxGeometry as="geometry" relative="1" - y="-10"><Array as="points"><Object x="460" - y="155"/></Array></mxGeometry></mxCell></root></mxGraphModel> -
-This graph is embedded in the page. -
- <mxGraphModel><root><mxCell id="0"/><mxCell - id="1" parent="0"/><mxCell id="2" vertex="1" parent="1" - value="Interval 1"><mxGeometry x="380" y="20" width="140" - height="30" as="geometry"/></mxCell><mxCell id="3" - vertex="1" parent="1" value="Interval 2"><mxGeometry x="200" - y="80" width="380" height="30" - as="geometry"/></mxCell><mxCell id="4" vertex="1" - parent="1" value="Interval 3"><mxGeometry x="40" y="140" - width="260" height="30" as="geometry"/></mxCell><mxCell - id="5" vertex="1" parent="1" value="Interval 4"><mxGeometry - x="120" y="200" width="240" height="30" - as="geometry"/></mxCell><mxCell id="6" vertex="1" - parent="1" value="Interval 5"><mxGeometry x="420" y="260" - width="80" height="30" as="geometry"/></mxCell><mxCell - id="7" edge="1" source="2" target="3" parent="1" - value="Transfer1"><mxGeometry as="geometry"><Array - as="points"><Object x="420" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="8" edge="1" source="2" target="6" parent="1" - value="Transfer2"><mxGeometry as="geometry" relative="1" - y="0"><Array as="points"><Object x="600" - y="60"/></Array></mxGeometry></mxCell><mxCell - id="9" edge="1" source="3" target="4" parent="1" - value="Transfer3"><mxGeometry as="geometry"><Array - as="points"><Object x="260" - y="120"/></Array></mxGeometry></mxCell><mxCell - id="10" edge="1" source="4" target="5" parent="1" - value="Transfer4"><mxGeometry as="geometry"><Array - as="points"><Object x="200" - y="180"/></Array></mxGeometry></mxCell><mxCell - id="11" edge="1" source="4" target="6" parent="1" - value="Transfer5"><mxGeometry as="geometry" relative="1" - y="-10"><Array as="points"><Object x="460" - y="155"/></Array></mxGeometry></mxCell></root></mxGraphModel> -
-This graph is embedded in the page.` - - -const divs = document.getElementsByTagName('*'); - -for (let i = 0; i < divs.length; i += 1) { - if (divs[i].className.toString().indexOf('mxgraph') >= 0) { - (function(container) { - const xml = mxUtils.getTextContent(container); - const xmlDocument = mxUtils.parseXml(xml); - - if ( - xmlDocument.documentElement != null && - xmlDocument.documentElement.nodeName === 'mxGraphModel' - ) { - const decoder = new Codec(xmlDocument); - const node = xmlDocument.documentElement; - - container.innerHTML = ''; - - const graph = new mxGraph(container); - graph.centerZoom = false; - graph.setTooltips(false); - graph.setEnabled(false); - - // Changes the default style for edges "in-place" - const style = graph.getStylesheet().getDefaultEdgeStyle(); - style.edge = mxEdgeStyle.ElbowConnector; - - // Enables panning with left mouse button - graph.getPlugin('PanningHandler').useLeftButtonForPanning = true; - graph.getPlugin('PanningHandler').ignoreCell = true; - graph.container.style.cursor = 'move'; - graph.setPanning(true); - - if (divs[i].style.width === '' && divs[i].style.height === '') { - graph.resizeContainer = true; - } else { - // Adds border for fixed size boxes - graph.border = 20; - } - - decoder.decode(node, graph.getDataModel()); - graph.resizeContainer = false; - - // Adds zoom buttons in top, left corner - const buttons = document.createElement('div'); - buttons.style.position = 'absolute'; - buttons.style.overflow = 'visible'; - - const bs = graph.getBorderSizes(); - buttons.style.top = `${container.offsetTop + bs.y}px`; - buttons.style.left = `${container.offsetLeft + bs.x}px`; - - let left = 0; - const bw = 16; - const bh = 16; - - function addButton(label, funct) { - const btn = document.createElement('div'); - mxUtils.write(btn, label); - btn.style.position = 'absolute'; - btn.style.backgroundColor = 'transparent'; - btn.style.border = '1px solid gray'; - btn.style.textAlign = 'center'; - btn.style.fontSize = '10px'; - btn.style.cursor = 'hand'; - btn.style.width = `${bw}px`; - btn.style.height = `${bh}px`; - btn.style.left = `${left}px`; - btn.style.top = '0px'; - - mxEvent.addListener(btn, 'click', function(evt) { - funct(); - mxEvent.consume(evt); - }); - - left += bw; - - buttons.appendChild(btn); - } - - addButton('+', function() { - graph.zoomIn(); - }); - - addButton('-', function() { - graph.zoomOut(); - }); - - if (container.nextSibling != null) { - container.parentNode.insertBefore(buttons, container.nextSibling); - } else { - container.appendChild(buttons); - } - } - })(divs[i]); - } -} diff --git a/packages/website/docs/usage/migrate-from-mxgraph.md b/packages/website/docs/usage/migrate-from-mxgraph.md index 2058bcde6..7ebd7b5d2 100644 --- a/packages/website/docs/usage/migrate-from-mxgraph.md +++ b/packages/website/docs/usage/migrate-from-mxgraph.md @@ -508,6 +508,7 @@ Be aware of the properties that have been renamed or whose value types have chan **Migration example** +If you want to write code to migrate mxGraph to maxGraph style, you can have a look at `packages/core/src/serialization/codecs/mxGraph/utils.ts`. ```js // Before