Merge pull request #1432 from mermaid-js/1418_html_labels_rendering_engine

1418 html labels rendering engine
This commit is contained in:
Knut Sveidqvist
2020-05-27 22:34:26 +02:00
committed by GitHub
9 changed files with 247 additions and 81 deletions

View File

@@ -18,37 +18,28 @@
</head> </head>
<body> <body>
<h1>info below</h1> <h1>info below</h1>
<div class="mermaid2" style="width: 100%; height: 20%;"> <div class="mermaid" style="width: 100%; height: 20%;">
flowchart LR stateDiagram-v2
a --> b
subgraph b [Test] [*] --> S1
c --> d -->e state "Some long name" as S1: The
end
</div> </div>
<div class="mermaid2" style="width: 100%; height: 20%;"> <div class="mermaid" style="width: 100%; height: 20%;">
flowchart LR stateDiagram-v2
a --> b
subgraph id1 [Test] [*] --> S1
a --apa--> c state "Some long name" as S1: The description\nwith multiple lines
b
c-->b
b-->H
end
G-->H
G-->c
</div> </div>
<div class="mermaid" style="width: 50%; height: 20%;"> <div class="mermaid2" style="width: 50%; height: 20%;">
flowchart LR flowchart LR
A{{A}}-->B{{B}}; A{{A}}-- apa -->B{{B}};
click A callback "Tooltip" click A callback "Tooltip"
click B "http://www.github.com" "This is a link" click B "http://www.github.com" "This is a link"
</div> </div>
<div class="mermaid" style="width: 50%; height: 20%;"> <div class="mermaid2" style="width: 50%; height: 20%;">
flowchart LR graph LR
A{{A}}-->B{{B}}; A{{A}}--apa-->B{{B}};
</div> </div>
<div class="mermaid2" style="width: 50%; height: 20%;"> <div class="mermaid2" style="width: 50%; height: 20%;">
@@ -264,7 +255,7 @@ stateDiagram-v2
// arrowMarkerAbsolute: true, // arrowMarkerAbsolute: true,
// themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}', // themeCSS: '.edgePath .path {stroke: red;} .arrowheadPath {fill: red;}',
logLevel: 0, logLevel: 0,
flowchart: { curve: 'linear', "htmlLabels": false }, flowchart: { curve: 'linear', "htmlLabels": true },
// gantt: { axisFormat: '%m/%d/%Y' }, // gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50, showSequenceNumbers: true }, sequence: { actorMargin: 50, showSequenceNumbers: true },
// sequenceDiagram: { actorMargin: 300 } // deprecated // sequenceDiagram: { actorMargin: 300 } // deprecated

View File

