Source code

Revision control

Other Tools

1
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2
* vim: set ts=8 sts=2 et sw=2 tw=80:
3
* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
/* Intl.Collator implementation. */
8
9
#include "builtin/intl/Collator.h"
10
11
#include "mozilla/Assertions.h"
12
#include "mozilla/Span.h"
13
14
#include "jsapi.h"
15
16
#include "builtin/Array.h"
17
#include "builtin/intl/CommonFunctions.h"
18
#include "builtin/intl/LanguageTag.h"
19
#include "builtin/intl/ScopedICUObject.h"
20
#include "builtin/intl/SharedIntlData.h"
21
#include "gc/FreeOp.h"
22
#include "js/CharacterEncoding.h"
23
#include "js/PropertySpec.h"
24
#include "js/StableStringChars.h"
25
#include "js/TypeDecls.h"
26
#include "unicode/ucol.h"
27
#include "unicode/uenum.h"
28
#include "unicode/uloc.h"
29
#include "unicode/utypes.h"
30
#include "vm/GlobalObject.h"
31
#include "vm/JSContext.h"
32
#include "vm/Runtime.h"
33
#include "vm/StringType.h"
34
35
#include "vm/JSObject-inl.h"
36
37
using namespace js;
38
39
using JS::AutoStableStringChars;
40
41
using js::intl::IcuLocale;
42
using js::intl::ReportInternalError;
43
using js::intl::SharedIntlData;
44
using js::intl::StringsAreEqual;
45
46
const JSClassOps CollatorObject::classOps_ = {nullptr, /* addProperty */
47
nullptr, /* delProperty */
48
nullptr, /* enumerate */
49
nullptr, /* newEnumerate */
50
nullptr, /* resolve */
51
nullptr, /* mayResolve */
52
CollatorObject::finalize};
53
54
const JSClass CollatorObject::class_ = {
55
js_Object_str,
56
JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) |
57
JSCLASS_HAS_CACHED_PROTO(JSProto_Collator) |
58
JSCLASS_FOREGROUND_FINALIZE,
59
&CollatorObject::classOps_, &CollatorObject::classSpec_};
60
61
const JSClass& CollatorObject::protoClass_ = PlainObject::class_;
62
63
static bool collator_toSource(JSContext* cx, unsigned argc, Value* vp) {
64
CallArgs args = CallArgsFromVp(argc, vp);
65
args.rval().setString(cx->names().Collator);
66
return true;
67
}
68
69
static const JSFunctionSpec collator_static_methods[] = {
70
JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf",
71
1, 0),
72
JS_FS_END};
73
74
static const JSFunctionSpec collator_methods[] = {
75
JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0),
76
JS_FN(js_toSource_str, collator_toSource, 0, 0), JS_FS_END};
77
78
static const JSPropertySpec collator_properties[] = {
79
JS_SELF_HOSTED_GET("compare", "$Intl_Collator_compare_get", 0),
80
JS_STRING_SYM_PS(toStringTag, "Object", JSPROP_READONLY), JS_PS_END};
81
82
static bool Collator(JSContext* cx, unsigned argc, Value* vp);
83
84
const ClassSpec CollatorObject::classSpec_ = {
85
GenericCreateConstructor<Collator, 0, gc::AllocKind::FUNCTION>,
86
GenericCreatePrototype<CollatorObject>,
87
collator_static_methods,
88
nullptr,
89
collator_methods,
90
collator_properties,
91
nullptr,
92
ClassSpec::DontDefineConstructor};
93
94
/**
95
* 10.1.2 Intl.Collator([ locales [, options]])
96
*
97
* ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
98
*/
99
static bool Collator(JSContext* cx, const CallArgs& args) {
100
// Step 1 (Handled by OrdinaryCreateFromConstructor fallback code).
101
102
// Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
103
RootedObject proto(cx);
104
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Collator, &proto)) {
105
return false;
106
}
107
108
Rooted<CollatorObject*> collator(
109
cx, NewObjectWithClassProto<CollatorObject>(cx, proto));
110
if (!collator) {
111
return false;
112
}
113
114
HandleValue locales = args.get(0);
115
HandleValue options = args.get(1);
116
117
// Step 6.
118
if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator,
119
locales, options)) {
120
return false;
121
}
122
123
args.rval().setObject(*collator);
124
return true;
125
}
126
127
static bool Collator(JSContext* cx, unsigned argc, Value* vp) {
128
CallArgs args = CallArgsFromVp(argc, vp);
129
return Collator(cx, args);
130
}
131
132
bool js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) {
133
CallArgs args = CallArgsFromVp(argc, vp);
134
MOZ_ASSERT(args.length() == 2);
135
MOZ_ASSERT(!args.isConstructing());
136
137
return Collator(cx, args);
138
}
139
140
void js::CollatorObject::finalize(JSFreeOp* fop, JSObject* obj) {
141
MOZ_ASSERT(fop->onMainThread());
142
143
if (UCollator* coll = obj->as<CollatorObject>().getCollator()) {
144
intl::RemoveICUCellMemory(fop, obj, CollatorObject::EstimatedMemoryUse);
145
146
ucol_close(coll);
147
}
148
}
149
150
bool js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) {
151
CallArgs args = CallArgsFromVp(argc, vp);
152
MOZ_ASSERT(args.length() == 1);
153
MOZ_ASSERT(args[0].isString());
154
155
UniqueChars locale = intl::EncodeLocale(cx, args[0].toString());
156
if (!locale) {
157
return false;
158
}
159
UErrorCode status = U_ZERO_ERROR;
160
UEnumeration* values =
161
ucol_getKeywordValuesForLocale("co", locale.get(), false, &status);
162
if (U_FAILURE(status)) {
163
ReportInternalError(cx);
164
return false;
165
}
166
ScopedICUObject<UEnumeration, uenum_close> toClose(values);
167
168
uint32_t count = uenum_count(values, &status);
169
if (U_FAILURE(status)) {
170
ReportInternalError(cx);
171
return false;
172
}
173
174
RootedObject collations(cx, NewDenseEmptyArray(cx));
175
if (!collations) {
176
return false;
177
}
178
179
// The first element of the collations array must be |null| per
180
// ES2017 Intl, 10.2.3 Internal Slots.
181
if (!NewbornArrayPush(cx, collations, NullValue())) {
182
return false;
183
}
184
185
for (uint32_t i = 0; i < count; i++) {
186
const char* collation = uenum_next(values, nullptr, &status);
187
if (U_FAILURE(status)) {
188
ReportInternalError(cx);
189
return false;
190
}
191
192
// Per ECMA-402, 10.2.3, we don't include standard and search:
193
// "The values 'standard' and 'search' must not be used as elements in
194
// any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co
195
// array."
196
if (StringsAreEqual(collation, "standard") ||
197
StringsAreEqual(collation, "search")) {
198
continue;
199
}
200
201
// ICU returns old-style keyword values; map them to BCP 47 equivalents.
202
collation = uloc_toUnicodeLocaleType("co", collation);
203
if (!collation) {
204
ReportInternalError(cx);
205
return false;
206
}
207
208
JSString* jscollation = NewStringCopyZ<CanGC>(cx, collation);
209
if (!jscollation) {
210
return false;
211
}
212
if (!NewbornArrayPush(cx, collations, StringValue(jscollation))) {
213
return false;
214
}
215
}
216
217
args.rval().setObject(*collations);
218
return true;
219
}
220
221
/**
222
* Returns a new UCollator with the locale and collation options
223
* of the given Collator.
224
*/
225
static UCollator* NewUCollator(JSContext* cx,
226
Handle<CollatorObject*> collator) {
227
RootedValue value(cx);
228
229
RootedObject internals(cx, intl::GetInternalsObject(cx, collator));
230
if (!internals) {
231
return nullptr;
232
}
233
234
if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
235
return nullptr;
236
}
237
UniqueChars locale = intl::EncodeLocale(cx, value.toString());
238
if (!locale) {
239
return nullptr;
240
}
241
242
// UCollator options with default values.
243
UColAttributeValue uStrength = UCOL_DEFAULT;
244
UColAttributeValue uCaseLevel = UCOL_OFF;
245
UColAttributeValue uAlternate = UCOL_DEFAULT;
246
UColAttributeValue uNumeric = UCOL_OFF;
247
// Normalization is always on to meet the canonical equivalence requirement.
248
UColAttributeValue uNormalization = UCOL_ON;
249
UColAttributeValue uCaseFirst = UCOL_DEFAULT;
250
251
if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) {
252
return nullptr;
253
}
254
255
{
256
JSLinearString* usage = value.toString()->ensureLinear(cx);
257
if (!usage) {
258
return nullptr;
259
}
260
if (StringEqualsLiteral(usage, "search")) {
261
// ICU expects search as a Unicode locale extension on locale.
262
intl::LanguageTag tag(cx);
263
if (!intl::LanguageTagParser::parse(
264
cx, mozilla::MakeStringSpan(locale.get()), tag)) {
265
return nullptr;
266
}
267
268
JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx);
269
270
if (!keywords.emplaceBack("co", cx->names().search)) {
271
return nullptr;
272
}
273
274
// |ApplyUnicodeExtensionToTag| applies the new keywords to the front of
275
// the Unicode extension subtag. We're then relying on ICU to follow RFC
276
// 6067, which states that any trailing keywords using the same key
277
// should be ignored.
278
if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) {
279
return nullptr;
280
}
281
282
locale = tag.toStringZ(cx);
283
if (!locale) {
284
return nullptr;
285
}
286
} else {
287
MOZ_ASSERT(StringEqualsLiteral(usage, "sort"));
288
}
289
}
290
291
// We don't need to look at the collation property - it can only be set
292
// via the Unicode locale extension and is therefore already set on
293
// locale.
294
295
if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) {
296
return nullptr;
297
}
298
299
{
300
JSLinearString* sensitivity = value.toString()->ensureLinear(cx);
301
if (!sensitivity) {
302
return nullptr;
303
}
304
if (StringEqualsLiteral(sensitivity, "base")) {
305
uStrength = UCOL_PRIMARY;
306
} else if (StringEqualsLiteral(sensitivity, "accent")) {
307
uStrength = UCOL_SECONDARY;
308
} else if (StringEqualsLiteral(sensitivity, "case")) {
309
uStrength = UCOL_PRIMARY;
310
uCaseLevel = UCOL_ON;
311
} else {
312
MOZ_ASSERT(StringEqualsLiteral(sensitivity, "variant"));
313
uStrength = UCOL_TERTIARY;
314
}
315
}
316
317
if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation,
318
&value)) {
319
return nullptr;
320
}
321
// According to the ICU team, UCOL_SHIFTED causes punctuation to be
322
// ignored. Looking at Unicode Technical Report 35, Unicode Locale Data
323
// Markup Language, "shifted" causes whitespace and punctuation to be
324
// ignored - that's a bit more than asked for, but there's no way to get
325
// less.
326
if (value.toBoolean()) {
327
uAlternate = UCOL_SHIFTED;
328
}
329
330
if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) {
331
return nullptr;
332
}
333
if (!value.isUndefined() && value.toBoolean()) {
334
uNumeric = UCOL_ON;
335
}
336
337
if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) {
338
return nullptr;
339
}
340
if (!value.isUndefined()) {
341
JSLinearString* caseFirst = value.toString()->ensureLinear(cx);
342
if (!caseFirst) {
343
return nullptr;
344
}
345
if (StringEqualsLiteral(caseFirst, "upper")) {
346
uCaseFirst = UCOL_UPPER_FIRST;
347
} else if (StringEqualsLiteral(caseFirst, "lower")) {
348
uCaseFirst = UCOL_LOWER_FIRST;
349
} else {
350
MOZ_ASSERT(StringEqualsLiteral(caseFirst, "false"));
351
uCaseFirst = UCOL_OFF;
352
}
353
}
354
355
UErrorCode status = U_ZERO_ERROR;
356
UCollator* coll = ucol_open(IcuLocale(locale.get()), &status);
357
if (U_FAILURE(status)) {
358
ReportInternalError(cx);
359
return nullptr;
360
}
361
362
ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status);
363
ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status);
364
ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status);
365
ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status);
366
ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status);
367
ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status);
368
if (U_FAILURE(status)) {
369
ucol_close(coll);
370
ReportInternalError(cx);
371
return nullptr;
372
}
373
374
return coll;
375
}
376
377
static bool intl_CompareStrings(JSContext* cx, UCollator* coll,
378
HandleString str1, HandleString str2,
379
MutableHandleValue result) {
380
MOZ_ASSERT(str1);
381
MOZ_ASSERT(str2);
382
383
if (str1 == str2) {
384
result.setInt32(0);
385
return true;
386
}
387
388
AutoStableStringChars stableChars1(cx);
389
if (!stableChars1.initTwoByte(cx, str1)) {
390
return false;
391
}
392
393
AutoStableStringChars stableChars2(cx);
394
if (!stableChars2.initTwoByte(cx, str2)) {
395
return false;
396
}
397
398
mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange();
399
mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange();
400
401
UCollationResult uresult =
402
ucol_strcoll(coll, chars1.begin().get(), chars1.length(),
403
chars2.begin().get(), chars2.length());
404
int32_t res;
405
switch (uresult) {
406
case UCOL_LESS:
407
res = -1;
408
break;
409
case UCOL_EQUAL:
410
res = 0;
411
break;
412
case UCOL_GREATER:
413
res = 1;
414
break;
415
default:
416
MOZ_CRASH("ucol_strcoll returned bad UCollationResult");
417
}
418
result.setInt32(res);
419
return true;
420
}
421
422
bool js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) {
423
CallArgs args = CallArgsFromVp(argc, vp);
424
MOZ_ASSERT(args.length() == 3);
425
MOZ_ASSERT(args[0].isObject());
426
MOZ_ASSERT(args[1].isString());
427
MOZ_ASSERT(args[2].isString());
428
429
Rooted<CollatorObject*> collator(cx,
430
&args[0].toObject().as<CollatorObject>());
431
432
// Obtain a cached UCollator object.
433
UCollator* coll = collator->getCollator();
434
if (!coll) {
435
coll = NewUCollator(cx, collator);
436
if (!coll) {
437
return false;
438
}
439
collator->setCollator(coll);
440
441
intl::AddICUCellMemory(collator, CollatorObject::EstimatedMemoryUse);
442
}
443
444
// Use the UCollator to actually compare the strings.
445
RootedString str1(cx, args[1].toString());
446
RootedString str2(cx, args[2].toString());
447
return intl_CompareStrings(cx, coll, str1, str2, args.rval());
448
}
449
450
bool js::intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp) {
451
CallArgs args = CallArgsFromVp(argc, vp);
452
MOZ_ASSERT(args.length() == 1);
453
MOZ_ASSERT(args[0].isString());
454
455
SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref();
456
457
RootedString locale(cx, args[0].toString());
458
bool isUpperFirst;
459
if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) {
460
return false;
461
}
462
463
args.rval().setBoolean(isUpperFirst);
464
return true;
465
}