diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index f04fc5fe22..a6277f27da 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -7,9 +7,17 @@ import erMarkers from './erMarkers'; import { configureSvgSize } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; import { parseGenericTypes } from '../common/common'; +import { v4 as uuid4 } from 'uuid'; +/** Regex used to remove chars from the entity name so the result can be used in an id */ +const BAD_ID_CHARS_REGEXP = /[^A-Za-z0-9]([\W])*/g; + +// Configuration let conf = {}; +// Map so we can look up the id of an entity based on the name +let entityNameIds = new Map(); + /** * Allows the top-level API module to inject config specific to this renderer, storing it in the * local conf object. Note that generic config still needs to be retrieved using getConfig() @@ -313,15 +321,18 @@ const drawEntities = function (svgNode, entities, graph) { const keys = Object.keys(entities); let firstOne; - keys.forEach(function (id) { + keys.forEach(function (entityName) { + const entityId = generateId(entityName, 'entity'); + entityNameIds.set(entityName, entityId); + // Create a group for each entity - const groupNode = svgNode.append('g').attr('id', id); + const groupNode = svgNode.append('g').attr('id', entityId); - firstOne = firstOne === undefined ? id : firstOne; + firstOne = firstOne === undefined ? entityId : firstOne; // Label the entity - this is done first so that we can get the bounding box // which then determines the size of the rectangle - const textId = 'entity-' + id; + const textId = 'text-' + entityId; const textNode = groupNode .append('text') .attr('class', 'er entityLabel') @@ -334,12 +345,12 @@ const drawEntities = function (svgNode, entities, graph) { 'style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px' ) - .text(id); + .text(entityName); const { width: entityWidth, height: entityHeight } = drawAttributes( groupNode, textNode, - entities[id].attributes + entities[entityName].attributes ); // Draw the rectangle - insert it before the text so that the text is not obscured @@ -356,12 +367,12 @@ const drawEntities = function (svgNode, entities, graph) { const rectBBox = rectNode.node().getBBox(); - // Add the entity to the graph - graph.setNode(id, { + // Add the entity to the graph using the entityId + graph.setNode(entityId, { width: rectBBox.width, height: rectBBox.height, shape: 'rect', - id: id, + id: entityId, }); }); return firstOne; @@ -405,7 +416,12 @@ const getEdgeName = function (rel) { */ const addRelationships = function (relationships, g) { relationships.forEach(function (r) { - g.setEdge(r.entityA, r.entityB, { relationship: r }, getEdgeName(r)); + g.setEdge( + entityNameIds.get(r.entityA), + entityNameIds.get(r.entityB), + { relationship: r }, + getEdgeName(r) + ); }); return relationships; }; // addRelationships @@ -425,7 +441,11 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) { relCnt++; // Find the edge relating to this relationship - const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel)); + const edge = g.edge( + entityNameIds.get(rel.entityA), + entityNameIds.get(rel.entityB), + getEdgeName(rel) + ); // Get a function that will generate the line path const lineFunction = line() @@ -652,6 +672,35 @@ export const draw = function (text, id, _version, diagObj) { addSVGAccessibilityFields(diagObj.db, svg, id); }; // draw +/** + * Return a unique id based on the given string. Start with the prefix, then a hyphen, then the + * simplified str, then a hyphen, then a unique uuid. (Hyphens are only included if needed.) + * Although the official XML standard for ids says that many more characters are valid in the id, + * this keeps things simple by accepting only A-Za-z0-9. + * + * @param {string} [str?=''] Given string to use as the basis for the id. Default is `''` + * @param {string} [prefix?=''] String to put at the start, followed by '-'. Default is `''` + * @param str + * @param prefix + * @returns {string} + * @see https://www.w3.org/TR/xml/#NT-Name + */ +export function generateId(str = '', prefix = '') { + const simplifiedStr = str.replace(BAD_ID_CHARS_REGEXP, ''); + return `${strWithHyphen(prefix)}${strWithHyphen(simplifiedStr)}${uuid4()}`; +} + +/** + * Append a hyphen to a string only if the string isn't empty + * + * @param {string} str + * @returns {string} + * @todo This could be moved into a string utility file/class. + */ +function strWithHyphen(str = '') { + return str.length > 0 ? `${str}-` : ''; +} + export default { setConf, draw,