Merge pull request #4259 from mermaid-js/svgDrawRefactor

Refactor to consolidate shared svgDraw components
This commit is contained in:
Justin Greywolf 2023-05-03 16:57:09 -07:00 committed by GitHub
commit 7fd4814abc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 219 deletions

View File

@ -90,8 +90,12 @@
"sidharth",
"sidharthv",
"sphinxcontrib",
"startx",
"starty",
"statediagram",
"steph",
"stopx",
"stopy",
"stylis",
"substate",
"sveidqvist",
@ -104,6 +108,7 @@
"tuleap",
"ugge",
"unist",
"valign",
"verdana",
"viewports",
"vinod",

View File

@ -1,28 +1,9 @@
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
for (let attrKey in rectData.attrs) {
rectElem.attr(attrKey, rectData.attrs[attrKey]);
}
}
if (rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class);
}
return rectElem;
return svgDrawCommon.drawRect(elem, rectData);
};
export const drawImage = function (elem, width, height, x, y, link) {
@ -236,7 +217,8 @@ export const drawC4Shape = function (elem, c4Shape, conf) {
// <rect fill="#08427B" height="119.2188" rx="2.5" ry="2.5" stroke="#073B6F" stroke-width="0.5" width="110" x="120" y="7"/>
// draw rect of c4Shape
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
switch (c4Shape.typeC4Shape.text) {
case 'person':
case 'external_person':
@ -479,6 +461,7 @@ export const insertArrowHead = function (elem) {
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
};
export const insertArrowEnd = function (elem) {
elem
.append('defs')
@ -493,6 +476,7 @@ export const insertArrowEnd = function (elem) {
.append('path')
.attr('d', 'M 10 0 L 0 5 L 10 10 z'); // this is actual shape for arrowhead
};
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*
@ -511,6 +495,7 @@ export const insertArrowFilledHead = function (elem) {
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};
/**
* Setup node number. The result is appended to the svg.
*
@ -532,6 +517,7 @@ export const insertDynamicNumber = function (elem) {
.attr('r', 6);
// .style("fill", '#f00');
};
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*
@ -568,20 +554,6 @@ export const insertArrowCrossHead = function (elem) {
// this is actual shape for arrowhead
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
stroke: '#666',
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
};
const getC4ShapeFont = (cnf, typeC4Shape) => {
return {
fontFamily: cnf[typeC4Shape + 'FontFamily'],
@ -714,6 +686,4 @@ export default {
insertDatabaseIcon,
insertComputerIcon,
insertClockIcon,
getNoteRect,
sanitizeUrl, // TODO why is this exported?
};

View File

@ -486,6 +486,7 @@ const buildLegacyDisplay = function (text) {
cssStyle,
};
};
/**
* Adds a <tspan> for a member in a diagram
*

View File

@ -0,0 +1,114 @@
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
for (let attrKey in rectData.attrs) {
rectElem.attr(attrKey, rectData.attrs[attrKey]);
}
}
if (rectData.class !== 'undefined') {
rectElem.attr('class', rectData.class);
}
return rectElem;
};
/**
* Draws a background rectangle
*
* @param {any} elem Diagram (reference for bounds)
* @param {any} bounds Shape of the rectangle
*/
export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
});
rectElem.lower();
};
export const drawText = function (elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.text(nText);
return textElem;
};
export const drawImage = function (elem, x, y, link) {
const imageElem = elem.append('image');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = function (elem, x, y, link) {
const imageElem = elem.append('use');
imageElem.attr('x', x);
imageElem.attr('y', y);
const sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', '#' + sanitizedLink);
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#EDF2AE',
stroke: '#666',
anchor: 'start',
rx: 0,
ry: 0,
};
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
width: 100,
height: 100,
fill: undefined,
anchor: undefined,
'text-anchor': 'start',
style: '#666',
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};

View File

@ -70,6 +70,7 @@ const defaultBkg = function (elem, node, section) {
.attr('x2', node.width)
.attr('y2', node.height);
};
const rectBkg = function (elem, node) {
elem
.append('rect')
@ -78,6 +79,7 @@ const rectBkg = function (elem, node) {
.attr('height', node.height)
.attr('width', node.width);
};
const cloudBkg = function (elem, node) {
const w = node.width;
const h = node.height;
@ -108,6 +110,7 @@ const cloudBkg = function (elem, node) {
H0 V0 Z`
);
};
const bangBkg = function (elem, node) {
const w = node.width;
const h = node.height;
@ -139,6 +142,7 @@ const bangBkg = function (elem, node) {
H0 V0 Z`
);
};
const circleBkg = function (elem, node) {
elem
.append('circle')

View File

@ -3,6 +3,7 @@ import { select, selectAll } from 'd3';
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js';
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import * as configApi from '../../config.js';
import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';
@ -225,7 +226,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) {
bounds.bumpVerticalPos(conf.boxMargin);
noteModel.height = conf.boxMargin;
noteModel.starty = bounds.getVerticalPos();
const rect = svgDraw.getNoteRect();
const rect = svgDrawCommon.getNoteRect();
rect.x = noteModel.startx;
rect.y = noteModel.starty;
rect.width = noteModel.width || conf.width;
@ -233,7 +234,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) {
const g = elem.append('g');
const rectElem = svgDraw.drawRect(g, rect);
const textObj = svgDraw.getTextObj();
const textObj = svgDrawCommon.getTextObj();
textObj.x = noteModel.startx;
textObj.y = noteModel.starty;
textObj.width = rect.width;
@ -347,7 +348,7 @@ function boundMessage(_diagram, msgModel): number {
const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) {
const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel;
const textDims = utils.calculateTextDimensions(message, messageFont(conf));
const textObj = svgDraw.getTextObj();
const textObj = svgDrawCommon.getTextObj();
textObj.x = startx;
textObj.y = starty + 10;
textObj.width = stopx - startx;

View File

@ -1,33 +1,13 @@
import common from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon';
import { addFunction } from '../../interactionDb.js';
import { parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.class !== undefined) {
rectElem.attr('class', rectData.class);
}
return rectElem;
return svgDrawCommon.drawRect(elem, rectData);
};
// const sanitizeUrl = function (s) {
// return s
// .replace(/&/g, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/javascript:/g, '');
// };
const addPopupInteraction = (id, actorCnt) => {
addFunction(() => {
const arr = document.querySelectorAll(id);
@ -43,6 +23,7 @@ const addPopupInteraction = (id, actorCnt) => {
});
});
};
export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMenus) {
if (actor.links === undefined || actor.links === null || Object.keys(actor.links).length === 0) {
return { height: 0, width: 0 };
@ -107,22 +88,6 @@ export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMe
return { height: rectData.height + linkY, width: menuWidth };
};
export const drawImage = function (elem, x, y, link) {
const imageElem = elem.append('image');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', sanitizedLink);
};
export const drawEmbeddedImage = function (elem, x, y, link) {
const imageElem = elem.append('use');
imageElem.attr('x', x);
imageElem.attr('y', y);
var sanitizedLink = sanitizeUrl(link);
imageElem.attr('xlink:href', '#' + sanitizedLink);
};
export const popupMenu = function (popid) {
return (
"var pu = document.getElementById('" +
@ -152,9 +117,10 @@ const popupMenuDownFunc = function (popupId) {
pu.style.display = 'none';
}
};
export const drawText = function (elem, textData) {
let prevTextHeight = 0,
textHeight = 0;
let prevTextHeight = 0;
let textHeight = 0;
const lines = textData.text.split(common.lineBreakRegex);
const [_textFontSize, _textFontSizePx] = parseFontSize(textData.fontSize);
@ -188,6 +154,7 @@ export const drawText = function (elem, textData) {
break;
}
}
if (
textData.anchor !== undefined &&
textData.textMargin !== undefined &&
@ -217,6 +184,7 @@ export const drawText = function (elem, textData) {
break;
}
}
for (let [i, line] of lines.entries()) {
if (
textData.textMargin !== undefined &&
@ -371,7 +339,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
}
}
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
var cssclass = 'actor';
if (actor.properties != null && actor.properties['class']) {
cssclass = actor.properties['class'];
@ -391,9 +359,9 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
if (actor.properties != null && actor.properties['icon']) {
const iconSrc = actor.properties['icon'].trim();
if (iconSrc.charAt(0) === '@') {
drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
svgDrawCommon.drawEmbeddedImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc.substr(1));
} else {
drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
svgDrawCommon.drawImage(g, rect.x + rect.width - 20, rect.y + 10, iconSrc);
}
}
@ -438,7 +406,7 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const actElem = elem.append('g');
actElem.attr('class', 'actor-man');
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
rect.x = actor.x;
rect.y = actor.y;
rect.fill = '#eaeaea';
@ -447,7 +415,6 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
rect.class = 'actor';
rect.rx = 3;
rect.ry = 3;
// drawRect(actElem, rect);
actElem
.append('line')
@ -532,6 +499,7 @@ export const drawBox = function (elem, box, conf) {
export const anchorElement = function (elem) {
return elem.append('g');
};
/**
* Draws an activation in the diagram
*
@ -542,7 +510,7 @@ export const anchorElement = function (elem) {
* @param {any} actorActivations - Number of activations on the actor.
*/
export const drawActivation = function (elem, bounds, verticalPos, conf, actorActivations) {
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
const g = bounds.anchored;
rect.x = bounds.startx;
rect.y = bounds.starty;
@ -594,7 +562,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
});
}
let txt = getTextObj();
let txt = svgDrawCommon.getTextObj();
txt.text = labelText;
txt.x = loopModel.startx;
txt.y = loopModel.starty;
@ -610,7 +578,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
txt.class = 'labelText';
drawLabel(g, txt);
txt = getTextObj();
txt = svgDrawCommon.getTextObj();
txt.text = loopModel.title;
txt.x = loopModel.startx + labelBoxWidth / 2 + (loopModel.stopx - loopModel.startx) / 2;
txt.y = loopModel.starty + boxMargin + boxTextMargin;
@ -661,16 +629,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
* @param {any} bounds Shape of the rectangle
*/
export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
stroke: bounds.stroke,
class: 'rect',
});
rectElem.lower();
svgDrawCommon.drawBackgroundRect(elem, bounds);
};
export const insertDatabaseIcon = function (elem) {
@ -737,6 +696,7 @@ export const insertArrowHead = function (elem) {
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
};
/**
* Setup arrow head and define the marker. The result is appended to the svg.
*
@ -755,6 +715,7 @@ export const insertArrowFilledHead = function (elem) {
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
};
/**
* Setup node number. The result is appended to the svg.
*
@ -776,6 +737,7 @@ export const insertSequenceNumber = function (elem) {
.attr('r', 6);
// .style("fill", '#f00');
};
/**
* Setup cross head and define the marker. The result is appended to the svg.
*
@ -802,37 +764,6 @@ export const insertArrowCrossHead = function (elem) {
// this is actual shape for arrowhead
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
fill: undefined,
anchor: undefined,
style: '#666',
width: undefined,
height: undefined,
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
stroke: '#666',
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
};
const _drawTextCandidateFunc = (function () {
/**
* @param {any} content
@ -1062,8 +993,6 @@ export default {
drawActor,
drawBox,
drawPopup,
drawImage,
drawEmbeddedImage,
anchorElement,
drawActivation,
drawLoop,
@ -1075,8 +1004,6 @@ export default {
insertDatabaseIcon,
insertComputerIcon,
insertClockIcon,
getTextObj,
getNoteRect,
popupMenu,
popdownMenu,
fixLifeLineHeights,

View File

@ -174,16 +174,4 @@ describe('svgDraw', function () {
expect(rect.lower).toHaveBeenCalled();
});
});
describe('sanitizeUrl', function () {
it('should sanitize malicious urls', function () {
const maliciousStr = 'javascript:script:alert(1)';
const result = svgDraw.sanitizeUrl(maliciousStr);
expect(result).not.toContain('javascript:alert(1)');
});
it('should not sanitize non dangerous urls', function () {
const maliciousStr = 'javajavascript:script:alert(1)';
const result = svgDraw.sanitizeUrl(maliciousStr);
expect(result).not.toContain('javascript:alert(1)');
});
});
});

View File

@ -1,21 +1,8 @@
import { arc as d3arc } from 'd3';
import * as svgDrawCommon from '../common/svgDrawCommon';
export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect');
rectElem.attr('x', rectData.x);
rectElem.attr('y', rectData.y);
rectElem.attr('fill', rectData.fill);
rectElem.attr('stroke', rectData.stroke);
rectElem.attr('width', rectData.width);
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
if (rectData.class !== undefined) {
rectElem.attr('class', rectData.class);
}
return rectElem;
return svgDrawCommon.drawRect(elem, rectData);
};
export const drawFace = function (element, faceData) {
@ -128,25 +115,7 @@ export const drawCircle = function (element, circleData) {
};
export const drawText = function (elem, textData) {
// Remove and ignore br:s
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
const textElem = elem.append('text');
textElem.attr('x', textData.x);
textElem.attr('y', textData.y);
textElem.attr('class', 'legend');
textElem.style('text-anchor', textData.anchor);
if (textData.class !== undefined) {
textElem.attr('class', textData.class);
}
const span = textElem.append('tspan');
span.attr('x', textData.x + textData.textMargin * 2);
span.text(nText);
return textElem;
return svgDrawCommon.drawText(elem, textData);
};
export const drawLabel = function (elem, txtObject) {
@ -192,7 +161,7 @@ export const drawLabel = function (elem, txtObject) {
export const drawSection = function (elem, section, conf) {
const g = elem.append('g');
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
rect.x = section.x;
rect.y = section.y;
rect.fill = section.fill;
@ -249,7 +218,7 @@ export const drawTask = function (elem, task, conf) {
score: task.score,
});
const rect = getNoteRect();
const rect = svgDrawCommon.getNoteRect();
rect.x = task.x;
rect.y = task.y;
rect.fill = task.fill;
@ -298,41 +267,7 @@ export const drawTask = function (elem, task, conf) {
* @param {any} bounds The bounds of the drawing
*/
export const drawBackgroundRect = function (elem, bounds) {
const rectElem = drawRect(elem, {
x: bounds.startx,
y: bounds.starty,
width: bounds.stopx - bounds.startx,
height: bounds.stopy - bounds.starty,
fill: bounds.fill,
class: 'rect',
});
rectElem.lower();
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
fill: undefined,
'text-anchor': 'start',
width: 100,
height: 100,
textMargin: 0,
rx: 0,
ry: 0,
};
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
svgDrawCommon.drawBackgroundRect(elem, bounds);
};
const _drawTextCandidateFunc = (function () {
@ -475,7 +410,5 @@ export default {
drawLabel,
drawTask,
drawBackgroundRect,
getTextObj,
getNoteRect,
initGraphics,
};