Skip to content

Commit

Permalink
feat(util): expose calc() expression API (#2749)
Browse files Browse the repository at this point in the history
  • Loading branch information
kumilingus authored Sep 2, 2024
1 parent 0d29e70 commit f842946
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 33 deletions.
6 changes: 3 additions & 3 deletions packages/joint-core/src/dia/attributes/eval.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isCalcAttribute, evalCalcAttribute } from './calc.mjs';
import { isCalcExpression, evalCalcExpression } from '../../util/calc.mjs';

const calcAttributesList = [
'transform',
Expand Down Expand Up @@ -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);
}
Expand Down
10 changes: 5 additions & 5 deletions packages/joint-core/src/dia/attributes/text.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/joint-core/src/elementTools/HoverConnect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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({

Expand All @@ -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));
},
Expand Down
9 changes: 4 additions & 5 deletions packages/joint-core/src/layout/ports/port.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 };
}
Expand Down
9 changes: 4 additions & 5 deletions packages/joint-core/src/linkTools/Button.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/joint-core/src/util/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
6 changes: 6 additions & 0 deletions packages/joint-core/types/joint.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit f842946

Please sign in to comment.