From fc1f95edc4ad25a7d0b4d42eaa374459c5609448 Mon Sep 17 00:00:00 2001 From: Moonchild Date: Mon, 21 Dec 2020 16:24:37 +0000 Subject: [PATCH 1/2] Issue #1700 - Apply background color instead of inset shadow for findbar input. This changes the toolkit theme for Linux and Windows (Mac already did this) to style the input textbox on the findbar with -moz-appearance:none so as to give us styling control (as opposed to using a native widget style) and apply a neutral style to it. Also ensures Mac remains readable in case of poorly contrasting text. --- toolkit/themes/linux/global/findBar.css | 15 ++++++++++++--- toolkit/themes/osx/global/findBar.css | 1 + toolkit/themes/windows/global/findBar.css | 16 +++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/toolkit/themes/linux/global/findBar.css b/toolkit/themes/linux/global/findBar.css index e3e2ad0865..4a9df23aec 100644 --- a/toolkit/themes/linux/global/findBar.css +++ b/toolkit/themes/linux/global/findBar.css @@ -5,9 +5,9 @@ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); findbar { + padding: 4px 8px; border-top: 2px solid; -moz-border-top-colors: ThreeDShadow ThreeDHighlight; - padding-bottom: 1px; min-width: 1px; transition-property: margin-bottom, opacity, visibility; transition-duration: 150ms, 150ms, 0s; @@ -73,13 +73,22 @@ findbar[hidden] { list-style-image: url("chrome://global/skin/icons/loading_16.png"); } +.findbar-textbox { + -moz-appearance: none; + border: 1px solid ThreeDShadow; + box-shadow: 0 0 1px 0 ThreeDShadow inset; + margin: 0; + padding: 5px; + width: 14em; +} + .findbar-textbox[status="notfound"] { - box-shadow: 0 0 0 1em #f66 inset; + background-color: #f66; color: white; } .findbar-textbox[flash="true"] { - box-shadow: 0 0 0 1em yellow inset; + background-color: yellow; color: black; } diff --git a/toolkit/themes/osx/global/findBar.css b/toolkit/themes/osx/global/findBar.css index 44983d80fa..869a624322 100644 --- a/toolkit/themes/osx/global/findBar.css +++ b/toolkit/themes/osx/global/findBar.css @@ -196,6 +196,7 @@ label.findbar-find-fast:-moz-lwtheme, .findbar-textbox[flash="true"] { background-color: #F7E379; + color: #000; } .findbar-textbox[status="notfound"] { diff --git a/toolkit/themes/windows/global/findBar.css b/toolkit/themes/windows/global/findBar.css index 34b3ae49b8..ecb1b2db13 100644 --- a/toolkit/themes/windows/global/findBar.css +++ b/toolkit/themes/windows/global/findBar.css @@ -15,7 +15,8 @@ } findbar { - padding-top: 1px; + padding: 4px 2px; + box-shadow: 0 1px 1px rgba(0,0,0,.1) inset; background-image: linear-gradient(rgba(0,0,0,.15) 1px, rgba(255,255,255,.15) 1px); background-size: 100% 2px; background-repeat: no-repeat; @@ -108,13 +109,22 @@ findbar[hidden] { list-style-image: url("chrome://global/skin/icons/information-16.png"); } +.findbar-textbox { + -moz-appearance: none; + border: 1px solid ThreeDShadow; + border-radius: 2px; + margin: 0; + padding: 1px 5px; + width: 14em; +} + .findbar-textbox[status="notfound"] { - box-shadow: 0 0 0 1em #f66 inset; + background-color: #f66; color: white; } .findbar-textbox[flash="true"] { - box-shadow: 0 0 0 1em yellow inset; + background-color: yellow; color: black; } From aa38ed97fa5e7f7930167933ff8a7727dbf53624 Mon Sep 17 00:00:00 2001 From: Moonchild Date: Tue, 22 Dec 2020 20:32:56 +0000 Subject: [PATCH 2/2] Issue #1701 - Implement Intl.PluralRules API --- config/check_spidermonkey_style.py | 2 + js/public/Class.h | 2 +- js/src/builtin/Intl.cpp | 486 ++++++++++++++++++++++++++++- js/src/builtin/Intl.h | 48 +++ js/src/builtin/Intl.js | 351 ++++++++++++++++++--- js/src/vm/CommonPropertyNames.h | 3 + js/src/vm/GlobalObject.h | 6 + js/src/vm/SelfHosting.cpp | 3 + 8 files changed, 834 insertions(+), 67 deletions(-) diff --git a/config/check_spidermonkey_style.py b/config/check_spidermonkey_style.py index cc8695cee7..4667a1a2ff 100644 --- a/config/check_spidermonkey_style.py +++ b/config/check_spidermonkey_style.py @@ -79,6 +79,7 @@ included_inclnames_to_ignore = set([ 'prtypes.h', # NSPR 'selfhosted.out.h', # generated in $OBJDIR 'shellmoduleloader.out.h', # generated in $OBJDIR + 'unicode/plurrule.h', # ICU 'unicode/timezone.h', # ICU 'unicode/ucal.h', # ICU 'unicode/uclean.h', # ICU @@ -90,6 +91,7 @@ included_inclnames_to_ignore = set([ 'unicode/unorm.h', # ICU 'unicode/unum.h', # ICU 'unicode/unumsys.h', # ICU + 'unicode/upluralrules.h', # ICU 'unicode/ustring.h', # ICU 'unicode/utypes.h', # ICU 'vtune/VTuneWrapper.h' # VTune diff --git a/js/public/Class.h b/js/public/Class.h index 9cb3778920..094e4a7ed7 100644 --- a/js/public/Class.h +++ b/js/public/Class.h @@ -778,7 +778,7 @@ struct JSClass { // application. #define JSCLASS_GLOBAL_APPLICATION_SLOTS 5 #define JSCLASS_GLOBAL_SLOT_COUNT \ - (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 45) + (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 46) #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \ (JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n))) #define JSCLASS_GLOBAL_FLAGS \ diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index d231d7bec4..622e773e0f 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -10,6 +10,7 @@ #include "builtin/Intl.h" +#include "mozilla/Casting.h" #include "mozilla/PodOperations.h" #include "mozilla/Range.h" #include "mozilla/ScopeExit.h" @@ -22,6 +23,7 @@ #include "jsobj.h" #include "builtin/IntlTimeZoneData.h" +#include "unicode/plurrule.h" #include "unicode/ucal.h" #include "unicode/ucol.h" #include "unicode/udat.h" @@ -29,6 +31,7 @@ #include "unicode/uenum.h" #include "unicode/unum.h" #include "unicode/unumsys.h" +#include "unicode/upluralrules.h" #include "unicode/ustring.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -43,6 +46,7 @@ using namespace js; +using mozilla::AssertedCast; using mozilla::IsFinite; using mozilla::IsNegativeZero; using mozilla::MakeScopeExit; @@ -962,6 +966,89 @@ js::intl_numberingSystem(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, GetInternals(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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + 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(); +} + + /** * Returns a new UNumberFormat with the locale and number formatting options * of the given NumberFormat. @@ -1044,35 +1131,25 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) if (hasP) { if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, &value)) - { return nullptr; - } - uMinimumSignificantDigits = int32_t(value.toNumber()); + uMinimumSignificantDigits = value.toInt32(); if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, &value)) - { return nullptr; - } - uMaximumSignificantDigits = int32_t(value.toNumber()); + uMaximumSignificantDigits = value.toInt32(); } else { if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, &value)) - { return nullptr; - } - uMinimumIntegerDigits = int32_t(value.toNumber()); + uMinimumIntegerDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, &value)) - { return nullptr; - } - uMinimumFractionDigits = int32_t(value.toNumber()); + uMinimumFractionDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, &value)) - { return nullptr; - } - uMaximumFractionDigits = int32_t(value.toNumber()); + uMaximumFractionDigits = AssertedCast(value.toInt32()); } if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) @@ -2323,6 +2400,381 @@ js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) return true; } +/**************** 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 (!IntlInitialize(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 (!CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!IntlInitialize(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 (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +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, GetInternals(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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + ScopedICUObject closeUFormattable(fmt); + + double y = ufmt_getDouble(fmt, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + UPluralType category; + + if (equal(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(equal(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType(icuLocale(locale.ptr()), category, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + 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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* str = NewStringCopyN(cx, chars.begin(), size); + 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 (equal(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(equal(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType( + icuLocale(locale.ptr()), + category, + &status + ); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + 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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + 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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + 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; +} + bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) { @@ -2761,6 +3213,9 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); if (!numberFormatProto) return false; + RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global)); + if (!pluralRulesProto) + return false; // The |Intl| object is fully set up now, so define the global property. RootedValue intlValue(cx, ObjectValue(*intl)); @@ -2782,6 +3237,7 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto)); global->setReservedSlot(DATE_TIME_FORMAT_PROTO, ObjectValue(*dateTimeFormatProto)); global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto)); + global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto)); // Also cache |Intl| to implement spec language that conditions behavior // based on values being equal to "the standard built-in |Intl| object". diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 5384d9be11..fd1fc5da6a 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -358,6 +358,54 @@ intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); +/******************** 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); + /** * 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.js b/js/src/builtin/Intl.js index 6391c3e706..281b0f4243 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -21,6 +21,9 @@ intl_availableCalendars: false, intl_patternForSkeleton: false, intl_FormatDateTime: false, + intl_SelectPluralRule: false, + intl_GetPluralCategories: false, + intl_GetCalendarInfo: false, */ /* @@ -857,6 +860,7 @@ function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { return BestAvailableLocaleHelper(availableLocales, locale, false); } +var noRelevantExtensionKeys = []; /** * Compares a BCP 47 language priority list against the set of locales in @@ -1270,7 +1274,9 @@ function initializeIntlObject(obj) { function setLazyData(internals, type, lazyData) { assert(internals.type === "partial", "can't set lazy data for anything but a newborn"); - assert(type === "Collator" || type === "DateTimeFormat" || type == "NumberFormat", "bad type"); + assert(type === "Collator" || type === "DateTimeFormat" || + type == "NumberFormat" || type === "PluralRules", + "bad type"); assert(IsObject(lazyData), "non-object lazy data"); // Set in reverse order so that the .type change is a barrier. @@ -1320,7 +1326,9 @@ function isInitializedIntlObject(obj) { if (IsObject(internals)) { assert(callFunction(std_Object_hasOwnProperty, internals, "type"), "missing type"); var type = internals.type; - assert(type === "partial" || type === "Collator" || type === "DateTimeFormat" || type === "NumberFormat", "unexpected type"); + assert(type === "partial" || type === "Collator" || + type === "DateTimeFormat" || type === "NumberFormat" || type === "PluralRules", + "unexpected type"); assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData"); assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps"); } else { @@ -1377,6 +1385,8 @@ function getInternals(obj) internalProps = resolveCollatorInternals(lazyData) else if (type === "DateTimeFormat") internalProps = resolveDateTimeFormatInternals(lazyData) + else if (type === "PluralRules") + internalProps = resolvePluralRulesInternals(lazyData) else internalProps = resolveNumberFormatInternals(lazyData); setInternalProperties(internals, internalProps); @@ -1776,45 +1786,39 @@ function resolveNumberFormatInternals(lazyNumberFormatData) { // Step 6. var opt = lazyNumberFormatData.opt; - // Compute effective locale. - // Step 9. var NumberFormat = numberFormatInternalProperties; - // Step 10. + // Step 9. var localeData = NumberFormat.localeData; - // Step 11. + // Step 10. var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), lazyNumberFormatData.requestedLocales, lazyNumberFormatData.opt, NumberFormat.relevantExtensionKeys, localeData); - // Steps 12-13. (Step 14 is not relevant to our implementation.) + // Steps 11-12. (Step 13 is not relevant to our implementation.) internalProps.locale = r.locale; internalProps.numberingSystem = r.nu; // Compute formatting options. - // Step 16. + // Step 15. var s = lazyNumberFormatData.style; internalProps.style = s; - // Steps 20, 22. + // Steps 19, 21. if (s === "currency") { internalProps.currency = lazyNumberFormatData.currency; internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; } - // Step 24. internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; - // Steps 27. internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; - // Step 30. internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; - // Step 33. if ("minimumSignificantDigits" in lazyNumberFormatData) { // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the // actual presence (versus undefined-ness) of these properties. @@ -1823,10 +1827,10 @@ function resolveNumberFormatInternals(lazyNumberFormatData) { internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; } - // Step 35. + // Step 27. internalProps.useGrouping = lazyNumberFormatData.useGrouping; - // Step 42. + // Step 34. internalProps.boundFormat = undefined; // The caller is responsible for associating |internalProps| with the right @@ -1854,6 +1858,41 @@ function getNumberFormatInternals(obj, methodName) { 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. @@ -1903,7 +1942,7 @@ function InitializeNumberFormat(numberFormat, locales, options) { // } // // 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 + // so every NumberFormat lazy data object has *all* these properties, never a // subset of them. var lazyNumberFormatData = std_Object_create(null); @@ -1933,11 +1972,11 @@ function InitializeNumberFormat(numberFormat, locales, options) { opt.localeMatcher = matcher; // Compute formatting options. - // Step 15. + // Step 14. var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); lazyNumberFormatData.style = s; - // Steps 17-20. + // Steps 16-19. var c = GetOption(options, "currency", "string", undefined, undefined); if (c !== undefined && !IsWellFormedCurrencyCode(c)) ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); @@ -1946,54 +1985,36 @@ function InitializeNumberFormat(numberFormat, locales, options) { if (c === undefined) ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); - // Steps 20.a-c. + // Steps 19.a-c. c = toASCIIUpperCase(c); lazyNumberFormatData.currency = c; cDigits = CurrencyDigits(c); } - // Step 21. + // Step 20. var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); if (s === "currency") lazyNumberFormatData.currencyDisplay = cd; - // Step 23. - var mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); - lazyNumberFormatData.minimumIntegerDigits = mnid; + // Steps 22-24. + SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); - // Steps 25-26. - var mnfdDefault = (s === "currency") ? cDigits : 0; - var mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault); - lazyNumberFormatData.minimumFractionDigits = mnfd; - - // Steps 28-29. - var mxfdDefault; - if (s === "currency") - mxfdDefault = std_Math_max(mnfd, cDigits); - else if (s === "percent") - mxfdDefault = std_Math_max(mnfd, 0); - else - mxfdDefault = std_Math_max(mnfd, 3); - var mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20, mxfdDefault); - lazyNumberFormatData.maximumFractionDigits = mxfd; - - // Steps 31-32. - var mnsd = options.minimumSignificantDigits; - var mxsd = options.maximumSignificantDigits; - - // Step 33. - if (mnsd !== undefined || mxsd !== undefined) { - mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1); - mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21); - lazyNumberFormatData.minimumSignificantDigits = mnsd; - lazyNumberFormatData.maximumSignificantDigits = mxsd; + // Step 25. + if (lazyNumberFormatData.maximumFractionDigits === undefined) { + let mxfdDefault = s === "currency" + ? cDigits + : s === "percent" + ? 0 + : 3; + lazyNumberFormatData.maximumFractionDigits = + std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); } - // Step 34. + // Step 26. var g = GetOption(options, "useGrouping", "boolean", undefined, true); lazyNumberFormatData.useGrouping = g; - // Step 43. + // Steps 35-36. // // We've done everything that must be done now: mark the lazy data as fully // computed and install it. @@ -2990,6 +3011,234 @@ 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; +} + + function Intl_getCanonicalLocales(locales) { let codes = CanonicalizeLocaleList(locales); let result = []; diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 789a573dcc..a88406bc61 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -178,6 +178,7 @@ macro(InitializeCollator, InitializeCollator, "InitializeCollator") \ macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \ macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \ + macro(InitializePluralRules, InitializePluralRules, "InitializePluralRules") \ macro(innermost, innermost, "innermost") \ macro(inNursery, inNursery, "inNursery") \ macro(input, input, "input") \ @@ -270,6 +271,8 @@ macro(parseInt, parseInt, "parseInt") \ macro(pattern, pattern, "pattern") \ macro(pending, pending, "pending") \ + macro(PluralRules, PluralRules, "PluralRules") \ + macro(PluralRulesSelect, PluralRulesSelect, "Intl_PluralRules_Select") \ macro(public, public_, "public") \ macro(preventExtensions, preventExtensions, "preventExtensions") \ macro(private, private_, "private") \ diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index 011f90aa16..f9c0149f12 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -109,6 +109,7 @@ class GlobalObject : public NativeObject COLLATOR_PROTO, NUMBER_FORMAT_PROTO, DATE_TIME_FORMAT_PROTO, + PLURAL_RULES_PROTO, MODULE_PROTO, IMPORT_ENTRY_PROTO, EXPORT_ENTRY_PROTO, @@ -507,6 +508,11 @@ class GlobalObject : public NativeObject return getOrCreateObject(cx, global, DATE_TIME_FORMAT_PROTO, initIntlObject); } + static JSObject* + getOrCreatePluralRulesPrototype(JSContext* cx, Handle global) { + return getOrCreateObject(cx, global, PLURAL_RULES_PROTO, initIntlObject); + } + static bool ensureModulePrototypesCreated(JSContext *cx, Handle global); JSObject* maybeGetModulePrototype() { diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index dc1dfb9fad..df326a69e7 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2605,6 +2605,9 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0), JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0), + JS_FN("intl_PluralRules_availableLocales", intl_PluralRules_availableLocales, 0,0), + JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2, 0), + JS_FN("intl_SelectPluralRule", intl_SelectPluralRule, 2,0), JS_INLINABLE_FN("IsRegExpObject", intrinsic_IsInstanceOfBuiltin, 1,0,