TextBreakIteratorICU.h   [plain text]


/*
 * Copyright (C) 2017 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
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#pragma once

#include <unicode/ubrk.h>
#include <wtf/Optional.h>
#include <wtf/text/icu/UTextProviderLatin1.h>

#define USE_ICU_CARET_ITERATOR (PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101200)

namespace WTF {

#if USE_ICU_CARET_ITERATOR
static String caretRules()
{
    static StaticStringImpl caretRuleString(
        // This rule set is based on character-break iterator rules of ICU 57
        // <http://source.icu-project.org/repos/icu/icu/tags/release-57-1/source/data/brkitr/>.
        // The major differences from the original ones are listed below:
        // * Replaced '[\p{Grapheme_Cluster_Break = SpacingMark}]' with '[\p{General_Category = Spacing Mark} - $Extend]' for ICU 3.8 or earlier;
        // * Removed rules that prevent a caret from moving after prepend characters (Bug 24342);
        // * Added rules that prevent a caret from moving after virama signs of Indic languages except Tamil (Bug 15790), and;
        // * Added rules that prevent a caret from moving before Japanese half-width katakara voiced marks.
        // * Added rules for regional indicator symbols.
        "$CR      = [\\p{Grapheme_Cluster_Break = CR}];"
        "$LF      = [\\p{Grapheme_Cluster_Break = LF}];"
        "$Control = [\\p{Grapheme_Cluster_Break = Control}];"
        "$VoiceMarks = [\\uFF9E\\uFF9F];" // Japanese half-width katakana voiced marks
        "$Extend  = [\\p{Grapheme_Cluster_Break = Extend} $VoiceMarks - [\\u0E30 \\u0E32 \\u0E45 \\u0EB0 \\u0EB2]];"
        "$SpacingMark = [[\\p{General_Category = Spacing Mark}] - $Extend];"
        "$L       = [\\p{Grapheme_Cluster_Break = L}];"
        "$V       = [\\p{Grapheme_Cluster_Break = V}];"
        "$T       = [\\p{Grapheme_Cluster_Break = T}];"
        "$LV      = [\\p{Grapheme_Cluster_Break = LV}];"
        "$LVT     = [\\p{Grapheme_Cluster_Break = LVT}];"
        "$Hin0    = [\\u0905-\\u0939];" // Devanagari Letter A,...,Ha
        "$HinV    = \\u094D;" // Devanagari Sign Virama
        "$Hin1    = [\\u0915-\\u0939];" // Devanagari Letter Ka,...,Ha
        "$Ben0    = [\\u0985-\\u09B9];" // Bengali Letter A,...,Ha
        "$BenV    = \\u09CD;" // Bengali Sign Virama
        "$Ben1    = [\\u0995-\\u09B9];" // Bengali Letter Ka,...,Ha
        "$Pan0    = [\\u0A05-\\u0A39];" // Gurmukhi Letter A,...,Ha
        "$PanV    = \\u0A4D;" // Gurmukhi Sign Virama
        "$Pan1    = [\\u0A15-\\u0A39];" // Gurmukhi Letter Ka,...,Ha
        "$Guj0    = [\\u0A85-\\u0AB9];" // Gujarati Letter A,...,Ha
        "$GujV    = \\u0ACD;" // Gujarati Sign Virama
        "$Guj1    = [\\u0A95-\\u0AB9];" // Gujarati Letter Ka,...,Ha
        "$Ori0    = [\\u0B05-\\u0B39];" // Oriya Letter A,...,Ha
        "$OriV    = \\u0B4D;" // Oriya Sign Virama
        "$Ori1    = [\\u0B15-\\u0B39];" // Oriya Letter Ka,...,Ha
        "$Tel0    = [\\u0C05-\\u0C39];" // Telugu Letter A,...,Ha
        "$TelV    = \\u0C4D;" // Telugu Sign Virama
        "$Tel1    = [\\u0C14-\\u0C39];" // Telugu Letter Ka,...,Ha
        "$Kan0    = [\\u0C85-\\u0CB9];" // Kannada Letter A,...,Ha
        "$KanV    = \\u0CCD;" // Kannada Sign Virama
        "$Kan1    = [\\u0C95-\\u0CB9];" // Kannada Letter A,...,Ha
        "$Mal0    = [\\u0D05-\\u0D39];" // Malayalam Letter A,...,Ha
        "$MalV    = \\u0D4D;" // Malayalam Sign Virama
        "$Mal1    = [\\u0D15-\\u0D39];" // Malayalam Letter A,...,Ha
        "$RI      = [\\U0001F1E6-\\U0001F1FF];" // Emoji regional indicators
        "$ZWJ     = \\u200D;" // Zero width joiner
        "$EmojiVar = [\\uFE0F];" // Emoji-style variation selector
        "$EmojiForSeqs = [\\u2640 \\u2642 \\u26F9 \\u2764 \\U0001F308 \\U0001F3C3-\\U0001F3C4 \\U0001F3CA-\\U0001F3CC \\U0001F3F3 \\U0001F441 \\U0001F466-\\U0001F469 \\U0001F46E-\\U0001F46F \\U0001F471 \\U0001F473 \\U0001F477 \\U0001F481-\\U0001F482 \\U0001F486-\\U0001F487 \\U0001F48B \\U0001F575 \\U0001F5E8 \\U0001F645-\\U0001F647 \\U0001F64B \\U0001F64D-\\U0001F64E \\U0001F6A3 \\U0001F6B4-\\U0001F6B6 \\u2695-\\u2696 \\u2708 \\U0001F33E \\U0001F373 \\U0001F393 \\U0001F3A4 \\U0001F3A8 \\U0001F3EB \\U0001F3ED \\U0001F4BB-\\U0001F4BC \\U0001F527 \\U0001F52C \\U0001F680 \\U0001F692 \\U0001F926 \\U0001F937-\\U0001F939 \\U0001F93C-\\U0001F93E];" // Emoji that participate in ZWJ sequences
        "$EmojiForMods = [\\u261D \\u26F9 \\u270A-\\u270D \\U0001F385 \\U0001F3C3-\\U0001F3C4 \\U0001F3CA \\U0001F3CB \\U0001F442-\\U0001F443 \\U0001F446-\\U0001F450 \\U0001F466-\\U0001F478 \\U0001F47C \\U0001F481-\\U0001F483 \\U0001F485-\\U0001F487 \\U0001F4AA \\U0001F575 \\U0001F590 \\U0001F595 \\U0001F596 \\U0001F645-\\U0001F647 \\U0001F64B-\\U0001F64F \\U0001F6A3 \\U0001F6B4-\\U0001F6B6 \\U0001F6C0 \\U0001F918 \\U0001F3C2 \\U0001F3C7 \\U0001F3CC \\U0001F574 \\U0001F57A \\U0001F6CC \\U0001F919-\\U0001F91E \\U0001F926 \\U0001F930 \\U0001F933-\\U0001F939 \\U0001F93C-\\U0001F93E] ;" // Emoji that take Fitzpatrick modifiers
        "$EmojiMods = [\\U0001F3FB-\\U0001F3FF];" // Fitzpatrick modifiers
        "!!chain;"
        "!!RINoChain;"
        "!!forward;"
        "$CR $LF;"
        "$L ($L | $V | $LV | $LVT);"
        "($LV | $V) ($V | $T);"
        "($LVT | $T) $T;"
        "$RI $RI $Extend* / $RI;"
        "$RI $RI $Extend*;"
        "[^$Control $CR $LF] $Extend;"
        "[^$Control $CR $LF] $SpacingMark;"
        "$Hin0 $HinV $Hin1;" // Devanagari Virama (forward)
        "$Ben0 $BenV $Ben1;" // Bengali Virama (forward)
        "$Pan0 $PanV $Pan1;" // Gurmukhi Virama (forward)
        "$Guj0 $GujV $Guj1;" // Gujarati Virama (forward)
        "$Ori0 $OriV $Ori1;" // Oriya Virama (forward)
        "$Tel0 $TelV $Tel1;" // Telugu Virama (forward)
        "$Kan0 $KanV $Kan1;" // Kannada Virama (forward)
        "$Mal0 $MalV $Mal1;" // Malayalam Virama (forward)
        "$ZWJ $EmojiForSeqs;" // Don't break in emoji ZWJ sequences
        "$EmojiForMods $EmojiVar? $EmojiMods;" // Don't break between relevant emoji (possibly with variation selector) and Fitzpatrick modifier
        "!!reverse;"
        "$LF $CR;"
        "($L | $V | $LV | $LVT) $L;"
        "($V | $T) ($LV | $V);"
        "$T ($LVT | $T);"
        "$Extend* $RI $RI / $Extend* $RI $RI;"
        "$Extend* $RI $RI;"
        "$Extend      [^$Control $CR $LF];"
        "$SpacingMark [^$Control $CR $LF];"
        "$Hin1 $HinV $Hin0;" // Devanagari Virama (backward)
        "$Ben1 $BenV $Ben0;" // Bengali Virama (backward)
        "$Pan1 $PanV $Pan0;" // Gurmukhi Virama (backward)
        "$Guj1 $GujV $Guj0;" // Gujarati Virama (backward)
        "$Ori1 $OriV $Ori0;" // Gujarati Virama (backward)
        "$Tel1 $TelV $Tel0;" // Telugu Virama (backward)
        "$Kan1 $KanV $Kan0;" // Kannada Virama (backward)
        "$Mal1 $MalV $Mal0;" // Malayalam Virama (backward)
        "$EmojiForSeqs $ZWJ;" // Don't break in emoji ZWJ sequences
        "$EmojiMods $EmojiVar? $EmojiForMods;" // Don't break between relevant emoji (possibly with variation selector) and Fitzpatrick modifier
        "!!safe_reverse;"
        "$RI $RI+;"
        "[$EmojiVar $EmojiMods]+ $EmojiForMods;"
        "!!safe_forward;"
        "$RI $RI+;"
        "$EmojiForMods [$EmojiVar $EmojiMods]+;"
    );
    return caretRuleString;
}
#endif

class TextBreakIteratorICU {
public:
    enum class Mode {
        Line,
        Character,
#if USE_ICU_CARET_ITERATOR
        Caret,
#endif
    };

    void set8BitText(const LChar* buffer, unsigned length)
    {
        UTextWithBuffer textLocal;
        textLocal.text = UTEXT_INITIALIZER;
        textLocal.text.extraSize = sizeof(textLocal.buffer);
        textLocal.text.pExtra = textLocal.buffer;

        UErrorCode status = U_ZERO_ERROR;
        UText* text = openLatin1UTextProvider(&textLocal, buffer, length, &status);
        ASSERT(U_SUCCESS(status));
        ASSERT(text);

        ubrk_setUText(m_iterator, text, &status);
        ASSERT(U_SUCCESS(status));

        utext_close(text);
    }

    TextBreakIteratorICU(StringView string, Mode mode, const char *locale)
    {
        UBreakIteratorType type;
        switch (mode) {
        case Mode::Line:
            type = UBRK_LINE;
            break;
        case Mode::Character:
            type = UBRK_CHARACTER;
            break;
#if USE_ICU_CARET_ITERATOR
        case Mode::Caret:
            type = UBRK_CHARACTER;
            break;
#endif
        default:
            ASSERT_NOT_REACHED();
            type = UBRK_CHARACTER;
            break;
        }

        bool requiresSet8BitText = string.is8Bit();

        const UChar *text = requiresSet8BitText ? nullptr : string.characters16();
        int32_t textLength = requiresSet8BitText ? 0 : string.length();

        // FIXME: Handle weak / normal / strict line breaking.
        UErrorCode status = U_ZERO_ERROR;
#if USE_ICU_CARET_ITERATOR
        if (mode == Mode::Caret) {
            static NeverDestroyed<String> caretRules = WTF::caretRules();
            static NeverDestroyed<StringView::UpconvertedCharacters> upconvertedRules = StringView(caretRules).upconvertedCharacters();
            UParseError parseError;
            m_iterator = ubrk_openRules(upconvertedRules.get(), caretRules.get().length(), text, textLength, &parseError, &status);
        } else
#endif
            m_iterator = ubrk_open(type, locale, text, textLength, &status);
        ASSERT(U_SUCCESS(status));

        if (requiresSet8BitText)
            set8BitText(string.characters8(), string.length());
    }

    TextBreakIteratorICU() = delete;
    TextBreakIteratorICU(const TextBreakIteratorICU&) = delete;

    TextBreakIteratorICU(TextBreakIteratorICU&& other)
        : m_iterator(other.m_iterator)
    {
        other.m_iterator = nullptr;
    }

    TextBreakIteratorICU& operator=(const TextBreakIteratorICU&) = delete;

    TextBreakIteratorICU& operator=(TextBreakIteratorICU&& other)
    {
        if (m_iterator)
            ubrk_close(m_iterator);
        m_iterator = other.m_iterator;
        other.m_iterator = nullptr;
        return *this;
    }

    ~TextBreakIteratorICU()
    {
        if (m_iterator)
            ubrk_close(m_iterator);
    }

    void setText(StringView string)
    {
        if (string.is8Bit()) {
            set8BitText(string.characters8(), string.length());
            return;
        }
        UErrorCode status = U_ZERO_ERROR;
        ubrk_setText(m_iterator, string.characters16(), string.length(), &status);
        ASSERT(U_SUCCESS(status));
    }

    std::optional<unsigned> preceding(unsigned location) const
    {
        auto result = ubrk_preceding(m_iterator, location);
        if (result == UBRK_DONE)
            return { };
        return result;
    }

    std::optional<unsigned> following(unsigned location) const
    {
        auto result = ubrk_following(m_iterator, location);
        if (result == UBRK_DONE)
            return { };
        return result;
    }

    bool isBoundary(unsigned location) const
    {
        return ubrk_isBoundary(m_iterator, location);
    }

private:
    UBreakIterator* m_iterator;
};

}