Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/core/__tests__/util/mathUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2025-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 { isNumeric } from '../../src/util/mathUtils';

describe('isNumeric', () => {
test.each([null, undefined])('nullish value: %s', (value: null | undefined) => {
expect(isNumeric(value)).toBeFalsy();
});

test('number', () => {
expect(isNumeric(9465.45654)).toBeTruthy();
});

test('Infinity', () => {
expect(isNumeric(Infinity)).toBeFalsy();
});

test('empty string', () => {
expect(isNumeric('')).toBeFalsy();
});

test('hexadecimal in string', () => {
expect(isNumeric('0x354354')).toBeFalsy();
});

test('string which is not a number', () => {
expect(isNumeric('354354b')).toBeFalsy();
});

test('too large number in a string', () => {
expect(isNumeric('9465456546987616587651356846')).toBeTruthy();
});

test('Object', () => {
expect(isNumeric({ attribute: 'value' })).toBeFalsy();
});
});
4 changes: 2 additions & 2 deletions packages/core/src/editor/EditorPopupMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ export class EditorPopupMenu {
) {
let addSeparator = false;

while (item != null) {
while (item) {
if (item.nodeName === 'add') {
const condition = item.getAttribute('if');

if (condition == null || conditions[condition]) {
let as = <string>item.getAttribute('as');
let as = item.getAttribute('as')!;
as = Translations.get(as) || as;
const funct = eval(getTextContent(<Text>(<unknown>item)));
const action = item.getAttribute('action');
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/internal-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Copyright 2025-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.
*/

/**
* @internal
* @private
*/
export type UserObject = {
nodeType?: number;
getAttribute?: (name: string) => string | null;
hasAttribute?: (name: string) => boolean;
setAttribute?: (name: string, value: string) => void;
clone?: () => UserObject;
cloneNode?: (deep: boolean) => UserObject;
};
9 changes: 4 additions & 5 deletions packages/core/src/serialization/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ limitations under the License.

import CellPath from '../view/cell/CellPath';
import CodecRegistry from './CodecRegistry';
import { NODETYPE } from '../util/Constants';
import Cell from '../view/cell/Cell';
import { GlobalConfig } from '../util/config';
import { getFunctionName } from '../util/StringUtils';
import { importNode, isNode } from '../util/domUtils';
import { importNode, isElement, isNode } from '../util/domUtils';

const createXmlDocument = () => {
return document.implementation.createDocument('', '', null);
Expand Down Expand Up @@ -248,7 +247,7 @@ class Codec {
* Adds the given element to {@link elements} if it has an ID.
*/
addElement(node: Element): void {
if (node.nodeType === NODETYPE.ELEMENT) {
if (isElement(node)) {
const id = node.getAttribute('id');

if (id != null) {
Expand Down Expand Up @@ -359,7 +358,7 @@ class Codec {
this.updateElements();
let obj = null;

if (node != null && node.nodeType === NODETYPE.ELEMENT) {
if (isElement(node)) {
const dec = CodecRegistry.getCodecByName(node.nodeName);

if (dec != null) {
Expand Down Expand Up @@ -429,7 +428,7 @@ class Codec {
* Default is `true`.
*/
decodeCell(node: Element, restoreStructures = true): Cell | null {
if (node?.nodeType !== NODETYPE.ELEMENT) {
if (!isElement(node)) {
return null;
}

Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/serialization/ObjectCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import ObjectIdentity from '../util/ObjectIdentity';
import { GlobalConfig } from '../util/config';
import Geometry from '../view/geometry/Geometry';
import Point from '../view/geometry/Point';
import { NODETYPE } from '../util/Constants';
import { isInteger, isNumeric } from '../util/mathUtils';
import { getTextContent } from '../util/domUtils';
import { getTextContent, isElement } from '../util/domUtils';
import { load } from '../util/MaxXmlRequest';
import type Codec from './Codec';

Expand Down Expand Up @@ -791,10 +790,10 @@ class ObjectCodec {
decodeChildren(dec: Codec, node: Element, obj?: any): void {
let child = <Element>node.firstChild;

while (child != null) {
while (child) {
const tmp = <Element>child.nextSibling;

if (child.nodeType === NODETYPE.ELEMENT && !this.processInclude(dec, child, obj)) {
if (isElement(child) && !this.processInclude(dec, child, obj)) {
this.decodeChild(dec, child, obj);
}

Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/serialization/codecs/CellCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import CodecRegistry from '../CodecRegistry';
import ObjectCodec from '../ObjectCodec';
import Cell from '../../view/cell/Cell';
import type Codec from '../Codec';
import { NODETYPE } from '../../util/Constants';
import { importNode } from '../../util/domUtils';
import { importNode, isElement } from '../../util/domUtils';
import { removeWhitespace } from '../../util/StringUtils';

/**
Expand Down Expand Up @@ -82,15 +81,15 @@ export class CellCodec extends ObjectCodec {
isExcluded(obj: any, attr: string, value: Element, isWrite: boolean) {
return (
super.isExcluded(obj, attr, value, isWrite) ||
(isWrite && attr === 'value' && value.nodeType === NODETYPE.ELEMENT)
(isWrite && attr === 'value' && isElement(value))
);
}

/**
* Encodes a {@link Cell} and wraps the XML up inside the XML of the user object (inversion).
*/
afterEncode(enc: Codec, obj: Cell, node: Element) {
if (obj.value != null && obj.value.nodeType === NODETYPE.ELEMENT) {
if (isElement(obj.value)) {
// Wraps the graphical annotation up in the user object (inversion)
// by putting the result of the default encoding into a clone of the
// user object (node type 1) and returning this cloned user object.
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/serialization/codecs/ChildChangeCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ limitations under the License.
import ObjectCodec from '../ObjectCodec';
import ChildChange from '../../view/undoable_changes/ChildChange';
import type Codec from '../Codec';
import { NODETYPE } from '../../util/Constants';

import { isElement } from '../../util/domUtils';

/**
* Codec for {@link ChildChange}s.
Expand Down Expand Up @@ -93,7 +94,7 @@ export class ChildChangeCodec extends ObjectCodec {
* Decodes any child nodes as using the respective codec from the registry.
*/
beforeDecode(dec: Codec, _node: Element, obj: any): any {
if (_node.firstChild != null && _node.firstChild.nodeType === NODETYPE.ELEMENT) {
if (isElement(_node.firstChild)) {
// Makes sure the original node isn't modified
const node = _node.cloneNode(true);

Expand All @@ -104,23 +105,23 @@ export class ChildChangeCodec extends ObjectCodec {
(<Element>tmp.parentNode).removeChild(tmp);
tmp = tmp2;

while (tmp != null) {
while (tmp) {
tmp2 = <Element>tmp.nextSibling;

if (tmp.nodeType === NODETYPE.ELEMENT) {
if (isElement(tmp)) {
// Ignores all existing cells because those do not need to
// be re-inserted into the model. Since the encoded version
// of these cells contains the new parent, this would leave
// to an inconsistent state on the model (i.e. a parent
// change without a call to parentForCellChanged).
const id = <string>tmp.getAttribute('id');
const id = tmp.getAttribute('id')!;

if (dec.lookup(id) == null) {
dec.decodeCell(tmp);
}
}

(<Element>tmp.parentNode).removeChild(tmp);
tmp.parentNode?.removeChild(tmp);
tmp = tmp2;
}

Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/serialization/codecs/RootChangeCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ limitations under the License.
import ObjectCodec from '../ObjectCodec';
import RootChange from '../../view/undoable_changes/RootChange';
import type Codec from '../Codec';
import { NODETYPE } from '../../util/Constants';

import { isElement } from '../../util/domUtils';

/**
* Codec for {@link RootChange}s.
Expand Down Expand Up @@ -50,21 +51,21 @@ export class RootChangeCodec extends ObjectCodec {
* Decodes the optional children as cells using the respective decoder.
*/
beforeDecode(dec: Codec, node: Element, obj: any): any {
if (node.firstChild != null && node.firstChild.nodeType === NODETYPE.ELEMENT) {
if (isElement(node.firstChild)) {
// Makes sure the original node isn't modified
node = <Element>node.cloneNode(true);

let tmp = <Element>node.firstChild;
obj.root = dec.decodeCell(tmp, false);

let tmp2 = <Element>tmp.nextSibling;
(<Element>tmp.parentNode).removeChild(tmp);
tmp.parentNode?.removeChild(tmp);
tmp = tmp2;

while (tmp != null) {
tmp2 = <Element>tmp.nextSibling;
dec.decodeCell(tmp);
(<Element>tmp.parentNode).removeChild(tmp);
tmp.parentNode?.removeChild(tmp);
tmp = tmp2;
}
}
Expand Down
49 changes: 22 additions & 27 deletions packages/core/src/serialization/codecs/StylesheetCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import type Codec from '../Codec';
import StyleRegistry from '../../view/style/StyleRegistry';
import { clone } from '../../util/cloneUtils';
import { GlobalConfig } from '../../util/config';
import { NODETYPE } from '../../util/Constants';
import { isNumeric } from '../../util/mathUtils';
import { getTextContent } from '../../util/domUtils';
import { getTextContent, isElement } from '../../util/domUtils';

/**
* Codec for {@link Stylesheet}s.
Expand Down Expand Up @@ -94,57 +93,54 @@ export class StylesheetCodec extends ObjectCodec {
* Reads a sequence of the following child nodes and attributes:
*
* Child Nodes:
*
* add - Adds a new style.
* - `add` - Adds a new style.
*
* Attributes:
*
* as - Name of the style.
* extend - Name of the style to inherit from.
* - `as` - Name of the style.
* - `extend` - Name of the style to inherit from.
*
* Each node contains another sequence of add and remove nodes with the following attributes:
*
* as - Name of the style (see {@link Constants}).
* value - Value for the style.
* - `as` - Name of the style (see properties of {@link CellStateStyle}).
* - `value` - Value for the style.
*
* Instead of the value-attribute, one can put Javascript expressions into the node as follows if {@link allowEval} is `true`:
* <add as="perimeter">mxPerimeter.RectanglePerimeter</add>
* <add as="perimeter">Perimeter.RectanglePerimeter</add>
*
* A remove node will remove the entry with the name given in the as-attribute from the style.
*
* Example:
*
* ```javascript
* <mxStylesheet as="stylesheet">
* <Stylesheet as="stylesheet">
* <add as="text">
* <add as="fontSize" value="12"/>
* </add>
* <add as="defaultVertex" extend="text">
* <add as="shape" value="rectangle"/>
* </add>
* </mxStylesheet>
* </Stylesheet>
* ```
*/
decode(dec: Codec, _node: Element, into: any): any {
const obj = into || new this.template.constructor();
const id = _node.getAttribute('id');

if (id != null) {
if (id) {
dec.objects[id] = obj;
}

let node: Element | ChildNode | null = _node.firstChild;

while (node != null) {
while (node) {
if (!this.processInclude(dec, <Element>node, obj) && node.nodeName === 'add') {
const as = (<Element>node).getAttribute('as');

if (as != null) {
if (as) {
const extend = (<Element>node).getAttribute('extend');
let style = extend != null ? clone(obj.styles[extend]) : null;
let style = extend ? clone(obj.styles[extend]) : null;

if (style == null) {
if (extend != null) {
if (!style) {
if (extend) {
GlobalConfig.logger.warn(
`StylesheetCodec.decode: stylesheet ${extend} not found to extend`
);
Expand All @@ -154,26 +150,25 @@ export class StylesheetCodec extends ObjectCodec {
}

let entry = node.firstChild;

while (entry != null) {
if (entry.nodeType === NODETYPE.ELEMENT) {
const key = <string>(<Element>entry).getAttribute('as');
while (entry) {
if (isElement(entry)) {
const key = entry.getAttribute('as')!;

if (entry.nodeName === 'add') {
const text = getTextContent(<Text>(<unknown>entry));
let value = null;

if (text != null && text.length > 0 && StylesheetCodec.allowEval) {
if (text && StylesheetCodec.allowEval) {
value = eval(text);
} else {
value = (<Element>entry).getAttribute('value');
value = entry.getAttribute('value');

if (isNumeric(value)) {
value = parseFloat(<string>value);
value = parseFloat(value);
}
}

if (value != null) {
if (value) {
style[key] = value;
}
} else if (entry.nodeName === 'remove') {
Expand Down
Loading