[GTK] Add support for automatic hyphenation
[WebKit-https.git] / Source / WebCore / platform / text / gtk / HyphenationLibHyphen.cpp
1 /*
2  * Copyright (C) 2010 Apple Inc. All rights reserved.
3  * Copyright (C) 2015 Igalia S.L.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
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  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 #include "config.h"
28 #include "Hyphenation.h"
29
30 #if USE(LIBHYPHEN)
31
32 #include "AtomicStringKeyedMRUCache.h"
33 #include "FileSystem.h"
34 #include "GtkUtilities.h"
35 #include <hyphen.h>
36 #include <wtf/HashMap.h>
37 #include <wtf/NeverDestroyed.h>
38 #include <wtf/gobject/GUniquePtr.h>
39 #include <wtf/text/AtomicStringHash.h>
40 #include <wtf/text/CString.h>
41 #include <wtf/text/StringView.h>
42
43 namespace WebCore {
44
45 static const char* const gDictionaryDirectories[] = {
46     "/usr/share/hyphen",
47     "/usr/local/share/hyphen",
48 };
49
50 static String extractLocaleFromDictionaryFilePath(const String& filePath)
51 {
52     // Dictionary files always have the form "hyph_<locale name>.dic"
53     // so we strip everything except the locale.
54     String fileName = pathGetFileName(filePath);
55     static const int prefixLength = 5;
56     static const int suffixLength = 4;
57     return fileName.substring(prefixLength, fileName.length() - prefixLength - suffixLength);
58 }
59
60 static void scanDirectoryForDicionaries(const char* directoryPath, HashMap<AtomicString, String>& availableLocales)
61 {
62     for (const auto& filePath : listDirectory(directoryPath, "hyph_*.dic"))
63         availableLocales.set(AtomicString(extractLocaleFromDictionaryFilePath(filePath)), filePath);
64 }
65
66 #if defined(DEVELOPMENT_BUILD)
67 static void scanTestDictionariesDirectoryIfNecessary(HashMap<AtomicString, String>& availableLocales)
68 {
69     // It's unfortunate that we need to look for the dictionaries this way, but
70     // libhyphen doesn't have the concept of installed dictionaries. Instead,
71     // we have this special case for WebKit tests.
72     CString buildDirectory = webkitBuildDirectory();
73     GUniquePtr<char> dictionariesPath(g_build_filename(buildDirectory.data(), "DependenciesGTK", "Root", "webkitgtk-test-dicts", nullptr));
74     if (g_file_test(dictionariesPath.get(), static_cast<GFileTest>(G_FILE_TEST_IS_DIR))) {
75         scanDirectoryForDicionaries(dictionariesPath.get(), availableLocales);
76         return;
77     }
78
79     // Try alternative dictionaries path for people not using JHBuild.
80     dictionariesPath.reset(g_build_filename(buildDirectory.data(), "webkitgtk-test-dicts", nullptr));
81     scanDirectoryForDicionaries(dictionariesPath.get(), availableLocales);
82 }
83 #endif
84
85 static HashMap<AtomicString, String>& availableLocales()
86 {
87     static bool scannedLocales = false;
88     static HashMap<AtomicString, String> availableLocales;
89
90     if (!scannedLocales) {
91         for (size_t i = 0; i < WTF_ARRAY_LENGTH(gDictionaryDirectories); i++)
92             scanDirectoryForDicionaries(gDictionaryDirectories[i], availableLocales);
93
94 #if defined(DEVELOPMENT_BUILD)
95         scanTestDictionariesDirectoryIfNecessary(availableLocales);
96 #endif
97
98         scannedLocales = true;
99     }
100
101     return availableLocales;
102 }
103
104 bool canHyphenate(const AtomicString& localeIdentifier)
105 {
106     if (localeIdentifier.isNull())
107         return false;
108     return availableLocales().contains(localeIdentifier);
109 }
110
111 class HyphenationDictionary : public RefCounted<HyphenationDictionary> {
112     WTF_MAKE_NONCOPYABLE(HyphenationDictionary);
113     WTF_MAKE_FAST_ALLOCATED;
114 public:
115     typedef std::unique_ptr<HyphenDict, void(*)(HyphenDict*)> HyphenDictUniquePtr;
116
117     virtual ~HyphenationDictionary() { }
118     static RefPtr<HyphenationDictionary> createNull()
119     {
120         return adoptRef(new HyphenationDictionary());
121     }
122
123     static RefPtr<HyphenationDictionary> create(const CString& dictPath)
124     {
125         return adoptRef(new HyphenationDictionary(dictPath));
126     }
127
128     HyphenDict* libhyphenDictionary() const
129     {
130         return m_libhyphenDictionary.get();
131     }
132
133 private:
134     HyphenationDictionary(const CString& dictPath)
135         : m_libhyphenDictionary(HyphenDictUniquePtr(hnj_hyphen_load(dictPath.data()), hnj_hyphen_free))
136     {
137     }
138
139     HyphenationDictionary()
140         : m_libhyphenDictionary(HyphenDictUniquePtr(nullptr, hnj_hyphen_free))
141     {
142     }
143
144     HyphenDictUniquePtr m_libhyphenDictionary;
145 };
146
147 template<>
148 RefPtr<HyphenationDictionary> AtomicStringKeyedMRUCache<RefPtr<HyphenationDictionary>>::createValueForNullKey()
149 {
150     return HyphenationDictionary::createNull();
151 }
152
153 template<>
154 RefPtr<HyphenationDictionary> AtomicStringKeyedMRUCache<RefPtr<HyphenationDictionary>>::createValueForKey(const AtomicString& localeIdentifier)
155 {
156     ASSERT(availableLocales().get(localeIdentifier));
157     return HyphenationDictionary::create(fileSystemRepresentation(availableLocales().get(localeIdentifier)));
158 }
159
160 static AtomicStringKeyedMRUCache<RefPtr<HyphenationDictionary>>& hyphenDictionaryCache()
161 {
162     static NeverDestroyed<AtomicStringKeyedMRUCache<RefPtr<HyphenationDictionary>>> cache;
163     return cache;
164 }
165
166 static void countLeadingSpaces(const CString& utf8String, int32_t& pointerOffset, int32_t& characterOffset)
167 {
168     pointerOffset = 0;
169     characterOffset = 0;
170     const char* stringData = utf8String.data();
171     UChar32 character = 0;
172     while (static_cast<unsigned>(pointerOffset) < utf8String.length()) {
173         int32_t nextPointerOffset = pointerOffset;
174         U8_NEXT(stringData, nextPointerOffset, static_cast<int32_t>(utf8String.length()), character);
175
176         if (character < 0 || !u_isUWhiteSpace(character))
177             return;
178
179         pointerOffset = nextPointerOffset;
180         characterOffset++;
181     }
182 }
183
184 size_t lastHyphenLocation(StringView string, size_t beforeIndex, const AtomicString& localeIdentifier)
185 {
186     ASSERT(availableLocales().contains(localeIdentifier));
187     RefPtr<HyphenationDictionary> dictionary = hyphenDictionaryCache().get(localeIdentifier);
188
189     // libhyphen accepts strings in UTF-8 format, but WebCore can only provide StringView
190     // which stores either UTF-16 or Latin1 data. This is unfortunate for performance
191     // reasons and we should consider switching to a more flexible hyphenation library
192     // if it is available.
193     CString utf8StringCopy = string.toStringWithoutCopying().utf8();
194
195     // WebCore often passes strings like " wordtohyphenate" to the platform layer. Since
196     // libhyphen isn't advanced enough to deal with leading spaces (presumably CoreFoundation
197     // can), we should find the appropriate indexes into the string to skip them.
198     int32_t leadingSpaceBytes;
199     int32_t leadingSpaceCharacters;
200     countLeadingSpaces(utf8StringCopy, leadingSpaceBytes, leadingSpaceCharacters);
201
202     // The libhyphen documentation specifies that this array should be 5 bytes longer than
203     // the byte length of the input string.
204     Vector<char> hyphenArray(utf8StringCopy.length() - leadingSpaceBytes + 5);
205     char* hyphenArrayData = hyphenArray.data();
206
207     char** replacements = nullptr;
208     int* positions = nullptr;
209     int* removedCharacterCounts = nullptr;
210     hnj_hyphen_hyphenate2(dictionary->libhyphenDictionary(),
211         utf8StringCopy.data() + leadingSpaceBytes,
212         utf8StringCopy.length() - leadingSpaceBytes,
213         hyphenArrayData,
214         nullptr, /* output parameter for hyphenated word */
215         &replacements,
216         &positions,
217         &removedCharacterCounts);
218
219     if (replacements) {
220         for (unsigned i = 0; i < utf8StringCopy.length() - leadingSpaceBytes - 1; i++)
221             free(replacements[i]);
222         free(replacements);
223     }
224
225     free(positions);
226     free(removedCharacterCounts);
227
228     for (int i = beforeIndex - leadingSpaceCharacters - 1; i >= 0; i--) {
229         // libhyphen will put an odd number in hyphenArrayData at all
230         // hyphenation points. A number & 1 will be true for odd numbers.
231         if (hyphenArrayData[i] & 1)
232             return i + leadingSpaceCharacters;
233     }
234
235     return 0;
236 }
237
238 } // namespace WebCore
239
240 #endif // USE(LIBHYPHEN)