Repository: maxGraph/maxGraph
Pull request #306 (2024-01-15 11:28:41)

feat: declare exports in package.json
It lets use `moduleResolution=bundler` in TypeScript applications.
In particular, it lets use it with Storybook and ts-example in this repository.

### Notes

See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing


Patch:
diff --git a/package-lock.json b/package-lock.json
index bcb549a31..da1579d55 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25084,9 +25084,23 @@
       "name": "@maxgraph/ts-example-built-with-vite",
       "license": "Apache-2.0",
       "devDependencies": {
+        "typescript": "~5.3.3",
         "vite": "~4.4.12"
       }
     },
+    "packages/ts-example/node_modules/typescript": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+      "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
     "packages/ts-example/node_modules/vite": {
       "version": "4.4.11",
       "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz",
diff --git a/packages/core/package.json b/packages/core/package.json
index 30676b341..6149d365f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -13,6 +13,17 @@
   ],
   "module": "dist/index.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/index.d.ts",
+        "default": "./dist/index.js"
+      }
+    },
+    "./css/*": "./css/*",
+    "./images/*": "./images/*",
+    "./package.json": "./package.json"
+  },
   "files": [
     "css",
     "dist",
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,36 +414,36 @@ 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 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;
-        }
-      }
+    // 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);
 
-      if (!this.isCellCodec(decoder)) {
-        decoder = CodecRegistry.getCodec(Cell);
-      }
-      cell = decoder?.decode(this, node);
+    // 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;
 
-      if (restoreStructures) {
-        this.insertIntoGraph(cell);
+      while (child != null && !this.isCellCodec(decoder)) {
+        decoder = CodecRegistry.getCodec(child.nodeName);
+        child = child.nextSibling as Element;
       }
     }
