this repo has no description
at main 3.9 kB view raw
1import type I18N from '@amp/web-apps-localization'; 2import he from 'he'; 3 4export function isString(string: unknown): string is string { 5 return typeof string === 'string'; 6} 7 8export function concatWithMiddot(pieces: string[], i18n: I18N): string { 9 if (!pieces.length) { 10 return ''; 11 } 12 13 return ( 14 pieces.reduce((memo, current) => { 15 return i18n.t('ASE.Web.AppStore.ContentA.Middot.ContentB', { 16 contentA: memo, 17 contentB: current, 18 }); 19 }) || '' 20 ); 21} 22 23/** 24 * Truncates a block of text to fit within a character limit, with a bias towards ending on a 25 * full sentence. If no complete sentence fits within the limit, it falls back to a word-based 26 * truncation with an ellipsis. 27 * 28 * @param {string} text - The text to truncate. 29 * @param {number} limit - The maximum number of characters allowed before truncation. 30 * @param {string} [locale=en_US] - The locale to use when breaking the text into segments. 31 * @returns {string} Truncated text clipped to the limit, ideally ending on a natural stopping point. 32 */ 33export function truncateAroundLimit( 34 text: string, 35 limit: number, 36 locale: string = 'en-US', 37): string { 38 // If the text is shorter than the limit, return all the text, unaltered. 39 if (text.length <= limit) { 40 return text; 41 } 42 43 const decodedText = he.decode(text); 44 45 const isSegemnterSupported = typeof Intl.Segmenter === 'function'; 46 const terminatingPunctuation = '…'; 47 48 // A very naive fallback if the browser doesn't support `Segementer`, 49 // which just truncates the text to the last space before the `limit`. 50 if (!isSegemnterSupported) { 51 const truncatedText = decodedText.slice(0, limit); 52 const indexOfLastSpace = truncatedText.lastIndexOf(' '); 53 if (indexOfLastSpace) { 54 return ( 55 truncatedText.slice(0, indexOfLastSpace).trim() + 56 terminatingPunctuation 57 ); 58 } else { 59 // If the text is an _exteremly_ long word or block of text, like a URL 60 return truncatedText.trim() + terminatingPunctuation; 61 } 62 } 63 64 const sentences = Array.from( 65 new Intl.Segmenter(locale, { granularity: 'sentence' }).segment(text), 66 (s) => s.segment, 67 ); 68 69 let result = ''; 70 for (const sentence of sentences) { 71 // If there is still room to add another sentence without going over the limit, add it. 72 if (result.length + sentence.length <= limit) { 73 result += sentence; 74 } else { 75 break; 76 } 77 } 78 79 result = result.trim(); 80 81 // If the result we built based on full sentences is close-enough to the desired limit 82 // (e.g. within the threshold of 75% of 160), we can use it. 83 if (result.length >= limit * 0.75) { 84 return result; 85 } 86 87 // Otherwise, fallback to building up single words until we approach the limit. 88 const segments = Array.from( 89 new Intl.Segmenter(locale, { granularity: 'word' }).segment( 90 decodedText, 91 ), 92 ); 93 94 result = ''; 95 for (const { segment } of segments) { 96 if (result.length + segment.length <= limit) { 97 result += segment; 98 } else { 99 break; 100 } 101 } 102 103 return result.trim() + terminatingPunctuation; 104} 105 106export function escapeHtml(text: string): string { 107 return text 108 .replace(/&/g, '&amp;') 109 .replace(/</g, '&lt;') 110 .replace(/>/g, '&gt;'); 111} 112 113export function commaSeparatedList(items: Array<string>, locale = 'en') { 114 return new Intl.ListFormat(locale, { 115 style: 'long', 116 type: 'conjunction', 117 }).format(items); 118} 119 120export function stripTags(text: string) { 121 return text.replace(/(<([^>]+)>)/gi, ''); 122} 123 124export function stripUnicodeWhitespace(text: string) { 125 return text.replace(/[\u0000-\u001F]/g, ''); 126}