DisplayNames.cpp (19225B)
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.DisplayNames implementation. */ 8 9 #include "builtin/intl/DisplayNames.h" 10 11 #include "mozilla/Assertions.h" 12 #include "mozilla/intl/DisplayNames.h" 13 #include "mozilla/PodOperations.h" 14 #include "mozilla/Span.h" 15 16 #include "jsnum.h" 17 #include "jspubtd.h" 18 19 #include "builtin/intl/CommonFunctions.h" 20 #include "builtin/intl/FormatBuffer.h" 21 #include "builtin/intl/LocaleNegotiation.h" 22 #include "gc/AllocKind.h" 23 #include "gc/GCContext.h" 24 #include "js/CallArgs.h" 25 #include "js/Class.h" 26 #include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor 27 #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* 28 #include "js/Printer.h" 29 #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties 30 #include "js/PropertyDescriptor.h" 31 #include "js/PropertySpec.h" 32 #include "js/RootingAPI.h" 33 #include "js/TypeDecls.h" 34 #include "js/Utility.h" 35 #include "vm/GlobalObject.h" 36 #include "vm/JSContext.h" 37 #include "vm/JSObject.h" 38 #include "vm/Runtime.h" 39 #include "vm/SelfHosting.h" 40 #include "vm/Stack.h" 41 #include "vm/StringType.h" 42 43 #include "vm/JSObject-inl.h" 44 #include "vm/NativeObject-inl.h" 45 46 using namespace js; 47 using namespace js::intl; 48 49 const JSClassOps DisplayNamesObject::classOps_ = { 50 nullptr, /* addProperty */ 51 nullptr, /* delProperty */ 52 nullptr, /* enumerate */ 53 nullptr, /* newEnumerate */ 54 nullptr, /* resolve */ 55 nullptr, /* mayResolve */ 56 DisplayNamesObject::finalize, 57 }; 58 59 const JSClass DisplayNamesObject::class_ = { 60 "Intl.DisplayNames", 61 JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) | 62 JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) | 63 JSCLASS_FOREGROUND_FINALIZE, 64 &DisplayNamesObject::classOps_, 65 &DisplayNamesObject::classSpec_, 66 }; 67 68 const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_; 69 70 static bool displayNames_supportedLocalesOf(JSContext* cx, unsigned argc, 71 Value* vp); 72 73 static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) { 74 CallArgs args = CallArgsFromVp(argc, vp); 75 args.rval().setString(cx->names().DisplayNames); 76 return true; 77 } 78 79 static const JSFunctionSpec displayNames_static_methods[] = { 80 JS_FN("supportedLocalesOf", displayNames_supportedLocalesOf, 1, 0), 81 JS_FS_END, 82 }; 83 84 static const JSFunctionSpec displayNames_methods[] = { 85 JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0), 86 JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0, 87 0), 88 JS_FN("toSource", displayNames_toSource, 0, 0), 89 JS_FS_END, 90 }; 91 92 static const JSPropertySpec displayNames_properties[] = { 93 JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY), 94 JS_PS_END, 95 }; 96 97 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp); 98 99 const ClassSpec DisplayNamesObject::classSpec_ = { 100 GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>, 101 GenericCreatePrototype<DisplayNamesObject>, 102 displayNames_static_methods, 103 nullptr, 104 displayNames_methods, 105 displayNames_properties, 106 nullptr, 107 ClassSpec::DontDefineConstructor, 108 }; 109 110 enum class DisplayNamesOptions { 111 Standard, 112 113 // Calendar display names are no longer available with the current spec 114 // proposal text, but may be re-enabled in the future. For our internal use 115 // we still need to have them present, so use a feature guard for now. 116 EnableMozExtensions, 117 }; 118 119 /** 120 * Initialize a new Intl.DisplayNames object using the named self-hosted 121 * function. 122 */ 123 static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj, 124 Handle<PropertyName*> initializer, 125 HandleValue locales, 126 HandleValue options, 127 DisplayNamesOptions dnoptions) { 128 FixedInvokeArgs<4> args(cx); 129 130 args[0].setObject(*obj); 131 args[1].set(locales); 132 args[2].set(options); 133 args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions); 134 135 RootedValue ignored(cx); 136 if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, 137 &ignored)) { 138 return false; 139 } 140 141 MOZ_ASSERT(ignored.isUndefined(), 142 "Unexpected return value from non-legacy Intl object initializer"); 143 return true; 144 } 145 146 /** 147 * Intl.DisplayNames ([ locales [ , options ]]) 148 */ 149 static bool DisplayNames(JSContext* cx, const CallArgs& args, 150 DisplayNamesOptions dnoptions) { 151 // Step 1. 152 if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) { 153 return false; 154 } 155 156 // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). 157 RootedObject proto(cx); 158 if (dnoptions == DisplayNamesOptions::Standard) { 159 if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames, 160 &proto)) { 161 return false; 162 } 163 } else { 164 RootedObject newTarget(cx, &args.newTarget().toObject()); 165 if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) { 166 return false; 167 } 168 } 169 170 Rooted<DisplayNamesObject*> displayNames(cx); 171 displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto); 172 if (!displayNames) { 173 return false; 174 } 175 176 HandleValue locales = args.get(0); 177 HandleValue options = args.get(1); 178 179 // Steps 3-26. 180 if (!InitializeDisplayNamesObject(cx, displayNames, 181 cx->names().InitializeDisplayNames, locales, 182 options, dnoptions)) { 183 return false; 184 } 185 186 // Step 27. 187 args.rval().setObject(*displayNames); 188 return true; 189 } 190 191 static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) { 192 CallArgs args = CallArgsFromVp(argc, vp); 193 return DisplayNames(cx, args, DisplayNamesOptions::Standard); 194 } 195 196 static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) { 197 CallArgs args = CallArgsFromVp(argc, vp); 198 return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions); 199 } 200 201 void js::DisplayNamesObject::finalize(JS::GCContext* gcx, JSObject* obj) { 202 MOZ_ASSERT(gcx->onMainThread()); 203 204 if (mozilla::intl::DisplayNames* displayNames = 205 obj->as<DisplayNamesObject>().getDisplayNames()) { 206 intl::RemoveICUCellMemory(gcx, obj, DisplayNamesObject::EstimatedMemoryUse); 207 delete displayNames; 208 } 209 } 210 211 bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) { 212 RootedObject ctor(cx, GlobalObject::createConstructor( 213 cx, MozDisplayNames, cx->names().DisplayNames, 2)); 214 if (!ctor) { 215 return false; 216 } 217 218 RootedObject proto( 219 cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global())); 220 if (!proto) { 221 return false; 222 } 223 224 if (!LinkConstructorAndPrototype(cx, ctor, proto)) { 225 return false; 226 } 227 228 if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) { 229 return false; 230 } 231 232 if (!JS_DefineFunctions(cx, proto, displayNames_methods)) { 233 return false; 234 } 235 236 if (!JS_DefineProperties(cx, proto, displayNames_properties)) { 237 return false; 238 } 239 240 RootedValue ctorValue(cx, ObjectValue(*ctor)); 241 return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0); 242 } 243 244 static mozilla::intl::DisplayNames* NewDisplayNames( 245 JSContext* cx, const char* locale, 246 mozilla::intl::DisplayNames::Options& options) { 247 auto result = mozilla::intl::DisplayNames::TryCreate(locale, options); 248 if (result.isErr()) { 249 intl::ReportInternalError(cx, result.unwrapErr()); 250 return nullptr; 251 } 252 return result.unwrap().release(); 253 } 254 255 static mozilla::intl::DisplayNames* GetOrCreateDisplayNames( 256 JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, 257 mozilla::intl::DisplayNames::Options& options) { 258 // Obtain a cached mozilla::intl::DisplayNames object. 259 mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames(); 260 if (!dn) { 261 dn = NewDisplayNames(cx, locale, options); 262 if (!dn) { 263 return nullptr; 264 } 265 displayNames->setDisplayNames(dn); 266 267 intl::AddICUCellMemory(displayNames, 268 DisplayNamesObject::EstimatedMemoryUse); 269 } 270 return dn; 271 } 272 273 static void ReportInvalidOptionError(JSContext* cx, HandleString type, 274 HandleString option) { 275 if (UniqueChars optionStr = QuoteString(cx, option, '"')) { 276 if (UniqueChars typeStr = QuoteString(cx, type)) { 277 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 278 JSMSG_INVALID_OPTION_VALUE, typeStr.get(), 279 optionStr.get()); 280 } 281 } 282 } 283 284 static void ReportInvalidOptionError(JSContext* cx, const char* type, 285 HandleString option) { 286 if (UniqueChars str = QuoteString(cx, option, '"')) { 287 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 288 JSMSG_INVALID_OPTION_VALUE, type, str.get()); 289 } 290 } 291 292 static void ReportInvalidOptionError(JSContext* cx, const char* type, 293 double option) { 294 ToCStringBuf cbuf; 295 const char* str = NumberToCString(&cbuf, option); 296 MOZ_ASSERT(str); 297 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 298 JSMSG_INVALID_DIGITS_VALUE, str); 299 } 300 301 /** 302 * intl_ComputeDisplayName(displayNames, locale, calendar, style, 303 * languageDisplay, fallback, type, code) 304 */ 305 bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) { 306 CallArgs args = CallArgsFromVp(argc, vp); 307 MOZ_ASSERT(args.length() == 8); 308 309 Rooted<DisplayNamesObject*> displayNames( 310 cx, &args[0].toObject().as<DisplayNamesObject>()); 311 312 UniqueChars locale = intl::EncodeLocale(cx, args[1].toString()); 313 if (!locale) { 314 return false; 315 } 316 317 Rooted<JSLinearString*> calendar(cx, args[2].toString()->ensureLinear(cx)); 318 if (!calendar) { 319 return false; 320 } 321 322 Rooted<JSLinearString*> code(cx, args[7].toString()->ensureLinear(cx)); 323 if (!code) { 324 return false; 325 } 326 327 mozilla::intl::DisplayNames::Style style; 328 { 329 JSLinearString* styleStr = args[3].toString()->ensureLinear(cx); 330 if (!styleStr) { 331 return false; 332 } 333 334 if (StringEqualsLiteral(styleStr, "long")) { 335 style = mozilla::intl::DisplayNames::Style::Long; 336 } else if (StringEqualsLiteral(styleStr, "short")) { 337 style = mozilla::intl::DisplayNames::Style::Short; 338 } else if (StringEqualsLiteral(styleStr, "narrow")) { 339 style = mozilla::intl::DisplayNames::Style::Narrow; 340 } else { 341 MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated")); 342 style = mozilla::intl::DisplayNames::Style::Abbreviated; 343 } 344 } 345 346 mozilla::intl::DisplayNames::LanguageDisplay languageDisplay; 347 { 348 JSLinearString* language = args[4].toString()->ensureLinear(cx); 349 if (!language) { 350 return false; 351 } 352 353 if (StringEqualsLiteral(language, "dialect")) { 354 languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect; 355 } else { 356 MOZ_ASSERT(language->empty() || 357 StringEqualsLiteral(language, "standard")); 358 languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard; 359 } 360 } 361 362 mozilla::intl::DisplayNames::Fallback fallback; 363 { 364 JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx); 365 if (!fallbackStr) { 366 return false; 367 } 368 369 if (StringEqualsLiteral(fallbackStr, "none")) { 370 fallback = mozilla::intl::DisplayNames::Fallback::None; 371 } else { 372 MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code")); 373 fallback = mozilla::intl::DisplayNames::Fallback::Code; 374 } 375 } 376 377 Rooted<JSLinearString*> type(cx, args[6].toString()->ensureLinear(cx)); 378 if (!type) { 379 return false; 380 } 381 382 mozilla::intl::DisplayNames::Options options{ 383 style, 384 languageDisplay, 385 }; 386 387 // If a calendar exists, set it as an option. 388 JS::UniqueChars calendarChars = nullptr; 389 if (!calendar->empty()) { 390 calendarChars = JS_EncodeStringToUTF8(cx, calendar); 391 if (!calendarChars) { 392 return false; 393 } 394 } 395 396 mozilla::intl::DisplayNames* dn = 397 GetOrCreateDisplayNames(cx, displayNames, locale.get(), options); 398 if (!dn) { 399 return false; 400 } 401 402 // The "code" is usually a small ASCII string, so try to avoid an allocation 403 // by copying it to the stack. Unfortunately we can't pass a string span of 404 // the JSString directly to the unified DisplayNames API, as the 405 // intl::FormatBuffer will be written to. This writing can trigger a GC and 406 // invalidate the span, creating a nogc rooting hazard. 407 JS::UniqueChars utf8 = nullptr; 408 unsigned char ascii[32]; 409 mozilla::Span<const char> codeSpan = nullptr; 410 if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) { 411 JS::AutoCheckCannotGC nogc; 412 mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length()); 413 codeSpan = 414 mozilla::Span(reinterpret_cast<const char*>(ascii), code->length()); 415 } else { 416 utf8 = JS_EncodeStringToUTF8(cx, code); 417 if (!utf8) { 418 return false; 419 } 420 codeSpan = mozilla::MakeStringSpan(utf8.get()); 421 } 422 423 intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); 424 mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result = 425 mozilla::Ok{}; 426 427 if (StringEqualsLiteral(type, "language")) { 428 result = dn->GetLanguage(buffer, codeSpan, fallback); 429 } else if (StringEqualsLiteral(type, "script")) { 430 result = dn->GetScript(buffer, codeSpan, fallback); 431 } else if (StringEqualsLiteral(type, "region")) { 432 result = dn->GetRegion(buffer, codeSpan, fallback); 433 } else if (StringEqualsLiteral(type, "currency")) { 434 result = dn->GetCurrency(buffer, codeSpan, fallback); 435 } else if (StringEqualsLiteral(type, "calendar")) { 436 result = dn->GetCalendar(buffer, codeSpan, fallback); 437 } else if (StringEqualsLiteral(type, "weekday")) { 438 double d = LinearStringToNumber(code); 439 if (!IsInteger(d) || d < 1 || d > 7) { 440 ReportInvalidOptionError(cx, "weekday", d); 441 return false; 442 } 443 result = 444 dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d), 445 mozilla::MakeStringSpan(calendarChars.get()), fallback); 446 } else if (StringEqualsLiteral(type, "month")) { 447 double d = LinearStringToNumber(code); 448 if (!IsInteger(d) || d < 1 || d > 13) { 449 ReportInvalidOptionError(cx, "month", d); 450 return false; 451 } 452 453 result = 454 dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d), 455 mozilla::MakeStringSpan(calendarChars.get()), fallback); 456 457 } else if (StringEqualsLiteral(type, "quarter")) { 458 double d = LinearStringToNumber(code); 459 460 // Inlined implementation of `IsValidQuarterCode ( quarter )`. 461 if (!IsInteger(d) || d < 1 || d > 4) { 462 ReportInvalidOptionError(cx, "quarter", d); 463 return false; 464 } 465 466 result = 467 dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d), 468 mozilla::MakeStringSpan(calendarChars.get()), fallback); 469 470 } else if (StringEqualsLiteral(type, "dayPeriod")) { 471 mozilla::intl::DayPeriod dayPeriod; 472 if (StringEqualsLiteral(code, "am")) { 473 dayPeriod = mozilla::intl::DayPeriod::AM; 474 } else if (StringEqualsLiteral(code, "pm")) { 475 dayPeriod = mozilla::intl::DayPeriod::PM; 476 } else { 477 ReportInvalidOptionError(cx, "dayPeriod", code); 478 return false; 479 } 480 result = dn->GetDayPeriod(buffer, dayPeriod, 481 mozilla::MakeStringSpan(calendarChars.get()), 482 fallback); 483 484 } else { 485 MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField")); 486 mozilla::intl::DateTimeField field; 487 if (StringEqualsLiteral(code, "era")) { 488 field = mozilla::intl::DateTimeField::Era; 489 } else if (StringEqualsLiteral(code, "year")) { 490 field = mozilla::intl::DateTimeField::Year; 491 } else if (StringEqualsLiteral(code, "quarter")) { 492 field = mozilla::intl::DateTimeField::Quarter; 493 } else if (StringEqualsLiteral(code, "month")) { 494 field = mozilla::intl::DateTimeField::Month; 495 } else if (StringEqualsLiteral(code, "weekOfYear")) { 496 field = mozilla::intl::DateTimeField::WeekOfYear; 497 } else if (StringEqualsLiteral(code, "weekday")) { 498 field = mozilla::intl::DateTimeField::Weekday; 499 } else if (StringEqualsLiteral(code, "day")) { 500 field = mozilla::intl::DateTimeField::Day; 501 } else if (StringEqualsLiteral(code, "dayPeriod")) { 502 field = mozilla::intl::DateTimeField::DayPeriod; 503 } else if (StringEqualsLiteral(code, "hour")) { 504 field = mozilla::intl::DateTimeField::Hour; 505 } else if (StringEqualsLiteral(code, "minute")) { 506 field = mozilla::intl::DateTimeField::Minute; 507 } else if (StringEqualsLiteral(code, "second")) { 508 field = mozilla::intl::DateTimeField::Second; 509 } else if (StringEqualsLiteral(code, "timeZoneName")) { 510 field = mozilla::intl::DateTimeField::TimeZoneName; 511 } else { 512 ReportInvalidOptionError(cx, "dateTimeField", code); 513 return false; 514 } 515 516 intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); 517 mozilla::intl::DateTimePatternGenerator* dtpgen = 518 sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); 519 if (!dtpgen) { 520 return false; 521 } 522 523 result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback); 524 } 525 526 if (result.isErr()) { 527 switch (result.unwrapErr()) { 528 case mozilla::intl::DisplayNamesError::InternalError: 529 intl::ReportInternalError(cx); 530 break; 531 case mozilla::intl::DisplayNamesError::OutOfMemory: 532 ReportOutOfMemory(cx); 533 break; 534 case mozilla::intl::DisplayNamesError::InvalidOption: 535 ReportInvalidOptionError(cx, type, code); 536 break; 537 case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag: 538 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 539 JSMSG_DUPLICATE_VARIANT_SUBTAG); 540 break; 541 case mozilla::intl::DisplayNamesError::InvalidLanguageTag: 542 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 543 JSMSG_INVALID_LANGUAGE_TAG); 544 break; 545 } 546 return false; 547 } 548 549 JSString* str = buffer.toString(cx); 550 if (!str) { 551 return false; 552 } 553 554 if (str->empty()) { 555 args.rval().setUndefined(); 556 } else { 557 args.rval().setString(str); 558 } 559 560 return true; 561 } 562 563 /** 564 * Intl.DisplayNames.supportedLocalesOf ( locales [ , options ] ) 565 */ 566 static bool displayNames_supportedLocalesOf(JSContext* cx, unsigned argc, 567 Value* vp) { 568 CallArgs args = CallArgsFromVp(argc, vp); 569 570 // Steps 1-3. 571 auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::DisplayNames, 572 args.get(0), args.get(1)); 573 if (!array) { 574 return false; 575 } 576 args.rval().setObject(*array); 577 return true; 578 }