#4220 Parsing the text as markdown and rendering accordingly

This commit is contained in:
Knut Sveidqvist
2023-03-20 14:15:26 +01:00
parent 853d9b7f98
commit a1c50b8079
8 changed files with 434 additions and 84 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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')

View File

@@ -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);
}
};

View 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('');
}

View 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);
});