1/*
2 * Copyright (C) 2005, 2006, 2007, 2014 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include "config.h"
30#include "StringTruncator.h"
31
32#include "FontCascade.h"
33#include <wtf/text/TextBreakIterator.h>
34#include "TextRun.h"
35#include <unicode/ubrk.h>
36#include <wtf/Assertions.h>
37#include <wtf/Vector.h>
38#include <wtf/text/StringView.h>
39#include <wtf/unicode/CharacterNames.h>
40
41namespace WebCore {
42
43#define STRING_BUFFER_SIZE 2048
44
45typedef unsigned TruncationFunction(const String&, unsigned length, unsigned keepCount, UChar* buffer, bool shouldInsertEllipsis);
46
47static inline int textBreakAtOrPreceding(UBreakIterator* it, int offset)
48{
49 if (ubrk_isBoundary(it, offset))
50 return offset;
51
52 int result = ubrk_preceding(it, offset);
53 return result == UBRK_DONE ? 0 : result;
54}
55
56static inline int boundedTextBreakFollowing(UBreakIterator* it, int offset, int length)
57{
58 int result = ubrk_following(it, offset);
59 return result == UBRK_DONE ? length : result;
60}
61
62static unsigned centerTruncateToBuffer(const String& string, unsigned length, unsigned keepCount, UChar* buffer, bool shouldInsertEllipsis)
63{
64 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < length);
65 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < STRING_BUFFER_SIZE);
66
67 unsigned omitStart = (keepCount + 1) / 2;
68 NonSharedCharacterBreakIterator it(StringView(string).substring(0, length));
69 unsigned omitEnd = boundedTextBreakFollowing(it, omitStart + (length - keepCount) - 1, length);
70 omitStart = textBreakAtOrPreceding(it, omitStart);
71
72#if PLATFORM(IOS_FAMILY)
73 // FIXME: We should guard this code behind an editing behavior. Then we can remove the PLATFORM(IOS_FAMILY)-guard.
74 // Or just turn it on for all platforms. It seems like good behavior everywhere. Might be better to generalize
75 // it to handle all whitespace, not just "space".
76
77 // Strip single character before ellipsis character, when that character is preceded by a space
78 if (omitStart > 1 && string[omitStart - 1] != space && omitStart > 2 && string[omitStart - 2] == space)
79 --omitStart;
80
81 // Strip whitespace before and after the ellipsis character
82 while (omitStart > 1 && string[omitStart - 1] == space)
83 --omitStart;
84
85 // Strip single character after ellipsis character, when that character is followed by a space
86 if ((length - omitEnd) > 1 && string[omitEnd] != space && (length - omitEnd) > 2 && string[omitEnd + 1] == space)
87 ++omitEnd;
88
89 while ((length - omitEnd) > 1 && string[omitEnd] == space)
90 ++omitEnd;
91#endif
92
93 unsigned truncatedLength = omitStart + shouldInsertEllipsis + (length - omitEnd);
94 ASSERT(truncatedLength <= length);
95
96 StringView(string).substring(0, omitStart).getCharactersWithUpconvert(buffer);
97 if (shouldInsertEllipsis)
98 buffer[omitStart++] = horizontalEllipsis;
99 StringView(string).substring(omitEnd, length - omitEnd).getCharactersWithUpconvert(&buffer[omitStart]);
100 return truncatedLength;
101}
102
103static unsigned rightTruncateToBuffer(const String& string, unsigned length, unsigned keepCount, UChar* buffer, bool shouldInsertEllipsis)
104{
105 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < length);
106 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < STRING_BUFFER_SIZE);
107
108#if PLATFORM(IOS_FAMILY)
109 // FIXME: We should guard this code behind an editing behavior. Then we can remove the PLATFORM(IOS_FAMILY)-guard.
110 // Or just turn it on for all platforms. It seems like good behavior everywhere. Might be better to generalize
111 // it to handle all whitespace, not just "space".
112
113 // Strip single character before ellipsis character, when that character is preceded by a space
114 if (keepCount > 1 && string[keepCount - 1] != space && keepCount > 2 && string[keepCount - 2] == space)
115 --keepCount;
116
117 // Strip whitespace before the ellipsis character
118 while (keepCount > 1 && string[keepCount - 1] == space)
119 --keepCount;
120#endif
121
122 NonSharedCharacterBreakIterator it(StringView(string).substring(0, length));
123 unsigned keepLength = textBreakAtOrPreceding(it, keepCount);
124 unsigned truncatedLength = shouldInsertEllipsis ? keepLength + 1 : keepLength;
125
126 StringView(string).substring(0, keepLength).getCharactersWithUpconvert(buffer);
127 if (shouldInsertEllipsis)
128 buffer[keepLength] = horizontalEllipsis;
129
130 return truncatedLength;
131}
132
133static unsigned rightClipToCharacterBuffer(const String& string, unsigned length, unsigned keepCount, UChar* buffer, bool)
134{
135 ASSERT(keepCount < length);
136 ASSERT(keepCount < STRING_BUFFER_SIZE);
137
138 NonSharedCharacterBreakIterator it(StringView(string).substring(0, length));
139 unsigned keepLength = textBreakAtOrPreceding(it, keepCount);
140 StringView(string).substring(0, keepLength).getCharactersWithUpconvert(buffer);
141
142 return keepLength;
143}
144
145static unsigned rightClipToWordBuffer(const String& string, unsigned length, unsigned keepCount, UChar* buffer, bool)
146{
147 ASSERT(keepCount < length);
148 ASSERT(keepCount < STRING_BUFFER_SIZE);
149
150 UBreakIterator* it = wordBreakIterator(StringView(string).substring(0, length));
151 unsigned keepLength = textBreakAtOrPreceding(it, keepCount);
152 StringView(string).substring(0, keepLength).getCharactersWithUpconvert(buffer);
153
154#if PLATFORM(IOS_FAMILY)
155 // FIXME: We should guard this code behind an editing behavior. Then we can remove the PLATFORM(IOS_FAMILY)-guard.
156 // Or just turn it on for all platforms. It seems like good behavior everywhere. Might be better to generalize
157 // it to handle all whitespace, not just "space".
158
159 // Motivated by <rdar://problem/7439327> truncation should not include a trailing space
160 while (keepLength && string[keepLength - 1] == space)
161 --keepLength;
162#endif
163
164 return keepLength;
165}
166
167static unsigned leftTruncateToBuffer(const String& string, unsigned length, unsigned keepCount, UChar* buffer, bool shouldInsertEllipsis)
168{
169 ASSERT(keepCount < length);
170 ASSERT(keepCount < STRING_BUFFER_SIZE);
171
172 unsigned startIndex = length - keepCount;
173
174 NonSharedCharacterBreakIterator it(string);
175 unsigned adjustedStartIndex = startIndex;
176 boundedTextBreakFollowing(it, startIndex, length - startIndex);
177
178 // Strip single character after ellipsis character, when that character is preceded by a space
179 if (adjustedStartIndex < length && string[adjustedStartIndex] != space
180 && adjustedStartIndex < length - 1 && string[adjustedStartIndex + 1] == space)
181 ++adjustedStartIndex;
182
183 // Strip whitespace after the ellipsis character
184 while (adjustedStartIndex < length && string[adjustedStartIndex] == space)
185 ++adjustedStartIndex;
186
187 if (shouldInsertEllipsis) {
188 buffer[0] = horizontalEllipsis;
189 StringView(string).substring(adjustedStartIndex, length - adjustedStartIndex + 1).getCharactersWithUpconvert(&buffer[1]);
190 return length - adjustedStartIndex + 1;
191 }
192 StringView(string).substring(adjustedStartIndex, length - adjustedStartIndex + 1).getCharactersWithUpconvert(&buffer[0]);
193 return length - adjustedStartIndex;
194}
195
196static float stringWidth(const FontCascade& renderer, const UChar* characters, unsigned length)
197{
198 TextRun run(StringView(characters, length));
199 return renderer.width(run);
200}
201
202static String truncateString(const String& string, float maxWidth, const FontCascade& font, TruncationFunction truncateToBuffer, float* resultWidth = nullptr, bool shouldInsertEllipsis = true, float customTruncationElementWidth = 0, bool alwaysTruncate = false)
203{
204 if (string.isEmpty())
205 return string;
206
207 if (resultWidth)
208 *resultWidth = 0;
209
210 ASSERT(maxWidth >= 0);
211
212 float currentEllipsisWidth = shouldInsertEllipsis ? stringWidth(font, &horizontalEllipsis, 1) : customTruncationElementWidth;
213
214 UChar stringBuffer[STRING_BUFFER_SIZE];
215 unsigned truncatedLength;
216 unsigned keepCount;
217 unsigned length = string.length();
218
219 if (length > STRING_BUFFER_SIZE) {
220 if (shouldInsertEllipsis)
221 keepCount = STRING_BUFFER_SIZE - 1; // need 1 character for the ellipsis
222 else
223 keepCount = 0;
224 truncatedLength = centerTruncateToBuffer(string, length, keepCount, stringBuffer, shouldInsertEllipsis);
225 } else {
226 keepCount = length;
227 StringView(string).getCharactersWithUpconvert(stringBuffer);
228 truncatedLength = length;
229 }
230
231 float width = stringWidth(font, stringBuffer, truncatedLength);
232 if (!shouldInsertEllipsis && alwaysTruncate)
233 width += customTruncationElementWidth;
234 if ((width - maxWidth) < 0.0001) { // Ignore rounding errors.
235 if (resultWidth)
236 *resultWidth = width;
237 return string;
238 }
239
240 unsigned keepCountForLargestKnownToFit = 0;
241 float widthForLargestKnownToFit = currentEllipsisWidth;
242
243 unsigned keepCountForSmallestKnownToNotFit = keepCount;
244 float widthForSmallestKnownToNotFit = width;
245
246 if (currentEllipsisWidth >= maxWidth) {
247 keepCountForLargestKnownToFit = 1;
248 keepCountForSmallestKnownToNotFit = 2;
249 }
250
251 while (keepCountForLargestKnownToFit + 1 < keepCountForSmallestKnownToNotFit) {
252 ASSERT_WITH_SECURITY_IMPLICATION(widthForLargestKnownToFit <= maxWidth);
253 ASSERT_WITH_SECURITY_IMPLICATION(widthForSmallestKnownToNotFit > maxWidth);
254
255 float ratio = (keepCountForSmallestKnownToNotFit - keepCountForLargestKnownToFit)
256 / (widthForSmallestKnownToNotFit - widthForLargestKnownToFit);
257 keepCount = static_cast<unsigned>(maxWidth * ratio);
258
259 if (keepCount <= keepCountForLargestKnownToFit)
260 keepCount = keepCountForLargestKnownToFit + 1;
261 else if (keepCount >= keepCountForSmallestKnownToNotFit)
262 keepCount = keepCountForSmallestKnownToNotFit - 1;
263
264 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < length);
265 ASSERT(keepCount > 0);
266 ASSERT_WITH_SECURITY_IMPLICATION(keepCount < keepCountForSmallestKnownToNotFit);
267 ASSERT_WITH_SECURITY_IMPLICATION(keepCount > keepCountForLargestKnownToFit);
268
269 truncatedLength = truncateToBuffer(string, length, keepCount, stringBuffer, shouldInsertEllipsis);
270
271 width = stringWidth(font, stringBuffer, truncatedLength);
272 if (!shouldInsertEllipsis)
273 width += customTruncationElementWidth;
274 if (width <= maxWidth) {
275 keepCountForLargestKnownToFit = keepCount;
276 widthForLargestKnownToFit = width;
277 if (resultWidth)
278 *resultWidth = width;
279 } else {
280 keepCountForSmallestKnownToNotFit = keepCount;
281 widthForSmallestKnownToNotFit = width;
282 }
283 }
284
285 if (keepCountForLargestKnownToFit == 0) {
286 keepCountForLargestKnownToFit = 1;
287 }
288
289 if (keepCount != keepCountForLargestKnownToFit) {
290 keepCount = keepCountForLargestKnownToFit;
291 truncatedLength = truncateToBuffer(string, length, keepCount, stringBuffer, shouldInsertEllipsis);
292 }
293
294 return String(stringBuffer, truncatedLength);
295}
296
297String StringTruncator::centerTruncate(const String& string, float maxWidth, const FontCascade& font)
298{
299 return truncateString(string, maxWidth, font, centerTruncateToBuffer);
300}
301
302String StringTruncator::rightTruncate(const String& string, float maxWidth, const FontCascade& font)
303{
304 return truncateString(string, maxWidth, font, rightTruncateToBuffer);
305}
306
307float StringTruncator::width(const String& string, const FontCascade& font)
308{
309 return stringWidth(font, StringView(string).upconvertedCharacters(), string.length());
310}
311
312String StringTruncator::centerTruncate(const String& string, float maxWidth, const FontCascade& font, float& resultWidth, bool shouldInsertEllipsis, float customTruncationElementWidth)
313{
314 return truncateString(string, maxWidth, font, centerTruncateToBuffer, &resultWidth, shouldInsertEllipsis, customTruncationElementWidth);
315}
316
317String StringTruncator::rightTruncate(const String& string, float maxWidth, const FontCascade& font, float& resultWidth, bool shouldInsertEllipsis, float customTruncationElementWidth)
318{
319 return truncateString(string, maxWidth, font, rightTruncateToBuffer, &resultWidth, shouldInsertEllipsis, customTruncationElementWidth);
320}
321
322String StringTruncator::leftTruncate(const String& string, float maxWidth, const FontCascade& font, float& resultWidth, bool shouldInsertEllipsis, float customTruncationElementWidth)
323{
324 return truncateString(string, maxWidth, font, leftTruncateToBuffer, &resultWidth, shouldInsertEllipsis, customTruncationElementWidth);
325}
326
327String StringTruncator::rightClipToCharacter(const String& string, float maxWidth, const FontCascade& font, float& resultWidth, bool shouldInsertEllipsis, float customTruncationElementWidth)
328{
329 return truncateString(string, maxWidth, font, rightClipToCharacterBuffer, &resultWidth, shouldInsertEllipsis, customTruncationElementWidth);
330}
331
332String StringTruncator::rightClipToWord(const String& string, float maxWidth, const FontCascade& font, float& resultWidth, bool shouldInsertEllipsis, float customTruncationElementWidth, bool alwaysTruncate)
333{
334 return truncateString(string, maxWidth, font, rightClipToWordBuffer, &resultWidth, shouldInsertEllipsis, customTruncationElementWidth, alwaysTruncate);
335}
336
337} // namespace WebCore
338