|
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
|
|
import { getLineHeight } from "../fonts";
|
|
|
import { API } from "../tests/helpers/api";
|
|
|
import {
|
|
|
computeContainerDimensionForBoundText,
|
|
|
getContainerCoords,
|
|
|
getBoundTextMaxWidth,
|
|
|
getBoundTextMaxHeight,
|
|
|
wrapText,
|
|
|
detectLineHeight,
|
|
|
getLineHeightInPx,
|
|
|
parseTokens,
|
|
|
} from "./textElement";
|
|
|
import type { ExcalidrawTextElementWithContainer, FontString } from "./types";
|
|
|
|
|
|
describe("Test wrapText", () => {
|
|
|
// font is irrelevant as jsdom does not support FontFace API
|
|
|
// `measureText` width is mocked to return `text.length` by `jest-canvas-mock`
|
|
|
// https://github.com/hustcc/jest-canvas-mock/blob/master/src/classes/TextMetrics.js
|
|
|
const font = "10px Cascadia, Segoe UI Emoji" as FontString;
|
|
|
|
|
|
it("should wrap the text correctly when word length is exactly equal to max width", () => {
|
|
|
const text = "Hello Excalidraw";
|
|
|
// Length of "Excalidraw" is 100 and exacty equal to max width
|
|
|
const res = wrapText(text, font, 100);
|
|
|
expect(res).toEqual(`Hello\nExcalidraw`);
|
|
|
});
|
|
|
|
|
|
it("should return the text as is if max width is invalid", () => {
|
|
|
const text = "Hello Excalidraw";
|
|
|
expect(wrapText(text, font, NaN)).toEqual(text);
|
|
|
expect(wrapText(text, font, -1)).toEqual(text);
|
|
|
expect(wrapText(text, font, Infinity)).toEqual(text);
|
|
|
});
|
|
|
|
|
|
it("should show the text correctly when max width reached", () => {
|
|
|
const text = "Hello😀";
|
|
|
const maxWidth = 10;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("H\ne\nl\nl\no\n😀");
|
|
|
});
|
|
|
|
|
|
it("should not wrap number when wrapping line", () => {
|
|
|
const text = "don't wrap this number 99,100.99";
|
|
|
const maxWidth = 300;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("don't wrap this number\n99,100.99");
|
|
|
});
|
|
|
|
|
|
it("should support multiple (multi-codepoint) emojis", () => {
|
|
|
const text = "😀🗺🔥👩🏽🦰👨👩👧👦🇨🇿";
|
|
|
const maxWidth = 1;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("😀\n🗺\n🔥\n👩🏽🦰\n👨👩👧👦\n🇨🇿");
|
|
|
});
|
|
|
|
|
|
it("should wrap the text correctly when text contains hyphen", () => {
|
|
|
let text =
|
|
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
|
|
const res = wrapText(text, font, 110);
|
|
|
expect(res).toBe(
|
|
|
`Wikipedia\nis hosted\nby\nWikimedia-\nFoundation,\na non-\nprofit\norganizatio\nn that also\nhosts a\nrange-of\nother\nprojects`,
|
|
|
);
|
|
|
|
|
|
text = "Hello thereusing-now";
|
|
|
expect(wrapText(text, font, 100)).toEqual("Hello\nthereusing\n-now");
|
|
|
});
|
|
|
|
|
|
it("should support wrapping nested lists", () => {
|
|
|
const text = `\tA) one tab\t\t- two tabs - 8 spaces`;
|
|
|
|
|
|
const maxWidth = 100;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe(`\tA) one\ntab\t\t- two\ntabs\n- 8 spaces`);
|
|
|
|
|
|
const maxWidth2 = 50;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe(`\tA)\none\ntab\n- two\ntabs\n- 8\nspace\ns`);
|
|
|
});
|
|
|
|
|
|
describe("When text is CJK", () => {
|
|
|
it("should break each CJK character when width is very small", () => {
|
|
|
// "안녕하세요" (Hangul) + "こんにちは世界" (Hiragana, Kanji) + "コンニチハ" (Katakana) + "你好" (Han) = "Hello Hello World Hello Hi"
|
|
|
const text = "안녕하세요こんにちは世界コンニチハ你好";
|
|
|
const maxWidth = 10;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe(
|
|
|
"안\n녕\n하\n세\n요\nこ\nん\nに\nち\nは\n世\n界\nコ\nン\nニ\nチ\nハ\n你\n好",
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("should break CJK text into longer segments when width is larger", () => {
|
|
|
// "안녕하세요" (Hangul) + "こんにちは世界" (Hiragana, Kanji) + "コンニチハ" (Katakana) + "你好" (Han) = "Hello Hello World Hello Hi"
|
|
|
const text = "안녕하세요こんにちは世界コンニチハ你好";
|
|
|
const maxWidth = 30;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
|
|
|
// measureText is mocked, so it's not precisely what would happen in prod
|
|
|
expect(res).toBe("안녕하\n세요こ\nんにち\nは世界\nコンニ\nチハ你\n好");
|
|
|
});
|
|
|
|
|
|
it("should handle a combination of CJK, latin, emojis and whitespaces", () => {
|
|
|
const text = `a醫 醫 bb 你好 world-i-😀🗺🔥`;
|
|
|
|
|
|
const maxWidth = 150;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe(`a醫 醫 bb 你\n好 world-i-😀🗺\n🔥`);
|
|
|
|
|
|
const maxWidth2 = 50;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe(`a醫 醫\nbb 你\n好\nworld\n-i-😀\n🗺🔥`);
|
|
|
|
|
|
const maxWidth3 = 30;
|
|
|
const res3 = wrapText(text, font, maxWidth3);
|
|
|
expect(res3).toBe(`a醫\n醫\nbb\n你好\nwor\nld-\ni-\n😀\n🗺\n🔥`);
|
|
|
});
|
|
|
|
|
|
it("should break before and after a regular CJK character", () => {
|
|
|
const text = "HelloたWorld";
|
|
|
const maxWidth1 = 50;
|
|
|
const res1 = wrapText(text, font, maxWidth1);
|
|
|
expect(res1).toBe("Hello\nた\nWorld");
|
|
|
|
|
|
const maxWidth2 = 60;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe("Helloた\nWorld");
|
|
|
});
|
|
|
|
|
|
it("should break before and after certain CJK symbols", () => {
|
|
|
const text = "こんにちは〃世界";
|
|
|
const maxWidth1 = 50;
|
|
|
const res1 = wrapText(text, font, maxWidth1);
|
|
|
expect(res1).toBe("こんにちは\n〃世界");
|
|
|
|
|
|
const maxWidth2 = 60;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe("こんにちは〃\n世界");
|
|
|
});
|
|
|
|
|
|
it("should break after, not before for certain CJK pairs", () => {
|
|
|
const text = "Hello た。";
|
|
|
const maxWidth = 70;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("Hello\nた。");
|
|
|
});
|
|
|
|
|
|
it("should break before, not after for certain CJK pairs", () => {
|
|
|
const text = "Hello「たWorld」";
|
|
|
const maxWidth = 60;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("Hello\n「た\nWorld」");
|
|
|
});
|
|
|
|
|
|
it("should break after, not before for certain CJK character pairs", () => {
|
|
|
const text = "「Helloた」World";
|
|
|
const maxWidth = 70;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("「Hello\nた」World");
|
|
|
});
|
|
|
|
|
|
it("should break Chinese sentences", () => {
|
|
|
const text = `中国你好!这是一个测试。
|
|
|
我们来看看:人民币¥1234「很贵」
|
|
|
(括号)、逗号,句号。空格 换行 全角符号…—`;
|
|
|
|
|
|
const maxWidth1 = 80;
|
|
|
const res1 = wrapText(text, font, maxWidth1);
|
|
|
expect(res1).toBe(`中国你好!这是一\n个测试。
|
|
|
我们来看看:人民\n币¥1234「很\n贵」
|
|
|
(括号)、逗号,\n句号。空格 换行\n全角符号…—`);
|
|
|
|
|
|
const maxWidth2 = 50;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe(`中国你好!\n这是一个测\n试。
|
|
|
我们来看\n看:人民币\n¥1234\n「很贵」
|
|
|
(括号)、\n逗号,句\n号。空格\n换行 全角\n符号…—`);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
it("should break Japanese sentences", () => {
|
|
|
const text = `日本こんにちは!これはテストです。
|
|
|
見てみましょう:円¥1234「高い」
|
|
|
(括弧)、読点、句点。
|
|
|
空白 改行 全角記号…ー`;
|
|
|
|
|
|
const maxWidth1 = 80;
|
|
|
const res1 = wrapText(text, font, maxWidth1);
|
|
|
expect(res1).toBe(`日本こんにちは!\nこれはテストで\nす。
|
|
|
見てみましょ\nう:円¥1234\n「高い」
|
|
|
(括弧)、読\n点、句点。
|
|
|
空白 改行\n全角記号…ー`);
|
|
|
|
|
|
const maxWidth2 = 50;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe(`日本こんに\nちは!これ\nはテストで\nす。
|
|
|
見てみ\nましょう:\n円\n¥1234\n「高い」
|
|
|
(括\n弧)、読\n点、句点。
|
|
|
空白\n改行 全角\n記号…ー`);
|
|
|
});
|
|
|
|
|
|
it("should break Korean sentences", () => {
|
|
|
const text = `한국 안녕하세요! 이것은 테스트입니다.
|
|
|
우리 보자: 원화₩1234「비싸다」
|
|
|
(괄호), 쉼표, 마침표.
|
|
|
공백 줄바꿈 전각기호…—`;
|
|
|
|
|
|
const maxWidth1 = 80;
|
|
|
const res1 = wrapText(text, font, maxWidth1);
|
|
|
expect(res1).toBe(`한국 안녕하세\n요! 이것은 테\n스트입니다.
|
|
|
우리 보자: 원\n화₩1234「비\n싸다」
|
|
|
(괄호), 쉼\n표, 마침표.
|
|
|
공백 줄바꿈 전\n각기호…—`);
|
|
|
|
|
|
const maxWidth2 = 60;
|
|
|
const res2 = wrapText(text, font, maxWidth2);
|
|
|
expect(res2).toBe(`한국 안녕하\n세요! 이것\n은 테스트입\n니다.
|
|
|
우리 보자:\n원화\n₩1234\n「비싸다」
|
|
|
(괄호),\n쉼표, 마침\n표.
|
|
|
공백 줄바꿈\n전각기호…—`);
|
|
|
});
|
|
|
|
|
|
describe("When text contains leading whitespaces", () => {
|
|
|
const text = " \t Hello world";
|
|
|
|
|
|
it("should preserve leading whitespaces", () => {
|
|
|
const maxWidth = 120;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe(" \t Hello\nworld");
|
|
|
});
|
|
|
|
|
|
it("should break and collapse leading whitespaces when line breaks", () => {
|
|
|
const maxWidth = 60;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("\nHello\nworld");
|
|
|
});
|
|
|
|
|
|
it("should break and collapse leading whitespaces whe words break", () => {
|
|
|
const maxWidth = 30;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("\nHel\nlo\nwor\nld");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("When text contains trailing whitespaces", () => {
|
|
|
it("shouldn't add new lines for trailing spaces", () => {
|
|
|
const text = "Hello whats up ";
|
|
|
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe(text);
|
|
|
});
|
|
|
|
|
|
it("should ignore trailing whitespaces when line breaks", () => {
|
|
|
const text = "Hippopotomonstrosesquippedaliophobia ??????";
|
|
|
const maxWidth = 400;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("Hippopotomonstrosesquippedaliophobia\n??????");
|
|
|
});
|
|
|
|
|
|
it("should not ignore trailing whitespaces when word breaks", () => {
|
|
|
const text = "Hippopotomonstrosesquippedaliophobia ??????";
|
|
|
const maxWidth = 300;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("Hippopotomonstrosesquippedalio\nphobia ??????");
|
|
|
});
|
|
|
|
|
|
it("should ignore trailing whitespaces when word breaks and line breaks", () => {
|
|
|
const text = "Hippopotomonstrosesquippedaliophobia ??????";
|
|
|
const maxWidth = 180;
|
|
|
const res = wrapText(text, font, maxWidth);
|
|
|
expect(res).toBe("Hippopotomonstrose\nsquippedaliophobia\n??????");
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("When text doesn't contain new lines", () => {
|
|
|
const text = "Hello whats up";
|
|
|
|
|
|
[
|
|
|
{
|
|
|
desc: "break all words when width of each word is less than container width",
|
|
|
width: 80,
|
|
|
res: `Hello\nwhats\nup`,
|
|
|
},
|
|
|
{
|
|
|
desc: "break all characters when width of each character is less than container width",
|
|
|
width: 25,
|
|
|
res: `H
|
|
|
e
|
|
|
l
|
|
|
l
|
|
|
o
|
|
|
w
|
|
|
h
|
|
|
a
|
|
|
t
|
|
|
s
|
|
|
u
|
|
|
p`,
|
|
|
},
|
|
|
{
|
|
|
desc: "break words as per the width",
|
|
|
|
|
|
width: 140,
|
|
|
res: `Hello whats\nup`,
|
|
|
},
|
|
|
{
|
|
|
desc: "fit the container",
|
|
|
|
|
|
width: 250,
|
|
|
res: "Hello whats up",
|
|
|
},
|
|
|
{
|
|
|
desc: "should push the word if its equal to max width",
|
|
|
width: 60,
|
|
|
res: `Hello
|
|
|
whats
|
|
|
up`,
|
|
|
},
|
|
|
].forEach((data) => {
|
|
|
it(`should ${data.desc}`, () => {
|
|
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
|
expect(res).toEqual(data.res);
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("When text contain new lines", () => {
|
|
|
const text = `Hello
|
|
|
whats up`;
|
|
|
[
|
|
|
{
|
|
|
desc: "break all words when width of each word is less than container width",
|
|
|
width: 80,
|
|
|
res: `Hello\nwhats\nup`,
|
|
|
},
|
|
|
{
|
|
|
desc: "break all characters when width of each character is less than container width",
|
|
|
width: 25,
|
|
|
res: `H
|
|
|
e
|
|
|
l
|
|
|
l
|
|
|
o
|
|
|
w
|
|
|
h
|
|
|
a
|
|
|
t
|
|
|
s
|
|
|
u
|
|
|
p`,
|
|
|
},
|
|
|
{
|
|
|
desc: "break words as per the width",
|
|
|
|
|
|
width: 150,
|
|
|
res: `Hello
|
|
|
whats up`,
|
|
|
},
|
|
|
{
|
|
|
desc: "fit the container",
|
|
|
|
|
|
width: 250,
|
|
|
res: `Hello
|
|
|
whats up`,
|
|
|
},
|
|
|
].forEach((data) => {
|
|
|
it(`should respect new lines and ${data.desc}`, () => {
|
|
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
|
expect(res).toEqual(data.res);
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("When text is long", () => {
|
|
|
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
|
|
|
[
|
|
|
{
|
|
|
desc: "fit characters of long string as per container width",
|
|
|
width: 170,
|
|
|
res: `hellolongtextthi\nsiswhatsupwithyo\nuIamtypingggggan\ndtypinggg break\nit now`,
|
|
|
},
|
|
|
{
|
|
|
desc: "fit characters of long string as per container width and break words as per the width",
|
|
|
|
|
|
width: 130,
|
|
|
res: `hellolongtex
|
|
|
tthisiswhats
|
|
|
upwithyouIam
|
|
|
typingggggan
|
|
|
dtypinggg
|
|
|
break it now`,
|
|
|
},
|
|
|
{
|
|
|
desc: "fit the long text when container width is greater than text length and move the rest to next line",
|
|
|
|
|
|
width: 600,
|
|
|
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg\nbreak it now`,
|
|
|
},
|
|
|
].forEach((data) => {
|
|
|
it(`should ${data.desc}`, () => {
|
|
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
|
|
expect(res).toEqual(data.res);
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test parseTokens", () => {
|
|
|
it("should tokenize latin", () => {
|
|
|
let text = "Excalidraw is a virtual collaborative whiteboard";
|
|
|
|
|
|
expect(parseTokens(text)).toEqual([
|
|
|
"Excalidraw",
|
|
|
" ",
|
|
|
"is",
|
|
|
" ",
|
|
|
"a",
|
|
|
" ",
|
|
|
"virtual",
|
|
|
" ",
|
|
|
"collaborative",
|
|
|
" ",
|
|
|
"whiteboard",
|
|
|
]);
|
|
|
|
|
|
text =
|
|
|
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
|
|
|
expect(parseTokens(text)).toEqual([
|
|
|
"Wikipedia",
|
|
|
" ",
|
|
|
"is",
|
|
|
" ",
|
|
|
"hosted",
|
|
|
" ",
|
|
|
"by",
|
|
|
" ",
|
|
|
"Wikimedia-",
|
|
|
" ",
|
|
|
"Foundation,",
|
|
|
" ",
|
|
|
"a",
|
|
|
" ",
|
|
|
"non-",
|
|
|
"profit",
|
|
|
" ",
|
|
|
"organization",
|
|
|
" ",
|
|
|
"that",
|
|
|
" ",
|
|
|
"also",
|
|
|
" ",
|
|
|
"hosts",
|
|
|
" ",
|
|
|
"a",
|
|
|
" ",
|
|
|
"range-",
|
|
|
"of",
|
|
|
" ",
|
|
|
"other",
|
|
|
" ",
|
|
|
"projects",
|
|
|
]);
|
|
|
});
|
|
|
|
|
|
it("should not tokenize number", () => {
|
|
|
const text = "99,100.99";
|
|
|
const tokens = parseTokens(text);
|
|
|
expect(tokens).toEqual(["99,100.99"]);
|
|
|
});
|
|
|
|
|
|
it("should tokenize joined emojis", () => {
|
|
|
const text = `😬🌍🗺🔥☂️👩🏽🦰👨👩👧👦👩🏾🔬🏳️🌈🧔♀️🧑🤝🧑🙅🏽♂️✅0️⃣🇨🇿🦅`;
|
|
|
const tokens = parseTokens(text);
|
|
|
|
|
|
expect(tokens).toEqual([
|
|
|
"😬",
|
|
|
"🌍",
|
|
|
"🗺",
|
|
|
"🔥",
|
|
|
"☂️",
|
|
|
"👩🏽🦰",
|
|
|
"👨👩👧👦",
|
|
|
"👩🏾🔬",
|
|
|
"🏳️🌈",
|
|
|
"🧔♀️",
|
|
|
"🧑🤝🧑",
|
|
|
"🙅🏽♂️",
|
|
|
"✅",
|
|
|
"0️⃣",
|
|
|
"🇨🇿",
|
|
|
"🦅",
|
|
|
]);
|
|
|
});
|
|
|
|
|
|
it("should tokenize emojis mixed with mixed text", () => {
|
|
|
const text = `😬a🌍b🗺c🔥d☂️《👩🏽🦰》👨👩👧👦德👩🏾🔬こ🏳️🌈안🧔♀️g🧑🤝🧑h🙅🏽♂️e✅f0️⃣g🇨🇿10🦅#hash`;
|
|
|
const tokens = parseTokens(text);
|
|
|
|
|
|
expect(tokens).toEqual([
|
|
|
"😬",
|
|
|
"a",
|
|
|
"🌍",
|
|
|
"b",
|
|
|
"🗺",
|
|
|
"c",
|
|
|
"🔥",
|
|
|
"d",
|
|
|
"☂️",
|
|
|
"《",
|
|
|
"👩🏽🦰",
|
|
|
"》",
|
|
|
"👨👩👧👦",
|
|
|
"德",
|
|
|
"👩🏾🔬",
|
|
|
"こ",
|
|
|
"🏳️🌈",
|
|
|
"안",
|
|
|
"🧔♀️",
|
|
|
"g",
|
|
|
"🧑🤝🧑",
|
|
|
"h",
|
|
|
"🙅🏽♂️",
|
|
|
"e",
|
|
|
"✅",
|
|
|
"f0️⃣g", // bummer, but ok, as we traded kecaps not breaking (less common) for hash and numbers not breaking (more common)
|
|
|
"🇨🇿",
|
|
|
"10", // nice! do not break the number, as it's by default matched by \p{Emoji}
|
|
|
"🦅",
|
|
|
"#hash", // nice! do not break the hash, as it's by default matched by \p{Emoji}
|
|
|
]);
|
|
|
});
|
|
|
|
|
|
it("should tokenize decomposed chars into their composed variants", () => {
|
|
|
// each input character is in a decomposed form
|
|
|
const text = "čでäぴέ다й한";
|
|
|
expect(text.normalize("NFC").length).toEqual(8);
|
|
|
expect(text).toEqual(text.normalize("NFD"));
|
|
|
|
|
|
const tokens = parseTokens(text);
|
|
|
expect(tokens.length).toEqual(8);
|
|
|
expect(tokens).toEqual(["č", "で", "ä", "ぴ", "έ", "다", "й", "한"]);
|
|
|
});
|
|
|
|
|
|
it("should tokenize artificial CJK", () => {
|
|
|
const text = `《道德經》醫-醫こんにちは世界!안녕하세요세계;다.다...원/달(((다)))[[1]]〚({((한))>)〛た…[Hello] World?ニューヨーク・¥3700.55す。090-1234-5678¥1,000〜$5,000「素晴らしい!」〔重要〕#1:Taro君30%は、(たなばた)〰¥110±¥570で20℃〜9:30〜10:00【一番】`;
|
|
|
|
|
|
// [
|
|
|
// '《道', '德', '經》', '醫-',
|
|
|
// '醫', 'こ', 'ん', 'に',
|
|
|
// 'ち', 'は', '世', '界!',
|
|
|
// '안', '녕', '하', '세',
|
|
|
// '요', '세', '계;', '다.',
|
|
|
// '다...', '원/', '달', '(((다)))',
|
|
|
// '[[1]]', '〚({((한))>)〛', 'た…', '[Hello]',
|
|
|
// ' ', 'World?', 'ニ', 'ュ',
|
|
|
// 'ー', 'ヨ', 'ー', 'ク・',
|
|
|
// '¥3700.55', 'す。', '090-', '1234-',
|
|
|
// '5678¥1,000', '〜', '$5,000', '「素',
|
|
|
// '晴', 'ら', 'し', 'い!」',
|
|
|
// '〔重', '要〕', '#', '1:',
|
|
|
// 'Taro', '君', '30%', 'は、',
|
|
|
// '(た', 'な', 'ば', 'た)',
|
|
|
// '〰', '¥110±', '¥570', 'で',
|
|
|
// '20℃', '〜', '9:30', '〜',
|
|
|
// '10:00', '【一', '番】'
|
|
|
// ]
|
|
|
const tokens = parseTokens(text);
|
|
|
|
|
|
// Latin
|
|
|
expect(tokens).toContain("[[1]]");
|
|
|
expect(tokens).toContain("[Hello]");
|
|
|
expect(tokens).toContain("World?");
|
|
|
expect(tokens).toContain("Taro");
|
|
|
|
|
|
// Chinese
|
|
|
expect(tokens).toContain("《道");
|
|
|
expect(tokens).toContain("德");
|
|
|
expect(tokens).toContain("經》");
|
|
|
expect(tokens).toContain("醫-");
|
|
|
expect(tokens).toContain("醫");
|
|
|
|
|
|
// Japanese
|
|
|
expect(tokens).toContain("こ");
|
|
|
expect(tokens).toContain("ん");
|
|
|
expect(tokens).toContain("に");
|
|
|
expect(tokens).toContain("ち");
|
|
|
expect(tokens).toContain("は");
|
|
|
expect(tokens).toContain("世");
|
|
|
expect(tokens).toContain("ニ");
|
|
|
expect(tokens).toContain("ク・");
|
|
|
expect(tokens).toContain("界!");
|
|
|
expect(tokens).toContain("た…");
|
|
|
expect(tokens).toContain("す。");
|
|
|
expect(tokens).toContain("ュ");
|
|
|
expect(tokens).toContain("ー");
|
|
|
expect(tokens).toContain("「素");
|
|
|
expect(tokens).toContain("晴");
|
|
|
expect(tokens).toContain("ら");
|
|
|
expect(tokens).toContain("し");
|
|
|
expect(tokens).toContain("い!」");
|
|
|
expect(tokens).toContain("君");
|
|
|
expect(tokens).toContain("は、");
|
|
|
expect(tokens).toContain("(た");
|
|
|
expect(tokens).toContain("な");
|
|
|
expect(tokens).toContain("ば");
|
|
|
expect(tokens).toContain("た)");
|
|
|
expect(tokens).toContain("で");
|
|
|
expect(tokens).toContain("【一");
|
|
|
expect(tokens).toContain("番】");
|
|
|
|
|
|
// Check for Korean
|
|
|
expect(tokens).toContain("안");
|
|
|
expect(tokens).toContain("녕");
|
|
|
expect(tokens).toContain("하");
|
|
|
expect(tokens).toContain("세");
|
|
|
expect(tokens).toContain("요");
|
|
|
expect(tokens).toContain("세");
|
|
|
expect(tokens).toContain("계;");
|
|
|
expect(tokens).toContain("다.");
|
|
|
expect(tokens).toContain("다...");
|
|
|
expect(tokens).toContain("원/");
|
|
|
expect(tokens).toContain("달");
|
|
|
expect(tokens).toContain("(((다)))");
|
|
|
expect(tokens).toContain("〚({((한))>)〛");
|
|
|
|
|
|
// Numbers and units
|
|
|
expect(tokens).toContain("¥3700.55");
|
|
|
expect(tokens).toContain("090-");
|
|
|
expect(tokens).toContain("1234-");
|
|
|
expect(tokens).toContain("5678¥1,000");
|
|
|
expect(tokens).toContain("$5,000");
|
|
|
expect(tokens).toContain("1:");
|
|
|
expect(tokens).toContain("30%");
|
|
|
expect(tokens).toContain("¥110±");
|
|
|
expect(tokens).toContain("¥570");
|
|
|
expect(tokens).toContain("20℃");
|
|
|
expect(tokens).toContain("9:30");
|
|
|
expect(tokens).toContain("10:00");
|
|
|
|
|
|
// Punctuation and symbols
|
|
|
expect(tokens).toContain("〜");
|
|
|
expect(tokens).toContain("〰");
|
|
|
expect(tokens).toContain("#");
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test measureText", () => {
|
|
|
describe("Test getContainerCoords", () => {
|
|
|
const params = { width: 200, height: 100, x: 10, y: 20 };
|
|
|
|
|
|
it("should compute coords correctly when ellipse", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "ellipse",
|
|
|
...params,
|
|
|
});
|
|
|
expect(getContainerCoords(element)).toEqual({
|
|
|
x: 44.2893218813452455,
|
|
|
y: 39.64466094067262,
|
|
|
});
|
|
|
});
|
|
|
|
|
|
it("should compute coords correctly when rectangle", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "rectangle",
|
|
|
...params,
|
|
|
});
|
|
|
expect(getContainerCoords(element)).toEqual({
|
|
|
x: 15,
|
|
|
y: 25,
|
|
|
});
|
|
|
});
|
|
|
|
|
|
it("should compute coords correctly when diamond", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "diamond",
|
|
|
...params,
|
|
|
});
|
|
|
expect(getContainerCoords(element)).toEqual({
|
|
|
x: 65,
|
|
|
y: 50,
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test computeContainerDimensionForBoundText", () => {
|
|
|
const params = {
|
|
|
width: 178,
|
|
|
height: 194,
|
|
|
};
|
|
|
|
|
|
it("should compute container height correctly for rectangle", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "rectangle",
|
|
|
...params,
|
|
|
});
|
|
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
|
160,
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("should compute container height correctly for ellipse", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "ellipse",
|
|
|
...params,
|
|
|
});
|
|
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
|
226,
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("should compute container height correctly for diamond", () => {
|
|
|
const element = API.createElement({
|
|
|
type: "diamond",
|
|
|
...params,
|
|
|
});
|
|
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
|
320,
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test getBoundTextMaxWidth", () => {
|
|
|
const params = {
|
|
|
width: 178,
|
|
|
height: 194,
|
|
|
};
|
|
|
|
|
|
it("should return max width when container is rectangle", () => {
|
|
|
const container = API.createElement({ type: "rectangle", ...params });
|
|
|
expect(getBoundTextMaxWidth(container, null)).toBe(168);
|
|
|
});
|
|
|
|
|
|
it("should return max width when container is ellipse", () => {
|
|
|
const container = API.createElement({ type: "ellipse", ...params });
|
|
|
expect(getBoundTextMaxWidth(container, null)).toBe(116);
|
|
|
});
|
|
|
|
|
|
it("should return max width when container is diamond", () => {
|
|
|
const container = API.createElement({ type: "diamond", ...params });
|
|
|
expect(getBoundTextMaxWidth(container, null)).toBe(79);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test getBoundTextMaxHeight", () => {
|
|
|
const params = {
|
|
|
width: 178,
|
|
|
height: 194,
|
|
|
id: '"container-id',
|
|
|
};
|
|
|
|
|
|
const boundTextElement = API.createElement({
|
|
|
type: "text",
|
|
|
id: "text-id",
|
|
|
x: 560.51171875,
|
|
|
y: 202.033203125,
|
|
|
width: 154,
|
|
|
height: 175,
|
|
|
fontSize: 20,
|
|
|
fontFamily: 1,
|
|
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
|
|
textAlign: "center",
|
|
|
verticalAlign: "middle",
|
|
|
containerId: params.id,
|
|
|
}) as ExcalidrawTextElementWithContainer;
|
|
|
|
|
|
it("should return max height when container is rectangle", () => {
|
|
|
const container = API.createElement({ type: "rectangle", ...params });
|
|
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(184);
|
|
|
});
|
|
|
|
|
|
it("should return max height when container is ellipse", () => {
|
|
|
const container = API.createElement({ type: "ellipse", ...params });
|
|
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(127);
|
|
|
});
|
|
|
|
|
|
it("should return max height when container is diamond", () => {
|
|
|
const container = API.createElement({ type: "diamond", ...params });
|
|
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(87);
|
|
|
});
|
|
|
|
|
|
it("should return max height when container is arrow", () => {
|
|
|
const container = API.createElement({
|
|
|
type: "arrow",
|
|
|
...params,
|
|
|
});
|
|
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(194);
|
|
|
});
|
|
|
|
|
|
it("should return max height when container is arrow and height is less than threshold", () => {
|
|
|
const container = API.createElement({
|
|
|
type: "arrow",
|
|
|
...params,
|
|
|
height: 70,
|
|
|
boundElements: [{ type: "text", id: "text-id" }],
|
|
|
});
|
|
|
|
|
|
expect(getBoundTextMaxHeight(container, boundTextElement)).toBe(
|
|
|
boundTextElement.height,
|
|
|
);
|
|
|
});
|
|
|
});
|
|
|
});
|
|
|
|
|
|
const textElement = API.createElement({
|
|
|
type: "text",
|
|
|
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
|
|
|
fontSize: 20,
|
|
|
fontFamily: 1,
|
|
|
height: 175,
|
|
|
});
|
|
|
|
|
|
describe("Test detectLineHeight", () => {
|
|
|
it("should return correct line height", () => {
|
|
|
expect(detectLineHeight(textElement)).toBe(1.25);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test getLineHeightInPx", () => {
|
|
|
it("should return correct line height", () => {
|
|
|
expect(
|
|
|
getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
|
|
|
).toBe(25);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("Test getDefaultLineHeight", () => {
|
|
|
it("should return line height using default font family when not passed", () => {
|
|
|
//@ts-ignore
|
|
|
expect(getLineHeight()).toBe(1.25);
|
|
|
});
|
|
|
|
|
|
it("should return line height using default font family for unknown font", () => {
|
|
|
const UNKNOWN_FONT = 5;
|
|
|
expect(getLineHeight(UNKNOWN_FONT)).toBe(1.25);
|
|
|
});
|
|
|
|
|
|
it("should return correct line height", () => {
|
|
|
expect(getLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
|
|
|
});
|
|
|
});
|