From c41df420d773971f3a366f69279bee1ca901e031 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 9 Jun 2023 11:06:45 +0530 Subject: [PATCH 01/21] Add splitText --- .../src/rendering-util/splitText.spec.ts | 37 +++++ .../mermaid/src/rendering-util/splitText.ts | 135 ++++++++++++++++++ tsconfig.json | 2 +- 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 packages/mermaid/src/rendering-util/splitText.spec.ts create mode 100644 packages/mermaid/src/rendering-util/splitText.ts diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts new file mode 100644 index 000000000..6444627d2 --- /dev/null +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -0,0 +1,37 @@ +import { splitTextToChars, splitLineToFitWidthLoop, type CheckFitFunction } from './splitText.js'; +import { describe, it, expect } from 'vitest'; + +describe('splitText', () => { + it.each([ + { str: '', split: [] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'ok', split: ['o', 'k'] }, + ])('should split $str into graphemes', ({ str, split }: { str: string; split: string[] }) => { + expect(splitTextToChars(str)).toEqual(split); + }); +}); + +describe('split lines', () => { + it.each([ + // empty string + { str: '', width: 1, split: [''] }, + // Width >= Individual words + { str: 'hello world', width: 5, split: ['hello', 'world'] }, + { str: 'hello world', width: 7, split: ['hello', 'world'] }, + // width > full line + { str: 'hello world', width: 20, split: ['hello world'] }, + // width < individual word + { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, + { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ', width: 6, split: ['Flag πŸ³οΈβ€βš§οΈ', 'this πŸ³οΈβ€πŸŒˆ'] }, + ])( + 'should split $str into lines of $width characters', + ({ str, split, width }: { str: string; width: number; split: string[] }) => { + const checkFn: CheckFitFunction = (text: string) => { + return splitTextToChars(text).length <= width; + }; + expect(splitLineToFitWidthLoop(str.split(' '), checkFn)).toEqual(split); + } + ); +}); diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts new file mode 100644 index 000000000..de71fdafd --- /dev/null +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -0,0 +1,135 @@ +export type CheckFitFunction = (text: string) => boolean; + +/** + * Splits a string into graphemes if available, otherwise characters. + */ +export function splitTextToChars(text: string): string[] { + if (Intl.Segmenter) { + return [...new Intl.Segmenter().segment(text)].map((s) => s.segment); + } + return [...text]; +} + +export function splitWordToFitWidth(checkFit: CheckFitFunction, word: string): string[] { + console.error('splitWordToFitWidth', word); + const characters = splitTextToChars(word); + if (characters.length === 0) { + return []; + } + const newWord = []; + let lastCheckedCharacter = ''; + while (characters.length > 0) { + lastCheckedCharacter = characters.shift() ?? ' '; + if (checkFit([...newWord, lastCheckedCharacter].join(''))) { + newWord.push(lastCheckedCharacter); + } else if (newWord.length === 0) { + // Even the first character was too long, we cannot split it, so return it as is. + // This is an edge case that can happen when the first character is a long grapheme. + return [lastCheckedCharacter, characters.join('')]; + } else { + // The last character was too long, so we need to put it back and return the rest. + characters.unshift(lastCheckedCharacter); + break; + } + } + if (characters.length === 0) { + return [newWord.join('')]; + } + console.error({ newWord, characters }); + return [newWord.join(''), ...splitWordToFitWidth(checkFit, characters.join(''))]; +} + +export function splitWordToFitWidth2(checkFit: CheckFitFunction, word: string): [string, string] { + console.error('splitWordToFitWidth2', word); + const characters = splitTextToChars(word); + if (characters.length === 0) { + return ['', '']; + } + const newWord = []; + let lastCheckedCharacter = ''; + while (characters.length > 0) { + lastCheckedCharacter = characters.shift() ?? ' '; + if (checkFit([...newWord, lastCheckedCharacter].join(''))) { + newWord.push(lastCheckedCharacter); + } else if (newWord.length === 0) { + // Even the first character was too long, we cannot split it, so return it as is. + // This is an edge case that can happen when the first character is a long grapheme. + return [lastCheckedCharacter, characters.join('')]; + } else { + // The last character was too long, so we need to put it back and return the rest. + characters.unshift(lastCheckedCharacter); + break; + } + } + console.error({ newWord, characters }); + return [newWord.join(''), characters.join('')]; +} + +export function splitLineToFitWidth( + words: string[], + checkFit: CheckFitFunction, + lines: string[] = [], + popped: string[] = [] +): string[] { + console.error('splitLineToFitWidth', { words, lines, popped }); + // Return if there is nothing left to split + if (words.length === 0 && popped.length === 0) { + return lines; + } + const remainingText = words.join(' '); + if (checkFit(remainingText)) { + lines.push(remainingText); + words = [...popped]; + } + if (words.length > 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + popped.unshift(words.pop()!); + return splitLineToFitWidth(words, checkFit, lines, popped); + } else if (words.length === 1) { + const [word, rest] = splitWordToFitWidth(checkFit, words[0]); + lines.push(word); + console.error({ word, rest }); + if (rest) { + return splitLineToFitWidth([rest], checkFit, lines, []); + } + } + return lines; +} + +export function splitLineToFitWidthLoop(words: string[], checkFit: CheckFitFunction): string[] { + console.error('splitLineToFitWidthLoop', { words }); + if (words.length === 0) { + return []; + } + + const lines: string[] = []; + let newLine: string[] = []; + let lastCheckedWord = ''; + while (words.length > 0) { + lastCheckedWord = words.shift() ?? ' '; + console.error({ lastCheckedWord, words }); + if (checkFit([...newLine, lastCheckedWord].join(' '))) { + newLine.push(lastCheckedWord); + } else { + console.error({ newLine }); + if (newLine.length === 0) { + const [word, rest] = splitWordToFitWidth2(checkFit, lastCheckedWord); + console.error({ word, rest }); + lines.push(word); + if (rest) { + words.unshift(rest); + } + } else { + words.unshift(lastCheckedWord); + lines.push(newLine.join(' ')); + newLine = []; + } + } + console.error({ newLine, lastCheckedWord, words, lines }); + } + if (newLine.length > 0) { + lines.push(newLine.join(' ')); + } + console.error({ newLine, lastCheckedWord, words, lines }); + return lines; +} diff --git a/tsconfig.json b/tsconfig.json index 29c790cbb..4cbf209a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "lib": [ "DOM", - "ES2021" + "ES2022" ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ From 0a437f5800081d3fd0f1d047d7d801509fdbc885 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 9 Jun 2023 16:48:30 +0530 Subject: [PATCH 02/21] feat: split unicode properly --- .../src/rendering-util/splitText.spec.ts | 5 +- .../mermaid/src/rendering-util/splitText.ts | 163 +++++++----------- 2 files changed, 63 insertions(+), 105 deletions(-) diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 6444627d2..77bd6102c 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -1,4 +1,4 @@ -import { splitTextToChars, splitLineToFitWidthLoop, type CheckFitFunction } from './splitText.js'; +import { splitTextToChars, splitLineToFitWidth, type CheckFitFunction } from './splitText.js'; import { describe, it, expect } from 'vitest'; describe('splitText', () => { @@ -6,6 +6,7 @@ describe('splitText', () => { { str: '', split: [] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'ok', split: ['o', 'k'] }, + { str: 'abc', split: ['a', 'b', 'c'] }, ])('should split $str into graphemes', ({ str, split }: { str: string; split: string[] }) => { expect(splitTextToChars(str)).toEqual(split); }); @@ -31,7 +32,7 @@ describe('split lines', () => { const checkFn: CheckFitFunction = (text: string) => { return splitTextToChars(text).length <= width; }; - expect(splitLineToFitWidthLoop(str.split(' '), checkFn)).toEqual(split); + expect(splitLineToFitWidth(str, checkFn)).toEqual(split); } ); }); diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index de71fdafd..b8ee7a1b0 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -10,126 +10,83 @@ export function splitTextToChars(text: string): string[] { return [...text]; } -export function splitWordToFitWidth(checkFit: CheckFitFunction, word: string): string[] { - console.error('splitWordToFitWidth', word); - const characters = splitTextToChars(word); - if (characters.length === 0) { - return []; +/** + * Splits a string into words. + */ +function splitLineToWords(text: string): string[] { + if (Intl.Segmenter) { + return [...new Intl.Segmenter(undefined, { granularity: 'word' }).segment(text)] + .map((s) => s.segment) + .filter((word) => word !== ' '); } - const newWord = []; - let lastCheckedCharacter = ''; - while (characters.length > 0) { - lastCheckedCharacter = characters.shift() ?? ' '; - if (checkFit([...newWord, lastCheckedCharacter].join(''))) { - newWord.push(lastCheckedCharacter); - } else if (newWord.length === 0) { - // Even the first character was too long, we cannot split it, so return it as is. - // This is an edge case that can happen when the first character is a long grapheme. - return [lastCheckedCharacter, characters.join('')]; - } else { - // The last character was too long, so we need to put it back and return the rest. - characters.unshift(lastCheckedCharacter); - break; - } - } - if (characters.length === 0) { - return [newWord.join('')]; - } - console.error({ newWord, characters }); - return [newWord.join(''), ...splitWordToFitWidth(checkFit, characters.join(''))]; + return text.split(' '); } -export function splitWordToFitWidth2(checkFit: CheckFitFunction, word: string): [string, string] { - console.error('splitWordToFitWidth2', word); +/** + * Splits a word into two parts, the first part fits the width and the remaining part. + * @param checkFit - Function to check if word fits + * @param word - Word to split + * @returns [first part of word that fits, rest of word] + */ +export function splitWordToFitWidth(checkFit: CheckFitFunction, word: string): [string, string] { const characters = splitTextToChars(word); if (characters.length === 0) { return ['', '']; } - const newWord = []; - let lastCheckedCharacter = ''; - while (characters.length > 0) { - lastCheckedCharacter = characters.shift() ?? ' '; - if (checkFit([...newWord, lastCheckedCharacter].join(''))) { - newWord.push(lastCheckedCharacter); - } else if (newWord.length === 0) { - // Even the first character was too long, we cannot split it, so return it as is. - // This is an edge case that can happen when the first character is a long grapheme. - return [lastCheckedCharacter, characters.join('')]; - } else { - // The last character was too long, so we need to put it back and return the rest. - characters.unshift(lastCheckedCharacter); - break; - } - } - console.error({ newWord, characters }); - return [newWord.join(''), characters.join('')]; + return splitWordToFitWidthRecursion(checkFit, [], characters); } -export function splitLineToFitWidth( +function splitWordToFitWidthRecursion( + checkFit: CheckFitFunction, + usedChars: string[], + remainingChars: string[] +): [string, string] { + if (remainingChars.length === 0) { + return [usedChars.join(''), '']; + } + const [nextChar, ...rest] = remainingChars; + const newWord = [...usedChars, nextChar]; + if (checkFit(newWord.join(''))) { + return splitWordToFitWidthRecursion(checkFit, newWord, rest); + } + return [usedChars.join(''), remainingChars.join('')]; +} + +export function splitLineToFitWidth(line: string, checkFit: CheckFitFunction): string[] { + return splitLineToFitWidthRecursion(splitLineToWords(line), checkFit); +} + +function splitLineToFitWidthRecursion( words: string[], checkFit: CheckFitFunction, lines: string[] = [], - popped: string[] = [] + newLine = '' ): string[] { - console.error('splitLineToFitWidth', { words, lines, popped }); // Return if there is nothing left to split - if (words.length === 0 && popped.length === 0) { - return lines; - } - const remainingText = words.join(' '); - if (checkFit(remainingText)) { - lines.push(remainingText); - words = [...popped]; - } - if (words.length > 1) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - popped.unshift(words.pop()!); - return splitLineToFitWidth(words, checkFit, lines, popped); - } else if (words.length === 1) { - const [word, rest] = splitWordToFitWidth(checkFit, words[0]); - lines.push(word); - console.error({ word, rest }); - if (rest) { - return splitLineToFitWidth([rest], checkFit, lines, []); - } - } - return lines; -} - -export function splitLineToFitWidthLoop(words: string[], checkFit: CheckFitFunction): string[] { - console.error('splitLineToFitWidthLoop', { words }); if (words.length === 0) { - return []; + // If there is a new line, add it to the lines + if (newLine.length > 0) { + lines.push(newLine); + } + return lines.length > 0 ? lines : ['']; + } + const nextWord = words.shift() ?? ' '; + const lineWithNextWord = newLine ? `${newLine} ${nextWord}` : nextWord; + if (checkFit(lineWithNextWord)) { + // nextWord fits, so we can add it to the new line and continue + return splitLineToFitWidthRecursion(words, checkFit, lines, lineWithNextWord); } - const lines: string[] = []; - let newLine: string[] = []; - let lastCheckedWord = ''; - while (words.length > 0) { - lastCheckedWord = words.shift() ?? ' '; - console.error({ lastCheckedWord, words }); - if (checkFit([...newLine, lastCheckedWord].join(' '))) { - newLine.push(lastCheckedWord); - } else { - console.error({ newLine }); - if (newLine.length === 0) { - const [word, rest] = splitWordToFitWidth2(checkFit, lastCheckedWord); - console.error({ word, rest }); - lines.push(word); - if (rest) { - words.unshift(rest); - } - } else { - words.unshift(lastCheckedWord); - lines.push(newLine.join(' ')); - newLine = []; - } - } - console.error({ newLine, lastCheckedWord, words, lines }); - } + // nextWord doesn't fit, so we need to split it if (newLine.length > 0) { - lines.push(newLine.join(' ')); + // There was text in newLine, so add it to lines and push nextWord back into words. + lines.push(newLine); + words.unshift(nextWord); + } else { + // There was no text in newLine, so we need to split nextWord + const [line, rest] = splitWordToFitWidth(checkFit, nextWord); + lines.push(line); + words.unshift(rest); } - console.error({ newLine, lastCheckedWord, words, lines }); - return lines; + return splitLineToFitWidthRecursion(words, checkFit, lines); } From d4edd98b8a66c8f9a0e300376d2a55ad174cc74c Mon Sep 17 00:00:00 2001 From: sidharthv96 Date: Fri, 9 Jun 2023 11:24:11 +0000 Subject: [PATCH 03/21] Update docs --- docs/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5a822425a..c854a139c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -517,7 +517,7 @@ mermaidAPI.initialize({ - About Markpad integration [#323](https://github.com/knsv/mermaid/issues/323) - How to link backwards in flowchart? [#321](https://github.com/knsv/mermaid/issues/321) - Help with editor [#310](https://github.com/knsv/mermaid/issues/310) -- +1 [#293](https://github.com/knsv/mermaid/issues/293) +- \+1 [#293](https://github.com/knsv/mermaid/issues/293) - Basic chart does not render on Chome, but does in Firefox [#290](https://github.com/knsv/mermaid/issues/290) - Live editor is broken [#285](https://github.com/knsv/mermaid/issues/285) - "No such file or directory" trying to run mermaid 0.5.7 on OS X [#284](https://github.com/knsv/mermaid/issues/284) From 7b4601762ad6f604871dce82d272ce8cf915d5b1 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 9 Jun 2023 16:57:13 +0530 Subject: [PATCH 04/21] Use splitLineToFitWidth function --- .../mermaid/src/rendering-util/createText.js | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/mermaid/src/rendering-util/createText.js b/packages/mermaid/src/rendering-util/createText.js index 871f3425e..517097d7a 100644 --- a/packages/mermaid/src/rendering-util/createText.js +++ b/packages/mermaid/src/rendering-util/createText.js @@ -1,6 +1,7 @@ import { log } from '../logger.js'; import { decodeEntities } from '../mermaidAPI.js'; import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js'; +import { splitLineToFitWidth } from './splitText.js'; /** * @param dom * @param styleFn @@ -118,31 +119,10 @@ function createFormattedText(width, g, structuredText, addBackground = false) { * 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 = (str) => computeWidthOfText(labelGroup, lineHeight, str) <= width; + const linesUnderWidth = checkWidth(fullStr) + ? [fullStr] + : splitLineToFitWidth(fullStr, 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) { From b3ce56c7fcab1140d20f7cea2c5f131fc773c2cc Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 9 Jun 2023 17:03:02 +0530 Subject: [PATCH 05/21] Cleanup --- packages/mermaid/src/rendering-util/createText.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/mermaid/src/rendering-util/createText.js b/packages/mermaid/src/rendering-util/createText.js index 517097d7a..06fba94c7 100644 --- a/packages/mermaid/src/rendering-util/createText.js +++ b/packages/mermaid/src/rendering-util/createText.js @@ -109,9 +109,6 @@ function createFormattedText(width, g, structuredText, addBackground = false) { const labelGroup = g.append('g'); let 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) => { /** @@ -139,7 +136,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 { From a379cd02e70f22506b1169c9bc33d651ea56cb0c Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 12 Jun 2023 14:57:56 +0530 Subject: [PATCH 06/21] Add logs --- packages/mermaid/src/rendering-util/splitText.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index b8ee7a1b0..1cd16e35b 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -41,6 +41,8 @@ function splitWordToFitWidthRecursion( usedChars: string[], remainingChars: string[] ): [string, string] { + // eslint-disable-next-line no-console + console.error({ usedChars, remainingChars }); if (remainingChars.length === 0) { return [usedChars.join(''), '']; } @@ -62,6 +64,8 @@ function splitLineToFitWidthRecursion( lines: string[] = [], newLine = '' ): string[] { + // eslint-disable-next-line no-console + console.error({ words, lines, newLine }); // Return if there is nothing left to split if (words.length === 0) { // If there is a new line, add it to the lines From 5903792207f651cbb0a2ad6726b0ffe6039dc532 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 13 Jun 2023 10:34:24 +0530 Subject: [PATCH 07/21] rename handle-markdown-text --- .../{handle-markdown-text.js => handle-markdown-text.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/mermaid/src/rendering-util/{handle-markdown-text.js => handle-markdown-text.ts} (100%) diff --git a/packages/mermaid/src/rendering-util/handle-markdown-text.js b/packages/mermaid/src/rendering-util/handle-markdown-text.ts similarity index 100% rename from packages/mermaid/src/rendering-util/handle-markdown-text.js rename to packages/mermaid/src/rendering-util/handle-markdown-text.ts From dd4e14690d0b239616482b0c39618824bddc2ea0 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 13 Jun 2023 11:26:25 +0530 Subject: [PATCH 08/21] Add types --- .../rendering-util/handle-markdown-text.ts | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/mermaid/src/rendering-util/handle-markdown-text.ts b/packages/mermaid/src/rendering-util/handle-markdown-text.ts index 5102429d3..04dbe5b76 100644 --- a/packages/mermaid/src/rendering-util/handle-markdown-text.ts +++ b/packages/mermaid/src/rendering-util/handle-markdown-text.ts @@ -1,11 +1,12 @@ +import type { Content } from 'mdast'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { dedent } from 'ts-dedent'; /** - * @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 @@ -14,19 +15,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) { const preprocessedMarkdown = preprocessMarkdown(markdown); const { children } = fromMarkdown(preprocessedMarkdown); - const lines = [[]]; + const lines: { content: string; type: string }[][] = [[]]; let currentLine = 0; - /** - * @param {import('mdast').Content} node - * @param {string} [parentType] - */ - function processNode(node, parentType = 'normal') { + function processNode(node: Content, parentType = 'normal') { if (node.type === 'text') { const textLines = node.value.split('\n'); textLines.forEach((textLine, index) => { @@ -58,17 +55,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, '
'); } else if (node.type === 'strong') { From b36a0177dbb0ecc0b703613f30ce4ab7bad6aeb7 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 13 Jun 2023 11:42:39 +0530 Subject: [PATCH 09/21] Use joiner to split unicode --- .../src/rendering-util/splitText.spec.ts | 29 +++++++++++++++---- .../mermaid/src/rendering-util/splitText.ts | 29 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 77bd6102c..3dafb80ee 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -13,26 +13,43 @@ describe('splitText', () => { }); describe('split lines', () => { + const createCheckFn = (width: number): CheckFitFunction => { + return (text: string) => { + return splitTextToChars(text).length <= width; + }; + }; + it.each([ // empty string - { str: '', width: 1, split: [''] }, - // Width >= Individual words - { str: 'hello world', width: 5, split: ['hello', 'world'] }, { str: 'hello world', width: 7, split: ['hello', 'world'] }, // width > full line { str: 'hello world', width: 20, split: ['hello world'] }, // width < individual word { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, + { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, + { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 2, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 3, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 1, split: ['δΈ­', 'ζ–‡', 'δΈ­'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 2, split: ['δΈ­ζ–‡', 'δΈ­'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 3, split: ['δΈ­ζ–‡δΈ­'] }, { str: 'Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ', width: 6, split: ['Flag πŸ³οΈβ€βš§οΈ', 'this πŸ³οΈβ€πŸŒˆ'] }, ])( 'should split $str into lines of $width characters', ({ str, split, width }: { str: string; width: number; split: string[] }) => { - const checkFn: CheckFitFunction = (text: string) => { - return splitTextToChars(text).length <= width; - }; + const checkFn = createCheckFn(width); expect(splitLineToFitWidth(str, checkFn)).toEqual(split); } ); + + it('should handle strings with newlines', () => { + const checkFn: CheckFitFunction = createCheckFn(6); + const str = `Flag + πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ`; + expect(() => splitLineToFitWidth(str, checkFn)).toThrowErrorMatchingInlineSnapshot( + '"splitLineToFitWidth does not support newlines in the line"' + ); + }); }); diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index 1cd16e35b..c1d25ea13 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -15,11 +15,17 @@ export function splitTextToChars(text: string): string[] { */ function splitLineToWords(text: string): string[] { if (Intl.Segmenter) { - return [...new Intl.Segmenter(undefined, { granularity: 'word' }).segment(text)] - .map((s) => s.segment) - .filter((word) => word !== ' '); + return [...new Intl.Segmenter(undefined, { granularity: 'word' }).segment(text)].map( + (s) => s.segment + ); } - return text.split(' '); + // Split by ' ' removes the ' 's from the result. + const words = text.split(' '); + // Add the ' 's back to the result. + const wordsWithSpaces = words.flatMap((s) => [s, ' ']); + // Remove last space. + wordsWithSpaces.pop(); + return wordsWithSpaces; } /** @@ -55,7 +61,11 @@ function splitWordToFitWidthRecursion( } export function splitLineToFitWidth(line: string, checkFit: CheckFitFunction): string[] { - return splitLineToFitWidthRecursion(splitLineToWords(line), checkFit); + if (line.includes('\n')) { + throw new Error('splitLineToFitWidth does not support newlines in the line'); + } + const words = splitLineToWords(line); + return splitLineToFitWidthRecursion(words, checkFit); } function splitLineToFitWidthRecursion( @@ -74,8 +84,15 @@ function splitLineToFitWidthRecursion( } return lines.length > 0 ? lines : ['']; } + let joiner = ''; + if (words[0] === ' ') { + joiner = ' '; + words.shift(); + } const nextWord = words.shift() ?? ' '; - const lineWithNextWord = newLine ? `${newLine} ${nextWord}` : nextWord; + + const nextWordWithJoiner = joiner + nextWord; + const lineWithNextWord = newLine ? `${newLine}${nextWordWithJoiner}` : nextWordWithJoiner; if (checkFit(lineWithNextWord)) { // nextWord fits, so we can add it to the new line and continue return splitLineToFitWidthRecursion(words, checkFit, lines, lineWithNextWord); From f5484636aae16327f84042505c6355d962727f25 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 6 Jul 2023 16:12:09 +0530 Subject: [PATCH 10/21] createText to TS --- .../mermaid/src/rendering-util/{createText.js => createText.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/mermaid/src/rendering-util/{createText.js => createText.ts} (100%) diff --git a/packages/mermaid/src/rendering-util/createText.js b/packages/mermaid/src/rendering-util/createText.ts similarity index 100% rename from packages/mermaid/src/rendering-util/createText.js rename to packages/mermaid/src/rendering-util/createText.ts From 60a93f7377a468547a316a3049ebcd88f8839c6a Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 6 Jul 2023 20:34:17 +0530 Subject: [PATCH 11/21] Handle proper formatting for markdown strings --- .../mermaid/src/rendering-util/createText.ts | 103 ++++++------------ .../handle-markdown-text.spec.ts | 21 +++- .../rendering-util/handle-markdown-text.ts | 7 +- .../src/rendering-util/splitText.spec.ts | 46 +++++++- .../mermaid/src/rendering-util/splitText.ts | 72 +++++++----- .../mermaid/src/rendering-util/types.d.ts | 7 ++ 6 files changed, 145 insertions(+), 111 deletions(-) create mode 100644 packages/mermaid/src/rendering-util/types.d.ts diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 06fba94c7..4afe2f7f2 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -1,25 +1,17 @@ +/* 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'; import { splitLineToFitWidth } from './splitText.js'; -/** - * @param dom - * @param styleFn - */ +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'); @@ -65,12 +57,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') @@ -79,55 +71,41 @@ 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; } -/** - * 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 - */ -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'); 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(' '); - const checkWidth = (str) => computeWidthOfText(labelGroup, lineHeight, str) <= width; - const linesUnderWidth = checkWidth(fullStr) - ? [fullStr] - : splitLineToFitWidth(fullStr, checkWidth); + 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; @@ -143,44 +121,25 @@ 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. - */ -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 = ['"', "'", '.', ',', ':', ';', '!', '?', '(', ')', '[', ']', '{', '}']; + // 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 = ( @@ -210,7 +169,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); diff --git a/packages/mermaid/src/rendering-util/handle-markdown-text.spec.ts b/packages/mermaid/src/rendering-util/handle-markdown-text.spec.ts index 8ae519cfa..3ca7a3d7a 100644 --- a/packages/mermaid/src/rendering-util/handle-markdown-text.spec.ts +++ b/packages/mermaid/src/rendering-util/handle-markdown-text.spec.ts @@ -152,9 +152,8 @@ 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' }, @@ -162,9 +161,21 @@ it('markdownToLines - Mixed formatting', () => { { 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', () => { diff --git a/packages/mermaid/src/rendering-util/handle-markdown-text.ts b/packages/mermaid/src/rendering-util/handle-markdown-text.ts index 04dbe5b76..ae76faf8a 100644 --- a/packages/mermaid/src/rendering-util/handle-markdown-text.ts +++ b/packages/mermaid/src/rendering-util/handle-markdown-text.ts @@ -1,6 +1,7 @@ import type { Content } from 'mdast'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { dedent } from 'ts-dedent'; +import { MarkdownLine, MarkdownWordType } from './types.js'; /** * @param markdown - markdown to process @@ -17,13 +18,13 @@ function preprocessMarkdown(markdown: string): string { /** * @param markdown - markdown to split into lines */ -export function markdownToLines(markdown: string) { +export function markdownToLines(markdown: string): MarkdownLine[] { const preprocessedMarkdown = preprocessMarkdown(markdown); const { children } = fromMarkdown(preprocessedMarkdown); - const lines: { content: string; type: string }[][] = [[]]; + const lines: MarkdownLine[] = [[]]; let currentLine = 0; - function processNode(node: Content, parentType = 'normal') { + function processNode(node: Content, parentType: MarkdownWordType = 'normal') { if (node.type === 'text') { const textLines = node.value.split('\n'); textLines.forEach((textLine, index) => { diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 3dafb80ee..a09d683d3 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -1,5 +1,6 @@ -import { splitTextToChars, splitLineToFitWidth, type CheckFitFunction } from './splitText.js'; +import { splitTextToChars, splitLineToFitWidth, splitLineToWords } from './splitText.js'; import { describe, it, expect } from 'vitest'; +import type { CheckFitFunction, MarkdownLine, MarkdownWordType } from './types.js'; describe('splitText', () => { it.each([ @@ -13,12 +14,35 @@ describe('splitText', () => { }); describe('split lines', () => { + /** + * Creates a checkFunction for a given width + * @param width - width of characters to fit in a line + * @returns checkFunction + */ const createCheckFn = (width: number): CheckFitFunction => { - return (text: string) => { - return splitTextToChars(text).length <= width; + return (text: MarkdownLine) => { + // Join all words into a single string + const joinedContent = text.map((w) => w.content).join(''); + const characters = splitTextToChars(joinedContent); + return characters.length <= width; }; }; + it('should create valid checkFit function', () => { + const checkFit5 = createCheckFn(5); + expect(checkFit5([{ content: 'hello', type: 'normal' }])).toBe(true); + expect( + checkFit5([ + { content: 'hello', type: 'normal' }, + { content: 'world', type: 'normal' }, + ]) + ).toBe(false); + const checkFit1 = createCheckFn(1); + expect(checkFit1([{ content: 'A', type: 'normal' }])).toBe(true); + expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(true); + expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(false); + }); + it.each([ // empty string { str: 'hello world', width: 7, split: ['hello', 'world'] }, @@ -40,7 +64,10 @@ describe('split lines', () => { 'should split $str into lines of $width characters', ({ str, split, width }: { str: string; width: number; split: string[] }) => { const checkFn = createCheckFn(width); - expect(splitLineToFitWidth(str, checkFn)).toEqual(split); + const line: MarkdownLine = getLineFromString(str); + expect(splitLineToFitWidth(line, checkFn)).toEqual( + split.map((str) => splitLineToWords(str).map((content) => ({ content, type: 'normal' }))) + ); } ); @@ -48,8 +75,17 @@ describe('split lines', () => { const checkFn: CheckFitFunction = createCheckFn(6); const str = `Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ`; - expect(() => splitLineToFitWidth(str, checkFn)).toThrowErrorMatchingInlineSnapshot( + expect(() => + splitLineToFitWidth(getLineFromString(str), checkFn) + ).toThrowErrorMatchingInlineSnapshot( '"splitLineToFitWidth does not support newlines in the line"' ); }); }); + +const getLineFromString = (str: string, type: MarkdownWordType = 'normal'): MarkdownLine => { + return splitLineToWords(str).map((content) => ({ + content, + type, + })); +}; diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index c1d25ea13..f32f3aacf 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -1,4 +1,4 @@ -export type CheckFitFunction = (text: string) => boolean; +import type { CheckFitFunction, MarkdownLine, MarkdownWord, MarkdownWordType } from './types.js'; /** * Splits a string into graphemes if available, otherwise characters. @@ -13,7 +13,7 @@ export function splitTextToChars(text: string): string[] { /** * Splits a string into words. */ -function splitLineToWords(text: string): string[] { +export function splitLineToWords(text: string): string[] { if (Intl.Segmenter) { return [...new Intl.Segmenter(undefined, { granularity: 'word' }).segment(text)].map( (s) => s.segment @@ -34,46 +34,61 @@ function splitLineToWords(text: string): string[] { * @param word - Word to split * @returns [first part of word that fits, rest of word] */ -export function splitWordToFitWidth(checkFit: CheckFitFunction, word: string): [string, string] { - const characters = splitTextToChars(word); +export function splitWordToFitWidth( + checkFit: CheckFitFunction, + word: MarkdownWord +): [MarkdownWord, MarkdownWord] { + const characters = splitTextToChars(word.content); if (characters.length === 0) { - return ['', '']; + return [ + { content: '', type: word.type }, + { content: '', type: word.type }, + ]; } - return splitWordToFitWidthRecursion(checkFit, [], characters); + return splitWordToFitWidthRecursion(checkFit, [], characters, word.type); } function splitWordToFitWidthRecursion( checkFit: CheckFitFunction, usedChars: string[], - remainingChars: string[] -): [string, string] { + remainingChars: string[], + type: MarkdownWordType +): [MarkdownWord, MarkdownWord] { // eslint-disable-next-line no-console console.error({ usedChars, remainingChars }); if (remainingChars.length === 0) { - return [usedChars.join(''), '']; + return [ + { content: usedChars.join(''), type }, + { content: '', type }, + ]; } const [nextChar, ...rest] = remainingChars; const newWord = [...usedChars, nextChar]; - if (checkFit(newWord.join(''))) { - return splitWordToFitWidthRecursion(checkFit, newWord, rest); + if (checkFit([{ content: newWord.join(''), type }])) { + return splitWordToFitWidthRecursion(checkFit, newWord, rest, type); } - return [usedChars.join(''), remainingChars.join('')]; + return [ + { content: usedChars.join(''), type }, + { content: remainingChars.join(''), type }, + ]; } -export function splitLineToFitWidth(line: string, checkFit: CheckFitFunction): string[] { - if (line.includes('\n')) { +export function splitLineToFitWidth( + line: MarkdownLine, + checkFit: CheckFitFunction +): MarkdownLine[] { + if (line.some(({ content }) => content.includes('\n'))) { throw new Error('splitLineToFitWidth does not support newlines in the line'); } - const words = splitLineToWords(line); - return splitLineToFitWidthRecursion(words, checkFit); + return splitLineToFitWidthRecursion(line, checkFit); } function splitLineToFitWidthRecursion( - words: string[], + words: MarkdownWord[], checkFit: CheckFitFunction, - lines: string[] = [], - newLine = '' -): string[] { + lines: MarkdownLine[] = [], + newLine: MarkdownLine = [] +): MarkdownLine[] { // eslint-disable-next-line no-console console.error({ words, lines, newLine }); // Return if there is nothing left to split @@ -82,17 +97,22 @@ function splitLineToFitWidthRecursion( if (newLine.length > 0) { lines.push(newLine); } - return lines.length > 0 ? lines : ['']; + return lines.length > 0 ? lines : []; } let joiner = ''; - if (words[0] === ' ') { + if (words[0].content === ' ') { joiner = ' '; words.shift(); } - const nextWord = words.shift() ?? ' '; + const nextWord: MarkdownWord = words.shift() ?? { content: ' ', type: 'normal' }; + + // const nextWordWithJoiner: MarkdownWord = { ...nextWord, content: joiner + nextWord.content }; + const lineWithNextWord: MarkdownLine = [...newLine]; + if (joiner !== '') { + lineWithNextWord.push({ content: joiner, type: 'normal' }); + } + lineWithNextWord.push(nextWord); - const nextWordWithJoiner = joiner + nextWord; - const lineWithNextWord = newLine ? `${newLine}${nextWordWithJoiner}` : nextWordWithJoiner; if (checkFit(lineWithNextWord)) { // nextWord fits, so we can add it to the new line and continue return splitLineToFitWidthRecursion(words, checkFit, lines, lineWithNextWord); @@ -106,7 +126,7 @@ function splitLineToFitWidthRecursion( } else { // There was no text in newLine, so we need to split nextWord const [line, rest] = splitWordToFitWidth(checkFit, nextWord); - lines.push(line); + lines.push([line]); words.unshift(rest); } return splitLineToFitWidthRecursion(words, checkFit, lines); diff --git a/packages/mermaid/src/rendering-util/types.d.ts b/packages/mermaid/src/rendering-util/types.d.ts new file mode 100644 index 000000000..aec99e636 --- /dev/null +++ b/packages/mermaid/src/rendering-util/types.d.ts @@ -0,0 +1,7 @@ +export type MarkdownWordType = 'normal' | 'strong' | 'emphasis'; +export interface MarkdownWord { + content: string; + type: MarkdownWordType; +} +export type MarkdownLine = MarkdownWord[]; +export type CheckFitFunction = (text: MarkdownLine) => boolean; From 5ac70bbc00c4bead77a369772b6808d72b46b734 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 6 Jul 2023 21:22:28 +0530 Subject: [PATCH 12/21] Fix flowchart failure --- packages/mermaid/src/rendering-util/splitText.spec.ts | 2 ++ packages/mermaid/src/rendering-util/splitText.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index a09d683d3..95512edfb 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -53,6 +53,8 @@ describe('split lines', () => { { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, + // width = 0, impossible, so split into individual characters + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 0, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 2, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 3, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index f32f3aacf..64a6cebbe 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -67,6 +67,11 @@ function splitWordToFitWidthRecursion( if (checkFit([{ content: newWord.join(''), type }])) { return splitWordToFitWidthRecursion(checkFit, newWord, rest, type); } + if (usedChars.length === 0 && nextChar) { + // If the first character does not fit, split it anyway + usedChars.push(nextChar); + remainingChars.shift(); + } return [ { content: usedChars.join(''), type }, { content: remainingChars.join(''), type }, @@ -127,7 +132,9 @@ function splitLineToFitWidthRecursion( // There was no text in newLine, so we need to split nextWord const [line, rest] = splitWordToFitWidth(checkFit, nextWord); lines.push([line]); - words.unshift(rest); + if (rest.content) { + words.unshift(rest); + } } return splitLineToFitWidthRecursion(words, checkFit, lines); } From 7d996c3d336932f1020af0c1d94f9c4a17d6376d Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 6 Jul 2023 21:48:18 +0530 Subject: [PATCH 13/21] Cleanup --- packages/mermaid/src/rendering-util/splitText.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index 64a6cebbe..8b31c4ce6 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -39,12 +39,6 @@ export function splitWordToFitWidth( word: MarkdownWord ): [MarkdownWord, MarkdownWord] { const characters = splitTextToChars(word.content); - if (characters.length === 0) { - return [ - { content: '', type: word.type }, - { content: '', type: word.type }, - ]; - } return splitWordToFitWidthRecursion(checkFit, [], characters, word.type); } @@ -54,8 +48,6 @@ function splitWordToFitWidthRecursion( remainingChars: string[], type: MarkdownWordType ): [MarkdownWord, MarkdownWord] { - // eslint-disable-next-line no-console - console.error({ usedChars, remainingChars }); if (remainingChars.length === 0) { return [ { content: usedChars.join(''), type }, @@ -94,8 +86,6 @@ function splitLineToFitWidthRecursion( lines: MarkdownLine[] = [], newLine: MarkdownLine = [] ): MarkdownLine[] { - // eslint-disable-next-line no-console - console.error({ words, lines, newLine }); // Return if there is nothing left to split if (words.length === 0) { // If there is a new line, add it to the lines @@ -110,8 +100,6 @@ function splitLineToFitWidthRecursion( words.shift(); } const nextWord: MarkdownWord = words.shift() ?? { content: ' ', type: 'normal' }; - - // const nextWordWithJoiner: MarkdownWord = { ...nextWord, content: joiner + nextWord.content }; const lineWithNextWord: MarkdownLine = [...newLine]; if (joiner !== '') { lineWithNextWord.push({ content: joiner, type: 'normal' }); @@ -128,7 +116,7 @@ function splitLineToFitWidthRecursion( // There was text in newLine, so add it to lines and push nextWord back into words. lines.push(newLine); words.unshift(nextWord); - } else { + } else if (nextWord.content) { // There was no text in newLine, so we need to split nextWord const [line, rest] = splitWordToFitWidth(checkFit, nextWord); lines.push([line]); From d58c41dbc082c36ab38d914e4d4e5702adf7c785 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 7 Jul 2023 10:02:04 +0530 Subject: [PATCH 14/21] Add tests without Intl.Segmenter --- .../mermaid/src/rendering-util/createText.ts | 5 - .../src/rendering-util/splitText.spec.ts | 154 ++++++++++++------ .../mermaid/src/rendering-util/splitText.ts | 2 +- 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 4afe2f7f2..5f086e986 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -14,11 +14,7 @@ function applyStyle(dom, styleFn) { 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'; @@ -130,7 +126,6 @@ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) { .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 { diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 95512edfb..00db27ea2 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -1,46 +1,85 @@ import { splitTextToChars, splitLineToFitWidth, splitLineToWords } from './splitText.js'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import type { CheckFitFunction, MarkdownLine, MarkdownWordType } from './types.js'; -describe('splitText', () => { - it.each([ - { str: '', split: [] }, - { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, - { str: 'ok', split: ['o', 'k'] }, - { str: 'abc', split: ['a', 'b', 'c'] }, - ])('should split $str into graphemes', ({ str, split }: { str: string; split: string[] }) => { - expect(splitTextToChars(str)).toEqual(split); +describe('when Intl.Segmenter is available', () => { + describe('splitText', () => { + it.each([ + { str: '', split: [] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'ok', split: ['o', 'k'] }, + { str: 'abc', split: ['a', 'b', 'c'] }, + ])('should split $str into graphemes', ({ str, split }: { str: string; split: string[] }) => { + expect(splitTextToChars(str)).toEqual(split); + }); + }); + + describe('split lines', () => { + it('should create valid checkFit function', () => { + const checkFit5 = createCheckFn(5); + expect(checkFit5([{ content: 'hello', type: 'normal' }])).toBe(true); + expect( + checkFit5([ + { content: 'hello', type: 'normal' }, + { content: 'world', type: 'normal' }, + ]) + ).toBe(false); + const checkFit1 = createCheckFn(1); + expect(checkFit1([{ content: 'A', type: 'normal' }])).toBe(true); + expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(true); + expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(false); + }); + + it.each([ + // empty string + { str: 'hello world', width: 7, split: ['hello', 'world'] }, + // width > full line + { str: 'hello world', width: 20, split: ['hello world'] }, + // width < individual word + { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, + { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, + { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, + { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, + // width = 0, impossible, so split into individual characters + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 0, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 2, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 3, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 1, split: ['δΈ­', 'ζ–‡', 'δΈ­'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 2, split: ['δΈ­ζ–‡', 'δΈ­'] }, + { str: 'δΈ­ζ–‡δΈ­', width: 3, split: ['δΈ­ζ–‡δΈ­'] }, + { str: 'Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ', width: 6, split: ['Flag πŸ³οΈβ€βš§οΈ', 'this πŸ³οΈβ€πŸŒˆ'] }, + ])( + 'should split $str into lines of $width characters', + ({ str, split, width }: { str: string; width: number; split: string[] }) => { + const checkFn = createCheckFn(width); + const line: MarkdownLine = getLineFromString(str); + expect(splitLineToFitWidth(line, checkFn)).toEqual( + split.map((str) => getLineFromString(str)) + ); + } + ); }); }); -describe('split lines', () => { - /** - * Creates a checkFunction for a given width - * @param width - width of characters to fit in a line - * @returns checkFunction - */ - const createCheckFn = (width: number): CheckFitFunction => { - return (text: MarkdownLine) => { - // Join all words into a single string - const joinedContent = text.map((w) => w.content).join(''); - const characters = splitTextToChars(joinedContent); - return characters.length <= width; - }; - }; +describe('when Intl.segmenter is not available', () => { + beforeAll(() => { + vi.stubGlobal('Intl', { Segmenter: undefined }); + }); + afterAll(() => { + vi.unstubAllGlobals(); + }); - it('should create valid checkFit function', () => { - const checkFit5 = createCheckFn(5); - expect(checkFit5([{ content: 'hello', type: 'normal' }])).toBe(true); - expect( - checkFit5([ - { content: 'hello', type: 'normal' }, - { content: 'world', type: 'normal' }, - ]) - ).toBe(false); - const checkFit1 = createCheckFn(1); - expect(checkFit1([{ content: 'A', type: 'normal' }])).toBe(true); - expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(true); - expect(checkFit1([{ content: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€βš§οΈ', type: 'normal' }])).toBe(false); + it.each([ + { str: '', split: [] }, + { + str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', + split: [...'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'], + }, + { str: 'ok', split: ['o', 'k'] }, + { str: 'abc', split: ['a', 'b', 'c'] }, + ])('should split $str into characters', ({ str, split }: { str: string; split: string[] }) => { + expect(splitTextToChars(str)).toEqual(split); }); it.each([ @@ -52,37 +91,34 @@ describe('split lines', () => { { str: 'hello world', width: 3, split: ['hel', 'lo', 'wor', 'ld'] }, { str: 'hello 12 world', width: 4, split: ['hell', 'o 12', 'worl', 'd'] }, { str: 'hello 1 2 world', width: 4, split: ['hell', 'o 1', '2', 'worl', 'd'] }, - { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, + { str: 'hello 1 2 world', width: 6, split: ['hello', ' 1 2', 'world'] }, // width = 0, impossible, so split into individual characters - { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 0, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, - { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: ['πŸ³οΈβ€βš§οΈ', 'πŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, - { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 2, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆ', 'πŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, - { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 3, split: ['πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, + { str: 'abc', width: 0, split: ['a', 'b', 'c'] }, + { str: 'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»', width: 1, split: [...'πŸ³οΈβ€βš§οΈπŸ³οΈβ€πŸŒˆπŸ‘©πŸΎβ€β€οΈβ€πŸ‘¨πŸ»'] }, { str: 'δΈ­ζ–‡δΈ­', width: 1, split: ['δΈ­', 'ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 2, split: ['δΈ­ζ–‡', 'δΈ­'] }, { str: 'δΈ­ζ–‡δΈ­', width: 3, split: ['δΈ­ζ–‡δΈ­'] }, - { str: 'Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ', width: 6, split: ['Flag πŸ³οΈβ€βš§οΈ', 'this πŸ³οΈβ€πŸŒˆ'] }, ])( 'should split $str into lines of $width characters', ({ str, split, width }: { str: string; width: number; split: string[] }) => { const checkFn = createCheckFn(width); const line: MarkdownLine = getLineFromString(str); expect(splitLineToFitWidth(line, checkFn)).toEqual( - split.map((str) => splitLineToWords(str).map((content) => ({ content, type: 'normal' }))) + split.map((str) => getLineFromString(str)) ); } ); +}); - it('should handle strings with newlines', () => { - const checkFn: CheckFitFunction = createCheckFn(6); - const str = `Flag +it('should handle strings with newlines', () => { + const checkFn: CheckFitFunction = createCheckFn(6); + const str = `Flag πŸ³οΈβ€βš§οΈ this πŸ³οΈβ€πŸŒˆ`; - expect(() => - splitLineToFitWidth(getLineFromString(str), checkFn) - ).toThrowErrorMatchingInlineSnapshot( - '"splitLineToFitWidth does not support newlines in the line"' - ); - }); + expect(() => + splitLineToFitWidth(getLineFromString(str), checkFn) + ).toThrowErrorMatchingInlineSnapshot( + '"splitLineToFitWidth does not support newlines in the line"' + ); }); const getLineFromString = (str: string, type: MarkdownWordType = 'normal'): MarkdownLine => { @@ -91,3 +127,17 @@ const getLineFromString = (str: string, type: MarkdownWordType = 'normal'): Mark type, })); }; + +/** + * Creates a checkFunction for a given width + * @param width - width of characters to fit in a line + * @returns checkFunction + */ +const createCheckFn = (width: number): CheckFitFunction => { + return (text: MarkdownLine) => { + // Join all words into a single string + const joinedContent = text.map((w) => w.content).join(''); + const characters = splitTextToChars(joinedContent); + return characters.length <= width; + }; +}; diff --git a/packages/mermaid/src/rendering-util/splitText.ts b/packages/mermaid/src/rendering-util/splitText.ts index 8b31c4ce6..103e8ca67 100644 --- a/packages/mermaid/src/rendering-util/splitText.ts +++ b/packages/mermaid/src/rendering-util/splitText.ts @@ -22,7 +22,7 @@ export function splitLineToWords(text: string): string[] { // Split by ' ' removes the ' 's from the result. const words = text.split(' '); // Add the ' 's back to the result. - const wordsWithSpaces = words.flatMap((s) => [s, ' ']); + const wordsWithSpaces = words.flatMap((s) => [s, ' ']).filter((s) => s); // Remove last space. wordsWithSpaces.pop(); return wordsWithSpaces; From 28406fc9c4ff0a8426d4f08d0395630b39955aee Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Fri, 7 Jul 2023 10:05:05 +0530 Subject: [PATCH 15/21] Add comments --- .../mermaid/src/rendering-util/createText.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index 5f086e986..2705a37ac 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -76,6 +76,15 @@ function computeWidthOfText(parentNode: any, lineHeight: number, line: MarkdownL return textLength; } +/** + * Creates a formatted text element by breaking lines and applying styles based on + * the given structuredText. + * + * @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: number, g: any, @@ -117,6 +126,13 @@ function createFormattedText( } } +/** + * Updates the text content and styles of the given tspan element based on the + * provided wrappedLine data. + * + * @param tspan - The tspan element to update. + * @param wrappedLine - The line data to apply to the tspan element. + */ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) { tspan.text(''); From eca2efa46da3a3276c73c3e80a7d2f5e9e532475 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Sat, 8 Jul 2023 19:01:45 +0530 Subject: [PATCH 16/21] Update packages/mermaid/src/rendering-util/splitText.spec.ts Co-authored-by: Alois Klink --- packages/mermaid/src/rendering-util/splitText.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 00db27ea2..017fe9c6a 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -62,7 +62,11 @@ describe('when Intl.Segmenter is available', () => { }); }); -describe('when Intl.segmenter is not available', () => { +/** + * Intl.Segmenter is not supported in Firefox yet, + * see https://bugzilla.mozilla.org/show_bug.cgi?id=1423593 + */ +describe('when Intl.Segmenter is not available', () => { beforeAll(() => { vi.stubGlobal('Intl', { Segmenter: undefined }); }); From 68305fea9ecbba269e3fc94e24b2b52e8e0ae402 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Tue, 25 Jul 2023 18:56:58 +0530 Subject: [PATCH 17/21] Fix lint --- packages/mermaid/src/rendering-util/splitText.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/rendering-util/splitText.spec.ts b/packages/mermaid/src/rendering-util/splitText.spec.ts index 017fe9c6a..bc7df08dd 100644 --- a/packages/mermaid/src/rendering-util/splitText.spec.ts +++ b/packages/mermaid/src/rendering-util/splitText.spec.ts @@ -64,7 +64,7 @@ describe('when Intl.Segmenter is available', () => { /** * Intl.Segmenter is not supported in Firefox yet, - * see https://bugzilla.mozilla.org/show_bug.cgi?id=1423593 + * see https://bugzilla.mozilla.org/show_bug.cgi?id=1423593 */ describe('when Intl.Segmenter is not available', () => { beforeAll(() => { From 4ea1227e29a321eec0f8d1557b181fe756b3ddcc Mon Sep 17 00:00:00 2001 From: sidharthv96 Date: Tue, 25 Jul 2023 13:33:37 +0000 Subject: [PATCH 18/21] Update docs --- docs/config/usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/usage.md b/docs/config/usage.md index 4203e3a13..7246fe318 100644 --- a/docs/config/usage.md +++ b/docs/config/usage.md @@ -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