mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-18 14:59:53 +02:00
Merge branch 'develop' into feature/1295_generic_rendering_engine
This commit is contained in:
83
src/diagrams/er/erDb.js
Normal file
83
src/diagrams/er/erDb.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
import { logger } from '../../logger';
|
||||
|
||||
let entities = {};
|
||||
let relationships = [];
|
||||
let title = '';
|
||||
|
||||
const Cardinality = {
|
||||
ONLY_ONE_TO_ONE_OR_MORE: 'ONLY_ONE_TO_ONE_OR_MORE',
|
||||
ONLY_ONE_TO_ZERO_OR_MORE: 'ONLY_ONE_TO_ZERO_OR_MORE',
|
||||
ZERO_OR_ONE_TO_ZERO_OR_MORE: 'ZERO_OR_ONE_TO_ZERO_OR_MORE',
|
||||
ZERO_OR_ONE_TO_ONE_OR_MORE: 'ZERO_OR_ONE_TO_ONE_OR_MORE',
|
||||
ONE_OR_MORE_TO_ONLY_ONE: 'ONE_OR_MORE_TO_ONLY_ONE',
|
||||
ZERO_OR_MORE_TO_ONLY_ONE: 'ZERO_OR_MORE_TO_ONLY_ONE',
|
||||
ZERO_OR_MORE_TO_ZERO_OR_ONE: 'ZERO_OR_MORE_TO_ZERO_OR_ONE',
|
||||
ONE_OR_MORE_TO_ZERO_OR_ONE: 'ONE_OR_MORE_TO_ZERO_OR_ONE',
|
||||
ZERO_OR_ONE_TO_ONLY_ONE: 'ZERO_OR_ONE_TO_ONLY_ONE',
|
||||
ONLY_ONE_TO_ONLY_ONE: 'ONLY_ONE_TO_ONLY_ONE',
|
||||
ONLY_ONE_TO_ZERO_OR_ONE: 'ONLY_ONE_TO_ZERO_OR_ONE',
|
||||
ZERO_OR_ONE_TO_ZERO_OR_ONE: 'ZERO_OR_ONE_TO_ZERO_OR_ONE',
|
||||
ZERO_OR_MORE_TO_ZERO_OR_MORE: 'ZERO_OR_MORE_TO_ZERO_OR_MORE',
|
||||
ZERO_OR_MORE_TO_ONE_OR_MORE: 'ZERO_OR_MORE_TO_ONE_OR_MORE',
|
||||
ONE_OR_MORE_TO_ZERO_OR_MORE: 'ONE_OR_MORE_TO_ZERO_OR_MORE',
|
||||
ONE_OR_MORE_TO_ONE_OR_MORE: 'ONE_OR_MORE_TO_ONE_OR_MORE'
|
||||
};
|
||||
|
||||
const addEntity = function(name) {
|
||||
if (typeof entities[name] === 'undefined') {
|
||||
entities[name] = name;
|
||||
logger.debug('Added new entity :', name);
|
||||
}
|
||||
};
|
||||
|
||||
const getEntities = () => entities;
|
||||
|
||||
/**
|
||||
* Add a relationship
|
||||
* @param entA The first entity in the relationship
|
||||
* @param rolA The role played by the first entity in relation to the second
|
||||
* @param entB The second entity in the relationship
|
||||
* @param card The cardinality of the relationship between the two entities
|
||||
*/
|
||||
const addRelationship = function(entA, rolA, entB, card) {
|
||||
let rel = {
|
||||
entityA: entA,
|
||||
roleA: rolA,
|
||||
entityB: entB,
|
||||
cardinality: card
|
||||
};
|
||||
|
||||
relationships.push(rel);
|
||||
logger.debug('Added new relationship :', rel);
|
||||
};
|
||||
|
||||
const getRelationships = () => relationships;
|
||||
|
||||
// Keep this - TODO: revisit...allow the diagram to have a title
|
||||
const setTitle = function(txt) {
|
||||
title = txt;
|
||||
};
|
||||
|
||||
const getTitle = function() {
|
||||
return title;
|
||||
};
|
||||
|
||||
const clear = function() {
|
||||
entities = {};
|
||||
relationships = [];
|
||||
title = '';
|
||||
};
|
||||
|
||||
export default {
|
||||
Cardinality,
|
||||
addEntity,
|
||||
getEntities,
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
clear,
|
||||
setTitle,
|
||||
getTitle
|
||||
};
|
168
src/diagrams/er/erMarkers.js
Normal file
168
src/diagrams/er/erMarkers.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const ERMarkers = {
|
||||
ONLY_ONE_START: 'ONLY_ONE_START',
|
||||
ONLY_ONE_END: 'ONLY_ONE_END',
|
||||
ZERO_OR_ONE_START: 'ZERO_OR_ONE_START',
|
||||
ZERO_OR_ONE_END: 'ZERO_OR_ONE_END',
|
||||
ONE_OR_MORE_START: 'ONE_OR_MORE_START',
|
||||
ONE_OR_MORE_END: 'ONE_OR_MORE_END',
|
||||
ZERO_OR_MORE_START: 'ZERO_OR_MORE_START',
|
||||
ZERO_OR_MORE_END: 'ZERO_OR_MORE_END'
|
||||
};
|
||||
|
||||
/**
|
||||
* Put the markers into the svg DOM for later use with edge paths
|
||||
*/
|
||||
const insertMarkers = function(elem, conf) {
|
||||
let marker;
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18 M15,0 L15,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONLY_ONE_END)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 18)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,0 L3,18 M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_START)
|
||||
.attr('refX', 0)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 21)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M9,0 L9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_ONE_END)
|
||||
.attr('refX', 30)
|
||||
.attr('refY', 9)
|
||||
.attr('markerWidth', 30)
|
||||
.attr('markerHeight', 18)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 9)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,0 L21,18');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONE_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27');
|
||||
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ONE_OR_MORE_END)
|
||||
.attr('refX', 27)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 45)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_START)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 48)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18');
|
||||
|
||||
marker = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ERMarkers.ZERO_OR_MORE_END)
|
||||
.attr('refX', 39)
|
||||
.attr('refY', 18)
|
||||
.attr('markerWidth', 57)
|
||||
.attr('markerHeight', 36)
|
||||
.attr('orient', 'auto');
|
||||
marker
|
||||
.append('circle')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'white')
|
||||
.attr('cx', 9)
|
||||
.attr('cy', 18)
|
||||
.attr('r', 6);
|
||||
marker
|
||||
.append('path')
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none')
|
||||
.attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default {
|
||||
ERMarkers,
|
||||
insertMarkers
|
||||
};
|
414
src/diagrams/er/erRenderer.js
Normal file
414
src/diagrams/er/erRenderer.js
Normal file
@@ -0,0 +1,414 @@
|
||||
import graphlib from 'graphlib';
|
||||
import * as d3 from 'd3';
|
||||
import erDb from './erDb';
|
||||
import erParser from './parser/erDiagram';
|
||||
import dagre from 'dagre';
|
||||
import { getConfig } from '../../config';
|
||||
import { logger } from '../../logger';
|
||||
import erMarkers from './erMarkers';
|
||||
|
||||
const conf = {};
|
||||
|
||||
/**
|
||||
* 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() imported from the config module
|
||||
*/
|
||||
export const setConf = function(cnf) {
|
||||
const keys = Object.keys(cnf);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
conf[keys[i]] = cnf[keys[i]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Use D3 to construct the svg elements for the entities
|
||||
* @param svgNode the svg node that contains the diagram
|
||||
* @param entities The entities to be drawn
|
||||
* @param g The graph that contains the vertex and edge definitions post-layout
|
||||
* @return The first entity that was inserted
|
||||
*/
|
||||
const drawEntities = function(svgNode, entities, graph) {
|
||||
const keys = Object.keys(entities);
|
||||
let firstOne;
|
||||
|
||||
keys.forEach(function(id) {
|
||||
// Create a group for each entity
|
||||
const groupNode = svgNode.append('g').attr('id', id);
|
||||
|
||||
firstOne = firstOne === undefined ? id : 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 textNode = groupNode
|
||||
.append('text')
|
||||
.attr('id', textId)
|
||||
.attr('x', 0)
|
||||
.attr('y', (conf.fontSize + 2 * conf.entityPadding) / 2)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(id);
|
||||
|
||||
// Calculate the width and height of the entity
|
||||
const textBBox = textNode.node().getBBox();
|
||||
const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2);
|
||||
const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2);
|
||||
|
||||
// Make sure the text gets centred relative to the entity box
|
||||
textNode.attr('transform', 'translate(' + entityWidth / 2 + ',' + entityHeight / 2 + ')');
|
||||
|
||||
// Draw the rectangle - insert it before the text so that the text is not obscured
|
||||
const rectNode = groupNode
|
||||
.insert('rect', '#' + textId)
|
||||
.attr('fill', conf.fill)
|
||||
.attr('fill-opacity', conf.fillOpacity)
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', entityWidth)
|
||||
.attr('height', entityHeight);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(id, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: id
|
||||
});
|
||||
});
|
||||
return firstOne;
|
||||
}; // drawEntities
|
||||
|
||||
const adjustEntities = function(svgNode, graph) {
|
||||
graph.nodes().forEach(function(v) {
|
||||
if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') {
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(graph.node(v).x - graph.node(v).width / 2) +
|
||||
',' +
|
||||
(graph.node(v).y - graph.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const getEdgeName = function(rel) {
|
||||
return (rel.entityA + rel.roleA + rel.entityB).replace(/\s/g, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Add each relationship to the graph
|
||||
* @param relationships the relationships to be added
|
||||
* @param g the graph
|
||||
* @return {Array} The array of relationships
|
||||
*/
|
||||
const addRelationships = function(relationships, g) {
|
||||
relationships.forEach(function(r) {
|
||||
g.setEdge(r.entityA, r.entityB, { relationship: r }, getEdgeName(r));
|
||||
});
|
||||
return relationships;
|
||||
}; // addRelationships
|
||||
|
||||
let relCnt = 0;
|
||||
/**
|
||||
* Draw a relationship using edge information from the graph
|
||||
* @param svg the svg node
|
||||
* @param rel the relationship to draw in the svg
|
||||
* @param g the graph containing the edge information
|
||||
*/
|
||||
const drawRelationshipFromLayout = function(svg, rel, g, insert) {
|
||||
relCnt++;
|
||||
|
||||
// Find the edge relating to this relationship
|
||||
const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel));
|
||||
|
||||
// Get a function that will generate the line path
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.x(function(d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function(d) {
|
||||
return d.y;
|
||||
})
|
||||
.curve(d3.curveBasis);
|
||||
|
||||
// Insert the line at the right place
|
||||
const svgPath = svg
|
||||
.insert('path', '#' + insert)
|
||||
.attr('d', lineFunction(edge.points))
|
||||
.attr('stroke', conf.stroke)
|
||||
.attr('fill', 'none');
|
||||
|
||||
// TODO: Understand this better
|
||||
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, '\\)');
|
||||
}
|
||||
|
||||
// Decide which start and end markers it needs. It may be possible to be more concise here
|
||||
// by reversing a start marker to make an end marker...but this will do for now
|
||||
switch (rel.cardinality) {
|
||||
case erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE:
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
|
||||
break;
|
||||
case erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE:
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
|
||||
);
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
|
||||
break;
|
||||
}
|
||||
|
||||
// Now label the relationship
|
||||
|
||||
// Find the half-way point
|
||||
const len = svgPath.node().getTotalLength();
|
||||
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
|
||||
|
||||
// Append a text node containing the label
|
||||
const labelId = 'rel' + relCnt;
|
||||
|
||||
const labelNode = svg
|
||||
.append('text')
|
||||
.attr('id', labelId)
|
||||
.attr('x', labelPoint.x)
|
||||
.attr('y', labelPoint.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize)
|
||||
.text(rel.roleA);
|
||||
|
||||
// Figure out how big the opaque 'container' rectangle needs to be
|
||||
const labelBBox = labelNode.node().getBBox();
|
||||
|
||||
// Insert the opaque rectangle in front of the text label
|
||||
svg
|
||||
.insert('rect', '#' + labelId)
|
||||
.attr('x', labelPoint.x - labelBBox.width / 2)
|
||||
.attr('y', labelPoint.y - labelBBox.height / 2)
|
||||
.attr('width', labelBBox.width)
|
||||
.attr('height', labelBBox.height)
|
||||
.attr('fill', 'white')
|
||||
.attr('fill-opacity', '85%');
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draw en E-R diagram in the tag with id: id based on the text definition of the diagram
|
||||
* @param text the text of the diagram
|
||||
* @param id the unique id of the DOM node that contains the diagram
|
||||
*/
|
||||
export const draw = function(text, id) {
|
||||
logger.info('Drawing ER diagram');
|
||||
erDb.clear();
|
||||
const parser = erParser.parser;
|
||||
parser.yy = erDb;
|
||||
|
||||
// Parse the text to populate erDb
|
||||
try {
|
||||
parser.parse(text);
|
||||
} catch (err) {
|
||||
logger.debug('Parsing failed');
|
||||
}
|
||||
|
||||
// Get a reference to the svg node that contains the text
|
||||
const svg = d3.select(`[id='${id}']`);
|
||||
|
||||
// Add cardinality marker definitions to the svg
|
||||
erMarkers.insertMarkers(svg, conf);
|
||||
|
||||
// Now we have to construct the diagram in a specific way:
|
||||
// ---
|
||||
// 1. Create all the entities in the svg node at 0,0, but with the correct dimensions (allowing for text content)
|
||||
// 2. Make sure they are all added to the graph
|
||||
// 3. Add all the edges (relationships) to the graph aswell
|
||||
// 4. Let dagre do its magic to layout the graph. This assigns:
|
||||
// - the centre co-ordinates for each node, bearing in mind the dimensions and edge relationships
|
||||
// - the path co-ordinates for each edge
|
||||
// But it has no impact on the svg child nodes - the diagram remains with every entity rooted at 0,0
|
||||
// 5. Now assign a transform to each entity in the svg node so that it gets drawn in the correct place, as determined by
|
||||
// its centre point, which is obtained from the graph, and it's width and height
|
||||
// 6. And finally, create all the edges in the svg node using information from the graph
|
||||
// ---
|
||||
|
||||
// Create the graph
|
||||
let g;
|
||||
|
||||
// TODO: Explore directed vs undirected graphs, and how the layout is affected
|
||||
// An E-R diagram could be said to be undirected, but there is merit in setting
|
||||
// the direction from parent to child in a one-to-many as this influences graphlib to
|
||||
// put the parent above the child (does it?), which is intuitive. Most relationships
|
||||
// in ER diagrams are one-to-many.
|
||||
g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
directed: true,
|
||||
compound: false
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: conf.layoutDirection,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
nodesep: 100,
|
||||
edgesep: 100,
|
||||
ranksep: 100
|
||||
})
|
||||
.setDefaultEdgeLabel(function() {
|
||||
return {};
|
||||
});
|
||||
|
||||
// Draw the entities (at 0,0), returning the first svg node that got
|
||||
// inserted - this represents the insertion point for relationship paths
|
||||
const firstEntity = drawEntities(svg, erDb.getEntities(), g);
|
||||
|
||||
// TODO: externalise the addition of entities to the graph - it's a bit 'buried' in the above
|
||||
|
||||
// Add all the relationships to the graph
|
||||
const relationships = addRelationships(erDb.getRelationships(), g);
|
||||
|
||||
dagre.layout(g); // Node and edge positions will be updated
|
||||
|
||||
// Adjust the positions of the entities so that they adhere to the layout
|
||||
adjustEntities(svg, g);
|
||||
|
||||
// Draw the relationships
|
||||
relationships.forEach(function(rel) {
|
||||
drawRelationshipFromLayout(svg, rel, g, firstEntity);
|
||||
});
|
||||
|
||||
const padding = 8; // TODO: move this to config
|
||||
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 4;
|
||||
const height = svgBounds.height + padding * 4;
|
||||
logger.debug(
|
||||
`new ViewBox 0 0 ${width} ${height}`,
|
||||
`translate(${padding - g._label.marginx}, ${padding - g._label.marginy})`
|
||||
);
|
||||
|
||||
if (conf.useMaxWidth) {
|
||||
svg.attr('width', '100%');
|
||||
svg.attr('style', `max-width: ${width}px;`);
|
||||
} else {
|
||||
svg.attr('height', height);
|
||||
svg.attr('width', width);
|
||||
}
|
||||
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`);
|
||||
svg
|
||||
.select('g')
|
||||
.attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`);
|
||||
}; // draw
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw
|
||||
};
|
84
src/diagrams/er/parser/erDiagram.jison
Normal file
84
src/diagrams/er/parser/erDiagram.jison
Normal file
@@ -0,0 +1,84 @@
|
||||
%lex
|
||||
|
||||
%x string
|
||||
%options case-insensitive
|
||||
|
||||
%%
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
["] { this.begin("string");}
|
||||
<string>["] { this.popState(); }
|
||||
<string>[^"]* { return 'STR'; }
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
[A-Za-z][A-Za-z0-9\-]* return 'ALPHANUM';
|
||||
\>\?\-\?\< return 'ZERO_OR_MORE_TO_ZERO_OR_MORE';
|
||||
\>\?\-\!\< return 'ZERO_OR_MORE_TO_ONE_OR_MORE';
|
||||
\>\!\-\!\< return 'ONE_OR_MORE_TO_ONE_OR_MORE';
|
||||
\>\!\-\?\< return 'ONE_OR_MORE_TO_ZERO_OR_MORE';
|
||||
\!\-\!\< return 'ONLY_ONE_TO_ONE_OR_MORE';
|
||||
\!\-\?\< return 'ONLY_ONE_TO_ZERO_OR_MORE';
|
||||
\?\-\?\< return 'ZERO_OR_ONE_TO_ZERO_OR_MORE';
|
||||
\?\-\!\< return 'ZERO_OR_ONE_TO_ONE_OR_MORE';
|
||||
\>\!\-\! return 'ONE_OR_MORE_TO_ONLY_ONE';
|
||||
\>\?\-\! return 'ZERO_OR_MORE_TO_ONLY_ONE';
|
||||
\>\?\-\? return 'ZERO_OR_MORE_TO_ZERO_OR_ONE';
|
||||
\>\!\-\? return 'ONE_OR_MORE_TO_ZERO_OR_ONE';
|
||||
\?\-\! return 'ZERO_OR_ONE_TO_ONLY_ONE';
|
||||
\!\-\! return 'ONLY_ONE_TO_ONLY_ONE';
|
||||
\!\-\? return 'ONLY_ONE_TO_ZERO_OR_ONE';
|
||||
\?\-\? return 'ZERO_OR_ONE_TO_ZERO_OR_ONE';
|
||||
. return yytext[0];
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
| document statement
|
||||
;
|
||||
|
||||
statement
|
||||
: entityName relationship entityName ':' role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
||||
};
|
||||
|
||||
entityName
|
||||
: 'ALPHANUM' { $$ = $1; }
|
||||
;
|
||||
|
||||
relationship
|
||||
: 'ONLY_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONE_OR_MORE; }
|
||||
| 'ONLY_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE; }
|
||||
| 'ZERO_OR_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE; }
|
||||
| 'ZERO_OR_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE; }
|
||||
| 'ONE_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONLY_ONE; }
|
||||
| 'ZERO_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE; }
|
||||
| 'ZERO_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE; }
|
||||
| 'ONE_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE; }
|
||||
| 'ONLY_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONLY_ONE; }
|
||||
| 'ONLY_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE; }
|
||||
| 'ZERO_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE; }
|
||||
| 'ZERO_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE; }
|
||||
| 'ONE_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE; }
|
||||
| 'ONE_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE; }
|
||||
;
|
||||
|
||||
role
|
||||
: 'STR' { $$ = $1; }
|
||||
| 'ALPHANUM' { $$ = $1; }
|
||||
;
|
||||
%%
|
224
src/diagrams/er/parser/erDiagram.spec.js
Normal file
224
src/diagrams/er/parser/erDiagram.spec.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import erDb from '../erDb';
|
||||
import erDiagram from './erDiagram';
|
||||
import { setConfig } from '../../../config';
|
||||
import logger from '../../../logger';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict'
|
||||
});
|
||||
|
||||
describe('when parsing ER diagram it...', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
erDiagram.parser.yy = erDb;
|
||||
erDiagram.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should associate two entities correctly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nCAR !-?< DRIVER : "insured for"');
|
||||
const entities = erDb.getEntities();
|
||||
const relationships = erDb.getRelationships();
|
||||
const carEntity = entities.CAR;
|
||||
const driverEntity = entities.DRIVER;
|
||||
|
||||
expect(carEntity).toBe('CAR');
|
||||
expect(driverEntity).toBe('DRIVER');
|
||||
expect(relationships.length).toBe(1);
|
||||
expect(relationships[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should not create duplicate entities', function() {
|
||||
const line1 = 'CAR !-?< DRIVER : "insured for"';
|
||||
const line2 = 'DRIVER !-! LICENSE : has';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(3);
|
||||
});
|
||||
|
||||
it('should create the role specified', function() {
|
||||
const teacherRole = 'is teacher of';
|
||||
const line1 = `TEACHER >?-?< STUDENT : "${teacherRole}"`;
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}`);
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(rels[0].roleA).toBe(`${teacherRole}`);
|
||||
});
|
||||
|
||||
it('should allow recursive relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nNODE !-?< NODE : "leads to"');
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow more than one relationship between the same two entities', function() {
|
||||
const line1 = 'CAR !-?< PERSON : "insured for"';
|
||||
const line2 = 'CAR >?-! PERSON : "owned by"';
|
||||
erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`);
|
||||
const entities = erDb.getEntities();
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(entities).length).toBe(2);
|
||||
expect(rels.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should limit the number of relationships between the same two entities', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
it ('should not allow multiple relationships between the same two entities unless the roles are different', function() {
|
||||
/* TODO */
|
||||
});
|
||||
|
||||
it('should handle only-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA !-!< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA !-?< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE);
|
||||
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ?-?< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ?-!< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >!-! B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >?-! B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >?-? B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >!-? B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ?-! B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-only-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA !-! B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE);
|
||||
});
|
||||
|
||||
it('should handle only-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA !-? B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-one-to-zero-or-one relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA ?-? B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >?-?< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >!-!< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle zero-or-more-to-one-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >?-!< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE);
|
||||
});
|
||||
|
||||
it('should handle one-or-more-to-zero-or-more relationships', function() {
|
||||
erDiagram.parser.parse('erDiagram\nA >!-?< B : has');
|
||||
const rels = erDb.getRelationships();
|
||||
|
||||
expect(Object.keys(erDb.getEntities()).length).toBe(2);
|
||||
expect(rels.length).toBe(1);
|
||||
expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE);
|
||||
});
|
||||
|
||||
it('should not accept a syntax error', function() {
|
||||
const doc = 'erDiagram\nA xxx B : has';
|
||||
expect(() => {
|
||||
erDiagram.parser.parse(doc);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
});
|
@@ -41,6 +41,9 @@ import infoDb from './diagrams/info/infoDb';
|
||||
import pieRenderer from './diagrams/pie/pieRenderer';
|
||||
import pieParser from './diagrams/pie/parser/pie';
|
||||
import pieDb from './diagrams/pie/pieDb';
|
||||
import erDb from './diagrams/er/erDb';
|
||||
import erParser from './diagrams/er/parser/erDiagram';
|
||||
import erRenderer from './diagrams/er/erRenderer';
|
||||
|
||||
const themes = {};
|
||||
for (const themeName of ['default', 'forest', 'dark', 'neutral']) {
|
||||
@@ -346,6 +349,54 @@ const config = {
|
||||
edgeLengthFactor: '20',
|
||||
compositTitleSize: 35,
|
||||
radius: 5
|
||||
},
|
||||
|
||||
/**
|
||||
* The object containing configurations specific for entity relationship diagrams
|
||||
*/
|
||||
er: {
|
||||
/**
|
||||
* Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL',
|
||||
* where T = top, B = bottom, L = left, and R = right.
|
||||
*/
|
||||
layoutDirection: 'TB',
|
||||
|
||||
/**
|
||||
* The mimimum width of an entity box
|
||||
*/
|
||||
minEntityWidth: 100,
|
||||
|
||||
/**
|
||||
* The minimum height of an entity box
|
||||
*/
|
||||
minEntityHeight: 75,
|
||||
|
||||
/**
|
||||
* The minimum internal padding between the text in an entity box and the enclosing box borders
|
||||
*/
|
||||
entityPadding: 15,
|
||||
|
||||
/**
|
||||
* Stroke color of box edges and lines
|
||||
*/
|
||||
stroke: 'gray',
|
||||
|
||||
/**
|
||||
* Fill color of entity boxes
|
||||
*/
|
||||
fill: 'honeydew',
|
||||
|
||||
/**
|
||||
* Opacity of entity boxes - if you want to see how the crows feet
|
||||
* retain their elegant joins to the boxes regardless of the angle of incidence
|
||||
* then override this to something less than 100%
|
||||
*/
|
||||
fillOpacity: '100%',
|
||||
|
||||
/**
|
||||
* Font size
|
||||
*/
|
||||
fontSize: '12px'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -398,6 +449,11 @@ function parse(text) {
|
||||
parser = pieParser;
|
||||
parser.parser.yy = pieDb;
|
||||
break;
|
||||
case 'er':
|
||||
logger.debug('er');
|
||||
parser = erParser;
|
||||
parser.parser.yy = erDb;
|
||||
break;
|
||||
}
|
||||
|
||||
parser.parser.yy.parseError = (str, hash) => {
|
||||
@@ -620,6 +676,10 @@ const render = function(id, _txt, cb, container) {
|
||||
pieRenderer.setConf(config.class);
|
||||
pieRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
case 'er':
|
||||
erRenderer.setConf(config.er);
|
||||
erRenderer.draw(txt, id, pkg.version);
|
||||
break;
|
||||
}
|
||||
|
||||
d3.select(`[id="${id}"]`)
|
||||
|
@@ -52,6 +52,10 @@ export const detectType = function(text) {
|
||||
return 'pie';
|
||||
}
|
||||
|
||||
if (text.match(/^\s*erDiagram/)) {
|
||||
return 'er';
|
||||
}
|
||||
|
||||
return 'flowchart';
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user