packages/mermaid/src/rendering-util/splitText.spec.ts
import { splitTextToChars, splitLineToFitWidth, splitLineToWords } from './splitText.js';
import { describe, it, expect, vi } from 'vitest';
import type { CheckFitFunction, MarkdownLine, MarkdownWordType } from './types.js';
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))
);
}
);
});
});
/**
* 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 });
});
afterAll(() => {
vi.unstubAllGlobals();
});
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([
// 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: 'abc', width: 0, split: ['a', 'b', 'c'] },
{ str: 'π³οΈββ§οΈπ³οΈβππ©πΎββ€οΈβπ¨π»', width: 1, split: [...'π³οΈββ§οΈπ³οΈβππ©πΎββ€οΈβπ¨π»'] },
{ str: 'δΈζδΈ', width: 1, split: ['δΈ', 'ζ', 'δΈ'] },
{ str: 'δΈζδΈ', width: 2, split: ['δΈζ', 'δΈ'] },
{ str: 'δΈζδΈ', width: 3, split: ['δΈζδΈ'] },
])(
'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))
);
}
);
});
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"'
);
});
const getLineFromString = (str: string, type: MarkdownWordType = 'normal'): MarkdownLine => {
return splitLineToWords(str).map((content) => ({
content,
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;
};
};
// cspell:ignore worl