Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split formatted markdown strings with unicode support. #4470

Merged
merged 29 commits into from Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c41df42
Add splitText
sidharthv96 Jun 9, 2023
0a437f5
feat: split unicode properly
sidharthv96 Jun 9, 2023
d4edd98
Update docs
sidharthv96 Jun 9, 2023
7b46017
Use splitLineToFitWidth function
sidharthv96 Jun 9, 2023
3f080e5
Merge branch 'sidv/splitUnicode' of https://github.com/mermaid-js/mer…
sidharthv96 Jun 9, 2023
b3ce56c
Cleanup
sidharthv96 Jun 9, 2023
a379cd0
Add logs
sidharthv96 Jun 12, 2023
c85e479
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jun 12, 2023
5903792
rename handle-markdown-text
sidharthv96 Jun 13, 2023
dd4e146
Add types
sidharthv96 Jun 13, 2023
b36a017
Use joiner to split unicode
sidharthv96 Jun 13, 2023
25a85ee
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jun 13, 2023
445da58
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jun 13, 2023
b0a104e
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jul 6, 2023
f548463
createText to TS
sidharthv96 Jul 6, 2023
60a93f7
Handle proper formatting for markdown strings
sidharthv96 Jul 6, 2023
5ac70bb
Fix flowchart failure
sidharthv96 Jul 6, 2023
7d996c3
Cleanup
sidharthv96 Jul 6, 2023
b14bcda
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jul 6, 2023
d58c41d
Add tests without Intl.Segmenter
sidharthv96 Jul 7, 2023
28406fc
Add comments
sidharthv96 Jul 7, 2023
eca2efa
Update packages/mermaid/src/rendering-util/splitText.spec.ts
sidharthv96 Jul 8, 2023
ea192cc
Merge branch 'develop' into sidv/splitUnicode
sidharthv96 Jul 25, 2023
68305fe
Fix lint
sidharthv96 Jul 25, 2023
4ea1227
Update docs
sidharthv96 Jul 25, 2023
409dedb
Remove redundant test.
sidharthv96 Jul 25, 2023
a7d4072
Update packages/mermaid/src/rendering-util/types.d.ts
sidharthv96 Jul 25, 2023
841ae15
Add comments
sidharthv96 Jul 25, 2023
651bc98
Merge branch 'sidv/splitUnicode' of https://github.com/mermaid-js/mer…
sidharthv96 Jul 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/config/usage.md
Expand Up @@ -228,7 +228,7 @@ mermaid fully supports webpack. Here is a [working demo](https://github.com/merm

The main idea of the API is to be able to call a render function with the graph definition as a string. The render function will render the graph and call a callback with the resulting SVG code. With this approach it is up to the site creator to fetch the graph definition from the site (perhaps from a textarea), render it and place the graph somewhere in the site.

The example below show an outline of how this could be used. The example just logs the resulting SVG to the JavaScript console.
The example below shows an example of how this could be used. The example just logs the resulting SVG to the JavaScript console.

```html
<script type="module">
Expand Down
16 changes: 0 additions & 16 deletions packages/mermaid/src/diagrams/class/classDiagramGrammar.spec.ts

This file was deleted.

@@ -1,31 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// @ts-nocheck TODO: Fix types
import { log } from '../logger.js';
import { decodeEntities } from '../mermaidAPI.js';
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js';
/**
* @param dom
* @param styleFn
*/
import { splitLineToFitWidth } from './splitText.js';
import { MarkdownLine, MarkdownWord } from './types.js';

function applyStyle(dom, styleFn) {
if (styleFn) {
dom.attr('style', styleFn);
}
}

/**
* @param element
* @param {any} node
* @param width
* @param classes
* @param addBackground
* @returns {SVGForeignObjectElement} Node
*/
function addHtmlSpan(element, node, width, classes, addBackground = false) {
const fo = element.append('foreignObject');
// const newEl = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
// const newEl = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
const div = fo.append('xhtml:div');
// const div = body.append('div');
// const div = fo.append('div');

const label = node.label;
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
Expand Down Expand Up @@ -64,12 +53,12 @@ function addHtmlSpan(element, node, width, classes, addBackground = false) {
/**
* 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.
* @param textElement - The parent text element to append the tspan element.
* @param lineIndex - The index of the current line in the structuredText array.
* @param lineHeight - The line height value for the text.
* @returns The created tspan element.
*/
function createTspan(textElement, lineIndex, lineHeight) {
function createTspan(textElement: any, lineIndex: number, lineHeight: number) {
return textElement
.append('tspan')
.attr('class', 'text-outer-tspan')
Expand All @@ -78,17 +67,10 @@ function createTspan(textElement, lineIndex, lineHeight) {
.attr('dy', lineHeight + 'em');
}

/**
* Compute the width of rendered text
* @param {object} parentNode
* @param {number} lineHeight
* @param {string} text
* @returns {number}
*/
function computeWidthOfText(parentNode, lineHeight, text) {
function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownLine): number {
const testElement = parentNode.append('text');
const testSpan = createTspan(testElement, 1, lineHeight);
updateTextContentAndStyles(testSpan, [{ content: text, type: 'normal' }]);
updateTextContentAndStyles(testSpan, line);
const textLength = testSpan.node().getComputedTextLength();
testElement.remove();
return textLength;
Expand All @@ -98,59 +80,37 @@ function computeWidthOfText(parentNode, lineHeight, text) {
* 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.
* @param addBackground
* @param width - The maximum allowed width of the text.
* @param g - The parent group element to append the formatted text.
* @param structuredText - The structured text data to format.
* @param addBackground - Whether to add a background to the text.
*/
function createFormattedText(width, g, structuredText, addBackground = false) {
function createFormattedText(
sidharthv96 marked this conversation as resolved.
Show resolved Hide resolved
width: number,
g: any,
structuredText: MarkdownWord[][],
addBackground = false
) {
const lineHeight = 1.1;
const labelGroup = g.append('g');
let bkg = labelGroup.insert('rect').attr('class', 'background');
const bkg = labelGroup.insert('rect').attr('class', 'background');
const textElement = labelGroup.append('text').attr('y', '-10.1');
// .attr('dominant-baseline', 'middle')
// .attr('text-anchor', 'middle');
// .attr('text-anchor', 'middle');
let lineIndex = 0;
structuredText.forEach((line) => {
for (const line of structuredText) {
/**
* Preprocess raw string content of line data
* Creating an array of strings pre-split to satisfy width limit
*/
let fullStr = line.map((data) => data.content).join(' ');
let tempStr = '';
let linesUnderWidth = [];
let prevIndex = 0;
if (computeWidthOfText(labelGroup, lineHeight, fullStr) <= width) {
linesUnderWidth.push(fullStr);
} else {
for (let i = 0; i <= fullStr.length; i++) {
tempStr = fullStr.slice(prevIndex, i);
log.info(tempStr, prevIndex, i);
if (computeWidthOfText(labelGroup, lineHeight, tempStr) > width) {
const subStr = fullStr.slice(prevIndex, i);
// Break at space if any
const lastSpaceIndex = subStr.lastIndexOf(' ');
if (lastSpaceIndex > -1) {
i = prevIndex + lastSpaceIndex + 1;
}
linesUnderWidth.push(fullStr.slice(prevIndex, i).trim());
prevIndex = i;
tempStr = null;
}
}
if (tempStr != null) {
linesUnderWidth.push(tempStr);
}
}
const checkWidth = (line: MarkdownLine) =>
computeWidthOfText(labelGroup, lineHeight, line) <= width;
const linesUnderWidth = checkWidth(line) ? [line] : splitLineToFitWidth(line, checkWidth);
/** Add each prepared line as a tspan to the parent node */
const preparedLines = linesUnderWidth.map((w) => ({ content: w, type: line.type }));
for (const preparedLine of preparedLines) {
let tspan = createTspan(textElement, lineIndex, lineHeight);
updateTextContentAndStyles(tspan, [preparedLine]);
for (const preparedLine of linesUnderWidth) {
const tspan = createTspan(textElement, lineIndex, lineHeight);
updateTextContentAndStyles(tspan, preparedLine);
lineIndex++;
}
});
}
if (addBackground) {
const bbox = textElement.node().getBBox();
const padding = 2;
Expand All @@ -159,7 +119,6 @@ function createFormattedText(width, g, structuredText, addBackground = false) {
.attr('y', -padding)
.attr('width', bbox.width + 2 * padding)
.attr('height', bbox.height + 2 * padding);
// .style('fill', 'red');

return labelGroup.node();
} else {
Expand All @@ -171,40 +130,27 @@ function createFormattedText(width, g, structuredText, addBackground = false) {
* 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.
* @param tspan - The tspan element to update.
* @param wrappedLine - The line data to apply to the tspan element.
*/
function updateTextContentAndStyles(tspan, wrappedLine) {
function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) {
tspan.text('');

wrappedLine.forEach((word, index) => {
const innerTspan = tspan
.append('tspan')
.attr('font-style', word.type === 'em' ? 'italic' : 'normal')
.attr('font-style', word.type === 'emphasis' ? 'italic' : 'normal')
nirname marked this conversation as resolved.
Show resolved Hide resolved
.attr('class', 'text-inner-tspan')
.attr('font-weight', word.type === 'strong' ? 'bold' : 'normal');
const special = ['"', "'", '.', ',', ':', ';', '!', '?', '(', ')', '[', ']', '{', '}'];
if (index === 0) {
innerTspan.text(word.content);
} else {
// TODO: check what joiner to use.
innerTspan.text(' ' + word.content);
}
});
}

/**
*
* @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 = (
Expand Down Expand Up @@ -234,7 +180,7 @@ export const createText = (
),
labelStyle: style.replace('fill:', 'color:'),
};
let vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
const vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
return vertexNode;
} else {
const structuredText = markdownToLines(text);
Expand Down
21 changes: 16 additions & 5 deletions packages/mermaid/src/rendering-util/handle-markdown-text.spec.ts
Expand Up @@ -152,19 +152,30 @@ test('markdownToLines - Only italic formatting', () => {
});

it('markdownToLines - Mixed formatting', () => {
const input = `*Italic* and **bold** formatting`;

const expectedOutput = [
let input = `*Italic* and **bold** formatting`;
let expected = [
[
{ content: 'Italic', type: 'emphasis' },
{ content: 'and', type: 'normal' },
{ content: 'bold', type: 'strong' },
{ content: 'formatting', type: 'normal' },
],
];
expect(markdownToLines(input)).toEqual(expected);

const output = markdownToLines(input);
expect(output).toEqual(expectedOutput);
input = `*Italic with space* and **bold ws** formatting`;
expected = [
[
{ content: 'Italic', type: 'emphasis' },
{ content: 'with', type: 'emphasis' },
{ content: 'space', type: 'emphasis' },
{ content: 'and', type: 'normal' },
{ content: 'bold', type: 'strong' },
{ content: 'ws', type: 'strong' },
{ content: 'formatting', type: 'normal' },
],
];
expect(markdownToLines(input)).toEqual(expected);
});

it('markdownToLines - Mixed formatting', () => {
Expand Down
@@ -1,11 +1,13 @@
import type { Content } from 'mdast';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { dedent } from 'ts-dedent';
import { MarkdownLine, MarkdownWordType } from './types.js';

/**
* @param {string} markdown markdown to process
* @returns {string} processed markdown
* @param markdown - markdown to process
* @returns processed markdown
*/
function preprocessMarkdown(markdown) {
function preprocessMarkdown(markdown: string): string {
// Replace multiple newlines with a single newline
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, '\n');
// Remove extra spaces at the beginning of each line
Expand All @@ -14,19 +16,15 @@ function preprocessMarkdown(markdown) {
}

/**
* @param {string} markdown markdown to split into lines
* @param markdown - markdown to split into lines
*/
export function markdownToLines(markdown) {
export function markdownToLines(markdown: string): MarkdownLine[] {
const preprocessedMarkdown = preprocessMarkdown(markdown);
const { children } = fromMarkdown(preprocessedMarkdown);
const lines = [[]];
const lines: MarkdownLine[] = [[]];
let currentLine = 0;

/**
* @param {import('mdast').Content} node
* @param {string} [parentType]
*/
function processNode(node, parentType = 'normal') {
function processNode(node: Content, parentType: MarkdownWordType = 'normal') {
if (node.type === 'text') {
const textLines = node.value.split('\n');
textLines.forEach((textLine, index) => {
Expand Down Expand Up @@ -58,17 +56,10 @@ export function markdownToLines(markdown) {
return lines;
}

/**
* @param {string} markdown markdown to convert to HTML
* @returns {string} HTML
*/
export function markdownToHTML(markdown) {
export function markdownToHTML(markdown: string) {
const { children } = fromMarkdown(markdown);

/**
* @param {import('mdast').Content} node
*/
function output(node) {
function output(node: Content): string {
if (node.type === 'text') {
return node.value.replace(/\n/g, '<br/>');
} else if (node.type === 'strong') {
Expand Down