diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /src/utils/string-formatting.ts | |
init commit
Diffstat (limited to 'src/utils/string-formatting.ts')
| -rw-r--r-- | src/utils/string-formatting.ts | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/src/utils/string-formatting.ts b/src/utils/string-formatting.ts new file mode 100644 index 0000000..ff9f8bb --- /dev/null +++ b/src/utils/string-formatting.ts @@ -0,0 +1,126 @@ +import type I18N from '@amp/web-apps-localization'; +import he from 'he'; + +export function isString(string: unknown): string is string { + return typeof string === 'string'; +} + +export function concatWithMiddot(pieces: string[], i18n: I18N): string { + if (!pieces.length) { + return ''; + } + + return ( + pieces.reduce((memo, current) => { + return i18n.t('ASE.Web.AppStore.ContentA.Middot.ContentB', { + contentA: memo, + contentB: current, + }); + }) || '' + ); +} + +/** + * Truncates a block of text to fit within a character limit, with a bias towards ending on a + * full sentence. If no complete sentence fits within the limit, it falls back to a word-based + * truncation with an ellipsis. + * + * @param {string} text - The text to truncate. + * @param {number} limit - The maximum number of characters allowed before truncation. + * @param {string} [locale=en_US] - The locale to use when breaking the text into segments. + * @returns {string} Truncated text clipped to the limit, ideally ending on a natural stopping point. + */ +export function truncateAroundLimit( + text: string, + limit: number, + locale: string = 'en-US', +): string { + // If the text is shorter than the limit, return all the text, unaltered. + if (text.length <= limit) { + return text; + } + + const decodedText = he.decode(text); + + const isSegemnterSupported = typeof Intl.Segmenter === 'function'; + const terminatingPunctuation = '…'; + + // A very naive fallback if the browser doesn't support `Segementer`, + // which just truncates the text to the last space before the `limit`. + if (!isSegemnterSupported) { + const truncatedText = decodedText.slice(0, limit); + const indexOfLastSpace = truncatedText.lastIndexOf(' '); + if (indexOfLastSpace) { + return ( + truncatedText.slice(0, indexOfLastSpace).trim() + + terminatingPunctuation + ); + } else { + // If the text is an _exteremly_ long word or block of text, like a URL + return truncatedText.trim() + terminatingPunctuation; + } + } + + const sentences = Array.from( + new Intl.Segmenter(locale, { granularity: 'sentence' }).segment(text), + (s) => s.segment, + ); + + let result = ''; + for (const sentence of sentences) { + // If there is still room to add another sentence without going over the limit, add it. + if (result.length + sentence.length <= limit) { + result += sentence; + } else { + break; + } + } + + result = result.trim(); + + // If the result we built based on full sentences is close-enough to the desired limit + // (e.g. within the threshold of 75% of 160), we can use it. + if (result.length >= limit * 0.75) { + return result; + } + + // Otherwise, fallback to building up single words until we approach the limit. + const segments = Array.from( + new Intl.Segmenter(locale, { granularity: 'word' }).segment( + decodedText, + ), + ); + + result = ''; + for (const { segment } of segments) { + if (result.length + segment.length <= limit) { + result += segment; + } else { + break; + } + } + + return result.trim() + terminatingPunctuation; +} + +export function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +export function commaSeparatedList(items: Array<string>, locale = 'en') { + return new Intl.ListFormat(locale, { + style: 'long', + type: 'conjunction', + }).format(items); +} + +export function stripTags(text: string) { + return text.replace(/(<([^>]+)>)/gi, ''); +} + +export function stripUnicodeWhitespace(text: string) { + return text.replace(/[\u0000-\u001F]/g, ''); +} |