+
+    if (!this.isCellCodec(decoder)) {
+      decoder = CodecRegistry.getCodec(Cell);
+    }
+    const cell = decoder?.decode(this, node);
+
+    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 <CellCodec>.
-   * 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
+    // <Array as="points">
+    //   <Object x="420" y="60"/>
+    // </Array>
+
+    const originalPoints = (obj as Geometry).points;
+    if (originalPoints) {
+      const points: Array<Point> = [];
+      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<string, string>([['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/core/src/view/GraphView.ts b/packages/core/src/view/GraphView.ts
index 65ed3e3ad..d050cd703 100644
--- a/packages/core/src/view/GraphView.ts
+++ b/packages/core/src/view/GraphView.ts
@@ -897,11 +897,11 @@ export class GraphView extends EventSource {
 
           if (geo.relative && pState) {
             if (pState.cell.isEdge()) {
-              const origin = this.getPoint(pState, geo);
+              const point = this.getPoint(pState, geo);
 
-              if (origin) {
-                origin.x += origin.x / this.scale - pState.origin.x - this.translate.x;
-                origin.y += origin.y / this.scale - pState.origin.y - this.translate.y;
+              if (point) {
+                origin.x += point.x / this.scale - pState.origin.x - this.translate.x;
+                origin.y += point.y / this.scale - pState.origin.y - this.translate.y;
               }
             } else {
               origin.x += geo.x * pState.unscaledWidth + offset.x;
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 = `<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>`;
+
+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<string, any>) => {
+  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 = `
-<h1>Codec</h1>
-
-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.
-<div className="mxgraph" style="position:relative;overflow:auto;">
-  &lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id="0"/&gt;&lt;mxCell
-  id="1" parent="0"/&gt;&lt;mxCell id="2" vertex="1" parent="1"
-  value="Interval 1"&gt;&lt;mxGeometry x="380" y="0" width="140"
-  height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="3"
-  vertex="1" parent="1" value="Interval 2"&gt;&lt;mxGeometry x="200"
-  y="80" width="380" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="4" vertex="1"
-  parent="1" value="Interval 3"&gt;&lt;mxGeometry x="40" y="140"
-  width="260" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="5" vertex="1" parent="1" value="Interval 4"&gt;&lt;mxGeometry
-  x="120" y="200" width="240" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="6" vertex="1"
-  parent="1" value="Interval 5"&gt;&lt;mxGeometry x="420" y="260"
-  width="80" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="7" edge="1" source="2" target="3" parent="1"
-  value="Transfer1"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="420"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="8" edge="1" source="2" target="6" parent="1"
-  value=""&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-30"&gt;&lt;Array as="points"&gt;&lt;Object x="600"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="9" edge="1" source="3" target="4" parent="1"
-  value="Transfer3"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="260"
-  y="120"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="10" edge="1" source="4" target="5" parent="1"
-  value="Transfer4"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="200"
-  y="180"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="11" edge="1" source="4" target="6" parent="1"
-  value="Transfer5"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-10"&gt;&lt;Array as="points"&gt;&lt;Object x="460"
-  y="155"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;
-</div>
-This graph is embedded in the page.
-<div
-  className="mxgraph"
-  style="position:relative;background:#eeeeee;border:1px solid gray;overflow:auto;width:400px;height:200px;"
->
-  &lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id="0"/&gt;&lt;mxCell
-  id="1" parent="0"/&gt;&lt;mxCell id="2" vertex="1" parent="1"
-  value="Interval 1"&gt;&lt;mxGeometry x="380" y="0" width="140"
-  height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="3"
-  vertex="1" parent="1" value="Interval 2"&gt;&lt;mxGeometry x="200"
-  y="80" width="380" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="4" vertex="1"
-  parent="1" value="Interval 3"&gt;&lt;mxGeometry x="40" y="140"
-  width="260" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="5" vertex="1" parent="1" value="Interval 4"&gt;&lt;mxGeometry
-  x="120" y="200" width="240" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="6" vertex="1"
-  parent="1" value="Interval 5"&gt;&lt;mxGeometry x="420" y="260"
-  width="80" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="7" edge="1" source="2" target="3" parent="1"
-  value="Transfer1"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="420"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="8" edge="1" source="2" target="6" parent="1"
-  value=""&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-30"&gt;&lt;Array as="points"&gt;&lt;Object x="600"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="9" edge="1" source="3" target="4" parent="1"
-  value="Transfer3"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="260"
-  y="120"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="10" edge="1" source="4" target="5" parent="1"
-  value="Transfer4"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="200"
-  y="180"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="11" edge="1" source="4" target="6" parent="1"
-  value="Transfer5"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-10"&gt;&lt;Array as="points"&gt;&lt;Object x="460"
-  y="155"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;
-</div>
-This graph is embedded in the page.
-<div
-  className="mxgraph"
-  style="position:relative;background:#eeeeee;border:6px solid gray;overflow:auto;width:400px;height:200px;"
->
-  &lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id="0"/&gt;&lt;mxCell
-  id="1" parent="0"/&gt;&lt;mxCell id="2" vertex="1" parent="1"
-  value="Interval 1"&gt;&lt;mxGeometry x="380" y="20" width="140"
-  height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="3"
-  vertex="1" parent="1" value="Interval 2"&gt;&lt;mxGeometry x="200"
-  y="80" width="380" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="4" vertex="1"
-  parent="1" value="Interval 3"&gt;&lt;mxGeometry x="40" y="140"
-  width="260" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="5" vertex="1" parent="1" value="Interval 4"&gt;&lt;mxGeometry
-  x="120" y="200" width="240" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="6" vertex="1"
-  parent="1" value="Interval 5"&gt;&lt;mxGeometry x="420" y="260"
-  width="80" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="7" edge="1" source="2" target="3" parent="1"
-  value="Transfer1"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="420"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="8" edge="1" source="2" target="6" parent="1"
-  value="Transfer2"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="0"&gt;&lt;Array as="points"&gt;&lt;Object x="600"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="9" edge="1" source="3" target="4" parent="1"
-  value="Transfer3"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="260"
-  y="120"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="10" edge="1" source="4" target="5" parent="1"
-  value="Transfer4"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="200"
-  y="180"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="11" edge="1" source="4" target="6" parent="1"
-  value="Transfer5"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-10"&gt;&lt;Array as="points"&gt;&lt;Object x="460"
-  y="155"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;
-</div>
-This graph is embedded in the page.
-<div
-  className="mxgraph"
-  style="position:relative;overflow:hidden;border:6px solid gray;"
->
-  &lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id="0"/&gt;&lt;mxCell
-  id="1" parent="0"/&gt;&lt;mxCell id="2" vertex="1" parent="1"
-  value="Interval 1"&gt;&lt;mxGeometry x="380" y="20" width="140"
-  height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="3"
-  vertex="1" parent="1" value="Interval 2"&gt;&lt;mxGeometry x="200"
-  y="80" width="380" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="4" vertex="1"
-  parent="1" value="Interval 3"&gt;&lt;mxGeometry x="40" y="140"
-  width="260" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="5" vertex="1" parent="1" value="Interval 4"&gt;&lt;mxGeometry
-  x="120" y="200" width="240" height="30"
-  as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell id="6" vertex="1"
-  parent="1" value="Interval 5"&gt;&lt;mxGeometry x="420" y="260"
-  width="80" height="30" as="geometry"/&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="7" edge="1" source="2" target="3" parent="1"
-  value="Transfer1"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="420"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="8" edge="1" source="2" target="6" parent="1"
-  value="Transfer2"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="0"&gt;&lt;Array as="points"&gt;&lt;Object x="600"
-  y="60"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="9" edge="1" source="3" target="4" parent="1"
-  value="Transfer3"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="260"
-  y="120"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="10" edge="1" source="4" target="5" parent="1"
-  value="Transfer4"&gt;&lt;mxGeometry as="geometry"&gt;&lt;Array
-  as="points"&gt;&lt;Object x="200"
-  y="180"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell
-  id="11" edge="1" source="4" target="6" parent="1"
-  value="Transfer5"&gt;&lt;mxGeometry as="geometry" relative="1"
-  y="-10"&gt;&lt;Array as="points"&gt;&lt;Object x="460"
-  y="155"/&gt;&lt;/Array&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;
-</div>
-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/html/tsconfig.json b/packages/html/tsconfig.json
index 61ec4ac29..a2a56eaf8 100644
--- a/packages/html/tsconfig.json
+++ b/packages/html/tsconfig.json
@@ -9,8 +9,7 @@
 
     /* Bundler mode */
     "allowJs": true,
-    // TODO restore bundler - require exports in the package.json of @maxgraph/core
-    "moduleResolution": "Node",
+    "moduleResolution": "bundler",
     "allowImportingTsExtensions": true,
     "resolveJsonModule": true,
     "isolatedModules": true,
diff --git a/packages/ts-example/package.json b/packages/ts-example/package.json
index 6c3227170..c2280ee74 100644
--- a/packages/ts-example/package.json
+++ b/packages/ts-example/package.json
@@ -2,12 +2,14 @@
   "name": "@maxgraph/ts-example-built-with-vite",
   "license": "Apache-2.0",
   "private": true,
+  "type": "module",
   "scripts": {
     "dev": "vite",
     "build": "tsc --version && tsc && vite build --base ./",
     "preview": "vite preview"
   },
   "devDependencies": {
+    "typescript": "~5.3.3",
     "vite": "~4.4.12"
   }
 }
diff --git a/packages/ts-example/tsconfig.json b/packages/ts-example/tsconfig.json
index 5c380a7de..2aa41a6d7 100644
--- a/packages/ts-example/tsconfig.json
+++ b/packages/ts-example/tsconfig.json
@@ -3,6 +3,7 @@
   "compilerOptions": {
     "useDefineForClassFields": true,
     "sourceMap": true,
+    "moduleResolution": "bundler",
     "resolveJsonModule": true,
     "noEmit": true,
     "noUnusedLocals": true,
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


Test patch:
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(<CellStyle>{
+      baseStyleNames: ['rectangle', 'customRectangle'],
+      fontColor: 'yellow',
+      gradientColor: 'white',
+    });
+  });
+
+  // renamed properties (see migration guide)
+  test('With renamed properties', () => {
+    // @ts-ignore
+    expect(convertStyleFromString('autosize=1')).toEqual(<CellStyle>{
+      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 = `<mxGraphModel>
+  <root>
+    <mxCell id="0"/>
+    <mxCell id="1" parent="0"/>
+    <mxCell id="2" vertex="1" parent="1" value="Vertex #2">
+      <mxGeometry x="380" y="20" width="140" height="30" as="geometry"/>
+    </mxCell>
+    <mxCell id="3" vertex="1" parent="1" value="Vertex #3">
+      <mxGeometry x="200" y="80" width="380" height="30" as="geometry"/>
+    </mxCell>
+    <mxCell id="7" edge="1" source="2" target="3" parent="1" value="Edge #7">
+      <mxGeometry as="geometry">
+        <Array as="points">
+          <Object x="420" y="60"/>
+        </Array>
+      </mxGeometry>
+    </mxCell>
+  </root>
+</mxGraphModel>
+  `;
+
+    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 = `<mxGraphModel>
+  <root>
+    <mxCell id="0"/>
+    <mxCell id="1" parent="0"/>
+    <mxCell id="2" vertex="1" parent="1" value="Vertex with style" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#E6E6E6;dashed=1;">
+    </mxCell>
+  </root>
+</mxGraphModel>`;
+
+    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(`<mxGraphModel dx="1502" dy="926" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1"
+              pageScale="1" pageWidth="1920" pageHeight="1200" math="0" shadow="0">
+  <root>
+    <mxCell id="0"/>
+    <mxCell id="1" parent="0"/>
+    <mxCell id="2Ija7sB8CSz23ppRtK8d-5" value="PostgreSQL"
+            style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontStyle=1;fontSize=27;"
+            parent="1" vertex="1">
+      <mxGeometry x="650" y="430" width="210" height="80" as="geometry"/>
+    </mxCell>
+  </root>
+</mxGraphModel>`);
+
+    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 = `<GraphDataModel>
     <root>
@@ -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 ?? {});
+  }
+}