Make normal case fast in the input element limitString function
authordarin@apple.com <darin@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 22 Nov 2016 17:16:02 +0000 (17:16 +0000)
committerdarin@apple.com <darin@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 22 Nov 2016 17:16:02 +0000 (17:16 +0000)
https://bugs.webkit.org/show_bug.cgi?id=165023

Reviewed by Dan Bernstein.

Source/WebCore:

When running Speedometer, the limitLength function was showing up as hot.
Fixed a couple obvious problems with that function's performance.

* html/TextFieldInputType.cpp:
(WebCore::isASCIILineBreak): Deleted. The isHTMLLineBreak function does
the same thing, but faster.
(WebCore::limitLength): Added a FIXME comment explaining that the function
isn't really a good idea. Don't call through to numCharactersInGraphemeClusters
at all for 8-bit strings since we don't allow CR or LF characters in the string
anyway, so there are no grapheme clusters more than a single code unit. Removed
optimization when the length is the string's length that String::left already does.
(WebCore::TextFieldInputType::sanitizeValue): Use isHTMLLineBreak instead of
isASCIILineBreak.
(WebCore::TextFieldInputType::handleBeforeTextInsertedEvent): Ditto.

* platform/LocalizedStrings.cpp: Use auto a lot more rather than writing out
RetainPtr.
(WebCore::truncatedStringForLookupMenuItem): Removed unneeded special case for
empty strings. Removed unneeded string with the ellipsis character in it, since
the makeString function already knows how to append a character to a string.

* rendering/RenderText.cpp:
(WebCore::mapLineBreakToIteratorMode): Updated for change to LineBreakIteratorMode.
* rendering/SimpleLineLayoutTextFragmentIterator.cpp:
(WebCore::SimpleLineLayout::TextFragmentIterator::nextBreakablePosition): Ditto.

Source/WTF:

* wtf/text/LineBreakIteratorPoolICU.h: Removed many unneeded includes.
Simplified the class a bit, removing some extra definitions.
(WTF::LineBreakIteratorPool::sharedPool): Use NeverDestroyed instead of new.
(WTF::LineBreakIteratorPool::makeLocaleWithBreakKeyword): Reimplemented in
a simpler way without using StringBuilder. Also updated for change to
LineBreakIteratorMode.
(WTF::LineBreakIteratorPool::put): Use uncheckedAppend since the code is
careful to only use the inline capacity in the vector.

* wtf/text/TextBreakIterator.cpp: Moved some includes in here from the header.
(WTF::mapLineIteratorModeToRules): Updated for change to LineBreakIteratorMode.
(WTF::openLineBreakIterator): Ditto.
(WTF::numGraphemeClusters): Added a fast path for all 8-bit strings; don't
use ICU for that case, even if there is a CR character in it.
(WTF::numCharactersInGraphemeClusters): Added a fast path for strings that are
short enough to entirely fit without even looking at the characters; that's a
case we likely hit all the time. Also added a fast path for all 8-bit strings.

* wtf/text/TextBreakIterator.h: Changed LineBreakIteratorMode to be an enum
class and not repeat UAX14 in the names of the modes. Initialize data members
in the class definition rather than the constructors.

Tools:

* TestWebKitAPI/CMakeLists.txt: Added TextBreakIterator.cpp.
* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj: Ditto.
* TestWebKitAPI/Tests/WTF/TextBreakIterator.cpp: Added.
Contains some tests for the numGraphemeClusters and
numCharactersInGraphemeClusters functions that I used to make sure
that the new fast paths I added work correctly.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@208963 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
Source/WTF/ChangeLog
Source/WTF/wtf/text/LineBreakIteratorPoolICU.h
Source/WTF/wtf/text/TextBreakIterator.cpp
Source/WTF/wtf/text/TextBreakIterator.h
Source/WebCore/ChangeLog
Source/WebCore/html/TextFieldInputType.cpp
Source/WebCore/platform/LocalizedStrings.cpp
Source/WebCore/rendering/RenderText.cpp
Source/WebCore/rendering/SimpleLineLayoutTextFragmentIterator.cpp
Tools/ChangeLog
Tools/TestWebKitAPI/CMakeLists.txt
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WTF/TextBreakIterator.cpp [new file with mode: 0644]

index 22cfea2..5679556 100644 (file)
@@ -1,3 +1,32 @@
+2016-11-22  Darin Adler  <darin@apple.com>
+
+        Make normal case fast in the input element limitString function
+        https://bugs.webkit.org/show_bug.cgi?id=165023
+
+        Reviewed by Dan Bernstein.
+
+        * wtf/text/LineBreakIteratorPoolICU.h: Removed many unneeded includes.
+        Simplified the class a bit, removing some extra definitions.
+        (WTF::LineBreakIteratorPool::sharedPool): Use NeverDestroyed instead of new.
+        (WTF::LineBreakIteratorPool::makeLocaleWithBreakKeyword): Reimplemented in
+        a simpler way without using StringBuilder. Also updated for change to
+        LineBreakIteratorMode.
+        (WTF::LineBreakIteratorPool::put): Use uncheckedAppend since the code is
+        careful to only use the inline capacity in the vector.
+
+        * wtf/text/TextBreakIterator.cpp: Moved some includes in here from the header.
+        (WTF::mapLineIteratorModeToRules): Updated for change to LineBreakIteratorMode.
+        (WTF::openLineBreakIterator): Ditto.
+        (WTF::numGraphemeClusters): Added a fast path for all 8-bit strings; don't
+        use ICU for that case, even if there is a CR character in it.
+        (WTF::numCharactersInGraphemeClusters): Added a fast path for strings that are
+        short enough to entirely fit without even looking at the characters; that's a
+        case we likely hit all the time. Also added a fast path for all 8-bit strings.
+
+        * wtf/text/TextBreakIterator.h: Changed LineBreakIteratorMode to be an enum
+        class and not repeat UAX14 in the names of the modes. Initialize data members
+        in the class definition rather than the constructors.
+
 2016-11-21  Mark Lam  <mark.lam@apple.com>
 
         Hasher::addCharacters() should be able to handle zero length strings.
