From 0172619642b035629f25bc8dc53d945d72bb2a2d Mon Sep 17 00:00:00 2001 From: Martok Date: Sun, 12 Feb 2023 22:32:41 +0100 Subject: [PATCH 01/24] Issue #2046 - Move ScopedICUObject into builtin/intl/ScopedICUObject.h --- js/src/builtin/Intl.cpp | 27 +--------------- js/src/builtin/intl/ScopedICUObject.h | 44 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 js/src/builtin/intl/ScopedICUObject.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 494a0c0473..f24a221fdb 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -22,6 +22,7 @@ #include "jscntxt.h" #include "jsobj.h" +#include "builtin/intl/ScopedICUObject.h" #include "builtin/IntlTimeZoneData.h" #include "ds/Sort.h" #include "unicode/plurrule.h" @@ -183,32 +184,6 @@ icuLocale(const char* locale) return locale; } -// Simple RAII for ICU objects. Unfortunately, ICU's C++ API is uniformly -// unstable, so we can't use its smart pointers for this. -template -class ScopedICUObject -{ - T* ptr_; - - public: - explicit ScopedICUObject(T* ptr) - : ptr_(ptr) - {} - - ~ScopedICUObject() { - if (ptr_) - Delete(ptr_); - } - - // In cases where an object should be deleted on abnormal exits, - // but returned to the caller if everything goes well, call forget() - // to transfer the object just before returning. - T* forget() { - T* tmp = ptr_; - ptr_ = nullptr; - return tmp; - } -}; // The inline capacity we use for the char16_t Vectors. static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; diff --git a/js/src/builtin/intl/ScopedICUObject.h b/js/src/builtin/intl/ScopedICUObject.h new file mode 100644 index 0000000000..4a2e88186d --- /dev/null +++ b/js/src/builtin/intl/ScopedICUObject.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +#ifndef builtin_intl_ScopedICUObject_h +#define builtin_intl_ScopedICUObject_h + +/* + * A simple RAII class to assure ICU objects are automatically deallocated at + * scope end. Unfortunately, ICU's C++ API is uniformly unstable, so we can't + * use its smart pointers for this. + */ + +namespace js { + +template +class ScopedICUObject +{ + T* ptr_; + + public: + explicit ScopedICUObject(T* ptr) + : ptr_(ptr) + {} + + ~ScopedICUObject() { + if (ptr_) + Delete(ptr_); + } + + // In cases where an object should be deleted on abnormal exits, + // but returned to the caller if everything goes well, call forget() + // to transfer the object just before returning. + T* forget() { + T* tmp = ptr_; + ptr_ = nullptr; + return tmp; + } +}; + +} // namespace js + +#endif /* builtin_intl_ScopedICUObject_h */ \ No newline at end of file From 50e3c1f5e65f7c1cd68d1217c5f5ab081c6ec019 Mon Sep 17 00:00:00 2001 From: Martok Date: Sun, 12 Feb 2023 23:27:21 +0100 Subject: [PATCH 02/24] Issue #2046 - Move functionality used in the implementation of multiple Intl.* constructors into builtin/intl/CommonFunctions.* --- js/src/builtin/Intl.cpp | 363 ++++++++---------------- js/src/builtin/intl/CommonFunctions.cpp | 116 ++++++++ js/src/builtin/intl/CommonFunctions.h | 105 +++++++ js/src/moz.build | 1 + 4 files changed, 341 insertions(+), 244 deletions(-) create mode 100644 js/src/builtin/intl/CommonFunctions.cpp create mode 100644 js/src/builtin/intl/CommonFunctions.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index f24a221fdb..28533129ed 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -22,6 +22,7 @@ #include "jscntxt.h" #include "jsobj.h" +#include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/IntlTimeZoneData.h" #include "ds/Sort.h" @@ -56,137 +57,11 @@ using mozilla::IsNaN; using mozilla::IsNegativeZero; using mozilla::PodCopy; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; -/* - * Pervasive note: ICU functions taking a UErrorCode in/out parameter always - * test that parameter before doing anything, and will return immediately if - * the value indicates that a failure occurred in a prior ICU call, - * without doing anything else. See - * http://userguide.icu-project.org/design#TOC-Error-Handling - */ - -/******************** Common to Intl constructors ********************/ - -static bool -IntlInitialize(JSContext* cx, HandleObject obj, Handle initializer, - HandleValue locales, HandleValue options) -{ - RootedValue initializerValue(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), initializer, &initializerValue)) - return false; - MOZ_ASSERT(initializerValue.isObject()); - MOZ_ASSERT(initializerValue.toObject().is()); - - FixedInvokeArgs<3> args(cx); - - args[0].setObject(*obj); - args[1].set(locales); - args[2].set(options); - - RootedValue thisv(cx, NullValue()); - RootedValue ignored(cx); - return js::Call(cx, initializerValue, thisv, args, &ignored); -} - -static bool -CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions) -{ - RootedObject options(cx, NewObjectWithGivenProto(cx, nullptr)); - if (!options) - return false; - defaultOptions.setObject(*options); - return true; -} - -// CountAvailable and GetAvailable describe the signatures used for ICU API -// to determine available locales for various functionality. -typedef int32_t -(* CountAvailable)(); - -typedef const char* -(* GetAvailable)(int32_t localeIndex); - -static bool -intl_availableLocales(JSContext* cx, CountAvailable countAvailable, - GetAvailable getAvailable, MutableHandleValue result) -{ - RootedObject locales(cx, NewObjectWithGivenProto(cx, nullptr)); - if (!locales) - return false; - - uint32_t count = countAvailable(); - RootedValue t(cx, BooleanValue(true)); - for (uint32_t i = 0; i < count; i++) { - const char* locale = getAvailable(i); - auto lang = DuplicateString(cx, locale); - if (!lang) - return false; - char* p; - while ((p = strchr(lang.get(), '_'))) - *p = '-'; - RootedAtom a(cx, Atomize(cx, lang.get(), strlen(lang.get()))); - if (!a) - return false; - if (!DefineProperty(cx, locales, a->asPropertyName(), t, nullptr, nullptr, - JSPROP_ENUMERATE)) - { - return false; - } - } - - result.setObject(*locales); - return true; -} - -/** - * Returns the object holding the internal properties for obj. - */ -static JSObject* -GetInternals(JSContext* cx, HandleObject obj) -{ - RootedValue getInternalsValue(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().getInternals, - &getInternalsValue)) - { - return nullptr; - } - MOZ_ASSERT(getInternalsValue.isObject()); - MOZ_ASSERT(getInternalsValue.toObject().is()); - - FixedInvokeArgs<1> args(cx); - - args[0].setObject(*obj); - - RootedValue v(cx, NullValue()); - if (!js::Call(cx, getInternalsValue, v, args, &v)) - return nullptr; - - return &v.toObject(); -} - -static bool -equal(const char* s1, const char* s2) -{ - return !strcmp(s1, s2); -} - -static bool -equal(JSAutoByteString& s1, const char* s2) -{ - return !strcmp(s1.ptr(), s2); -} - -static const char* -icuLocale(const char* locale) -{ - if (equal(locale, "und")) - return ""; // ICU root locale - return locale; -} - - -// The inline capacity we use for the char16_t Vectors. -static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; /******************** Collator ********************/ @@ -295,7 +170,7 @@ Collator(JSContext* cx, const CallArgs& args, bool construct) RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); // Step 6. - if (!IntlInitialize(cx, obj, cx->names().InitializeCollator, locales, options)) + if (!intl::InitializeObject(cx, obj, cx->names().InitializeCollator, locales, options)) return false; args.rval().setObject(*obj); @@ -376,11 +251,11 @@ CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle } RootedValue options(cx); - if (!CreateDefaultOptions(cx, &options)) + if (!intl::CreateDefaultOptions(cx, &options)) return nullptr; // 10.2.1 and 10.3 - if (!IntlInitialize(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) + if (!intl::InitializeObject(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) return nullptr; // 8.1 @@ -398,7 +273,7 @@ js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args.length() == 0); RootedValue result(cx); - if (!intl_availableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) + if (!GetAvailableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) return false; args.rval().set(result); return true; @@ -417,14 +292,14 @@ js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) UErrorCode status = U_ZERO_ERROR; UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(values); uint32_t count = uenum_count(values, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -436,7 +311,7 @@ js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) for (uint32_t i = 0; i < count; i++) { const char* collation = uenum_next(values, nullptr, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -444,18 +319,18 @@ js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) // "The values 'standard' and 'search' must not be used as elements in // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co // array." - if (equal(collation, "standard") || equal(collation, "search")) + if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) continue; // ICU returns old-style keyword values; map them to BCP 47 equivalents // (see http://bugs.icu-project.org/trac/ticket/9620). - if (equal(collation, "dictionary")) + if (StringsAreEqual(collation, "dictionary")) collation = "dict"; - else if (equal(collation, "gb2312han")) + else if (StringsAreEqual(collation, "gb2312han")) collation = "gb2312"; - else if (equal(collation, "phonebook")) + else if (StringsAreEqual(collation, "phonebook")) collation = "phonebk"; - else if (equal(collation, "traditional")) + else if (StringsAreEqual(collation, "traditional")) collation = "trad"; RootedString jscollation(cx, JS_NewStringCopyZ(cx, collation)); @@ -479,7 +354,7 @@ NewUCollator(JSContext* cx, HandleObject collator) { RootedValue value(cx); - RootedObject internals(cx, GetInternals(cx, collator)); + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); if (!internals) return nullptr; @@ -503,7 +378,7 @@ NewUCollator(JSContext* cx, HandleObject collator) JSAutoByteString usage(cx, value.toString()); if (!usage) return nullptr; - if (equal(usage, "search")) { + if (StringsAreEqual(usage, "search")) { // ICU expects search as a Unicode locale extension on locale. // Unicode locale extensions must occur before private use extensions. const char* oldLocale = locale.ptr(); @@ -542,15 +417,15 @@ NewUCollator(JSContext* cx, HandleObject collator) JSAutoByteString sensitivity(cx, value.toString()); if (!sensitivity) return nullptr; - if (equal(sensitivity, "base")) { + if (StringsAreEqual(sensitivity, "base")) { uStrength = UCOL_PRIMARY; - } else if (equal(sensitivity, "accent")) { + } else if (StringsAreEqual(sensitivity, "accent")) { uStrength = UCOL_SECONDARY; - } else if (equal(sensitivity, "case")) { + } else if (StringsAreEqual(sensitivity, "case")) { uStrength = UCOL_PRIMARY; uCaseLevel = UCOL_ON; } else { - MOZ_ASSERT(equal(sensitivity, "variant")); + MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); uStrength = UCOL_TERTIARY; } @@ -575,18 +450,18 @@ NewUCollator(JSContext* cx, HandleObject collator) JSAutoByteString caseFirst(cx, value.toString()); if (!caseFirst) return nullptr; - if (equal(caseFirst, "upper")) + if (StringsAreEqual(caseFirst, "upper")) uCaseFirst = UCOL_UPPER_FIRST; - else if (equal(caseFirst, "lower")) + else if (StringsAreEqual(caseFirst, "lower")) uCaseFirst = UCOL_LOWER_FIRST; else - MOZ_ASSERT(equal(caseFirst, "false")); + MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); } UErrorCode status = U_ZERO_ERROR; - UCollator* coll = ucol_open(icuLocale(locale.ptr()), &status); + UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } @@ -598,7 +473,7 @@ NewUCollator(JSContext* cx, HandleObject collator) ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); if (U_FAILURE(status)) { ucol_close(coll); - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } @@ -799,7 +674,7 @@ NumberFormat(JSContext* cx, const CallArgs& args, bool construct) RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); // Step 3. - if (!IntlInitialize(cx, obj, cx->names().InitializeNumberFormat, locales, options)) + if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options)) return false; args.rval().setObject(*obj); @@ -885,11 +760,11 @@ CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeNumberFormat, UndefinedHandleValue, + if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue, options)) { return nullptr; @@ -910,7 +785,7 @@ js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args.length() == 0); RootedValue result(cx); - if (!intl_availableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) + if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) return false; args.rval().set(result); return true; @@ -928,9 +803,9 @@ js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) return false; UErrorCode status = U_ZERO_ERROR; - UNumberingSystem* numbers = unumsys_open(icuLocale(locale.ptr()), &status); + UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -957,7 +832,7 @@ js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) static UNumberFormat* NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) { - RootedObject internals(cx, GetInternals(cx, pluralRules)); + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); if (!internals) return nullptr; @@ -1007,9 +882,9 @@ NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) } UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, icuLocale(locale.ptr()), nullptr, &status); + UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } ScopedICUObject toClose(nf); @@ -1037,7 +912,7 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) { RootedValue value(cx); - RootedObject internals(cx, GetInternals(cx, numberFormat)); + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); if (!internals) return nullptr; @@ -1070,7 +945,7 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) if (!style) return nullptr; - if (equal(style, "currency")) { + if (StringsAreEqual(style, "currency")) { if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) return nullptr; currency = value.toString(); @@ -1088,18 +963,18 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) JSAutoByteString currencyDisplay(cx, value.toString()); if (!currencyDisplay) return nullptr; - if (equal(currencyDisplay, "code")) { + if (StringsAreEqual(currencyDisplay, "code")) { uStyle = UNUM_CURRENCY_ISO; - } else if (equal(currencyDisplay, "symbol")) { + } else if (StringsAreEqual(currencyDisplay, "symbol")) { uStyle = UNUM_CURRENCY; } else { - MOZ_ASSERT(equal(currencyDisplay, "name")); + MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); uStyle = UNUM_CURRENCY_PLURAL; } - } else if (equal(style, "percent")) { + } else if (StringsAreEqual(style, "percent")) { uStyle = UNUM_PERCENT; } else { - MOZ_ASSERT(equal(style, "decimal")); + MOZ_ASSERT(StringsAreEqual(style, "decimal")); uStyle = UNUM_DECIMAL; } @@ -1136,9 +1011,9 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) uUseGrouping = value.toBoolean(); UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(uStyle, nullptr, 0, icuLocale(locale.ptr()), nullptr, &status); + UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } ScopedICUObject toClose(nf); @@ -1146,7 +1021,7 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) if (uCurrency) { unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } } @@ -1197,7 +1072,7 @@ PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, MOZ_ASSERT(size == resultSize); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -1296,7 +1171,7 @@ intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHand UFieldPositionIterator* fpositer = ufieldpositer_open(&status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -1782,7 +1657,7 @@ DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); // Step 3. - if (!IntlInitialize(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) + if (!intl::InitializeObject(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) return false; args.rval().setObject(*obj); @@ -1866,11 +1741,11 @@ CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeDateTimeFormat, UndefinedHandleValue, + if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, options)) { return nullptr; @@ -1891,7 +1766,7 @@ js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp MOZ_ASSERT(args.length() == 0); RootedValue result(cx); - if (!intl_availableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) + if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) return false; args.rval().set(result); return true; @@ -1902,11 +1777,11 @@ js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp static const char* bcp47CalendarName(const char* icuName) { - if (equal(icuName, "ethiopic-amete-alem")) + if (StringsAreEqual(icuName, "ethiopic-amete-alem")) return "ethioaa"; - if (equal(icuName, "gregorian")) + if (StringsAreEqual(icuName, "gregorian")) return "gregory"; - if (equal(icuName, "islamic-civil")) + if (StringsAreEqual(icuName, "islamic-civil")) return "islamicc"; return icuName; } @@ -1938,7 +1813,7 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) const char* calendar = ucal_getType(cal, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -1954,21 +1829,21 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) // Now get the calendars that "would make a difference", i.e., not the default. UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(values); uint32_t count = uenum_count(values, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } for (; count > 0; count--) { const char* calendar = uenum_next(values, nullptr, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2057,7 +1932,7 @@ static bool IsLegacyICUTimeZone(const char* timeZone) { for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { - if (equal(timeZone, legacyTimeZone)) + if (StringsAreEqual(timeZone, legacyTimeZone)) return true; } return false; @@ -2081,7 +1956,7 @@ js::SharedIntlData::ensureTimeZones(JSContext* cx) UErrorCode status = U_ZERO_ERROR; UEnumeration* values = ucal_openTimeZones(&status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(values); @@ -2091,7 +1966,7 @@ js::SharedIntlData::ensureTimeZones(JSContext* cx) int32_t size; const char* rawTimeZone = uenum_next(values, &size, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2325,7 +2200,7 @@ js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) Char16ToUChar(chars.begin()), size, isSystemID, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2363,7 +2238,7 @@ js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) ucal_getDefaultTimeZone(Char16ToUChar(chars.begin()), size, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2386,14 +2261,14 @@ js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { const char* rootLocale = ""; UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(cal); int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2425,9 +2300,9 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(icuLocale(locale.ptr()), &status); + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(gen); @@ -2435,7 +2310,7 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) int32_t size = udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), skeletonLen, nullptr, 0, &status); if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedJSFreePtr pattern(cx->pod_malloc(size + 1)); @@ -2446,7 +2321,7 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), skeletonLen, pattern, size, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2466,7 +2341,7 @@ NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) { RootedValue value(cx); - RootedObject internals(cx, GetInternals(cx, dateTimeFormat)); + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); if (!internals) return nullptr; @@ -2504,10 +2379,10 @@ NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) UErrorCode status = U_ZERO_ERROR; UDateFormat* df = - udat_open(UDAT_PATTERN, UDAT_PATTERN, icuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, + udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, uPattern, uPatternLength, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return nullptr; } @@ -2542,7 +2417,7 @@ intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue udat_format(df, x, Char16ToUChar(chars.begin()), size, nullptr, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2656,7 +2531,7 @@ intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHand UErrorCode status = U_ZERO_ERROR; UFieldPositionIterator* fpositer = ufieldpositer_open(&status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(fpositer); @@ -2671,7 +2546,7 @@ intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHand udat_formatForFields(df, x, Char16ToUChar(chars.begin()), resultSize, fpositer, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -2899,7 +2774,7 @@ PluralRules(JSContext* cx, const CallArgs& args, bool construct) RootedValue locales(cx, args.get(0)); RootedValue options(cx, args.get(1)); - if (!IntlInitialize(cx, obj, cx->names().InitializePluralRules, locales, options)) + if (!intl::InitializeObject(cx, obj, cx->names().InitializePluralRules, locales, options)) return false; args.rval().setObject(*obj); @@ -2960,10 +2835,10 @@ CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializePluralRules, UndefinedHandleValue, + if (!intl::InitializeObject(cx, proto, cx->names().InitializePluralRules, UndefinedHandleValue, options)) { return nullptr; @@ -2985,7 +2860,7 @@ js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp) RootedValue result(cx); // We're going to use ULocale availableLocales as per ICU recommendation: // https://ssl.icu-project.org/trac/ticket/12756 - if (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) return false; args.rval().set(result); return true; @@ -3004,7 +2879,7 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) ScopedICUObject closeNumberFormat(nf); - RootedObject internals(cx, GetInternals(cx, pluralRules)); + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); if (!internals) return false; @@ -3046,7 +2921,7 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) UFormattable* fmt = unum_parseToUFormattable(nf, nullptr, uFmtNumValue, stableChars.twoByteRange().length(), 0, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3054,22 +2929,22 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) double y = ufmt_getDouble(fmt, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } UPluralType category; - if (equal(type, "cardinal")) { + if (StringsAreEqual(type, "cardinal")) { category = UPLURAL_TYPE_CARDINAL; } else { - MOZ_ASSERT(equal(type, "ordinal")); + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); category = UPLURAL_TYPE_ORDINAL; } - UPluralRules* pr = uplrules_openForType(icuLocale(locale.ptr()), category, &status); + UPluralRules* pr = uplrules_openForType(IcuLocale(locale.ptr()), category, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3087,7 +2962,7 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) uplrules_select(pr, y, Char16ToUChar(chars.begin()), size, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3117,20 +2992,20 @@ js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) UPluralType category; - if (equal(type, "cardinal")) { + if (StringsAreEqual(type, "cardinal")) { category = UPLURAL_TYPE_CARDINAL; } else { - MOZ_ASSERT(equal(type, "ordinal")); + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); category = UPLURAL_TYPE_ORDINAL; } UPluralRules* pr = uplrules_openForType( - icuLocale(locale.ptr()), + IcuLocale(locale.ptr()), category, &status ); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3144,7 +3019,7 @@ js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) UEnumeration* ue = uenum_openFromStringEnumeration(kwenum, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3162,7 +3037,7 @@ js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) do { cat = uenum_next(ue, &catSize, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3271,7 +3146,7 @@ RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) RootedValue options(cx, args.get(1)); // Step 3. - if (!IntlInitialize(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) + if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) return false; args.rval().setObject(*relativeTimeFormat); @@ -3320,10 +3195,10 @@ CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeRelativeTimeFormat, UndefinedHandleValue, + if (!intl::InitializeObject(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue, options)) { return nullptr; @@ -3346,7 +3221,7 @@ js::intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value RootedValue result(cx); // We're going to use ULocale availableLocales as per ICU recommendation: // https://ssl.icu-project.org/trac/ticket/12756 - if (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) return false; args.rval().set(result); return true; @@ -3374,7 +3249,7 @@ js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) RootedObject relativeTimeFormat(cx, &args[0].toObject()); - RootedObject internals(cx, GetInternals(cx, relativeTimeFormat)); + RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); if (!internals) return false; @@ -3452,10 +3327,10 @@ js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) return false; UErrorCode status = U_ZERO_ERROR; URelativeDateTimeFormatter* rtf = - ureldatefmt_open(icuLocale(locale.ptr()), nullptr, relDateTimeStyle, + ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3495,7 +3370,7 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) int32_t uTimeZoneLength = 0; UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.ptr(), UCAL_DEFAULT, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject toClose(cal); @@ -3518,7 +3393,7 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) UCalendarWeekdayType prevDayType = ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3528,7 +3403,7 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) UCalendarDaysOfWeek dayOfWeek = static_cast(i); UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3548,7 +3423,7 @@ js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) // At the time this code was added, ICU apparently never behaves this way, // so just throw, so that users will report a bug and we can decide what to // do. - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; default: break; @@ -3614,19 +3489,19 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) UErrorCode status = U_ZERO_ERROR; UDateFormat* fmt = - udat_open(UDAT_DEFAULT, UDAT_DEFAULT, icuLocale(locale.ptr()), + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(locale.ptr()), nullptr, 0, nullptr, 0, &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject datToClose(fmt); // UDateTimePatternGenerator will be needed for translations of date and // time fields like "month", "week", "day" etc. - UDateTimePatternGenerator* dtpg = udatpg_open(icuLocale(locale.ptr()), &status); + UDateTimePatternGenerator* dtpg = udatpg_open(IcuLocale(locale.ptr()), &status); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } ScopedICUObject datPgToClose(dtpg); @@ -3703,7 +3578,7 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } @@ -3727,12 +3602,12 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return false; } - if (equal(style, "narrow")) { + if (StringsAreEqual(style, "narrow")) { symbolType = UDAT_STANDALONE_NARROW_MONTHS; - } else if (equal(style, "short")) { + } else if (StringsAreEqual(style, "short")) { symbolType = UDAT_STANDALONE_SHORT_MONTHS; } else { - MOZ_ASSERT(equal(style, "long")); + MOZ_ASSERT(StringsAreEqual(style, "long")); symbolType = UDAT_STANDALONE_MONTHS; } @@ -3770,12 +3645,12 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return false; } - if (equal(style, "narrow")) { + if (StringsAreEqual(style, "narrow")) { symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; - } else if (equal(style, "short")) { + } else if (StringsAreEqual(style, "short")) { symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; } else { - MOZ_ASSERT(equal(style, "long")); + MOZ_ASSERT(StringsAreEqual(style, "long")); symbolType = UDAT_STANDALONE_WEEKDAYS; } @@ -3835,7 +3710,7 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) resultSize, &status); } if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + intl::ReportInternalError(cx); return false; } diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp new file mode 100644 index 0000000000..c211ff42ed --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +/* Operations used to implement multiple Intl.* classes. */ + +#include "builtin/intl/CommonFunctions.h" + +#include "mozilla/Assertions.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" // for GetErrorMessage, JSMSG_INTERNAL_INTL_ERROR +#include "jsobj.h" + +#include "js/Value.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" + +#include "jsobjinlines.h" + +bool +js::intl::CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions) +{ + RootedObject options(cx, NewObjectWithGivenProto(cx, nullptr)); + if (!options) + return false; + defaultOptions.setObject(*options); + return true; +} + +bool +js::intl::InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue locales, HandleValue options) +{ + RootedValue initializerValue(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), initializer, &initializerValue)) + return false; + MOZ_ASSERT(initializerValue.isObject()); + MOZ_ASSERT(initializerValue.toObject().is()); + + FixedInvokeArgs<3> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + + RootedValue thisv(cx, NullValue()); + RootedValue ignored(cx); + return js::Call(cx, initializerValue, thisv, args, &ignored); +} + +/** + * Returns the object holding the internal properties for obj. + */ +JSObject* +js::intl::GetInternalsObject(JSContext* cx, HandleObject obj) +{ + RootedValue getInternalsValue(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().getInternals, + &getInternalsValue)) + { + return nullptr; + } + MOZ_ASSERT(getInternalsValue.isObject()); + MOZ_ASSERT(getInternalsValue.toObject().is()); + + FixedInvokeArgs<1> args(cx); + + args[0].setObject(*obj); + + RootedValue v(cx, NullValue()); + if (!js::Call(cx, getInternalsValue, v, args, &v)) + return nullptr; + + return &v.toObject(); +} + +void +js::intl::ReportInternalError(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); +} + +bool +js::intl::GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, + GetAvailable getAvailable, MutableHandleValue result) +{ + RootedObject locales(cx, NewObjectWithGivenProto(cx, nullptr)); + if (!locales) + return false; + + uint32_t count = countAvailable(); + RootedValue t(cx, BooleanValue(true)); + for (uint32_t i = 0; i < count; i++) { + const char* locale = getAvailable(i); + auto lang = DuplicateString(cx, locale); + if (!lang) + return false; + char* p; + while ((p = strchr(lang.get(), '_'))) + *p = '-'; + RootedAtom a(cx, Atomize(cx, lang.get(), strlen(lang.get()))); + if (!a) + return false; + if (!DefineProperty(cx, locales, a->asPropertyName(), t, nullptr, nullptr, + JSPROP_ENUMERATE)) + { + return false; + } + } + + result.setObject(*locales); + return true; +} diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h new file mode 100644 index 0000000000..e8bbb69077 --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_CommonFunctions_h +#define builtin_intl_CommonFunctions_h + +#include "mozilla/Assertions.h" +#include "mozilla/TypeTraits.h" + +#include +#include +#include + +#include "js/RootingAPI.h" +#include "js/Vector.h" +#include "vm/String.h" + +namespace JS { class Value; } + +class JSObject; + +namespace js { + +namespace intl { + +/** + * Setup the |options| argument of |IntlInitialize| + */ +extern bool +CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions); + +/** + * Initialize a new Intl.* object using the named self-hosted function. + */ +extern bool +InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue locales, HandleValue options); + +/** + * Returns the object holding the internal properties for obj. + */ +extern JSObject* +GetInternalsObject(JSContext* cx, JS::Handle obj); + +/** Report an Intl internal error not directly tied to a spec step. */ +extern void +ReportInternalError(JSContext* cx); + +static inline bool +StringsAreEqual(const char* s1, const char* s2) +{ + return !strcmp(s1, s2); +} + +static inline bool +StringsAreEqual(JSAutoByteString& s1, const char* s2) +{ + return !strcmp(s1.ptr(), s2); +} + +static inline const char* +IcuLocale(const char* locale) +{ + if (StringsAreEqual(locale, "und")) + return ""; // ICU root locale + + return locale; +} + +// Starting with ICU 59, UChar defaults to char16_t. +static_assert(mozilla::IsSame::value, + "SpiderMonkey doesn't support redefining UChar to a different type"); + +// The inline capacity we use for a Vector. Use this to ensure that +// our uses of ICU string functions, below and elsewhere, will try to fill the +// buffer's entire inline capacity before growing it and heap-allocating. +static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; + +// CountAvailable and GetAvailable describe the signatures used for ICU API +// to determine available locales for various functionality. +using CountAvailable = int32_t (*)(); +using GetAvailable = const char* (*)(int32_t localeIndex); + +/** + * Return an object whose own property names are the locales indicated as + * available by |countAvailable| that provides an overall count, and by + * |getAvailable| that when called passing a number less than that count, + * returns the corresponding locale as a borrowed string. For example: + * + * RootedValue v(cx); + * if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &v)) + * return false; + */ +extern bool +GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, GetAvailable getAvailable, + JS::MutableHandle result); + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_CommonFunctions_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 948840139c..165ccd2d0c 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -115,6 +115,7 @@ UNIFIED_SOURCES += [ 'builtin/AtomicsObject.cpp', 'builtin/Eval.cpp', 'builtin/Intl.cpp', + 'builtin/intl/CommonFunctions.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', From eee13d26532623b69d434b4ca1038bb6bc13c3b3 Mon Sep 17 00:00:00 2001 From: Martok Date: Mon, 13 Feb 2023 00:23:46 +0100 Subject: [PATCH 03/24] Issue #2046 - Create helper method to call ICU string conversion methods Based-on: m-c 1333844 --- js/src/builtin/Intl.cpp | 211 +++++--------------------- js/src/builtin/intl/CommonFunctions.h | 40 +++++ 2 files changed, 78 insertions(+), 173 deletions(-) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 28533129ed..688b7698a8 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -57,6 +57,7 @@ using mozilla::IsNaN; using mozilla::IsNegativeZero; using mozilla::PodCopy; +using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; @@ -1050,33 +1051,9 @@ PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, if (IsNegativeZero(*x)) *x = 0.0; - MOZ_ASSERT(formattedChars.length() == 0, - "formattedChars must initially be empty"); - MOZ_ALWAYS_TRUE(formattedChars.resize(INITIAL_CHAR_BUFFER_SIZE)); - UErrorCode status = U_ZERO_ERROR; - - int32_t resultSize; - resultSize = - unum_formatDoubleForFields(nf, *x, - Char16ToUChar(formattedChars.begin()), INITIAL_CHAR_BUFFER_SIZE, - fpositer, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!formattedChars.resize(size_t(resultSize))) - return false; - status = U_ZERO_ERROR; -#ifdef DEBUG - int32_t size = -#endif - unum_formatDoubleForFields(nf, *x, Char16ToUChar(formattedChars.begin()), resultSize, - fpositer, &status); - MOZ_ASSERT(size == resultSize); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - return formattedChars.resize(size_t(resultSize)); + return CallICU(cx, formattedChars, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); + }) >= 0; } static bool @@ -2182,30 +2159,10 @@ js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) mozilla::Range tzchars = stableChars.twoByteRange(); - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UBool* isSystemID = nullptr; - UErrorCode status = U_ZERO_ERROR; - int32_t size = ucal_getCanonicalTimeZoneID(Char16ToUChar(tzchars.begin().get()), - tzchars.length(), Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, isSystemID, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - MOZ_ASSERT(size >= 0); - if (!chars.resize(size_t(size))) - return false; - status = U_ZERO_ERROR; - ucal_getCanonicalTimeZoneID(Char16ToUChar(tzchars.begin().get()), tzchars.length(), - Char16ToUChar(chars.begin()), size, isSystemID, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - MOZ_ASSERT(size >= 0); - JSString* str = NewStringCopyN(cx, chars.begin(), size_t(size)); + JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { + return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), + chars, size, nullptr, status); + }); if (!str) return false; args.rval().setString(str); @@ -2223,27 +2180,7 @@ js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) // needed. js::ResyncICUDefaultTimeZone(); - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - int32_t size = ucal_getDefaultTimeZone(Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - MOZ_ASSERT(size >= 0); - if (!chars.resize(size_t(size))) - return false; - status = U_ZERO_ERROR; - ucal_getDefaultTimeZone(Char16ToUChar(chars.begin()), size, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - MOZ_ASSERT(size >= 0); - JSString* str = NewStringCopyN(cx, chars.begin(), size_t(size)); + JSString* str = CallICU(cx, ucal_getDefaultTimeZone); if (!str) return false; args.rval().setString(str); @@ -2307,25 +2244,11 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) } ScopedICUObject toClose(gen); - int32_t size = udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), - skeletonLen, nullptr, 0, &status); - if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) { - intl::ReportInternalError(cx); - return false; - } - ScopedJSFreePtr pattern(cx->pod_malloc(size + 1)); - if (!pattern) - return false; - pattern[size] = '\0'; - status = U_ZERO_ERROR; - udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), - skeletonLen, pattern, size, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - RootedString str(cx, JS_NewUCStringCopyZ(cx, reinterpret_cast(pattern.get()))); + JSString* str = + CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, + chars, size, status); + }); if (!str) return false; args.rval().setString(str); @@ -2404,29 +2327,13 @@ intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue return false; } - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - UErrorCode status = U_ZERO_ERROR; - int size = udat_format(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - nullptr, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(size)) - return false; - status = U_ZERO_ERROR; - udat_format(df, x, Char16ToUChar(chars.begin()), size, nullptr, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - JSString* str = NewStringCopyN(cx, chars.begin(), size); + JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { + return udat_format(df, x, chars, size, nullptr, status); + }); if (!str) return false; result.setString(str); - return true; } @@ -2536,33 +2443,22 @@ intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHand } ScopedICUObject toClose(fpositer); - int resultSize = - udat_formatForFields(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - fpositer, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(resultSize)) - return false; - status = U_ZERO_ERROR; - udat_formatForFields(df, x, Char16ToUChar(chars.begin()), resultSize, fpositer, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); + RootedString overallResult(cx); + overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return udat_formatForFields(df, x, chars, size, fpositer, status); + }); + if (!overallResult) return false; - } RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); if (!partsArray) return false; - if (resultSize == 0) { + if (overallResult->length() == 0) { // An empty string contains no parts, so avoid extra work below. result.setObject(*partsArray); return true; } - RootedString overallResult(cx, NewStringCopyN(cx, chars.begin(), resultSize)); - if (!overallResult) - return false; - size_t lastEndIndex = 0; uint32_t partIndex = 0; @@ -2950,23 +2846,9 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) ScopedICUObject closePluralRules(pr); - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - int size = uplrules_select(pr, y, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(size)) - return false; - status = U_ZERO_ERROR; - uplrules_select(pr, y, Char16ToUChar(chars.begin()), size, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - JSString* str = NewStringCopyN(cx, chars.begin(), size); + JSString* str = CallICU(cx, [pr, y](UChar* chars, int32_t size, UErrorCode* status) { + return uplrules_select(pr, y, chars, size, status); + }); if (!str) return false; @@ -3333,21 +3215,17 @@ js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) intl::ReportInternalError(cx); return false; } - - int32_t size; - - if (relDateTimeNumeric == RelativeTimeNumeric::Auto) { - size = ureldatefmt_format(rtf, t, relDateTimeUnit, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - } else { - MOZ_ASSERT(relDateTimeNumeric == RelativeTimeNumeric::Always); - size = ureldatefmt_formatNumeric(rtf, t, relDateTimeUnit, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - } - ScopedICUObject closeRelativeTimeFormat(rtf); - JSString* str = NewStringCopyN(cx, chars.begin(), size); + JSString* str = + CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, + UErrorCode* status) + { + auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto + ? ureldatefmt_format + : ureldatefmt_formatNumeric; + return fmt(rtf, t, relDateTimeUnit, chars, size, status); + }); if (!str) return false; @@ -3699,22 +3577,9 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return false; } - int32_t resultSize = - udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(resultSize)) - return false; - status = U_ZERO_ERROR; - udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), - resultSize, &status); - } - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - JSString* word = NewStringCopyN(cx, chars.begin(), resultSize); + JSString* word = CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size, UErrorCode* status) { + return udat_getSymbols(fmt, symbolType, index, chars, size, status); + }); if (!word) return false; diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h index e8bbb69077..90d3a5636b 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -79,6 +79,46 @@ static_assert(mozilla::IsSame::value, // buffer's entire inline capacity before growing it and heap-allocating. static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; +template +static int32_t +CallICU(JSContext* cx, Vector& chars, const ICUStringFunction& strFn) +{ + MOZ_ASSERT(chars.length() == 0); + MOZ_ALWAYS_TRUE(chars.resize(InlineCapacity)); + + UErrorCode status = U_ZERO_ERROR; + int32_t size = strFn(chars.begin(), InlineCapacity, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + MOZ_ASSERT(size >= 0); + if (!chars.resize(size_t(size))) + return -1; + status = U_ZERO_ERROR; + strFn(chars.begin(), size, &status); + } + if (U_FAILURE(status)) { + ReportInternalError(cx); + return -1; + } + + MOZ_ASSERT(size >= 0); + if (!chars.resize(size_t(size))) + return -1; + return size; +} + +template +static JSString* +CallICU(JSContext* cx, const ICUStringFunction& strFn) +{ + Vector chars(cx); + + int32_t size = CallICU(cx, chars, strFn); + if (size < 0) + return nullptr; + + return NewStringCopyN(cx, chars.begin(), size_t(size)); +} + // CountAvailable and GetAvailable describe the signatures used for ICU API // to determine available locales for various functionality. using CountAvailable = int32_t (*)(); From e7565ff1aef14709057cfc777b4bf78b602f2848 Mon Sep 17 00:00:00 2001 From: Martok Date: Mon, 13 Feb 2023 01:33:19 +0100 Subject: [PATCH 04/24] Issue #2046 - Move Intl.NumberFormat functionality into builtin/intl/NumberFormat.* --- js/src/builtin/Intl.cpp | 895 +------------------------ js/src/builtin/Intl.h | 45 -- js/src/builtin/SelfHostingDefines.h | 2 + js/src/builtin/intl/CommonFunctions.h | 1 + js/src/builtin/intl/ICUHeader.h | 23 + js/src/builtin/intl/NumberFormat.cpp | 904 ++++++++++++++++++++++++++ js/src/builtin/intl/NumberFormat.h | 89 +++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 9 files changed, 1025 insertions(+), 936 deletions(-) create mode 100644 js/src/builtin/intl/ICUHeader.h create mode 100644 js/src/builtin/intl/NumberFormat.cpp create mode 100644 js/src/builtin/intl/NumberFormat.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 688b7698a8..4aa46ad7d0 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -23,21 +23,11 @@ #include "jsobj.h" #include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/IntlTimeZoneData.h" #include "ds/Sort.h" -#include "unicode/plurrule.h" -#include "unicode/ucal.h" -#include "unicode/ucol.h" -#include "unicode/udat.h" -#include "unicode/udatpg.h" -#include "unicode/udisplaycontext.h" -#include "unicode/uenum.h" -#include "unicode/unum.h" -#include "unicode/unumsys.h" -#include "unicode/upluralrules.h" -#include "unicode/ureldatefmt.h" -#include "unicode/ustring.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" #include "vm/Interpreter.h" @@ -53,7 +43,6 @@ using namespace js; using mozilla::AssertedCast; using mozilla::IsFinite; -using mozilla::IsNaN; using mozilla::IsNegativeZero; using mozilla::PodCopy; @@ -566,261 +555,6 @@ js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) return true; } - -/******************** NumberFormat ********************/ - -static void numberFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UNUMBER_FORMAT_SLOT = 0; -static const uint32_t NUMBER_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps NumberFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - numberFormat_finalize -}; - -static const Class NumberFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(NUMBER_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &NumberFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().NumberFormat); - return true; -} -#endif - -static const JSFunctionSpec numberFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec numberFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 11.2.1 Intl.NumberFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -NumberFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when NumberFormat is called - // because of backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 11.1.2.1 step 3 - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - // ES Intl 1st ed., 11.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 11.1.2.1 step 5 - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - // ES Intl 1st ed., 11.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &NumberFormatClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 3. - if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return NumberFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return NumberFormat(cx, args, true); -} - -static void -numberFormat_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UNUMBER_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UNumberFormat* nf = static_cast(slot.toPrivate())) - unum_close(nf); - } -} - -static JSObject* -CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &NumberFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 11.2.2 - if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) - return nullptr; - - // 11.3.2 and 11.3.3 - if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) - return nullptr; - - /* - * Install the getter for NumberFormat.prototype.format, which returns a - * bound formatting function for the specified NumberFormat object (suitable - * for passing to methods like Array.prototype.map). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - // 11.2.1 and 11.3 - if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -bool -js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - UErrorCode status = U_ZERO_ERROR; - UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject toClose(numbers); - - const char* name = unumsys_getName(numbers); - RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); - if (!jsname) - return false; - - args.rval().setString(jsname); - return true; -} - /** * * This creates new UNumberFormat with calculated digit formatting @@ -903,629 +637,6 @@ NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) return toClose.forget(); } - -/** - * Returns a new UNumberFormat with the locale and number formatting options - * of the given NumberFormat. - */ -static UNumberFormat* -NewUNumberFormat(JSContext* cx, HandleObject numberFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // UNumberFormat options with default values - UNumberFormatStyle uStyle = UNUM_DECIMAL; - const UChar* uCurrency = nullptr; - uint32_t uMinimumIntegerDigits = 1; - uint32_t uMinimumFractionDigits = 0; - uint32_t uMaximumFractionDigits = 3; - int32_t uMinimumSignificantDigits = -1; - int32_t uMaximumSignificantDigits = -1; - bool uUseGrouping = true; - - // Sprinkle appropriate rooting flavor over things the GC might care about. - RootedString currency(cx); - AutoStableStringChars stableChars(cx); - - // We don't need to look at numberingSystem - it can only be set via - // the Unicode locale extension and is therefore already set on locale. - - if (!GetProperty(cx, internals, internals, cx->names().style, &value)) - return nullptr; - JSAutoByteString style(cx, value.toString()); - if (!style) - return nullptr; - - if (StringsAreEqual(style, "currency")) { - if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) - return nullptr; - currency = value.toString(); - MOZ_ASSERT(currency->length() == 3, - "IsWellFormedCurrencyCode permits only length-3 strings"); - if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) - return nullptr; - // uCurrency remains owned by stableChars. - uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); - if (!uCurrency) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) - return nullptr; - JSAutoByteString currencyDisplay(cx, value.toString()); - if (!currencyDisplay) - return nullptr; - if (StringsAreEqual(currencyDisplay, "code")) { - uStyle = UNUM_CURRENCY_ISO; - } else if (StringsAreEqual(currencyDisplay, "symbol")) { - uStyle = UNUM_CURRENCY; - } else { - MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); - uStyle = UNUM_CURRENCY_PLURAL; - } - } else if (StringsAreEqual(style, "percent")) { - uStyle = UNUM_PERCENT; - } else { - MOZ_ASSERT(StringsAreEqual(style, "decimal")); - uStyle = UNUM_DECIMAL; - } - - RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); - bool hasP; - if (!HasProperty(cx, internals, id, &hasP)) - return nullptr; - if (hasP) { - if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, - &value)) - return nullptr; - uMinimumSignificantDigits = value.toInt32(); - if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, - &value)) - return nullptr; - uMaximumSignificantDigits = value.toInt32(); - } else { - if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, - &value)) - return nullptr; - uMinimumIntegerDigits = AssertedCast(value.toInt32()); - if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, - &value)) - return nullptr; - uMinimumFractionDigits = AssertedCast(value.toInt32()); - if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, - &value)) - return nullptr; - uMaximumFractionDigits = AssertedCast(value.toInt32()); - } - - if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) - return nullptr; - uUseGrouping = value.toBoolean(); - - UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uCurrency) { - unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - } - if (uMinimumSignificantDigits != -1) { - unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); - unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); - unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); - } else { - unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); - unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); - unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); - } - unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); - unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); - - return toClose.forget(); -} - -using FormattedNumberChars = Vector; - -static bool -PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, - UFieldPositionIterator* fpositer, FormattedNumberChars& formattedChars) -{ - // PartitionNumberPattern doesn't consider -0.0 to be negative. - if (IsNegativeZero(*x)) - *x = 0.0; - - return CallICU(cx, formattedChars, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); - }) >= 0; -} - -static bool -intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) -{ - // Passing null for |fpositer| will just not compute partition information, - // letting us common up all ICU number-formatting code. - FormattedNumberChars chars(cx); - if (!PartitionNumberPattern(cx, nf, &x, nullptr, chars)) - return false; - - JSString* str = NewStringCopyN(cx, chars.begin(), chars.length()); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) -{ - // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This - // list is deliberately exhaustive: cases might have to be added/removed if - // this code is compiled with a different ICU with more UNumberFormatFields - // enum initializers. Please guard such cases with appropriate ICU - // version-testing #ifdefs, should cross-version divergence occur. - switch (fieldName) { - case UNUM_INTEGER_FIELD: - if (IsNaN(d)) - return &JSAtomState::nan; - if (!IsFinite(d)) - return &JSAtomState::infinity; - return &JSAtomState::integer; - - case UNUM_GROUPING_SEPARATOR_FIELD: - return &JSAtomState::group; - - case UNUM_DECIMAL_SEPARATOR_FIELD: - return &JSAtomState::decimal; - - case UNUM_FRACTION_FIELD: - return &JSAtomState::fraction; - - case UNUM_SIGN_FIELD: { - MOZ_ASSERT(!IsNegativeZero(d), - "-0 should have been excluded by PartitionNumberPattern"); - - // Manual trawling through the ICU call graph appears to indicate that - // the basic formatting we request will never include a positive sign. - // But this analysis may be mistaken, so don't absolutely trust it. - return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign; - } - - case UNUM_PERCENT_FIELD: - return &JSAtomState::percentSign; - - case UNUM_CURRENCY_FIELD: - return &JSAtomState::currency; - - case UNUM_PERMILL_FIELD: - MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " - "we don't use any user-defined patterns that " - "would require a permill field"); - break; - - case UNUM_EXPONENT_SYMBOL_FIELD: - case UNUM_EXPONENT_SIGN_FIELD: - case UNUM_EXPONENT_FIELD: - MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " - "formatted number, even though UNUM_SCIENTIFIC " - "and scientific notation were never requested"); - break; - - case UNUM_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - break; - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) -{ - UErrorCode status = U_ZERO_ERROR; - - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - MOZ_ASSERT(fpositer); - ScopedICUObject toClose(fpositer); - - FormattedNumberChars chars(cx); - if (!PartitionNumberPattern(cx, nf, &x, fpositer, chars)) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - - RootedString overallResult(cx, NewStringCopyN(cx, chars.begin(), chars.length())); - if (!overallResult) - return false; - - // First, vacuum up fields in the overall formatted string. - - struct Field - { - uint32_t begin; - uint32_t end; - FieldType type; - - // Needed for vector-resizing scratch space. - Field() = default; - - Field(uint32_t begin, uint32_t end, FieldType type) - : begin(begin), end(end), type(type) - {} - }; - - using FieldsVector = Vector; - FieldsVector fields(cx); - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt < endIndexInt, - "erm, aren't fields always non-empty?"); - - FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); - if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) - return false; - } - - // Second, merge sort the fields vector. Expand the vector to have scratch - // space for performing the sort. - size_t fieldsLen = fields.length(); - if (!fields.resizeUninitialized(fieldsLen * 2)) - return false; - - MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, - [](const Field& left, const Field& right, - bool* lessOrEqual) - { - // Sort first by begin index, then to place - // enclosing fields before nested fields. - *lessOrEqual = left.begin < right.begin || - (left.begin == right.begin && - left.end > right.end); - return true; - })); - - // Deallocate the scratch space. - if (!fields.resize(fieldsLen)) - return false; - - // Third, iterate over the sorted field list to generate a sequence of - // parts (what ECMA-402 actually exposes). A part is a maximal character - // sequence entirely within no field or a single most-nested field. - // - // Diagrams may be helpful to illustrate how fields map to parts. Consider - // formatting -28,114,774,228,750.32, the US national surplus (negative - // because it's actually a debt) on March 31, 2021. - // - // var options = - // { style: "currency", currency: "USD", currencyDisplay: "name" }; - // var usdFormatter = new Intl.NumberFormat("en-US", options); - // usdFormatter.format(-28114774228750.32); - // - // The formatted result is "-28,114,774,228,750.32 US dollars". ICU - // identifies these fields in the string: - // - // UNUM_GROUPING_SEPARATOR_FIELD - // | - // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD - // | __________/| | - // | / | | | | - // "-28,114,774,228,750.32 US dollars" - // \________________/ |/ \_______/ - // | | | - // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD - // | - // UNUM_FRACTION_FIELD - // - // These fields map to parts as follows: - // - // integer decimal - // _____|________ | - // / /| |\ |\ |\ | literal - // /| / | | \ | \ | \| | - // "-28,114,774,228,750.32 US dollars" - // | \___|___|___/ |/ \________/ - // | | | | - // | group | currency - // | | - // minusSign fraction - // - // The sign is a part. Each comma is a part, splitting the integer field - // into parts for trillions/billions/&c. digits. The decimal point is a - // part. Cents are a part. The space between cents and currency is a part - // (outside any field). Last, the currency field is a part. - // - // Because parts fully partition the formatted string, we only track the - // end of each part -- the beginning is implicitly the last part's end. - struct Part - { - uint32_t end; - FieldType type; - }; - - class PartGenerator - { - // The fields in order from start to end, then least to most nested. - const FieldsVector& fields; - - // Index of the current field, in |fields|, being considered to - // determine part boundaries. |lastEnd <= fields[index].begin| is an - // invariant. - size_t index; - - // The end index of the last part produced, always less than or equal - // to |limit|, strictly increasing. - uint32_t lastEnd; - - // The length of the overall formatted string. - const uint32_t limit; - - Vector enclosingFields; - - void popEnclosingFieldsEndingAt(uint32_t end) { - MOZ_ASSERT_IF(enclosingFields.length() > 0, - fields[enclosingFields.back()].end >= end); - - while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) - enclosingFields.popBack(); - } - - bool nextPartInternal(Part* part) { - size_t len = fields.length(); - MOZ_ASSERT(index <= len); - - // If we're out of fields, all that remains are part(s) consisting - // of trailing portions of enclosing fields, and maybe a final - // literal part. - if (index == len) { - if (enclosingFields.length() > 0) { - const auto& enclosing = fields[enclosingFields.popCopy()]; - part->end = enclosing.end; - part->type = enclosing.type; - - // If additional enclosing fields end where this part ends, - // pop them as well. - popEnclosingFieldsEndingAt(part->end); - } else { - part->end = limit; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise we still have a field to process. - const Field* current = &fields[index]; - MOZ_ASSERT(lastEnd <= current->begin); - MOZ_ASSERT(current->begin < current->end); - - // But first, deal with inter-field space. - if (lastEnd < current->begin) { - if (enclosingFields.length() > 0) { - // Space between fields, within an enclosing field, is part - // of that enclosing field, until the start of the current - // field or the end of the enclosing field, whichever is - // earlier. - const auto& enclosing = fields[enclosingFields.back()]; - part->end = std::min(enclosing.end, current->begin); - part->type = enclosing.type; - popEnclosingFieldsEndingAt(part->end); - } else { - // If there's no enclosing field, the space is a literal. - part->end = current->begin; - part->type = &JSAtomState::literal; - } - - return true; - } - - // Otherwise, the part spans a prefix of the current field. Find - // the most-nested field containing that prefix. - const Field* next; - do { - current = &fields[index]; - - // If the current field is last, the part extends to its end. - if (++index == len) { - part->end = current->end; - part->type = current->type; - return true; - } - - next = &fields[index]; - MOZ_ASSERT(current->begin <= next->begin); - MOZ_ASSERT(current->begin < next->end); - - // If the next field nests within the current field, push an - // enclosing field. (If there are no nested fields, don't - // bother pushing a field that'd be immediately popped.) - if (current->end > next->begin) { - if (!enclosingFields.append(index - 1)) - return false; - } - - // Do so until the next field begins after this one. - } while (current->begin == next->begin); - - part->type = current->type; - - if (current->end <= next->begin) { - // The next field begins after the current field ends. Therefore - // the current part ends at the end of the current field. - part->end = current->end; - popEnclosingFieldsEndingAt(part->end); - } else { - // The current field encloses the next one. The current part - // ends where the next field/part will start. - part->end = next->begin; - } - - return true; - } - - public: - PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) - : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) - {} - - bool nextPart(bool* hasPart, Part* part) { - // There are no parts left if we've partitioned the entire string. - if (lastEnd == limit) { - MOZ_ASSERT(enclosingFields.length() == 0); - *hasPart = false; - return true; - } - - if (!nextPartInternal(part)) - return false; - - *hasPart = true; - lastEnd = part->end; - return true; - } - }; - - // Finally, generate the result array. - size_t lastEndIndex = 0; - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue propVal(cx); - - PartGenerator gen(cx, fields, chars.length()); - do { - bool hasPart; - Part part; - if (!gen.nextPart(&hasPart, &part)) - return false; - - if (!hasPart) - break; - - FieldType type = part.type; - size_t endIndex = part.end; - - MOZ_ASSERT(lastEndIndex < endIndex); - - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - propVal.setString(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); - if (!partSubstr) - return false; - - propVal.setString(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) - return false; - - propVal.setObject(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, propVal)) - return false; - - lastEndIndex = endIndex; - partIndex++; - } while (true); - - MOZ_ASSERT(lastEndIndex == chars.length(), - "result array must partition the entire string"); - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumber()); - MOZ_ASSERT(args[2].isBoolean()); - - RootedObject numberFormat(cx, &args[0].toObject()); - - // Obtain a UNumberFormat object, cached if possible. - bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatClass; - UNumberFormat* nf; - if (isNumberFormatInstance) { - void* priv = - numberFormat->as().getReservedSlot(UNUMBER_FORMAT_SLOT).toPrivate(); - nf = static_cast(priv); - if (!nf) { - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - numberFormat->as().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nf)); - } - } else { - // There's no good place to cache the ICU number format for an object - // that has been initialized as a NumberFormat but is not a - // NumberFormat instance. One possibility might be to add a - // NumberFormat instance as an internal property to each such object. - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - } - - // Use the UNumberFormat to actually format the number. - double d = args[1].toNumber(); - RootedValue result(cx); - - bool success; - if (args[2].toBoolean()) { - success = intl_FormatNumberToParts(cx, nf, d, &result); - } else { - MOZ_ASSERT(!args[2].toBoolean(), - "shouldn't be doing formatToParts without an ICU that " - "supports it"); - success = intl_FormatNumber(cx, nf, d, &result); - } - - if (!isNumberFormatInstance) - unum_close(nf); - if (!success) - return false; - args.rval().set(result); - return true; -} - - /******************** DateTimeFormat ********************/ static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); @@ -2337,6 +1448,8 @@ intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue return true; } +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + static FieldType GetFieldTypeForFormatField(UDateFormatField fieldName) { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 330b8a4963..1026da5e5e 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -218,51 +218,6 @@ extern MOZ_MUST_USE bool intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); -/******************** NumberFormat ********************/ - -/** - * Returns a new instance of the standard built-in NumberFormat constructor. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: numberFormat = intl_NumberFormat(locales, options) - */ -extern MOZ_MUST_USE bool -intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for number formatting - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_NumberFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns the numbering system type identifier per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * default numbering system for the given locale. - * - * Usage: defaultNumberingSystem = intl_numberingSystem(locale) - */ -extern MOZ_MUST_USE bool -intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a string representing the number x according to the effective - * locale and the formatting options of the given NumberFormat. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - * - * Usage: formatted = intl_FormatNumber(numberFormat, x) - */ -extern MOZ_MUST_USE bool -intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp); - - /******************** DateTimeFormat ********************/ /** diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h index e233f5c397..5bd31f58ec 100644 --- a/js/src/builtin/SelfHostingDefines.h +++ b/js/src/builtin/SelfHostingDefines.h @@ -96,6 +96,8 @@ #define REGEXP_STRING_ITERATOR_FLAGS_SLOT 2 #define REGEXP_STRING_ITERATOR_DONE_SLOT 3 +#define INTL_INTERNALS_OBJECT_SLOT 0 + #define MODULE_OBJECT_ENVIRONMENT_SLOT 1 #define MODULE_OBJECT_STATUS_SLOT 3 #define MODULE_OBJECT_EVALUATION_ERROR_SLOT 4 diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h index 90d3a5636b..aae221b011 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -14,6 +14,7 @@ #include #include +#include "builtin/intl/ICUHeader.h" #include "js/RootingAPI.h" #include "js/Vector.h" #include "vm/String.h" diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h new file mode 100644 index 0000000000..0a0e280838 --- /dev/null +++ b/js/src/builtin/intl/ICUHeader.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_ICUHeader_h +#define builtin_intl_ICUHeader_h + +#include "unicode/plurrule.h" +#include "unicode/ucal.h" +#include "unicode/ucol.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/udisplaycontext.h" +#include "unicode/uenum.h" +#include "unicode/unum.h" +#include "unicode/unumsys.h" +#include "unicode/upluralrules.h" +#include "unicode/ureldatefmt.h" +#include "unicode/ustring.h" + +#endif /* builtin_intl_ICUHeader_h */ diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp new file mode 100644 index 0000000000..4fc08a0920 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -0,0 +1,904 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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.NumberFormat implementation. */ + +#include "builtin/intl/NumberFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" + +#include +#include +#include + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "ds/Sort.h" +#include "js/RootingAPI.h" + +#include "js/TypeDecls.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" + +#include "jsobjinlines.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::IsFinite; +using mozilla::IsNaN; +using mozilla::IsNegativeZero; +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/******************** NumberFormat ********************/ + +const ClassOps NumberFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + NumberFormatObject::finalize +}; + +const Class NumberFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &NumberFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().NumberFormat); + return true; +} +#endif + +static const JSFunctionSpec numberFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec numberFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 11.2.1 Intl.NumberFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +NumberFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when NumberFormat is called + // because of backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 11.1.2.1 step 3 + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + // ES Intl 1st ed., 11.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 11.1.2.1 step 5 + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + // ES Intl 1st ed., 11.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 3. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return NumberFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return NumberFormat(cx, args, true); +} + +void +js::NumberFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UNumberFormat* nf = static_cast(slot.toPrivate())) + unum_close(nf); + } +} + +JSObject* +js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, + &NumberFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 11.2.2 + if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods)) + return nullptr; + + // 11.3.2 and 11.3.3 + if (!JS_DefineFunctions(cx, proto, numberFormat_methods)) + return nullptr; + + /* + * Install the getter for NumberFormat.prototype.format, which returns a + * bound formatting function for the specified NumberFormat object (suitable + * for passing to methods like Array.prototype.map). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + // 11.2.1 and 11.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +bool +js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + UErrorCode status = U_ZERO_ERROR; + UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject toClose(numbers); + + const char* name = unumsys_getName(numbers); + RootedString jsname(cx, JS_NewStringCopyZ(cx, name)); + if (!jsname) + return false; + + args.rval().setString(jsname); + return true; +} + +/** + * Returns a new UNumberFormat with the locale and number formatting options + * of the given NumberFormat. + */ +static UNumberFormat* +NewUNumberFormat(JSContext* cx, HandleObject numberFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // UNumberFormat options with default values + UNumberFormatStyle uStyle = UNUM_DECIMAL; + const UChar* uCurrency = nullptr; + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + bool uUseGrouping = true; + + // Sprinkle appropriate rooting flavor over things the GC might care about. + RootedString currency(cx); + AutoStableStringChars stableChars(cx); + + // We don't need to look at numberingSystem - it can only be set via + // the Unicode locale extension and is therefore already set on locale. + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) + return nullptr; + JSAutoByteString style(cx, value.toString()); + if (!style) + return nullptr; + + if (StringsAreEqual(style, "currency")) { + if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) + return nullptr; + currency = value.toString(); + MOZ_ASSERT(currency->length() == 3, + "IsWellFormedCurrencyCode permits only length-3 strings"); + if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency)) + return nullptr; + // uCurrency remains owned by stableChars. + uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get()); + if (!uCurrency) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) + return nullptr; + JSAutoByteString currencyDisplay(cx, value.toString()); + if (!currencyDisplay) + return nullptr; + if (StringsAreEqual(currencyDisplay, "code")) { + uStyle = UNUM_CURRENCY_ISO; + } else if (StringsAreEqual(currencyDisplay, "symbol")) { + uStyle = UNUM_CURRENCY; + } else { + MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name")); + uStyle = UNUM_CURRENCY_PLURAL; + } + } else if (StringsAreEqual(style, "percent")) { + uStyle = UNUM_PERCENT; + } else { + MOZ_ASSERT(StringsAreEqual(style, "decimal")); + uStyle = UNUM_DECIMAL; + } + + RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); + bool hasP; + if (!HasProperty(cx, internals, id, &hasP)) + return nullptr; + if (hasP) { + if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, + &value)) + return nullptr; + uMinimumSignificantDigits = value.toInt32(); + if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, + &value)) + return nullptr; + uMaximumSignificantDigits = value.toInt32(); + } else { + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) + return nullptr; + uMinimumIntegerDigits = AssertedCast(value.toInt32()); + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) + return nullptr; + uUseGrouping = value.toBoolean(); + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject toClose(nf); + + if (uCurrency) { + unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + } + if (uMinimumSignificantDigits != -1) { + unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); + unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); + unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); + } else { + unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); + unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); + unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); + } + unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping); + unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP); + + return toClose.forget(); +} + +static JSString* +PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x, + UFieldPositionIterator* fpositer) +{ + // PartitionNumberPattern doesn't consider -0.0 to be negative. + if (IsNegativeZero(*x)) + *x = 0.0; + + return CallICU(cx, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status); + }); +} + +bool +js::intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) +{ + // Passing null for |fpositer| will just not compute partition information, + // letting us common up all ICU number-formatting code. + JSString* str = PartitionNumberPattern(cx, nf, &x, nullptr); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d) +{ + // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This + // list is deliberately exhaustive: cases might have to be added/removed if + // this code is compiled with a different ICU with more UNumberFormatFields + // enum initializers. Please guard such cases with appropriate ICU + // version-testing #ifdefs, should cross-version divergence occur. + switch (fieldName) { + case UNUM_INTEGER_FIELD: + if (IsNaN(d)) + return &JSAtomState::nan; + if (!IsFinite(d)) + return &JSAtomState::infinity; + return &JSAtomState::integer; + + case UNUM_GROUPING_SEPARATOR_FIELD: + return &JSAtomState::group; + + case UNUM_DECIMAL_SEPARATOR_FIELD: + return &JSAtomState::decimal; + + case UNUM_FRACTION_FIELD: + return &JSAtomState::fraction; + + case UNUM_SIGN_FIELD: { + MOZ_ASSERT(!IsNegativeZero(d), + "-0 should have been excluded by PartitionNumberPattern"); + + // Manual trawling through the ICU call graph appears to indicate that + // the basic formatting we request will never include a positive sign. + // But this analysis may be mistaken, so don't absolutely trust it. + return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign; + } + + case UNUM_PERCENT_FIELD: + return &JSAtomState::percentSign; + + case UNUM_CURRENCY_FIELD: + return &JSAtomState::currency; + + case UNUM_PERMILL_FIELD: + MOZ_ASSERT_UNREACHABLE("unexpected permill field found, even though " + "we don't use any user-defined patterns that " + "would require a permill field"); + break; + + case UNUM_EXPONENT_SYMBOL_FIELD: + case UNUM_EXPONENT_SIGN_FIELD: + case UNUM_EXPONENT_FIELD: + MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in " + "formatted number, even though UNUM_SCIENTIFIC " + "and scientific notation were never requested"); + break; + + case UNUM_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + break; + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result) +{ + UErrorCode status = U_ZERO_ERROR; + + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + MOZ_ASSERT(fpositer); + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx, PartitionNumberPattern(cx, nf, &x, fpositer)); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + + // First, vacuum up fields in the overall formatted string. + + struct Field + { + uint32_t begin; + uint32_t end; + FieldType type; + + // Needed for vector-resizing scratch space. + Field() = default; + + Field(uint32_t begin, uint32_t end, FieldType type) + : begin(begin), end(end), type(type) + {} + }; + + using FieldsVector = Vector; + FieldsVector fields(cx); + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt < endIndexInt, + "erm, aren't fields always non-empty?"); + + FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x); + if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type)) + return false; + } + + // Second, merge sort the fields vector. Expand the vector to have scratch + // space for performing the sort. + size_t fieldsLen = fields.length(); + if (!fields.resizeUninitialized(fieldsLen * 2)) + return false; + + MOZ_ALWAYS_TRUE(MergeSort(fields.begin(), fieldsLen, fields.begin() + fieldsLen, + [](const Field& left, const Field& right, + bool* lessOrEqual) + { + // Sort first by begin index, then to place + // enclosing fields before nested fields. + *lessOrEqual = left.begin < right.begin || + (left.begin == right.begin && + left.end > right.end); + return true; + })); + + // Deallocate the scratch space. + if (!fields.resize(fieldsLen)) + return false; + + // Third, iterate over the sorted field list to generate a sequence of + // parts (what ECMA-402 actually exposes). A part is a maximal character + // sequence entirely within no field or a single most-nested field. + // + // Diagrams may be helpful to illustrate how fields map to parts. Consider + // formatting -28,114,774,228,750.32, the US national surplus (negative + // because it's actually a debt) on March 31, 2021. + // + // var options = + // { style: "currency", currency: "USD", currencyDisplay: "name" }; + // var usdFormatter = new Intl.NumberFormat("en-US", options); + // usdFormatter.format(-28114774228750.32); + // + // The formatted result is "-28,114,774,228,750.32 US dollars". ICU + // identifies these fields in the string: + // + // UNUM_GROUPING_SEPARATOR_FIELD + // | + // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD + // | __________/| | + // | / | | | | + // "-28,114,774,228,750.32 US dollars" + // \________________/ |/ \_______/ + // | | | + // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD + // | + // UNUM_FRACTION_FIELD + // + // These fields map to parts as follows: + // + // integer decimal + // _____|________ | + // / /| |\ |\ |\ | literal + // /| / | | \ | \ | \| | + // "-28,114,774,228,750.32 US dollars" + // | \___|___|___/ |/ \________/ + // | | | | + // | group | currency + // | | + // minusSign fraction + // + // The sign is a part. Each comma is a part, splitting the integer field + // into parts for trillions/billions/&c. digits. The decimal point is a + // part. Cents are a part. The space between cents and currency is a part + // (outside any field). Last, the currency field is a part. + // + // Because parts fully partition the formatted string, we only track the + // end of each part -- the beginning is implicitly the last part's end. + struct Part + { + uint32_t end; + FieldType type; + }; + + class PartGenerator + { + // The fields in order from start to end, then least to most nested. + const FieldsVector& fields; + + // Index of the current field, in |fields|, being considered to + // determine part boundaries. |lastEnd <= fields[index].begin| is an + // invariant. + size_t index; + + // The end index of the last part produced, always less than or equal + // to |limit|, strictly increasing. + uint32_t lastEnd; + + // The length of the overall formatted string. + const uint32_t limit; + + Vector enclosingFields; + + void popEnclosingFieldsEndingAt(uint32_t end) { + MOZ_ASSERT_IF(enclosingFields.length() > 0, + fields[enclosingFields.back()].end >= end); + + while (enclosingFields.length() > 0 && fields[enclosingFields.back()].end == end) + enclosingFields.popBack(); + } + + bool nextPartInternal(Part* part) { + size_t len = fields.length(); + MOZ_ASSERT(index <= len); + + // If we're out of fields, all that remains are part(s) consisting + // of trailing portions of enclosing fields, and maybe a final + // literal part. + if (index == len) { + if (enclosingFields.length() > 0) { + const auto& enclosing = fields[enclosingFields.popCopy()]; + part->end = enclosing.end; + part->type = enclosing.type; + + // If additional enclosing fields end where this part ends, + // pop them as well. + popEnclosingFieldsEndingAt(part->end); + } else { + part->end = limit; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise we still have a field to process. + const Field* current = &fields[index]; + MOZ_ASSERT(lastEnd <= current->begin); + MOZ_ASSERT(current->begin < current->end); + + // But first, deal with inter-field space. + if (lastEnd < current->begin) { + if (enclosingFields.length() > 0) { + // Space between fields, within an enclosing field, is part + // of that enclosing field, until the start of the current + // field or the end of the enclosing field, whichever is + // earlier. + const auto& enclosing = fields[enclosingFields.back()]; + part->end = std::min(enclosing.end, current->begin); + part->type = enclosing.type; + popEnclosingFieldsEndingAt(part->end); + } else { + // If there's no enclosing field, the space is a literal. + part->end = current->begin; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise, the part spans a prefix of the current field. Find + // the most-nested field containing that prefix. + const Field* next; + do { + current = &fields[index]; + + // If the current field is last, the part extends to its end. + if (++index == len) { + part->end = current->end; + part->type = current->type; + return true; + } + + next = &fields[index]; + MOZ_ASSERT(current->begin <= next->begin); + MOZ_ASSERT(current->begin < next->end); + + // If the next field nests within the current field, push an + // enclosing field. (If there are no nested fields, don't + // bother pushing a field that'd be immediately popped.) + if (current->end > next->begin) { + if (!enclosingFields.append(index - 1)) + return false; + } + + // Do so until the next field begins after this one. + } while (current->begin == next->begin); + + part->type = current->type; + + if (current->end <= next->begin) { + // The next field begins after the current field ends. Therefore + // the current part ends at the end of the current field. + part->end = current->end; + popEnclosingFieldsEndingAt(part->end); + } else { + // The current field encloses the next one. The current part + // ends where the next field/part will start. + part->end = next->begin; + } + + return true; + } + + public: + PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) + : fields(vec), index(0), lastEnd(0), limit(limit), enclosingFields(cx) + {} + + bool nextPart(bool* hasPart, Part* part) { + // There are no parts left if we've partitioned the entire string. + if (lastEnd == limit) { + MOZ_ASSERT(enclosingFields.length() == 0); + *hasPart = false; + return true; + } + + if (!nextPartInternal(part)) + return false; + + *hasPart = true; + lastEnd = part->end; + return true; + } + }; + + // Finally, generate the result array. + size_t lastEndIndex = 0; + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue propVal(cx); + + PartGenerator gen(cx, fields, overallResult->length()); + do { + bool hasPart; + Part part; + if (!gen.nextPart(&hasPart, &part)) + return false; + + if (!hasPart) + break; + + FieldType type = part.type; + size_t endIndex = part.end; + + MOZ_ASSERT(lastEndIndex < endIndex); + + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + propVal.setString(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, propVal)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex); + if (!partSubstr) + return false; + + propVal.setString(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, propVal)) + return false; + + propVal.setObject(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, propVal)) + return false; + + lastEndIndex = endIndex; + partIndex++; + } while (true); + + MOZ_ASSERT(lastEndIndex == chars.length(), + "result array must partition the entire string"); + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isBoolean()); + + RootedObject numberFormat(cx, &args[0].toObject()); + + // Obtain a UNumberFormat object, cached if possible. + bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatObject::class_; + UNumberFormat* nf; + if (isNumberFormatInstance) { + void* priv = + numberFormat->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + nf = static_cast(priv); + if (!nf) { + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + numberFormat->as().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); + } + } else { + // There's no good place to cache the ICU number format for an object + // that has been initialized as a NumberFormat but is not a + // NumberFormat instance. One possibility might be to add a + // NumberFormat instance as an internal property to each such object. + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + } + + // Use the UNumberFormat to actually format the number. + double d = args[1].toNumber(); + RootedValue result(cx); + + bool success; + if (args[2].toBoolean()) { + success = intl_FormatNumberToParts(cx, nf, d, &result); + } else { + MOZ_ASSERT(!args[2].toBoolean(), + "shouldn't be doing formatToParts without an ICU that " + "supports it"); + success = js::intl_FormatNumber(cx, nf, d, &result); + } + + if (!isNumberFormatInstance) + unum_close(nf); + if (!success) + return false; + args.rval().set(result); + return true; +} + diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h new file mode 100644 index 0000000000..37cf943972 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_NumberFormat_h +#define builtin_intl_NumberFormat_h + +#include "mozilla/Attributes.h" + +#include + +#include "unicode/unum.h" // for UNumberFormat +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class NumberFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UNUMBER_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* +CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global); + +/** + * Returns a new instance of the standard built-in NumberFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: numberFormat = intl_NumberFormat(locales, options) + */ +extern MOZ_MUST_USE bool +intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for number formatting + * by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_NumberFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns the numbering system type identifier per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * default numbering system for the given locale. + * + * Usage: defaultNumberingSystem = intl_numberingSystem(locale) + */ +extern MOZ_MUST_USE bool +intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a string representing the number x according to the effective + * locale and the formatting options of the given NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + * + * Usage: formatted = intl_FormatNumber(numberFormat, x) + */ +extern MOZ_MUST_USE bool +intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp); +extern MOZ_MUST_USE bool +intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result); + +} // namespace js + +#endif /* builtin_intl_NumberFormat_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 165ccd2d0c..a741ed521e 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -116,6 +116,7 @@ UNIFIED_SOURCES += [ 'builtin/Eval.cpp', 'builtin/Intl.cpp', 'builtin/intl/CommonFunctions.cpp', + 'builtin/intl/NumberFormat.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 48b2da7b2c..d4c8395aaa 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -23,6 +23,7 @@ #include "selfhosted.out.h" #include "builtin/Intl.h" +#include "builtin/intl/NumberFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" From 6da7d528d654257cdc1133887899ce94b2b7c930 Mon Sep 17 00:00:00 2001 From: Martok Date: Mon, 13 Feb 2023 01:37:40 +0100 Subject: [PATCH 05/24] Issue #2046 - Move NewUNumberFormatForPluralRules next to its only use --- js/src/builtin/Intl.cpp | 164 ++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 4aa46ad7d0..cbbda9b1c1 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -555,88 +555,6 @@ js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) return true; } -/** - * - * This creates new UNumberFormat with calculated digit formatting - * properties for PluralRules. - * - * This is similar to NewUNumberFormat but doesn't allow for currency or - * percent types. - * - */ -static UNumberFormat* -NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) -{ - RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); - if (!internals) - return nullptr; - - RootedValue value(cx); - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - uint32_t uMinimumIntegerDigits = 1; - uint32_t uMinimumFractionDigits = 0; - uint32_t uMaximumFractionDigits = 3; - int32_t uMinimumSignificantDigits = -1; - int32_t uMaximumSignificantDigits = -1; - - RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); - bool hasP; - if (!HasProperty(cx, internals, id, &hasP)) - return nullptr; - if (hasP) { - if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, - &value)) - return nullptr; - uMinimumSignificantDigits = value.toInt32(); - - if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, - &value)) - return nullptr; - uMaximumSignificantDigits = value.toInt32(); - } else { - if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, - &value)) - return nullptr; - uMinimumIntegerDigits = AssertedCast(value.toInt32()); - - if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, - &value)) - return nullptr; - uMinimumFractionDigits = AssertedCast(value.toInt32()); - - if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, - &value)) - return nullptr; - uMaximumFractionDigits = AssertedCast(value.toInt32()); - } - - UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uMinimumSignificantDigits != -1) { - unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); - unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); - unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); - } else { - unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); - unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); - unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); - } - - return toClose.forget(); -} - /******************** DateTimeFormat ********************/ static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); @@ -1875,6 +1793,88 @@ js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp) return true; } +/** + * + * This creates new UNumberFormat with calculated digit formatting + * properties for PluralRules. + * + * This is similar to NewUNumberFormat but doesn't allow for currency or + * percent types. + * + */ +static UNumberFormat* +NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) +{ + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); + if (!internals) + return nullptr; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + + RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); + bool hasP; + if (!HasProperty(cx, internals, id, &hasP)) + return nullptr; + if (hasP) { + if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, + &value)) + return nullptr; + uMinimumSignificantDigits = value.toInt32(); + + if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, + &value)) + return nullptr; + uMaximumSignificantDigits = value.toInt32(); + } else { + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) + return nullptr; + uMinimumIntegerDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject toClose(nf); + + if (uMinimumSignificantDigits != -1) { + unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); + unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); + unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); + } else { + unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); + unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); + unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); + } + + return toClose.forget(); +} + bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) { From 53c2f58c5b5a9ff5e1992a0a8c5a2002728dc085 Mon Sep 17 00:00:00 2001 From: Martok Date: Wed, 15 Feb 2023 22:31:00 +0100 Subject: [PATCH 06/24] Issue #2046 - Move Intl.Collator functionality into builtin/intl/Collator.* --- js/src/builtin/Intl.cpp | 504 +---------------------------- js/src/builtin/Intl.h | 48 --- js/src/builtin/intl/Collator.cpp | 528 +++++++++++++++++++++++++++++++ js/src/builtin/intl/Collator.h | 94 ++++++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 6 files changed, 625 insertions(+), 551 deletions(-) create mode 100644 js/src/builtin/intl/Collator.cpp create mode 100644 js/src/builtin/intl/Collator.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index cbbda9b1c1..f5375a2f7f 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -22,6 +22,7 @@ #include "jscntxt.h" #include "jsobj.h" +#include "builtin/intl/Collator.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" @@ -52,509 +53,6 @@ using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; - -/******************** Collator ********************/ - -static void collator_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UCOLLATOR_SLOT = 0; -static const uint32_t COLLATOR_SLOTS_COUNT = 1; - -static const ClassOps CollatorClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - collator_finalize -}; - -static const Class CollatorClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(COLLATOR_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &CollatorClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -collator_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().Collator); - return true; -} -#endif - -static const JSFunctionSpec collator_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec collator_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, collator_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 10.1.2 Intl.Collator([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -Collator(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when Collator is called because of - // backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 10.1.2.1 step 3 - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - // ES Intl 1st ed., 10.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 10.1.2.1 step 5 - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - // ES Intl 1st ed., 10.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &CollatorClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UCOLLATOR_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 6. - if (!intl::InitializeObject(cx, obj, cx->names().InitializeCollator, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return Collator(cx, args, args.isConstructing()); -} - -bool -js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_Collator is an intrinsic for self-hosted JavaScript, so it cannot - // be used with "new", but it still has to be treated as a constructor. - return Collator(cx, args, true); -} - -static void -collator_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UCOLLATOR_SLOT); - if (!slot.isUndefined()) { - if (UCollator* coll = static_cast(slot.toPrivate())) - ucol_close(coll); - } -} - -static JSObject* -CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, - 0)); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &CollatorClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UCOLLATOR_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 10.2.2 - if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) - return nullptr; - - // 10.3.2 and 10.3.3 - if (!JS_DefineFunctions(cx, proto, collator_methods)) - return nullptr; - - /* - * Install the getter for Collator.prototype.compare, which returns a bound - * comparison function for the specified Collator object (suitable for - * passing to methods like Array.prototype.sort). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) - return nullptr; - if (!DefineProperty(cx, proto, cx->names().compare, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - // 10.2.1 and 10.3 - if (!intl::InitializeObject(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) - return nullptr; - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -bool -js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - RootedObject collations(cx, NewDenseEmptyArray(cx)); - if (!collations) - return false; - - uint32_t index = 0; - for (uint32_t i = 0; i < count; i++) { - const char* collation = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - // Per ECMA-402, 10.2.3, we don't include standard and search: - // "The values 'standard' and 'search' must not be used as elements in - // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co - // array." - if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) - continue; - - // ICU returns old-style keyword values; map them to BCP 47 equivalents - // (see http://bugs.icu-project.org/trac/ticket/9620). - if (StringsAreEqual(collation, "dictionary")) - collation = "dict"; - else if (StringsAreEqual(collation, "gb2312han")) - collation = "gb2312"; - else if (StringsAreEqual(collation, "phonebook")) - collation = "phonebk"; - else if (StringsAreEqual(collation, "traditional")) - collation = "trad"; - - RootedString jscollation(cx, JS_NewStringCopyZ(cx, collation)); - if (!jscollation) - return false; - RootedValue element(cx, StringValue(jscollation)); - if (!DefineElement(cx, collations, index++, element)) - return false; - } - - args.rval().setObject(*collations); - return true; -} - -/** - * Returns a new UCollator with the locale and collation options - * of the given Collator. - */ -static UCollator* -NewUCollator(JSContext* cx, HandleObject collator) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // UCollator options with default values. - UColAttributeValue uStrength = UCOL_DEFAULT; - UColAttributeValue uCaseLevel = UCOL_OFF; - UColAttributeValue uAlternate = UCOL_DEFAULT; - UColAttributeValue uNumeric = UCOL_OFF; - // Normalization is always on to meet the canonical equivalence requirement. - UColAttributeValue uNormalization = UCOL_ON; - UColAttributeValue uCaseFirst = UCOL_DEFAULT; - - if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) - return nullptr; - JSAutoByteString usage(cx, value.toString()); - if (!usage) - return nullptr; - if (StringsAreEqual(usage, "search")) { - // ICU expects search as a Unicode locale extension on locale. - // Unicode locale extensions must occur before private use extensions. - const char* oldLocale = locale.ptr(); - const char* p; - size_t index; - size_t localeLen = strlen(oldLocale); - if ((p = strstr(oldLocale, "-x-"))) - index = p - oldLocale; - else - index = localeLen; - - const char* insert; - if ((p = strstr(oldLocale, "-u-")) && static_cast(p - oldLocale) < index) { - index = p - oldLocale + 2; - insert = "-co-search"; - } else { - insert = "-u-co-search"; - } - size_t insertLen = strlen(insert); - char* newLocale = cx->pod_malloc(localeLen + insertLen + 1); - if (!newLocale) - return nullptr; - memcpy(newLocale, oldLocale, index); - memcpy(newLocale + index, insert, insertLen); - memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0' - locale.clear(); - locale.initBytes(newLocale); - } - - // We don't need to look at the collation property - it can only be set - // via the Unicode locale extension and is therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) - return nullptr; - JSAutoByteString sensitivity(cx, value.toString()); - if (!sensitivity) - return nullptr; - if (StringsAreEqual(sensitivity, "base")) { - uStrength = UCOL_PRIMARY; - } else if (StringsAreEqual(sensitivity, "accent")) { - uStrength = UCOL_SECONDARY; - } else if (StringsAreEqual(sensitivity, "case")) { - uStrength = UCOL_PRIMARY; - uCaseLevel = UCOL_ON; - } else { - MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); - uStrength = UCOL_TERTIARY; - } - - if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) - return nullptr; - // According to the ICU team, UCOL_SHIFTED causes punctuation to be - // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data - // Markup Language, "shifted" causes whitespace and punctuation to be - // ignored - that's a bit more than asked for, but there's no way to get - // less. - if (value.toBoolean()) - uAlternate = UCOL_SHIFTED; - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return nullptr; - if (!value.isUndefined() && value.toBoolean()) - uNumeric = UCOL_ON; - - if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) - return nullptr; - if (!value.isUndefined()) { - JSAutoByteString caseFirst(cx, value.toString()); - if (!caseFirst) - return nullptr; - if (StringsAreEqual(caseFirst, "upper")) - uCaseFirst = UCOL_UPPER_FIRST; - else if (StringsAreEqual(caseFirst, "lower")) - uCaseFirst = UCOL_LOWER_FIRST; - else - MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); - } - - UErrorCode status = U_ZERO_ERROR; - UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - - ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); - ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); - ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); - ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); - ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); - ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); - if (U_FAILURE(status)) { - ucol_close(coll); - intl::ReportInternalError(cx); - return nullptr; - } - - return coll; -} - -static bool -intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, - MutableHandleValue result) -{ - MOZ_ASSERT(str1); - MOZ_ASSERT(str2); - - if (str1 == str2) { - result.setInt32(0); - return true; - } - - AutoStableStringChars stableChars1(cx); - if (!stableChars1.initTwoByte(cx, str1)) - return false; - - AutoStableStringChars stableChars2(cx); - if (!stableChars2.initTwoByte(cx, str2)) - return false; - - mozilla::Range chars1 = stableChars1.twoByteRange(); - mozilla::Range chars2 = stableChars2.twoByteRange(); - - UCollationResult uresult = ucol_strcoll(coll, - Char16ToUChar(chars1.begin().get()), chars1.length(), - Char16ToUChar(chars2.begin().get()), chars2.length()); - int32_t res; - switch (uresult) { - case UCOL_LESS: res = -1; break; - case UCOL_EQUAL: res = 0; break; - case UCOL_GREATER: res = 1; break; - default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); - } - result.setInt32(res); - return true; -} - -bool -js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isString()); - MOZ_ASSERT(args[2].isString()); - - RootedObject collator(cx, &args[0].toObject()); - - // Obtain a UCollator object, cached if possible. - // XXX Does this handle Collator instances from other globals correctly? - bool isCollatorInstance = collator->getClass() == &CollatorClass; - UCollator* coll; - if (isCollatorInstance) { - void* priv = collator->as().getReservedSlot(UCOLLATOR_SLOT).toPrivate(); - coll = static_cast(priv); - if (!coll) { - coll = NewUCollator(cx, collator); - if (!coll) - return false; - collator->as().setReservedSlot(UCOLLATOR_SLOT, PrivateValue(coll)); - } - } else { - // There's no good place to cache the ICU collator for an object - // that has been initialized as a Collator but is not a Collator - // instance. One possibility might be to add a Collator instance as an - // internal property to each such object. - coll = NewUCollator(cx, collator); - if (!coll) - return false; - } - - // Use the UCollator to actually compare the strings. - RootedString str1(cx, args[1].toString()); - RootedString str2(cx, args[2].toString()); - RootedValue result(cx); - bool success = intl_CompareStrings(cx, coll, str1, str2, &result); - - if (!isCollatorInstance) - ucol_close(coll); - if (!success) - return false; - args.rval().set(result); - return true; -} - /******************** DateTimeFormat ********************/ static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 1026da5e5e..f98d40f3e0 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -170,54 +170,6 @@ class SharedIntlData */ -/******************** Collator ********************/ - -/** - * Returns a new instance of the standard built-in Collator constructor. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: collator = intl_Collator(locales, options) - */ -extern MOZ_MUST_USE bool -intl_Collator(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for collation - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_Collator_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array with the collation type identifiers per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * collations supported for the given locale. "standard" and "search" are - * excluded. - * - * Usage: collations = intl_availableCollations(locale) - */ -extern MOZ_MUST_USE bool -intl_availableCollations(JSContext* cx, unsigned argc, Value* vp); - -/** - * Compares x and y (which must be String values), and returns a number less - * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according - * to the sort order for the locale and collation options of the given - * Collator. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.2. - * - * Usage: result = intl_CompareStrings(collator, x, y) - */ -extern MOZ_MUST_USE bool -intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); - - /******************** DateTimeFormat ********************/ /** diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp new file mode 100644 index 0000000000..903917df89 --- /dev/null +++ b/js/src/builtin/intl/Collator.cpp @@ -0,0 +1,528 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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.Collator implementation. */ + +#include "builtin/intl/Collator.h" + +#include "mozilla/Assertions.h" + +#include "jsapi.h" +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" +#include "vm/String.h" + +#include "jsobjinlines.h" + +using namespace js; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::ReportInternalError; +using js::intl::StringsAreEqual; + +/******************** Collator ********************/ + +const ClassOps CollatorObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + CollatorObject::finalize +}; + +const Class CollatorObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &CollatorObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +collator_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Collator); + return true; +} +#endif + +static const JSFunctionSpec collator_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec collator_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, collator_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 10.1.2 Intl.Collator([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +Collator(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when Collator is called because of + // backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 10.1.2.1 step 3 + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + // ES Intl 1st ed., 10.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 10.1.2.1 step 5 + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + // ES Intl 1st ed., 10.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 6. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeCollator, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return Collator(cx, args, args.isConstructing()); +} + +bool +js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_Collator is an intrinsic for self-hosted JavaScript, so it cannot + // be used with "new", but it still has to be treated as a constructor. + return Collator(cx, args, true); +} + +void +js::CollatorObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(CollatorObject::UCOLLATOR_SLOT); + if (!slot.isUndefined()) { + if (UCollator* coll = static_cast(slot.toPrivate())) + ucol_close(coll); + } +} + +JSObject* +js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, + 0)); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &CollatorObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 10.2.2 + if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) + return nullptr; + + // 10.3.2 and 10.3.3 + if (!JS_DefineFunctions(cx, proto, collator_methods)) + return nullptr; + + /* + * Install the getter for Collator.prototype.compare, which returns a bound + * comparison function for the specified Collator object (suitable for + * passing to methods like Array.prototype.sort). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) + return nullptr; + if (!DefineProperty(cx, proto, cx->names().compare, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + // 10.2.1 and 10.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) + return nullptr; + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +bool +js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + RootedObject collations(cx, NewDenseEmptyArray(cx)); + if (!collations) + return false; + + uint32_t index = 0; + for (uint32_t i = 0; i < count; i++) { + const char* collation = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + // Per ECMA-402, 10.2.3, we don't include standard and search: + // "The values 'standard' and 'search' must not be used as elements in + // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co + // array." + if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) + continue; + + // ICU returns old-style keyword values; map them to BCP 47 equivalents + // (see http://bugs.icu-project.org/trac/ticket/9620). + if (StringsAreEqual(collation, "dictionary")) + collation = "dict"; + else if (StringsAreEqual(collation, "gb2312han")) + collation = "gb2312"; + else if (StringsAreEqual(collation, "phonebook")) + collation = "phonebk"; + else if (StringsAreEqual(collation, "traditional")) + collation = "trad"; + + RootedString jscollation(cx, JS_NewStringCopyZ(cx, collation)); + if (!jscollation) + return false; + RootedValue element(cx, StringValue(jscollation)); + if (!DefineElement(cx, collations, index++, element)) + return false; + } + + args.rval().setObject(*collations); + return true; +} + +/** + * Returns a new UCollator with the locale and collation options + * of the given Collator. + */ +static UCollator* +NewUCollator(JSContext* cx, HandleObject collator) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // UCollator options with default values. + UColAttributeValue uStrength = UCOL_DEFAULT; + UColAttributeValue uCaseLevel = UCOL_OFF; + UColAttributeValue uAlternate = UCOL_DEFAULT; + UColAttributeValue uNumeric = UCOL_OFF; + // Normalization is always on to meet the canonical equivalence requirement. + UColAttributeValue uNormalization = UCOL_ON; + UColAttributeValue uCaseFirst = UCOL_DEFAULT; + + if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) + return nullptr; + JSAutoByteString usage(cx, value.toString()); + if (!usage) + return nullptr; + if (StringsAreEqual(usage, "search")) { + // ICU expects search as a Unicode locale extension on locale. + // Unicode locale extensions must occur before private use extensions. + const char* oldLocale = locale.ptr(); + const char* p; + size_t index; + size_t localeLen = strlen(oldLocale); + if ((p = strstr(oldLocale, "-x-"))) + index = p - oldLocale; + else + index = localeLen; + + const char* insert; + if ((p = strstr(oldLocale, "-u-")) && static_cast(p - oldLocale) < index) { + index = p - oldLocale + 2; + insert = "-co-search"; + } else { + insert = "-u-co-search"; + } + size_t insertLen = strlen(insert); + char* newLocale = cx->pod_malloc(localeLen + insertLen + 1); + if (!newLocale) + return nullptr; + memcpy(newLocale, oldLocale, index); + memcpy(newLocale + index, insert, insertLen); + memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0' + locale.clear(); + locale.initBytes(newLocale); + } + + // We don't need to look at the collation property - it can only be set + // via the Unicode locale extension and is therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) + return nullptr; + JSAutoByteString sensitivity(cx, value.toString()); + if (!sensitivity) + return nullptr; + if (StringsAreEqual(sensitivity, "base")) { + uStrength = UCOL_PRIMARY; + } else if (StringsAreEqual(sensitivity, "accent")) { + uStrength = UCOL_SECONDARY; + } else if (StringsAreEqual(sensitivity, "case")) { + uStrength = UCOL_PRIMARY; + uCaseLevel = UCOL_ON; + } else { + MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); + uStrength = UCOL_TERTIARY; + } + + if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) + return nullptr; + // According to the ICU team, UCOL_SHIFTED causes punctuation to be + // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data + // Markup Language, "shifted" causes whitespace and punctuation to be + // ignored - that's a bit more than asked for, but there's no way to get + // less. + if (value.toBoolean()) + uAlternate = UCOL_SHIFTED; + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return nullptr; + if (!value.isUndefined() && value.toBoolean()) + uNumeric = UCOL_ON; + + if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) + return nullptr; + if (!value.isUndefined()) { + JSAutoByteString caseFirst(cx, value.toString()); + if (!caseFirst) + return nullptr; + if (StringsAreEqual(caseFirst, "upper")) + uCaseFirst = UCOL_UPPER_FIRST; + else if (StringsAreEqual(caseFirst, "lower")) + uCaseFirst = UCOL_LOWER_FIRST; + else + MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); + } + + UErrorCode status = U_ZERO_ERROR; + UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); + ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); + ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); + ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); + ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); + ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); + if (U_FAILURE(status)) { + ucol_close(coll); + intl::ReportInternalError(cx); + return nullptr; + } + + return coll; +} + +static bool +intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, + MutableHandleValue result) +{ + MOZ_ASSERT(str1); + MOZ_ASSERT(str2); + + if (str1 == str2) { + result.setInt32(0); + return true; + } + + AutoStableStringChars stableChars1(cx); + if (!stableChars1.initTwoByte(cx, str1)) + return false; + + AutoStableStringChars stableChars2(cx); + if (!stableChars2.initTwoByte(cx, str2)) + return false; + + mozilla::Range chars1 = stableChars1.twoByteRange(); + mozilla::Range chars2 = stableChars2.twoByteRange(); + + UCollationResult uresult = ucol_strcoll(coll, + Char16ToUChar(chars1.begin().get()), chars1.length(), + Char16ToUChar(chars2.begin().get()), chars2.length()); + int32_t res; + switch (uresult) { + case UCOL_LESS: res = -1; break; + case UCOL_EQUAL: res = 0; break; + case UCOL_GREATER: res = 1; break; + default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); + } + result.setInt32(res); + return true; +} + +bool +js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString()); + + Rooted collator(cx, &args[0].toObject().as()); + + // Obtain a UCollator object, cached if possible. + // XXX Does this handle Collator instances from other globals correctly? + bool isCollatorInstance = collator->getClass() == &CollatorObject::class_; + UCollator* coll; + if (isCollatorInstance) { + void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate(); + coll = static_cast(priv); + if (!coll) { + coll = NewUCollator(cx, collator); + if (!coll) + return false; + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll)); + } + } else { + // There's no good place to cache the ICU collator for an object + // that has been initialized as a Collator but is not a Collator + // instance. One possibility might be to add a Collator instance as an + // internal property to each such object. + coll = NewUCollator(cx, collator); + if (!coll) + return false; + } + + // Use the UCollator to actually compare the strings. + RootedString str1(cx, args[1].toString()); + RootedString str2(cx, args[2].toString()); + RootedValue result(cx); + bool success = intl_CompareStrings(cx, coll, str1, str2, &result); + + if (!isCollatorInstance) + ucol_close(coll); + if (!success) + return false; + args.rval().set(result); + return true; +} diff --git a/js/src/builtin/intl/Collator.h b/js/src/builtin/intl/Collator.h new file mode 100644 index 0000000000..c748a7f724 --- /dev/null +++ b/js/src/builtin/intl/Collator.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_Collator_h +#define builtin_intl_Collator_h + +#include "mozilla/Attributes.h" + +#include + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; +class GlobalObject; + +/******************** Collator ********************/ + +class CollatorObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UCOLLATOR_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* +CreateCollatorPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new instance of the standard built-in Collator constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: collator = intl_Collator(locales, options) + */ +extern MOZ_MUST_USE bool +intl_Collator(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for collation + * by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_Collator_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array with the collation type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * collations supported for the given locale. "standard" and "search" are + * excluded. + * + * Usage: collations = intl_availableCollations(locale) + */ +extern MOZ_MUST_USE bool +intl_availableCollations(JSContext* cx, unsigned argc, Value* vp); + +/** + * Compares x and y (which must be String values), and returns a number less + * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according + * to the sort order for the locale and collation options of the given + * Collator. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.2. + * + * Usage: result = intl_CompareStrings(collator, x, y) + */ +extern MOZ_MUST_USE bool +intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); + + +} // namespace js + +#endif /* builtin_intl_Collator_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index a741ed521e..dce9e5ef92 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -115,6 +115,7 @@ UNIFIED_SOURCES += [ 'builtin/AtomicsObject.cpp', 'builtin/Eval.cpp', 'builtin/Intl.cpp', + 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/NumberFormat.cpp', 'builtin/MapObject.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index d4c8395aaa..6e7da8f053 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -23,6 +23,7 @@ #include "selfhosted.out.h" #include "builtin/Intl.h" +#include "builtin/intl/Collator.h" #include "builtin/intl/NumberFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" From ce98604d2e127fced95ec142947fdc0be4dc8489 Mon Sep 17 00:00:00 2001 From: Martok Date: Wed, 15 Feb 2023 22:58:28 +0100 Subject: [PATCH 07/24] Issue #2046 - Move SharedIntlData into its own builtin/intl/SharedIntlData.* files so the world doesn't have to import all shared Intl functionality. --- js/src/builtin/Intl.cpp | 277 +---------------------- js/src/builtin/Intl.h | 134 ----------- js/src/builtin/intl/Collator.cpp | 2 + js/src/builtin/intl/SharedIntlData.cpp | 300 +++++++++++++++++++++++++ js/src/builtin/intl/SharedIntlData.h | 164 ++++++++++++++ js/src/moz.build | 1 + js/src/vm/Runtime.h | 3 +- 7 files changed, 471 insertions(+), 410 deletions(-) create mode 100644 js/src/builtin/intl/SharedIntlData.cpp create mode 100644 js/src/builtin/intl/SharedIntlData.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index f5375a2f7f..48f8791ac4 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -27,7 +27,7 @@ #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" -#include "builtin/IntlTimeZoneData.h" +#include "builtin/intl/SharedIntlData.h" #include "ds/Sort.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -51,6 +51,7 @@ using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; using js::intl::StringsAreEqual; /******************** DateTimeFormat ********************/ @@ -363,280 +364,6 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) return true; } -template -static constexpr Char -ToUpperASCII(Char c) -{ - return ('a' <= c && c <= 'z') - ? (c & ~0x20) - : c; -} - -static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); -static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); -static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); -static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); -static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); -static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); - -template -static bool -EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) -{ - for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { - if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) - return false; - } - return true; -} - -template -static js::HashNumber -HashStringIgnoreCaseASCII(const Char* s, size_t length) -{ - uint32_t hash = 0; - for (size_t i = 0; i < length; i++) - hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); - return hash; -} - -js::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) - : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) -{ - if (isLatin1) { - latin1Chars = timeZone->latin1Chars(nogc); - hash = HashStringIgnoreCaseASCII(latin1Chars, length); - } else { - twoByteChars = timeZone->twoByteChars(nogc); - hash = HashStringIgnoreCaseASCII(twoByteChars, length); - } -} - -bool -js::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) -{ - if (key->length() != lookup.length) - return false; - - // Compare time zone names ignoring ASCII case differences. - if (key->hasLatin1Chars()) { - const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); - } - - const char16_t* keyChars = key->twoByteChars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); -} - -static bool -IsLegacyICUTimeZone(const char* timeZone) -{ - for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { - if (StringsAreEqual(timeZone, legacyTimeZone)) - return true; - } - return false; -} - -bool -js::SharedIntlData::ensureTimeZones(JSContext* cx) -{ - if (timeZoneDataInitialized) - return true; - - // If initTimeZones() was called previously, but didn't complete due to - // OOM, clear all sets/maps and start from scratch. - if (availableTimeZones.initialized()) - availableTimeZones.finish(); - if (!availableTimeZones.init()) { - ReportOutOfMemory(cx); - return false; - } - - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucal_openTimeZones(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - RootedAtom timeZone(cx); - while (true) { - int32_t size; - const char* rawTimeZone = uenum_next(values, &size, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - if (rawTimeZone == nullptr) - break; - - // Skip legacy ICU time zone names. - if (IsLegacyICUTimeZone(rawTimeZone)) - continue; - - MOZ_ASSERT(size >= 0); - timeZone = Atomize(cx, rawTimeZone, size_t(size)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); - - // ICU shouldn't report any duplicate time zone names, but if it does, - // just ignore the duplicate name. - if (!p && !availableTimeZones.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaZonesTreatedAsLinksByICU.initialized()) - ianaZonesTreatedAsLinksByICU.finish(); - if (!ianaZonesTreatedAsLinksByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { - MOZ_ASSERT(rawTimeZone != nullptr); - timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); - - if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) - ianaLinksCanonicalizedDifferentlyByICU.finish(); - if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - RootedAtom linkName(cx); - RootedAtom& target = timeZone; - for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { - const char* rawLinkName = linkAndTarget.link; - const char* rawTarget = linkAndTarget.target; - - MOZ_ASSERT(rawLinkName != nullptr); - linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); - if (!linkName) - return false; - - MOZ_ASSERT(rawTarget != nullptr); - target = Atomize(cx, rawTarget, strlen(rawTarget)); - if (!target) - return false; - - TimeZoneHasher::Lookup lookup(linkName); - TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); - - if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { - ReportOutOfMemory(cx); - return false; - } - } - - MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); - timeZoneDataInitialized = true; - - return true; -} - -bool -js::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) - result.set(*p); - - return true; -} - -bool -js::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); - - if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { - // The effectively supported time zones aren't known at compile time, - // when - // 1. SpiderMonkey was compiled with "--with-system-icu". - // 2. ICU's dynamic time zone data loading feature was used. - // (ICU supports loading time zone files at runtime through the - // ICU_TIMEZONE_FILES_DIR environment variable.) - // Ensure ICU supports the new target zone before applying the update. - TimeZoneName targetTimeZone = p->value(); - TimeZoneHasher::Lookup targetLookup(targetTimeZone); - if (availableTimeZones.has(targetLookup)) - result.set(targetTimeZone); - } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { - result.set(*p); - } - - return true; -} - -void -js::SharedIntlData::destroyInstance() -{ - availableTimeZones.finish(); - ianaZonesTreatedAsLinksByICU.finish(); - ianaLinksCanonicalizedDifferentlyByICU.finish(); -} - -void -js::SharedIntlData::trace(JSTracer* trc) -{ - // Atoms are always tenured. - if (!trc->runtime()->isHeapMinorCollecting()) { - availableTimeZones.trace(trc); - ianaZonesTreatedAsLinksByICU.trace(trc); - ianaLinksCanonicalizedDifferentlyByICU.trace(trc); - } -} - -size_t -js::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const -{ - return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + - ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + - ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); -} - bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) { diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index f98d40f3e0..0f934debfb 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -31,140 +31,6 @@ namespace js { extern JSObject* InitIntlClass(JSContext* cx, HandleObject obj); -/** - * Stores Intl data which can be shared across compartments (but not contexts). - * - * Used for data which is expensive when computed repeatedly or is not - * available through ICU. - */ -class SharedIntlData -{ - /** - * Information tracking the set of the supported time zone names, derived - * from the IANA time zone database . - * - * There are two kinds of IANA time zone names: Zone and Link (denoted as - * such in database source files). Zone names are the canonical, preferred - * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to - * target Zone names for their meaning, e.g. Asia/Calcutta targets - * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a - * sense of deprecation: some Link names also exist partly for convenience, - * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. - * - * Two data sources determine the time zone names we support: those ICU - * supports and IANA's zone information. - * - * Unfortunately the names ICU and IANA support, and their Link - * relationships from name to target, aren't identical, so we can't simply - * implicitly trust ICU's name handling. We must perform various - * preprocessing of user-provided zone names and post-processing of - * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. - * - * Also see and - * . - */ - - using TimeZoneName = JSAtom*; - - struct TimeZoneHasher - { - struct Lookup - { - union { - const JS::Latin1Char* latin1Chars; - const char16_t* twoByteChars; - }; - bool isLatin1; - size_t length; - JS::AutoCheckCannotGC nogc; - HashNumber hash; - - explicit Lookup(JSFlatString* timeZone); - }; - - static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } - static bool match(TimeZoneName key, const Lookup& lookup); - }; - - using TimeZoneSet = js::GCHashSet; - - using TimeZoneMap = js::GCHashMap; - - /** - * As a threshold matter, available time zones are those time zones ICU - * supports, via ucal_openTimeZones. But ICU supports additional non-IANA - * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in - * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards - * compatibility purposes. This set consists of ICU's supported time zones, - * minus all backwards-compatibility time zones. - */ - TimeZoneSet availableTimeZones; - - /** - * IANA treats some time zone names as Zones, that ICU instead treats as - * Links. For example, IANA considers "America/Indiana/Indianapolis" to be - * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU - * considers the former a Link that targets "America/Indianapolis" (which - * IANA treats as a Link). - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name in this set, we must *not* return ICU's - * canonicalization. - */ - TimeZoneSet ianaZonesTreatedAsLinksByICU; - - /** - * IANA treats some time zone names as Links to one target, that ICU - * instead treats as either Zones, or Links to different targets. An - * example of the former is "Asia/Calcutta, which IANA assigns the target - * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter - * is "America/Virgin", which IANA assigns the target - * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name that's a key in this map, we *must* return - * the corresponding value and *must not* return ICU's canonicalization. - */ - TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; - - bool timeZoneDataInitialized = false; - - /** - * Precomputes the available time zone names, because it's too expensive to - * call ucal_openTimeZones() repeatedly. - */ - bool ensureTimeZones(JSContext* cx); - - public: - /** - * Returns the validated time zone name in |result|. If the input time zone - * isn't a valid IANA time zone name, |result| remains unchanged. - */ - bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - /** - * Returns the canonical time zone name in |result|. If no canonical name - * was found, |result| remains unchanged. - * - * This method only handles time zones which are canonicalized differently - * by ICU when compared to IANA. - */ - bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - void destroyInstance(); - - void trace(JSTracer* trc); - - size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; -}; - /* * The following functions are for use by self-hosted code. */ diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp index 903917df89..426daedc5a 100644 --- a/js/src/builtin/intl/Collator.cpp +++ b/js/src/builtin/intl/Collator.cpp @@ -16,6 +16,7 @@ #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" #include "js/TypeDecls.h" #include "vm/GlobalObject.h" #include "vm/Runtime.h" @@ -27,6 +28,7 @@ using namespace js; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::ReportInternalError; +using js::intl::SharedIntlData; using js::intl::StringsAreEqual; /******************** Collator ********************/ diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp new file mode 100644 index 0000000000..f2da97b361 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -0,0 +1,300 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +/* Runtime-wide Intl data shared across compartments. */ + +#include "builtin/intl/SharedIntlData.h" + +#include "mozilla/Assertions.h" +#include "mozilla/HashFunctions.h" + +#include + +#include "jsatom.h" +#include "jsstr.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/IntlTimeZoneData.h" +#include "js/Utility.h" + +using js::HashNumber; +using js::intl::StringsAreEqual; + +template +static constexpr Char +ToUpperASCII(Char c) +{ + return ('a' <= c && c <= 'z') + ? (c & ~0x20) + : c; +} + +static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); +static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); +static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); +static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); +static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); +static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); + +template +static HashNumber +HashStringIgnoreCaseASCII(const Char* s, size_t length) +{ + uint32_t hash = 0; + for (size_t i = 0; i < length; i++) + hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); + return hash; +} + +template +static bool +EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) +{ + for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { + if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) + return false; + } + return true; +} + +js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) + : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) +{ + if (isLatin1) { + latin1Chars = timeZone->latin1Chars(nogc); + hash = HashStringIgnoreCaseASCII(latin1Chars, length); + } else { + twoByteChars = timeZone->twoByteChars(nogc); + hash = HashStringIgnoreCaseASCII(twoByteChars, length); + } +} + +bool +js::intl::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) +{ + if (key->length() != lookup.length) + return false; + + // Compare time zone names ignoring ASCII case differences. + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); +} + +static bool +IsLegacyICUTimeZone(const char* timeZone) +{ + for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { + if (StringsAreEqual(timeZone, legacyTimeZone)) + return true; + } + return false; +} + +bool +js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) +{ + if (timeZoneDataInitialized) + return true; + + // If initTimeZones() was called previously, but didn't complete due to + // OOM, clear all sets/maps and start from scratch. + if (availableTimeZones.initialized()) + availableTimeZones.finish(); + if (!availableTimeZones.init()) { + ReportOutOfMemory(cx); + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucal_openTimeZones(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + RootedAtom timeZone(cx); + while (true) { + int32_t size; + const char* rawTimeZone = uenum_next(values, &size, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (rawTimeZone == nullptr) + break; + + // Skip legacy ICU time zone names. + if (IsLegacyICUTimeZone(rawTimeZone)) + continue; + + MOZ_ASSERT(size >= 0); + timeZone = Atomize(cx, rawTimeZone, size_t(size)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate time zone names, but if it does, + // just ignore the duplicate name. + if (!p && !availableTimeZones.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaZonesTreatedAsLinksByICU.initialized()) + ianaZonesTreatedAsLinksByICU.finish(); + if (!ianaZonesTreatedAsLinksByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { + MOZ_ASSERT(rawTimeZone != nullptr); + timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); + + if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) + ianaLinksCanonicalizedDifferentlyByICU.finish(); + if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + RootedAtom linkName(cx); + RootedAtom& target = timeZone; + for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { + const char* rawLinkName = linkAndTarget.link; + const char* rawTarget = linkAndTarget.target; + + MOZ_ASSERT(rawLinkName != nullptr); + linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); + if (!linkName) + return false; + + MOZ_ASSERT(rawTarget != nullptr); + target = Atomize(cx, rawTarget, strlen(rawTarget)); + if (!target) + return false; + + TimeZoneHasher::Lookup lookup(linkName); + TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); + + if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); + timeZoneDataInitialized = true; + + return true; +} + +bool +js::intl::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) + result.set(*p); + + return true; +} + +bool +js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); + + if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { + // The effectively supported time zones aren't known at compile time, + // when + // 1. SpiderMonkey was compiled with "--with-system-icu". + // 2. ICU's dynamic time zone data loading feature was used. + // (ICU supports loading time zone files at runtime through the + // ICU_TIMEZONE_FILES_DIR environment variable.) + // Ensure ICU supports the new target zone before applying the update. + TimeZoneName targetTimeZone = p->value(); + TimeZoneHasher::Lookup targetLookup(targetTimeZone); + if (availableTimeZones.has(targetLookup)) + result.set(targetTimeZone); + } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { + result.set(*p); + } + + return true; +} + +void +js::intl::SharedIntlData::destroyInstance() +{ + availableTimeZones.finish(); + ianaZonesTreatedAsLinksByICU.finish(); + ianaLinksCanonicalizedDifferentlyByICU.finish(); +} + +void +js::intl::SharedIntlData::trace(JSTracer* trc) +{ + // Atoms are always tenured. + if (!trc->runtime()->isHeapMinorCollecting()) { + availableTimeZones.trace(trc); + ianaZonesTreatedAsLinksByICU.trace(trc); + ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + } +} + +size_t +js::intl::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + + ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + + ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); +} diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h new file mode 100644 index 0000000000..c047de3801 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.h @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_SharedIntlData_h +#define builtin_intl_SharedIntlData_h + +#include "mozilla/MemoryReporting.h" + +#include + +#include "jsalloc.h" +#include "js/CharacterEncoding.h" +#include "js/GCAPI.h" +#include "js/GCHashTable.h" +#include "js/RootingAPI.h" +#include "js/Utility.h" +#include "vm/String.h" + +namespace js { + +namespace intl { + +/** + * Stores Intl data which can be shared across compartments (but not contexts). + * + * Used for data which is expensive when computed repeatedly or is not + * available through ICU. + */ +class SharedIntlData +{ + /** + * Information tracking the set of the supported time zone names, derived + * from the IANA time zone database . + * + * There are two kinds of IANA time zone names: Zone and Link (denoted as + * such in database source files). Zone names are the canonical, preferred + * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to + * target Zone names for their meaning, e.g. Asia/Calcutta targets + * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a + * sense of deprecation: some Link names also exist partly for convenience, + * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. + * + * Two data sources determine the time zone names we support: those ICU + * supports and IANA's zone information. + * + * Unfortunately the names ICU and IANA support, and their Link + * relationships from name to target, aren't identical, so we can't simply + * implicitly trust ICU's name handling. We must perform various + * preprocessing of user-provided zone names and post-processing of + * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. + * + * Also see and + * . + */ + + using TimeZoneName = JSAtom*; + + struct TimeZoneHasher + { + struct Lookup + { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + }; + bool isLatin1; + size_t length; + JS::AutoCheckCannotGC nogc; + HashNumber hash; + + explicit Lookup(JSFlatString* timeZone); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(TimeZoneName key, const Lookup& lookup); + }; + + using TimeZoneSet = js::GCHashSet; + + using TimeZoneMap = js::GCHashMap; + + /** + * As a threshold matter, available time zones are those time zones ICU + * supports, via ucal_openTimeZones. But ICU supports additional non-IANA + * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in + * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards + * compatibility purposes. This set consists of ICU's supported time zones, + * minus all backwards-compatibility time zones. + */ + TimeZoneSet availableTimeZones; + + /** + * IANA treats some time zone names as Zones, that ICU instead treats as + * Links. For example, IANA considers "America/Indiana/Indianapolis" to be + * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU + * considers the former a Link that targets "America/Indianapolis" (which + * IANA treats as a Link). + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name in this set, we must *not* return ICU's + * canonicalization. + */ + TimeZoneSet ianaZonesTreatedAsLinksByICU; + + /** + * IANA treats some time zone names as Links to one target, that ICU + * instead treats as either Zones, or Links to different targets. An + * example of the former is "Asia/Calcutta, which IANA assigns the target + * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter + * is "America/Virgin", which IANA assigns the target + * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name that's a key in this map, we *must* return + * the corresponding value and *must not* return ICU's canonicalization. + */ + TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; + + bool timeZoneDataInitialized = false; + + /** + * Precomputes the available time zone names, because it's too expensive to + * call ucal_openTimeZones() repeatedly. + */ + bool ensureTimeZones(JSContext* cx); + + public: + /** + * Returns the validated time zone name in |result|. If the input time zone + * isn't a valid IANA time zone name, |result| remains unchanged. + */ + bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + /** + * Returns the canonical time zone name in |result|. If no canonical name + * was found, |result| remains unchanged. + * + * This method only handles time zones which are canonicalized differently + * by ICU when compared to IANA. + */ + bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + void destroyInstance(); + + void trace(JSTracer* trc); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; +}; + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_SharedIntlData_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index dce9e5ef92..0b80c31802 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -118,6 +118,7 @@ UNIFIED_SOURCES += [ 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/NumberFormat.cpp', + 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 1bbe4658fb..0fc6e859e7 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -26,6 +26,7 @@ #endif #include "builtin/AtomicsObject.h" #include "builtin/Intl.h" +#include "builtin/intl/SharedIntlData.h" #include "builtin/Promise.h" #include "ds/FixedSizeHash.h" #include "frontend/NameCollections.h" @@ -810,7 +811,7 @@ struct JSRuntime : public JS::shadow::Runtime, const char* getDefaultLocale(); /* Shared Intl data for this runtime. */ - js::SharedIntlData sharedIntlData; + js::intl::SharedIntlData sharedIntlData; void traceSharedIntlData(JSTracer* trc); From c981fb13ea11d3fab393a63c47bfdc2dd29efb7e Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 00:14:14 +0100 Subject: [PATCH 08/24] Issue #2046 - Move Intl.DateTimeFormat functionality into builtin/intl/DateTimeFormat.* --- js/src/builtin/Intl.cpp | 785 +----------------------- js/src/builtin/Intl.h | 96 --- js/src/builtin/intl/DateTimeFormat.cpp | 819 +++++++++++++++++++++++++ js/src/builtin/intl/DateTimeFormat.h | 143 +++++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 6 files changed, 965 insertions(+), 880 deletions(-) create mode 100644 js/src/builtin/intl/DateTimeFormat.cpp create mode 100644 js/src/builtin/intl/DateTimeFormat.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 48f8791ac4..3bf86b53a3 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -24,10 +24,10 @@ #include "builtin/intl/Collator.h" #include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" -#include "builtin/intl/SharedIntlData.h" #include "ds/Sort.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -45,796 +45,13 @@ using namespace js; using mozilla::AssertedCast; using mozilla::IsFinite; using mozilla::IsNegativeZero; -using mozilla::PodCopy; using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; -using js::intl::SharedIntlData; using js::intl::StringsAreEqual; -/******************** DateTimeFormat ********************/ - -static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UDATE_FORMAT_SLOT = 0; -static const uint32_t DATE_TIME_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps DateTimeFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - dateTimeFormat_finalize -}; - -static const Class DateTimeFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(DATE_TIME_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &DateTimeFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().DateTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec dateTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec dateTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when DateTimeFormat is called - // because of backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 12.1.2.1 step 3 - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - // ES Intl 1st ed., 12.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 12.1.2.1 step 5 - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - // ES Intl 1st ed., 12.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &DateTimeFormatClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 3. - if (!intl::InitializeObject(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return DateTimeFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it - // cannot be used with "new", but it still has to be treated as a - // constructor. - return DateTimeFormat(cx, args, true); -} - -static void -dateTimeFormat_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UDATE_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UDateFormat* df = static_cast(slot.toPrivate())) - udat_close(df); - } -} - -static JSObject* -CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &DateTimeFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 12.2.2 - if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) - return nullptr; - - // 12.3.2 and 12.3.3 - if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) - return nullptr; - - // Install a getter for DateTimeFormat.prototype.format that returns a - // formatting function bound to a specified DateTimeFormat object (suitable - // for passing to methods like Array.prototype.map). - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, - &getter)) - { - return nullptr; - } - if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - // 12.2.1 and 12.3 - if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -// ICU returns old-style keyword values; map them to BCP 47 equivalents -// (see http://bugs.icu-project.org/trac/ticket/9620). -static const char* -bcp47CalendarName(const char* icuName) -{ - if (StringsAreEqual(icuName, "ethiopic-amete-alem")) - return "ethioaa"; - if (StringsAreEqual(icuName, "gregorian")) - return "gregory"; - if (StringsAreEqual(icuName, "islamic-civil")) - return "islamicc"; - return icuName; -} - -bool -js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - RootedObject calendars(cx, NewDenseEmptyArray(cx)); - if (!calendars) - return false; - uint32_t index = 0; - - // We need the default calendar for the locale as the first result. - UErrorCode status = U_ZERO_ERROR; - RootedString jscalendar(cx); - { - UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); - - // This correctly handles nullptr |cal| when opening failed. - ScopedICUObject closeCalendar(cal); - - const char* calendar = ucal_getType(cal, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - } - - RootedValue element(cx, StringValue(jscalendar)); - if (!DefineElement(cx, calendars, index++, element)) - return false; - - // Now get the calendars that "would make a difference", i.e., not the default. - UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - for (; count > 0; count--) { - const char* calendar = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - element = StringValue(jscalendar); - if (!DefineElement(cx, calendars, index++, element)) - return false; - } - - args.rval().setObject(*calendars); - return true; -} - -bool -js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - RootedString timeZone(cx, args[0].toString()); - RootedString validatedTimeZone(cx); - if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) - return false; - - if (validatedTimeZone) - args.rval().setString(validatedTimeZone); - else - args.rval().setNull(); - - return true; -} - -bool -js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - // Some time zone names are canonicalized differently by ICU -- handle - // those first: - RootedString timeZone(cx, args[0].toString()); - RootedString ianaTimeZone(cx); - if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) - return false; - - if (ianaTimeZone) { - args.rval().setString(ianaTimeZone); - return true; - } - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, timeZone)) - return false; - - mozilla::Range tzchars = stableChars.twoByteRange(); - - JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { - return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), - chars, size, nullptr, status); - }); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - // The current default might be stale, because JS::ResetTimeZone() doesn't - // immediately update ICU's default time zone. So perform an update if - // needed. - js::ResyncICUDefaultTimeZone(); - - JSString* str = CallICU(cx, ucal_getDefaultTimeZone); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - UErrorCode status = U_ZERO_ERROR; - const UChar* uTimeZone = nullptr; - int32_t uTimeZoneLength = 0; - const char* rootLocale = ""; - UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(cal); - - int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - args.rval().setInt32(offset); - return true; -} - -bool -js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(args[0].isString()); - MOZ_ASSERT(args[1].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); - if (!skeletonFlat) - return false; - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, skeletonFlat)) - return false; - - mozilla::Range skeletonChars = stableChars.twoByteRange(); - uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); - - UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(gen); - - JSString* str = - CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { - return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, - chars, size, status); - }); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -/** - * Returns a new UDateFormat with the locale and date-time formatting options - * of the given DateTimeFormat. - */ -static UDateFormat* -NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // We don't need to look at calendar and numberingSystem - they can only be - // set via the Unicode locale extension and are therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) - return nullptr; - - AutoStableStringChars timeZoneChars(cx); - Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); - if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) - return nullptr; - - const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); - uint32_t uTimeZoneLength = u_strlen(uTimeZone); - - if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) - return nullptr; - - AutoStableStringChars patternChars(cx); - Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); - if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) - return nullptr; - - const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); - uint32_t uPatternLength = u_strlen(uPattern); - - UErrorCode status = U_ZERO_ERROR; - UDateFormat* df = - udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, - uPattern, uPatternLength, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - - // ECMAScript requires the Gregorian calendar to be used from the beginning - // of ECMAScript time. - UCalendar* cal = const_cast(udat_getCalendar(df)); - ucal_setGregorianChange(cal, StartOfTime, &status); - - // An error here means the calendar is not Gregorian, so we don't care. - - return df; -} - -static bool -intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { - return udat_format(df, x, chars, size, nullptr, status); - }); - if (!str) - return false; - - result.setString(str); - return true; -} - -using FieldType = ImmutablePropertyNamePtr JSAtomState::*; - -static FieldType -GetFieldTypeForFormatField(UDateFormatField fieldName) -{ - // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This - // switch is deliberately exhaustive: cases might have to be added/removed - // if this code is compiled with a different ICU with more - // UDateFormatField enum initializers. Please guard such cases with - // appropriate ICU version-testing #ifdefs, should cross-version divergence - // occur. - switch (fieldName) { - case UDAT_ERA_FIELD: - return &JSAtomState::era; - case UDAT_YEAR_FIELD: - case UDAT_YEAR_WOY_FIELD: - case UDAT_EXTENDED_YEAR_FIELD: - case UDAT_YEAR_NAME_FIELD: - return &JSAtomState::year; - - case UDAT_MONTH_FIELD: - case UDAT_STANDALONE_MONTH_FIELD: - return &JSAtomState::month; - - case UDAT_DATE_FIELD: - case UDAT_JULIAN_DAY_FIELD: - return &JSAtomState::day; - - case UDAT_HOUR_OF_DAY1_FIELD: - case UDAT_HOUR_OF_DAY0_FIELD: - case UDAT_HOUR1_FIELD: - case UDAT_HOUR0_FIELD: - return &JSAtomState::hour; - - case UDAT_MINUTE_FIELD: - return &JSAtomState::minute; - - case UDAT_SECOND_FIELD: - return &JSAtomState::second; - - case UDAT_DAY_OF_WEEK_FIELD: - case UDAT_STANDALONE_DAY_FIELD: - case UDAT_DOW_LOCAL_FIELD: - case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: - return &JSAtomState::weekday; - - case UDAT_AM_PM_FIELD: - return &JSAtomState::dayPeriod; - - case UDAT_TIMEZONE_FIELD: - return &JSAtomState::timeZoneName; - - case UDAT_FRACTIONAL_SECOND_FIELD: - case UDAT_DAY_OF_YEAR_FIELD: - case UDAT_WEEK_OF_YEAR_FIELD: - case UDAT_WEEK_OF_MONTH_FIELD: - case UDAT_MILLISECONDS_IN_DAY_FIELD: - case UDAT_TIMEZONE_RFC_FIELD: - case UDAT_TIMEZONE_GENERIC_FIELD: - case UDAT_QUARTER_FIELD: - case UDAT_STANDALONE_QUARTER_FIELD: - case UDAT_TIMEZONE_SPECIAL_FIELD: - case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: - case UDAT_TIMEZONE_ISO_FIELD: - case UDAT_TIMEZONE_ISO_LOCAL_FIELD: -#ifndef U_HIDE_INTERNAL_API - case UDAT_RELATED_YEAR_FIELD: -#endif -#ifndef U_HIDE_DRAFT_API - case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: - case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: -#endif -#ifndef U_HIDE_INTERNAL_API - case UDAT_TIME_SEPARATOR_FIELD: -#endif - // These fields are all unsupported. - return nullptr; - - case UDAT_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject toClose(fpositer); - - RootedString overallResult(cx); - overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { - return udat_formatForFields(df, x, chars, size, fpositer, status); - }); - if (!overallResult) - return false; - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - if (overallResult->length() == 0) { - // An empty string contains no parts, so avoid extra work below. - result.setObject(*partsArray); - return true; - } - - size_t lastEndIndex = 0; - - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue partType(cx); - RootedValue val(cx); - - auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - partType = StringValue(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, partType)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); - if (!partSubstr) - return false; - - val = StringValue(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, val)) - return false; - - val = ObjectValue(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, val)) - return false; - - lastEndIndex = endIndex; - partIndex++; - return true; - }; - - int32_t fieldInt, beginIndexInt, endIndexInt; - while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { - MOZ_ASSERT(beginIndexInt >= 0); - MOZ_ASSERT(endIndexInt >= 0); - MOZ_ASSERT(beginIndexInt <= endIndexInt, - "field iterator returning invalid range"); - - size_t beginIndex(beginIndexInt); - size_t endIndex(endIndexInt); - - // Technically this isn't guaranteed. But it appears true in pratice, - // and http://bugs.icu-project.org/trac/ticket/12024 is expected to - // correct the documentation lapse. - MOZ_ASSERT(lastEndIndex <= beginIndex, - "field iteration didn't return fields in order start to " - "finish as expected"); - - if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { - if (lastEndIndex < beginIndex) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) - return false; - } - - if (!AppendPart(type, beginIndex, endIndex)) - return false; - } - } - - // Append any final literal. - if (lastEndIndex < overallResult->length()) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) - return false; - } - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - MOZ_ASSERT(args[0].isObject()); - MOZ_ASSERT(args[1].isNumber()); - MOZ_ASSERT(args[2].isBoolean()); - - RootedObject dateTimeFormat(cx, &args[0].toObject()); - - // Obtain a UDateFormat object, cached if possible. - bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatClass; - UDateFormat* df; - if (isDateTimeFormatInstance) { - void* priv = - dateTimeFormat->as().getReservedSlot(UDATE_FORMAT_SLOT).toPrivate(); - df = static_cast(priv); - if (!df) { - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - dateTimeFormat->as().setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(df)); - } - } else { - // There's no good place to cache the ICU date-time format for an object - // that has been initialized as a DateTimeFormat but is not a - // DateTimeFormat instance. One possibility might be to add a - // DateTimeFormat instance as an internal property to each such object. - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - } - - // Use the UDateFormat to actually format the time stamp. - RootedValue result(cx); - bool success = args[2].toBoolean() - ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) - : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); - - if (!isDateTimeFormatInstance) - udat_close(df); - if (!success) - return false; - args.rval().set(result); - return true; -} - /**************** PluralRules *****************/ static void pluralRules_finalize(FreeOp* fop, JSObject* obj); diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 0f934debfb..49a76dfb5c 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -35,102 +35,6 @@ InitIntlClass(JSContext* cx, HandleObject obj); * The following functions are for use by self-hosted code. */ - -/******************** DateTimeFormat ********************/ - -/** - * Returns a new instance of the standard built-in DateTimeFormat constructor. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for date and time - * formatting by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_DateTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array with the calendar type identifiers per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * supported calendars for the given locale. The default calendar is - * element 0. - * - * Usage: calendars = intl_availableCalendars(locale) - */ -extern MOZ_MUST_USE bool -intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); - -/** - * 6.4.1 IsValidTimeZoneName ( timeZone ) - * - * Verifies that the given string is a valid time zone name. If it is a valid - * time zone name, its IANA time zone name is returned. Otherwise returns null. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - * - * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) - */ -extern MOZ_MUST_USE bool -intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the canonicalized time zone name. Canonicalization resolves link - * names to their target time zones. - * - * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) - */ -extern MOZ_MUST_USE bool -intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the default time zone name. The time zone name is not canonicalized. - * - * Usage: icuDefaultTimeZone = intl_defaultTimeZone() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the raw offset from GMT in milliseconds for the default time zone. - * - * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return a pattern in the date-time format pattern language of Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * best-fit date-time format pattern corresponding to skeleton for the - * given locale. - * - * Usage: pattern = intl_patternForSkeleton(locale, skeleton) - */ -extern MOZ_MUST_USE bool -intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a String value representing x (which must be a Number value) - * according to the effective locale and the formatting options of the - * given DateTimeFormat. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - * - * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) - */ -extern MOZ_MUST_USE bool -intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); - /******************** PluralRules ********************/ /** diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp new file mode 100644 index 0000000000..167beea0c1 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -0,0 +1,819 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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.DateTimeFormat implementation. */ + +#include "builtin/intl/DateTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Range.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/IntlTimeZoneData.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsFinite; + +using JS::ClippedTime; +using JS::TimeClip; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** DateTimeFormat ********************/ + +const ClassOps DateTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DateTimeFormatObject::finalize +}; + +const Class DateTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &DateTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DateTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec dateTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec dateTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when DateTimeFormat is called + // because of backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 12.1.2.1 step 3 + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + // ES Intl 1st ed., 12.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 12.1.2.1 step 5 + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + // ES Intl 1st ed., 12.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 3. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_DateTimeFormat is an intrinsic for self-hosted JavaScript, so it + // cannot be used with "new", but it still has to be treated as a + // constructor. + return DateTimeFormat(cx, args, true); +} + +void +js::DateTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UDateFormat* df = static_cast(slot.toPrivate())) + udat_close(df); + } +} + +JSObject* +js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, + &DateTimeFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 12.2.2 + if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) + return nullptr; + + // 12.3.2 and 12.3.3 + if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) + return nullptr; + + // Install a getter for DateTimeFormat.prototype.format that returns a + // formatting function bound to a specified DateTimeFormat object (suitable + // for passing to methods like Array.prototype.map). + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, + &getter)) + { + return nullptr; + } + if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue, + JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), + nullptr, JSPROP_GETTER | JSPROP_SHARED)) + { + return nullptr; + } + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + // 12.2.1 and 12.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +// ICU returns old-style keyword values; map them to BCP 47 equivalents +// (see http://bugs.icu-project.org/trac/ticket/9620). +static const char* +bcp47CalendarName(const char* icuName) +{ + if (StringsAreEqual(icuName, "ethiopic-amete-alem")) + return "ethioaa"; + if (StringsAreEqual(icuName, "gregorian")) + return "gregory"; + if (StringsAreEqual(icuName, "islamic-civil")) + return "islamicc"; + return icuName; +} + +bool +js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + RootedObject calendars(cx, NewDenseEmptyArray(cx)); + if (!calendars) + return false; + uint32_t index = 0; + + // We need the default calendar for the locale as the first result. + UErrorCode status = U_ZERO_ERROR; + RootedString jscalendar(cx); + { + UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); + + // This correctly handles nullptr |cal| when opening failed. + ScopedICUObject closeCalendar(cal); + + const char* calendar = ucal_getType(cal, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + } + + RootedValue element(cx, StringValue(jscalendar)); + if (!DefineElement(cx, calendars, index++, element)) + return false; + + // Now get the calendars that "would make a difference", i.e., not the default. + UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + for (; count > 0; count--) { + const char* calendar = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + } + + args.rval().setObject(*calendars); + return true; +} + +bool +js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + RootedString timeZone(cx, args[0].toString()); + RootedString validatedTimeZone(cx); + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) + return false; + + if (validatedTimeZone) + args.rval().setString(validatedTimeZone); + else + args.rval().setNull(); + + return true; +} + +bool +js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + RootedString timeZone(cx, args[0].toString()); + RootedString ianaTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) + return false; + + if (ianaTimeZone) { + args.rval().setString(ianaTimeZone); + return true; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) + return false; + + mozilla::Range tzchars = stableChars.twoByteRange(); + + JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { + return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), + chars, size, nullptr, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + // The current default might be stale, because JS::ResetTimeZone() doesn't + // immediately update ICU's default time zone. So perform an update if + // needed. + js::ResyncICUDefaultTimeZone(); + + JSString* str = CallICU(cx, ucal_getDefaultTimeZone); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + const char* rootLocale = ""; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(cal); + + int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + args.rval().setInt32(offset); + return true; +} + +bool +js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); + if (!skeletonFlat) + return false; + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, skeletonFlat)) + return false; + + mozilla::Range skeletonChars = stableChars.twoByteRange(); + uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); + + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + JSString* str = + CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, + chars, size, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +/** + * Returns a new UDateFormat with the locale and date-time formatting options + * of the given DateTimeFormat. + */ +static UDateFormat* +NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // We don't need to look at calendar and numberingSystem - they can only be + // set via the Unicode locale extension and are therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) + return nullptr; + + AutoStableStringChars timeZoneChars(cx); + Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); + if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) + return nullptr; + + const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); + uint32_t uTimeZoneLength = u_strlen(uTimeZone); + + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) + return nullptr; + + AutoStableStringChars patternChars(cx); + Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); + if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) + return nullptr; + + const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); + uint32_t uPatternLength = u_strlen(uPattern); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = + udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, + uPattern, uPatternLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + // ECMAScript requires the Gregorian calendar to be used from the beginning + // of ECMAScript time. + UCalendar* cal = const_cast(udat_getCalendar(df)); + ucal_setGregorianChange(cal, StartOfTime, &status); + + // An error here means the calendar is not Gregorian, so we don't care. + + return df; +} + +static bool +intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { + return udat_format(df, x, chars, size, nullptr, status); + }); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForFormatField(UDateFormatField fieldName) +{ + // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This + // switch is deliberately exhaustive: cases might have to be added/removed + // if this code is compiled with a different ICU with more + // UDateFormatField enum initializers. Please guard such cases with + // appropriate ICU version-testing #ifdefs, should cross-version divergence + // occur. + switch (fieldName) { + case UDAT_ERA_FIELD: + return &JSAtomState::era; + case UDAT_YEAR_FIELD: + case UDAT_YEAR_WOY_FIELD: + case UDAT_EXTENDED_YEAR_FIELD: + case UDAT_YEAR_NAME_FIELD: + return &JSAtomState::year; + + case UDAT_MONTH_FIELD: + case UDAT_STANDALONE_MONTH_FIELD: + return &JSAtomState::month; + + case UDAT_DATE_FIELD: + case UDAT_JULIAN_DAY_FIELD: + return &JSAtomState::day; + + case UDAT_HOUR_OF_DAY1_FIELD: + case UDAT_HOUR_OF_DAY0_FIELD: + case UDAT_HOUR1_FIELD: + case UDAT_HOUR0_FIELD: + return &JSAtomState::hour; + + case UDAT_MINUTE_FIELD: + return &JSAtomState::minute; + + case UDAT_SECOND_FIELD: + return &JSAtomState::second; + + case UDAT_DAY_OF_WEEK_FIELD: + case UDAT_STANDALONE_DAY_FIELD: + case UDAT_DOW_LOCAL_FIELD: + case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: + return &JSAtomState::weekday; + + case UDAT_AM_PM_FIELD: + return &JSAtomState::dayPeriod; + + case UDAT_TIMEZONE_FIELD: + return &JSAtomState::timeZoneName; + + case UDAT_FRACTIONAL_SECOND_FIELD: + case UDAT_DAY_OF_YEAR_FIELD: + case UDAT_WEEK_OF_YEAR_FIELD: + case UDAT_WEEK_OF_MONTH_FIELD: + case UDAT_MILLISECONDS_IN_DAY_FIELD: + case UDAT_TIMEZONE_RFC_FIELD: + case UDAT_TIMEZONE_GENERIC_FIELD: + case UDAT_QUARTER_FIELD: + case UDAT_STANDALONE_QUARTER_FIELD: + case UDAT_TIMEZONE_SPECIAL_FIELD: + case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: + case UDAT_TIMEZONE_ISO_FIELD: + case UDAT_TIMEZONE_ISO_LOCAL_FIELD: +#ifndef U_HIDE_INTERNAL_API + case UDAT_RELATED_YEAR_FIELD: +#endif +#ifndef U_HIDE_DRAFT_API + case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: + case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: +#endif +#ifndef U_HIDE_INTERNAL_API + case UDAT_TIME_SEPARATOR_FIELD: +#endif + // These fields are all unsupported. + return nullptr; + + case UDAT_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + UErrorCode status = U_ZERO_ERROR; + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx); + overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return udat_formatForFields(df, x, chars, size, fpositer, status); + }); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + if (overallResult->length() == 0) { + // An empty string contains no parts, so avoid extra work below. + result.setObject(*partsArray); + return true; + } + + size_t lastEndIndex = 0; + + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue partType(cx); + RootedValue val(cx); + + auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + partType = StringValue(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, partType)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); + if (!partSubstr) + return false; + + val = StringValue(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, val)) + return false; + + val = ObjectValue(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, val)) + return false; + + lastEndIndex = endIndex; + partIndex++; + return true; + }; + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt >= 0); + MOZ_ASSERT(endIndexInt >= 0); + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex(beginIndexInt); + size_t endIndex(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { + if (lastEndIndex < beginIndex) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) + return false; + } + + if (!AppendPart(type, beginIndex, endIndex)) + return false; + } + } + + // Append any final literal. + if (lastEndIndex < overallResult->length()) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) + return false; + } + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isNumber()); + MOZ_ASSERT(args[2].isBoolean()); + + RootedObject dateTimeFormat(cx, &args[0].toObject()); + + // Obtain a UDateFormat object, cached if possible. + bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatObject::class_; + UDateFormat* df; + if (isDateTimeFormatInstance) { + void* priv = + dateTimeFormat->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); + df = static_cast(priv); + if (!df) { + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + dateTimeFormat->as().setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); + } + } else { + // There's no good place to cache the ICU date-time format for an object + // that has been initialized as a DateTimeFormat but is not a + // DateTimeFormat instance. One possibility might be to add a + // DateTimeFormat instance as an internal property to each such object. + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + } + + // Use the UDateFormat to actually format the time stamp. + RootedValue result(cx); + bool success = args[2].toBoolean() + ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) + : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); + + if (!isDateTimeFormatInstance) + udat_close(df); + if (!success) + return false; + args.rval().set(result); + return true; +} + diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h new file mode 100644 index 0000000000..9bdddc58f2 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_DateTimeFormat_h +#define builtin_intl_DateTimeFormat_h + +#include "mozilla/Attributes.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; +class GlobalObject; + +/******************** DateTimeFormat ********************/ + +class DateTimeFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UDATE_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* + +CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new instance of the standard built-in DateTimeFormat constructor. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: dateTimeFormat = intl_DateTimeFormat(locales, options) + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for date and time + * formatting by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_DateTimeFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array with the calendar type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * supported calendars for the given locale. The default calendar is + * element 0. + * + * Usage: calendars = intl_availableCalendars(locale) + */ +extern MOZ_MUST_USE bool +intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); + +/** + * 6.4.1 IsValidTimeZoneName ( timeZone ) + * + * Verifies that the given string is a valid time zone name. If it is a valid + * time zone name, its IANA time zone name is returned. Otherwise returns null. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + * + * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) + */ +extern MOZ_MUST_USE bool +intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the canonicalized time zone name. Canonicalization resolves link + * names to their target time zones. + * + * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) + */ +extern MOZ_MUST_USE bool +intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the default time zone name. The time zone name is not canonicalized. + * + * Usage: icuDefaultTimeZone = intl_defaultTimeZone() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the raw offset from GMT in milliseconds for the default time zone. + * + * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return a pattern in the date-time format pattern language of Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * best-fit date-time format pattern corresponding to skeleton for the + * given locale. + * + * Usage: pattern = intl_patternForSkeleton(locale, skeleton) + */ +extern MOZ_MUST_USE bool +intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a String value representing x (which must be a Number value) + * according to the effective locale and the formatting options of the + * given DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * + * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) + */ +extern MOZ_MUST_USE bool +intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); + + +} // namespace js + +#endif /* builtin_intl_DateTimeFormat_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 0b80c31802..54b7423730 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -117,6 +117,7 @@ UNIFIED_SOURCES += [ 'builtin/Intl.cpp', 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', + 'builtin/intl/DateTimeFormat.cpp', 'builtin/intl/NumberFormat.cpp', 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 6e7da8f053..a8a5e5abff 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -24,6 +24,7 @@ #include "builtin/Intl.h" #include "builtin/intl/Collator.h" +#include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/NumberFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" From ed316832bf89319b51c2cb14a3a35a8142d3ee1a Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 01:33:43 +0100 Subject: [PATCH 09/24] Issue #2046 - Move Intl.PluralRules functionality into builtin/intl/PluralRules.* --- js/src/builtin/Intl.cpp | 444 +------------------------- js/src/builtin/Intl.h | 48 --- js/src/builtin/intl/PluralRules.cpp | 473 ++++++++++++++++++++++++++++ js/src/builtin/intl/PluralRules.h | 91 ++++++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 6 files changed, 567 insertions(+), 491 deletions(-) create mode 100644 js/src/builtin/intl/PluralRules.cpp create mode 100644 js/src/builtin/intl/PluralRules.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 3bf86b53a3..70539fadf5 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -27,6 +27,7 @@ #include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" +#include "builtin/intl/PluralRules.h" #include "builtin/intl/ScopedICUObject.h" #include "ds/Sort.h" #include "vm/DateTime.h" @@ -52,449 +53,6 @@ using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; -/**************** PluralRules *****************/ - -static void pluralRules_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UPLURAL_RULES_SLOT = 0; -static const uint32_t PLURAL_RULES_SLOTS_COUNT = 1; - -static const ClassOps PluralRulesClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - pluralRules_finalize -}; - -static const Class PluralRulesClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(PLURAL_RULES_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &PluralRulesClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().PluralRules); - return true; -} -#endif - -static const JSFunctionSpec pluralRules_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_PluralRules_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec pluralRules_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * PluralRules constructor. - * Spec: ECMAScript 402 API, PluralRules, 1.1 - */ -static bool -PluralRules(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - if (!construct) { - JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); - if (!intl) - return false; - RootedValue self(cx, args.thisv()); - if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { - obj = ToObject(cx, self); - if (!obj) - return false; - - bool extensible; - if (!IsExtensible(cx, obj, &extensible)) - return false; - if (!extensible) - return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); - } else { - construct = true; - } - } - if (construct) { - RootedObject proto(cx, GlobalObject::getOrCreatePluralRulesPrototype(cx, cx->global())); - if (!proto) - return false; - obj = NewObjectWithGivenProto(cx, &PluralRulesClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - if (!intl::InitializeObject(cx, obj, cx->names().InitializePluralRules, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -PluralRules(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return PluralRules(cx, args, args.isConstructing()); -} - -bool -js::intl_PluralRules(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - return PluralRules(cx, args, true); -} - -static void -pluralRules_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(UPLURAL_RULES_SLOT); - if (!slot.isUndefined()) { - if (UPluralRules* pr = static_cast(slot.toPrivate())) - uplrules_close(pr); - } -} - -static JSObject* -CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = global->createConstructor(cx, &PluralRules, cx->names().PluralRules, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &PluralRulesClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - if (!JS_DefineFunctions(cx, ctor, pluralRules_static_methods)) - return nullptr; - - if (!JS_DefineFunctions(cx, proto, pluralRules_methods)) - return nullptr; - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - if (!intl::InitializeObject(cx, proto, cx->names().InitializePluralRules, UndefinedHandleValue, - options)) - { - return nullptr; - } - - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().PluralRules, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - // We're going to use ULocale availableLocales as per ICU recommendation: - // https://ssl.icu-project.org/trac/ticket/12756 - if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -/** - * - * This creates new UNumberFormat with calculated digit formatting - * properties for PluralRules. - * - * This is similar to NewUNumberFormat but doesn't allow for currency or - * percent types. - * - */ -static UNumberFormat* -NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) -{ - RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); - if (!internals) - return nullptr; - - RootedValue value(cx); - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - uint32_t uMinimumIntegerDigits = 1; - uint32_t uMinimumFractionDigits = 0; - uint32_t uMaximumFractionDigits = 3; - int32_t uMinimumSignificantDigits = -1; - int32_t uMaximumSignificantDigits = -1; - - RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); - bool hasP; - if (!HasProperty(cx, internals, id, &hasP)) - return nullptr; - if (hasP) { - if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, - &value)) - return nullptr; - uMinimumSignificantDigits = value.toInt32(); - - if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, - &value)) - return nullptr; - uMaximumSignificantDigits = value.toInt32(); - } else { - if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, - &value)) - return nullptr; - uMinimumIntegerDigits = AssertedCast(value.toInt32()); - - if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, - &value)) - return nullptr; - uMinimumFractionDigits = AssertedCast(value.toInt32()); - - if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, - &value)) - return nullptr; - uMaximumFractionDigits = AssertedCast(value.toInt32()); - } - - UErrorCode status = U_ZERO_ERROR; - UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uMinimumSignificantDigits != -1) { - unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); - unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); - unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); - } else { - unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); - unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); - unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); - } - - return toClose.forget(); -} - -bool -js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - RootedObject pluralRules(cx, &args[0].toObject()); - - UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules); - if (!nf) - return false; - - ScopedICUObject closeNumberFormat(nf); - - RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); - if (!internals) - return false; - - RootedValue value(cx); - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return false; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return false; - - if (!GetProperty(cx, internals, internals, cx->names().type, &value)) - return false; - JSAutoByteString type(cx, value.toString()); - if (!type) - return false; - - double x = args[1].toNumber(); - - // We need a NumberFormat in order to format the number - // using the number formatting options (minimum/maximum*Digits) - // before we push the result to PluralRules - // - // This should be fixed in ICU 59 and we'll be able to switch to that - // API: http://bugs.icu-project.org/trac/ticket/12763 - // - RootedValue fmtNumValue(cx); - if (!intl_FormatNumber(cx, nf, x, &fmtNumValue)) - return false; - RootedString fmtNumValueString(cx, fmtNumValue.toString()); - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, fmtNumValueString)) - return false; - - const UChar* uFmtNumValue = Char16ToUChar(stableChars.twoByteRange().begin().get()); - - UErrorCode status = U_ZERO_ERROR; - - UFormattable* fmt = unum_parseToUFormattable(nf, nullptr, uFmtNumValue, - stableChars.twoByteRange().length(), 0, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject closeUFormattable(fmt); - - double y = ufmt_getDouble(fmt, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - UPluralType category; - - if (StringsAreEqual(type, "cardinal")) { - category = UPLURAL_TYPE_CARDINAL; - } else { - MOZ_ASSERT(StringsAreEqual(type, "ordinal")); - category = UPLURAL_TYPE_ORDINAL; - } - - UPluralRules* pr = uplrules_openForType(IcuLocale(locale.ptr()), category, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject closePluralRules(pr); - - JSString* str = CallICU(cx, [pr, y](UChar* chars, int32_t size, UErrorCode* status) { - return uplrules_select(pr, y, chars, size, status); - }); - if (!str) - return false; - - args.rval().setString(str); - return true; -} - -bool -js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSAutoByteString type(cx, args[1].toString()); - if (!type) - return false; - - UErrorCode status = U_ZERO_ERROR; - - UPluralType category; - - if (StringsAreEqual(type, "cardinal")) { - category = UPLURAL_TYPE_CARDINAL; - } else { - MOZ_ASSERT(StringsAreEqual(type, "ordinal")); - category = UPLURAL_TYPE_ORDINAL; - } - - UPluralRules* pr = uplrules_openForType( - IcuLocale(locale.ptr()), - category, - &status - ); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject closePluralRules(pr); - - // We should get a C API for that in ICU 59 and switch to it - // https://ssl.icu-project.org/trac/ticket/12772 - // - icu::StringEnumeration* kwenum = - reinterpret_cast(pr)->getKeywords(status); - UEnumeration* ue = uenum_openFromStringEnumeration(kwenum, &status); - - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - ScopedICUObject closeEnum(ue); - - RootedObject res(cx, NewDenseEmptyArray(cx)); - if (!res) - return false; - - RootedValue element(cx); - uint32_t i = 0; - int32_t catSize; - const char* cat; - - do { - cat = uenum_next(ue, &catSize, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - - if (!cat) - break; - - JSString* str = NewStringCopyN(cx, cat, catSize); - if (!str) - return false; - - element.setString(str); - if (!DefineElement(cx, res, i, element)) - return false; - i++; - } while (true); - - args.rval().setObject(*res); - return true; -} - /**************** RelativeTimeFormat *****************/ static void relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj); diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 49a76dfb5c..dca971a7be 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -35,54 +35,6 @@ InitIntlClass(JSContext* cx, HandleObject obj); * The following functions are for use by self-hosted code. */ -/******************** PluralRules ********************/ - -/** - * Returns a new PluralRules instance. - * Self-hosted code cannot cache this constructor (as it does for others in - * Utilities.js) because it is initialized after self-hosted code is compiled. - * - * Usage: pluralRules = intl_PluralRules(locales, options) - */ -extern MOZ_MUST_USE bool -intl_PluralRules(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for plural rules - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_PluralRules_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a plural rule for the number x according to the effective - * locale and the formatting options of the given PluralRules. - * - * A plural rule is a grammatical category that expresses count distinctions - * (such as "one", "two", "few" etc.). - * - * Usage: rule = intl_SelectPluralRule(pluralRules, x) - */ -extern MOZ_MUST_USE bool -intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array of plural rules categories for a given - * locale and type. - * - * Usage: categories = intl_GetPluralCategories(locale, type) - * - * Example: - * - * intl_getPluralCategories('pl', 'cardinal'); // ['one', 'few', 'many', 'other'] - */ -extern MOZ_MUST_USE bool -intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp); - /******************** RelativeTimeFormat ********************/ /** diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp new file mode 100644 index 0000000000..78a2f3a847 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.cpp @@ -0,0 +1,473 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +/* Implementation of the Intl.PluralRules proposal. */ + +#include "builtin/intl/PluralRules.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" +#include "vm/String.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/**************** PluralRules *****************/ + +const ClassOps PluralRulesObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + PluralRulesObject::finalize +}; + +const Class PluralRulesObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &PluralRulesObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().PluralRules); + return true; +} +#endif + +static const JSFunctionSpec pluralRules_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_PluralRules_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec pluralRules_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * PluralRules constructor. + * Spec: ECMAScript 402 API, PluralRules, 1.1 + */ +static bool +PluralRules(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + if (!construct) { + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + obj = ToObject(cx, self); + if (!obj) + return false; + + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + construct = true; + } + } + if (construct) { + RootedObject proto(cx, GlobalObject::getOrCreatePluralRulesPrototype(cx, cx->global())); + if (!proto) + return false; + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(PluralRulesObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + if (!intl::InitializeObject(cx, obj, cx->names().InitializePluralRules, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +PluralRules(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return PluralRules(cx, args, args.isConstructing()); +} + +bool +js::intl_PluralRules(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + return PluralRules(cx, args, true); +} + +void +js::PluralRulesObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT); + if (!slot.isUndefined()) { + if (UPluralRules* pr = static_cast(slot.toPrivate())) + uplrules_close(pr); + } +} + +JSObject* +js::CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = global->createConstructor(cx, &PluralRules, cx->names().PluralRules, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &PluralRulesObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (!JS_DefineFunctions(cx, ctor, pluralRules_static_methods)) + return nullptr; + + if (!JS_DefineFunctions(cx, proto, pluralRules_methods)) + return nullptr; + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!intl::InitializeObject(cx, proto, cx->names().InitializePluralRules, UndefinedHandleValue, + options)) + { + return nullptr; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().PluralRules, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + // We're going to use ULocale availableLocales as per ICU recommendation: + // https://ssl.icu-project.org/trac/ticket/12756 + if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +/** + * + * This creates new UNumberFormat with calculated digit formatting + * properties for PluralRules. + * + * This is similar to NewUNumberFormat but doesn't allow for currency or + * percent types. + * + */ +static UNumberFormat* +NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) +{ + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); + if (!internals) + return nullptr; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + + RootedId id(cx, NameToId(cx->names().minimumSignificantDigits)); + bool hasP; + if (!HasProperty(cx, internals, id, &hasP)) + return nullptr; + if (hasP) { + if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, + &value)) + return nullptr; + uMinimumSignificantDigits = value.toInt32(); + + if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, + &value)) + return nullptr; + uMaximumSignificantDigits = value.toInt32(); + } else { + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) + return nullptr; + uMinimumIntegerDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + ScopedICUObject toClose(nf); + + if (uMinimumSignificantDigits != -1) { + unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true); + unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits); + unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits); + } else { + unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits); + unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits); + unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits); + } + + return toClose.forget(); +} + +bool +js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject pluralRules(cx, &args[0].toObject()); + + UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules); + if (!nf) + return false; + + ScopedICUObject closeNumberFormat(nf); + + RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); + if (!internals) + return false; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return false; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return false; + + if (!GetProperty(cx, internals, internals, cx->names().type, &value)) + return false; + JSAutoByteString type(cx, value.toString()); + if (!type) + return false; + + double x = args[1].toNumber(); + + // We need a NumberFormat in order to format the number + // using the number formatting options (minimum/maximum*Digits) + // before we push the result to PluralRules + // + // This should be fixed in ICU 59 and we'll be able to switch to that + // API: http://bugs.icu-project.org/trac/ticket/12763 + // + RootedValue fmtNumValue(cx); + if (!intl_FormatNumber(cx, nf, x, &fmtNumValue)) + return false; + RootedString fmtNumValueString(cx, fmtNumValue.toString()); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, fmtNumValueString)) + return false; + + const UChar* uFmtNumValue = Char16ToUChar(stableChars.twoByteRange().begin().get()); + + UErrorCode status = U_ZERO_ERROR; + + UFormattable* fmt = unum_parseToUFormattable(nf, nullptr, uFmtNumValue, + stableChars.twoByteRange().length(), 0, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closeUFormattable(fmt); + + double y = ufmt_getDouble(fmt, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + UPluralType category; + + if (StringsAreEqual(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType(IcuLocale(locale.ptr()), category, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closePluralRules(pr); + + JSString* str = CallICU(cx, [pr, y](UChar* chars, int32_t size, UErrorCode* status) { + return uplrules_select(pr, y, chars, size, status); + }); + if (!str) + return false; + + args.rval().setString(str); + return true; +} + +bool +js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSAutoByteString type(cx, args[1].toString()); + if (!type) + return false; + + UErrorCode status = U_ZERO_ERROR; + + UPluralType category; + + if (StringsAreEqual(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType( + IcuLocale(locale.ptr()), + category, + &status + ); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closePluralRules(pr); + + // We should get a C API for that in ICU 59 and switch to it + // https://ssl.icu-project.org/trac/ticket/12772 + // + icu::StringEnumeration* kwenum = + reinterpret_cast(pr)->getKeywords(status); + UEnumeration* ue = uenum_openFromStringEnumeration(kwenum, &status); + + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closeEnum(ue); + + RootedObject res(cx, NewDenseEmptyArray(cx)); + if (!res) + return false; + + RootedValue element(cx); + uint32_t i = 0; + int32_t catSize; + const char* cat; + + do { + cat = uenum_next(ue, &catSize, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (!cat) + break; + + JSString* str = NewStringCopyN(cx, cat, catSize); + if (!str) + return false; + + element.setString(str); + if (!DefineElement(cx, res, i, element)) + return false; + i++; + } while (true); + + args.rval().setObject(*res); + return true; +} diff --git a/js/src/builtin/intl/PluralRules.h b/js/src/builtin/intl/PluralRules.h new file mode 100644 index 0000000000..7213786766 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_PluralRules_h +#define builtin_intl_PluralRules_h + +#include "mozilla/Attributes.h" + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class PluralRulesObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UPLURAL_RULES_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* +CreatePluralRulesPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new PluralRules instance. + * Self-hosted code cannot cache this constructor (as it does for others in + * Utilities.js) because it is initialized after self-hosted code is compiled. + * + * Usage: pluralRules = intl_PluralRules(locales, options) + */ +extern MOZ_MUST_USE bool +intl_PluralRules(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for plural rules + * by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_PluralRules_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a plural rule for the number x according to the effective + * locale and the formatting options of the given PluralRules. + * + * A plural rule is a grammatical category that expresses count distinctions + * (such as "one", "two", "few" etc.). + * + * Usage: rule = intl_SelectPluralRule(pluralRules, x) + */ +extern MOZ_MUST_USE bool +intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array of plural rules categories for a given + * locale and type. + * + * Usage: categories = intl_GetPluralCategories(locale, type) + * + * Example: + * + * intl_getPluralCategories('pl', 'cardinal'); // ['one', 'few', 'many', 'other'] + */ +extern MOZ_MUST_USE bool +intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp); + +} // namespace js + +#endif /* builtin_intl_PluralRules_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 54b7423730..23fcd7bdf6 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -119,6 +119,7 @@ UNIFIED_SOURCES += [ 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/DateTimeFormat.cpp', 'builtin/intl/NumberFormat.cpp', + 'builtin/intl/PluralRules.cpp', 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index a8a5e5abff..3248f8d47a 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -26,6 +26,7 @@ #include "builtin/intl/Collator.h" #include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/NumberFormat.h" +#include "builtin/intl/PluralRules.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" From c7dab6c035a9135dcaaf63fa0b002b95b38ef8eb Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 21:26:50 +0100 Subject: [PATCH 10/24] Issue #2046 - Move Intl.RelativeTimeFormat functionality into builtin/intl/RelativeTimeFormat.* --- js/src/builtin/Intl.cpp | 298 +------------------ js/src/builtin/Intl.h | 25 -- js/src/builtin/intl/RelativeTimeFormat.cpp | 321 +++++++++++++++++++++ js/src/builtin/intl/RelativeTimeFormat.h | 68 +++++ js/src/moz.build | 1 + js/src/vm/SelfHosting.cpp | 1 + 6 files changed, 393 insertions(+), 321 deletions(-) create mode 100644 js/src/builtin/intl/RelativeTimeFormat.cpp create mode 100644 js/src/builtin/intl/RelativeTimeFormat.h diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index 70539fadf5..be601f3592 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -28,6 +28,7 @@ #include "builtin/intl/ICUHeader.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/PluralRules.h" +#include "builtin/intl/RelativeTimeFormat.h" #include "builtin/intl/ScopedICUObject.h" #include "ds/Sort.h" #include "vm/DateTime.h" @@ -43,9 +44,7 @@ using namespace js; -using mozilla::AssertedCast; using mozilla::IsFinite; -using mozilla::IsNegativeZero; using js::intl::CallICU; using js::intl::GetAvailableLocales; @@ -53,298 +52,7 @@ using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; -/**************** RelativeTimeFormat *****************/ - -static void relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t URELATIVE_TIME_FORMAT_SLOT = 0; -static const uint32_t RELATIVE_TIME_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps RelativeTimeFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* enumerate */ - nullptr, /* newEnumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - relativeTimeFormat_finalize -}; - -static const Class RelativeTimeFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(RELATIVE_TIME_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &RelativeTimeFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().RelativeTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec relativeTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec relativeTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -static const JSPropertySpec relativeTimeFormat_properties[] = { - JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), - JS_PS_END}; - -/** - * RelativeTimeFormat constructor. - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 - */ -static bool -RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - // Step 1. - if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) - return false; - - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - RootedObject relativeTimeFormat(cx); - relativeTimeFormat = NewObjectWithGivenProto(cx, &RelativeTimeFormatClass, proto); - if (!relativeTimeFormat) - return false; - - relativeTimeFormat->as().setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); - - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 3. - if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) - return false; - - args.rval().setObject(*relativeTimeFormat); - return true; -} - -static void -relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj) -{ - MOZ_ASSERT(fop->onMainThread()); - - // This is-undefined check shouldn't be necessary, but for internal - // brokenness in object allocation code. For the moment, hack around it by - // explicitly guarding against the possibility of the reserved slot not - // containing a private. See bug 949220. - const Value& slot = obj->as().getReservedSlot(URELATIVE_TIME_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) - ureldatefmt_close(rtf); - } -} - -static JSObject* -CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &RelativeTimeFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) - return nullptr; - - if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) - return nullptr; - - if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) - return nullptr; - - RootedValue options(cx); - if (!intl::CreateDefaultOptions(cx, &options)) - return nullptr; - - if (!intl::InitializeObject(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { - return nullptr; - } - - return proto; -} - -bool -js::intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - // We're going to use ULocale availableLocales as per ICU recommendation: - // https://ssl.icu-project.org/trac/ticket/12756 - if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - - -enum class RelativeTimeNumeric -{ - /** - * Only strings with numeric components like `1 day ago`. - */ - Always, - /** - * Natural-language strings like `yesterday` when possible, - * otherwise strings with numeric components as in `7 months ago`. - */ - Auto, -}; - -bool -js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - - RootedObject relativeTimeFormat(cx, &args[0].toObject()); - - RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); - if (!internals) - return false; - - RootedValue value(cx); - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return false; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return false; - - if (!GetProperty(cx, internals, internals, cx->names().style, &value)) - return false; - RootedLinearString style(cx, value.toString()->ensureLinear(cx)); - if (!style) - return false; - - double t = args[1].toNumber(); - - UDateRelativeDateTimeFormatterStyle relDateTimeStyle; - - if (StringEqualsAscii(style, "short")) { - relDateTimeStyle = UDAT_STYLE_SHORT; - } else if (StringEqualsAscii(style, "narrow")) { - relDateTimeStyle = UDAT_STYLE_NARROW; - } else { - MOZ_ASSERT(StringEqualsAscii(style, "long")); - relDateTimeStyle = UDAT_STYLE_LONG; - } - - URelativeDateTimeUnit relDateTimeUnit; - { - JSLinearString* unit = args[2].toString()->ensureLinear(cx); - if (!unit) { - return false; - } - - if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { - relDateTimeUnit = UDAT_REL_UNIT_SECOND; - } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { - relDateTimeUnit = UDAT_REL_UNIT_MINUTE; - } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { - relDateTimeUnit = UDAT_REL_UNIT_HOUR; - } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { - relDateTimeUnit = UDAT_REL_UNIT_DAY; - } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { - relDateTimeUnit = UDAT_REL_UNIT_WEEK; - } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { - relDateTimeUnit = UDAT_REL_UNIT_MONTH; - } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { - relDateTimeUnit = UDAT_REL_UNIT_QUARTER; - } else { - MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); - relDateTimeUnit = UDAT_REL_UNIT_YEAR; - } - } - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return false; - RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); - if (!numeric) - return false; - - RelativeTimeNumeric relDateTimeNumeric; - - if (StringEqualsAscii(numeric, "auto")) { - relDateTimeNumeric = RelativeTimeNumeric::Auto; - } else { - MOZ_ASSERT(StringEqualsAscii(numeric, "always")); - relDateTimeNumeric = RelativeTimeNumeric::Always; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - UErrorCode status = U_ZERO_ERROR; - URelativeDateTimeFormatter* rtf = - ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, - UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); - if (U_FAILURE(status)) { - intl::ReportInternalError(cx); - return false; - } - ScopedICUObject closeRelativeTimeFormat(rtf); - - JSString* str = - CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, - UErrorCode* status) - { - auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto - ? ureldatefmt_format - : ureldatefmt_formatNumeric; - return fmt(rtf, t, relDateTimeUnit, chars, size, status); - }); - if (!str) - return false; - - args.rval().setString(str); - return true; -} +/******************** Intl ********************/ bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) @@ -714,8 +422,6 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return true; } -/******************** Intl ********************/ - const Class js::IntlClass = { js_Object_str, JSCLASS_HAS_CACHED_PROTO(JSProto_Intl) diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index dca971a7be..938ab4de95 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -35,31 +35,6 @@ InitIntlClass(JSContext* cx, HandleObject obj); * The following functions are for use by self-hosted code. */ -/******************** RelativeTimeFormat ********************/ - -/** - * Returns an object indicating the supported locales for relative time format - * by having a true-valued property for each such locale with the - * canonicalized language tag as the property name. The object has no - * prototype. - * - * Usage: availableLocales = intl_RelativeTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a relative time as a string formatted according to the effective - * locale and the formatting options of the given RelativeTimeFormat. - * - * t should be a number representing a number to be formatted. - * unit should be "second", "minute", "hour", "day", "week", "month", "quarter", or "year". - * - * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t, unit) - */ -extern MOZ_MUST_USE bool -intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp); - /** * Returns a plain object with calendar information for a single valid locale * (callers must perform this validation). The object will have these diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp new file mode 100644 index 0000000000..101d1eb935 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -0,0 +1,321 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +/* Implementation of the Intl.RelativeTimeFormat proposal. */ + +#include "builtin/intl/RelativeTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsNegativeZero; +using mozilla::Range; +using mozilla::RangedPtr; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::StringsAreEqual; + +/**************** RelativeTimeFormat *****************/ + +const ClassOps RelativeTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + RelativeTimeFormatObject::finalize +}; + +const Class RelativeTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &RelativeTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().RelativeTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec relativeTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec relativeTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +static const JSPropertySpec relativeTimeFormat_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), + JS_PS_END}; + +/** + * RelativeTimeFormat constructor. + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 + */ +static bool +RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) + return false; + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + RootedObject relativeTimeFormat(cx); + relativeTimeFormat = NewObjectWithGivenProto(cx, proto); + if (!relativeTimeFormat) + return false; + + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::INTERNALS_SLOT, NullValue()); + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) + return false; + + args.rval().setObject(*relativeTimeFormat); + return true; +} + +void +js::RelativeTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) +{ + MOZ_ASSERT(fop->onMainThread()); + + // This is-undefined check shouldn't be necessary, but for internal + // brokenness in object allocation code. For the moment, hack around it by + // explicitly guarding against the possibility of the reserved slot not + // containing a private. See bug 949220. + const Value& slot = obj->as().getReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) + ureldatefmt_close(rtf); + } +} + +JSObject* +js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &RelativeTimeFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) + return nullptr; + + if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) + return nullptr; + + if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) + return nullptr; + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!intl::InitializeObject(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { + return nullptr; + } + + return proto; +} + +bool +js::intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + // We're going to use ULocale availableLocales as per ICU recommendation: + // https://ssl.icu-project.org/trac/ticket/12756 + if (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + + +enum class RelativeTimeNumeric +{ + /** + * Only strings with numeric components like `1 day ago`. + */ + Always, + /** + * Natural-language strings like `yesterday` when possible, + * otherwise strings with numeric components as in `7 months ago`. + */ + Auto, +}; + +bool +js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + RootedObject relativeTimeFormat(cx, &args[0].toObject()); + + RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); + if (!internals) + return false; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return false; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return false; + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) + return false; + RootedLinearString style(cx, value.toString()->ensureLinear(cx)); + if (!style) + return false; + + double t = args[1].toNumber(); + + UDateRelativeDateTimeFormatterStyle relDateTimeStyle; + + if (StringEqualsAscii(style, "short")) { + relDateTimeStyle = UDAT_STYLE_SHORT; + } else if (StringEqualsAscii(style, "narrow")) { + relDateTimeStyle = UDAT_STYLE_NARROW; + } else { + MOZ_ASSERT(StringEqualsAscii(style, "long")); + relDateTimeStyle = UDAT_STYLE_LONG; + } + + URelativeDateTimeUnit relDateTimeUnit; + { + JSLinearString* unit = args[2].toString()->ensureLinear(cx); + if (!unit) { + return false; + } + + if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { + relDateTimeUnit = UDAT_REL_UNIT_SECOND; + } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { + relDateTimeUnit = UDAT_REL_UNIT_MINUTE; + } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { + relDateTimeUnit = UDAT_REL_UNIT_HOUR; + } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { + relDateTimeUnit = UDAT_REL_UNIT_DAY; + } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { + relDateTimeUnit = UDAT_REL_UNIT_WEEK; + } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { + relDateTimeUnit = UDAT_REL_UNIT_MONTH; + } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { + relDateTimeUnit = UDAT_REL_UNIT_QUARTER; + } else { + MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); + relDateTimeUnit = UDAT_REL_UNIT_YEAR; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return false; + RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); + if (!numeric) + return false; + + RelativeTimeNumeric relDateTimeNumeric; + + if (StringEqualsAscii(numeric, "auto")) { + relDateTimeNumeric = RelativeTimeNumeric::Auto; + } else { + MOZ_ASSERT(StringEqualsAscii(numeric, "always")); + relDateTimeNumeric = RelativeTimeNumeric::Always; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + UErrorCode status = U_ZERO_ERROR; + URelativeDateTimeFormatter* rtf = + ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, + UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject closeRelativeTimeFormat(rtf); + + JSString* str = + CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, + UErrorCode* status) + { + auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto + ? ureldatefmt_format + : ureldatefmt_formatNumeric; + return fmt(rtf, t, relDateTimeUnit, chars, size, status); + }); + if (!str) + return false; + + args.rval().setString(str); + return true; +} diff --git a/js/src/builtin/intl/RelativeTimeFormat.h b/js/src/builtin/intl/RelativeTimeFormat.h new file mode 100644 index 0000000000..70185c182b --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: set ts=8 sts=4 et sw=4 tw=99: + * 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/. */ + +#ifndef builtin_intl_RelativeTimeFormat_h +#define builtin_intl_RelativeTimeFormat_h + +#include "mozilla/Attributes.h" + +#include + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class RelativeTimeFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t URELATIVE_TIME_FORMAT_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT, + "INTERNALS_SLOT must match self-hosting define for internals object slot"); + private: + static const ClassOps classOps_; + + static void finalize(FreeOp* fop, JSObject* obj); +}; + +extern JSObject* +CreateRelativeTimeFormatPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns an object indicating the supported locales for relative time format + * by having a true-valued property for each such locale with the + * canonicalized language tag as the property name. The object has no + * prototype. + * + * Usage: availableLocales = intl_RelativeTimeFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a relative time as a string formatted according to the effective + * locale and the formatting options of the given RelativeTimeFormat. + * + * t should be a number representing a number to be formatted. + * unit should be "second", "minute", "hour", "day", "week", "month", "quarter", or "year". + * + * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t, unit) + */ +extern MOZ_MUST_USE bool +intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp); + +} // namespace js + +#endif /* builtin_intl_RelativeTimeFormat_h */ \ No newline at end of file diff --git a/js/src/moz.build b/js/src/moz.build index 23fcd7bdf6..333c82cd40 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -120,6 +120,7 @@ UNIFIED_SOURCES += [ 'builtin/intl/DateTimeFormat.cpp', 'builtin/intl/NumberFormat.cpp', 'builtin/intl/PluralRules.cpp', + 'builtin/intl/RelativeTimeFormat.cpp', 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 3248f8d47a..817450a8d8 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -27,6 +27,7 @@ #include "builtin/intl/DateTimeFormat.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/PluralRules.h" +#include "builtin/intl/RelativeTimeFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" From 5ad62567e026ad7dcaa7f9bc65fce961c4ecbea1 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 22:24:52 +0100 Subject: [PATCH 11/24] Issue #2046 - Trim builtin/Intl.*'s #include set down to what is required for Intl itself --- js/src/builtin/Intl.cpp | 23 ++------------- js/src/builtin/Intl.h | 51 ++++++--------------------------- js/src/builtin/intl/ICUHeader.h | 27 +++++++++++++++++ js/src/jsstr.cpp | 2 +- 4 files changed, 39 insertions(+), 64 deletions(-) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index be601f3592..94ef824304 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -3,22 +3,15 @@ * 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/. */ -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ +/* Implementation of the Intl object and its non-constructor properties. */ #include "builtin/Intl.h" -#include "mozilla/Casting.h" -#include "mozilla/FloatingPoint.h" -#include "mozilla/PodOperations.h" +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" #include "mozilla/Range.h" -#include - #include "jsapi.h" -#include "jsatom.h" #include "jscntxt.h" #include "jsobj.h" @@ -30,22 +23,12 @@ #include "builtin/intl/PluralRules.h" #include "builtin/intl/RelativeTimeFormat.h" #include "builtin/intl/ScopedICUObject.h" -#include "ds/Sort.h" -#include "vm/DateTime.h" #include "vm/GlobalObject.h" -#include "vm/Interpreter.h" -#include "vm/Stack.h" -#include "vm/StringBuffer.h" -#include "vm/Unicode.h" #include "jsobjinlines.h" -#include "vm/NativeObject-inl.h" - using namespace js; -using mozilla::IsFinite; - using js::intl::CallICU; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 938ab4de95..9ff3e74d76 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -6,21 +6,13 @@ #ifndef builtin_Intl_h #define builtin_Intl_h -#include "mozilla/HashFunctions.h" -#include "mozilla/MemoryReporting.h" +#include "mozilla/Attributes.h" -#include "jsalloc.h" -#include "NamespaceImports.h" +#include "js/RootingAPI.h" -#include "js/GCAPI.h" -#include "js/GCHashTable.h" - -#include "unicode/utypes.h" - -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ +struct JSContext; +class JSObject; +namespace JS { class Value; } namespace js { @@ -29,7 +21,7 @@ namespace js { * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 */ extern JSObject* -InitIntlClass(JSContext* cx, HandleObject obj); +InitIntlClass(JSContext* cx, JS::Handle obj); /* * The following functions are for use by self-hosted code. @@ -59,7 +51,7 @@ InitIntlClass(JSContext* cx, HandleObject obj); * NOTE: "calendar" and "locale" properties are *not* added to the object. */ extern MOZ_MUST_USE bool -intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp); +intl_GetCalendarInfo(JSContext* cx, unsigned argc, JS::Value* vp); /** * Returns an Array with CLDR-based fields display names. @@ -101,34 +93,7 @@ intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp); * ] */ extern MOZ_MUST_USE bool -intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp); - -/** - * Cast char16_t* strings to UChar* strings used by ICU. - */ -inline const UChar* -Char16ToUChar(const char16_t* chars) -{ - return reinterpret_cast(chars); -} - -inline UChar* -Char16ToUChar(char16_t* chars) -{ - return reinterpret_cast(chars); -} - -inline char16_t* -UCharToChar16(UChar* chars) -{ - return reinterpret_cast(chars); -} - -inline const char16_t* -UCharToChar16(const UChar* chars) -{ - return reinterpret_cast(chars); -} +intl_ComputeDisplayNames(JSContext* cx, unsigned argc, JS::Value* vp); } // namespace js diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h index 0a0e280838..08fe2c1719 100644 --- a/js/src/builtin/intl/ICUHeader.h +++ b/js/src/builtin/intl/ICUHeader.h @@ -20,4 +20,31 @@ #include "unicode/ureldatefmt.h" #include "unicode/ustring.h" +/** + * Cast char16_t* strings to UChar* strings used by ICU. + */ +inline const UChar* +Char16ToUChar(const char16_t* chars) +{ + return reinterpret_cast(chars); +} + +inline UChar* +Char16ToUChar(char16_t* chars) +{ + return reinterpret_cast(chars); +} + +inline char16_t* +UCharToChar16(UChar* chars) +{ + return reinterpret_cast(chars); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + return reinterpret_cast(chars); +} + #endif /* builtin_intl_ICUHeader_h */ diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index b9e10b61b9..6726da9457 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -30,7 +30,7 @@ #include "jstypes.h" #include "jsutil.h" -#include "builtin/Intl.h" +#include "builtin/intl/ICUHeader.h" #include "builtin/RegExp.h" #include "jit/InlinableNatives.h" #include "js/Conversions.h" From 5574b06ff590af82c344f1d5933596f1185d6b91 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 22:41:00 +0100 Subject: [PATCH 12/24] Issue #2046 - Move builtin/Intl.* to builtin/intl/IntlObject.* --- js/src/builtin/{Intl.cpp => intl/IntlObject.cpp} | 2 +- js/src/builtin/{Intl.h => intl/IntlObject.h} | 6 +++--- js/src/jsapi.cpp | 1 - js/src/moz.build | 2 +- js/src/vm/GlobalObject.cpp | 1 - js/src/vm/Runtime.h | 1 - js/src/vm/SelfHosting.cpp | 4 ++-- 7 files changed, 7 insertions(+), 10 deletions(-) rename js/src/builtin/{Intl.cpp => intl/IntlObject.cpp} (99%) rename js/src/builtin/{Intl.h => intl/IntlObject.h} (96%) diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/intl/IntlObject.cpp similarity index 99% rename from js/src/builtin/Intl.cpp rename to js/src/builtin/intl/IntlObject.cpp index 94ef824304..6bb57adf41 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/intl/IntlObject.cpp @@ -5,7 +5,7 @@ /* Implementation of the Intl object and its non-constructor properties. */ -#include "builtin/Intl.h" +#include "builtin/intl/IntlObject.h" #include "mozilla/Assertions.h" #include "mozilla/Likely.h" diff --git a/js/src/builtin/Intl.h b/js/src/builtin/intl/IntlObject.h similarity index 96% rename from js/src/builtin/Intl.h rename to js/src/builtin/intl/IntlObject.h index 9ff3e74d76..1f0b26c545 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/intl/IntlObject.h @@ -3,8 +3,8 @@ * 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/. */ -#ifndef builtin_Intl_h -#define builtin_Intl_h +#ifndef builtin_intl_IntlObject_h +#define builtin_intl_IntlObject_h #include "mozilla/Attributes.h" @@ -97,4 +97,4 @@ intl_ComputeDisplayNames(JSContext* cx, unsigned argc, JS::Value* vp); } // namespace js -#endif /* builtin_Intl_h */ +#endif /* builtin_intl_IntlObject_h */ diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 2b660e2c11..bae1a06475 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -43,7 +43,6 @@ #include "builtin/AtomicsObject.h" #include "builtin/Eval.h" -#include "builtin/Intl.h" #include "builtin/MapObject.h" #include "builtin/Promise.h" #include "builtin/RegExp.h" diff --git a/js/src/moz.build b/js/src/moz.build index 333c82cd40..dcb80ec4fc 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -114,10 +114,10 @@ EXPORTS.js += [ UNIFIED_SOURCES += [ 'builtin/AtomicsObject.cpp', 'builtin/Eval.cpp', - 'builtin/Intl.cpp', 'builtin/intl/Collator.cpp', 'builtin/intl/CommonFunctions.cpp', 'builtin/intl/DateTimeFormat.cpp', + 'builtin/intl/IntlObject.cpp', 'builtin/intl/NumberFormat.cpp', 'builtin/intl/PluralRules.cpp', 'builtin/intl/RelativeTimeFormat.cpp', diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 2c379eee89..85a001d6a3 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -16,7 +16,6 @@ #include "builtin/AtomicsObject.h" #include "builtin/Eval.h" -#include "builtin/Intl.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 0fc6e859e7..8ad1b00623 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -25,7 +25,6 @@ # include "wasm/WasmSignalHandlers.h" #endif #include "builtin/AtomicsObject.h" -#include "builtin/Intl.h" #include "builtin/intl/SharedIntlData.h" #include "builtin/Promise.h" #include "ds/FixedSizeHash.h" diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 817450a8d8..73f2a0a291 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -22,9 +22,9 @@ #include "jswrapper.h" #include "selfhosted.out.h" -#include "builtin/Intl.h" #include "builtin/intl/Collator.h" #include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/IntlObject.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/PluralRules.h" #include "builtin/intl/RelativeTimeFormat.h" @@ -2448,7 +2448,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FOR_EACH_REFERENCE_TYPE_REPR(LOAD_AND_STORE_REFERENCE_FN_DECLS) #undef LOAD_AND_STORE_REFERENCE_FN_DECLS - // See builtin/Intl.h for descriptions of the intl_* functions. + // See builtin/intl/*.h for descriptions of the intl_* functions. JS_FN("intl_availableCalendars", intl_availableCalendars, 1,0), JS_FN("intl_availableCollations", intl_availableCollations, 1,0), JS_FN("intl_canonicalizeTimeZone", intl_canonicalizeTimeZone, 1,0), From f8e3f811979bf3d4446daf0281fa9dc114df46ac Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 23:42:45 +0100 Subject: [PATCH 13/24] Issue #2046 - Move various generated files from builtin/Intl* to builtin/intl/*, and add "Generated" to their names for clarity --- js/src/builtin/intl/DateTimeFormat.cpp | 2 +- .../LangTagMappingsGenerated.js} | 0 js/src/builtin/intl/SharedIntlData.cpp | 2 +- .../TimeZoneDataGenerated.h} | 6 ++-- js/src/builtin/{ => intl}/make_intl_data.py | 34 +++++++++---------- js/src/moz.build | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) rename js/src/builtin/{IntlData.js => intl/LangTagMappingsGenerated.js} (100%) rename js/src/builtin/{IntlTimeZoneData.h => intl/TimeZoneDataGenerated.h} (97%) rename js/src/builtin/{ => intl}/make_intl_data.py (97%) diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index 167beea0c1..bb43f45bda 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -18,7 +18,7 @@ #include "builtin/intl/ICUHeader.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/intl/SharedIntlData.h" -#include "builtin/IntlTimeZoneData.h" +#include "builtin/intl/TimeZoneDataGenerated.h" #include "vm/GlobalObject.h" #include "vm/Runtime.h" diff --git a/js/src/builtin/IntlData.js b/js/src/builtin/intl/LangTagMappingsGenerated.js similarity index 100% rename from js/src/builtin/IntlData.js rename to js/src/builtin/intl/LangTagMappingsGenerated.js diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp index f2da97b361..8ae3c17fe2 100644 --- a/js/src/builtin/intl/SharedIntlData.cpp +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -19,7 +19,7 @@ #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" #include "builtin/intl/ScopedICUObject.h" -#include "builtin/IntlTimeZoneData.h" +#include "builtin/intl/TimeZoneDataGenerated.h" #include "js/Utility.h" using js::HashNumber; diff --git a/js/src/builtin/IntlTimeZoneData.h b/js/src/builtin/intl/TimeZoneDataGenerated.h similarity index 97% rename from js/src/builtin/IntlTimeZoneData.h rename to js/src/builtin/intl/TimeZoneDataGenerated.h index c0e9491567..a0e1895e37 100644 --- a/js/src/builtin/IntlTimeZoneData.h +++ b/js/src/builtin/intl/TimeZoneDataGenerated.h @@ -1,8 +1,8 @@ // Generated by make_intl_data.py. DO NOT EDIT. // tzdata version = 2022e -#ifndef builtin_IntlTimeZoneData_h -#define builtin_IntlTimeZoneData_h +#ifndef builtin_intl_TimeZoneDataGenerated_h +#define builtin_intl_TimeZoneDataGenerated_h namespace js { namespace timezone { @@ -143,4 +143,4 @@ const char* const legacyICUTimeZones[] = { } // namespace timezone } // namespace js -#endif /* builtin_IntlTimeZoneData_h */ +#endif /* builtin_intl_TimeZoneDataGenerated_h */ diff --git a/js/src/builtin/make_intl_data.py b/js/src/builtin/intl/make_intl_data.py similarity index 97% rename from js/src/builtin/make_intl_data.py rename to js/src/builtin/intl/make_intl_data.py index b81d5951f2..a81001e0f3 100644 --- a/js/src/builtin/make_intl_data.py +++ b/js/src/builtin/intl/make_intl_data.py @@ -12,8 +12,8 @@ Target "langtags": This script extracts information about mappings between deprecated and current BCP 47 language tags from the IANA Language Subtag Registry and - converts it to JavaScript object definitions in IntlData.js. The definitions - are used in Intl.js. + converts it to JavaScript object definitions in + LangTagMappingsGenerated.js. The definitions are used in Intl.js. The IANA Language Subtag Registry is imported from https://www.iana.org/assignments/language-subtag-registry @@ -190,7 +190,7 @@ def writeLanguageTagData(intlData, fileDate, url, langTagMappings, langSubtagMap "Mappings from extlang subtags to preferred values", fileDate, url) def updateLangTags(args): - """ Update the IntlData.js file. """ + """ Update the LangTagMappingsGenerated.js file. """ url = args.url out = args.out filename = args.file @@ -685,8 +685,8 @@ def processTimeZones(tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignor println(tzdataVersionComment.format(version)) println(u"") - println(u"#ifndef builtin_IntlTimeZoneData_h") - println(u"#define builtin_IntlTimeZoneData_h") + println(u"#ifndef builtin_intl_TimeZoneDataGenerated_h") + println(u"#define builtin_intl_TimeZoneDataGenerated_h") println(u"") println(u"namespace js {") @@ -727,7 +727,7 @@ def processTimeZones(tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignor println(u"} // namespace timezone") println(u"} // namespace js") println(u"") - println(u"#endif /* builtin_IntlTimeZoneData_h */") + println(u"#endif /* builtin_intl_TimeZoneDataGenerated_h */") def updateBackzoneLinks(tzdataDir, links): (backzoneZones, backzoneLinks) = readIANAFiles(tzdataDir, ["backzone"]) @@ -878,16 +878,9 @@ def generateTzDataTests(tzdataDir, version, ignoreBackzone, testDir): generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, testDir) generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, testDir) -def updateTzdata(args): +def updateTzdata(topsrcdir, args): """ Update the time zone cpp file. """ - # This script must reside in js/src/builtin to work correctly. - (thisDir, thisFile) = os.path.split(os.path.abspath(sys.argv[0])) - thisDir = os.path.normpath(thisDir) - if "/".join(thisDir.split(os.sep)[-3:]) != "js/src/builtin": - raise RuntimeError("%s must reside in js/src/builtin" % sys.argv[0]) - topsrcdir = "/".join(thisDir.split(os.sep)[:-3]) - icuDir = os.path.join(topsrcdir, "intl/icu/source") if not os.path.isdir(icuDir): raise RuntimeError("not a directory: %s" % icuDir) @@ -947,6 +940,13 @@ def updateTzdata(args): if __name__ == "__main__": import argparse + # This script must reside in js/src/builtin/intl to work correctly. + (thisDir, thisFile) = os.path.split(os.path.abspath(sys.argv[0])) + dirPaths = os.path.normpath(thisDir).split(os.sep) + if "/".join(dirPaths[-4:]) != "js/src/builtin/intl": + raise RuntimeError("%s must reside in js/src/builtin/intl" % sys.argv[0]) + topsrcdir = "/".join(dirPaths[:-4]) + def EnsureHttps(v): if not v.startswith("https:"): raise argparse.ArgumentTypeError("URL protocol must be https: " % v) @@ -963,7 +963,7 @@ if __name__ == "__main__": type=EnsureHttps, help="Download url for language-subtag-registry.txt (default: %(default)s)") parser_tags.add_argument("--out", - default="IntlData.js", + default="LangTagMappingsGenerated.js", help="Output file (default: %(default)s)") parser_tags.add_argument("file", nargs="?", @@ -984,9 +984,9 @@ if __name__ == "__main__": "accurate time zone canonicalization reflecting the actual time " "zones as used by ICU.") parser_tz.add_argument("--out", - default="IntlTimeZoneData.h", + default="TimeZoneDataGenerated.h", help="Output file (default: %(default)s)") - parser_tz.set_defaults(func=updateTzdata) + parser_tz.set_defaults(func=partial(updateTzdata, topsrcdir)) args = parser.parse_args() args.func(args) diff --git a/js/src/moz.build b/js/src/moz.build index dcb80ec4fc..f7af0fec36 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -669,7 +669,7 @@ selfhosted.inputs = [ 'builtin/Function.js', 'builtin/Generator.js', 'builtin/Intl.js', - 'builtin/IntlData.js', + 'builtin/intl/LangTagMappingsGenerated.js', 'builtin/Iterator.js', 'builtin/Map.js', 'builtin/Module.js', From 7f2b67f7ddac69dd9e55d2e4fa6ddcfdf5c90368 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 23:53:01 +0100 Subject: [PATCH 14/24] Issue #2046 - Move the self-hosting of non-constructor properties of Intl to a new builtin/intl/IntlObject.js file --- js/src/builtin/Intl.js | 157 ----------------------------- js/src/builtin/intl/IntlObject.js | 162 ++++++++++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 163 insertions(+), 157 deletions(-) create mode 100644 js/src/builtin/intl/IntlObject.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index e7d1b58ed6..0cba09e4f4 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -3503,161 +3503,4 @@ function Intl_RelativeTimeFormat_resolvedOptions() { return result; } -function Intl_getCanonicalLocales(locales) { - let codes = CanonicalizeLocaleList(locales); - let result = []; - let len = codes.length; - let k = 0; - - while (k < len) { - _DefineDataProperty(result, k, codes[k]); - k++; - } - return result; -} - -function Intl_getCalendarInfo(locales) { - const requestedLocales = CanonicalizeLocaleList(locales); - - const DateTimeFormat = dateTimeFormatInternalProperties; - const localeData = DateTimeFormat.localeData; - - const localeOpt = new Record(); - localeOpt.localeMatcher = "best fit"; - - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - requestedLocales, - localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - const result = intl_GetCalendarInfo(r.locale); - result.calendar = r.ca; - result.locale = r.locale; - - return result; -} - -/** - * This function is a custom method designed after Intl API, but currently - * not part of the spec or spec proposal. - * We want to use it internally to retrieve translated values from CLDR in - * order to ensure they're aligned with what Intl API returns. - * - * This API may one day be a foundation for an ECMA402 API spec proposal. - * - * The function takes two arguments - locales which is a list of locale strings - * and options which is an object with two optional properties: - * - * keys: - * an Array of string values that are paths to individual terms - * - * style: - * a String with a value "long", "short" or "narrow" - * - * It returns an object with properties: - * - * locale: - * a negotiated locale string - * - * style: - * negotiated style - * - * values: - * A key-value pair list of requested keys and corresponding - * translated values - * - */ -function Intl_getDisplayNames(locales, options) { - // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). - const requestedLocales = CanonicalizeLocaleList(locales); - - // 2. If options is undefined, then - if (options === undefined) - // a. Let options be ObjectCreate(%ObjectPrototype%). - options = {}; - // 3. Else, - else - // a. Let options be ? ToObject(options). - options = ToObject(options); - - const DateTimeFormat = dateTimeFormatInternalProperties; - - // 4. Let localeData be %DateTimeFormat%.[[localeData]]. - const localeData = DateTimeFormat.localeData; - - // 5. Let opt be a new Record. - const localeOpt = new Record(); - // 6. Set localeOpt.[[localeMatcher]] to "best fit". - localeOpt.localeMatcher = "best fit"; - - // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, - // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - requestedLocales, - localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). - const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); - // 9. Let keys be ? Get(options, "keys"). - let keys = options.keys; - - // 10. If keys is undefined, - if (keys === undefined) { - // a. Let keys be ArrayCreate(0). - keys = []; - } else if (!IsObject(keys)) { - // 11. Else, - // a. If Type(keys) is not Object, throw a TypeError exception. - ThrowTypeError(JSMSG_INVALID_KEYS_TYPE); - } - - // 12. Let processedKeys be ArrayCreate(0). - // (This really should be a List, but we use an Array here in order that - // |intl_ComputeDisplayNames| may infallibly access the list's length via - // |ArrayObject::length|.) - let processedKeys = []; - // 13. Let len be ? ToLength(? Get(keys, "length")). - let len = ToLength(keys.length); - // 14. Let i be 0. - // 15. Repeat, while i < len - for (let i = 0; i < len; i++) { - // a. Let processedKey be ? ToString(? Get(keys, i)). - // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey). - callFunction(std_Array_push, processedKeys, ToString(keys[i])); - } - - // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys). - const names = intl_ComputeDisplayNames(r.locale, style, processedKeys); - - // 17. Let values be ObjectCreate(%ObjectPrototype%). - const values = {}; - - // 18. Set i to 0. - // 19. Repeat, while i < len - for (let i = 0; i < len; i++) { - // a. Let key be ? Get(processedKeys, i). - const key = processedKeys[i]; - // b. Let name be ? Get(names, i). - const name = names[i]; - // c. Assert: Type(name) is string. - assert(typeof name === "string", "unexpected non-string value"); - // d. Assert: the length of name is greater than zero. - assert(name.length > 0, "empty string value"); - // e. Perform ? DefinePropertyOrThrow(values, key, name). - _DefineDataProperty(values, key, name); - } - - // 20. Let options be ObjectCreate(%ObjectPrototype%). - // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]). - // 22. Perform ! DefinePropertyOrThrow(result, "style", style). - // 23. Perform ! DefinePropertyOrThrow(result, "values", values). - const result = { locale: r.locale, style, values }; - - // 24. Return result. - return result; - -} diff --git a/js/src/builtin/intl/IntlObject.js b/js/src/builtin/intl/IntlObject.js new file mode 100644 index 0000000000..826ad27ff0 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.js @@ -0,0 +1,162 @@ +/* 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/. */ + +function Intl_getCanonicalLocales(locales) { + let codes = CanonicalizeLocaleList(locales); + let result = []; + + let len = codes.length; + let k = 0; + + while (k < len) { + _DefineDataProperty(result, k, codes[k]); + k++; + } + return result; +} + +function Intl_getCalendarInfo(locales) { + const requestedLocales = CanonicalizeLocaleList(locales); + + const DateTimeFormat = dateTimeFormatInternalProperties; + const localeData = DateTimeFormat.localeData; + + const localeOpt = new Record(); + localeOpt.localeMatcher = "best fit"; + + const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + const result = intl_GetCalendarInfo(r.locale); + result.calendar = r.ca; + result.locale = r.locale; + + return result; +} + +/** + * This function is a custom method designed after Intl API, but currently + * not part of the spec or spec proposal. + * We want to use it internally to retrieve translated values from CLDR in + * order to ensure they're aligned with what Intl API returns. + * + * This API may one day be a foundation for an ECMA402 API spec proposal. + * + * The function takes two arguments - locales which is a list of locale strings + * and options which is an object with two optional properties: + * + * keys: + * an Array of string values that are paths to individual terms + * + * style: + * a String with a value "long", "short" or "narrow" + * + * It returns an object with properties: + * + * locale: + * a negotiated locale string + * + * style: + * negotiated style + * + * values: + * A key-value pair list of requested keys and corresponding + * translated values + * + */ +function Intl_getDisplayNames(locales, options) { + // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). + const requestedLocales = CanonicalizeLocaleList(locales); + + // 2. If options is undefined, then + if (options === undefined) + // a. Let options be ObjectCreate(%ObjectPrototype%). + options = {}; + // 3. Else, + else + // a. Let options be ? ToObject(options). + options = ToObject(options); + + const DateTimeFormat = dateTimeFormatInternalProperties; + + // 4. Let localeData be %DateTimeFormat%.[[localeData]]. + const localeData = DateTimeFormat.localeData; + + // 5. Let opt be a new Record. + const localeOpt = new Record(); + // 6. Set localeOpt.[[localeMatcher]] to "best fit". + localeOpt.localeMatcher = "best fit"; + + // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, + // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). + const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). + const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); + // 9. Let keys be ? Get(options, "keys"). + let keys = options.keys; + + // 10. If keys is undefined, + if (keys === undefined) { + // a. Let keys be ArrayCreate(0). + keys = []; + } else if (!IsObject(keys)) { + // 11. Else, + // a. If Type(keys) is not Object, throw a TypeError exception. + ThrowTypeError(JSMSG_INVALID_KEYS_TYPE); + } + + // 12. Let processedKeys be ArrayCreate(0). + // (This really should be a List, but we use an Array here in order that + // |intl_ComputeDisplayNames| may infallibly access the list's length via + // |ArrayObject::length|.) + let processedKeys = []; + // 13. Let len be ? ToLength(? Get(keys, "length")). + let len = ToLength(keys.length); + // 14. Let i be 0. + // 15. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let processedKey be ? ToString(? Get(keys, i)). + // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey). + callFunction(std_Array_push, processedKeys, ToString(keys[i])); + } + + // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys). + const names = intl_ComputeDisplayNames(r.locale, style, processedKeys); + + // 17. Let values be ObjectCreate(%ObjectPrototype%). + const values = {}; + + // 18. Set i to 0. + // 19. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let key be ? Get(processedKeys, i). + const key = processedKeys[i]; + // b. Let name be ? Get(names, i). + const name = names[i]; + // c. Assert: Type(name) is string. + assert(typeof name === "string", "unexpected non-string value"); + // d. Assert: the length of name is greater than zero. + assert(name.length > 0, "empty string value"); + // e. Perform ? DefinePropertyOrThrow(values, key, name). + _DefineDataProperty(values, key, name); + } + + // 20. Let options be ObjectCreate(%ObjectPrototype%). + // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]). + // 22. Perform ! DefinePropertyOrThrow(result, "style", style). + // 23. Perform ! DefinePropertyOrThrow(result, "values", values). + const result = { locale: r.locale, style, values }; + + // 24. Return result. + return result; + +} diff --git a/js/src/moz.build b/js/src/moz.build index f7af0fec36..03d766ee08 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -669,6 +669,7 @@ selfhosted.inputs = [ 'builtin/Function.js', 'builtin/Generator.js', 'builtin/Intl.js', + 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', 'builtin/Iterator.js', 'builtin/Map.js', From ae0eddc1654d14c6b16be51c2d3c2061724e3f35 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 23:55:05 +0100 Subject: [PATCH 15/24] Issue #2046 - Move Intl.RelativeTimeFormat self-hosted code to a new builtin/intl/RelativeTimeFormat.js file --- js/src/builtin/Intl.js | 240 --------------------- js/src/builtin/intl/RelativeTimeFormat.js | 242 ++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 243 insertions(+), 240 deletions(-) create mode 100644 js/src/builtin/intl/RelativeTimeFormat.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index 0cba09e4f4..c5addf706f 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -3264,243 +3264,3 @@ function Intl_PluralRules_resolvedOptions() { return result; } - -/********** Intl.RelativeTimeFormat **********/ - -/** - * RelativeTimeFormat internal properties. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3. - */ -var relativeTimeFormatInternalProperties = { - localeData: relativeTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() // eslint-disable-line object-shorthand - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_RelativeTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: [], -}; - -function relativeTimeFormatLocaleData() { - // RelativeTimeFormat doesn't support any extension keys. - return {}; -} - -/** - * Compute an internal properties object from |lazyRelativeTimeFormatData|. - */ -function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { - assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - var RelativeTimeFormat = relativeTimeFormatInternalProperties; - - // Steps 7-8. - const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), - lazyRelativeTimeFormatData.requestedLocales, - lazyRelativeTimeFormatData.opt, - RelativeTimeFormat.relevantExtensionKeys, - RelativeTimeFormat.localeData); - - // Step 9-10. - internalProps.locale = r.locale; - - // Step 11. - assert(r.locale === r.dataLocale, - "resolved locale matches the resolved data-locale when no extension-keys are present"); - - // Step 13. - internalProps.style = lazyRelativeTimeFormatData.style; - - // Step 15. - internalProps.numeric = lazyRelativeTimeFormatData.numeric; - - // Steps 16-20 (Not relevant in our implementation). - - return internalProps; -} - -/** - * Returns an object containing the RelativeTimeFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't RelativeTimeFormat-initialized. - */ -function getRelativeTimeFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "RelativeTimeFormat", methodName); - assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals"); - - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Initializes an object as a RelativeTimeFormat. - * - * This method is complicated a moderate bit by its implementing initialization - * as a *lazy* concept. Everything that must happen now, does -- but we defer - * all the work we can until the object is actually used as a RelativeTimeFormat. - * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1. - */ -function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { - assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat"); - - if (isInitializedIntlObject(relativeTimeFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - let internals = initializeIntlObject(relativeTimeFormat); - - // Lazy RelativeTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // style: "long" / "short" / "narrow", - // numeric: "always" / "auto", - // - // opt: // opt object computed in InitializeRelativeTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // } - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every RelativeTimeFormat lazy data object has *all* these properties, never a - // subset of them. - const lazyRelativeTimeFormatData = std_Object_create(null); - - // Step 1. - let requestedLocales = CanonicalizeLocaleList(locales); - lazyRelativeTimeFormatData.requestedLocales = requestedLocales; - - // Steps 2-3. - if (options === undefined) - options = std_Object_create(null); - else - options = ToObject(options); - - // Step 4. - let opt = new Record(); - - // Steps 5-6. - let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - lazyRelativeTimeFormatData.opt = opt; - - // Steps 12-13. - const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); - lazyRelativeTimeFormatData.style = style; - - // Steps 14-15. - const numeric = GetOption(options, "numeric", "string", ["always", "auto"], "always"); - lazyRelativeTimeFormatData.numeric = numeric; - - setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData) -} - -/** - * Returns the subset of the given locale list for which this locale list has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.2. - */ -function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - // Step 1. - var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, - relativeTimeFormatInternalProperties); - // Step 2. - let requestedLocales = CanonicalizeLocaleList(locales); - - // Step 3. - return SupportedLocales(availableLocales, requestedLocales, options); -} - -/** - * Returns a String value representing the written form of a relative date - * formatted according to the effective locale and the formatting options - * of this RelativeTimeFormat object. - * - * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3. - */ -function Intl_RelativeTimeFormat_format(value, unit) { - // Step 1. - let relativeTimeFormat = this; - - // Step 2. - let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format"); - - // Step 3. - let t = ToNumber(value); - - // Step 4. - let u = ToString(unit); - - // PartitionRelativeTimePattern, step 4. - if (!Number_isFinite(t)) { - ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat"); - } - - // PartitionRelativeTimePattern, step 5. - switch (u) { - case "second": - case "seconds": - case "minute": - case "minutes": - case "hour": - case "hours": - case "day": - case "days": - case "week": - case "weeks": - case "month": - case "months": - case "quarter": - case "quarters": - case "year": - case "years": - break; - default: - ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, "unit", u); - } - - // Step 5. - return intl_FormatRelativeTime(relativeTimeFormat, t, u, internals.numeric); -} - -/** - * Returns the resolved options for a PluralRules object. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4. - */ -function Intl_RelativeTimeFormat_resolvedOptions() { - var internals = getRelativeTimeFormatInternals(this, "resolvedOptions"); - - // Steps 4-5. - var result = { - locale: internals.locale, - style: internals.style, - numeric: internals.numeric, - }; - - // Step 6. - return result; -} - - diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js new file mode 100644 index 0000000000..a37f067825 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.js @@ -0,0 +1,242 @@ +/* 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.RelativeTimeFormat **********/ + +/** + * RelativeTimeFormat internal properties. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3. + */ +var relativeTimeFormatInternalProperties = { + localeData: relativeTimeFormatLocaleData, + _availableLocales: null, + availableLocales: function() // eslint-disable-line object-shorthand + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_RelativeTimeFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: [], +}; + +function relativeTimeFormatLocaleData() { + // RelativeTimeFormat doesn't support any extension keys. + return {}; +} + +/** + * Compute an internal properties object from |lazyRelativeTimeFormatData|. + */ +function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { + assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var RelativeTimeFormat = relativeTimeFormatInternalProperties; + + // Steps 7-8. + const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), + lazyRelativeTimeFormatData.requestedLocales, + lazyRelativeTimeFormatData.opt, + RelativeTimeFormat.relevantExtensionKeys, + RelativeTimeFormat.localeData); + + // Step 9-10. + internalProps.locale = r.locale; + + // Step 11. + assert(r.locale === r.dataLocale, + "resolved locale matches the resolved data-locale when no extension-keys are present"); + + // Step 13. + internalProps.style = lazyRelativeTimeFormatData.style; + + // Step 15. + internalProps.numeric = lazyRelativeTimeFormatData.numeric; + + // Steps 16-20 (Not relevant in our implementation). + + return internalProps; +} + +/** + * Returns an object containing the RelativeTimeFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't RelativeTimeFormat-initialized. + */ +function getRelativeTimeFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "RelativeTimeFormat", methodName); + assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals"); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a RelativeTimeFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a RelativeTimeFormat. + * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1. + */ +function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { + assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat"); + + if (isInitializedIntlObject(relativeTimeFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + let internals = initializeIntlObject(relativeTimeFormat); + + // Lazy RelativeTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "long" / "short" / "narrow", + // numeric: "always" / "auto", + // + // opt: // opt object computed in InitializeRelativeTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every RelativeTimeFormat lazy data object has *all* these properties, never a + // subset of them. + const lazyRelativeTimeFormatData = std_Object_create(null); + + // Step 1. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyRelativeTimeFormatData.requestedLocales = requestedLocales; + + // Steps 2-3. + if (options === undefined) + options = std_Object_create(null); + else + options = ToObject(options); + + // Step 4. + let opt = new Record(); + + // Steps 5-6. + let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + lazyRelativeTimeFormatData.opt = opt; + + // Steps 12-13. + const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); + lazyRelativeTimeFormatData.style = style; + + // Steps 14-15. + const numeric = GetOption(options, "numeric", "string", ["always", "auto"], "always"); + lazyRelativeTimeFormatData.numeric = numeric; + + setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData) +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.2. + */ +function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 1. + var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, + relativeTimeFormatInternalProperties); + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the written form of a relative date + * formatted according to the effective locale and the formatting options + * of this RelativeTimeFormat object. + * + * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3. + */ +function Intl_RelativeTimeFormat_format(value, unit) { + // Step 1. + let relativeTimeFormat = this; + + // Step 2. + let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format"); + + // Step 3. + let t = ToNumber(value); + + // Step 4. + let u = ToString(unit); + + // PartitionRelativeTimePattern, step 4. + if (!Number_isFinite(t)) { + ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat"); + } + + // PartitionRelativeTimePattern, step 5. + switch (u) { + case "second": + case "seconds": + case "minute": + case "minutes": + case "hour": + case "hours": + case "day": + case "days": + case "week": + case "weeks": + case "month": + case "months": + case "quarter": + case "quarters": + case "year": + case "years": + break; + default: + ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, "unit", u); + } + + // Step 5. + return intl_FormatRelativeTime(relativeTimeFormat, t, u, internals.numeric); +} + +/** + * Returns the resolved options for a PluralRules object. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4. + */ +function Intl_RelativeTimeFormat_resolvedOptions() { + var internals = getRelativeTimeFormatInternals(this, "resolvedOptions"); + + // Steps 4-5. + var result = { + locale: internals.locale, + style: internals.style, + numeric: internals.numeric, + }; + + // Step 6. + return result; +} + diff --git a/js/src/moz.build b/js/src/moz.build index 03d766ee08..409c116969 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -671,6 +671,7 @@ selfhosted.inputs = [ 'builtin/Intl.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', + 'builtin/intl/RelativeTimeFormat.js', 'builtin/Iterator.js', 'builtin/Map.js', 'builtin/Module.js', From e55cdd19a0bc9a333e2c42a40124d9d2a345b599 Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 23:56:35 +0100 Subject: [PATCH 16/24] Issue #2046 - Move Intl.PluralRules self-hosted code to a new builtin/intl/PluralRules.js file --- js/src/builtin/Intl.js | 227 ---------------------------- js/src/builtin/intl/PluralRules.js | 231 +++++++++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 232 insertions(+), 227 deletions(-) create mode 100644 js/src/builtin/intl/PluralRules.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index c5addf706f..950d02d4bb 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -3037,230 +3037,3 @@ function resolveICUPattern(pattern, result) { } } -/********** Intl.PluralRules **********/ - -/** - * PluralRules internal properties. - * - * Spec: ECMAScript 402 API, PluralRules, 1.3.3. - */ -var pluralRulesInternalProperties = { - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_PluralRules_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - } -}; - -/** - * Compute an internal properties object from |lazyPluralRulesData|. - */ -function resolvePluralRulesInternals(lazyPluralRulesData) { - assert(IsObject(lazyPluralRulesData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - var requestedLocales = lazyPluralRulesData.requestedLocales; - - var PluralRules = pluralRulesInternalProperties; - - // Step 13. - const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules), - lazyPluralRulesData.requestedLocales, - lazyPluralRulesData.opt, - noRelevantExtensionKeys, undefined); - - // Step 14. - internalProps.locale = r.locale; - internalProps.type = lazyPluralRulesData.type; - - internalProps.pluralCategories = intl_GetPluralCategories( - internalProps.locale, - internalProps.type); - - internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits; - internalProps.minimumFractionDigits = lazyPluralRulesData.minimumFractionDigits; - internalProps.maximumFractionDigits = lazyPluralRulesData.maximumFractionDigits; - - if ("minimumSignificantDigits" in lazyPluralRulesData) { - assert("maximumSignificantDigits" in lazyPluralRulesData, "min/max sig digits mismatch"); - internalProps.minimumSignificantDigits = lazyPluralRulesData.minimumSignificantDigits; - internalProps.maximumSignificantDigits = lazyPluralRulesData.maximumSignificantDigits; - } - - return internalProps; -} - -/** - * Returns an object containing the PluralRules internal properties of |obj|, - * or throws a TypeError if |obj| isn't PluralRules-initialized. - */ -function getPluralRulesInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "PluralRules", methodName); - assert(internals.type === "PluralRules", "bad type escaped getIntlObjectInternals"); - - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - internalProps = resolvePluralRulesInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Initializes an object as a PluralRules. - * - * This method is complicated a moderate bit by its implementing initialization - * as a *lazy* concept. Everything that must happen now, does -- but we defer - * all the work we can until the object is actually used as a PluralRules. - * This later work occurs in |resolvePluralRulesInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript 402 API, PluralRules, 1.1.1. - */ -function InitializePluralRules(pluralRules, locales, options) { - assert(IsObject(pluralRules), "InitializePluralRules"); - - // Step 1. - if (isInitializedIntlObject(pluralRules)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - let internals = initializeIntlObject(pluralRules); - - // Lazy PluralRules data has the following structure: - // - // { - // requestedLocales: List of locales, - // type: "cardinal" / "ordinal", - // - // opt: // opt object computer in InitializePluralRules - // { - // localeMatcher: "lookup" / "best fit", - // } - // - // minimumIntegerDigits: integer ∈ [1, 21], - // minimumFractionDigits: integer ∈ [0, 20], - // maximumFractionDigits: integer ∈ [0, 20], - // - // // optional - // minimumSignificantDigits: integer ∈ [1, 21], - // maximumSignificantDigits: integer ∈ [1, 21], - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every PluralRules lazy data object has *all* these properties, never a - // subset of them. - const lazyPluralRulesData = std_Object_create(null); - - // Step 3. - let requestedLocales = CanonicalizeLocaleList(locales); - lazyPluralRulesData.requestedLocales = requestedLocales; - - // Steps 4-5. - if (options === undefined) - options = {}; - else - options = ToObject(options); - - // Step 6. - const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal"); - lazyPluralRulesData.type = type; - - // Step 8. - let opt = new Record(); - lazyPluralRulesData.opt = opt; - - // Steps 9-10. - let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - - // Step 11. - SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0); - - // Step 12. - if (lazyPluralRulesData.maximumFractionDigits === undefined) { - lazyPluralRulesData.maximumFractionDigits = - std_Math_max(lazyPluralRulesData.minimumFractionDigits, 3); - } - - setLazyData(internals, "PluralRules", lazyPluralRulesData) -} - -/** - * Returns the subset of the given locale list for which this locale list has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript 402 API, PluralRules, 1.3.2. - */ -function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - // Step 1. - var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales, - pluralRulesInternalProperties); - // Step 2. - let requestedLocales = CanonicalizeLocaleList(locales); - - // Step 3. - return SupportedLocales(availableLocales, requestedLocales, options); -} - -/** - * Returns a String value representing the plural category matching - * the number passed as value according to the - * effective locale and the formatting options of this PluralRules. - * - * Spec: ECMAScript 402 API, PluralRules, 1.4.3. - */ -function Intl_PluralRules_select(value) { - // Step 1. - let pluralRules = this; - // Step 2. - let internals = getPluralRulesInternals(pluralRules, "select"); - - // Steps 3-4. - let n = ToNumber(value); - - // Step 5. - return intl_SelectPluralRule(pluralRules, n); -} - -/** - * Returns the resolved options for a PluralRules object. - * - * Spec: ECMAScript 402 API, PluralRules, 1.4.4. - */ -function Intl_PluralRules_resolvedOptions() { - var internals = getPluralRulesInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - type: internals.type, - pluralCategories: callFunction(std_Array_slice, internals.pluralCategories, 0), - minimumIntegerDigits: internals.minimumIntegerDigits, - minimumFractionDigits: internals.minimumFractionDigits, - maximumFractionDigits: internals.maximumFractionDigits, - }; - - var optionalProperties = [ - "minimumSignificantDigits", - "maximumSignificantDigits" - ]; - - for (var i = 0; i < optionalProperties.length; i++) { - var p = optionalProperties[i]; - if (callFunction(std_Object_hasOwnProperty, internals, p)) - _DefineDataProperty(result, p, internals[p]); - } - return result; -} - diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js new file mode 100644 index 0000000000..1fac1c9352 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.js @@ -0,0 +1,231 @@ +/* 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.PluralRules **********/ + +/** + * PluralRules internal properties. + * + * Spec: ECMAScript 402 API, PluralRules, 1.3.3. + */ +var pluralRulesInternalProperties = { + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_PluralRules_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + } +}; + +/** + * Compute an internal properties object from |lazyPluralRulesData|. + */ +function resolvePluralRulesInternals(lazyPluralRulesData) { + assert(IsObject(lazyPluralRulesData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var requestedLocales = lazyPluralRulesData.requestedLocales; + + var PluralRules = pluralRulesInternalProperties; + + // Step 13. + const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules), + lazyPluralRulesData.requestedLocales, + lazyPluralRulesData.opt, + noRelevantExtensionKeys, undefined); + + // Step 14. + internalProps.locale = r.locale; + internalProps.type = lazyPluralRulesData.type; + + internalProps.pluralCategories = intl_GetPluralCategories( + internalProps.locale, + internalProps.type); + + internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits; + internalProps.minimumFractionDigits = lazyPluralRulesData.minimumFractionDigits; + internalProps.maximumFractionDigits = lazyPluralRulesData.maximumFractionDigits; + + if ("minimumSignificantDigits" in lazyPluralRulesData) { + assert("maximumSignificantDigits" in lazyPluralRulesData, "min/max sig digits mismatch"); + internalProps.minimumSignificantDigits = lazyPluralRulesData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = lazyPluralRulesData.maximumSignificantDigits; + } + + return internalProps; +} + +/** + * Returns an object containing the PluralRules internal properties of |obj|, + * or throws a TypeError if |obj| isn't PluralRules-initialized. + */ +function getPluralRulesInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "PluralRules", methodName); + assert(internals.type === "PluralRules", "bad type escaped getIntlObjectInternals"); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + internalProps = resolvePluralRulesInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a PluralRules. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a PluralRules. + * This later work occurs in |resolvePluralRulesInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, PluralRules, 1.1.1. + */ +function InitializePluralRules(pluralRules, locales, options) { + assert(IsObject(pluralRules), "InitializePluralRules"); + + // Step 1. + if (isInitializedIntlObject(pluralRules)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + let internals = initializeIntlObject(pluralRules); + + // Lazy PluralRules data has the following structure: + // + // { + // requestedLocales: List of locales, + // type: "cardinal" / "ordinal", + // + // opt: // opt object computer in InitializePluralRules + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // minimumIntegerDigits: integer ∈ [1, 21], + // minimumFractionDigits: integer ∈ [0, 20], + // maximumFractionDigits: integer ∈ [0, 20], + // + // // optional + // minimumSignificantDigits: integer ∈ [1, 21], + // maximumSignificantDigits: integer ∈ [1, 21], + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every PluralRules lazy data object has *all* these properties, never a + // subset of them. + const lazyPluralRulesData = std_Object_create(null); + + // Step 3. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyPluralRulesData.requestedLocales = requestedLocales; + + // Steps 4-5. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Step 6. + const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal"); + lazyPluralRulesData.type = type; + + // Step 8. + let opt = new Record(); + lazyPluralRulesData.opt = opt; + + // Steps 9-10. + let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + + // Step 11. + SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0); + + // Step 12. + if (lazyPluralRulesData.maximumFractionDigits === undefined) { + lazyPluralRulesData.maximumFractionDigits = + std_Math_max(lazyPluralRulesData.minimumFractionDigits, 3); + } + + setLazyData(internals, "PluralRules", lazyPluralRulesData) +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript 402 API, PluralRules, 1.3.2. + */ +function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 1. + var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales, + pluralRulesInternalProperties); + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the plural category matching + * the number passed as value according to the + * effective locale and the formatting options of this PluralRules. + * + * Spec: ECMAScript 402 API, PluralRules, 1.4.3. + */ +function Intl_PluralRules_select(value) { + // Step 1. + let pluralRules = this; + // Step 2. + let internals = getPluralRulesInternals(pluralRules, "select"); + + // Steps 3-4. + let n = ToNumber(value); + + // Step 5. + return intl_SelectPluralRule(pluralRules, n); +} + +/** + * Returns the resolved options for a PluralRules object. + * + * Spec: ECMAScript 402 API, PluralRules, 1.4.4. + */ +function Intl_PluralRules_resolvedOptions() { + var internals = getPluralRulesInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + type: internals.type, + pluralCategories: callFunction(std_Array_slice, internals.pluralCategories, 0), + minimumIntegerDigits: internals.minimumIntegerDigits, + minimumFractionDigits: internals.minimumFractionDigits, + maximumFractionDigits: internals.maximumFractionDigits, + }; + + var optionalProperties = [ + "minimumSignificantDigits", + "maximumSignificantDigits" + ]; + + for (var i = 0; i < optionalProperties.length; i++) { + var p = optionalProperties[i]; + if (callFunction(std_Object_hasOwnProperty, internals, p)) + _DefineDataProperty(result, p, internals[p]); + } + return result; +} + diff --git a/js/src/moz.build b/js/src/moz.build index 409c116969..767a92042d 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -671,6 +671,7 @@ selfhosted.inputs = [ 'builtin/Intl.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', + 'builtin/intl/PluralRules.js', 'builtin/intl/RelativeTimeFormat.js', 'builtin/Iterator.js', 'builtin/Map.js', From 38284ba8b4aadd851e445cdfb7898676c494a43c Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 16 Feb 2023 23:58:32 +0100 Subject: [PATCH 17/24] Issue #2046 - Move Intl.DateTimeFormat self-hosted code to a new builtin/intl/DateTimeFormat.js file --- js/src/builtin/Intl.js | 809 ------------------------- js/src/builtin/intl/DateTimeFormat.js | 813 ++++++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 814 insertions(+), 809 deletions(-) create mode 100644 js/src/builtin/intl/DateTimeFormat.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index 950d02d4bb..f1ec0163a9 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -2228,812 +2228,3 @@ function Intl_NumberFormat_resolvedOptions() { } -/********** Intl.DateTimeFormat **********/ - - -/** - * Compute an internal properties object from |lazyDateTimeFormatData|. - */ -function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { - assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?"); - - // Lazy DateTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // - // localeOpt: // *first* opt computed in InitializeDateTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // - // hour12: true / false, // optional - // } - // - // timeZone: IANA time zone name, - // - // formatOpt: // *second* opt computed in InitializeDateTimeFormat - // { - // // all the properties/values listed in Table 3 - // // (weekday, era, year, month, day, &c.) - // } - // - // formatMatcher: "basic" / "best fit", - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every DateTimeFormat lazy data object has *all* these properties, - // never a subset of them. - - var internalProps = std_Object_create(null); - - // Compute effective locale. - // Step 8. - var DateTimeFormat = dateTimeFormatInternalProperties; - - // Step 9. - var localeData = DateTimeFormat.localeData; - - // Step 10. - var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - lazyDateTimeFormatData.requestedLocales, - lazyDateTimeFormatData.localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - // Steps 11-13. - internalProps.locale = r.locale; - internalProps.calendar = r.ca; - internalProps.numberingSystem = r.nu; - - // Compute formatting options. - // Step 14. - var dataLocale = r.dataLocale; - - // Steps 15-17. - var tz = lazyDateTimeFormatData.timeZone; - if (tz === undefined) { - // Step 16. - tz = DefaultTimeZone(); - } - internalProps.timeZone = tz; - - // Step 18. - var formatOpt = lazyDateTimeFormatData.formatOpt; - - // Steps 27-28, more or less - see comment after this function. - var pattern = toBestICUPattern(dataLocale, formatOpt); - - // Step 29. - internalProps.pattern = pattern; - - // Step 30. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the DateTimeFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't DateTimeFormat-initialized. - */ -function getDateTimeFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "DateTimeFormat", methodName); - assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveDateTimeFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Components of date and time formats and their values. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -var dateTimeComponentValues = { - weekday: ["narrow", "short", "long"], - era: ["narrow", "short", "long"], - year: ["2-digit", "numeric"], - month: ["2-digit", "numeric", "narrow", "short", "long"], - day: ["2-digit", "numeric"], - hour: ["2-digit", "numeric"], - minute: ["2-digit", "numeric"], - second: ["2-digit", "numeric"], - timeZoneName: ["short", "long"] -}; - - -var dateTimeComponents = std_Object_getOwnPropertyNames(dateTimeComponentValues); - - -/** - * Initializes an object as a DateTimeFormat. - * - * This method is complicated a moderate bit by its implementing initialization - * as a *lazy* concept. Everything that must happen now, does -- but we defer - * all the work we can until the object is actually used as a DateTimeFormat. - * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function InitializeDateTimeFormat(dateTimeFormat, locales, options) { - assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat"); - - // Step 1. - if (isInitializedIntlObject(dateTimeFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(dateTimeFormat); - - // Lazy DateTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // - // localeOpt: // *first* opt computed in InitializeDateTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // } - // - // timeZone: IANA time zone name, - // - // formatOpt: // *second* opt computed in InitializeDateTimeFormat - // { - // // all the properties/values listed in Table 3 - // // (weekday, era, year, month, day, &c.) - // - // hour12: true / false // optional - // } - // - // formatMatcher: "basic" / "best fit", - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every DateTimeFormat lazy data object has *all* these properties, - // never a subset of them. - var lazyDateTimeFormatData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyDateTimeFormatData.requestedLocales = requestedLocales; - - // Step 4. - options = ToDateTimeOptions(options, "any", "date"); - - // Compute options that impact interpretation of locale. - // Step 5. - var localeOpt = new Record(); - lazyDateTimeFormatData.localeOpt = localeOpt; - - // Steps 6-7. - var localeMatcher = - GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], - "best fit"); - localeOpt.localeMatcher = localeMatcher; - - // Steps 15-17. - var tz = options.timeZone; - if (tz !== undefined) { - // Step 15.a. - tz = ToString(tz); - - // Step 15.b. - var timeZone = intl_IsValidTimeZoneName(tz); - if (timeZone === null) - ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); - - // Step 15.c. - tz = CanonicalizeTimeZoneName(timeZone); - } - lazyDateTimeFormatData.timeZone = tz; - - // Step 18. - var formatOpt = new Record(); - lazyDateTimeFormatData.formatOpt = formatOpt; - - // Step 19. - var i, prop; - for (i = 0; i < dateTimeComponents.length; i++) { - prop = dateTimeComponents[i]; - var value = GetOption(options, prop, "string", dateTimeComponentValues[prop], undefined); - formatOpt[prop] = value; - } - - // Steps 20-21 provided by ICU - see comment after this function. - - // Step 22. - // - // For some reason (ICU not exposing enough interface?) we drop the - // requested format matcher on the floor after this. In any case, even if - // doing so is justified, we have to do this work here in case it triggers - // getters or similar. (bug 852837) - var formatMatcher = - GetOption(options, "formatMatcher", "string", ["basic", "best fit"], - "best fit"); - - // Steps 23-25 provided by ICU, more or less - see comment after this function. - - // Step 26. - var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); - - // Pass hr12 on to ICU. - if (hr12 !== undefined) - formatOpt.hour12 = hr12; - - // Step 31. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "DateTimeFormat", lazyDateTimeFormatData); -} - - -// Intl.DateTimeFormat and ICU skeletons and patterns -// ================================================== -// -// Different locales have different ways to display dates using the same -// basic components. For example, en-US might use "Sept. 24, 2012" while -// fr-FR might use "24 Sept. 2012". The intent of Intl.DateTimeFormat is to -// permit production of a format for the locale that best matches the -// set of date-time components and their desired representation as specified -// by the API client. -// -// ICU supports specification of date and time formats in three ways: -// -// 1) A style is just one of the identifiers FULL, LONG, MEDIUM, or SHORT. -// The date-time components included in each style and their representation -// are defined by ICU using CLDR locale data (CLDR is the Unicode -// Consortium's Common Locale Data Repository). -// -// 2) A skeleton is a string specifying which date-time components to include, -// and which representations to use for them. For example, "yyyyMMMMdd" -// specifies a year with at least four digits, a full month name, and a -// two-digit day. It does not specify in which order the components appear, -// how they are separated, the localized strings for textual components -// (such as weekday or month), whether the month is in format or -// stand-alone form¹, or the numbering system used for numeric components. -// All that information is filled in by ICU using CLDR locale data. -// ¹ The format form is the one used in formatted strings that include a -// day; the stand-alone form is used when not including days, e.g., in -// calendar headers. The two forms differ at least in some Slavic languages, -// e.g. Russian: "22 марта 2013 г." vs. "Март 2013". -// -// 3) A pattern is a string specifying which date-time components to include, -// in which order, with which separators, in which grammatical case. For -// example, "EEEE, d MMMM y" specifies the full localized weekday name, -// followed by comma and space, followed by the day, followed by space, -// followed by the full month name in format form, followed by space, -// followed by the full year. It -// still does not specify localized strings for textual components and the -// numbering system - these are determined by ICU using CLDR locale data or -// possibly API parameters. -// -// All actual formatting in ICU is done with patterns; styles and skeletons -// have to be mapped to patterns before processing. -// -// The options of DateTimeFormat most closely correspond to ICU skeletons. This -// implementation therefore, in the toBestICUPattern function, converts -// DateTimeFormat options to ICU skeletons, and then lets ICU map skeletons to -// actual ICU patterns. The pattern may not directly correspond to what the -// skeleton requests, as the mapper (UDateTimePatternGenerator) is constrained -// by the available locale data for the locale. The resulting ICU pattern is -// kept as the DateTimeFormat's [[pattern]] internal property and passed to ICU -// in the format method. -// -// An ICU pattern represents the information of the following DateTimeFormat -// internal properties described in the specification, which therefore don't -// exist separately in the implementation: -// - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], -// [[second]], [[timeZoneName]] -// - [[hour12]] -// - [[hourNo0]] -// When needed for the resolvedOptions method, the resolveICUPattern function -// maps the instance's ICU pattern back to the specified properties of the -// object returned by resolvedOptions. -// -// ICU date-time skeletons and patterns aren't fully documented in the ICU -// documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best -// documentation at this point is in UTR 35: -// http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns - - -/** - * Returns an ICU pattern string for the given locale and representing the - * specified options as closely as possible given available locale data. - */ -function toBestICUPattern(locale, options) { - // Create an ICU skeleton representing the specified options. See - // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table - var skeleton = ""; - switch (options.weekday) { - case "narrow": - skeleton += "EEEEE"; - break; - case "short": - skeleton += "E"; - break; - case "long": - skeleton += "EEEE"; - } - switch (options.era) { - case "narrow": - skeleton += "GGGGG"; - break; - case "short": - skeleton += "G"; - break; - case "long": - skeleton += "GGGG"; - break; - } - switch (options.year) { - case "2-digit": - skeleton += "yy"; - break; - case "numeric": - skeleton += "y"; - break; - } - switch (options.month) { - case "2-digit": - skeleton += "MM"; - break; - case "numeric": - skeleton += "M"; - break; - case "narrow": - skeleton += "MMMMM"; - break; - case "short": - skeleton += "MMM"; - break; - case "long": - skeleton += "MMMM"; - break; - } - switch (options.day) { - case "2-digit": - skeleton += "dd"; - break; - case "numeric": - skeleton += "d"; - break; - } - var hourSkeletonChar = "j"; - if (options.hour12 !== undefined) { - if (options.hour12) - hourSkeletonChar = "h"; - else - hourSkeletonChar = "H"; - } - switch (options.hour) { - case "2-digit": - skeleton += hourSkeletonChar + hourSkeletonChar; - break; - case "numeric": - skeleton += hourSkeletonChar; - break; - } - switch (options.minute) { - case "2-digit": - skeleton += "mm"; - break; - case "numeric": - skeleton += "m"; - break; - } - switch (options.second) { - case "2-digit": - skeleton += "ss"; - break; - case "numeric": - skeleton += "s"; - break; - } - switch (options.timeZoneName) { - case "short": - skeleton += "z"; - break; - case "long": - skeleton += "zzzz"; - break; - } - - // Let ICU convert the ICU skeleton to an ICU pattern for the given locale. - return intl_patternForSkeleton(locale, skeleton); -} - - -/** - * Returns a new options object that includes the provided options (if any) - * and fills in default components if required components are not defined. - * Required can be "date", "time", or "any". - * Defaults can be "date", "time", or "all". - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function ToDateTimeOptions(options, required, defaults) { - assert(typeof required === "string", "ToDateTimeOptions"); - assert(typeof defaults === "string", "ToDateTimeOptions"); - - // Steps 1-3. - if (options === undefined) - options = null; - else - options = ToObject(options); - options = std_Object_create(options); - - // Step 4. - var needDefaults = true; - - // Step 5. - if ((required === "date" || required === "any") && - (options.weekday !== undefined || options.year !== undefined || - options.month !== undefined || options.day !== undefined)) - { - needDefaults = false; - } - - // Step 6. - if ((required === "time" || required === "any") && - (options.hour !== undefined || options.minute !== undefined || - options.second !== undefined)) - { - needDefaults = false; - } - - // Step 7. - if (needDefaults && (defaults === "date" || defaults === "all")) { - // The specification says to call [[DefineOwnProperty]] with false for - // the Throw parameter, while Object.defineProperty uses true. For the - // calls here, the difference doesn't matter because we're adding - // properties to a new object. - _DefineDataProperty(options, "year", "numeric"); - _DefineDataProperty(options, "month", "numeric"); - _DefineDataProperty(options, "day", "numeric"); - } - - // Step 8. - if (needDefaults && (defaults === "time" || defaults === "all")) { - // See comment for step 7. - _DefineDataProperty(options, "hour", "numeric"); - _DefineDataProperty(options, "minute", "numeric"); - _DefineDataProperty(options, "second", "numeric"); - } - - // Step 9. - return options; -} - - -/** - * Compares the date and time components requested by options with the available - * date and time formats in formats, and selects the best match according - * to a specified basic matching algorithm. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function BasicFormatMatcher(options, formats) { - // Steps 1-6. - var removalPenalty = 120, - additionPenalty = 20, - longLessPenalty = 8, - longMorePenalty = 6, - shortLessPenalty = 6, - shortMorePenalty = 3; - - // Table 3. - var properties = ["weekday", "era", "year", "month", "day", - "hour", "minute", "second", "timeZoneName"]; - - // Step 11.c.vi.1. - var values = ["2-digit", "numeric", "narrow", "short", "long"]; - - // Steps 7-8. - var bestScore = -Infinity; - var bestFormat; - - // Steps 9-11. - var i = 0; - var len = formats.length; - while (i < len) { - // Steps 11.a-b. - var format = formats[i]; - var score = 0; - - // Step 11.c. - var formatProp; - for (var j = 0; j < properties.length; j++) { - var property = properties[j]; - - // Step 11.c.i. - var optionsProp = options[property]; - // Step missing from spec. - // https://bugs.ecmascript.org/show_bug.cgi?id=1254 - formatProp = undefined; - - // Steps 11.c.ii-iii. - if (callFunction(std_Object_hasOwnProperty, format, property)) - formatProp = format[property]; - - if (optionsProp === undefined && formatProp !== undefined) { - // Step 11.c.iv. - score -= additionPenalty; - } else if (optionsProp !== undefined && formatProp === undefined) { - // Step 11.c.v. - score -= removalPenalty; - } else { - // Step 11.c.vi. - var optionsPropIndex = callFunction(ArrayIndexOf, values, optionsProp); - var formatPropIndex = callFunction(ArrayIndexOf, values, formatProp); - var delta = std_Math_max(std_Math_min(formatPropIndex - optionsPropIndex, 2), -2); - if (delta === 2) - score -= longMorePenalty; - else if (delta === 1) - score -= shortMorePenalty; - else if (delta === -1) - score -= shortLessPenalty; - else if (delta === -2) - score -= longLessPenalty; - } - } - - // Step 11.d. - if (score > bestScore) { - bestScore = score; - bestFormat = format; - } - - // Step 11.e. - i++; - } - - // Step 12. - return bestFormat; -} - - -/** - * Compares the date and time components requested by options with the available - * date and time formats in formats, and selects the best match according - * to an unspecified best-fit matching algorithm. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function BestFitFormatMatcher(options, formats) { - // this implementation doesn't have anything better - return BasicFormatMatcher(options, formats); -} - - -/** - * Returns the subset of the given locale list for which this locale list has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 12.2.2. - */ -function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -/** - * DateTimeFormat internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.2.3. - */ -var dateTimeFormatInternalProperties = { - localeData: dateTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_DateTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["ca", "nu"] -}; - - -function dateTimeFormatLocaleData(locale) { - return { - ca: intl_availableCalendars(locale), - nu: getNumberingSystems(locale) - }; -} - - -/** - * Function to be bound and returned by Intl.DateTimeFormat.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function dateTimeFormatFormatToBind() { - // Steps 1.a.i-ii - var date = arguments.length > 0 ? arguments[0] : undefined; - var x = (date === undefined) ? std_Date_now() : ToNumber(date); - - // Step 1.a.iii. - return intl_FormatDateTime(this, x, false); -} - -/** - * Returns a function bound to this DateTimeFormat that returns a String value - * representing the result of calling ToNumber(date) according to the - * effective locale and the formatting options of this DateTimeFormat. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function Intl_DateTimeFormat_format_get() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "format"); - - // Step 1. - if (internals.boundFormat === undefined) { - // Step 1.a. - var F = dateTimeFormatFormatToBind; - - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; - } - - // Step 2. - return internals.boundFormat; -} - - -function Intl_DateTimeFormat_formatToParts() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - getDateTimeFormatInternals(this, "formatToParts"); - - // Steps 1.a.i-ii - var date = arguments.length > 0 ? arguments[0] : undefined; - var x = (date === undefined) ? std_Date_now() : ToNumber(date); - - // Step 1.a.iii. - return intl_FormatDateTime(this, x, true); -} - - -/** - * Returns the resolved options for a DateTimeFormat object. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4. - */ -function Intl_DateTimeFormat_resolvedOptions() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - calendar: internals.calendar, - numberingSystem: internals.numberingSystem, - timeZone: internals.timeZone - }; - resolveICUPattern(internals.pattern, result); - return result; -} - - -// Table mapping ICU pattern characters back to the corresponding date-time -// components of DateTimeFormat. See -// http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table -var icuPatternCharToComponent = { - E: "weekday", - G: "era", - y: "year", - M: "month", - L: "month", - d: "day", - h: "hour", - H: "hour", - k: "hour", - K: "hour", - m: "minute", - s: "second", - z: "timeZoneName", - v: "timeZoneName", - V: "timeZoneName" -}; - - -/** - * Maps an ICU pattern string to a corresponding set of date-time components - * and their values, and adds properties for these components to the result - * object, which will be returned by the resolvedOptions method. For the - * interpretation of ICU pattern characters, see - * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table - */ -function resolveICUPattern(pattern, result) { - assert(IsObject(result), "resolveICUPattern"); - var i = 0; - while (i < pattern.length) { - var c = pattern[i++]; - if (c === "'") { - while (i < pattern.length && pattern[i] !== "'") - i++; - i++; - } else { - var count = 1; - while (i < pattern.length && pattern[i] === c) { - i++; - count++; - } - var value; - switch (c) { - // "text" cases - case "G": - case "E": - case "z": - case "v": - case "V": - if (count <= 3) - value = "short"; - else if (count === 4) - value = "long"; - else - value = "narrow"; - break; - // "number" cases - case "y": - case "d": - case "h": - case "H": - case "m": - case "s": - case "k": - case "K": - if (count === 2) - value = "2-digit"; - else - value = "numeric"; - break; - // "text & number" cases - case "M": - case "L": - if (count === 1) - value = "numeric"; - else if (count === 2) - value = "2-digit"; - else if (count === 3) - value = "short"; - else if (count === 4) - value = "long"; - else - value = "narrow"; - break; - default: - // skip other pattern characters and literal text - } - if (callFunction(std_Object_hasOwnProperty, icuPatternCharToComponent, c)) - _DefineDataProperty(result, icuPatternCharToComponent[c], value); - if (c === "h" || c === "K") - _DefineDataProperty(result, "hour12", true); - else if (c === "H" || c === "k") - _DefineDataProperty(result, "hour12", false); - } - } -} - diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js new file mode 100644 index 0000000000..9c5c907ce3 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -0,0 +1,813 @@ +/* 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.DateTimeFormat **********/ + + +/** + * Compute an internal properties object from |lazyDateTimeFormatData|. + */ +function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { + assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?"); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // + // hour12: true / false, // optional + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // } + // + // formatMatcher: "basic" / "best fit", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + + var internalProps = std_Object_create(null); + + // Compute effective locale. + // Step 8. + var DateTimeFormat = dateTimeFormatInternalProperties; + + // Step 9. + var localeData = DateTimeFormat.localeData; + + // Step 10. + var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + lazyDateTimeFormatData.requestedLocales, + lazyDateTimeFormatData.localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // Steps 11-13. + internalProps.locale = r.locale; + internalProps.calendar = r.ca; + internalProps.numberingSystem = r.nu; + + // Compute formatting options. + // Step 14. + var dataLocale = r.dataLocale; + + // Steps 15-17. + var tz = lazyDateTimeFormatData.timeZone; + if (tz === undefined) { + // Step 16. + tz = DefaultTimeZone(); + } + internalProps.timeZone = tz; + + // Step 18. + var formatOpt = lazyDateTimeFormatData.formatOpt; + + // Steps 27-28, more or less - see comment after this function. + var pattern = toBestICUPattern(dataLocale, formatOpt); + + // Step 29. + internalProps.pattern = pattern; + + // Step 30. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the DateTimeFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't DateTimeFormat-initialized. + */ +function getDateTimeFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "DateTimeFormat", methodName); + assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveDateTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Components of date and time formats and their values. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +var dateTimeComponentValues = { + weekday: ["narrow", "short", "long"], + era: ["narrow", "short", "long"], + year: ["2-digit", "numeric"], + month: ["2-digit", "numeric", "narrow", "short", "long"], + day: ["2-digit", "numeric"], + hour: ["2-digit", "numeric"], + minute: ["2-digit", "numeric"], + second: ["2-digit", "numeric"], + timeZoneName: ["short", "long"] +}; + + +var dateTimeComponents = std_Object_getOwnPropertyNames(dateTimeComponentValues); + + +/** + * Initializes an object as a DateTimeFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a DateTimeFormat. + * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function InitializeDateTimeFormat(dateTimeFormat, locales, options) { + assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat"); + + // Step 1. + if (isInitializedIntlObject(dateTimeFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(dateTimeFormat); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // + // hour12: true / false // optional + // } + // + // formatMatcher: "basic" / "best fit", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + var lazyDateTimeFormatData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyDateTimeFormatData.requestedLocales = requestedLocales; + + // Step 4. + options = ToDateTimeOptions(options, "any", "date"); + + // Compute options that impact interpretation of locale. + // Step 5. + var localeOpt = new Record(); + lazyDateTimeFormatData.localeOpt = localeOpt; + + // Steps 6-7. + var localeMatcher = + GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], + "best fit"); + localeOpt.localeMatcher = localeMatcher; + + // Steps 15-17. + var tz = options.timeZone; + if (tz !== undefined) { + // Step 15.a. + tz = ToString(tz); + + // Step 15.b. + var timeZone = intl_IsValidTimeZoneName(tz); + if (timeZone === null) + ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); + + // Step 15.c. + tz = CanonicalizeTimeZoneName(timeZone); + } + lazyDateTimeFormatData.timeZone = tz; + + // Step 18. + var formatOpt = new Record(); + lazyDateTimeFormatData.formatOpt = formatOpt; + + // Step 19. + var i, prop; + for (i = 0; i < dateTimeComponents.length; i++) { + prop = dateTimeComponents[i]; + var value = GetOption(options, prop, "string", dateTimeComponentValues[prop], undefined); + formatOpt[prop] = value; + } + + // Steps 20-21 provided by ICU - see comment after this function. + + // Step 22. + // + // For some reason (ICU not exposing enough interface?) we drop the + // requested format matcher on the floor after this. In any case, even if + // doing so is justified, we have to do this work here in case it triggers + // getters or similar. (bug 852837) + var formatMatcher = + GetOption(options, "formatMatcher", "string", ["basic", "best fit"], + "best fit"); + + // Steps 23-25 provided by ICU, more or less - see comment after this function. + + // Step 26. + var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); + + // Pass hr12 on to ICU. + if (hr12 !== undefined) + formatOpt.hour12 = hr12; + + // Step 31. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "DateTimeFormat", lazyDateTimeFormatData); +} + + +// Intl.DateTimeFormat and ICU skeletons and patterns +// ================================================== +// +// Different locales have different ways to display dates using the same +// basic components. For example, en-US might use "Sept. 24, 2012" while +// fr-FR might use "24 Sept. 2012". The intent of Intl.DateTimeFormat is to +// permit production of a format for the locale that best matches the +// set of date-time components and their desired representation as specified +// by the API client. +// +// ICU supports specification of date and time formats in three ways: +// +// 1) A style is just one of the identifiers FULL, LONG, MEDIUM, or SHORT. +// The date-time components included in each style and their representation +// are defined by ICU using CLDR locale data (CLDR is the Unicode +// Consortium's Common Locale Data Repository). +// +// 2) A skeleton is a string specifying which date-time components to include, +// and which representations to use for them. For example, "yyyyMMMMdd" +// specifies a year with at least four digits, a full month name, and a +// two-digit day. It does not specify in which order the components appear, +// how they are separated, the localized strings for textual components +// (such as weekday or month), whether the month is in format or +// stand-alone form¹, or the numbering system used for numeric components. +// All that information is filled in by ICU using CLDR locale data. +// ¹ The format form is the one used in formatted strings that include a +// day; the stand-alone form is used when not including days, e.g., in +// calendar headers. The two forms differ at least in some Slavic languages, +// e.g. Russian: "22 марта 2013 г." vs. "Март 2013". +// +// 3) A pattern is a string specifying which date-time components to include, +// in which order, with which separators, in which grammatical case. For +// example, "EEEE, d MMMM y" specifies the full localized weekday name, +// followed by comma and space, followed by the day, followed by space, +// followed by the full month name in format form, followed by space, +// followed by the full year. It +// still does not specify localized strings for textual components and the +// numbering system - these are determined by ICU using CLDR locale data or +// possibly API parameters. +// +// All actual formatting in ICU is done with patterns; styles and skeletons +// have to be mapped to patterns before processing. +// +// The options of DateTimeFormat most closely correspond to ICU skeletons. This +// implementation therefore, in the toBestICUPattern function, converts +// DateTimeFormat options to ICU skeletons, and then lets ICU map skeletons to +// actual ICU patterns. The pattern may not directly correspond to what the +// skeleton requests, as the mapper (UDateTimePatternGenerator) is constrained +// by the available locale data for the locale. The resulting ICU pattern is +// kept as the DateTimeFormat's [[pattern]] internal property and passed to ICU +// in the format method. +// +// An ICU pattern represents the information of the following DateTimeFormat +// internal properties described in the specification, which therefore don't +// exist separately in the implementation: +// - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], +// [[second]], [[timeZoneName]] +// - [[hour12]] +// - [[hourNo0]] +// When needed for the resolvedOptions method, the resolveICUPattern function +// maps the instance's ICU pattern back to the specified properties of the +// object returned by resolvedOptions. +// +// ICU date-time skeletons and patterns aren't fully documented in the ICU +// documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best +// documentation at this point is in UTR 35: +// http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + + +/** + * Returns an ICU pattern string for the given locale and representing the + * specified options as closely as possible given available locale data. + */ +function toBestICUPattern(locale, options) { + // Create an ICU skeleton representing the specified options. See + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + var skeleton = ""; + switch (options.weekday) { + case "narrow": + skeleton += "EEEEE"; + break; + case "short": + skeleton += "E"; + break; + case "long": + skeleton += "EEEE"; + } + switch (options.era) { + case "narrow": + skeleton += "GGGGG"; + break; + case "short": + skeleton += "G"; + break; + case "long": + skeleton += "GGGG"; + break; + } + switch (options.year) { + case "2-digit": + skeleton += "yy"; + break; + case "numeric": + skeleton += "y"; + break; + } + switch (options.month) { + case "2-digit": + skeleton += "MM"; + break; + case "numeric": + skeleton += "M"; + break; + case "narrow": + skeleton += "MMMMM"; + break; + case "short": + skeleton += "MMM"; + break; + case "long": + skeleton += "MMMM"; + break; + } + switch (options.day) { + case "2-digit": + skeleton += "dd"; + break; + case "numeric": + skeleton += "d"; + break; + } + var hourSkeletonChar = "j"; + if (options.hour12 !== undefined) { + if (options.hour12) + hourSkeletonChar = "h"; + else + hourSkeletonChar = "H"; + } + switch (options.hour) { + case "2-digit": + skeleton += hourSkeletonChar + hourSkeletonChar; + break; + case "numeric": + skeleton += hourSkeletonChar; + break; + } + switch (options.minute) { + case "2-digit": + skeleton += "mm"; + break; + case "numeric": + skeleton += "m"; + break; + } + switch (options.second) { + case "2-digit": + skeleton += "ss"; + break; + case "numeric": + skeleton += "s"; + break; + } + switch (options.timeZoneName) { + case "short": + skeleton += "z"; + break; + case "long": + skeleton += "zzzz"; + break; + } + + // Let ICU convert the ICU skeleton to an ICU pattern for the given locale. + return intl_patternForSkeleton(locale, skeleton); +} + + +/** + * Returns a new options object that includes the provided options (if any) + * and fills in default components if required components are not defined. + * Required can be "date", "time", or "any". + * Defaults can be "date", "time", or "all". + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function ToDateTimeOptions(options, required, defaults) { + assert(typeof required === "string", "ToDateTimeOptions"); + assert(typeof defaults === "string", "ToDateTimeOptions"); + + // Steps 1-3. + if (options === undefined) + options = null; + else + options = ToObject(options); + options = std_Object_create(options); + + // Step 4. + var needDefaults = true; + + // Step 5. + if ((required === "date" || required === "any") && + (options.weekday !== undefined || options.year !== undefined || + options.month !== undefined || options.day !== undefined)) + { + needDefaults = false; + } + + // Step 6. + if ((required === "time" || required === "any") && + (options.hour !== undefined || options.minute !== undefined || + options.second !== undefined)) + { + needDefaults = false; + } + + // Step 7. + if (needDefaults && (defaults === "date" || defaults === "all")) { + // The specification says to call [[DefineOwnProperty]] with false for + // the Throw parameter, while Object.defineProperty uses true. For the + // calls here, the difference doesn't matter because we're adding + // properties to a new object. + _DefineDataProperty(options, "year", "numeric"); + _DefineDataProperty(options, "month", "numeric"); + _DefineDataProperty(options, "day", "numeric"); + } + + // Step 8. + if (needDefaults && (defaults === "time" || defaults === "all")) { + // See comment for step 7. + _DefineDataProperty(options, "hour", "numeric"); + _DefineDataProperty(options, "minute", "numeric"); + _DefineDataProperty(options, "second", "numeric"); + } + + // Step 9. + return options; +} + + +/** + * Compares the date and time components requested by options with the available + * date and time formats in formats, and selects the best match according + * to a specified basic matching algorithm. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function BasicFormatMatcher(options, formats) { + // Steps 1-6. + var removalPenalty = 120, + additionPenalty = 20, + longLessPenalty = 8, + longMorePenalty = 6, + shortLessPenalty = 6, + shortMorePenalty = 3; + + // Table 3. + var properties = ["weekday", "era", "year", "month", "day", + "hour", "minute", "second", "timeZoneName"]; + + // Step 11.c.vi.1. + var values = ["2-digit", "numeric", "narrow", "short", "long"]; + + // Steps 7-8. + var bestScore = -Infinity; + var bestFormat; + + // Steps 9-11. + var i = 0; + var len = formats.length; + while (i < len) { + // Steps 11.a-b. + var format = formats[i]; + var score = 0; + + // Step 11.c. + var formatProp; + for (var j = 0; j < properties.length; j++) { + var property = properties[j]; + + // Step 11.c.i. + var optionsProp = options[property]; + // Step missing from spec. + // https://bugs.ecmascript.org/show_bug.cgi?id=1254 + formatProp = undefined; + + // Steps 11.c.ii-iii. + if (callFunction(std_Object_hasOwnProperty, format, property)) + formatProp = format[property]; + + if (optionsProp === undefined && formatProp !== undefined) { + // Step 11.c.iv. + score -= additionPenalty; + } else if (optionsProp !== undefined && formatProp === undefined) { + // Step 11.c.v. + score -= removalPenalty; + } else { + // Step 11.c.vi. + var optionsPropIndex = callFunction(ArrayIndexOf, values, optionsProp); + var formatPropIndex = callFunction(ArrayIndexOf, values, formatProp); + var delta = std_Math_max(std_Math_min(formatPropIndex - optionsPropIndex, 2), -2); + if (delta === 2) + score -= longMorePenalty; + else if (delta === 1) + score -= shortMorePenalty; + else if (delta === -1) + score -= shortLessPenalty; + else if (delta === -2) + score -= longLessPenalty; + } + } + + // Step 11.d. + if (score > bestScore) { + bestScore = score; + bestFormat = format; + } + + // Step 11.e. + i++; + } + + // Step 12. + return bestFormat; +} + + +/** + * Compares the date and time components requested by options with the available + * date and time formats in formats, and selects the best match according + * to an unspecified best-fit matching algorithm. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function BestFitFormatMatcher(options, formats) { + // this implementation doesn't have anything better + return BasicFormatMatcher(options, formats); +} + + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 12.2.2. + */ +function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, + dateTimeFormatInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +/** + * DateTimeFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.2.3. + */ +var dateTimeFormatInternalProperties = { + localeData: dateTimeFormatLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_DateTimeFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["ca", "nu"] +}; + + +function dateTimeFormatLocaleData(locale) { + return { + ca: intl_availableCalendars(locale), + nu: getNumberingSystems(locale) + }; +} + + +/** + * Function to be bound and returned by Intl.DateTimeFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function dateTimeFormatFormatToBind() { + // Steps 1.a.i-ii + var date = arguments.length > 0 ? arguments[0] : undefined; + var x = (date === undefined) ? std_Date_now() : ToNumber(date); + + // Step 1.a.iii. + return intl_FormatDateTime(this, x, false); +} + +/** + * Returns a function bound to this DateTimeFormat that returns a String value + * representing the result of calling ToNumber(date) according to the + * effective locale and the formatting options of this DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function Intl_DateTimeFormat_format_get() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + var internals = getDateTimeFormatInternals(this, "format"); + + // Step 1. + if (internals.boundFormat === undefined) { + // Step 1.a. + var F = dateTimeFormatFormatToBind; + + // Step 1.b-d. + var bf = callFunction(FunctionBind, F, this); + internals.boundFormat = bf; + } + + // Step 2. + return internals.boundFormat; +} + + +function Intl_DateTimeFormat_formatToParts() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + getDateTimeFormatInternals(this, "formatToParts"); + + // Steps 1.a.i-ii + var date = arguments.length > 0 ? arguments[0] : undefined; + var x = (date === undefined) ? std_Date_now() : ToNumber(date); + + // Step 1.a.iii. + return intl_FormatDateTime(this, x, true); +} + + +/** + * Returns the resolved options for a DateTimeFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4. + */ +function Intl_DateTimeFormat_resolvedOptions() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + var internals = getDateTimeFormatInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + calendar: internals.calendar, + numberingSystem: internals.numberingSystem, + timeZone: internals.timeZone + }; + resolveICUPattern(internals.pattern, result); + return result; +} + + +// Table mapping ICU pattern characters back to the corresponding date-time +// components of DateTimeFormat. See +// http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table +var icuPatternCharToComponent = { + E: "weekday", + G: "era", + y: "year", + M: "month", + L: "month", + d: "day", + h: "hour", + H: "hour", + k: "hour", + K: "hour", + m: "minute", + s: "second", + z: "timeZoneName", + v: "timeZoneName", + V: "timeZoneName" +}; + + +/** + * Maps an ICU pattern string to a corresponding set of date-time components + * and their values, and adds properties for these components to the result + * object, which will be returned by the resolvedOptions method. For the + * interpretation of ICU pattern characters, see + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + */ +function resolveICUPattern(pattern, result) { + assert(IsObject(result), "resolveICUPattern"); + var i = 0; + while (i < pattern.length) { + var c = pattern[i++]; + if (c === "'") { + while (i < pattern.length && pattern[i] !== "'") + i++; + i++; + } else { + var count = 1; + while (i < pattern.length && pattern[i] === c) { + i++; + count++; + } + var value; + switch (c) { + // "text" cases + case "G": + case "E": + case "z": + case "v": + case "V": + if (count <= 3) + value = "short"; + else if (count === 4) + value = "long"; + else + value = "narrow"; + break; + // "number" cases + case "y": + case "d": + case "h": + case "H": + case "m": + case "s": + case "k": + case "K": + if (count === 2) + value = "2-digit"; + else + value = "numeric"; + break; + // "text & number" cases + case "M": + case "L": + if (count === 1) + value = "numeric"; + else if (count === 2) + value = "2-digit"; + else if (count === 3) + value = "short"; + else if (count === 4) + value = "long"; + else + value = "narrow"; + break; + default: + // skip other pattern characters and literal text + } + if (callFunction(std_Object_hasOwnProperty, icuPatternCharToComponent, c)) + _DefineDataProperty(result, icuPatternCharToComponent[c], value); + if (c === "h" || c === "K") + _DefineDataProperty(result, "hour12", true); + else if (c === "H" || c === "k") + _DefineDataProperty(result, "hour12", false); + } + } +} + diff --git a/js/src/moz.build b/js/src/moz.build index 767a92042d..40407a1985 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -669,6 +669,7 @@ selfhosted.inputs = [ 'builtin/Function.js', 'builtin/Generator.js', 'builtin/Intl.js', + 'builtin/intl/DateTimeFormat.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', 'builtin/intl/PluralRules.js', From a56b9e83243743e68ff1573ffc970c0452d0dad1 Mon Sep 17 00:00:00 2001 From: Martok Date: Fri, 17 Feb 2023 00:00:36 +0100 Subject: [PATCH 18/24] Issue #2046 - Move Intl.NumberFormat self-hosted code to a new builtin/intl/NumberFormat.js file --- js/src/builtin/Intl.js | 469 --------------------------- js/src/builtin/intl/NumberFormat.js | 473 ++++++++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 474 insertions(+), 469 deletions(-) create mode 100644 js/src/builtin/intl/NumberFormat.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index f1ec0163a9..2070b2ee78 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -1759,472 +1759,3 @@ function Intl_Collator_resolvedOptions() { } -/********** Intl.NumberFormat **********/ - - -/** - * NumberFormat internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.2.3. - */ -var numberFormatInternalProperties = { - localeData: numberFormatLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_NumberFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["nu"] -}; - - -/** - * Compute an internal properties object from |lazyNumberFormatData|. - */ -function resolveNumberFormatInternals(lazyNumberFormatData) { - assert(IsObject(lazyNumberFormatData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - // Step 3. - var requestedLocales = lazyNumberFormatData.requestedLocales; - - // Compute options that impact interpretation of locale. - // Step 6. - var opt = lazyNumberFormatData.opt; - - var NumberFormat = numberFormatInternalProperties; - - // Step 9. - var localeData = NumberFormat.localeData; - - // Step 10. - var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), - lazyNumberFormatData.requestedLocales, - lazyNumberFormatData.opt, - NumberFormat.relevantExtensionKeys, - localeData); - - // Steps 11-12. (Step 13 is not relevant to our implementation.) - internalProps.locale = r.locale; - internalProps.numberingSystem = r.nu; - - // Compute formatting options. - // Step 15. - var s = lazyNumberFormatData.style; - internalProps.style = s; - - // Steps 19, 21. - if (s === "currency") { - internalProps.currency = lazyNumberFormatData.currency; - internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; - } - - internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; - - internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; - - internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; - - if ("minimumSignificantDigits" in lazyNumberFormatData) { - // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the - // actual presence (versus undefined-ness) of these properties. - assert("maximumSignificantDigits" in lazyNumberFormatData, "min/max sig digits mismatch"); - internalProps.minimumSignificantDigits = lazyNumberFormatData.minimumSignificantDigits; - internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; - } - - // Step 27. - internalProps.useGrouping = lazyNumberFormatData.useGrouping; - - // Step 34. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the NumberFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't NumberFormat-initialized. - */ -function getNumberFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "NumberFormat", methodName); - assert(internals.type === "NumberFormat", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveNumberFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Applies digit options used for number formatting onto the intl object. - * - * Spec: ECMAScript Internationalization API Specification, 11.1.1. - */ -function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault) { - // We skip Step 1 because we set the properties on a lazyData object. - - // Step 2-3. - assert(IsObject(options), "SetNumberFormatDigitOptions"); - assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions"); - - // Steps 4-6. - const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); - const mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault); - const mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20); - - // Steps 7-8. - let mnsd = options.minimumSignificantDigits; - let mxsd = options.maximumSignificantDigits; - - // Steps 9-11. - lazyData.minimumIntegerDigits = mnid; - lazyData.minimumFractionDigits = mnfd; - lazyData.maximumFractionDigits = mxfd; - - // Step 12. - if (mnsd !== undefined || mxsd !== undefined) { - mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1); - mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21); - lazyData.minimumSignificantDigits = mnsd; - lazyData.maximumSignificantDigits = mxsd; - } -} - - -/** - * Initializes an object as a NumberFormat. - * - * This method is complicated a moderate bit by its implementing initialization - * as a *lazy* concept. Everything that must happen now, does -- but we defer - * all the work we can until the object is actually used as a NumberFormat. - * This later work occurs in |resolveNumberFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript Internationalization API Specification, 11.1.1. - */ -function InitializeNumberFormat(numberFormat, locales, options) { - assert(IsObject(numberFormat), "InitializeNumberFormat"); - - // Step 1. - if (isInitializedIntlObject(numberFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(numberFormat); - - // Lazy NumberFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // style: "decimal" / "percent" / "currency", - // - // // fields present only if style === "currency": - // currency: a well-formed currency code (IsWellFormedCurrencyCode), - // currencyDisplay: "code" / "symbol" / "name", - // - // opt: // opt object computed in InitializeNumberFormat - // { - // localeMatcher: "lookup" / "best fit", - // } - // - // minimumIntegerDigits: integer ∈ [1, 21], - // minimumFractionDigits: integer ∈ [0, 20], - // maximumFractionDigits: integer ∈ [0, 20], - // - // // optional - // minimumSignificantDigits: integer ∈ [1, 21], - // maximumSignificantDigits: integer ∈ [1, 21], - // - // useGrouping: true / false, - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every NumberFormat lazy data object has *all* these properties, never a - // subset of them. - var lazyNumberFormatData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyNumberFormatData.requestedLocales = requestedLocales; - - // Steps 4-5. - // - // If we ever need more speed here at startup, we should try to detect the - // case where |options === undefined| and Object.prototype hasn't been - // mucked with. (|options| is fully consumed in this method, so it's not a - // concern that Object.prototype might be touched between now and when - // |resolveNumberFormatInternals| is called.) For now just keep it simple. - if (options === undefined) - options = {}; - else - options = ToObject(options); - - // Compute options that impact interpretation of locale. - // Step 6. - var opt = new Record(); - lazyNumberFormatData.opt = opt; - - // Steps 7-8. - var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - // Compute formatting options. - // Step 14. - var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); - lazyNumberFormatData.style = s; - - // Steps 16-19. - var c = GetOption(options, "currency", "string", undefined, undefined); - if (c !== undefined && !IsWellFormedCurrencyCode(c)) - ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); - var cDigits; - if (s === "currency") { - if (c === undefined) - ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); - - // Steps 19.a-c. - c = toASCIIUpperCase(c); - lazyNumberFormatData.currency = c; - cDigits = CurrencyDigits(c); - } - - // Step 20. - var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); - if (s === "currency") - lazyNumberFormatData.currencyDisplay = cd; - - // Steps 22-24. - SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); - - // Step 25. - if (lazyNumberFormatData.maximumFractionDigits === undefined) { - let mxfdDefault = s === "currency" - ? cDigits - : s === "percent" - ? 0 - : 3; - lazyNumberFormatData.maximumFractionDigits = - std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); - } - - // Step 26. - var g = GetOption(options, "useGrouping", "boolean", undefined, true); - lazyNumberFormatData.useGrouping = g; - - // Steps 35-36. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "NumberFormat", lazyNumberFormatData); -} - - -/** - * Mapping from currency codes to the number of decimal digits used for them. - * Default is 2 digits. - * - * Spec: ISO 4217 Currency and Funds Code List. - * http://www.currency-iso.org/en/home/tables/table-a1.html - */ -var currencyDigits = { - BHD: 3, - BIF: 0, - BYR: 0, - CLF: 4, - CLP: 0, - DJF: 0, - GNF: 0, - IQD: 3, - ISK: 0, - JOD: 3, - JPY: 0, - KMF: 0, - KRW: 0, - KWD: 3, - LYD: 3, - OMR: 3, - PYG: 0, - RWF: 0, - TND: 3, - UGX: 0, - UYI: 0, - VND: 0, - VUV: 0, - XAF: 0, - XOF: 0, - XPF: 0 -}; - - -/** - * Returns the number of decimal digits to be used for the given currency. - * - * Spec: ECMAScript Internationalization API Specification, 11.1.1. - */ -function getCurrencyDigitsRE() { - return internalIntlRegExps.currencyDigitsRE || - (internalIntlRegExps.currencyDigitsRE = RegExpCreate("^[A-Z]{3}$")); -} -function CurrencyDigits(currency) { - assert(typeof currency === "string", "CurrencyDigits"); - assert(regexp_test_no_statics(getCurrencyDigitsRE(), currency), "CurrencyDigits"); - - if (callFunction(std_Object_hasOwnProperty, currencyDigits, currency)) - return currencyDigits[currency]; - return 2; -} - - -/** - * Returns the subset of the given locale list for which this locale list has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 11.2.2. - */ -function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -function getNumberingSystems(locale) { - // ICU doesn't have an API to determine the set of numbering systems - // supported for a locale; it generally pretends that any numbering system - // can be used with any locale. Supporting a decimal numbering system - // (where only the digits are replaced) is easy, so we offer them all here. - // Algorithmic numbering systems are typically tied to one locale, so for - // lack of information we don't offer them. To increase chances that - // other software will process output correctly, we further restrict to - // those decimal numbering systems explicitly listed in table 2 of - // the ECMAScript Internationalization API Specification, 11.3.2, which - // in turn are those with full specifications in version 21 of Unicode - // Technical Standard #35 using digits that were defined in Unicode 5.0, - // the Unicode version supported in Windows Vista. - // The one thing we can find out from ICU is the default numbering system - // for a locale. - var defaultNumberingSystem = intl_numberingSystem(locale); - return [ - defaultNumberingSystem, - "arab", "arabext", "bali", "beng", "deva", - "fullwide", "gujr", "guru", "hanidec", "khmr", - "knda", "laoo", "latn", "limb", "mlym", - "mong", "mymr", "orya", "tamldec", "telu", - "thai", "tibt" - ]; -} - - -function numberFormatLocaleData(locale) { - return { - nu: getNumberingSystems(locale) - }; -} - - -/** - * Function to be bound and returned by Intl.NumberFormat.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - */ -function numberFormatFormatToBind(value) { - // Steps 1.a.i implemented by ECMAScript declaration binding instantiation, - // ES5.1 10.5, step 4.d.ii. - - // Step 1.a.ii-iii. - var x = ToNumber(value); - return intl_FormatNumber(this, x, /* formatToParts = */ false); -} - - -/** - * Returns a function bound to this NumberFormat that returns a String value - * representing the result of calling ToNumber(value) according to the - * effective locale and the formatting options of this NumberFormat. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - */ -function Intl_NumberFormat_format_get() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "format"); - - // Step 1. - if (internals.boundFormat === undefined) { - // Step 1.a. - var F = numberFormatFormatToBind; - - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; - } - // Step 2. - return internals.boundFormat; -} - -function Intl_NumberFormat_formatToParts(value) { - // Step 1. - var nf = this; - - // Steps 2-3. - getNumberFormatInternals(nf, "formatToParts"); - - // Step 4. - var x = ToNumber(value); - - // Step 5. - return intl_FormatNumber(nf, x, /* formatToParts = */ true); -} - -/** - * Returns the resolved options for a NumberFormat object. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. - */ -function Intl_NumberFormat_resolvedOptions() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - numberingSystem: internals.numberingSystem, - style: internals.style, - minimumIntegerDigits: internals.minimumIntegerDigits, - minimumFractionDigits: internals.minimumFractionDigits, - maximumFractionDigits: internals.maximumFractionDigits, - useGrouping: internals.useGrouping - }; - var optionalProperties = [ - "currency", - "currencyDisplay", - "minimumSignificantDigits", - "maximumSignificantDigits" - ]; - for (var i = 0; i < optionalProperties.length; i++) { - var p = optionalProperties[i]; - if (callFunction(std_Object_hasOwnProperty, internals, p)) - _DefineDataProperty(result, p, internals[p]); - } - return result; -} - - diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js new file mode 100644 index 0000000000..a65ba20885 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.js @@ -0,0 +1,473 @@ +/* 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.NumberFormat **********/ + + +/** + * NumberFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.2.3. + */ +var numberFormatInternalProperties = { + localeData: numberFormatLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_NumberFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["nu"] +}; + + +/** + * Compute an internal properties object from |lazyNumberFormatData|. + */ +function resolveNumberFormatInternals(lazyNumberFormatData) { + assert(IsObject(lazyNumberFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + // Step 3. + var requestedLocales = lazyNumberFormatData.requestedLocales; + + // Compute options that impact interpretation of locale. + // Step 6. + var opt = lazyNumberFormatData.opt; + + var NumberFormat = numberFormatInternalProperties; + + // Step 9. + var localeData = NumberFormat.localeData; + + // Step 10. + var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), + lazyNumberFormatData.requestedLocales, + lazyNumberFormatData.opt, + NumberFormat.relevantExtensionKeys, + localeData); + + // Steps 11-12. (Step 13 is not relevant to our implementation.) + internalProps.locale = r.locale; + internalProps.numberingSystem = r.nu; + + // Compute formatting options. + // Step 15. + var s = lazyNumberFormatData.style; + internalProps.style = s; + + // Steps 19, 21. + if (s === "currency") { + internalProps.currency = lazyNumberFormatData.currency; + internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; + } + + internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; + + internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; + + internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; + + if ("minimumSignificantDigits" in lazyNumberFormatData) { + // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the + // actual presence (versus undefined-ness) of these properties. + assert("maximumSignificantDigits" in lazyNumberFormatData, "min/max sig digits mismatch"); + internalProps.minimumSignificantDigits = lazyNumberFormatData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; + } + + // Step 27. + internalProps.useGrouping = lazyNumberFormatData.useGrouping; + + // Step 34. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the NumberFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't NumberFormat-initialized. + */ +function getNumberFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "NumberFormat", methodName); + assert(internals.type === "NumberFormat", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveNumberFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Applies digit options used for number formatting onto the intl object. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault) { + // We skip Step 1 because we set the properties on a lazyData object. + + // Step 2-3. + assert(IsObject(options), "SetNumberFormatDigitOptions"); + assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions"); + + // Steps 4-6. + const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); + const mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault); + const mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20); + + // Steps 7-8. + let mnsd = options.minimumSignificantDigits; + let mxsd = options.maximumSignificantDigits; + + // Steps 9-11. + lazyData.minimumIntegerDigits = mnid; + lazyData.minimumFractionDigits = mnfd; + lazyData.maximumFractionDigits = mxfd; + + // Step 12. + if (mnsd !== undefined || mxsd !== undefined) { + mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1); + mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21); + lazyData.minimumSignificantDigits = mnsd; + lazyData.maximumSignificantDigits = mxsd; + } +} + + +/** + * Initializes an object as a NumberFormat. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a NumberFormat. + * This later work occurs in |resolveNumberFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function InitializeNumberFormat(numberFormat, locales, options) { + assert(IsObject(numberFormat), "InitializeNumberFormat"); + + // Step 1. + if (isInitializedIntlObject(numberFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(numberFormat); + + // Lazy NumberFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "decimal" / "percent" / "currency", + // + // // fields present only if style === "currency": + // currency: a well-formed currency code (IsWellFormedCurrencyCode), + // currencyDisplay: "code" / "symbol" / "name", + // + // opt: // opt object computed in InitializeNumberFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // minimumIntegerDigits: integer ∈ [1, 21], + // minimumFractionDigits: integer ∈ [0, 20], + // maximumFractionDigits: integer ∈ [0, 20], + // + // // optional + // minimumSignificantDigits: integer ∈ [1, 21], + // maximumSignificantDigits: integer ∈ [1, 21], + // + // useGrouping: true / false, + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every NumberFormat lazy data object has *all* these properties, never a + // subset of them. + var lazyNumberFormatData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyNumberFormatData.requestedLocales = requestedLocales; + + // Steps 4-5. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and Object.prototype hasn't been + // mucked with. (|options| is fully consumed in this method, so it's not a + // concern that Object.prototype might be touched between now and when + // |resolveNumberFormatInternals| is called.) For now just keep it simple. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Compute options that impact interpretation of locale. + // Step 6. + var opt = new Record(); + lazyNumberFormatData.opt = opt; + + // Steps 7-8. + var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + // Compute formatting options. + // Step 14. + var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); + lazyNumberFormatData.style = s; + + // Steps 16-19. + var c = GetOption(options, "currency", "string", undefined, undefined); + if (c !== undefined && !IsWellFormedCurrencyCode(c)) + ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); + var cDigits; + if (s === "currency") { + if (c === undefined) + ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); + + // Steps 19.a-c. + c = toASCIIUpperCase(c); + lazyNumberFormatData.currency = c; + cDigits = CurrencyDigits(c); + } + + // Step 20. + var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); + if (s === "currency") + lazyNumberFormatData.currencyDisplay = cd; + + // Steps 22-24. + SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); + + // Step 25. + if (lazyNumberFormatData.maximumFractionDigits === undefined) { + let mxfdDefault = s === "currency" + ? cDigits + : s === "percent" + ? 0 + : 3; + lazyNumberFormatData.maximumFractionDigits = + std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); + } + + // Step 26. + var g = GetOption(options, "useGrouping", "boolean", undefined, true); + lazyNumberFormatData.useGrouping = g; + + // Steps 35-36. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "NumberFormat", lazyNumberFormatData); +} + + +/** + * Mapping from currency codes to the number of decimal digits used for them. + * Default is 2 digits. + * + * Spec: ISO 4217 Currency and Funds Code List. + * http://www.currency-iso.org/en/home/tables/table-a1.html + */ +var currencyDigits = { + BHD: 3, + BIF: 0, + BYR: 0, + CLF: 4, + CLP: 0, + DJF: 0, + GNF: 0, + IQD: 3, + ISK: 0, + JOD: 3, + JPY: 0, + KMF: 0, + KRW: 0, + KWD: 3, + LYD: 3, + OMR: 3, + PYG: 0, + RWF: 0, + TND: 3, + UGX: 0, + UYI: 0, + VND: 0, + VUV: 0, + XAF: 0, + XOF: 0, + XPF: 0 +}; + + +/** + * Returns the number of decimal digits to be used for the given currency. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function getCurrencyDigitsRE() { + return internalIntlRegExps.currencyDigitsRE || + (internalIntlRegExps.currencyDigitsRE = RegExpCreate("^[A-Z]{3}$")); +} +function CurrencyDigits(currency) { + assert(typeof currency === "string", "CurrencyDigits"); + assert(regexp_test_no_statics(getCurrencyDigitsRE(), currency), "CurrencyDigits"); + + if (callFunction(std_Object_hasOwnProperty, currencyDigits, currency)) + return currencyDigits[currency]; + return 2; +} + + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 11.2.2. + */ +function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, + numberFormatInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +function getNumberingSystems(locale) { + // ICU doesn't have an API to determine the set of numbering systems + // supported for a locale; it generally pretends that any numbering system + // can be used with any locale. Supporting a decimal numbering system + // (where only the digits are replaced) is easy, so we offer them all here. + // Algorithmic numbering systems are typically tied to one locale, so for + // lack of information we don't offer them. To increase chances that + // other software will process output correctly, we further restrict to + // those decimal numbering systems explicitly listed in table 2 of + // the ECMAScript Internationalization API Specification, 11.3.2, which + // in turn are those with full specifications in version 21 of Unicode + // Technical Standard #35 using digits that were defined in Unicode 5.0, + // the Unicode version supported in Windows Vista. + // The one thing we can find out from ICU is the default numbering system + // for a locale. + var defaultNumberingSystem = intl_numberingSystem(locale); + return [ + defaultNumberingSystem, + "arab", "arabext", "bali", "beng", "deva", + "fullwide", "gujr", "guru", "hanidec", "khmr", + "knda", "laoo", "latn", "limb", "mlym", + "mong", "mymr", "orya", "tamldec", "telu", + "thai", "tibt" + ]; +} + + +function numberFormatLocaleData(locale) { + return { + nu: getNumberingSystems(locale) + }; +} + + +/** + * Function to be bound and returned by Intl.NumberFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + */ +function numberFormatFormatToBind(value) { + // Steps 1.a.i implemented by ECMAScript declaration binding instantiation, + // ES5.1 10.5, step 4.d.ii. + + // Step 1.a.ii-iii. + var x = ToNumber(value); + return intl_FormatNumber(this, x, /* formatToParts = */ false); +} + + +/** + * Returns a function bound to this NumberFormat that returns a String value + * representing the result of calling ToNumber(value) according to the + * effective locale and the formatting options of this NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + */ +function Intl_NumberFormat_format_get() { + // Check "this NumberFormat object" per introduction of section 11.3. + var internals = getNumberFormatInternals(this, "format"); + + // Step 1. + if (internals.boundFormat === undefined) { + // Step 1.a. + var F = numberFormatFormatToBind; + + // Step 1.b-d. + var bf = callFunction(FunctionBind, F, this); + internals.boundFormat = bf; + } + // Step 2. + return internals.boundFormat; +} + +function Intl_NumberFormat_formatToParts(value) { + // Step 1. + var nf = this; + + // Steps 2-3. + getNumberFormatInternals(nf, "formatToParts"); + + // Step 4. + var x = ToNumber(value); + + // Step 5. + return intl_FormatNumber(nf, x, /* formatToParts = */ true); +} + +/** + * Returns the resolved options for a NumberFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. + */ +function Intl_NumberFormat_resolvedOptions() { + // Check "this NumberFormat object" per introduction of section 11.3. + var internals = getNumberFormatInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + numberingSystem: internals.numberingSystem, + style: internals.style, + minimumIntegerDigits: internals.minimumIntegerDigits, + minimumFractionDigits: internals.minimumFractionDigits, + maximumFractionDigits: internals.maximumFractionDigits, + useGrouping: internals.useGrouping + }; + var optionalProperties = [ + "currency", + "currencyDisplay", + "minimumSignificantDigits", + "maximumSignificantDigits" + ]; + for (var i = 0; i < optionalProperties.length; i++) { + var p = optionalProperties[i]; + if (callFunction(std_Object_hasOwnProperty, internals, p)) + _DefineDataProperty(result, p, internals[p]); + } + return result; +} + + diff --git a/js/src/moz.build b/js/src/moz.build index 40407a1985..b5bd6e2f77 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -672,6 +672,7 @@ selfhosted.inputs = [ 'builtin/intl/DateTimeFormat.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', + 'builtin/intl/NumberFormat.js', 'builtin/intl/PluralRules.js', 'builtin/intl/RelativeTimeFormat.js', 'builtin/Iterator.js', From 45720521cf10acd22941607f09e2b1b9c6080b5d Mon Sep 17 00:00:00 2001 From: Martok Date: Fri, 17 Feb 2023 00:01:59 +0100 Subject: [PATCH 19/24] Issue #2046 - Move Intl.Collator self-hosted code to a new builtin/intl/Collator.js file --- js/src/builtin/Intl.js | 353 ------------------------------- js/src/builtin/intl/Collator.js | 355 ++++++++++++++++++++++++++++++++ js/src/moz.build | 1 + 3 files changed, 356 insertions(+), 353 deletions(-) create mode 100644 js/src/builtin/intl/Collator.js diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index 2070b2ee78..11d57a2e59 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -1406,356 +1406,3 @@ function getInternals(obj) return internalProps; } - -/********** Intl.Collator **********/ - - -/** - * Mapping from Unicode extension keys for collation to options properties, - * their types and permissible values. - * - * Spec: ECMAScript Internationalization API Specification, 10.1.1. - */ -var collatorKeyMappings = { - kn: {property: "numeric", type: "boolean"}, - kf: {property: "caseFirst", type: "string", values: ["upper", "lower", "false"]} -}; - - -/** - * Compute an internal properties object from |lazyCollatorData|. - */ -function resolveCollatorInternals(lazyCollatorData) -{ - assert(IsObject(lazyCollatorData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - // Step 7. - internalProps.usage = lazyCollatorData.usage; - - // Step 8. - var Collator = collatorInternalProperties; - - // Step 9. - var collatorIsSorting = lazyCollatorData.usage === "sort"; - var localeData = collatorIsSorting - ? Collator.sortLocaleData - : Collator.searchLocaleData; - - // Compute effective locale. - // Step 14. - var relevantExtensionKeys = Collator.relevantExtensionKeys; - - // Step 15. - var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), - lazyCollatorData.requestedLocales, - lazyCollatorData.opt, - relevantExtensionKeys, - localeData); - - // Step 16. - internalProps.locale = r.locale; - - // Steps 17-19. - var key, property, value, mapping; - var i = 0, len = relevantExtensionKeys.length; - while (i < len) { - // Step 19.a. - key = relevantExtensionKeys[i]; - if (key === "co") { - // Step 19.b. - property = "collation"; - value = r.co === null ? "default" : r.co; - } else { - // Step 19.c. - mapping = collatorKeyMappings[key]; - property = mapping.property; - value = r[key]; - if (mapping.type === "boolean") - value = value === "true"; - } - - // Step 19.d. - internalProps[property] = value; - - // Step 19.e. - i++; - } - - // Compute remaining collation options. - // Steps 21-22. - var s = lazyCollatorData.rawSensitivity; - if (s === undefined) { - if (collatorIsSorting) { - // Step 21.a. - s = "variant"; - } else { - // Step 21.b. - var dataLocale = r.dataLocale; - var dataLocaleData = localeData(dataLocale); - s = dataLocaleData.sensitivity; - } - } - internalProps.sensitivity = s; - - // Step 24. - internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; - - // Step 25. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the Collator internal properties of |obj|, or - * throws a TypeError if |obj| isn't Collator-initialized. - */ -function getCollatorInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "Collator", methodName); - assert(internals.type === "Collator", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveCollatorInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - - -/** - * Initializes an object as a Collator. - * - * This method is complicated a moderate bit by its implementing initialization - * as a *lazy* concept. Everything that must happen now, does -- but we defer - * all the work we can until the object is actually used as a Collator. This - * later work occurs in |resolveCollatorInternals|; steps not noted here occur - * there. - * - * Spec: ECMAScript Internationalization API Specification, 10.1.1. - */ -function InitializeCollator(collator, locales, options) { - assert(IsObject(collator), "InitializeCollator"); - - // Step 1. - if (isInitializedIntlObject(collator)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(collator); - - // Lazy Collator data has the following structure: - // - // { - // requestedLocales: List of locales, - // usage: "sort" / "search", - // opt: // opt object computed in InitializeCollator - // { - // localeMatcher: "lookup" / "best fit", - // kn: true / false / undefined, - // kf: "upper" / "lower" / "false" / undefined - // } - // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined, - // ignorePunctuation: true / false - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every Collator lazy data object has *all* these properties, never a - // subset of them. - var lazyCollatorData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyCollatorData.requestedLocales = requestedLocales; - - // Steps 4-5. - // - // If we ever need more speed here at startup, we should try to detect the - // case where |options === undefined| and Object.prototype hasn't been - // mucked with. (|options| is fully consumed in this method, so it's not a - // concern that Object.prototype might be touched between now and when - // |resolveCollatorInternals| is called.) For now, just keep it simple. - if (options === undefined) - options = {}; - else - options = ToObject(options); - - // Compute options that impact interpretation of locale. - // Step 6. - var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); - lazyCollatorData.usage = u; - - // Step 10. - var opt = new Record(); - lazyCollatorData.opt = opt; - - // Steps 11-12. - var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - // Step 13, unrolled. - var numericValue = GetOption(options, "numeric", "boolean", undefined, undefined); - if (numericValue !== undefined) - numericValue = numericValue ? 'true' : 'false'; - opt.kn = numericValue; - - var caseFirstValue = GetOption(options, "caseFirst", "string", ["upper", "lower", "false"], undefined); - opt.kf = caseFirstValue; - - // Compute remaining collation options. - // Step 20. - var s = GetOption(options, "sensitivity", "string", - ["base", "accent", "case", "variant"], undefined); - lazyCollatorData.rawSensitivity = s; - - // Step 23. - var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); - lazyCollatorData.ignorePunctuation = ip; - - // Step 26. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "Collator", lazyCollatorData); -} - - -/** - * Returns the subset of the given locale list for which this locale list has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 10.2.2. - */ -function Intl_Collator_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -/** - * Collator internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3. - */ -var collatorInternalProperties = { - sortLocaleData: collatorSortLocaleData, - searchLocaleData: collatorSearchLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_Collator_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["co", "kn"] -}; - - -function collatorSortLocaleData(locale) { - var collations = intl_availableCollations(locale); - callFunction(std_Array_unshift, collations, null); - return { - co: collations, - kn: ["false", "true"] - }; -} - - -function collatorSearchLocaleData(locale) { - return { - co: [null], - kn: ["false", "true"], - // In theory the default sensitivity is locale dependent; - // in reality the CLDR/ICU default strength is always tertiary. - sensitivity: "variant" - }; -} - - -/** - * Function to be bound and returned by Intl.Collator.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function collatorCompareToBind(x, y) { - // Steps 1.a.i-ii implemented by ECMAScript declaration binding instantiation, - // ES5.1 10.5, step 4.d.ii. - - // Step 1.a.iii-v. - var X = ToString(x); - var Y = ToString(y); - return intl_CompareStrings(this, X, Y); -} - - -/** - * Returns a function bound to this Collator that compares x (converted to a - * String value) and y (converted to a String value), - * and returns a number less than 0 if x < y, 0 if x = y, or a number greater - * than 0 if x > y according to the sort order for the locale and collation - * options of this Collator object. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.2. - */ -function Intl_Collator_compare_get() { - // Check "this Collator object" per introduction of section 10.3. - var internals = getCollatorInternals(this, "compare"); - - // Step 1. - if (internals.boundCompare === undefined) { - // Step 1.a. - var F = collatorCompareToBind; - - // Step 1.b-d. - var bc = callFunction(FunctionBind, F, this); - internals.boundCompare = bc; - } - - // Step 2. - return internals.boundCompare; -} - - -/** - * Returns the resolved options for a Collator object. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.3 and 10.4. - */ -function Intl_Collator_resolvedOptions() { - // Check "this Collator object" per introduction of section 10.3. - var internals = getCollatorInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - usage: internals.usage, - sensitivity: internals.sensitivity, - ignorePunctuation: internals.ignorePunctuation - }; - - var relevantExtensionKeys = collatorInternalProperties.relevantExtensionKeys; - for (var i = 0; i < relevantExtensionKeys.length; i++) { - var key = relevantExtensionKeys[i]; - var property = (key === "co") ? "collation" : collatorKeyMappings[key].property; - _DefineDataProperty(result, property, internals[property]); - } - return result; -} - - diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js new file mode 100644 index 0000000000..5fb85f8e6a --- /dev/null +++ b/js/src/builtin/intl/Collator.js @@ -0,0 +1,355 @@ +/* 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.Collator **********/ + + +/** + * Mapping from Unicode extension keys for collation to options properties, + * their types and permissible values. + * + * Spec: ECMAScript Internationalization API Specification, 10.1.1. + */ +var collatorKeyMappings = { + kn: {property: "numeric", type: "boolean"}, + kf: {property: "caseFirst", type: "string", values: ["upper", "lower", "false"]} +}; + + +/** + * Compute an internal properties object from |lazyCollatorData|. + */ +function resolveCollatorInternals(lazyCollatorData) +{ + assert(IsObject(lazyCollatorData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + // Step 7. + internalProps.usage = lazyCollatorData.usage; + + // Step 8. + var Collator = collatorInternalProperties; + + // Step 9. + var collatorIsSorting = lazyCollatorData.usage === "sort"; + var localeData = collatorIsSorting + ? Collator.sortLocaleData + : Collator.searchLocaleData; + + // Compute effective locale. + // Step 14. + var relevantExtensionKeys = Collator.relevantExtensionKeys; + + // Step 15. + var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), + lazyCollatorData.requestedLocales, + lazyCollatorData.opt, + relevantExtensionKeys, + localeData); + + // Step 16. + internalProps.locale = r.locale; + + // Steps 17-19. + var key, property, value, mapping; + var i = 0, len = relevantExtensionKeys.length; + while (i < len) { + // Step 19.a. + key = relevantExtensionKeys[i]; + if (key === "co") { + // Step 19.b. + property = "collation"; + value = r.co === null ? "default" : r.co; + } else { + // Step 19.c. + mapping = collatorKeyMappings[key]; + property = mapping.property; + value = r[key]; + if (mapping.type === "boolean") + value = value === "true"; + } + + // Step 19.d. + internalProps[property] = value; + + // Step 19.e. + i++; + } + + // Compute remaining collation options. + // Steps 21-22. + var s = lazyCollatorData.rawSensitivity; + if (s === undefined) { + if (collatorIsSorting) { + // Step 21.a. + s = "variant"; + } else { + // Step 21.b. + var dataLocale = r.dataLocale; + var dataLocaleData = localeData(dataLocale); + s = dataLocaleData.sensitivity; + } + } + internalProps.sensitivity = s; + + // Step 24. + internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; + + // Step 25. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the Collator internal properties of |obj|, or + * throws a TypeError if |obj| isn't Collator-initialized. + */ +function getCollatorInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "Collator", methodName); + assert(internals.type === "Collator", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveCollatorInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + + +/** + * Initializes an object as a Collator. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a Collator. This + * later work occurs in |resolveCollatorInternals|; steps not noted here occur + * there. + * + * Spec: ECMAScript Internationalization API Specification, 10.1.1. + */ +function InitializeCollator(collator, locales, options) { + assert(IsObject(collator), "InitializeCollator"); + + // Step 1. + if (isInitializedIntlObject(collator)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(collator); + + // Lazy Collator data has the following structure: + // + // { + // requestedLocales: List of locales, + // usage: "sort" / "search", + // opt: // opt object computed in InitializeCollator + // { + // localeMatcher: "lookup" / "best fit", + // kn: true / false / undefined, + // kf: "upper" / "lower" / "false" / undefined + // } + // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined, + // ignorePunctuation: true / false + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every Collator lazy data object has *all* these properties, never a + // subset of them. + var lazyCollatorData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyCollatorData.requestedLocales = requestedLocales; + + // Steps 4-5. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and Object.prototype hasn't been + // mucked with. (|options| is fully consumed in this method, so it's not a + // concern that Object.prototype might be touched between now and when + // |resolveCollatorInternals| is called.) For now, just keep it simple. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Compute options that impact interpretation of locale. + // Step 6. + var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); + lazyCollatorData.usage = u; + + // Step 10. + var opt = new Record(); + lazyCollatorData.opt = opt; + + // Steps 11-12. + var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + // Step 13, unrolled. + var numericValue = GetOption(options, "numeric", "boolean", undefined, undefined); + if (numericValue !== undefined) + numericValue = numericValue ? 'true' : 'false'; + opt.kn = numericValue; + + var caseFirstValue = GetOption(options, "caseFirst", "string", ["upper", "lower", "false"], undefined); + opt.kf = caseFirstValue; + + // Compute remaining collation options. + // Step 20. + var s = GetOption(options, "sensitivity", "string", + ["base", "accent", "case", "variant"], undefined); + lazyCollatorData.rawSensitivity = s; + + // Step 23. + var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); + lazyCollatorData.ignorePunctuation = ip; + + // Step 26. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "Collator", lazyCollatorData); +} + + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 10.2.2. + */ +function Intl_Collator_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(collatorInternalProperties.availableLocales, + collatorInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +/** + * Collator internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3. + */ +var collatorInternalProperties = { + sortLocaleData: collatorSortLocaleData, + searchLocaleData: collatorSearchLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_Collator_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["co", "kn"] +}; + + +function collatorSortLocaleData(locale) { + var collations = intl_availableCollations(locale); + callFunction(std_Array_unshift, collations, null); + return { + co: collations, + kn: ["false", "true"] + }; +} + + +function collatorSearchLocaleData(locale) { + return { + co: [null], + kn: ["false", "true"], + // In theory the default sensitivity is locale dependent; + // in reality the CLDR/ICU default strength is always tertiary. + sensitivity: "variant" + }; +} + + +/** + * Function to be bound and returned by Intl.Collator.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function collatorCompareToBind(x, y) { + // Steps 1.a.i-ii implemented by ECMAScript declaration binding instantiation, + // ES5.1 10.5, step 4.d.ii. + + // Step 1.a.iii-v. + var X = ToString(x); + var Y = ToString(y); + return intl_CompareStrings(this, X, Y); +} + + +/** + * Returns a function bound to this Collator that compares x (converted to a + * String value) and y (converted to a String value), + * and returns a number less than 0 if x < y, 0 if x = y, or a number greater + * than 0 if x > y according to the sort order for the locale and collation + * options of this Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.2. + */ +function Intl_Collator_compare_get() { + // Check "this Collator object" per introduction of section 10.3. + var internals = getCollatorInternals(this, "compare"); + + // Step 1. + if (internals.boundCompare === undefined) { + // Step 1.a. + var F = collatorCompareToBind; + + // Step 1.b-d. + var bc = callFunction(FunctionBind, F, this); + internals.boundCompare = bc; + } + + // Step 2. + return internals.boundCompare; +} + + +/** + * Returns the resolved options for a Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.3 and 10.4. + */ +function Intl_Collator_resolvedOptions() { + // Check "this Collator object" per introduction of section 10.3. + var internals = getCollatorInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + usage: internals.usage, + sensitivity: internals.sensitivity, + ignorePunctuation: internals.ignorePunctuation + }; + + var relevantExtensionKeys = collatorInternalProperties.relevantExtensionKeys; + for (var i = 0; i < relevantExtensionKeys.length; i++) { + var key = relevantExtensionKeys[i]; + var property = (key === "co") ? "collation" : collatorKeyMappings[key].property; + _DefineDataProperty(result, property, internals[property]); + } + return result; +} + diff --git a/js/src/moz.build b/js/src/moz.build index b5bd6e2f77..d60a2c8d64 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -669,6 +669,7 @@ selfhosted.inputs = [ 'builtin/Function.js', 'builtin/Generator.js', 'builtin/Intl.js', + 'builtin/intl/Collator.js', 'builtin/intl/DateTimeFormat.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', From cf2e2ed90695a17d45fcd674a604567d3df18236 Mon Sep 17 00:00:00 2001 From: Martok Date: Fri, 17 Feb 2023 00:07:06 +0100 Subject: [PATCH 20/24] Issue #2046 - Move builtin/Intl.js (which now contains only shared functionality) to builtin/intl/CommonFunctions.js --- .../{Intl.js => intl/CommonFunctions.js} | 29 ------------------- js/src/moz.build | 2 +- 2 files changed, 1 insertion(+), 30 deletions(-) rename js/src/builtin/{Intl.js => intl/CommonFunctions.js} (97%) diff --git a/js/src/builtin/Intl.js b/js/src/builtin/intl/CommonFunctions.js similarity index 97% rename from js/src/builtin/Intl.js rename to js/src/builtin/intl/CommonFunctions.js index 11d57a2e59..0665ccef43 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/intl/CommonFunctions.js @@ -4,35 +4,6 @@ /* Portions Copyright Norbert Lindenberg 2011-2012. */ -/*global JSMSG_INTL_OBJECT_NOT_INITED: false, JSMSG_INVALID_LOCALES_ELEMENT: false, - JSMSG_INVALID_LANGUAGE_TAG: false, JSMSG_INVALID_LOCALE_MATCHER: false, - JSMSG_INVALID_OPTION_VALUE: false, JSMSG_INVALID_DIGITS_VALUE: false, - JSMSG_INTL_OBJECT_REINITED: false, JSMSG_INVALID_CURRENCY_CODE: false, - JSMSG_UNDEFINED_CURRENCY: false, JSMSG_INVALID_TIME_ZONE: false, - JSMSG_DATE_NOT_FINITE: false, JSMSG_INVALID_KEYS_TYPE: false, - JSMSG_INVALID_KEY: false, - intl_Collator_availableLocales: false, - intl_availableCollations: false, - intl_CompareStrings: false, - intl_NumberFormat_availableLocales: false, - intl_numberingSystem: false, - intl_FormatNumber: false, - intl_DateTimeFormat_availableLocales: false, - intl_availableCalendars: false, - intl_patternForSkeleton: false, - intl_FormatDateTime: false, - intl_SelectPluralRule: false, - intl_GetPluralCategories: false, - intl_FormatRelativeTime: false, - intl_GetCalendarInfo: false, -*/ - -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ - - /********** Locales, Time Zones, and Currencies **********/ diff --git a/js/src/moz.build b/js/src/moz.build index d60a2c8d64..d54547aecf 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -668,8 +668,8 @@ selfhosted.inputs = [ 'builtin/Error.js', 'builtin/Function.js', 'builtin/Generator.js', - 'builtin/Intl.js', 'builtin/intl/Collator.js', + 'builtin/intl/CommonFunctions.js', 'builtin/intl/DateTimeFormat.js', 'builtin/intl/IntlObject.js', 'builtin/intl/LangTagMappingsGenerated.js', From 87eaaee953daa7ac35f01e37edfc94bcfc4fda9c Mon Sep 17 00:00:00 2001 From: Martok Date: Fri, 17 Feb 2023 00:21:40 +0100 Subject: [PATCH 21/24] Issue #2046 - Move a bunch of functions in builtin/intl/CommonFunctions.js into more-specific files, where those functions are only used in a single more-specific file --- js/src/builtin/intl/CommonFunctions.js | 170 +++---------------------- js/src/builtin/intl/DateTimeFormat.js | 85 +++++++++++++ js/src/builtin/intl/NumberFormat.js | 38 ++++++ 3 files changed, 141 insertions(+), 152 deletions(-) diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js index 0665ccef43..48337e666a 100644 --- a/js/src/builtin/intl/CommonFunctions.js +++ b/js/src/builtin/intl/CommonFunctions.js @@ -4,29 +4,6 @@ /* Portions Copyright Norbert Lindenberg 2011-2012. */ -/********** Locales, Time Zones, and Currencies **********/ - - -/** - * Convert s to upper case, but limited to characters a-z. - * - * Spec: ECMAScript Internationalization API Specification, 6.1. - */ -function toASCIIUpperCase(s) { - assert(typeof s === "string", "toASCIIUpperCase"); - - // String.prototype.toUpperCase may map non-ASCII characters into ASCII, - // so go character by character (actually code unit by code unit, but - // since we only care about ASCII characters here, that's OK). - var result = ""; - for (var i = 0; i < s.length; i++) { - var c = callFunction(std_String_charCodeAt, s, i); - result += (0x61 <= c && c <= 0x7A) - ? callFunction(std_String_fromCharCode, null, c & ~0x20) - : s[i]; - } - return result; -} /** * Holder object for encapsulating regexp instances. @@ -314,6 +291,24 @@ function IsStructurallyValidLanguageTag(locale) { !regexp_test_no_statics(duplicateSingletonRE, locale); } +/** + * Joins the array elements in the given range with the supplied separator. + */ +function ArrayJoinRange(array, separator, from, to = array.length) { + assert(typeof separator === "string", "|separator| is a string value"); + assert(typeof from === "number", "|from| is a number value"); + assert(typeof to === "number", "|to| is a number value"); + assert(0 <= from && from <= to && to <= array.length, "|from| and |to| form a valid range"); + + if (from === to) + return ""; + + var result = array[from]; + for (var i = from + 1; i < to; i++) { + result += separator + array[i]; + } + return result; +} /** * Canonicalizes the given structurally valid BCP 47 language tag, including @@ -443,25 +438,6 @@ function CanonicalizeLanguageTag(locale) { return canonical; } -/** - * Joins the array elements in the given range with the supplied separator. - */ -function ArrayJoinRange(array, separator, from, to = array.length) { - assert(typeof separator === "string", "|separator| is a string value"); - assert(typeof from === "number", "|from| is a number value"); - assert(typeof to === "number", "|to| is a number value"); - assert(0 <= from && from <= to && to <= array.length, "|from| and |to| form a valid range"); - - if (from === to) - return ""; - - var result = array[from]; - for (var i = from + 1; i < to; i++) { - result += separator + array[i]; - } - return result; -} - function localeContainsNoUnicodeExtensions(locale) { // No "-u-", no possible Unicode extension. if (callFunction(std_String_indexOf, locale, "-u-") === -1) @@ -601,113 +577,6 @@ function DefaultLocale() { } -/** - * Verifies that the given string is a well-formed ISO 4217 currency code. - * - * Spec: ECMAScript Internationalization API Specification, 6.3.1. - */ -function getIsWellFormedCurrencyCodeRE() { - return internalIntlRegExps.isWellFormedCurrencyCodeRE || - (internalIntlRegExps.isWellFormedCurrencyCodeRE = RegExpCreate("[^A-Z]")); -} -function IsWellFormedCurrencyCode(currency) { - var c = ToString(currency); - var normalized = toASCIIUpperCase(c); - if (normalized.length !== 3) - return false; - return !regexp_test_no_statics(getIsWellFormedCurrencyCodeRE(), normalized); -} - - -var timeZoneCache = { - icuDefaultTimeZone: undefined, - defaultTimeZone: undefined, -}; - - -/** - * 6.4.2 CanonicalizeTimeZoneName ( timeZone ) - * - * Canonicalizes the given IANA time zone name. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - */ -function CanonicalizeTimeZoneName(timeZone) { - assert(typeof timeZone === "string", "CanonicalizeTimeZoneName"); - - // Step 1. (Not applicable, the input is already a valid IANA time zone.) - assert(timeZone !== "Etc/Unknown", "Invalid time zone"); - assert(timeZone === intl_IsValidTimeZoneName(timeZone), "Time zone name not normalized"); - - // Step 2. - var ianaTimeZone = intl_canonicalizeTimeZone(timeZone); - assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone"); - assert(ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone), "Unsupported canonical time zone"); - - // Step 3. - if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") { - // ICU/CLDR canonicalizes Etc/UCT to Etc/GMT, but following IANA and - // ECMA-402 to the letter means Etc/UCT is a separate time zone. - if (timeZone === "Etc/UCT" || timeZone === "UCT") - ianaTimeZone = "Etc/UCT"; - else - ianaTimeZone = "UTC"; - } - - // Step 4. - return ianaTimeZone; -} - - -/** - * 6.4.3 DefaultTimeZone () - * - * Returns the IANA time zone name for the host environment's current time zone. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - */ -function DefaultTimeZone() { - const icuDefaultTimeZone = intl_defaultTimeZone(); - if (timeZoneCache.icuDefaultTimeZone === icuDefaultTimeZone) - return timeZoneCache.defaultTimeZone; - - // Verify that the current ICU time zone is a valid ECMA-402 time zone. - var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone); - if (timeZone === null) { - // Before defaulting to "UTC", try to represent the default time zone - // using the Etc/GMT + offset format. This format only accepts full - // hour offsets. - const msPerHour = 60 * 60 * 1000; - var offset = intl_defaultTimeZoneOffset(); - assert(offset === (offset | 0), - "milliseconds offset shouldn't be able to exceed int32_t range"); - var offsetHours = offset / msPerHour, offsetHoursFraction = offset % msPerHour; - if (offsetHoursFraction === 0) { - // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset - // means a location west of GMT. - timeZone = "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours); - - // Check if the fallback is valid. - timeZone = intl_IsValidTimeZoneName(timeZone); - } - - // Fallback to "UTC" if everything else fails. - if (timeZone === null) - timeZone = "UTC"; - } - - // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC. - var defaultTimeZone = CanonicalizeTimeZoneName(timeZone); - - timeZoneCache.defaultTimeZone = defaultTimeZone; - timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone; - - return defaultTimeZone; -} - - -/********** Locale and Parameter Negotiation **********/ - /** * Add old-style language tags without script code for locales that in current * usage would include a script subtag. Also add an entry for the last-ditch @@ -1180,9 +1049,6 @@ function GetNumberOption(options, property, minimum, maximum, fallback) { } -/********** Property access for Intl objects **********/ - - /** * Weak map used to track the initialize-as-Intl status (and, if an object has * been so initialized, the Intl-specific internal properties) of all objects. diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js index 9c5c907ce3..75d7ead61a 100644 --- a/js/src/builtin/intl/DateTimeFormat.js +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -128,6 +128,91 @@ var dateTimeComponentValues = { var dateTimeComponents = std_Object_getOwnPropertyNames(dateTimeComponentValues); +var timeZoneCache = { + icuDefaultTimeZone: undefined, + defaultTimeZone: undefined, +}; + + +/** + * 6.4.2 CanonicalizeTimeZoneName ( timeZone ) + * + * Canonicalizes the given IANA time zone name. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function CanonicalizeTimeZoneName(timeZone) { + assert(typeof timeZone === "string", "CanonicalizeTimeZoneName"); + + // Step 1. (Not applicable, the input is already a valid IANA time zone.) + assert(timeZone !== "Etc/Unknown", "Invalid time zone"); + assert(timeZone === intl_IsValidTimeZoneName(timeZone), "Time zone name not normalized"); + + // Step 2. + var ianaTimeZone = intl_canonicalizeTimeZone(timeZone); + assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone"); + assert(ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone), "Unsupported canonical time zone"); + + // Step 3. + if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") { + // ICU/CLDR canonicalizes Etc/UCT to Etc/GMT, but following IANA and + // ECMA-402 to the letter means Etc/UCT is a separate time zone. + if (timeZone === "Etc/UCT" || timeZone === "UCT") + ianaTimeZone = "Etc/UCT"; + else + ianaTimeZone = "UTC"; + } + + // Step 4. + return ianaTimeZone; +} + + +/** + * 6.4.3 DefaultTimeZone () + * + * Returns the IANA time zone name for the host environment's current time zone. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function DefaultTimeZone() { + const icuDefaultTimeZone = intl_defaultTimeZone(); + if (timeZoneCache.icuDefaultTimeZone === icuDefaultTimeZone) + return timeZoneCache.defaultTimeZone; + + // Verify that the current ICU time zone is a valid ECMA-402 time zone. + var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone); + if (timeZone === null) { + // Before defaulting to "UTC", try to represent the default time zone + // using the Etc/GMT + offset format. This format only accepts full + // hour offsets. + const msPerHour = 60 * 60 * 1000; + var offset = intl_defaultTimeZoneOffset(); + assert(offset === (offset | 0), + "milliseconds offset shouldn't be able to exceed int32_t range"); + var offsetHours = offset / msPerHour, offsetHoursFraction = offset % msPerHour; + if (offsetHoursFraction === 0) { + // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset + // means a location west of GMT. + timeZone = "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours); + + // Check if the fallback is valid. + timeZone = intl_IsValidTimeZoneName(timeZone); + } + + // Fallback to "UTC" if everything else fails. + if (timeZone === null) + timeZone = "UTC"; + } + + // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC. + var defaultTimeZone = CanonicalizeTimeZoneName(timeZone); + + timeZoneCache.defaultTimeZone = defaultTimeZone; + timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone; + + return defaultTimeZone; +} /** * Initializes an object as a DateTimeFormat. diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js index a65ba20885..2ef1661586 100644 --- a/js/src/builtin/intl/NumberFormat.js +++ b/js/src/builtin/intl/NumberFormat.js @@ -149,6 +149,44 @@ function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault) { } } +/** + * Convert s to upper case, but limited to characters a-z. + * + * Spec: ECMAScript Internationalization API Specification, 6.1. + */ +function toASCIIUpperCase(s) { + assert(typeof s === "string", "toASCIIUpperCase"); + + // String.prototype.toUpperCase may map non-ASCII characters into ASCII, + // so go character by character (actually code unit by code unit, but + // since we only care about ASCII characters here, that's OK). + var result = ""; + for (var i = 0; i < s.length; i++) { + var c = callFunction(std_String_charCodeAt, s, i); + result += (0x61 <= c && c <= 0x7A) + ? callFunction(std_String_fromCharCode, null, c & ~0x20) + : s[i]; + } + return result; +} + +/** + * Verifies that the given string is a well-formed ISO 4217 currency code. + * + * Spec: ECMAScript Internationalization API Specification, 6.3.1. + */ +function getIsWellFormedCurrencyCodeRE() { + return internalIntlRegExps.isWellFormedCurrencyCodeRE || + (internalIntlRegExps.isWellFormedCurrencyCodeRE = RegExpCreate("[^A-Z]")); +} + +function IsWellFormedCurrencyCode(currency) { + var c = ToString(currency); + var normalized = toASCIIUpperCase(c); + if (normalized.length !== 3) + return false; + return !regexp_test_no_statics(getIsWellFormedCurrencyCodeRE(), normalized); +} /** * Initializes an object as a NumberFormat. From 6d3500bb48e6a049c82f7b4f0fc2ad7aa4b96ab2 Mon Sep 17 00:00:00 2001 From: FranklinDM Date: Mon, 20 Feb 2023 11:20:23 +0800 Subject: [PATCH 22/24] Issue #2111 - Implement CSSStyleRule.selectorText setter Unlike the original patch, this copies Chrome behavior: ignores invalid values of selectorText without any console log. Partially based on https://bugzilla.mozilla.org/show_bug.cgi?id=37468 --- layout/style/CSSStyleSheet.cpp | 6 +++++ layout/style/CSSStyleSheet.h | 1 + layout/style/StyleRule.cpp | 40 +++++++++++++++++++++++++++++++--- layout/style/StyleSheet.h | 1 + 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index 40a4ee4d84..4cefdfad95 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -1618,6 +1618,12 @@ CSSStyleSheet::DidDirty() ClearRuleCascades(); } +void +CSSStyleSheet::AssertHasUniqueInner() +{ + MOZ_ASSERT(mInner->mSheets.Length() == 1, "expected unique inner"); +} + nsresult CSSStyleSheet::RegisterNamespaceRule(css::Rule* aRule) { diff --git a/layout/style/CSSStyleSheet.h b/layout/style/CSSStyleSheet.h index 89189d7816..d777b1f652 100644 --- a/layout/style/CSSStyleSheet.h +++ b/layout/style/CSSStyleSheet.h @@ -217,6 +217,7 @@ public: void WillDirty(); void DidDirty(); + void AssertHasUniqueInner(); private: CSSStyleSheet(const CSSStyleSheet& aCopy, diff --git a/layout/style/StyleRule.cpp b/layout/style/StyleRule.cpp index bf5eefa156..8fe19bb34a 100644 --- a/layout/style/StyleRule.cpp +++ b/layout/style/StyleRule.cpp @@ -1571,9 +1571,43 @@ StyleRule::GetSelectorText(nsAString& aSelectorText) void StyleRule::SetSelectorText(const nsAString& aSelectorText) { - // XXX TBI - get a parser and re-parse the selectors, - // XXX then need to re-compute the cascade - // XXX and dirty sheet + CSSStyleSheet* sheet = GetStyleSheet(); + + nsIDocument* doc = GetDocument(); + RefPtr loader; + + if (doc) { + loader = doc->CSSLoader(); + } + + // NOTE: Passing a null loader means that the parser is always in + // standards mode and never in quirks mode. + nsCSSParser css(loader, sheet); + + // StyleRule lives inside of the Inner, it is unsafe to call WillDirty + // if sheet does not already have a unique Inner. + sheet->AssertHasUniqueInner(); + sheet->WillDirty(); + + nsCSSSelectorList* selectorList = nullptr; + + nsresult result = css.ParseSelectorString( + aSelectorText, sheet->GetSheetURI(), 0, &selectorList); + if (NS_FAILED(result)) { + // Ignore parsing errors and continue to use the previous value. + return; + } + + // Replace selector. + delete mSelector; + mSelector = selectorList; + + sheet->DidDirty(); + + if (doc) { + mozAutoDocUpdate updateBatch(doc, UPDATE_STYLE, true); + doc->StyleRuleChanged(sheet, this); + } } /* virtual */ size_t diff --git a/layout/style/StyleSheet.h b/layout/style/StyleSheet.h index 55d1147005..0b4af9f13b 100644 --- a/layout/style/StyleSheet.h +++ b/layout/style/StyleSheet.h @@ -186,6 +186,7 @@ public: // WillDirty and then make no change and skip the DidDirty call. inline void WillDirty(); inline void DidDirty(); + inline void AssertHasUniqueInner(); private: // Get a handle to the various stylesheet bits which live on the 'inner' for From 6dc8a7d8133f06bb9db174ed67d491a521d29adf Mon Sep 17 00:00:00 2001 From: FranklinDM Date: Sat, 18 Feb 2023 14:34:56 +0800 Subject: [PATCH 23/24] Issue #1382 - Annotate crash with database name when storage connection not closed Reported on https://bugzilla.mozilla.org/show_bug.cgi?id=1384036 This was fixed with an unrelated issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1372823 --- storage/mozStorageService.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/storage/mozStorageService.cpp b/storage/mozStorageService.cpp index 8c6f65232c..04be1f8733 100644 --- a/storage/mozStorageService.cpp +++ b/storage/mozStorageService.cpp @@ -27,7 +27,7 @@ #include "sqlite3.h" -#ifdef SQLITE_OS_WIN +#ifdef XP_WIN // "windows.h" was included and it can #define lots of things we care about... #undef CompareString #endif @@ -917,6 +917,12 @@ Service::Observe(nsISupports *, const char *aTopic, const char16_t *) getConnections(connections); for (uint32_t i = 0, n = connections.Length(); i < n; i++) { if (!connections[i]->isClosed()) { +#ifdef DEBUG + nsCString msg; + msg.AppendPrintf("Storage connection to '%s' was not closed.", + connections[i]->getFilename().get()); + NS_ERROR(msg.get()); +#endif MOZ_CRASH(); } } From b7d80962d0c11ae3b9f94fb8946000e5934a4eef Mon Sep 17 00:00:00 2001 From: FranklinDM Date: Wed, 17 Aug 2022 15:17:18 +0800 Subject: [PATCH 24/24] No issue - Improve fallback handling and resolve PATH issues with python clobber command --- python/mozbuild/mozbuild/mach_commands.py | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/python/mozbuild/mozbuild/mach_commands.py b/python/mozbuild/mozbuild/mach_commands.py index 6a7b28810a..a99ad6d47d 100644 --- a/python/mozbuild/mozbuild/mach_commands.py +++ b/python/mozbuild/mozbuild/mach_commands.py @@ -710,15 +710,36 @@ class Clobber(MachCommandBase): raise if 'python' in what: + # TODO: Once we move to Python 3, we should probably use + # shutil.which to get the fully qualified path for these commands. + cmd = '' if os.path.isdir(mozpath.join(self.topsrcdir, '.hg')): cmd = ['hg', 'purge', '--all', '-I', 'glob:**.py[co]'] elif os.path.isdir(mozpath.join(self.topsrcdir, '.git')): cmd = ['git', 'clean', '-f', '-x', '*.py[co]'] + + if not cmd: + ret = self.clobber_python_fallback() else: - cmd = ['find', '.', '-type', 'f', '-name', '*.py[co]', '-delete'] - ret = subprocess.call(cmd, cwd=self.topsrcdir) + # It is possible that git or hg is either not installed or + # excluded from PATH despite the existence of their data + # directories, so use a fallback instead of failing early. + try: + ret = subprocess.call(cmd, cwd=self.topsrcdir) + except OSError as e: + ret = self.clobber_python_fallback() + return ret + def clobber_python_fallback(self): + cmd = ['find', '.', '-type', 'f', '-name', '*.py[co]', '-delete'] + # Execute the command through the shell if we're on Windows to ensure + # that our copy of `find` is run rather than the OS default. + # This is because on Windows, Popen (and by extension, subprocess.call) + # ignores PATH and looks only at the current working directory. + use_shell = sys.platform.startswith('win') + return subprocess.call(cmd, cwd=self.topsrcdir, shell=use_shell) + @CommandProvider class Logs(MachCommandBase): """Provide commands to read mach logs."""