mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-18 23:09:49 +02:00
#4220 Parsing the text as markdown and rendering accordingly
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"@khanacademy/simple-markdown": "^0.8.6",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
|
@@ -33,7 +33,7 @@ export const addNode = (level, id, descr, type) => {
|
||||
id: cnt++,
|
||||
nodeId: sanitizeText(id),
|
||||
level,
|
||||
descr: sanitizeText(descr).replace(/\n/g, '<br />'),
|
||||
descr: sanitizeText(descr),
|
||||
type,
|
||||
children: [],
|
||||
width: getConfig().mindmap.maxNodeWidth,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { select } from 'd3';
|
||||
import * as db from './mindmapDb';
|
||||
import { createText, setSize } from '../../rendering-util/createText';
|
||||
import { createText } from '../../rendering-util/createText';
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
/**
|
||||
@@ -217,9 +217,8 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
||||
|
||||
// Create the wrapped text element
|
||||
const textElem = nodeElem.append('g');
|
||||
|
||||
const newEl = createText(textElem, node.descr, { useHtmlLabels: htmlLabels });
|
||||
const txt = textElem.node().appendChild(newEl);
|
||||
const newEl = createText(textElem, node.descr, { useHtmlLabels: htmlLabels, width: node.width });
|
||||
// const txt = textElem.node().appendChild(newEl);
|
||||
// const txt = textElem.append(newEl);
|
||||
// const txt = textElem
|
||||
// .append('text')
|
||||
|
@@ -3,7 +3,7 @@ import { log } from '../logger';
|
||||
import { getConfig } from '../config';
|
||||
import { evaluate } from '../diagrams/common/common';
|
||||
import { decodeEntities } from '../mermaidAPI';
|
||||
|
||||
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text';
|
||||
/**
|
||||
* @param dom
|
||||
* @param styleFn
|
||||
@@ -51,51 +51,79 @@ function addHtmlSpan(element, node) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text The text to be wrapped
|
||||
* @param {number} width The max width of the text
|
||||
* Creates a tspan element with the specified attributes for text positioning.
|
||||
*
|
||||
* @param {object} textElement - The parent text element to append the tspan element.
|
||||
* @param {number} lineIndex - The index of the current line in the structuredText array.
|
||||
* @param {number} lineHeight - The line height value for the text.
|
||||
* @returns {object} The created tspan element.
|
||||
*/
|
||||
function wrap(text, width) {
|
||||
text.each(function () {
|
||||
var text = select(this),
|
||||
words = text
|
||||
.text()
|
||||
.split(/(\s+|<br\/>)/)
|
||||
.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 === '<br/>') {
|
||||
line.pop();
|
||||
tspan.text(line.join(' ').trim());
|
||||
if (word === '<br/>') {
|
||||
line = [''];
|
||||
} else {
|
||||
line = [word];
|
||||
}
|
||||
function createTspan(textElement, lineIndex, lineHeight) {
|
||||
return textElement
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', lineIndex * lineHeight + 'em')
|
||||
.attr('dy', lineHeight + 'em');
|
||||
}
|
||||
|
||||
tspan = text
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', lineHeight + 'em')
|
||||
.text(word);
|
||||
/**
|
||||
* Creates a formatted text element by breaking lines and applying styles based on
|
||||
* the given structuredText.
|
||||
*
|
||||
* @param {number} width - The maximum allowed width of the text.
|
||||
* @param {object} g - The parent group element to append the formatted text.
|
||||
* @param {Array} structuredText - The structured text data to format.
|
||||
*/
|
||||
function createFormattedText(width, g, structuredText) {
|
||||
const lineHeight = 1.1;
|
||||
|
||||
const textElement = g.append('text');
|
||||
|
||||
structuredText.forEach((line, lineIndex) => {
|
||||
let tspan = createTspan(textElement, lineIndex, lineHeight);
|
||||
|
||||
let words = [...line].reverse();
|
||||
let currentWord;
|
||||
let wrappedLine = [];
|
||||
|
||||
while (words.length) {
|
||||
currentWord = words.pop();
|
||||
wrappedLine.push(currentWord);
|
||||
|
||||
updateTextContentAndStyles(tspan, wrappedLine);
|
||||
|
||||
if (tspan.node().getComputedTextLength() > width) {
|
||||
wrappedLine.pop();
|
||||
words.push(currentWord);
|
||||
|
||||
updateTextContentAndStyles(tspan, wrappedLine);
|
||||
|
||||
wrappedLine = [];
|
||||
tspan = createTspan(textElement, ++lineIndex, lineHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text content and styles of the given tspan element based on the
|
||||
* provided wrappedLine data.
|
||||
*
|
||||
* @param {object} tspan - The tspan element to update.
|
||||
* @param {Array} wrappedLine - The line data to apply to the tspan element.
|
||||
*/
|
||||
function updateTextContentAndStyles(tspan, wrappedLine) {
|
||||
tspan.text('');
|
||||
|
||||
wrappedLine.forEach((word) => {
|
||||
tspan
|
||||
.append('tspan')
|
||||
.attr('font-style', word.type === 'em' ? 'italic' : 'normal')
|
||||
.attr('font-weight', word.type === 'strong' ? 'bold' : 'normal')
|
||||
.text(word.content + ' ');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param el
|
||||
@@ -114,15 +142,17 @@ function wrap(text, width) {
|
||||
export const createText = (
|
||||
el,
|
||||
text = '',
|
||||
{ style = '', isTitle = false, classes = '', useHtmlLabels = true, isNode = true } = {}
|
||||
{ style = '', isTitle = false, classes = '', useHtmlLabels = true, isNode = true, width } = {}
|
||||
) => {
|
||||
log.info('createText', text, style, isTitle, classes, useHtmlLabels, isNode);
|
||||
if (useHtmlLabels) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
text = text.replace(/\\n|\n/g, '<br />');
|
||||
log.info('text' + text);
|
||||
// text = text.replace(/\\n|\n/g, '<br />');
|
||||
const htmlText = markdownToHTML(text);
|
||||
// log.info('markdo wnToHTML' + text, markdownToHTML(text));
|
||||
const node = {
|
||||
isNode,
|
||||
label: decodeEntities(text).replace(
|
||||
label: decodeEntities(htmlText).replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
@@ -131,31 +161,7 @@ export const createText = (
|
||||
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|<br\s*\/?>/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;
|
||||
const structuredText = markdownToLines(text);
|
||||
return createFormattedText(width, el, structuredText);
|
||||
}
|
||||
};
|
||||
|
78
packages/mermaid/src/rendering-util/handle-markdown-text.js
Normal file
78
packages/mermaid/src/rendering-util/handle-markdown-text.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import SimpleMarkdown from '@khanacademy/simple-markdown';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param markdown
|
||||
*/
|
||||
export function markdownToLines(markdown) {
|
||||
const mdParse = SimpleMarkdown.defaultBlockParse;
|
||||
const syntaxTree = mdParse(markdown);
|
||||
|
||||
let lines = [[]];
|
||||
let currentLine = 0;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @param parentType
|
||||
*/
|
||||
function processNode(node, parentType) {
|
||||
if (node.type === 'text') {
|
||||
const textLines = node.content.split('\n');
|
||||
textLines.forEach((textLine, index) => {
|
||||
if (index !== 0) {
|
||||
currentLine++;
|
||||
lines.push([]);
|
||||
}
|
||||
textLine.split(' ').forEach((word) => {
|
||||
if (word) {
|
||||
lines[currentLine].push({ content: word, type: parentType || 'normal' });
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (node.type === 'strong' || node.type === 'em') {
|
||||
node.content.forEach((contentNode) => {
|
||||
processNode(contentNode, node.type);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
syntaxTree.forEach((treeNode) => {
|
||||
if (treeNode.type === 'paragraph') {
|
||||
treeNode.content.forEach((contentNode) => {
|
||||
processNode(contentNode);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param markdown
|
||||
*/
|
||||
export function markdownToHTML(markdown) {
|
||||
const mdParse = SimpleMarkdown.defaultBlockParse;
|
||||
const syntaxTree = mdParse(markdown);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
*/
|
||||
function output(node) {
|
||||
if (node.type === 'text') {
|
||||
return node.content.replace(/\n/g, '<br>');
|
||||
} else if (node.type === 'strong') {
|
||||
return `<strong>${node.content.map(output).join('')}</strong>`;
|
||||
} else if (node.type === 'em') {
|
||||
return `<em>${node.content.map(output).join('')}</em>`;
|
||||
} else if (node.type === 'paragraph') {
|
||||
return `<p>${node.content.map(output).join('')}</p>`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return syntaxTree.map(output).join('');
|
||||
}
|
181
packages/mermaid/src/rendering-util/handle-markdown-text.spec.js
Normal file
181
packages/mermaid/src/rendering-util/handle-markdown-text.spec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
// import { test } from 'vitest';
|
||||
import { markdownToLines, markdownToHTML } from './handle-markdown-text';
|
||||
import { test } from 'vitest';
|
||||
|
||||
test('markdownToLines - Basic test', () => {
|
||||
const input = `This is regular text
|
||||
Here is a new line
|
||||
There is some words **with a bold** section
|
||||
Here is a line *with an italic* section`;
|
||||
|
||||
const expectedOutput = [
|
||||
[
|
||||
{ content: 'This', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'regular', type: 'normal' },
|
||||
{ content: 'text', type: 'normal' },
|
||||
],
|
||||
[
|
||||
{ content: 'Here', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'a', type: 'normal' },
|
||||
{ content: 'new', type: 'normal' },
|
||||
{ content: 'line', type: 'normal' },
|
||||
],
|
||||
[
|
||||
{ content: 'There', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'some', type: 'normal' },
|
||||
{ content: 'words', type: 'normal' },
|
||||
{ content: 'with', type: 'strong' },
|
||||
{ content: 'a', type: 'strong' },
|
||||
{ content: 'bold', type: 'strong' },
|
||||
{ content: 'section', type: 'normal' },
|
||||
],
|
||||
[
|
||||
{ content: 'Here', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'a', type: 'normal' },
|
||||
{ content: 'line', type: 'normal' },
|
||||
{ content: 'with', type: 'em' },
|
||||
{ content: 'an', type: 'em' },
|
||||
{ content: 'italic', type: 'em' },
|
||||
{ content: 'section', type: 'normal' },
|
||||
],
|
||||
];
|
||||
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToLines - Empty input', () => {
|
||||
const input = '';
|
||||
const expectedOutput = [[]];
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToLines - No formatting', () => {
|
||||
const input = `This is a simple test
|
||||
with no formatting`;
|
||||
|
||||
const expectedOutput = [
|
||||
[
|
||||
{ content: 'This', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'a', type: 'normal' },
|
||||
{ content: 'simple', type: 'normal' },
|
||||
{ content: 'test', type: 'normal' },
|
||||
],
|
||||
[
|
||||
{ content: 'with', type: 'normal' },
|
||||
{ content: 'no', type: 'normal' },
|
||||
{ content: 'formatting', type: 'normal' },
|
||||
],
|
||||
];
|
||||
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToLines - Only bold formatting', () => {
|
||||
const input = `This is a **bold** test`;
|
||||
|
||||
const expectedOutput = [
|
||||
[
|
||||
{ content: 'This', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'a', type: 'normal' },
|
||||
{ content: 'bold', type: 'strong' },
|
||||
{ content: 'test', type: 'normal' },
|
||||
],
|
||||
];
|
||||
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToLines - Only italic formatting', () => {
|
||||
const input = `This is an *italic* test`;
|
||||
|
||||
const expectedOutput = [
|
||||
[
|
||||
{ content: 'This', type: 'normal' },
|
||||
{ content: 'is', type: 'normal' },
|
||||
{ content: 'an', type: 'normal' },
|
||||
{ content: 'italic', type: 'em' },
|
||||
{ content: 'test', type: 'normal' },
|
||||
],
|
||||
];
|
||||
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('markdownToLines - Mixed formatting', () => {
|
||||
const input = `*Italic* and **bold** formatting`;
|
||||
|
||||
const expectedOutput = [
|
||||
[
|
||||
{ content: 'Italic', type: 'em' },
|
||||
{ content: 'and', type: 'normal' },
|
||||
{ content: 'bold', type: 'strong' },
|
||||
{ content: 'formatting', type: 'normal' },
|
||||
],
|
||||
];
|
||||
|
||||
const output = markdownToLines(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Basic test', () => {
|
||||
const input = `This is regular text
|
||||
Here is a new line
|
||||
There is some words **with a bold** section
|
||||
Here is a line *with an italic* section`;
|
||||
|
||||
const expectedOutput = `<p>This is regular text<br>Here is a new line<br>There is some words <strong>with a bold</strong> section<br>Here is a line <em>with an italic</em> section</p>`;
|
||||
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Empty input', () => {
|
||||
const input = '';
|
||||
const expectedOutput = '';
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - No formatting', () => {
|
||||
const input = `This is a simple test
|
||||
with no formatting`;
|
||||
|
||||
const expectedOutput = `<p>This is a simple test<br>with no formatting</p>`;
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Only bold formatting', () => {
|
||||
const input = `This is a **bold** test`;
|
||||
|
||||
const expectedOutput = `<p>This is a <strong>bold</strong> test</p>`;
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Only italic formatting', () => {
|
||||
const input = `This is an *italic* test`;
|
||||
|
||||
const expectedOutput = `<p>This is an <em>italic</em> test</p>`;
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('markdownToHTML - Mixed formatting', () => {
|
||||
const input = `*Italic* and **bold** formatting`;
|
||||
|
||||
const expectedOutput = `<p><em>Italic</em> and <strong>bold</strong> formatting</p>`;
|
||||
const output = markdownToHTML(input);
|
||||
expect(output).toEqual(expectedOutput);
|
||||
});
|
Reference in New Issue
Block a user