@@ -1,6 +1,8 @@
import intersectRect from './intersect/intersect-rect'; import intersectRect from './intersect/intersect-rect';
import { logger as log } from '../logger'; // eslint-disable-line import { logger as log } from '../logger'; // eslint-disable-line
import createLabel from './createLabel'; import createLabel from './createLabel';
import { select } from 'd3';
import { getConfig } from '../config';
const rect = (parent, node) => { const rect = (parent, node) => {
log.trace('Creating subgraph rect for ', node.id, node); log.trace('Creating subgraph rect for ', node.id, node);
@@ -17,10 +19,20 @@ const rect = (parent, node) => {
// Create the label and insert it after the rect // Create the label and insert it after the rect
const label = shapeSvg.insert('g').attr('class', 'cluster-label'); const label = shapeSvg.insert('g').attr('class', 'cluster-label');
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle)); const text = label
.node()
.appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
// Get the size of the label // Get the size of the label
const bbox = text.getBBox(); let bbox = text.getBBox();
if (getConfig().flowchart.htmlLabels) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
const padding = 0 * node.padding; const padding = 0 * node.padding;
const halfPadding = padding / 2; const halfPadding = padding / 2;
@@ -106,11 +118,20 @@ const roundedWithTitle = (parent, node) => {
const label = shapeSvg.insert('g').attr('class', 'cluster-label'); const label = shapeSvg.insert('g').attr('class', 'cluster-label');
const innerRect = shapeSvg.append('rect'); const innerRect = shapeSvg.append('rect');
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle)); const text = label
.node()
.appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
// Get the size of the label // Get the size of the label
const bbox = text.getBBox(); let bbox = text.getBBox();
if (getConfig().flowchart.htmlLabels) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
bbox = text.getBBox();
const padding = 0 * node.padding; const padding = 0 * node.padding;
const halfPadding = padding / 2; const halfPadding = padding / 2;
@@ -134,7 +155,7 @@ const roundedWithTitle = (parent, node) => {
'translate(' + 'translate(' +
(node.x - bbox.width / 2) + (node.x - bbox.width / 2) +
', ' + ', ' +
(node.y - node.height / 2 - node.padding / 3 + 3) + (node.y - node.height / 2 - node.padding / 3 + (getConfig().flowchart.htmlLabels ? 5 : 3)) +
')' ')'
); );
@@ -191,7 +212,7 @@ export const insertCluster = (elem, node) => {
clusterElems[node.id] = shapes[shape](elem, node); clusterElems[node.id] = shapes[shape](elem, node);
}; };
export const getClusterTitleWidth = (elem, node) => { export const getClusterTitleWidth = (elem, node) => {
const label = createLabel(node.labelText, node.labelStyle); const label = createLabel(node.labelText, node.labelStyle, undefined, true);
elem.node().appendChild(label); elem.node().appendChild(label);
const width = label.getBBox().width; const width = label.getBBox().width;
elem.node().removeChild(label); elem.node().removeChild(label);

View File

@@ -1,29 +1,123 @@
const createLabel = (vertexText, style, isTitle) => { import { select } from 'd3';
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); import { logger } from '../logger'; // eslint-disable-line
svgLabel.setAttribute('style', style.replace('color:', 'fill:')); // let vertexNode;
let rows = []; // if (getConfig().flowchart.htmlLabels) {
if (typeof vertexText === 'string') { // // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi); // const node = {
} else if (Array.isArray(vertexText)) { // label: vertexText.replace(/fa[lrsb]?:fa-[\w-]+/g, s => `<i class='${s.replace(':', ' ')}'></i>`)
rows = vertexText; // };
} else { // vertexNode = addHtmlLabel(svg, node).node();
rows = []; // vertexNode.parentNode.removeChild(vertexNode);
} // } else {
// const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
// svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:'));
for (let j = 0; j < rows.length; j++) { // const rows = vertexText.split(common.lineBreakRegex);
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); // for (let j = 0; j < rows.length; j++) {
tspan.setAttribute('dy', '1em'); // const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttribute('x', '0'); // tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
if (isTitle) { // tspan.setAttribute('dy', '1em');
tspan.setAttribute('class', 'title-row'); // tspan.setAttribute('x', '1');
} else { // tspan.textContent = rows[j];
tspan.setAttribute('class', 'row'); // svgLabel.appendChild(tspan);
} // }
tspan.textContent = rows[j].trim(); // vertexNode = svgLabel;
svgLabel.appendChild(tspan); // }
import { getConfig } from '../config';
function applyStyle(dom, styleFn) {
if (styleFn) {
dom.attr('style', styleFn);
}
}
function addHtmlLabel(node) {
// var fo = root.append('foreignObject').attr('width', '100000');
// var div = fo.append('xhtml:div');
// div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
// var label = node.label;
// switch (typeof label) {
// case 'function':
// div.insert(label);
// break;
// case 'object':
// // Currently we assume this is a DOM object.
// div.insert(function() {
// return label;
// });
// break;
// default:
// div.html(label);
// }
// applyStyle(div, node.labelStyle);
// div.style('display', 'inline-block');
// // Fix for firefox
// div.style('white-space', 'nowrap');
// var client = div.node().getBoundingClientRect();
// fo.attr('width', client.width).attr('height', client.height);
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
const div = fo.append('xhtml:div');
const label = node.label;
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
div.html('<span class="' + labelClass + '">' + label + '</span>');
applyStyle(div, node.labelStyle);
div.style('display', 'inline-block');
// Fix for firefox
div.style('white-space', 'nowrap');
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
return fo.node();
}
const createLabel = (_vertexText, style, isTitle, isNode) => {
let vertexText = _vertexText || '';
if (getConfig().flowchart.htmlLabels) {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
logger.info('vertexText' + vertexText);
const node = {
isNode,
label: vertexText.replace(
/fa[lrsb]?:fa-[\w-]+/g,
s => `<i class='${s.replace(':', ' ')}'></i>`
)
};
let vertexNode = addHtmlLabel(node);
// vertexNode.parentNode.removeChild(vertexNode);
return vertexNode;
} else {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
let rows = [];
if (typeof vertexText === 'string') {
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi);
} else if (Array.isArray(vertexText)) {
rows = vertexText;
} else {
rows = [];
}
for (let j = 0; j < rows.length; j++) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '0');
if (isTitle) {
tspan.setAttribute('class', 'title-row');
} else {
tspan.setAttribute('class', 'row');
}
tspan.textContent = rows[j].trim();
svgLabel.appendChild(tspan);
}
return svgLabel;
} }
return svgLabel;
}; };
export default createLabel; export default createLabel;

