Skip to content

Commit

Permalink
Merge pull request #4470 from mermaid-js/sidv/splitUnicode
Browse files Browse the repository at this point in the history
Split formatted markdown strings with unicode support.
  • Loading branch information
knsv authored Jul 25, 2023
2 parents 509a580 + 651bc98 commit c99e1c6
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 134 deletions.
2 changes: 1 addition & 1 deletion docs/config/usage.md
Original file line number Diff line number Diff line change
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.

Original file line number Diff line number Diff line change
@@ -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(
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')
.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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit c99e1c6

Please sign in to comment.