diff --git a/src/diagrams/class/classDiagram.spec.js b/src/diagrams/class/classDiagram.spec.js index ef319d156..ef2a98415 100644 --- a/src/diagrams/class/classDiagram.spec.js +++ b/src/diagrams/class/classDiagram.spec.js @@ -121,6 +121,8 @@ describe('class diagram, ', function () { ' flightNumber : Integer\n' + ' departureTime : Date\n' + '}'; + + parser.parse(str); }); it('should handle class definitions', function() { diff --git a/src/diagrams/class/classMemberRenderer.js b/src/diagrams/class/classMemberRenderer.js deleted file mode 100644 index 8ac3f5998..000000000 --- a/src/diagrams/class/classMemberRenderer.js +++ /dev/null @@ -1,137 +0,0 @@ -export const addTspan = function(textEl, txt, isFirst, conf) { - let member = parseMember(txt); - - const tSpan = textEl - .append('tspan') - .attr('x', conf.padding) - .text(member.displayText); - - if (member.cssStyle !== '') { - tSpan.attr('style', member.cssStyle); - } - - if (!isFirst) { - tSpan.attr('dy', conf.textHeight); - } -}; - -export const buildFieldDisplay = function(parsedText) { - let visibility = parsedText[1] ? parsedText[1].trim() : ''; - let fieldType = parsedText[2] ? parsedText[2].trim() : ''; - let genericType = parsedText[3] ? parseGenericTypes(parsedText[3]) : ''; - let fieldName = parsedText[4] ? parsedText[4].trim() : ''; - - return { - displayText: visibility + fieldType + genericType + ' ' + fieldName, - cssStyle: '' - }; -}; - -export const buildMethodDisplay = function(parsedText) { - let cssStyle = ''; - let displayText = parsedText; - - let visibility = parsedText[1] ? parsedText[1].trim() : ''; - let methodName = parsedText[2] ? parsedText[2].trim() : ''; - let parameters = parsedText[3] ? parseGenericTypes(parsedText[3]) : ''; - let classifier = parsedText[6] ? parsedText[6].trim() : ''; - let returnType = parsedText[7] ? ' : ' + parseGenericTypes(parsedText[7]).trim() : ''; - - displayText = visibility + methodName + '(' + parameters + ')' + returnType; - - cssStyle = parseClassifier(classifier); - - let member = { - displayText: displayText, - cssStyle: cssStyle - }; - - return member; -}; - -export const buildLegacyDisplay = function(text) { - // if for some reason we dont have any match, use old format to parse text - let memberText = ''; - let cssStyle = ''; - let returnType = ''; - let methodStart = text.indexOf('('); - let methodEnd = text.indexOf(')'); - - if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) { - let parsedText = text.match(/(\+|-|~|#)?(\w+)/); - let visibility = parsedText[1] ? parsedText[1].trim() : ''; - let methodName = parsedText[2]; - let parameters = text.substring(methodStart + 1, methodEnd); - let classifier = text.substring(methodEnd, methodEnd + 1); - cssStyle = parseClassifier(classifier); - - memberText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')'; - - if (methodEnd < memberText.length) { - returnType = text.substring(methodEnd + 2).trim(); - if (returnType !== '') { - returnType = ' : ' + parseGenericTypes(returnType); - } - } - } else { - // finally - if all else fails, just send the text back as written (other than parsing for generic types) - memberText = parseGenericTypes(text); - } - - let member = { - displayText: memberText + returnType, - cssStyle: cssStyle - }; - - return member; -}; - -export const parseGenericTypes = function(text) { - let cleanedText = text; - - if (text.indexOf('~') != -1) { - cleanedText = cleanedText.replace('~', '<'); - cleanedText = cleanedText.replace('~', '>'); - - return parseGenericTypes(cleanedText); - } else { - return cleanedText; - } -}; - -export const parseMember = function(text) { - const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+)$/; - const methodRegEx = /^(\+|-|~|#)?(\w+)\s?\(\s*(\w+(~\w+~|\[\])?\s*(\w+)?)?\s*\)\s?([*|$])?\s?(\w+(~\w+~|\[\])?)?\s*$/; - //const methodRegEx = /(\+|-|~|#)?(\w+)\s?\(\s*(\w+(~\w+~|\[\])?\s*(\w+)?)?\s*\)\s?([*|$])?\s?(\w+(~\w+~|\[\])?)?/; - - let fieldMatch = text.match(fieldRegEx); - let methodMatch = text.match(methodRegEx); - - if (fieldMatch) { - return buildFieldDisplay(fieldMatch); - } else if (methodMatch) { - return buildMethodDisplay(methodMatch); - } else { - return buildLegacyDisplay(text); - } -}; - -const parseClassifier = function(classifier) { - switch (classifier) { - case '*': - return 'font-style:italic;'; - case '$': - return 'text-decoration:underline;'; - default: - return ''; - } -}; - -export default { - addTspan, - buildFieldDisplay, - buildLegacyDisplay, - buildMethodDisplay, - parseGenericTypes, - parseMember -}; diff --git a/src/diagrams/class/classRenderer.js b/src/diagrams/class/classRenderer.js index 97543d1de..3943b889b 100644 --- a/src/diagrams/class/classRenderer.js +++ b/src/diagrams/class/classRenderer.js @@ -3,9 +3,8 @@ import dagre from 'dagre'; import graphlib from 'graphlib'; import { logger } from '../../logger'; import classDb, { lookUpDomId } from './classDb'; -import utils from '../../utils'; import { parser } from './parser/classDiagram'; -import memberRenderer from './classMemberRenderer'; +import svgDraw from './svgDraw'; parser.yy = classDb; @@ -135,285 +134,6 @@ const insertMarkers = function(elem) { .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); }; -let edgeCount = 0; -const drawEdge = function(elem, path, relation) { - const getRelationType = function(type) { - switch (type) { - case classDb.relationType.AGGREGATION: - return 'aggregation'; - case classDb.relationType.EXTENSION: - return 'extension'; - case classDb.relationType.COMPOSITION: - return 'composition'; - case classDb.relationType.DEPENDENCY: - return 'dependency'; - } - }; - - path.points = path.points.filter(p => !Number.isNaN(p.y)); - - // The data for our line - const lineData = path.points; - - // This is the accessor function we talked about above - const lineFunction = d3 - .line() - .x(function(d) { - return d.x; - }) - .y(function(d) { - return d.y; - }) - .curve(d3.curveBasis); - - const svgPath = elem - .append('path') - .attr('d', lineFunction(lineData)) - .attr('id', 'edge' + edgeCount) - .attr('class', 'relation'); - let url = ''; - if (conf.arrowMarkerAbsolute) { - url = - window.location.protocol + - '//' + - window.location.host + - window.location.pathname + - window.location.search; - url = url.replace(/\(/g, '\\('); - url = url.replace(/\)/g, '\\)'); - } - - if (relation.relation.lineType == 1) { - svgPath.attr('class', 'relation dashed-line'); - } - if (relation.relation.type1 !== 'none') { - svgPath.attr( - 'marker-start', - 'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')' - ); - } - if (relation.relation.type2 !== 'none') { - svgPath.attr( - 'marker-end', - 'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')' - ); - } - - let x, y; - const l = path.points.length; - // Calculate Label position - let labalPosition = utils.calcLabelPosition(path.points); - x = labalPosition.x; - y = labalPosition.y; - - let p1_card_x, p1_card_y; - // p1_card_padd_x = conf.padding * 2, - // p1_card_padd_y = conf.padding; - let p2_card_x, p2_card_y; - // p2_card_padd_x = conf.padding * 2, - // p2_card_padd_y = -conf.padding / 2; - if (l % 2 !== 0 && l > 1) { - let cardinality_1_point = utils.calcCardinalityPosition( - relation.relation.type1 !== 'none', - path.points, - path.points[0] - ); - let cardinality_2_point = utils.calcCardinalityPosition( - relation.relation.type2 !== 'none', - path.points, - path.points[l - 1] - ); - - logger.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point)); - logger.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point)); - - p1_card_x = cardinality_1_point.x; - p1_card_y = cardinality_1_point.y; - p2_card_x = cardinality_2_point.x; - p2_card_y = cardinality_2_point.y; - } - - if (typeof relation.title !== 'undefined') { - const g = elem.append('g').attr('class', 'classLabel'); - const label = g - .append('text') - .attr('class', 'label') - .attr('x', x) - .attr('y', y) - .attr('fill', 'red') - .attr('text-anchor', 'middle') - .text(relation.title); - - window.label = label; - const bounds = label.node().getBBox(); - - g.insert('rect', ':first-child') - .attr('class', 'box') - .attr('x', bounds.x - conf.padding / 2) - .attr('y', bounds.y - conf.padding / 2) - .attr('width', bounds.width + conf.padding) - .attr('height', bounds.height + conf.padding); - } - - logger.info('Rendering relation ' + JSON.stringify(relation)); - if (typeof relation.relationTitle1 !== 'undefined' && relation.relationTitle1 !== 'none') { - const g = elem.append('g').attr('class', 'cardinality'); - g.append('text') - .attr('class', 'type1') - .attr('x', p1_card_x) - .attr('y', p1_card_y) - .attr('fill', 'black') - .attr('font-size', '6') - .text(relation.relationTitle1); - } - if (typeof relation.relationTitle2 !== 'undefined' && relation.relationTitle2 !== 'none') { - const g = elem.append('g').attr('class', 'cardinality'); - g.append('text') - .attr('class', 'type2') - .attr('x', p2_card_x) - .attr('y', p2_card_y) - .attr('fill', 'black') - .attr('font-size', '6') - .text(relation.relationTitle2); - } - - edgeCount++; -}; - -const drawClass = function(elem, classDef) { - logger.info('Rendering class ' + classDef); - - let cssClassStr = 'classGroup '; - if (classDef.cssClasses.length > 0) { - cssClassStr = cssClassStr + classDef.cssClasses.join(' '); - } - - const id = classDef.id; - const classInfo = { - id: id, - label: classDef.id, - width: 0, - height: 0 - }; - - // add class group - const g = elem - .append('g') - .attr('id', lookUpDomId(id)) - .attr('class', cssClassStr); - - // add title - let title; - if (classDef.link) { - title = g - .append('svg:a') - .attr('xlink:href', classDef.link) - .attr('target', '_blank') - .append('text') - .attr('y', conf.textHeight + conf.padding) - .attr('x', 0); - } else { - title = g - .append('text') - .attr('y', conf.textHeight + conf.padding) - .attr('x', 0); - } - - // add annotations - let isFirst = true; - classDef.annotations.forEach(function(member) { - const titleText2 = title.append('tspan').text('«' + member + '»'); - if (!isFirst) titleText2.attr('dy', conf.textHeight); - isFirst = false; - }); - - let classTitleString = classDef.id; - - if (classDef.type !== undefined && classDef.type !== '') { - classTitleString += '<' + classDef.type + '>'; - } - - const classTitle = title - .append('tspan') - .text(classTitleString) - .attr('class', 'title'); - - // If class has annotations the title needs to have an offset of the text height - if (!isFirst) classTitle.attr('dy', conf.textHeight); - - const titleHeight = title.node().getBBox().height; - - const membersLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); - - const members = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); - - isFirst = true; - classDef.members.forEach(function(member) { - memberRenderer.addTspan(members, member, isFirst, conf); - isFirst = false; - }); - - const membersBox = members.node().getBBox(); - - const methodsLine = g - .append('line') // text label for the x axis - .attr('x1', 0) - .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) - .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); - - const methods = g - .append('text') // text label for the x axis - .attr('x', conf.padding) - .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) - .attr('fill', 'white') - .attr('class', 'classText'); - - isFirst = true; - - classDef.methods.forEach(function(method) { - memberRenderer.addTspan(methods, method, isFirst, conf); - isFirst = false; - }); - - const classBox = g.node().getBBox(); - const rect = g - .insert('rect', ':first-child') - .attr('x', 0) - .attr('y', 0) - .attr('width', classBox.width + 2 * conf.padding) - .attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin); - - const rectWidth = rect.node().getBBox().width; - - // Center title - // We subtract the width of each text element from the class box width and divide it by 2 - title.node().childNodes.forEach(function(x) { - x.setAttribute('x', (rectWidth - x.getBBox().width) / 2); - }); - - if (classDef.tooltip) { - title.insert('title').text(classDef.tooltip); - } - - membersLine.attr('x2', rectWidth); - methodsLine.attr('x2', rectWidth); - - classInfo.width = rectWidth; - classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; - - idCache[id] = classInfo; - return classInfo; -}; - export const setConf = function(cnf) { const keys = Object.keys(cnf); @@ -434,7 +154,7 @@ export const draw = function(text, id) { logger.info('Rendering diagram ' + text); - /// / Fetch the default direction, use TD if none was found + // Fetch the default direction, use TD if none was found const diagram = d3.select(`[id='${id}']`); insertMarkers(diagram); @@ -457,7 +177,8 @@ export const draw = function(text, id) { const keys = Object.keys(classes); for (let i = 0; i < keys.length; i++) { const classDef = classes[keys[i]]; - const node = drawClass(diagram, classDef); + const node = svgDraw.drawClass(diagram, classDef, conf); + idCache[node.id] = node; // Add nodes to the graph. The first argument is the node id. The second is // metadata about the node. In this case we're going to add labels to each of @@ -481,6 +202,7 @@ export const draw = function(text, id) { relation.title || 'DEFAULT' ); }); + dagre.layout(g); g.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') { @@ -495,10 +217,11 @@ export const draw = function(text, id) { ); } }); + g.edges().forEach(function(e) { if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') { logger.debug('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))); - drawEdge(diagram, g.edge(e), g.edge(e).relation); + svgDraw.drawEdge(diagram, g.edge(e), g.edge(e).relation, conf); } }); diff --git a/src/diagrams/class/svgDraw.js b/src/diagrams/class/svgDraw.js new file mode 100644 index 000000000..49af592c4 --- /dev/null +++ b/src/diagrams/class/svgDraw.js @@ -0,0 +1,413 @@ +import * as d3 from 'd3'; +import classDb, { lookUpDomId } from './classDb'; +import utils from '../../utils'; +import { logger } from '../../logger'; + +let edgeCount = 0; +export const drawEdge = function(elem, path, relation, conf) { + const getRelationType = function(type) { + switch (type) { + case classDb.relationType.AGGREGATION: + return 'aggregation'; + case classDb.relationType.EXTENSION: + return 'extension'; + case classDb.relationType.COMPOSITION: + return 'composition'; + case classDb.relationType.DEPENDENCY: + return 'dependency'; + } + }; + + path.points = path.points.filter(p => !Number.isNaN(p.y)); + + // The data for our line + const lineData = path.points; + + // This is the accessor function we talked about above + const lineFunction = d3 + .line() + .x(function(d) { + return d.x; + }) + .y(function(d) { + return d.y; + }) + .curve(d3.curveBasis); + + const svgPath = elem + .append('path') + .attr('d', lineFunction(lineData)) + .attr('id', 'edge' + edgeCount) + .attr('class', 'relation'); + let url = ''; + if (conf.arrowMarkerAbsolute) { + url = + window.location.protocol + + '//' + + window.location.host + + window.location.pathname + + window.location.search; + url = url.replace(/\(/g, '\\('); + url = url.replace(/\)/g, '\\)'); + } + + if (relation.relation.lineType == 1) { + svgPath.attr('class', 'relation dashed-line'); + } + if (relation.relation.type1 !== 'none') { + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + getRelationType(relation.relation.type1) + 'Start' + ')' + ); + } + if (relation.relation.type2 !== 'none') { + svgPath.attr( + 'marker-end', + 'url(' + url + '#' + getRelationType(relation.relation.type2) + 'End' + ')' + ); + } + + let x, y; + const l = path.points.length; + // Calculate Label position + let labelPosition = utils.calcLabelPosition(path.points); + x = labelPosition.x; + y = labelPosition.y; + + let p1_card_x, p1_card_y; + let p2_card_x, p2_card_y; + + if (l % 2 !== 0 && l > 1) { + let cardinality_1_point = utils.calcCardinalityPosition( + relation.relation.type1 !== 'none', + path.points, + path.points[0] + ); + let cardinality_2_point = utils.calcCardinalityPosition( + relation.relation.type2 !== 'none', + path.points, + path.points[l - 1] + ); + + logger.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point)); + logger.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point)); + + p1_card_x = cardinality_1_point.x; + p1_card_y = cardinality_1_point.y; + p2_card_x = cardinality_2_point.x; + p2_card_y = cardinality_2_point.y; + } + + if (typeof relation.title !== 'undefined') { + const g = elem.append('g').attr('class', 'classLabel'); + const label = g + .append('text') + .attr('class', 'label') + .attr('x', x) + .attr('y', y) + .attr('fill', 'red') + .attr('text-anchor', 'middle') + .text(relation.title); + + window.label = label; + const bounds = label.node().getBBox(); + + g.insert('rect', ':first-child') + .attr('class', 'box') + .attr('x', bounds.x - conf.padding / 2) + .attr('y', bounds.y - conf.padding / 2) + .attr('width', bounds.width + conf.padding) + .attr('height', bounds.height + conf.padding); + } + + logger.info('Rendering relation ' + JSON.stringify(relation)); + if (typeof relation.relationTitle1 !== 'undefined' && relation.relationTitle1 !== 'none') { + const g = elem.append('g').attr('class', 'cardinality'); + g.append('text') + .attr('class', 'type1') + .attr('x', p1_card_x) + .attr('y', p1_card_y) + .attr('fill', 'black') + .attr('font-size', '6') + .text(relation.relationTitle1); + } + if (typeof relation.relationTitle2 !== 'undefined' && relation.relationTitle2 !== 'none') { + const g = elem.append('g').attr('class', 'cardinality'); + g.append('text') + .attr('class', 'type2') + .attr('x', p2_card_x) + .attr('y', p2_card_y) + .attr('fill', 'black') + .attr('font-size', '6') + .text(relation.relationTitle2); + } + + edgeCount++; +}; + +export const drawClass = function(elem, classDef, conf) { + logger.info('Rendering class ' + classDef); + + let cssClassStr = 'classGroup '; + if (classDef.cssClasses.length > 0) { + cssClassStr = cssClassStr + classDef.cssClasses.join(' '); + } + + const id = classDef.id; + const classInfo = { + id: id, + label: classDef.id, + width: 0, + height: 0 + }; + + // add class group + const g = elem + .append('g') + .attr('id', lookUpDomId(id)) + .attr('class', cssClassStr); + + // add title + let title; + if (classDef.link) { + title = g + .append('svg:a') + .attr('xlink:href', classDef.link) + .attr('target', '_blank') + .append('text') + .attr('y', conf.textHeight + conf.padding) + .attr('x', 0); + } else { + title = g + .append('text') + .attr('y', conf.textHeight + conf.padding) + .attr('x', 0); + } + + // add annotations + let isFirst = true; + classDef.annotations.forEach(function(member) { + const titleText2 = title.append('tspan').text('«' + member + '»'); + if (!isFirst) titleText2.attr('dy', conf.textHeight); + isFirst = false; + }); + + let classTitleString = classDef.id; + + if (classDef.type !== undefined && classDef.type !== '') { + classTitleString += '<' + classDef.type + '>'; + } + + const classTitle = title + .append('tspan') + .text(classTitleString) + .attr('class', 'title'); + + // If class has annotations the title needs to have an offset of the text height + if (!isFirst) classTitle.attr('dy', conf.textHeight); + + const titleHeight = title.node().getBBox().height; + + const membersLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2); + + const members = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + conf.dividerMargin + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); + + isFirst = true; + classDef.members.forEach(function(member) { + addTspan(members, member, isFirst, conf); + isFirst = false; + }); + + const membersBox = members.node().getBBox(); + + const methodsLine = g + .append('line') // text label for the x axis + .attr('x1', 0) + .attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height) + .attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height); + + const methods = g + .append('text') // text label for the x axis + .attr('x', conf.padding) + .attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight) + .attr('fill', 'white') + .attr('class', 'classText'); + + isFirst = true; + + classDef.methods.forEach(function(method) { + addTspan(methods, method, isFirst, conf); + isFirst = false; + }); + + const classBox = g.node().getBBox(); + const rect = g + .insert('rect', ':first-child') + .attr('x', 0) + .attr('y', 0) + .attr('width', classBox.width + 2 * conf.padding) + .attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin); + + const rectWidth = rect.node().getBBox().width; + + // Center title + // We subtract the width of each text element from the class box width and divide it by 2 + title.node().childNodes.forEach(function(x) { + x.setAttribute('x', (rectWidth - x.getBBox().width) / 2); + }); + + if (classDef.tooltip) { + title.insert('title').text(classDef.tooltip); + } + + membersLine.attr('x2', rectWidth); + methodsLine.attr('x2', rectWidth); + + classInfo.width = rectWidth; + classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; + + return classInfo; +}; + +export const parseMember = function(text) { + const fieldRegEx = /^(\+|-|~|#)?(\w+)(~\w+~|\[\])?\s+(\w+)$/; + const methodRegEx = /^(\+|-|~|#)?(\w+)\s?\(\s*(\w+(~\w+~|\[\])?\s*(\w+)?)?\s*\)\s?([*|$])?\s?(\w+(~\w+~|\[\])?)?\s*$/; + + let fieldMatch = text.match(fieldRegEx); + let methodMatch = text.match(methodRegEx); + + if (fieldMatch) { + return buildFieldDisplay(fieldMatch); + } else if (methodMatch) { + return buildMethodDisplay(methodMatch); + } else { + return buildLegacyDisplay(text); + } +}; + +const buildFieldDisplay = function(parsedText) { + let visibility = parsedText[1] ? parsedText[1].trim() : ''; + let fieldType = parsedText[2] ? parsedText[2].trim() : ''; + let genericType = parsedText[3] ? parseGenericTypes(parsedText[3]) : ''; + let fieldName = parsedText[4] ? parsedText[4].trim() : ''; + + return { + displayText: visibility + fieldType + genericType + ' ' + fieldName, + cssStyle: '' + }; +}; + +const buildMethodDisplay = function(parsedText) { + let cssStyle = ''; + let displayText = parsedText; + + let visibility = parsedText[1] ? parsedText[1].trim() : ''; + let methodName = parsedText[2] ? parsedText[2].trim() : ''; + let parameters = parsedText[3] ? parseGenericTypes(parsedText[3]) : ''; + let classifier = parsedText[6] ? parsedText[6].trim() : ''; + let returnType = parsedText[7] ? ' : ' + parseGenericTypes(parsedText[7]).trim() : ''; + + displayText = visibility + methodName + '(' + parameters + ')' + returnType; + + cssStyle = parseClassifier(classifier); + + let member = { + displayText: displayText, + cssStyle: cssStyle + }; + + return member; +}; + +const buildLegacyDisplay = function(text) { + // if for some reason we dont have any match, use old format to parse text + let memberText = ''; + let cssStyle = ''; + let returnType = ''; + let methodStart = text.indexOf('('); + let methodEnd = text.indexOf(')'); + + if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) { + let parsedText = text.match(/(\+|-|~|#)?(\w+)/); + let visibility = parsedText[1] ? parsedText[1].trim() : ''; + let methodName = parsedText[2]; + let parameters = text.substring(methodStart + 1, methodEnd); + let classifier = text.substring(methodEnd, methodEnd + 1); + cssStyle = parseClassifier(classifier); + + memberText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')'; + + if (methodEnd < memberText.length) { + returnType = text.substring(methodEnd + 2).trim(); + if (returnType !== '') { + returnType = ' : ' + parseGenericTypes(returnType); + } + } + } else { + // finally - if all else fails, just send the text back as written (other than parsing for generic types) + memberText = parseGenericTypes(text); + } + + let member = { + displayText: memberText + returnType, + cssStyle: cssStyle + }; + + return member; +}; + +const addTspan = function(textEl, txt, isFirst, conf) { + let member = parseMember(txt); + + const tSpan = textEl + .append('tspan') + .attr('x', conf.padding) + .text(member.displayText); + + if (member.cssStyle !== '') { + tSpan.attr('style', member.cssStyle); + } + + if (!isFirst) { + tSpan.attr('dy', conf.textHeight); + } +}; + +const parseGenericTypes = function(text) { + let cleanedText = text; + + if (text.indexOf('~') != -1) { + cleanedText = cleanedText.replace('~', '<'); + cleanedText = cleanedText.replace('~', '>'); + + return parseGenericTypes(cleanedText); + } else { + return cleanedText; + } +}; + +const parseClassifier = function(classifier) { + switch (classifier) { + case '*': + return 'font-style:italic;'; + case '$': + return 'text-decoration:underline;'; + default: + return ''; + } +}; + +export default { + drawClass, + drawEdge, + parseMember +}; diff --git a/src/diagrams/class/classMemberRenderer.spec.js b/src/diagrams/class/svgDraw.spec.js similarity index 72% rename from src/diagrams/class/classMemberRenderer.spec.js rename to src/diagrams/class/svgDraw.spec.js index 6e97fd2a4..2a8273e13 100644 --- a/src/diagrams/class/classMemberRenderer.spec.js +++ b/src/diagrams/class/svgDraw.spec.js @@ -1,11 +1,11 @@ /* eslint-env jasmine */ -import memberRenderer from './classMemberRenderer'; +import svgDraw from './svgDraw'; describe('class member Renderer, ', function () { describe('when parsing text to build method display string', function () { it('should handle simple method declaration', function () { const str = 'foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo()'); expect(actual.cssStyle).toBe(''); @@ -13,7 +13,7 @@ describe('class member Renderer, ', function () { it('should handle public visibility', function () { const str = '+foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('+foo()'); expect(actual.cssStyle).toBe(''); @@ -21,7 +21,7 @@ describe('class member Renderer, ', function () { it('should handle private visibility', function () { const str = '-foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('-foo()'); expect(actual.cssStyle).toBe(''); @@ -29,7 +29,7 @@ describe('class member Renderer, ', function () { it('should handle protected visibility', function () { const str = '#foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('#foo()'); expect(actual.cssStyle).toBe(''); @@ -37,7 +37,7 @@ describe('class member Renderer, ', function () { it('should handle package/internal visibility', function () { const str = '~foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('~foo()'); expect(actual.cssStyle).toBe(''); @@ -45,7 +45,7 @@ describe('class member Renderer, ', function () { it('should ignore unknown character for visibility', function () { const str = '!foo()'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo()'); expect(actual.cssStyle).toBe(''); @@ -53,7 +53,7 @@ describe('class member Renderer, ', function () { it('should handle abstract classifier', function () { const str = 'foo()*'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo()'); expect(actual.cssStyle).toBe('font-style:italic;'); @@ -61,7 +61,7 @@ describe('class member Renderer, ', function () { it('should handle static classifier', function () { const str = 'foo()$'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo()'); expect(actual.cssStyle).toBe('text-decoration:underline;'); @@ -69,7 +69,7 @@ describe('class member Renderer, ', function () { it('should ignore unknown character for classifier', function () { const str = 'foo()!'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo()'); expect(actual.cssStyle).toBe(''); @@ -77,7 +77,7 @@ describe('class member Renderer, ', function () { it('should handle simple method declaration with parameters', function () { const str = 'foo(int id)'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(int id)'); expect(actual.cssStyle).toBe(''); @@ -85,7 +85,7 @@ describe('class member Renderer, ', function () { it('should handle simple method declaration with single item in parameters', function () { const str = 'foo(id)'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(id)'); expect(actual.cssStyle).toBe(''); @@ -93,7 +93,7 @@ describe('class member Renderer, ', function () { it('should handle simple method declaration with single item in parameters with extra spaces', function () { const str = ' foo ( id) '; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(id)'); expect(actual.cssStyle).toBe(''); @@ -101,7 +101,7 @@ describe('class member Renderer, ', function () { it('should handle method declaration with return value', function () { const str = 'foo(id) int'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(id) : int'); expect(actual.cssStyle).toBe(''); @@ -109,7 +109,7 @@ describe('class member Renderer, ', function () { it('should handle method declaration with generic return value', function () { const str = 'foo(id) List~int~'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(id) : List'); expect(actual.cssStyle).toBe(''); @@ -117,7 +117,7 @@ describe('class member Renderer, ', function () { it('should handle method declaration with generic parameter', function () { const str = 'foo(List~int~)'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('foo(List)'); expect(actual.cssStyle).toBe(''); @@ -125,33 +125,17 @@ describe('class member Renderer, ', function () { it('should handle method declaration with all possible markup', function () { const str = '+foo ( List~int~ ids )* List~Item~'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('+foo(List ids) : List'); expect(actual.cssStyle).toBe('font-style:italic;'); }); }); - describe('when parsing text for generic types', function () { - it('should handle open and close brackets in correct order', function () { - const str = 'foo(List~Item~)'; - let actual = memberRenderer.parseGenericTypes(str); - - expect(actual).toBe('foo(List)'); - }); - - it('should handle open and close brackets in correct order with multiple usages', function () { - const str = 'foo(List~Item~) List~Item~'; - let actual = memberRenderer.parseGenericTypes(str); - - expect(actual).toBe('foo(List) List'); - }); - }); - describe('when parsing text to build field display string', function () { it('should handle simple field declaration', function () { const str = 'int[] ids'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('int[] ids'); expect(actual.cssStyle).toBe(''); @@ -159,7 +143,7 @@ describe('class member Renderer, ', function () { it('should handle field declaration with generic type', function () { const str = 'List~int~ ids'; - let actual = memberRenderer.parseMember(str); + let actual = svgDraw.parseMember(str); expect(actual.displayText).toBe('List ids'); expect(actual.cssStyle).toBe('');