this repo has no description
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, '&')
109 .replace(/</g, '<')
110 .replace(/>/g, '>');
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}