View File

@@ -1,6 +1,6 @@
import { logger } from '../logger'; // eslint-disable-line import { logger } from '../logger'; // eslint-disable-line
import createLabel from './createLabel'; import createLabel from './createLabel';
import { line, curveBasis } from 'd3'; import { line, curveBasis, select } from 'd3';
import { getConfig } from '../config'; import { getConfig } from '../config';
let edgeLabels = {}; let edgeLabels = {};
@@ -21,7 +21,14 @@ export const insertEdgeLabel = (elem, edge) => {
label.node().appendChild(labelElement); label.node().appendChild(labelElement);
// Center the label // Center the label
const bbox = labelElement.getBBox(); let bbox = labelElement.getBBox();
if (getConfig().flowchart.htmlLabels) {
const div = labelElement.children[0];
const dv = select(labelElement);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
// Make element accessible by id for positioning // Make element accessible by id for positioning

View File

@@ -2,11 +2,12 @@ import intersect from './intersect/index.js';
import { select } from 'd3'; import { select } from 'd3';
import { logger } from '../logger'; // eslint-disable-line import { logger } from '../logger'; // eslint-disable-line
import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util'; import { labelHelper, updateNodeBounds, insertPolygonShape } from './shapes/util';
import { getConfig } from '../config';
import createLabel from './createLabel'; import createLabel from './createLabel';
import note from './shapes/note'; import note from './shapes/note';
const question = (parent, node) => { const question = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -28,7 +29,7 @@ const question = (parent, node) => {
}; };
const hexagon = (parent, node) => { const hexagon = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const f = 4; const f = 4;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -53,7 +54,7 @@ const hexagon = (parent, node) => {
}; };
const rect_left_inv_arrow = (parent, node) => { const rect_left_inv_arrow = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -75,7 +76,7 @@ const rect_left_inv_arrow = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const lean_right = (parent, node) => { const lean_right = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -97,7 +98,7 @@ const lean_right = (parent, node) => {
}; };
const lean_left = (parent, node) => { const lean_left = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -119,7 +120,7 @@ const lean_left = (parent, node) => {
}; };
const trapezoid = (parent, node) => { const trapezoid = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -140,7 +141,7 @@ const trapezoid = (parent, node) => {
}; };
const inv_trapezoid = (parent, node) => { const inv_trapezoid = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -160,7 +161,7 @@ const inv_trapezoid = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const rect_right_inv_arrow = (parent, node) => { const rect_right_inv_arrow = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@@ -181,7 +182,7 @@ const rect_right_inv_arrow = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const cylinder = (parent, node) => { const cylinder = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const rx = w / 2; const rx = w / 2;
@@ -248,7 +249,7 @@ const cylinder = (parent, node) => {
}; };
const rect = (parent, node) => { const rect = (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes); const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true);
logger.trace('Classes = ', node.classes); logger.trace('Classes = ', node.classes);
// add the rect // add the rect
@@ -296,20 +297,54 @@ const rectWithTitle = (parent, node) => {
const text2 = node.labelText.flat(); const text2 = node.labelText.flat();
logger.info('Label text', text2[0]); logger.info('Label text', text2[0]);
const text = label.node().appendChild(createLabel(text2[0], node.labelStyle, true)); const text = label.node().appendChild(createLabel(text2[0], node.labelStyle, true, true));
let bbox;
if (getConfig().flowchart.htmlLabels) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
logger.info('Text 2', text2);
const textRows = text2.slice(1, text2.length); const textRows = text2.slice(1, text2.length);
let titleBox = text.getBBox(); let titleBox = text.getBBox();
const descr = label const descr = label
.node() .node()
.appendChild(createLabel(textRows.join('<br/>'), node.labelStyle, true)); .appendChild(createLabel(textRows.join('<br/>'), node.labelStyle, true, true));
logger.info(descr); if (getConfig().flowchart.htmlLabels) {
const div = descr.children[0];
const dv = select(descr);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
// bbox = label.getBBox();
// logger.info(descr);
const halfPadding = node.padding / 2; const halfPadding = node.padding / 2;
select(descr).attr('transform', 'translate( 0' + ', ' + (titleBox.height + halfPadding) + ')'); select(descr).attr(
'transform',
'translate( ' +
// (titleBox.width - bbox.width) / 2 +
(bbox.width > titleBox.width ? 0 : (titleBox.width - bbox.width) / 2) +
', ' +
(titleBox.height + halfPadding + 5) +
')'
);
select(text).attr(
'transform',
'translate( ' +
// (titleBox.width - bbox.width) / 2 +
(bbox.width < titleBox.width ? 0 : -(titleBox.width - bbox.width) / 2) +
', ' +
0 +
')'
);
// Get the size of the label // Get the size of the label
// Bounding box for title and text // Bounding box for title and text
const bbox = label.node().getBBox(); bbox = label.node().getBBox();
// Center the label // Center the label
label.attr( label.attr(
@@ -341,7 +376,7 @@ const rectWithTitle = (parent, node) => {
}; };
const stadium = (parent, node) => { const stadium = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
const w = bbox.width + h / 4 + node.padding; const w = bbox.width + h / 4 + node.padding;
@@ -365,7 +400,7 @@ const stadium = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const circle = (parent, node) => { const circle = (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node); const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, undefined, true);
const circle = shapeSvg.insert('circle', ':first-child'); const circle = shapeSvg.insert('circle', ':first-child');
// center the circle around its coordinate // center the circle around its coordinate
@@ -386,7 +421,7 @@ const circle = (parent, node) => {
}; };
const subroutine = (parent, node) => { const subroutine = (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node); const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;

View File

@@ -3,7 +3,7 @@ import { logger } from '../../logger'; // eslint-disable-line
import intersect from '../intersect/index.js'; import intersect from '../intersect/index.js';
const note = (parent, node) => { const note = (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes); const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true);
logger.info('Classes = ', node.classes); logger.info('Classes = ', node.classes);
// add the rect // add the rect

View File

@@ -1,6 +1,7 @@
import createLabel from '../createLabel'; import createLabel from '../createLabel';
import { getConfig } from '../../config';
export const labelHelper = (parent, node, _classes) => { import { select } from 'd3';
export const labelHelper = (parent, node, _classes, isNode) => {
let classes; let classes;
if (!_classes) { if (!_classes) {
classes = 'node default'; classes = 'node default';
@@ -16,10 +17,20 @@ export const labelHelper = (parent, node, _classes) => {
// Create the label and insert it after the rect // Create the label and insert it after the rect
const label = shapeSvg.insert('g').attr('class', 'label'); const label = shapeSvg.insert('g').attr('class', 'label');
const text = label.node().appendChild(createLabel(node.labelText, node.labelStyle)); const text = label
.node()
.appendChild(createLabel(node.labelText, node.labelStyle, false, isNode));
// Get the size of the label // Get the size of the label
const bbox = text.getBBox(); let bbox = text.getBBox();
if (getConfig().flowchart.htmlLabels) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);
dv.attr('height', bbox.height);
}
const halfPadding = node.padding / 2; const halfPadding = node.padding / 2;

View File

@@ -148,6 +148,12 @@ export const clear = function() {
root: newDoc() root: newDoc()
}; };
currentDocument = documents.root; currentDocument = documents.root;
currentDocument = documents.root;
startCnt = 0;
endCnt = 0; // eslint-disable-line
classes = [];
}; };
export const getState = function(id) { export const getState = function(id) {
@@ -213,7 +219,7 @@ const getDividerId = () => {
return 'divider-id-' + dividerCnt; return 'divider-id-' + dividerCnt;
}; };
const classes = []; let classes = [];
const getClasses = () => classes; const getClasses = () => classes;

View File

@@ -15,7 +15,7 @@ export const setConf = function(cnf) {
} }
}; };
const nodeDb = {}; let nodeDb = {};
/** /**
* Returns the all the styles from classDef statements in the graph definition. * Returns the all the styles from classDef statements in the graph definition.
@@ -203,6 +203,7 @@ const setupDoc = (g, parent, doc, altFlag) => {
export const draw = function(text, id) { export const draw = function(text, id) {
logger.info('Drawing state diagram (v2)', id); logger.info('Drawing state diagram (v2)', id);
stateDb.clear(); stateDb.clear();
nodeDb = {};
const parser = state.parser; const parser = state.parser;
parser.yy = stateDb; parser.yy = stateDb;