9861d2ece17018c8c80fd0a65059e2bd403cf84e
[WebKit-https.git] / Source / JavaScriptCore / runtime / IntlCollator.cpp
1 /*
2  * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com)
3  * Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com)
4  * Copyright (C) 2016 Apple Inc. All Rights Reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
16  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
19  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
25  * THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 #include "config.h"
29 #include "IntlCollator.h"
30
31 #if ENABLE(INTL)
32
33 #include "Error.h"
34 #include "IntlCollatorConstructor.h"
35 #include "IntlObject.h"
36 #include "JSBoundFunction.h"
37 #include "JSCInlines.h"
38 #include "ObjectConstructor.h"
39 #include "SlotVisitorInlines.h"
40 #include "StructureInlines.h"
41 #include <unicode/ucol.h>
42 #include <wtf/unicode/Collator.h>
43
44 namespace JSC {
45
46 const ClassInfo IntlCollator::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlCollator) };
47
48 // FIXME: Implement kf (caseFirst).
49 static const char* const relevantExtensionKeys[2] = { "co", "kn" };
50 static const size_t indexOfExtensionKeyCo = 0;
51 static const size_t indexOfExtensionKeyKn = 1;
52
53 void IntlCollator::UCollatorDeleter::operator()(UCollator* collator) const
54 {
55     if (collator)
56         ucol_close(collator);
57 }
58
59 IntlCollator* IntlCollator::create(VM& vm, Structure* structure)
60 {
61     IntlCollator* format = new (NotNull, allocateCell<IntlCollator>(vm.heap)) IntlCollator(vm, structure);
62     format->finishCreation(vm);
63     return format;
64 }
65
66 Structure* IntlCollator::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
67 {
68     return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
69 }
70
71 IntlCollator::IntlCollator(VM& vm, Structure* structure)
72     : JSDestructibleObject(vm, structure)
73 {
74 }
75
76 void IntlCollator::finishCreation(VM& vm)
77 {
78     Base::finishCreation(vm);
79     ASSERT(inherits(info()));
80 }
81
82 void IntlCollator::destroy(JSCell* cell)
83 {
84     static_cast<IntlCollator*>(cell)->IntlCollator::~IntlCollator();
85 }
86
87 void IntlCollator::visitChildren(JSCell* cell, SlotVisitor& visitor)
88 {
89     IntlCollator* thisObject = jsCast<IntlCollator*>(cell);
90     ASSERT_GC_OBJECT_INHERITS(thisObject, info());
91
92     Base::visitChildren(thisObject, visitor);
93
94     visitor.append(&thisObject->m_boundCompare);
95 }
96
97 static Vector<String> sortLocaleData(const String& locale, size_t keyIndex)
98 {
99     // 9.1 Internal slots of Service Constructors & 10.2.3 Internal slots (ECMA-402 2.0)
100     Vector<String> keyLocaleData;
101     switch (keyIndex) {
102     case indexOfExtensionKeyCo: {
103         // 10.2.3 "The first element of [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co must be null for all locale values."
104         keyLocaleData.append({ });
105
106         UErrorCode status = U_ZERO_ERROR;
107         UEnumeration* enumeration = ucol_getKeywordValuesForLocale("collation", locale.utf8().data(), false, &status);
108         if (U_SUCCESS(status)) {
109             const char* collation;
110             while ((collation = uenum_next(enumeration, nullptr, &status)) && U_SUCCESS(status)) {
111                 // 10.2.3 "The values "standard" and "search" must not be used as elements in any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co array."
112                 if (!strcmp(collation, "standard") || !strcmp(collation, "search"))
113                     continue;
114
115                 // Map keyword values to BCP 47 equivalents.
116                 if (!strcmp(collation, "dictionary"))
117                     collation = "dict";
118                 else if (!strcmp(collation, "gb2312han"))
119                     collation = "gb2312";
120                 else if (!strcmp(collation, "phonebook"))
121                     collation = "phonebk";
122                 else if (!strcmp(collation, "traditional"))
123                     collation = "trad";
124
125                 keyLocaleData.append(collation);
126             }
127             uenum_close(enumeration);
128         }
129         break;
130     }
131     case indexOfExtensionKeyKn:
132         keyLocaleData.reserveInitialCapacity(2);
133         keyLocaleData.uncheckedAppend(ASCIILiteral("false"));
134         keyLocaleData.uncheckedAppend(ASCIILiteral("true"));
135         break;
136     default:
137         ASSERT_NOT_REACHED();
138     }
139     return keyLocaleData;
140 }
141
142 static Vector<String> searchLocaleData(const String&, size_t keyIndex)
143 {
144     // 9.1 Internal slots of Service Constructors & 10.2.3 Internal slots (ECMA-402 2.0)
145     Vector<String> keyLocaleData;
146     switch (keyIndex) {
147     case indexOfExtensionKeyCo:
148         // 10.2.3 "The first element of [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co must be null for all locale values."
149         keyLocaleData.reserveInitialCapacity(1);
150         keyLocaleData.append({ });
151         break;
152     case indexOfExtensionKeyKn:
153         keyLocaleData.reserveInitialCapacity(2);
154         keyLocaleData.uncheckedAppend(ASCIILiteral("false"));
155         keyLocaleData.uncheckedAppend(ASCIILiteral("true"));
156         break;
157     default:
158         ASSERT_NOT_REACHED();
159     }
160     return keyLocaleData;
161 }
162
163 void IntlCollator::initializeCollator(ExecState& state, JSValue locales, JSValue optionsValue)
164 {
165     VM& vm = state.vm();
166     auto scope = DECLARE_THROW_SCOPE(vm);
167
168     // 10.1.1 InitializeCollator (collator, locales, options) (ECMA-402 2.0)
169     // 1. If collator has an [[initializedIntlObject]] internal slot with value true, throw a TypeError exception.
170     // 2. Set collator.[[initializedIntlObject]] to true.
171
172     // 3. Let requestedLocales be CanonicalizeLocaleList(locales).
173     auto requestedLocales = canonicalizeLocaleList(state, locales);
174     // 4. ReturnIfAbrupt(requestedLocales).
175     RETURN_IF_EXCEPTION(scope, void());
176
177     // 5. If options is undefined, then
178     JSObject* options;
179     if (optionsValue.isUndefined()) {
180         // a. Let options be ObjectCreate(%ObjectPrototype%).
181         options = constructEmptyObject(&state);
182     } else { // 6. Else
183         // a. Let options be ToObject(options).
184         options = optionsValue.toObject(&state);
185         // b. ReturnIfAbrupt(options).
186         RETURN_IF_EXCEPTION(scope, void());
187     }
188
189     // 7. Let u be GetOption(options, "usage", "string", «"sort", "search"», "sort").
190     String usageString = intlStringOption(state, options, vm.propertyNames->usage, { "sort", "search" }, "usage must be either \"sort\" or \"search\"", "sort");
191     // 8. ReturnIfAbrupt(u).
192     RETURN_IF_EXCEPTION(scope, void());
193     // 9. Set collator.[[usage]] to u.
194     if (usageString == "sort")
195         m_usage = Usage::Sort;
196     else if (usageString == "search")
197         m_usage = Usage::Search;
198     else
199         ASSERT_NOT_REACHED();
200
201     // 10. If u is "sort", then
202     // a. Let localeData be the value of %Collator%.[[sortLocaleData]];
203     // 11. Else
204     // a. Let localeData be the value of %Collator%.[[searchLocaleData]].
205     Vector<String> (*localeData)(const String&, size_t);
206     if (m_usage == Usage::Sort)
207         localeData = sortLocaleData;
208     else
209         localeData = searchLocaleData;
210
211     // 12. Let opt be a new Record.
212     HashMap<String, String> opt;
213
214     // 13. Let matcher be GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit").
215     String matcher = intlStringOption(state, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit");
216     // 14. ReturnIfAbrupt(matcher).
217     RETURN_IF_EXCEPTION(scope, void());
218     // 15. Set opt.[[localeMatcher]] to matcher.
219     opt.add(ASCIILiteral("localeMatcher"), matcher);
220
221     // 16. For each row in Table 1, except the header row, do:
222     // a. Let key be the name given in the Key column of the row.
223     // b. Let prop be the name given in the Property column of the row.
224     // c. Let type be the string given in the Type column of the row.
225     // d. Let list be a List containing the Strings given in the Values column of the row, or undefined if no strings are given.
226     // e. Let value be GetOption(options, prop, type, list, undefined).
227     // f. ReturnIfAbrupt(value).
228     // g. If the string given in the Type column of the row is "boolean" and value is not undefined, then
229     //    i. Let value be ToString(value).
230     //    ii. ReturnIfAbrupt(value).
231     // h. Set opt.[[<key>]] to value.
232     {
233         String numericString;
234         bool usesFallback;
235         bool numeric = intlBooleanOption(state, options, vm.propertyNames->numeric, usesFallback);
236         RETURN_IF_EXCEPTION(scope, void());
237         if (!usesFallback)
238             numericString = ASCIILiteral(numeric ? "true" : "false");
239         opt.add(ASCIILiteral("kn"), numericString);
240     }
241     {
242         String caseFirst = intlStringOption(state, options, vm.propertyNames->caseFirst, { "upper", "lower", "false" }, "caseFirst must be either \"upper\", \"lower\", or \"false\"", nullptr);
243         RETURN_IF_EXCEPTION(scope, void());
244         opt.add(ASCIILiteral("kf"), caseFirst);
245     }
246
247     // 17. Let relevantExtensionKeys be the value of %Collator%.[[relevantExtensionKeys]].
248     // 18. Let r be ResolveLocale(%Collator%.[[availableLocales]], requestedLocales, opt, relevantExtensionKeys, localeData).
249     auto& availableLocales = state.jsCallee()->globalObject()->intlCollatorAvailableLocales();
250     auto result = resolveLocale(state, availableLocales, requestedLocales, opt, relevantExtensionKeys, WTF_ARRAY_LENGTH(relevantExtensionKeys), localeData);
251
252     // 19. Set collator.[[locale]] to the value of r.[[locale]].
253     m_locale = result.get(ASCIILiteral("locale"));
254     if (m_locale.isEmpty()) {
255         throwTypeError(&state, scope, ASCIILiteral("failed to initialize Collator due to invalid locale"));
256         return;
257     }
258
259     // 20. Let k be 0.
260     // 21. Let lenValue be Get(relevantExtensionKeys, "length").
261     // 22. Let len be ToLength(lenValue).
262     // 23. Repeat while k < len:
263     // a. Let Pk be ToString(k).
264     // b. Let key be Get(relevantExtensionKeys, Pk).
265     // c. ReturnIfAbrupt(key).
266     // d. If key is "co", then
267     //    i. Let property be "collation".
268     //    ii. Let value be the value of r.[[co]].
269     //    iii. If value is null, let value be "default".
270     // e. Else use the row of Table 1 that contains the value of key in the Key column:
271     //    i. Let property be the name given in the Property column of the row.
272     //    ii. Let value be the value of r.[[<key>]].
273     //    iii. If the name given in the Type column of the row is "boolean", let value be the result of comparing value with "true".
274     // f. Set collator.[[<property>]] to value.
275     // g. Increase k by 1.
276     const String& collation = result.get(ASCIILiteral("co"));
277     m_collation = collation.isNull() ? ASCIILiteral("default") : collation;
278     m_numeric = (result.get(ASCIILiteral("kn")) == "true");
279
280     // 24. Let s be GetOption(options, "sensitivity", "string", «"base", "accent", "case", "variant"», undefined).
281     String sensitivityString = intlStringOption(state, options, vm.propertyNames->sensitivity, { "base", "accent", "case", "variant" }, "sensitivity must be either \"base\", \"accent\", \"case\", or \"variant\"", nullptr);
282     // 25. ReturnIfAbrupt(s).
283     RETURN_IF_EXCEPTION(scope, void());
284     // 26. If s is undefined, then
285     // a. If u is "sort", then let s be "variant".
286     // b. Else
287     //    i. Let dataLocale be the value of r.[[dataLocale]].
288     //    ii. Let dataLocaleData be Get(localeData, dataLocale).
289     //    iii. Let s be Get(dataLocaleData, "sensitivity").
290     //    10.2.3 "[[searchLocaleData]][locale] must have a sensitivity property with a String value equal to "base", "accent", "case", or "variant" for all locale values."
291     // 27. Set collator.[[sensitivity]] to s.
292     if (sensitivityString == "base")
293         m_sensitivity = Sensitivity::Base;
294     else if (sensitivityString == "accent")
295         m_sensitivity = Sensitivity::Accent;
296     else if (sensitivityString == "case")
297         m_sensitivity = Sensitivity::Case;
298     else
299         m_sensitivity = Sensitivity::Variant;
300
301     // 28. Let ip be GetOption(options, "ignorePunctuation", "boolean", undefined, false).
302     bool usesFallback;
303     bool ignorePunctuation = intlBooleanOption(state, options, vm.propertyNames->ignorePunctuation, usesFallback);
304     if (usesFallback)
305         ignorePunctuation = false;
306     // 29. ReturnIfAbrupt(ip).
307     RETURN_IF_EXCEPTION(scope, void());
308     // 30. Set collator.[[ignorePunctuation]] to ip.
309     m_ignorePunctuation = ignorePunctuation;
310
311     // 31. Set collator.[[boundCompare]] to undefined.
312     // 32. Set collator.[[initializedCollator]] to true.
313     m_initializedCollator = true;
314
315     // 33. Return collator.
316 }
317
318 void IntlCollator::createCollator(ExecState& state)
319 {
320     VM& vm = state.vm();
321     auto scope = DECLARE_CATCH_SCOPE(vm);
322     ASSERT(!m_collator);
323
324     if (!m_initializedCollator) {
325         initializeCollator(state, jsUndefined(), jsUndefined());
326         ASSERT_UNUSED(scope, !scope.exception());
327     }
328
329     UErrorCode status = U_ZERO_ERROR;
330     auto collator = std::unique_ptr<UCollator, UCollatorDeleter>(ucol_open(m_locale.utf8().data(), &status));
331     if (U_FAILURE(status))
332         return;
333
334     UColAttributeValue strength = UCOL_PRIMARY;
335     UColAttributeValue caseLevel = UCOL_OFF;
336     switch (m_sensitivity) {
337     case Sensitivity::Base:
338         break;
339     case Sensitivity::Accent:
340         strength = UCOL_SECONDARY;
341         break;
342     case Sensitivity::Case:
343         caseLevel = UCOL_ON;
344         break;
345     case Sensitivity::Variant:
346         strength = UCOL_TERTIARY;
347         break;
348     default:
349         ASSERT_NOT_REACHED();
350     }
351     ucol_setAttribute(collator.get(), UCOL_STRENGTH, strength, &status);
352     ucol_setAttribute(collator.get(), UCOL_CASE_LEVEL, caseLevel, &status);
353
354     ucol_setAttribute(collator.get(), UCOL_NUMERIC_COLLATION, m_numeric ? UCOL_ON : UCOL_OFF, &status);
355
356     // FIXME: Setting UCOL_ALTERNATE_HANDLING to UCOL_SHIFTED causes punctuation and whitespace to be
357     // ignored. There is currently no way to ignore only punctuation.
358     ucol_setAttribute(collator.get(), UCOL_ALTERNATE_HANDLING, m_ignorePunctuation ? UCOL_SHIFTED : UCOL_DEFAULT, &status);
359
360     // "The method is required to return 0 when comparing Strings that are considered canonically
361     // equivalent by the Unicode standard."
362     ucol_setAttribute(collator.get(), UCOL_NORMALIZATION_MODE, UCOL_ON, &status);
363     if (U_FAILURE(status))
364         return;
365
366     m_collator = WTFMove(collator);
367 }
368
369 JSValue IntlCollator::compareStrings(ExecState& state, StringView x, StringView y)
370 {
371     VM& vm = state.vm();
372     auto scope = DECLARE_THROW_SCOPE(vm);
373
374     // 10.3.4 CompareStrings abstract operation (ECMA-402 2.0)
375     if (!m_collator) {
376         createCollator(state);
377         if (!m_collator)
378             return throwException(&state, scope, createError(&state, ASCIILiteral("Failed to compare strings.")));
379     }
380
381     UErrorCode status = U_ZERO_ERROR;
382     UCharIterator iteratorX = createIterator(x);
383     UCharIterator iteratorY = createIterator(y);
384     auto result = ucol_strcollIter(m_collator.get(), &iteratorX, &iteratorY, &status);
385     if (U_FAILURE(status))
386         return throwException(&state, scope, createError(&state, ASCIILiteral("Failed to compare strings.")));
387     return jsNumber(result);
388 }
389
390 const char* IntlCollator::usageString(Usage usage)
391 {
392     switch (usage) {
393     case Usage::Sort:
394         return "sort";
395     case Usage::Search:
396         return "search";
397     }
398     ASSERT_NOT_REACHED();
399     return nullptr;
400 }
401
402 const char* IntlCollator::sensitivityString(Sensitivity sensitivity)
403 {
404     switch (sensitivity) {
405     case Sensitivity::Base:
406         return "base";
407     case Sensitivity::Accent:
408         return "accent";
409     case Sensitivity::Case:
410         return "case";
411     case Sensitivity::Variant:
412         return "variant";
413     }
414     ASSERT_NOT_REACHED();
415     return nullptr;
416 }
417
418 JSObject* IntlCollator::resolvedOptions(ExecState& state)
419 {
420     VM& vm = state.vm();
421     auto scope = DECLARE_THROW_SCOPE(vm);
422
423     // 10.3.5 Intl.Collator.prototype.resolvedOptions() (ECMA-402 2.0)
424     // The function returns a new object whose properties and attributes are set as if
425     // constructed by an object literal assigning to each of the following properties the
426     // value of the corresponding internal slot of this Collator object (see 10.4): locale,
427     // usage, sensitivity, ignorePunctuation, collation, as well as those properties shown
428     // in Table 1 whose keys are included in the %Collator%[[relevantExtensionKeys]]
429     // internal slot of the standard built-in object that is the initial value of
430     // Intl.Collator.
431
432     if (!m_initializedCollator) {
433         initializeCollator(state, jsUndefined(), jsUndefined());
434         ASSERT_UNUSED(scope, !scope.exception());
435     }
436
437     JSObject* options = constructEmptyObject(&state);
438     options->putDirect(vm, vm.propertyNames->locale, jsString(&state, m_locale));
439     options->putDirect(vm, vm.propertyNames->usage, jsNontrivialString(&state, ASCIILiteral(usageString(m_usage))));
440     options->putDirect(vm, vm.propertyNames->sensitivity, jsNontrivialString(&state, ASCIILiteral(sensitivityString(m_sensitivity))));
441     options->putDirect(vm, vm.propertyNames->ignorePunctuation, jsBoolean(m_ignorePunctuation));
442     options->putDirect(vm, vm.propertyNames->collation, jsString(&state, m_collation));
443     options->putDirect(vm, vm.propertyNames->numeric, jsBoolean(m_numeric));
444     return options;
445 }
446
447 void IntlCollator::setBoundCompare(VM& vm, JSBoundFunction* format)
448 {
449     m_boundCompare.set(vm, this, format);
450 }
451
452 } // namespace JSC
453
454 #endif // ENABLE(INTL)