diff options
Diffstat (limited to 'js/src/builtin/intl/DisplayNames.cpp')
-rw-r--r-- | js/src/builtin/intl/DisplayNames.cpp | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/js/src/builtin/intl/DisplayNames.cpp b/js/src/builtin/intl/DisplayNames.cpp new file mode 100644 index 0000000000..d375be58f3 --- /dev/null +++ b/js/src/builtin/intl/DisplayNames.cpp @@ -0,0 +1,550 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: set ts=8 sts=2 et sw=2 tw=80: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Intl.DisplayNames implementation. */ + +#include "builtin/intl/DisplayNames.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/DisplayNames.h" +#include "mozilla/PodOperations.h" +#include "mozilla/Span.h" + +#include <algorithm> + +#include "jsnum.h" +#include "jspubtd.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "gc/AllocKind.h" +#include "gc/GCContext.h" +#include "js/CallArgs.h" +#include "js/Class.h" +#include "js/experimental/Intl.h" // JS::AddMozDisplayNamesConstructor +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/Printer.h" +#include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Utility.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/Runtime.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" +#include "vm/NativeObject-inl.h" + +using namespace js; + +const JSClassOps DisplayNamesObject::classOps_ = {nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DisplayNamesObject::finalize}; + +const JSClass DisplayNamesObject::class_ = { + "Intl.DisplayNames", + JSCLASS_HAS_RESERVED_SLOTS(DisplayNamesObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_DisplayNames) | + JSCLASS_FOREGROUND_FINALIZE, + &DisplayNamesObject::classOps_, &DisplayNamesObject::classSpec_}; + +const JSClass& DisplayNamesObject::protoClass_ = PlainObject::class_; + +static bool displayNames_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DisplayNames); + return true; +} + +static const JSFunctionSpec displayNames_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", + "Intl_DisplayNames_supportedLocalesOf", 1, 0), + JS_FS_END}; + +static const JSFunctionSpec displayNames_methods[] = { + JS_SELF_HOSTED_FN("of", "Intl_DisplayNames_of", 1, 0), + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DisplayNames_resolvedOptions", 0, + 0), + JS_FN("toSource", displayNames_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec displayNames_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.DisplayNames", JSPROP_READONLY), + JS_PS_END}; + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec DisplayNamesObject::classSpec_ = { + GenericCreateConstructor<DisplayNames, 2, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DisplayNamesObject>, + displayNames_static_methods, + nullptr, + displayNames_methods, + displayNames_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +enum class DisplayNamesOptions { + Standard, + + // Calendar display names are no longer available with the current spec + // proposal text, but may be re-enabled in the future. For our internal use + // we still need to have them present, so use a feature guard for now. + EnableMozExtensions, +}; + +/** + * Initialize a new Intl.DisplayNames object using the named self-hosted + * function. + */ +static bool InitializeDisplayNamesObject(JSContext* cx, HandleObject obj, + Handle<PropertyName*> initializer, + HandleValue locales, + HandleValue options, + DisplayNamesOptions dnoptions) { + FixedInvokeArgs<4> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + args[3].setBoolean(dnoptions == DisplayNamesOptions::EnableMozExtensions); + + RootedValue ignored(cx); + if (!CallSelfHostedFunction(cx, initializer, NullHandleValue, args, + &ignored)) { + return false; + } + + MOZ_ASSERT(ignored.isUndefined(), + "Unexpected return value from non-legacy Intl object initializer"); + return true; +} + +/** + * Intl.DisplayNames ([ locales [ , options ]]) + */ +static bool DisplayNames(JSContext* cx, const CallArgs& args, + DisplayNamesOptions dnoptions) { + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.DisplayNames")) { + return false; + } + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (dnoptions == DisplayNamesOptions::Standard) { + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_DisplayNames, + &proto)) { + return false; + } + } else { + RootedObject newTarget(cx, &args.newTarget().toObject()); + if (!GetPrototypeFromConstructor(cx, newTarget, JSProto_Null, &proto)) { + return false; + } + } + + Rooted<DisplayNamesObject*> displayNames(cx); + displayNames = NewObjectWithClassProto<DisplayNamesObject>(cx, proto); + if (!displayNames) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Steps 3-26. + if (!InitializeDisplayNamesObject(cx, displayNames, + cx->names().InitializeDisplayNames, locales, + options, dnoptions)) { + return false; + } + + // Step 27. + args.rval().setObject(*displayNames); + return true; +} + +static bool DisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::Standard); +} + +static bool MozDisplayNames(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return DisplayNames(cx, args, DisplayNamesOptions::EnableMozExtensions); +} + +void js::DisplayNamesObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (mozilla::intl::DisplayNames* displayNames = + obj->as<DisplayNamesObject>().getDisplayNames()) { + intl::RemoveICUCellMemory(gcx, obj, DisplayNamesObject::EstimatedMemoryUse); + delete displayNames; + } +} + +bool JS::AddMozDisplayNamesConstructor(JSContext* cx, HandleObject intl) { + RootedObject ctor(cx, GlobalObject::createConstructor( + cx, MozDisplayNames, cx->names().DisplayNames, 2)); + if (!ctor) { + return false; + } + + RootedObject proto( + cx, GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global())); + if (!proto) { + return false; + } + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) { + return false; + } + + if (!JS_DefineFunctions(cx, ctor, displayNames_static_methods)) { + return false; + } + + if (!JS_DefineFunctions(cx, proto, displayNames_methods)) { + return false; + } + + if (!JS_DefineProperties(cx, proto, displayNames_properties)) { + return false; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + return DefineDataProperty(cx, intl, cx->names().DisplayNames, ctorValue, 0); +} + +static mozilla::intl::DisplayNames* NewDisplayNames( + JSContext* cx, const char* locale, + mozilla::intl::DisplayNames::Options& options) { + auto result = mozilla::intl::DisplayNames::TryCreate(locale, options); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + return result.unwrap().release(); +} + +static mozilla::intl::DisplayNames* GetOrCreateDisplayNames( + JSContext* cx, Handle<DisplayNamesObject*> displayNames, const char* locale, + mozilla::intl::DisplayNames::Options& options) { + // Obtain a cached mozilla::intl::DisplayNames object. + mozilla::intl::DisplayNames* dn = displayNames->getDisplayNames(); + if (!dn) { + dn = NewDisplayNames(cx, locale, options); + if (!dn) { + return nullptr; + } + displayNames->setDisplayNames(dn); + + intl::AddICUCellMemory(displayNames, + DisplayNamesObject::EstimatedMemoryUse); + } + return dn; +} + +static void ReportInvalidOptionError(JSContext* cx, HandleString type, + HandleString option) { + if (UniqueChars optionStr = QuoteString(cx, option, '"')) { + if (UniqueChars typeStr = QuoteString(cx, type)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, typeStr.get(), + optionStr.get()); + } + } +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + HandleString option) { + if (UniqueChars str = QuoteString(cx, option, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, type, str.get()); + } +} + +static void ReportInvalidOptionError(JSContext* cx, const char* type, + double option) { + ToCStringBuf cbuf; + const char* str = NumberToCString(&cbuf, option); + MOZ_ASSERT(str); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_DIGITS_VALUE, str); +} + +/** + * intl_ComputeDisplayName(displayNames, locale, calendar, style, + * languageDisplay, fallback, type, code) + */ +bool js::intl_ComputeDisplayName(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 8); + + Rooted<DisplayNamesObject*> displayNames( + cx, &args[0].toObject().as<DisplayNamesObject>()); + + UniqueChars locale = intl::EncodeLocale(cx, args[1].toString()); + if (!locale) { + return false; + } + + Rooted<JSLinearString*> calendar(cx, args[2].toString()->ensureLinear(cx)); + if (!calendar) { + return false; + } + + Rooted<JSLinearString*> code(cx, args[7].toString()->ensureLinear(cx)); + if (!code) { + return false; + } + + mozilla::intl::DisplayNames::Style style; + { + JSLinearString* styleStr = args[3].toString()->ensureLinear(cx); + if (!styleStr) { + return false; + } + + if (StringEqualsLiteral(styleStr, "long")) { + style = mozilla::intl::DisplayNames::Style::Long; + } else if (StringEqualsLiteral(styleStr, "short")) { + style = mozilla::intl::DisplayNames::Style::Short; + } else if (StringEqualsLiteral(styleStr, "narrow")) { + style = mozilla::intl::DisplayNames::Style::Narrow; + } else { + MOZ_ASSERT(StringEqualsLiteral(styleStr, "abbreviated")); + style = mozilla::intl::DisplayNames::Style::Abbreviated; + } + } + + mozilla::intl::DisplayNames::LanguageDisplay languageDisplay; + { + JSLinearString* language = args[4].toString()->ensureLinear(cx); + if (!language) { + return false; + } + + if (StringEqualsLiteral(language, "dialect")) { + languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Dialect; + } else { + MOZ_ASSERT(language->empty() || + StringEqualsLiteral(language, "standard")); + languageDisplay = mozilla::intl::DisplayNames::LanguageDisplay::Standard; + } + } + + mozilla::intl::DisplayNames::Fallback fallback; + { + JSLinearString* fallbackStr = args[5].toString()->ensureLinear(cx); + if (!fallbackStr) { + return false; + } + + if (StringEqualsLiteral(fallbackStr, "none")) { + fallback = mozilla::intl::DisplayNames::Fallback::None; + } else { + MOZ_ASSERT(StringEqualsLiteral(fallbackStr, "code")); + fallback = mozilla::intl::DisplayNames::Fallback::Code; + } + } + + Rooted<JSLinearString*> type(cx, args[6].toString()->ensureLinear(cx)); + if (!type) { + return false; + } + + mozilla::intl::DisplayNames::Options options{ + style, + languageDisplay, + }; + + // If a calendar exists, set it as an option. + JS::UniqueChars calendarChars = nullptr; + if (!calendar->empty()) { + calendarChars = JS_EncodeStringToUTF8(cx, calendar); + if (!calendarChars) { + return false; + } + } + + mozilla::intl::DisplayNames* dn = + GetOrCreateDisplayNames(cx, displayNames, locale.get(), options); + if (!dn) { + return false; + } + + // The "code" is usually a small ASCII string, so try to avoid an allocation + // by copying it to the stack. Unfortunately we can't pass a string span of + // the JSString directly to the unified DisplayNames API, as the + // intl::FormatBuffer will be written to. This writing can trigger a GC and + // invalidate the span, creating a nogc rooting hazard. + JS::UniqueChars utf8 = nullptr; + unsigned char ascii[32]; + mozilla::Span<const char> codeSpan = nullptr; + if (code->length() < 32 && code->hasLatin1Chars() && StringIsAscii(code)) { + JS::AutoCheckCannotGC nogc; + mozilla::PodCopy(ascii, code->latin1Chars(nogc), code->length()); + codeSpan = + mozilla::Span(reinterpret_cast<const char*>(ascii), code->length()); + } else { + utf8 = JS_EncodeStringToUTF8(cx, code); + if (!utf8) { + return false; + } + codeSpan = mozilla::MakeStringSpan(utf8.get()); + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + mozilla::Result<mozilla::Ok, mozilla::intl::DisplayNamesError> result = + mozilla::Ok{}; + + if (StringEqualsLiteral(type, "language")) { + result = dn->GetLanguage(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "script")) { + result = dn->GetScript(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "region")) { + result = dn->GetRegion(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "currency")) { + result = dn->GetCurrency(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "calendar")) { + result = dn->GetCalendar(buffer, codeSpan, fallback); + } else if (StringEqualsLiteral(type, "weekday")) { + double d = LinearStringToNumber(code); + if (!IsInteger(d) || d < 1 || d > 7) { + ReportInvalidOptionError(cx, "weekday", d); + return false; + } + result = + dn->GetWeekday(buffer, static_cast<mozilla::intl::Weekday>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + } else if (StringEqualsLiteral(type, "month")) { + double d = LinearStringToNumber(code); + if (!IsInteger(d) || d < 1 || d > 13) { + ReportInvalidOptionError(cx, "month", d); + return false; + } + + result = + dn->GetMonth(buffer, static_cast<mozilla::intl::Month>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + + } else if (StringEqualsLiteral(type, "quarter")) { + double d = LinearStringToNumber(code); + + // Inlined implementation of `IsValidQuarterCode ( quarter )`. + if (!IsInteger(d) || d < 1 || d > 4) { + ReportInvalidOptionError(cx, "quarter", d); + return false; + } + + result = + dn->GetQuarter(buffer, static_cast<mozilla::intl::Quarter>(d), + mozilla::MakeStringSpan(calendarChars.get()), fallback); + + } else if (StringEqualsLiteral(type, "dayPeriod")) { + mozilla::intl::DayPeriod dayPeriod; + if (StringEqualsLiteral(code, "am")) { + dayPeriod = mozilla::intl::DayPeriod::AM; + } else if (StringEqualsLiteral(code, "pm")) { + dayPeriod = mozilla::intl::DayPeriod::PM; + } else { + ReportInvalidOptionError(cx, "dayPeriod", code); + return false; + } + result = dn->GetDayPeriod(buffer, dayPeriod, + mozilla::MakeStringSpan(calendarChars.get()), + fallback); + + } else { + MOZ_ASSERT(StringEqualsLiteral(type, "dateTimeField")); + mozilla::intl::DateTimeField field; + if (StringEqualsLiteral(code, "era")) { + field = mozilla::intl::DateTimeField::Era; + } else if (StringEqualsLiteral(code, "year")) { + field = mozilla::intl::DateTimeField::Year; + } else if (StringEqualsLiteral(code, "quarter")) { + field = mozilla::intl::DateTimeField::Quarter; + } else if (StringEqualsLiteral(code, "month")) { + field = mozilla::intl::DateTimeField::Month; + } else if (StringEqualsLiteral(code, "weekOfYear")) { + field = mozilla::intl::DateTimeField::WeekOfYear; + } else if (StringEqualsLiteral(code, "weekday")) { + field = mozilla::intl::DateTimeField::Weekday; + } else if (StringEqualsLiteral(code, "day")) { + field = mozilla::intl::DateTimeField::Day; + } else if (StringEqualsLiteral(code, "dayPeriod")) { + field = mozilla::intl::DateTimeField::DayPeriod; + } else if (StringEqualsLiteral(code, "hour")) { + field = mozilla::intl::DateTimeField::Hour; + } else if (StringEqualsLiteral(code, "minute")) { + field = mozilla::intl::DateTimeField::Minute; + } else if (StringEqualsLiteral(code, "second")) { + field = mozilla::intl::DateTimeField::Second; + } else if (StringEqualsLiteral(code, "timeZoneName")) { + field = mozilla::intl::DateTimeField::TimeZoneName; + } else { + ReportInvalidOptionError(cx, "dateTimeField", code); + return false; + } + + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + mozilla::intl::DateTimePatternGenerator* dtpgen = + sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); + if (!dtpgen) { + return false; + } + + result = dn->GetDateTimeField(buffer, field, *dtpgen, fallback); + } + + if (result.isErr()) { + switch (result.unwrapErr()) { + case mozilla::intl::DisplayNamesError::InternalError: + intl::ReportInternalError(cx); + break; + case mozilla::intl::DisplayNamesError::OutOfMemory: + ReportOutOfMemory(cx); + break; + case mozilla::intl::DisplayNamesError::InvalidOption: + ReportInvalidOptionError(cx, type, code); + break; + case mozilla::intl::DisplayNamesError::DuplicateVariantSubtag: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG); + break; + case mozilla::intl::DisplayNamesError::InvalidLanguageTag: + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG); + break; + } + return false; + } + + JSString* str = buffer.toString(cx); + if (!str) { + return false; + } + + if (str->empty()) { + args.rval().setUndefined(); + } else { + args.rval().setString(str); + } + + return true; +} |