diff --git a/packages/joint-core/src/dia/attributes/eval.mjs b/packages/joint-core/src/dia/attributes/eval.mjs index 6088c4b4a..b8ad118dc 100644 --- a/packages/joint-core/src/dia/attributes/eval.mjs +++ b/packages/joint-core/src/dia/attributes/eval.mjs @@ -1,4 +1,4 @@ -import { isCalcAttribute, evalCalcAttribute } from './calc.mjs'; +import { isCalcExpression, evalCalcExpression } from '../../util/calc.mjs'; const calcAttributesList = [ 'transform', @@ -53,8 +53,8 @@ export function evalAttributes(attrs, refBBox) { } export function evalAttribute(attrName, attrValue, refBBox) { - if (attrName in calcAttributes && isCalcAttribute(attrValue)) { - let evalAttrValue = evalCalcAttribute(attrValue, refBBox); + if (attrName in calcAttributes && isCalcExpression(attrValue)) { + let evalAttrValue = evalCalcExpression(attrValue, refBBox); if (attrName in positiveValueAttributes) { evalAttrValue = Math.max(0, evalAttrValue); } diff --git a/packages/joint-core/src/dia/attributes/text.mjs b/packages/joint-core/src/dia/attributes/text.mjs index 0aefef4d8..58e1d1a93 100644 --- a/packages/joint-core/src/dia/attributes/text.mjs +++ b/packages/joint-core/src/dia/attributes/text.mjs @@ -1,5 +1,5 @@ import { assign, isPlainObject, isObject, isPercentage, breakText } from '../../util/util.mjs'; -import { isCalcAttribute, evalCalcAttribute } from './calc.mjs'; +import { isCalcExpression, evalCalcExpression } from '../../util/calc.mjs'; import $ from '../../mvc/Dom/index.mjs'; import V from '../../V/index.mjs'; @@ -94,8 +94,8 @@ const textAttributesNS = { var width = value.width || 0; if (isPercentage(width)) { size.width = refBBox.width * parseFloat(width) / 100; - } else if (isCalcAttribute(width)) { - size.width = Number(evalCalcAttribute(width, refBBox)); + } else if (isCalcExpression(width)) { + size.width = Number(evalCalcExpression(width, refBBox)); } else { if (value.width === null) { // breakText() requires width to be specified. @@ -110,8 +110,8 @@ const textAttributesNS = { var height = value.height || 0; if (isPercentage(height)) { size.height = refBBox.height * parseFloat(height) / 100; - } else if (isCalcAttribute(height)) { - size.height = Number(evalCalcAttribute(height, refBBox)); + } else if (isCalcExpression(height)) { + size.height = Number(evalCalcExpression(height, refBBox)); } else { if (value.height === null) { // if height is not specified breakText() does not diff --git a/packages/joint-core/src/elementTools/HoverConnect.mjs b/packages/joint-core/src/elementTools/HoverConnect.mjs index 5fcbb4362..a657a2de6 100644 --- a/packages/joint-core/src/elementTools/HoverConnect.mjs +++ b/packages/joint-core/src/elementTools/HoverConnect.mjs @@ -2,7 +2,7 @@ import { HoverConnect as LinkHoverConnect } from '../linkTools/HoverConnect.mjs' import V from '../V/index.mjs'; import * as g from '../g/index.mjs'; import { getViewBBox } from '../linkTools/helpers.mjs'; -import { isCalcAttribute, evalCalcAttribute } from '../dia/attributes/calc.mjs'; +import { isCalcExpression, evalCalcExpression } from '../util/calc.mjs'; export const HoverConnect = LinkHoverConnect.extend({ @@ -15,9 +15,9 @@ export const HoverConnect = LinkHoverConnect.extend({ if (typeof trackPath === 'function') { trackPath = trackPath.call(this, view); } - if (isCalcAttribute(trackPath)) { + if (isCalcExpression(trackPath)) { const bbox = getViewBBox(view, useModelGeometry); - trackPath = evalCalcAttribute(trackPath, bbox); + trackPath = evalCalcExpression(trackPath, bbox); } return new g.Path(V.normalizePathData(trackPath)); }, diff --git a/packages/joint-core/src/layout/ports/port.mjs b/packages/joint-core/src/layout/ports/port.mjs index 20a6dbb05..f86d80c1f 100644 --- a/packages/joint-core/src/layout/ports/port.mjs +++ b/packages/joint-core/src/layout/ports/port.mjs @@ -1,4 +1,3 @@ -import { evalCalcAttribute, isCalcAttribute } from '../../dia/attributes/calc.mjs'; import * as g from '../../g/index.mjs'; import * as util from '../../util/index.mjs'; @@ -58,13 +57,13 @@ function argTransform(bbox, args) { let { x, y, angle } = args; if (util.isPercentage(x)) { x = parseFloat(x) / 100 * bbox.width; - } else if (isCalcAttribute(x)) { - x = Number(evalCalcAttribute(x, bbox)); + } else if (util.isCalcExpression(x)) { + x = Number(util.evalCalcExpression(x, bbox)); } if (util.isPercentage(y)) { y = parseFloat(y) / 100 * bbox.height; - } else if (isCalcAttribute(y)) { - y = Number(evalCalcAttribute(y, bbox)); + } else if (util.isCalcExpression(y)) { + y = Number(util.evalCalcExpression(y, bbox)); } return { x, y, angle }; } diff --git a/packages/joint-core/src/linkTools/Button.mjs b/packages/joint-core/src/linkTools/Button.mjs index cb01207fe..718e2cf46 100644 --- a/packages/joint-core/src/linkTools/Button.mjs +++ b/packages/joint-core/src/linkTools/Button.mjs @@ -1,4 +1,3 @@ -import { evalCalcAttribute, isCalcAttribute } from '../dia/attributes/calc.mjs'; import { ToolView } from '../dia/ToolView.mjs'; import { getViewBBox } from './helpers.mjs'; import * as util from '../util/index.mjs'; @@ -41,13 +40,13 @@ export const Button = ToolView.extend({ const { x: offsetX = 0, y: offsetY = 0 } = offset; if (util.isPercentage(x)) { x = parseFloat(x) / 100 * bbox.width; - } else if (isCalcAttribute(x)) { - x = Number(evalCalcAttribute(x, bbox)); + } else if (util.isCalcExpression(x)) { + x = Number(util.evalCalcExpression(x, bbox)); } if (util.isPercentage(y)) { y = parseFloat(y) / 100 * bbox.height; - } else if (isCalcAttribute(y)) { - y = Number(evalCalcAttribute(y, bbox)); + } else if (util.isCalcExpression(y)) { + y = Number(util.evalCalcExpression(y, bbox)); } let matrix = V.createSVGMatrix().translate(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2); if (rotate) matrix = matrix.rotate(angle); diff --git a/packages/joint-core/src/dia/attributes/calc.mjs b/packages/joint-core/src/util/calc.mjs similarity index 75% rename from packages/joint-core/src/dia/attributes/calc.mjs rename to packages/joint-core/src/util/calc.mjs index a5e7ec3a1..9b39ec5b4 100644 --- a/packages/joint-core/src/dia/attributes/calc.mjs +++ b/packages/joint-core/src/util/calc.mjs @@ -10,18 +10,22 @@ const props = { const propsList = Object.keys(props).map(key => props[key]).join(''); const numberPattern = '[-+]?[0-9]*\\.?[0-9]+(?:[eE][-+]?[0-9]+)?'; const findSpacesRegex = /\s/g; -const parseExpressionRegExp = new RegExp(`^(${numberPattern}\\*)?([${propsList}])(/${numberPattern})?([-+]{1,2}${numberPattern})?$`, 'g'); +const parseFormulaRegExp = new RegExp(`^(${numberPattern}\\*)?([${propsList}])(/${numberPattern})?([-+]{1,2}${numberPattern})?$`, 'g'); function throwInvalid(expression) { throw new Error(`Invalid calc() expression: ${expression}`); } -export function evalCalcExpression(expression, bbox) { - const match = parseExpressionRegExp.exec(expression.replace(findSpacesRegex, '')); - if (!match) throwInvalid(expression); - parseExpressionRegExp.lastIndex = 0; // reset regex results for the next run +/* +* Evaluate the given calc formula. +* e.g. 'w + 10' in a rect 100x100 -> 110 +*/ +export function evalCalcFormula(formula, rect) { + const match = parseFormulaRegExp.exec(formula.replace(findSpacesRegex, '')); + if (!match) throwInvalid(formula); + parseFormulaRegExp.lastIndex = 0; // reset regex results for the next run const [,multiply, property, divide, add] = match; - const { x, y, width, height } = bbox; + const { x, y, width, height } = rect; let value = 0; switch (property) { case props.width: { @@ -81,15 +85,23 @@ function evalAddExpression(addExpression) { return parseFloat(addExpression); } -export function isCalcAttribute(value) { +/* +* Check if the given value is a calc expression. +* e.g. 'calc(10 + 100)' -> true +*/ +export function isCalcExpression(value) { return typeof value === 'string' && value.includes('calc'); } const calcStart = 'calc('; const calcStartOffset = calcStart.length; -export function evalCalcAttribute(attributeValue, refBBox) { - let value = attributeValue; +/* +* Evaluate all calc formulas in the given expression. +* e.g. 'calc(w + 10)' in rect 100x100 -> '110' +*/ +export function evalCalcExpression(expression, rect) { + let value = expression; let startSearchIndex = 0; do { let calcIndex = value.indexOf(calcStart, startSearchIndex); @@ -116,11 +128,11 @@ export function evalCalcAttribute(attributeValue, refBBox) { } while (true); // Get the calc() expression without nested calcs (recursion) let expression = value.slice(calcIndex + calcStartOffset, calcEndIndex); - if (isCalcAttribute(expression)) { - expression = evalCalcAttribute(expression, refBBox); + if (isCalcExpression(expression)) { + expression = evalCalcExpression(expression, rect); } // Eval the calc() expression without nested calcs. - const calcValue = String(evalCalcExpression(expression, refBBox)); + const calcValue = String(evalCalcFormula(expression, rect)); // Replace the calc() expression and continue search value = value.slice(0, calcIndex) + calcValue + value.slice(calcEndIndex + 1); startSearchIndex = calcIndex + calcValue.length; diff --git a/packages/joint-core/src/util/index.mjs b/packages/joint-core/src/util/index.mjs index 0083e81d5..dc4c8965c 100644 --- a/packages/joint-core/src/util/index.mjs +++ b/packages/joint-core/src/util/index.mjs @@ -2,4 +2,5 @@ export * from './wrappers.mjs'; export * from './util.mjs'; export * from './cloneCells.mjs'; export * from './svgTagTemplate.mjs'; +export * from './calc.mjs'; export { getRectPoint } from './getRectPoint.mjs'; diff --git a/packages/joint-core/types/joint.d.ts b/packages/joint-core/types/joint.d.ts index 8c06a315f..418ac48c9 100644 --- a/packages/joint-core/types/joint.d.ts +++ b/packages/joint-core/types/joint.d.ts @@ -2451,6 +2451,12 @@ export namespace shapes { export namespace util { + export function isCalcExpression(value: any): boolean; + + export function evalCalcFormula(formula: string, rect: g.PlainRect): number; + + export function evalCalcExpression(expression: string, rect: g.PlainRect): string; + export function hashCode(str: string): string; export function getByPath(object: { [key: string]: any }, path: string | string[], delim?: string): any;