index d76607d..151eadf 100644 (file)
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef LineBreakIteratorPoolICU_h
-#define LineBreakIteratorPoolICU_h
+#pragma once
 
 #include "TextBreakIterator.h"
-#include "TextBreakIteratorInternalICU.h"
-#include <unicode/ubrk.h>
-#include <wtf/Assertions.h>
 #include <wtf/HashMap.h>
+#include <wtf/NeverDestroyed.h>
 #include <wtf/ThreadSpecific.h>
 #include <wtf/text/AtomicString.h>
-#include <wtf/text/CString.h>
-#include <wtf/text/StringBuilder.h>
 
 namespace WTF {
 
 class LineBreakIteratorPool {
     WTF_MAKE_NONCOPYABLE(LineBreakIteratorPool);
 public:
-    LineBreakIteratorPool() { }
+    LineBreakIteratorPool() = default;
 
     static LineBreakIteratorPool& sharedPool()
     {
-        static WTF::ThreadSpecific<LineBreakIteratorPool>* pool = new WTF::ThreadSpecific<LineBreakIteratorPool>;
-        return **pool;
+        static NeverDestroyed<WTF::ThreadSpecific<LineBreakIteratorPool>> pool;
+        return *pool.get();
     }
 
-    static String makeLocaleWithBreakKeyword(const AtomicString& locale, LineBreakIteratorMode mode)
+    static AtomicString makeLocaleWithBreakKeyword(const AtomicString& locale, LineBreakIteratorMode mode)
     {
-        StringBuilder localeWithKeyword;
-        localeWithKeyword.append(locale);
-        localeWithKeyword.appendLiteral("@break=");
         switch (mode) {
-        case LineBreakIteratorModeUAX14:
-            ASSERT_NOT_REACHED();
-            break;
-        case LineBreakIteratorModeUAX14Loose:
-            localeWithKeyword.appendLiteral("loose");
-            break;
-        case LineBreakIteratorModeUAX14Normal:
-            localeWithKeyword.appendLiteral("normal");
-            break;
-        case LineBreakIteratorModeUAX14Strict:
-            localeWithKeyword.appendLiteral("strict");
-            break;
+        case LineBreakIteratorMode::Default:
+            return locale;
+        case LineBreakIteratorMode::Loose:
+            return makeString(locale, "@break=loose");
+        case LineBreakIteratorMode::Normal:
+            return makeString(locale, "@break=normal");
+        case LineBreakIteratorMode::Strict:
+            return makeString(locale, "@break=strict");
         }
-        return localeWithKeyword.toString();
+        ASSERT_NOT_REACHED();
+        return locale;
     }
 
     TextBreakIterator* take(const AtomicString& locale, LineBreakIteratorMode mode, bool isCJK)
     {
-        AtomicString localeWithOptionalBreakKeyword;
-        if (mode == LineBreakIteratorModeUAX14)
-            localeWithOptionalBreakKeyword = locale;
-        else
-            localeWithOptionalBreakKeyword = makeLocaleWithBreakKeyword(locale, mode);
+        auto localeWithOptionalBreakKeyword = makeLocaleWithBreakKeyword(locale, mode);
 
-        TextBreakIterator* iterator = 0;
+        TextBreakIterator* iterator = nullptr;
         for (size_t i = 0; i < m_pool.size(); ++i) {
             if (m_pool[i].first == localeWithOptionalBreakKeyword) {
                 iterator = m_pool[i].second;
@@ -91,37 +76,31 @@ public:
         if (!iterator) {
             iterator = openLineBreakIterator(localeWithOptionalBreakKeyword, mode, isCJK);
             if (!iterator)
-                return 0;
+                return nullptr;
         }
 
         ASSERT(!m_vendedIterators.contains(iterator));
-        m_vendedIterators.set(iterator, localeWithOptionalBreakKeyword);
+        m_vendedIterators.add(iterator, localeWithOptionalBreakKeyword);
         return iterator;
     }
 
     void put(TextBreakIterator* iterator)
     {
-        ASSERT_ARG(iterator, m_vendedIterators.contains(iterator));
-
+        ASSERT(m_vendedIterators.contains(iterator));
         if (m_pool.size() == capacity) {
             closeLineBreakIterator(m_pool[0].second);
             m_pool.remove(0);
         }
-
-        m_pool.append(Entry(m_vendedIterators.take(iterator), iterator));
+        m_pool.uncheckedAppend({ m_vendedIterators.take(iterator), iterator });
     }
 
 private:
-    static const size_t capacity = 4;
+    static constexpr size_t capacity = 4;
 
-    typedef std::pair<AtomicString, TextBreakIterator*> Entry;
-    typedef Vector<Entry, capacity> Pool;
-    Pool m_pool;
+    Vector<std::pair<AtomicString, TextBreakIterator*>, capacity> m_pool;
     HashMap<TextBreakIterator*, AtomicString> m_vendedIterators;
 
     friend WTF::ThreadSpecific<LineBreakIteratorPool>::operator LineBreakIteratorPool*();
 };
 
 }
-
-#endif
index fc70a77..a985777 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * (C) 1999 Lars Knoll (knoll@kde.org)
- * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2015 Apple Inc. All rights reserved.
+ * Copyright (C) 2004-2016 Apple Inc. All rights reserved.
  * Copyright (C) 2007-2009 Torch Mobile, Inc.
  *
  * This library is free software; you can redistribute it and/or
 #include "TextBreakIterator.h"
 
 #include "LineBreakIteratorPoolICU.h"
+#include "TextBreakIteratorInternalICU.h"
 #include "UTextProviderLatin1.h"
 #include "UTextProviderUTF16.h"
 #include <atomic>
 #include <mutex>
-#include <wtf/text/StringView.h>
+#include <unicode/ubrk.h>
+#include <wtf/text/StringBuilder.h>
 
 // FIXME: This needs a better name
 #define ADDITIONAL_EMOJI_SUPPORT (PLATFORM(IOS) || (PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100))
@@ -734,16 +736,16 @@ static String mapLineIteratorModeToRules(LineBreakIteratorMode mode, bool isCJK)
     rulesBuilder.append(uax14Prologue);
     rulesBuilder.append(uax14AssignmentsBefore);
     switch (mode) {
-    case LineBreakIteratorModeUAX14:
+    case LineBreakIteratorMode::Default:
         rulesBuilder.append(isCJK ? uax14AssignmentsCustomDefaultCJK : uax14AssignmentsCustomDefaultNonCJK);
         break;
-    case LineBreakIteratorModeUAX14Loose:
+    case LineBreakIteratorMode::Loose:
         rulesBuilder.append(isCJK ? uax14AssignmentsCustomLooseCJK : uax14AssignmentsCustomLooseNonCJK);
         break;
-    case LineBreakIteratorModeUAX14Normal:
+    case LineBreakIteratorMode::Normal:
         rulesBuilder.append(isCJK ? uax14AssignmentsCustomNormalCJK : uax14AssignmentsCustomNormalNonCJK);
         break;
-    case LineBreakIteratorModeUAX14Strict:
+    case LineBreakIteratorMode::Strict:
         rulesBuilder.append(isCJK ? uax14AssignmentsCustomStrictCJK : uax14AssignmentsCustomStrictNonCJK);
         break;
     }
@@ -783,7 +785,7 @@ TextBreakIterator* openLineBreakIterator(const AtomicString& locale, LineBreakIt
     UBreakIterator* ubrkIter;
     UErrorCode openStatus = U_ZERO_ERROR;
     bool localeIsEmpty = locale.isEmpty();
-    if (mode == LineBreakIteratorModeUAX14)
+    if (mode == LineBreakIteratorMode::Default)
         ubrkIter = ubrk_open(UBRK_LINE, localeIsEmpty ? currentTextBreakLocaleID() : locale.string().utf8().data(), 0, 0, &openStatus);
     else {
         UParseError parseStatus;
@@ -902,40 +904,56 @@ unsigned numGraphemeClusters(StringView string)
     if (!stringLength)
         return 0;
 
-    // The only Latin-1 Extended Grapheme Cluster is CR LF
-    if (string.is8Bit() && !string.contains('\r'))
-        return stringLength;
+    // The only Latin-1 Extended Grapheme Cluster is CRLF.
+    if (string.is8Bit()) {
+        auto* characters = string.characters8();
+        unsigned numCRLF = 0;
+        for (unsigned i = 1; i < stringLength; ++i)
+            numCRLF += characters[i - 1] == '\r' && characters[i] == '\n';
+        return stringLength - numCRLF;
+    }
 
-    NonSharedCharacterBreakIterator it(string);
-    if (!it)
+    NonSharedCharacterBreakIterator iterator { string };
+    if (!iterator) {
+        ASSERT_NOT_REACHED();
         return stringLength;
+    }
 
-    unsigned num = 0;
-    while (textBreakNext(it) != TextBreakDone)
-        ++num;
-    return num;
+    unsigned numGraphemeClusters = 0;
+    while (textBreakNext(iterator) != TextBreakDone)
+        ++numGraphemeClusters;
+    return numGraphemeClusters;
 }
 
-unsigned numCharactersInGraphemeClusters(const StringView& s, unsigned numGraphemeClusters)
+unsigned numCharactersInGraphemeClusters(StringView string, unsigned numGraphemeClusters)
 {
-    unsigned stringLength = s.length();
+    unsigned stringLength = string.length();
 
-    if (!stringLength)
-        return 0;
+    if (stringLength <= numGraphemeClusters)
+        return stringLength;
 
-    // The only Latin-1 Extended Grapheme Cluster is CR LF
-    if (s.is8Bit() && !s.contains('\r'))
-        return std::min(stringLength, numGraphemeClusters);
+    // The only Latin-1 Extended Grapheme Cluster is CRLF.
+    if (string.is8Bit()) {
+        auto* characters = string.characters8();
+        unsigned i, j;
+        for (i = 0, j = 0; i < numGraphemeClusters && j + 1 < stringLength; ++i, ++j)
+            j += characters[j] == '\r' && characters[j + 1] == '\n';
+        return j + (i < numGraphemeClusters && j < stringLength);
+    }
 
-    NonSharedCharacterBreakIterator it(s);
-    if (!it)
-        return std::min(stringLength, numGraphemeClusters);
+    NonSharedCharacterBreakIterator iterator { string };
+    if (!iterator) {
+        ASSERT_NOT_REACHED();
+        return stringLength;
+    }
 
     for (unsigned i = 0; i < numGraphemeClusters; ++i) {
-        if (textBreakNext(it) == TextBreakDone)
+        if (textBreakNext(iterator) == TextBreakDone) {
+            ASSERT_NOT_REACHED();
             return stringLength;
+        }
     }
-    return textBreakCurrent(it);
+    return textBreakCurrent(iterator);
 }
 
 } // namespace WTF
index d698771..8afb558 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2006 Lars Knoll <lars@trolltech.com>
- * Copyright (C) 2007, 2011, 2012 Apple Inc. All rights reserved.
+ * Copyright (C) 2007-2016 Apple Inc. All rights reserved.
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Library General Public
  *
  */
 
-#ifndef TextBreakIterator_h
-#define TextBreakIterator_h
+#pragma once
 
-#include <wtf/text/AtomicString.h>
 #include <wtf/text/StringView.h>
 
 namespace WTF {
@@ -31,12 +29,7 @@ class TextBreakIterator;
 
 // Note: The returned iterator is good only until you get another iterator, with the exception of acquireLineBreakIterator.
 
-enum LineBreakIteratorMode {
-    LineBreakIteratorModeUAX14,
-    LineBreakIteratorModeUAX14Loose,
-    LineBreakIteratorModeUAX14Normal,
-    LineBreakIteratorModeUAX14Strict,
-};
+enum class LineBreakIteratorMode { Default, Loose, Normal, Strict };
 
 // This is similar to character break iterator in most cases, but is subject to
 // platform UI conventions. One notable example where this can be different
@@ -62,32 +55,24 @@ WTF_EXPORT_PRIVATE int textBreakFollowing(TextBreakIterator*, int);
 WTF_EXPORT_PRIVATE bool isTextBreak(TextBreakIterator*, int);
 WTF_EXPORT_PRIVATE bool isWordTextBreak(TextBreakIterator*);
 
-const int TextBreakDone = -1;
+constexpr int TextBreakDone = -1;
 
 WTF_EXPORT_PRIVATE bool isCJKLocale(const AtomicString&);
 
 class LazyLineBreakIterator {
 public:
     LazyLineBreakIterator()
-        : m_iterator(nullptr)
-        , m_cachedPriorContext(nullptr)
-        , m_mode(LineBreakIteratorModeUAX14)
-        , m_cachedPriorContextLength(0)
-        , m_isCJK(false)
     {
         resetPriorContext();
     }
 
-    LazyLineBreakIterator(StringView stringView, const AtomicString& locale = AtomicString(), LineBreakIteratorMode mode = LineBreakIteratorModeUAX14)
+    explicit LazyLineBreakIterator(StringView stringView, const AtomicString& locale = AtomicString(), LineBreakIteratorMode mode = LineBreakIteratorMode::Default)
         : m_stringView(stringView)
         , m_locale(locale)
-        , m_iterator(nullptr)
-        , m_cachedPriorContext(nullptr)
         , m_mode(mode)
-        , m_cachedPriorContextLength(0)
+        , m_isCJK(isCJKLocale(locale))
     {
         resetPriorContext();
-        m_isCJK = isCJKLocale(locale);
     }
 
     ~LazyLineBreakIterator()
@@ -97,7 +82,7 @@ public:
     }
 
     StringView stringView() const { return m_stringView; }
-    bool isLooseCJKMode() const { return m_isCJK && m_mode == LineBreakIteratorModeUAX14Loose; }
+    bool isLooseCJKMode() const { return m_isCJK && m_mode == LineBreakIteratorMode::Loose; }
 
     UChar lastCharacter() const
     {
@@ -176,15 +161,15 @@ public:
     }
 
 private:
-    static const unsigned priorContextCapacity = 2;
+    static constexpr unsigned priorContextCapacity = 2;
     StringView m_stringView;
     AtomicString m_locale;
-    TextBreakIterator* m_iterator;
-    const UChar* m_cachedPriorContext;
-    LineBreakIteratorMode m_mode;
-    unsigned m_cachedPriorContextLength;
+    TextBreakIterator* m_iterator { nullptr };
+    const UChar* m_cachedPriorContext { nullptr };
+    LineBreakIteratorMode m_mode { LineBreakIteratorMode::Default };
+    unsigned m_cachedPriorContextLength { 0 };
     UChar m_priorContext[priorContextCapacity];
-    bool m_isCJK;
+    bool m_isCJK { false };
 };
 
 // Iterates over "extended grapheme clusters", as defined in UAX #29.
@@ -210,17 +195,15 @@ private:
 // of a non-combining character and following combining characters is
 // counted as 1 grapheme cluster.
 WTF_EXPORT_PRIVATE unsigned numGraphemeClusters(StringView);
+
 // Returns the number of characters which will be less than or equal to
 // the specified grapheme cluster length.
-WTF_EXPORT_PRIVATE unsigned numCharactersInGraphemeClusters(const StringView&, unsigned);
+WTF_EXPORT_PRIVATE unsigned numCharactersInGraphemeClusters(StringView, unsigned);
 
 }
 
-using WTF::LineBreakIteratorMode;
-using WTF::LineBreakIteratorModeUAX14;
 using WTF::LazyLineBreakIterator;
+using WTF::LineBreakIteratorMode;
 using WTF::NonSharedCharacterBreakIterator;
 using WTF::TextBreakDone;
 using WTF::TextBreakIterator;
-
-#endif
index 23a5664..f1d7aab 100644 (file)
@@ -1,3 +1,36 @@
+2016-11-22  Darin Adler  <darin@apple.com>
+
+        Make normal case fast in the input element limitString function
+        https://bugs.webkit.org/show_bug.cgi?id=165023
+
+        Reviewed by Dan Bernstein.
+
+        When running Speedometer, the limitLength function was showing up as hot.
+        Fixed a couple obvious problems with that function's performance.
+
+        * html/TextFieldInputType.cpp:
+        (WebCore::isASCIILineBreak): Deleted. The isHTMLLineBreak function does
+        the same thing, but faster.
+        (WebCore::limitLength): Added a FIXME comment explaining that the function
+        isn't really a good idea. Don't call through to numCharactersInGraphemeClusters
+        at all for 8-bit strings since we don't allow CR or LF characters in the string
+        anyway, so there are no grapheme clusters more than a single code unit. Removed
+        optimization when the length is the string's length that String::left already does.
+        (WebCore::TextFieldInputType::sanitizeValue): Use isHTMLLineBreak instead of
+        isASCIILineBreak.
+        (WebCore::TextFieldInputType::handleBeforeTextInsertedEvent): Ditto.
+
+        * platform/LocalizedStrings.cpp: Use auto a lot more rather than writing out
+        RetainPtr.
+        (WebCore::truncatedStringForLookupMenuItem): Removed unneeded special case for
+        empty strings. Removed unneeded string with the ellipsis character in it, since
+        the makeString function already knows how to append a character to a string.
+
+        * rendering/RenderText.cpp:
+        (WebCore::mapLineBreakToIteratorMode): Updated for change to LineBreakIteratorMode.
+        * rendering/SimpleLineLayoutTextFragmentIterator.cpp:
+        (WebCore::SimpleLineLayout::TextFragmentIterator::nextBreakablePosition): Ditto.
+
 2016-11-21  Sergio Villar Senin  <svillar@igalia.com>
 
         [css-grid] Isolate size of internal representation from actual grid size
index eb2a3c5..5b7d901 100644 (file)
@@ -42,6 +42,7 @@
 #include "FrameSelection.h"
 #include "HTMLInputElement.h"
 #include "HTMLNames.h"
+#include "HTMLParserIdioms.h"
 #include "KeyboardEvent.h"
 #include "LocalizedStrings.h"
 #include "NodeRenderStyle.h"
@@ -378,22 +379,23 @@ bool TextFieldInputType::shouldUseInputMethod() const
     return true;
 }
 
