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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ _**Note:** Yet to be released breaking changes appear here._

**Breaking Changes**:
- `StylesheetCodec.allowEval` is now set to `false` by default to prevent unwanted use of the eval function, as it carries a possible security risk.
- `Utils.copyTextToClipboard` is no longer available. It was intended to be internal and had been made public by mistake.

## 0.16.0

Expand Down
34 changes: 34 additions & 0 deletions packages/core/__tests__/internal/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
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 { FONT } from '../../src/util/Constants';
import { matchBinaryMask } from '../../src/internal/utils';

describe('matchBinaryMask', () => {
test('match self', () => {
expect(matchBinaryMask(FONT.STRIKETHROUGH, FONT.STRIKETHROUGH)).toBeTruthy();
});
test('match', () => {
expect(matchBinaryMask(9465, FONT.BOLD)).toBeTruthy();
});
test('match another', () => {
expect(matchBinaryMask(19484, FONT.UNDERLINE)).toBeTruthy();
});
test('no match', () => {
expect(matchBinaryMask(46413, FONT.ITALIC)).toBeFalsy();
});
});
16 changes: 0 additions & 16 deletions packages/core/__tests__/util/styleUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.

import { describe, expect, test } from '@jest/globals';
import {
matchBinaryMask,
parseCssNumber,
setStyleFlag,
setCellStyleFlags,
Expand All @@ -26,21 +25,6 @@ import { FONT } from '../../src/util/Constants';
import { type CellStyle } from '../../src/types';
import { createGraphWithoutPlugins } from '../utils';

describe('matchBinaryMask', () => {
test('match self', () => {
expect(matchBinaryMask(FONT.STRIKETHROUGH, FONT.STRIKETHROUGH)).toBeTruthy();
});
test('match', () => {
expect(matchBinaryMask(9465, FONT.BOLD)).toBeTruthy();
});
test('match another', () => {
expect(matchBinaryMask(19484, FONT.UNDERLINE)).toBeTruthy();
});
test('no match', () => {
expect(matchBinaryMask(46413, FONT.ITALIC)).toBeFalsy();
});
});

describe('parseCssNumber', () => {
test.each([
['thin', 2],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import type PanningHandler from '../view/plugins/PanningHandler';
import { cloneCell } from '../util/cellArrayUtils';
import { TranslationsConfig } from '../i18n/config';
import type MaxPopupMenu from '../gui/MaxPopupMenu';
import { isNullish } from '../util/Utils';
import { isNullish } from '../internal/utils';

/**
* Extends {@link EventSource} to implement an application wrapper for a graph that
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/editor/EditorPopupMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ import MaxPopupMenu from '../gui/MaxPopupMenu';
import { getTextContent } from '../util/domUtils';
import Translations from '../i18n/Translations';
import Editor from './Editor';

import { PopupMenuItem } from '../types';
import { isNullish } from '../util/Utils';
import { doEval } from '../internal/utils';
import { doEval, isNullish } from '../internal/utils';

/**
* Creates popupmenus for mouse events.
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/gui/MaxLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ import { getInnerHtml, write } from '../util/domUtils';
import { toString } from '../util/StringUtils';
import MaxWindow, { popup } from './MaxWindow';
import { KeyboardEventListener, MouseEventListener } from '../types';
import { copyTextToClipboard } from '../util/Utils';
import { getElapseMillisecondsMessage } from '../util/logger';
import { getElapseMillisecondsMessage } from '../internal/time-utils';
import { VERSION } from '../util/Constants';
import { GlobalConfig } from '../util/config';

const copyTextToClipboard = (text: string): void => {
navigator.clipboard.writeText(text).then(
function () {
GlobalConfig.logger.info('Async: Copying to clipboard was successful!');
},
function (err) {
GlobalConfig.logger.error('Async: Could not copy text: ', err);
}
);
};

/**
* A singleton class that implements a simple console.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { isNullish } from '../util/Utils';
import { shallowCopy } from '../util/cloneUtils';
import { isNullish } from '../internal/utils';
import { shallowCopy } from '../internal/clone-utils';

function getNavigatorLanguage() {
return typeof window !== 'undefined' ? navigator.language : 'en';
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/internal/clone-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
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.
*/

/**
* Shallow copies properties from the source object to the target object.
*
* **WARNING**: This function performs only a **shallow** copy i.e. there is no deep copy of the properties that are objects, expect for arrays.
*
* @template T The type of the objects.
*
* @param source The source object from which properties will be copied.
* @param target The target object to which properties will be copied.
*
* @private not part of the public API, can be removed or changed without prior notice
* @since 0.14.0
*/
export const shallowCopy = <T extends object>(source: T, target: T): void => {
for (const key in source) {
// attempt to prevent prototype pollution
if (Object.prototype.hasOwnProperty.call(source, key)) {
const sourceValue = source[key];
if (Array.isArray(sourceValue)) {
// TypeScript cannot infer that the key in target will also be an array when source and target are of the same type
(target[key] as unknown[]) = [...sourceValue];
} else {
target[key] = sourceValue;
}
}
}
};
25 changes: 25 additions & 0 deletions packages/core/src/internal/time-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
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.
*/

/**
* If `baseTimestamp` is provided and not zero, returns a message describing the elapsed milliseconds since this value.
* Otherwise, returns an empty string.
* @param baseTimestamp the base timestamp to compute the elapsed milliseconds from
*
* @private not part of the public API, can be removed or changed without prior notice
*/
export const getElapseMillisecondsMessage = (baseTimestamp?: number): string =>
baseTimestamp ? ` (${new Date().getTime() - baseTimestamp} ms)` : '';
Comment thread
tbouffard marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ limitations under the License.
*/

/**
* @internal
* @private
*/
export type UserObject = {
Expand Down
49 changes: 48 additions & 1 deletion packages/core/src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,57 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { NODETYPE } from '../util/Constants';
import { UserObject } from './types';
import { GlobalConfig } from '../util/config';

/**
* @internal
* @private
*/
export const doEval = (expression: string): any => {
// eslint-disable-next-line no-eval -- valid here as we want this function to be the only place in the codebase that uses eval
return eval(expression);
};

/**
* Returns true if the parameter is not `nullish` and its nodeType relates to an {@link Element}.
* @private
*/
export const isElement = (node?: Node | UserObject | null): node is Element =>
node?.nodeType === NODETYPE.ELEMENT;

/**
* @private not part of the public API, can be removed or changed without prior notice
*/
export const isNullish = (v: string | object | null | undefined | number | boolean) =>
v === null || v === undefined;

/**
* Merge a mixin into the destination
* @param dest the destination class
*
* @private not part of the public API, can be removed or changed without prior notice
*/
export const mixInto = (dest: any) => (mixin: any) => {
const keys = Reflect.ownKeys(mixin);
try {
for (const key of keys) {
Object.defineProperty(dest.prototype, key, {
value: mixin[key],
writable: true,
});
}
} catch (e) {
GlobalConfig.logger.error('Error while mixing', e);
}
};

/**
* @param value the value to check.
* @param mask the binary mask to apply.
* @returns `true` if the value matches the binary mask.
* @private Subject to change prior being part of the public API.
*/
export const matchBinaryMask = (value: number, mask: number) => {
return (value & mask) === mask;
};
3 changes: 2 additions & 1 deletion packages/core/src/serialization/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import CodecRegistry from './CodecRegistry';
import Cell from '../view/cell/Cell';
import { GlobalConfig } from '../util/config';
import { getFunctionName } from '../util/StringUtils';
import { importNode, isElement, isNode } from '../util/domUtils';
import { importNode, isNode } from '../util/domUtils';
import { isElement } from '../internal/utils';

const createXmlDocument = () => {
return document.implementation.createDocument('', '', null);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/serialization/ObjectCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import { GlobalConfig } from '../util/config';
import Geometry from '../view/geometry/Geometry';
import Point from '../view/geometry/Point';
import { isInteger, isNumeric } from '../util/mathUtils';
import { getTextContent, isElement } from '../util/domUtils';
import { getTextContent } from '../util/domUtils';
import { load } from '../util/MaxXmlRequest';
import type Codec from './Codec';
import { doEval } from '../internal/utils';
import { doEval, isElement } from '../internal/utils';

/**
* Generic codec for JavaScript objects that implements a mapping between
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/serialization/codecs/CellCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import CodecRegistry from '../CodecRegistry';
import ObjectCodec from '../ObjectCodec';
import Cell from '../../view/cell/Cell';
import type Codec from '../Codec';
import { importNode, isElement } from '../../util/domUtils';
import { importNode } from '../../util/domUtils';
import { removeWhitespace } from '../../util/StringUtils';
import { isElement } from '../../internal/utils';

/**
* Codec for {@link Cell}s.
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/serialization/codecs/ChildChangeCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ limitations under the License.
import ObjectCodec from '../ObjectCodec';
import ChildChange from '../../view/undoable_changes/ChildChange';
import type Codec from '../Codec';

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

/**
* Codec for {@link ChildChange}s.
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/serialization/codecs/RootChangeCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ limitations under the License.
import ObjectCodec from '../ObjectCodec';
import RootChange from '../../view/undoable_changes/RootChange';
import type Codec from '../Codec';

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

/**
* Codec for {@link RootChange}s.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/serialization/codecs/StylesheetCodec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import StyleRegistry from '../../view/style/StyleRegistry';
import { clone } from '../../util/cloneUtils';
import { GlobalConfig } from '../../util/config';
import { isNumeric } from '../../util/mathUtils';
import { getTextContent, isElement } from '../../util/domUtils';
import { doEval } from '../../internal/utils';
import { getTextContent } from '../../util/domUtils';
import { doEval, isElement } from '../../internal/utils';

/**
* Codec for {@link Stylesheet}s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { GlobalConfig } from '../../../util/config';
import { convertPoint } from '../../../util/styleUtils';
import { getClientX, getClientY } from '../../../util/EventUtils';
import InternalEvent from '../../../view/event/InternalEvent';
import { getChildNodes, getTextContent, isElement } from '../../../util/domUtils';
import { getChildNodes, getTextContent } from '../../../util/domUtils';
import Translations from '../../../i18n/Translations';
import { doEval } from '../../../internal/utils';
import { doEval, isElement } from '../../../internal/utils';

/**
* Custom codec for configuring {@link EditorToolbar}s.
Expand Down
38 changes: 0 additions & 38 deletions packages/core/src/util/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
*/

import Client from '../Client';
import { GlobalConfig } from './config';

/**
* A singleton class that provides cross-browser helper methods.
Expand Down Expand Up @@ -53,40 +52,3 @@ export const utils = {
*/
errorImage: `${Client.imageBasePath}/error.gif`,
};

/**
* @private not part of the public API, can be removed or changed without prior notice
*/
export const isNullish = (v: string | object | null | undefined | number | boolean) =>
v === null || v === undefined;

/**
* Merge a mixin into the destination
* @param dest the destination class
*
* @private not part of the public API, can be removed or changed without prior notice
*/
export const mixInto = (dest: any) => (mixin: any) => {
const keys = Reflect.ownKeys(mixin);
try {
for (const key of keys) {
Object.defineProperty(dest.prototype, key, {
value: mixin[key],
writable: true,
});
}
} catch (e) {
GlobalConfig.logger.error('Error while mixing', e);
}
};

export const copyTextToClipboard = (text: string): void => {
navigator.clipboard.writeText(text).then(
function () {
GlobalConfig.logger.info('Async: Copying to clipboard was successful!');
},
function (err) {
GlobalConfig.logger.error('Async: Could not copy text: ', err);
}
);
};
Loading