react-native-skia-responsive-text
Version:
A library providing a wrapper for the React Native Skia Text component, offering features like user-friendly text alignment, efficient multiline text management, and straightforward text truncation.
300 lines (265 loc) • 8.03 kB
text/typescript
import { SkFont } from '@shopify/react-native-skia';
import { ELLIPSIS } from '../constants';
import { EllipsizeMode, TextLineData } from '../types';
import { trimLineCenter, trimLineEnd, trimLineStart } from './ellipsize';
import { mergeLines } from './text';
const wrapWithoutTrimmingFromStart = (
chunks: Array<string>,
font: SkFont,
width: number,
maxLines = Infinity
): Array<TextLineData> => {
if (maxLines <= 0) return [];
const result: Array<TextLineData> = [];
let currentLine: { chunks: Array<string>; width: number } = {
chunks: [],
width: 0
};
let nextChunkWidth = font.getTextWidth(chunks[0]!);
const addNewLine = () => {
result.push({
text: currentLine.chunks.join(''),
width: currentLine.width
});
currentLine = { chunks: [], width: 0 };
};
const shouldBreak = () => result.length === maxLines;
for (let i = 0; i < chunks.length; i++) {
if (shouldBreak()) break;
const chunk = chunks[i]!;
const chunkWidth = nextChunkWidth;
const nextChunk = chunks[i + 1];
nextChunkWidth = nextChunk ? font.getTextWidth(nextChunk) : Infinity;
// If the chunk will fit in the text line, don't break it
if (chunkWidth <= width) {
// If the current chunk won't fit in the current line, add it to the next line
if (currentLine.width + chunkWidth > width) {
addNewLine();
if (result.length === maxLines) {
break;
}
}
// Add the chunk to the current line
currentLine.chunks.push(chunk);
currentLine.width += chunkWidth;
}
// Otherwise, if the chunk is a single word, break it
else {
if (currentLine.chunks.length && result.length < maxLines - 1) {
addNewLine();
}
let j = 0;
let k = 1;
let nextSliceWidth = font.getTextWidth(chunk.slice(j, k));
for (; k <= chunk.length; k++) {
const slice = chunk.slice(j, k);
if (currentLine.width + nextSliceWidth > width || k === chunk.length) {
currentLine.chunks.push(slice);
currentLine.width += font.getTextWidth(slice);
if (k < chunk.length) addNewLine();
if (shouldBreak()) break;
j = k;
}
nextSliceWidth = font.getTextWidth(chunk.slice(j, k + 1));
}
}
}
// If the last line is not empty, add it to the result
if (currentLine.chunks.length && !shouldBreak()) {
addNewLine();
}
return result;
};
const wrapWithoutTrimmingFromEnd = (
chunks: Array<string>,
font: SkFont,
width: number,
maxLines = Infinity
): Array<TextLineData> => {
if (maxLines <= 0) return [];
const result: Array<TextLineData> = [];
let currentLine: { chunks: Array<string>; width: number } = {
chunks: [],
width: 0
};
let nextChunkWidth = font.getTextWidth(chunks[chunks.length - 1]!);
const addNewLine = () => {
result.push({
text: currentLine.chunks.reverse().join(''),
width: currentLine.width
});
currentLine = { chunks: [], width: 0 };
};
const shouldBreak = () => result.length === maxLines;
for (let i = chunks.length - 1; i >= 0; i--) {
if (shouldBreak()) break;
const chunk = chunks[i]!;
const chunkWidth = nextChunkWidth;
const nextChunk = chunks[i - 1];
nextChunkWidth = nextChunk ? font.getTextWidth(nextChunk) : Infinity;
// If the chunk will fit in the text line, don't break it
if (chunkWidth <= width) {
// If the current chunk won't fit in the current line, add it to the next line
if (chunkWidth + currentLine.width > width) {
addNewLine();
if (result.length === maxLines) {
break;
}
}
// Add the chunk to the current line
currentLine.chunks.push(chunk);
currentLine.width += chunkWidth;
}
// Otherwise, if the chunk is a single word, break it
else {
if (currentLine.chunks.length && result.length < maxLines - 1) {
addNewLine();
}
let j = chunk.length;
let k = j - 1;
let nextSliceWidth = font.getTextWidth(chunk.slice(k, j));
for (; k >= 0; k--) {
const slice = chunk.slice(k, j);
if (currentLine.width + nextSliceWidth > width || k === 0) {
currentLine.chunks.push(slice);
currentLine.width += font.getTextWidth(slice);
if (k > 0) addNewLine();
if (shouldBreak()) break;
j = k;
}
nextSliceWidth = font.getTextWidth(chunk.slice(k - 1, j));
}
}
}
// If the last line is not empty, add it to the result
if (currentLine.chunks.length && !shouldBreak()) {
addNewLine();
}
return result.reverse();
};
const wrapWithTailTrimming = (
chunks: Array<string>,
font: SkFont,
width: number,
numberOfLines: number,
suffix = ELLIPSIS
): Array<TextLineData> => {
const withoutTrimming = wrapWithoutTrimmingFromStart(
chunks,
font,
width,
numberOfLines + 1
);
if (withoutTrimming.length <= numberOfLines) {
return withoutTrimming;
}
return [
...withoutTrimming.slice(0, numberOfLines - 1),
trimLineEnd(
mergeLines(withoutTrimming.slice(numberOfLines - 1, numberOfLines + 1)),
font,
width,
suffix
)
];
};
const wrapWithClipTrimming = (
chunks: Array<string>,
font: SkFont,
width: number,
numberOfLines: number
): Array<TextLineData> =>
wrapWithTailTrimming(chunks, font, width, numberOfLines, '');
const wrapWithHeadTrimming = (
chunks: Array<string>,
font: SkFont,
width: number,
numberOfLines: number
): Array<TextLineData> => {
const withoutTrimming = wrapWithoutTrimmingFromEnd(
chunks,
font,
width,
numberOfLines + 1
);
// If trimming is not needed, return the result of wrapWithoutTrimmingFromStart
// (to make it look more natural)
if (withoutTrimming.length <= numberOfLines) {
return wrapWithoutTrimmingFromStart(chunks, font, width, numberOfLines);
}
return [
trimLineStart(mergeLines(withoutTrimming.slice(0, 2)), font, width),
...withoutTrimming.slice(2)
];
};
const wrapWithMiddleTrimming = (
chunks: Array<string>,
font: SkFont,
width: number,
numberOfLines: number
): Array<TextLineData> => {
const withoutFromStart = wrapWithoutTrimmingFromStart(
chunks,
font,
width,
numberOfLines + 1
);
// If trimming is not needed, return the result of wrapWithoutTrimmingFromStart
// (to make it look more natural)
if (withoutFromStart.length <= numberOfLines) {
return withoutFromStart;
}
const halfLines = Math.floor(numberOfLines / 2);
const withoutFromEnd = wrapWithoutTrimmingFromEnd(
chunks,
font,
width,
halfLines + 1
);
if (numberOfLines % 2 === 0) {
return [
...withoutFromStart.slice(0, halfLines - 1),
trimLineEnd(
mergeLines(withoutFromStart.slice(halfLines - 1, halfLines + 1)),
font,
width
),
trimLineStart(mergeLines(withoutFromEnd.slice(0, 2)), font, width),
...withoutFromEnd.slice(2)
];
}
return [
...withoutFromStart.slice(0, halfLines),
trimLineCenter(
mergeLines([withoutFromStart[halfLines]!, withoutFromEnd[0]!]),
font,
width
),
...withoutFromEnd.slice(1)
];
};
export const wrapText = (
textChunks: Array<string>,
font: SkFont,
width: number,
numberOfLines = Infinity,
ellipsizeMode: EllipsizeMode = 'tail'
): Array<TextLineData> => {
if (!textChunks.length) {
return [];
}
if (numberOfLines === Infinity) {
return wrapWithoutTrimmingFromStart(textChunks, font, width);
}
switch (ellipsizeMode) {
case 'head':
return wrapWithHeadTrimming(textChunks, font, width, numberOfLines);
case 'clip':
return wrapWithClipTrimming(textChunks, font, width, numberOfLines);
case 'middle':
return wrapWithMiddleTrimming(textChunks, font, width, numberOfLines);
case 'tail':
default:
return wrapWithTailTrimming(textChunks, font, width, numberOfLines);
}
};