-static bool isASCIILineBreak(UChar c)
-{
-    return c == '\r' || c == '\n';
-}
-
-static String limitLength(const String& string, int maxLength)
-{
-    unsigned newLength = numCharactersInGraphemeClusters(string, maxLength);
-    for (unsigned i = 0; i < newLength; ++i) {
-        const UChar current = string[i];
-        if (current < ' ' && current != '\t') {
-            newLength = i;
-            break;
-        }
-    }
-    return newLength < string.length() ? string.left(newLength) : string;
+// FIXME: The name of this function doesn't make clear the two jobs it does:
+// 1) Limits the string to a particular number of grapheme clusters.
+// 2) Truncates the string at the first character which is a control character other than tab.
+// FIXME: TextFieldInputType::sanitizeValue doesn't need a limit on grapheme clusters. A limit on code units would do.
+// FIXME: Where does the "truncate at first control character" rule come from?
+static String limitLength(const String& string, unsigned maxNumGraphemeClusters)
+{
+    StringView stringView { string };
+    unsigned firstNonTabControlCharacterIndex = stringView.find([] (UChar character) {
+        return character < ' ' && character != '\t';
+    });
+    unsigned limitedLength;
+    if (stringView.is8Bit())
+        limitedLength = std::min(firstNonTabControlCharacterIndex, maxNumGraphemeClusters);
+    else
+        limitedLength = numCharactersInGraphemeClusters(stringView.substring(0, firstNonTabControlCharacterIndex), maxNumGraphemeClusters);
+    return string.left(limitedLength);
 }
 
 static String autoFillButtonTypeToAccessibilityLabel(AutoFillButtonType autoFillButtonType)
@@ -441,7 +443,7 @@ static bool isAutoFillButtonTypeChanged(const AtomicString& attribute, AutoFillB
 
 String TextFieldInputType::sanitizeValue(const String& proposedValue) const
 {
-    return limitLength(proposedValue.removeCharacters(isASCIILineBreak), HTMLInputElement::maxEffectiveLength);
+    return limitLength(proposedValue.removeCharacters(isHTMLLineBreak), HTMLInputElement::maxEffectiveLength);
 }
 
 void TextFieldInputType::handleBeforeTextInsertedEvent(BeforeTextInsertedEvent& event)
@@ -477,13 +479,12 @@ void TextFieldInputType::handleBeforeTextInsertedEvent(BeforeTextInsertedEvent&
     // Truncate the inserted text to avoid violating the maxLength and other constraints.
     String eventText = event.text();
     unsigned textLength = eventText.length();
-    while (textLength > 0 && isASCIILineBreak(eventText[textLength - 1]))
+    while (textLength > 0 && isHTMLLineBreak(eventText[textLength - 1]))
         textLength--;
     eventText.truncate(textLength);
     eventText.replace("\r\n", " ");
     eventText.replace('\r', ' ');
     eventText.replace('\n', ' ');
-
     event.setText(limitLength(eventText, appendableLength));
 }
 
index 3d7f75f..f7513a4 100644 (file)
@@ -59,7 +59,7 @@ static String formatLocalizedString(String format, ...)
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wformat-nonliteral"
 #endif
-    RetainPtr<CFStringRef> result = adoptCF(CFStringCreateWithFormatAndArguments(0, 0, format.createCFString().get(), arguments));
+    auto result = adoptCF(CFStringCreateWithFormatAndArguments(0, 0, format.createCFString().get(), arguments));
 #if COMPILER(CLANG)
 #pragma clang diagnostic pop
 #endif
@@ -73,19 +73,17 @@ static String formatLocalizedString(String format, ...)
 }
 
 #if ENABLE(CONTEXT_MENUS)
+
 static String truncatedStringForLookupMenuItem(const String& original)
 {
-    if (original.isEmpty())
-        return original;
-
-    // Truncate the string if it's too long. This is in consistency with AppKit.
+    // Truncate the string if it's too long. This number is roughly the same as the one used by AppKit.
     unsigned maxNumberOfGraphemeClustersInLookupMenuItem = 24;
-    static NeverDestroyed<String> ellipsis(&horizontalEllipsis, 1);
 
     String trimmed = original.stripWhiteSpace();
     unsigned numberOfCharacters = numCharactersInGraphemeClusters(trimmed, maxNumberOfGraphemeClustersInLookupMenuItem);
-    return numberOfCharacters == trimmed.length() ? trimmed : trimmed.left(numberOfCharacters) + ellipsis.get();
+    return numberOfCharacters == trimmed.length() ? trimmed : makeString(trimmed.left(numberOfCharacters), horizontalEllipsis);
 }
+
 #endif
 
 String inputElementAltText()
@@ -135,13 +133,16 @@ String defaultDetailsSummaryText()
 }
 
 #if PLATFORM(COCOA)
