diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index fccd65004..36f481a08 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -54,7 +54,7 @@
-+%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%% graph BT a{The cat in the hat} -- 1o --> b @@ -66,12 +66,13 @@ h --3i -->a b --> d(The dog in the hog) c --> d--flowchart-elk TB - a --> b - a --> c - b --> d - c --> d ++mindmap + id1["`Start +second line 😎`"] + id2[Child] + id3[Child] + id4[Child]%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%% diff --git a/packages/mermaid/src/dagre-wrapper/createLabel.js b/packages/mermaid/src/dagre-wrapper/createLabel.js index af5032096..ff7834c4f 100644 --- a/packages/mermaid/src/dagre-wrapper/createLabel.js +++ b/packages/mermaid/src/dagre-wrapper/createLabel.js @@ -41,7 +41,13 @@ function addHtmlLabel(node) { div.attr('xmlns', 'http://www.w3.org/1999/xhtml'); return fo.node(); } - +/** + * @param _vertexText + * @param style + * @param isTitle + * @param isNode + * @deprecated svg-util/createText instead + */ const createLabel = (_vertexText, style, isTitle, isNode) => { let vertexText = _vertexText || ''; if (typeof vertexText === 'object') { diff --git a/packages/mermaid/src/diagrams/mindmap/mindmapDb.js b/packages/mermaid/src/diagrams/mindmap/mindmapDb.js index 71aa449d9..7585029cf 100644 --- a/packages/mermaid/src/diagrams/mindmap/mindmapDb.js +++ b/packages/mermaid/src/diagrams/mindmap/mindmapDb.js @@ -33,7 +33,7 @@ export const addNode = (level, id, descr, type) => { id: cnt++, nodeId: sanitizeText(id), level, - descr: sanitizeText(descr), + descr: sanitizeText(descr).replace(/\n/g, '
'), type, children: [], width: getConfig().mindmap.maxNodeWidth, diff --git a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison index d2f6bbf1a..84a6191cf 100644 --- a/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison +++ b/packages/mermaid/src/diagrams/mindmap/parser/mindmap.jison @@ -12,6 +12,7 @@ %} %x NODE %x NSTR +%x NSTR2 %x ICON %x CLASS @@ -41,6 +42,9 @@ // !(-\() return 'NODE_ID'; [^\(\[\n\-\)\{\}]+ return 'NODE_ID'; <> return 'EOF'; + ["][`] { this.begin("NSTR2");} + [^`"]+ { return "NODE_DESCR";} + [`]["] { this.popState();} ["] { yy.getLogger().trace('Starting NSTR');this.begin("NSTR");} [^"]+ { yy.getLogger().trace('description:', yytext); return "NODE_DESCR";} ["] {this.popState();} diff --git a/packages/mermaid/src/diagrams/mindmap/svgDraw.js b/packages/mermaid/src/diagrams/mindmap/svgDraw.js index 2b1aa021e..2c3dcca56 100644 --- a/packages/mermaid/src/diagrams/mindmap/svgDraw.js +++ b/packages/mermaid/src/diagrams/mindmap/svgDraw.js @@ -1,5 +1,6 @@ import { select } from 'd3'; import * as db from './mindmapDb'; +import { createText, setSize } from '../../rendering-util/createText'; const MAX_SECTIONS = 12; /** @@ -11,7 +12,7 @@ function wrap(text, width) { var text = select(this), words = text .text() - .split(/(\s+|
)/) + .split(/(\s+|
)/) .reverse(), word, line = [], @@ -28,10 +29,10 @@ function wrap(text, width) { word = words[words.length - 1 - j]; line.push(word); tspan.text(line.join(' ').trim()); - if (tspan.node().getComputedTextLength() > width || word === '
') { + if (tspan.node().getComputedTextLength() > width || word === '
') { line.pop(); tspan.text(line.join(' ').trim()); - if (word === '
') { + if (word === '
') { line = ['']; } else { line = [word]; @@ -203,6 +204,7 @@ const roundedRectBkg = function (elem, node) { * @returns {number} The height nodes dom element */ export const drawNode = function (elem, node, fullSection, conf) { + const htmlLabels = false; const section = fullSection % (MAX_SECTIONS - 1); const nodeElem = elem.append('g'); node.section = section; @@ -215,15 +217,29 @@ export const drawNode = function (elem, node, fullSection, conf) { // Create the wrapped text element const textElem = nodeElem.append('g'); - const txt = textElem - .append('text') - .text(node.descr) - .attr('dy', '1em') - .attr('alignment-baseline', 'middle') - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'middle') - .call(wrap, node.width); - const bbox = txt.node().getBBox(); + + const newEl = createText(textElem, node.descr, { useHtmlLabels: htmlLabels }); + const txt = textElem.node().appendChild(newEl); + // const txt = textElem.append(newEl); + // const txt = textElem + // .append('text') + // .text(node.descr) + // .attr('dy', '1em') + // .attr('alignment-baseline', 'middle') + // .attr('dominant-baseline', 'middle') + // .attr('text-anchor', 'middle') + // .call(wrap, node.width); + // const newerEl = textElem.node().appendChild(newEl); + // setSize(textElem); + if (!htmlLabels) { + textElem + .attr('dy', '1em') + .attr('alignment-baseline', 'middle') + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'middle'); + } + // .call(wrap, node.width); + const bbox = textElem.node().getBBox(); const fontSize = conf.fontSize.replace ? conf.fontSize.replace('px', '') : conf.fontSize; node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding; node.width = bbox.width + 2 * node.padding; @@ -267,7 +283,16 @@ export const drawNode = function (elem, node, fullSection, conf) { ); } } else { - textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')'); + if (!htmlLabels) { + const dx = node.width / 2; + const dy = node.padding / 2; + textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')'); + // textElem.attr('transform', 'translate(' + node.width / 2 + ', ' + node.padding / 2 + ')'); + } else { + const dx = (node.width - bbox.width) / 2; + const dy = (node.height - bbox.height) / 2; + textElem.attr('transform', 'translate(' + dx + ', ' + dy + ')'); + } } switch (node.type) { diff --git a/packages/mermaid/src/rendering-util/createText.js b/packages/mermaid/src/rendering-util/createText.js new file mode 100644 index 000000000..58e0f54a7 --- /dev/null +++ b/packages/mermaid/src/rendering-util/createText.js @@ -0,0 +1,161 @@ +import { select } from 'd3'; +import { log } from '../logger'; +import { getConfig } from '../config'; +import { evaluate } from '../diagrams/common/common'; +import { decodeEntities } from '../mermaidAPI'; + +/** + * @param dom + * @param styleFn + */ +function applyStyle(dom, styleFn) { + if (styleFn) { + dom.attr('style', styleFn); + } +} + +/** + * @param element + * @param {any} node + * @returns {SVGForeignObjectElement} Node + */ +function addHtmlSpan(element, node) { + const fo = element.append('foreignObject'); + const newEl = 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( + '' + + label + + '' + ); + + applyStyle(div, node.labelStyle); + div.style('display', 'inline-block'); + const bbox = div.node().getBoundingClientRect(); + fo.style('width', bbox.width); + fo.style('height', bbox.height); + + const divNode = div.node(); + window.divNode = divNode; + // Fix for firefox + div.style('white-space', 'nowrap'); + div.attr('xmlns', 'http://www.w3.org/1999/xhtml'); + return fo.node(); +} + +/** + * @param {string} text The text to be wrapped + * @param {number} width The max width of the text + */ +function wrap(text, width) { + text.each(function () { + var text = select(this), + words = text + .text() + .split(/(\s+|
)/) + .reverse(), + word, + line = [], + lineHeight = 1.1, // ems + y = text.attr('y'), + dy = parseFloat(text.attr('dy')), + tspan = text + .text(null) + .append('tspan') + .attr('x', 0) + .attr('y', y) + .attr('dy', dy + 'em'); + for (let j = 0; j < words.length; j++) { + word = words[words.length - 1 - j]; + line.push(word); + tspan.text(line.join(' ').trim()); + if (tspan.node().getComputedTextLength() > width || word === '
') { + line.pop(); + tspan.text(line.join(' ').trim()); + if (word === '
') { + line = ['']; + } else { + line = [word]; + } + + tspan = text + .append('tspan') + .attr('x', 0) + .attr('y', y) + .attr('dy', lineHeight + 'em') + .text(word); + } + } + }); +} + +/** + * + * @param el + * @param {*} text + * @param {*} param1 + * @param root0 + * @param root0.style + * @param root0.isTitle + * @param root0.classes + * @param root0.useHtmlLabels + * @param root0.isNode + * @returns + */ +// Note when using from flowcharts converting the API isNode means classes should be set accordingly. When using htmlLabels => to sett classes to'nodeLabel' when isNode=true otherwise 'edgeLabel' +// When not using htmlLabels => to set classes to 'title-row' when isTitle=true otherwise 'title-row' +export const createText = ( + el, + text = '', + { style = '', isTitle = false, classes = '', useHtmlLabels = true, isNode = true } = {} +) => { + if (useHtmlLabels) { + // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? + text = text.replace(/\\n|\n/g, '
'); + log.info('text' + text); + const node = { + isNode, + label: decodeEntities(text).replace( + /fa[blrs]?:fa-[\w-]+/g, + (s) => `` + ), + labelStyle: style.replace('fill:', 'color:'), + }; + let vertexNode = addHtmlSpan(el, node); + return vertexNode; + } else { + const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + svgText.setAttribute('style', style.replace('color:', 'fill:')); + // el.attr('style', style.replace('color:', 'fill:')); + let rows = []; + if (typeof text === 'string') { + rows = text.split(/\\n|\n|
/gi); + } else if (Array.isArray(text)) { + rows = text; + } else { + rows = []; + } + + for (const row of rows) { + 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 = row.trim(); + svgText.appendChild(tspan); + } + return svgText; + } +};