UNPKG

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
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); } };