+
 String copyImageUnknownFileLabel()
 {
     return WEB_UI_STRING("unknown", "Unknown filename");
 }
+
 #endif
 
 #if ENABLE(CONTEXT_MENUS)
+
 String contextMenuItemTagOpenLinkInNewWindow()
 {
     return WEB_UI_STRING("Open Link in New Window", "Open in New Window context menu item");
@@ -237,7 +238,7 @@ String contextMenuItemTagSearchInSpotlight()
 String contextMenuItemTagSearchWeb()
 {
 #if PLATFORM(COCOA)
-    RetainPtr<CFStringRef> searchProviderName = adoptCF(wkCopyDefaultSearchProviderDisplayName());
+    auto searchProviderName = adoptCF(wkCopyDefaultSearchProviderDisplayName());
     return formatLocalizedString(WEB_UI_STRING("Search with %@", "Search with search provider context menu item with provider name inserted"), searchProviderName.get());
 #else
     return WEB_UI_STRING("Search with Google", "Search with Google context menu item");
@@ -247,7 +248,7 @@ String contextMenuItemTagSearchWeb()
 String contextMenuItemTagLookUpInDictionary(const String& selectedString)
 {
 #if USE(CF)
-    RetainPtr<CFStringRef> selectedCFString = truncatedStringForLookupMenuItem(selectedString).createCFString();
+    auto selectedCFString = truncatedStringForLookupMenuItem(selectedString).createCFString();
     return formatLocalizedString(WEB_UI_STRING("Look Up “%@”", "Look Up context menu item with selected word"), selectedCFString.get());
 #else
     return WEB_UI_STRING("Look Up “<selection>”", "Look Up context menu item with selected word").replace("<selection>", truncatedStringForLookupMenuItem(selectedString));
@@ -324,6 +325,7 @@ String contextMenuItemTagOutline()
 }
 
 #if PLATFORM(COCOA)
+
 String contextMenuItemTagStyles()
 {
     return WEB_UI_STRING("Styles...", "Styles context menu item");
@@ -348,6 +350,7 @@ String contextMenuItemTagStopSpeaking()
 {
     return WEB_UI_STRING("Stop Speaking", "Stop speaking context menu item");
 }
+
 #endif
 
 String contextMenuItemTagWritingDirectionMenu()
@@ -544,6 +547,7 @@ String contextMenuItemTagInspectElement()
 #endif // ENABLE(CONTEXT_MENUS)
 
 #if !PLATFORM(IOS)
+
 String searchMenuNoRecentSearchesText()
 {
     return WEB_UI_STRING("No recent searches", "Label for only item in menu that appears when clicking on the search field image, when no searches have been performed");
@@ -558,6 +562,7 @@ String searchMenuClearRecentSearchesText()
 {
     return WEB_UI_STRING("Clear Recent Searches", "menu item in Recent Searches menu that empties menu's contents");
 }
+
 #endif // !PLATFORM(IOS)
 
 String AXWebAreaText()
@@ -880,6 +885,7 @@ String keygenKeychainItemName(const String& host)
 #endif
 
 #if PLATFORM(IOS)
+
 String htmlSelectMultipleItems(size_t count)
 {
     switch (count) {
@@ -911,21 +917,22 @@ String fileButtonNoMediaFilesSelectedLabel()
 {
     return WEB_UI_STRING("no media selected (multiple)", "Text to display in file button used in HTML forms for media files when no media files are selected and the button allows multiple files to be selected");
 }
+
 #endif
 
 String imageTitle(const String& filename, const IntSize& size)
 {
 #if USE(CF)
-    RetainPtr<CFLocaleRef> locale = adoptCF(CFLocaleCopyCurrent());
-    RetainPtr<CFNumberFormatterRef> formatter = adoptCF(CFNumberFormatterCreate(0, locale.get(), kCFNumberFormatterDecimalStyle));
+    auto locale = adoptCF(CFLocaleCopyCurrent());
+    auto formatter = adoptCF(CFNumberFormatterCreate(0, locale.get(), kCFNumberFormatterDecimalStyle));
 
     int widthInt = size.width();
-    RetainPtr<CFNumberRef> width = adoptCF(CFNumberCreate(0, kCFNumberIntType, &widthInt));
-    RetainPtr<CFStringRef> widthString = adoptCF(CFNumberFormatterCreateStringWithNumber(0, formatter.get(), width.get()));
+    auto width = adoptCF(CFNumberCreate(0, kCFNumberIntType, &widthInt));
+    auto widthString = adoptCF(CFNumberFormatterCreateStringWithNumber(0, formatter.get(), width.get()));
 
     int heightInt = size.height();
-    RetainPtr<CFNumberRef> height = adoptCF(CFNumberCreate(0, kCFNumberIntType, &heightInt));
-    RetainPtr<CFStringRef> heightString = adoptCF(CFNumberFormatterCreateStringWithNumber(0, formatter.get(), height.get()));
+    auto height = adoptCF(CFNumberCreate(0, kCFNumberIntType, &heightInt));
+    auto heightString = adoptCF(CFNumberFormatterCreateStringWithNumber(0, formatter.get(), height.get()));
 
     return formatLocalizedString(WEB_UI_STRING("%@ %@×%@ pixels", "window title for a standalone image (uses multiplication symbol, not x)"), filename.createCFString().get(), widthString.get(), heightString.get());
 #else
@@ -1126,7 +1133,7 @@ String validationMessageTooLongText(int, int maxLength)
 
 String validationMessageRangeUnderflowText(const String& minimum)
 {
-#if PLATFORM(COCOA)
+#if USE(CF)
     return formatLocalizedString(WEB_UI_STRING("Value must be greater than or equal to %@", "Validation message for input form controls with value lower than allowed minimum"), minimum.createCFString().get());
 #else
     UNUSED_PARAM(minimum);
@@ -1136,7 +1143,7 @@ String validationMessageRangeUnderflowText(const String& minimum)
 
 String validationMessageRangeOverflowText(const String& maximum)
 {
-#if PLATFORM(COCOA)
+#if USE(CF)
     return formatLocalizedString(WEB_UI_STRING("Value must be less than or equal to %@", "Validation message for input form controls with value higher than allowed maximum"), maximum.createCFString().get());
 #else
     UNUSED_PARAM(maximum);
@@ -1160,6 +1167,7 @@ String clickToExitFullScreenText()
 }
 
 #if ENABLE(VIDEO_TRACK)
+
 String textTrackSubtitlesText()
 {
     return WEB_UI_STRING("Subtitles", "Menu section heading for subtitles");
@@ -1185,7 +1193,10 @@ String audioTrackNoLabelText()
     return WEB_UI_STRING_KEY("Unknown", "Unknown (audio track)", "Menu item label for an audio track that has no other name");
 }
 
-#if PLATFORM(COCOA) || PLATFORM(WIN)
+#endif
+
+#if ENABLE(VIDEO_TRACK) && USE(CF)
+
 String textTrackCountryAndLanguageMenuItemText(const String& title, const String& country, const String& language)
 {
     return formatLocalizedString(WEB_UI_STRING("%@ (%@-%@)", "Text track display name format that includes the country and language of the subtitle, in the form of 'Title (Language-Country)'"), title.createCFString().get(), language.createCFString().get(), country.createCFString().get());
@@ -1220,7 +1231,6 @@ String audioDescriptionTrackSuffixText(const String& title)
 {
     return formatLocalizedString(WEB_UI_STRING("%@ AD", "Text track contains Audio Descriptions"), title.createCFString().get());
 }
-#endif
 
 #endif
 
@@ -1240,6 +1250,7 @@ String useBlockedPlugInContextMenuTitle()
 }
 
 #if ENABLE(SUBTLE_CRYPTO)
+
 String webCryptoMasterKeyKeychainLabel(const String& localizedApplicationName)
 {
     return formatLocalizedString(WEB_UI_STRING("%@ WebCrypto Master Key", "Name of application's single WebCrypto master key in Keychain"), localizedApplicationName.createCFString().get());
@@ -1249,9 +1260,11 @@ String webCryptoMasterKeyKeychainComment()
 {
     return WEB_UI_STRING("Used to encrypt WebCrypto keys in persistent storage, such as IndexedDB", "Description of WebCrypto master keys in Keychain");
 }
+
 #endif
 
 #if PLATFORM(MAC)
+
 String insertListTypeNone()
 {
     return WEB_UI_STRING("None", "Option in segmented control for choosing list type in text editing");
@@ -1281,6 +1294,7 @@ String exitFullScreenButtonAccessibilityTitle()
 {
     return WEB_UI_STRING("Exit Fullscreen", "Button for exiting fullscreen when in fullscreen media playback");
 }
+
 #endif // PLATFORM(MAC)
 
 } // namespace WebCore
index 44a1c52..ce156ec 100644 (file)
@@ -705,15 +705,16 @@ LineBreakIteratorMode mapLineBreakToIteratorMode(LineBreak lineBreak)
     switch (lineBreak) {
     case LineBreakAuto:
     case LineBreakAfterWhiteSpace:
-        return LineBreakIteratorModeUAX14;
+        return LineBreakIteratorMode::Default;
     case LineBreakLoose:
-        return LineBreakIteratorModeUAX14Loose;
+        return LineBreakIteratorMode::Loose;
     case LineBreakNormal:
-        return LineBreakIteratorModeUAX14Normal;
+        return LineBreakIteratorMode::Normal;
     case LineBreakStrict:
-        return LineBreakIteratorModeUAX14Strict;
+        return LineBreakIteratorMode::Strict;
     }
-    return LineBreakIteratorModeUAX14;
+    ASSERT_NOT_REACHED();
+    return LineBreakIteratorMode::Default;
 }
 
 void RenderText::computePreferredLogicalWidths(float leadWidth)
index cba75bc..f26a337 100644 (file)
@@ -121,7 +121,7 @@ unsigned TextFragmentIterator::nextBreakablePosition(const FlowContents::Segment
         UChar lastCharacter = textLength > 0 ? currentText[textLength - 1] : 0;
         UChar secondToLastCharacter = textLength > 1 ? currentText[textLength - 2] : 0;
         m_lineBreakIterator.setPriorContext(lastCharacter, secondToLastCharacter);
-        m_lineBreakIterator.resetStringAndReleaseIterator(segment.text, m_style.locale, LineBreakIteratorModeUAX14);
+        m_lineBreakIterator.resetStringAndReleaseIterator(segment.text, m_style.locale, LineBreakIteratorMode::Default);
     }
     const auto* characters = segment.text.characters<CharacterType>();
     unsigned segmentLength = segment.end - segment.start;
index 93c88ec..7b01976 100644 (file)
@@ -1,3 +1,17 @@
+2016-11-22  Darin Adler  <darin@apple.com>
+
+        Make normal case fast in the input element limitString function
+        https://bugs.webkit.org/show_bug.cgi?id=165023
+
+        Reviewed by Dan Bernstein.
+
+        * TestWebKitAPI/CMakeLists.txt: Added TextBreakIterator.cpp.
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj: Ditto.
+        * TestWebKitAPI/Tests/WTF/TextBreakIterator.cpp: Added.
+        Contains some tests for the numGraphemeClusters and
+        numCharactersInGraphemeClusters functions that I used to make sure
+        that the new fast paths I added work correctly.
+
 2016-11-22  Tomas Popela  <tpopela@redhat.com>
 
         Unreviewed, add myself as a WebKit committer.
index 053c86d..6bed8df 100644 (file)
@@ -80,6 +80,7 @@ set(TestWTF_SOURCES
     ${TESTWEBKITAPI_DIR}/Tests/WTF/StringImpl.cpp
     ${TESTWEBKITAPI_DIR}/Tests/WTF/StringOperators.cpp
     ${TESTWEBKITAPI_DIR}/Tests/WTF/StringView.cpp
+    ${TESTWEBKITAPI_DIR}/Tests/WTF/TextBreakIterator.cpp
     ${TESTWEBKITAPI_DIR}/Tests/WTF/Time.cpp
     ${TESTWEBKITAPI_DIR}/Tests/WTF/UniqueRef.cpp
     ${TESTWEBKITAPI_DIR}/Tests/WTF/Variant.cpp
index 34d539a..217fa53 100644 (file)
                837A35F11D9A1E7D00663C57 /* DownloadRequestBlobURL.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 837A35F01D9A1E6400663C57 /* DownloadRequestBlobURL.html */; };
                83CF1C301C4F1B8B00688447 /* StringUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 83CF1C2C1C4F19AE00688447 /* StringUtilities.mm */; };
                930AD402150698D00067970F /* lots-of-text.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 930AD401150698B30067970F /* lots-of-text.html */; };
+               9329AA291DE3F81E003ABD07 /* TextBreakIterator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9329AA281DE3F81E003ABD07 /* TextBreakIterator.cpp */; };
                932AE53D1D371047005DFFAF /* focus-inputs.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 93575C551D30366E000D604D /* focus-inputs.html */; };
                9361002914DC95A70061379D /* lots-of-iframes.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 9361002814DC957B0061379D /* lots-of-iframes.html */; };
                93625D271CD9741C006DC1F1 /* large-video-without-audio.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 93625D261CD973AF006DC1F1 /* large-video-without-audio.html */; };
                8AA28C1916D2FA7B002FF4DB /* LoadPageOnCrash.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = LoadPageOnCrash.cpp; sourceTree = "<group>"; };
                8DD76FA10486AA7600D96B5E /* TestWebKitAPI */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = TestWebKitAPI; sourceTree = BUILT_PRODUCTS_DIR; };
                930AD401150698B30067970F /* lots-of-text.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "lots-of-text.html"; sourceTree = "<group>"; };
+               9329AA281DE3F81E003ABD07 /* TextBreakIterator.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextBreakIterator.cpp; sourceTree = "<group>"; };
                9331407B17B4419000F083B1 /* DidNotHandleKeyDown.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DidNotHandleKeyDown.cpp; sourceTree = "<group>"; };
                93575C551D30366E000D604D /* focus-inputs.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "focus-inputs.html"; sourceTree = "<group>"; };
                9361002814DC957B0061379D /* lots-of-iframes.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "lots-of-iframes.html"; sourceTree = "<group>"; };
                                7C74D42D188228F300E5ED57 /* StringView.cpp */,
                                5597F8341D9596C80066BC21 /* SynchronizedFixedQueue.cpp */,
                                0BCD85691485C98B00EA2003 /* SetForScope.cpp */,
+                               9329AA281DE3F81E003ABD07 /* TextBreakIterator.cpp */,
                                0F2C20B71DCD544800542D9E /* Time.cpp */,
                                5C5E633D1D0B67940085A025 /* UniqueRef.cpp */,
                                7CD0D5AA1D5534DE000CC9E1 /* Variant.cpp */,
                                7C83DED21D0A590C00FEBCF3 /* HashMap.cpp in Sources */,
                                7C83DED41D0A590C00FEBCF3 /* HashSet.cpp in Sources */,
                                7C83DEE01D0A590C00FEBCF3 /* IntegerToStringConversion.cpp in Sources */,
+                               9329AA291DE3F81E003ABD07 /* TextBreakIterator.cpp in Sources */,
                                1ADAD1501D77A9F600212586 /* BlockPtr.mm in Sources */,
                                7C83DEE81D0A590C00FEBCF3 /* ListHashSet.cpp in Sources */,
                                7C83DF1D1D0A590C00FEBCF3 /* Lock.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WTF/TextBreakIterator.cpp b/Tools/TestWebKitAPI/Tests/WTF/TextBreakIterator.cpp
new file mode 100644 (file)
index 0000000..c2f84e3
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include <wtf/text/TextBreakIterator.h>
+
+namespace TestWebKitAPI {
+
+static String makeUTF16(std::vector<UChar> input)
+{
+    return { input.data(), static_cast<unsigned>(input.size()) };
+}
+
+TEST(WTF, TextBreakIteratorNumGraphemeClusters)
+{
+    EXPECT_EQ(0U, numGraphemeClusters(StringView { }));
+    EXPECT_EQ(0U, numGraphemeClusters(StringView { "" }));
+    EXPECT_EQ(0U, numGraphemeClusters(makeUTF16({ })));
+
+    EXPECT_EQ(1U, numGraphemeClusters(StringView { "a" }));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ 'a' })));
+    EXPECT_EQ(1U, numGraphemeClusters(StringView { "\r\n" }));
+    EXPECT_EQ(1U, numGraphemeClusters(StringView { "\n" }));
+    EXPECT_EQ(1U, numGraphemeClusters(StringView { "\r" }));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ '\r', '\n' })));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ '\n' })));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ '\r' })));
+
+    EXPECT_EQ(2U, numGraphemeClusters(StringView { "\n\r" }));
+    EXPECT_EQ(2U, numGraphemeClusters(makeUTF16({ '\n', '\r' })));
+
+    EXPECT_EQ(2U, numGraphemeClusters(StringView { "\r\n\r" }));
+    EXPECT_EQ(2U, numGraphemeClusters(makeUTF16({ '\r', '\n', '\r' })));
+
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ 'g', 0x308 })));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ 0x1100, 0x1161, 0x11A8 })));
+    EXPECT_EQ(1U, numGraphemeClusters(makeUTF16({ 0x0BA8, 0x0BBF })));
+
+    EXPECT_EQ(2U, numGraphemeClusters(makeUTF16({ 0x308, 'g' })));
+
+    EXPECT_EQ(3U, numGraphemeClusters(StringView { "\r\nbc" }));
+    EXPECT_EQ(3U, numGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' })));
+}
+
+TEST(WTF, TextBreakIteratorNumCharactersInGraphemeClusters)
+{
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { }, 0));
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { }, 1));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "" }, 0));
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "" }, 1));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ }), 0));
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ }), 1));
+
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(StringView { "a" }, 1));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ 'a' }), 1));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(StringView { "\n" }, 1));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(StringView { "\r" }, 1));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ '\n' }), 1));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ '\r' }), 1));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "abc" }, 0));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(StringView { "abc" }, 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "abc" }, 2));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(StringView { "abc" }, 3));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(StringView { "abc" }, 4));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ 'a', 'b', 'c' }), 0));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ 'a', 'b', 'c' }), 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ 'a', 'b', 'c' }), 2));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ 'a', 'b', 'c' }), 3));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ 'a', 'b', 'c' }), 4));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "\r\n" }, 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\r\n" }, 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\r\n" }, 2));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\r\n" }, 3));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n' }), 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n' }), 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n' }), 2));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n' }), 3));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "\n\r" }, 0));
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(StringView { "\n\r" }, 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\n\r" }, 2));
+
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ '\n', '\r' }), 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ '\n', '\r' }), 2));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "\r\n\r" }, 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\r\n\r" }, 1));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(StringView { "\r\n\r" }, 2));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(StringView { "\r\n\r" }, 3));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n', '\r' }), 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n', '\r' }), 1));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n', '\r' }), 2));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ '\r', '\n', '\r' }), 3));
+
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308 }), 1));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ 0x1100, 0x1161, 0x11A8 }), 1));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ 0x0BA8, 0x0BBF }), 1));
+
+    EXPECT_EQ(1U, numCharactersInGraphemeClusters(makeUTF16({ 0x308, 'g' }), 1));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 1));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 2));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 3));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 4));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(StringView { "\r\nbc" }, 5));
+
+    EXPECT_EQ(0U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 0));
+    EXPECT_EQ(2U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 1));
+    EXPECT_EQ(3U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 2));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 3));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 4));
+    EXPECT_EQ(4U, numCharactersInGraphemeClusters(makeUTF16({ 'g', 0x308, 'b', 'c' }), 5));
+}
+
+} // namespace TestWebKitAPI