knsv/mermaid

View on GitHub
packages/mermaid/src/rendering-util/splitText.spec.ts

Summary

Maintainability
C
1 day
Test Coverage
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