From 85612accf29d1bb59158ce207bda35dcbd634fea Mon Sep 17 00:00:00 2001 From: Martok Date: Thu, 29 Jun 2023 23:02:37 +0200 Subject: [PATCH] Issue #2046 - Introduce mozIntl.DateTimeFormat with mozExtensions Based-on: m-c 1329904 --- js/src/builtin/intl/CommonFunctions.cpp | 5 +- js/src/builtin/intl/CommonFunctions.h | 9 +- js/src/builtin/intl/DateTimeFormat.cpp | 113 ++++++++++++++++++++++-- js/src/builtin/intl/DateTimeFormat.h | 32 ++++++- js/src/builtin/intl/DateTimeFormat.js | 59 ++++++++++++- js/src/builtin/intl/IntlObject.cpp | 3 +- js/src/builtin/intl/NumberFormat.cpp | 3 +- js/src/jsfriendapi.h | 14 +++ js/src/shell/js.cpp | 3 + js/src/vm/SelfHosting.cpp | 1 + 10 files changed, 227 insertions(+), 15 deletions(-) diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp index 4b607b60cd..f83ba0b423 100644 --- a/js/src/builtin/intl/CommonFunctions.cpp +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -42,14 +42,15 @@ js::intl::InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, HandleValue thisValue, HandleValue locales, HandleValue options, - MutableHandleValue result) + DateTimeFormatOptions dtfOptions, MutableHandleValue result) { - FixedInvokeArgs<4> args(cx); + FixedInvokeArgs<5> args(cx); args[0].setObject(*obj); args[1].set(thisValue); args[2].set(locales); args[3].set(options); + args[4].setBoolean(dtfOptions == DateTimeFormatOptions::EnableMozExtensions); RootedValue thisv(cx, NullValue()); if (!js::CallSelfHostedFunction(cx, initializer, thisv, args, result)) diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h index 7d11c4a8f0..db4177a13d 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -38,10 +38,17 @@ InitializeObject(JSContext* cx, HandleObject obj, Handle initiali * self-hosted function. This is only for a few old Intl.* constructors, for * legacy reasons -- new ones should use the function above instead. */ + +enum class DateTimeFormatOptions +{ + Standard, + EnableMozExtensions, +}; + extern bool LegacyIntlInitialize(JSContext* cx, HandleObject obj, Handle initializer, HandleValue thisValue, HandleValue locales, HandleValue options, - MutableHandleValue result); + DateTimeFormatOptions dtfOptions, MutableHandleValue result); /** * Returns the object holding the internal properties for obj. */ diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index 6a5b6584a5..cf0db19dc4 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -33,6 +33,7 @@ using JS::ClippedTime; using JS::TimeClip; using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; @@ -89,7 +90,7 @@ static const JSFunctionSpec dateTimeFormat_methods[] = { * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b */ static bool -DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) +DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct, DateTimeFormatOptions dtfOptions) { // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). @@ -119,14 +120,28 @@ DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) // Step 3. return intl::LegacyIntlInitialize(cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, - thisValue, locales, options, args.rval()); + thisValue, locales, options, dtfOptions, args.rval()); } static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - return DateTimeFormat(cx, args, args.isConstructing()); + return DateTimeFormat(cx, args, args.isConstructing(), DateTimeFormatOptions::Standard); +} + +static bool +MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Don't allow to call mozIntl.DateTimeFormat as a function. That way we + // don't need to worry how to handle the legacy initialization semantics + // when applied on mozIntl.DateTimeFormat. + if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) + return false; + + return DateTimeFormat(cx, args, true, DateTimeFormatOptions::EnableMozExtensions); } bool @@ -138,7 +153,7 @@ js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) // 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); + return DateTimeFormat(cx, args, true, DateTimeFormatOptions::Standard); } void @@ -153,10 +168,12 @@ js::DateTimeFormatObject::finalize(FreeOp* fop, JSObject* obj) JSObject* js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, - MutableHandleObject constructor) + MutableHandleObject constructor, DateTimeFormatOptions dtfOptions) { RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); + ctor = dtfOptions == DateTimeFormatOptions::EnableMozExtensions + ? GlobalObject::createConstructor(cx, MozDateTimeFormat, cx->names().DateTimeFormat, 0) + : GlobalObject::createConstructor(cx, DateTimeFormat, cx->names().DateTimeFormat, 0); if (!ctor) return nullptr; @@ -200,6 +217,17 @@ js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle intl) +{ + Handle global = cx->global(); + + RootedObject mozDateTimeFormat(cx); + JSObject* mozDateTimeFormatProto = + CreateDateTimeFormatPrototype(cx, intl, global, &mozDateTimeFormat, DateTimeFormatOptions::EnableMozExtensions); + return mozDateTimeFormatProto != nullptr; +} + bool js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) { @@ -445,6 +473,79 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) return true; } +bool +js::intl_patternForStyle(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 4); + MOZ_ASSERT(args[0].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + UDateFormatStyle dateStyle = UDAT_NONE; + UDateFormatStyle timeStyle = UDAT_NONE; + + if (args[1].isString()) { + JSLinearString* dateStyleStr = args[1].toString()->ensureLinear(cx); + if (!dateStyleStr) + return false; + + if (StringEqualsAscii(dateStyleStr, "full")) + dateStyle = UDAT_FULL; + else if (StringEqualsAscii(dateStyleStr, "long")) + dateStyle = UDAT_LONG; + else if (StringEqualsAscii(dateStyleStr, "medium")) + dateStyle = UDAT_MEDIUM; + else if (StringEqualsAscii(dateStyleStr, "short")) + dateStyle = UDAT_SHORT; + else + MOZ_ASSERT_UNREACHABLE("unexpected dateStyle"); + } + + if (args[2].isString()) { + JSLinearString* timeStyleStr = args[2].toString()->ensureLinear(cx); + if (!timeStyleStr) + return false; + + if (StringEqualsAscii(timeStyleStr, "full")) + timeStyle = UDAT_FULL; + else if (StringEqualsAscii(timeStyleStr, "long")) + timeStyle = UDAT_LONG; + else if (StringEqualsAscii(timeStyleStr, "medium")) + timeStyle = UDAT_MEDIUM; + else if (StringEqualsAscii(timeStyleStr, "short")) + timeStyle = UDAT_SHORT; + else + MOZ_ASSERT_UNREACHABLE("unexpected timeStyle"); + } + + AutoStableStringChars timeZone(cx); + if (!timeZone.initTwoByte(cx, args[3].toString())) + return false; + + mozilla::Range timeZoneChars = timeZone.twoByteRange(); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = udat_open(timeStyle, dateStyle, IcuLocale(locale.ptr()), + Char16ToUChar(timeZoneChars.begin().get()), + timeZoneChars.length(), nullptr, -1, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject toClose(df); + + JSString* str = CallICU(cx, [df](UChar* chars, uint32_t size, UErrorCode* status) { + return udat_toPattern(df, false, 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. diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h index 26d46b77b6..b84cdd446c 100644 --- a/js/src/builtin/intl/DateTimeFormat.h +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -41,7 +41,8 @@ class DateTimeFormatObject : public NativeObject extern JSObject* CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, - JS::Handle global, MutableHandleObject constructor); + JS::Handle global, MutableHandleObject constructor, + intl::DateTimeFormatOptions dtfOptions); /** * Returns a new instance of the standard built-in DateTimeFormat constructor. @@ -124,6 +125,35 @@ intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_patternForSkeleton(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 style for the given locale. + * The function takes four arguments: + * + * locale + * BCP47 compliant locale string + * dateStyle + * A string with values: full or long or medium or short, or `undefined` + * timeStyle + * A string with values: full or long or medium or short, or `undefined` + * timeZone + * IANA time zone name + * + * Date and time style categories map to CLDR time/date standard + * format patterns. + * + * For the definition of a pattern string, see LDML 4.8: + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + * + * If `undefined` is passed to `dateStyle` or `timeStyle`, the respective + * portions of the pattern will not be included in the result. + * + * Usage: pattern = intl_patternForStyle(locale, dateStyle, timeStyle, timeZone) + */ +extern MOZ_MUST_USE bool +intl_patternForStyle(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 diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js index 388b6a1415..443fcdb1fd 100644 --- a/js/src/builtin/intl/DateTimeFormat.js +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -32,6 +32,18 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { // } // // formatMatcher: "basic" / "best fit", + // + // mozExtensions: true / false, + // + // + // // If mozExtensions is true: + // + // dateStyle: "full" / "long" / "medium" / "short" / undefined, + // + // timeStyle: "full" / "long" / "medium" / "short" / undefined, + // + // patternOption: + // String representing LDML Date Format pattern or undefined // } // // Note that lazy data is only installed as a final step of initialization, @@ -75,7 +87,26 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { var formatOpt = lazyDateTimeFormatData.formatOpt; // Steps 27-28, more or less - see comment after this function. - var pattern = toBestICUPattern(dataLocale, formatOpt); + var pattern; + if (lazyDateTimeFormatData.mozExtensions) { + if (lazyDateTimeFormatData.patternOption !== undefined) { + pattern = lazyDateTimeFormatData.patternOption; + + internalProps.patternOption = lazyDateTimeFormatData.patternOption; + } else if (lazyDateTimeFormatData.dateStyle || lazyDateTimeFormatData.timeStyle) { + pattern = intl_patternForStyle(dataLocale, + lazyDateTimeFormatData.dateStyle, lazyDateTimeFormatData.timeStyle, + lazyDateTimeFormatData.timeZone); + + internalProps.dateStyle = lazyDateTimeFormatData.dateStyle; + internalProps.timeStyle = lazyDateTimeFormatData.timeStyle; + } else { + pattern = toBestICUPattern(dataLocale, formatOpt); + } + internalProps.mozExtensions = true; + } else { + pattern = toBestICUPattern(dataLocale, formatOpt); + } // Step 29. internalProps.pattern = pattern; @@ -250,7 +281,7 @@ function UnwrapDateTimeFormat(dtf, methodName) { * * Spec: ECMAScript Internationalization API Specification, 12.1.1. */ -function InitializeDateTimeFormat(dateTimeFormat, thisValue, locales, options) { +function InitializeDateTimeFormat(dateTimeFormat, thisValue, locales, options, mozExtensions) { assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat called with non-Object"); assert(IsDateTimeFormat(dateTimeFormat), "InitializeDateTimeFormat called with non-DateTimeFormat"); @@ -324,6 +355,18 @@ function InitializeDateTimeFormat(dateTimeFormat, thisValue, locales, options) { var formatOpt = new Record(); lazyDateTimeFormatData.formatOpt = formatOpt; + lazyDateTimeFormatData.mozExtensions = mozExtensions; + + if (mozExtensions) { + let pattern = GetOption(options, "pattern", "string", undefined, undefined); + lazyDateTimeFormatData.patternOption = pattern; + + let dateStyle = GetOption(options, "dateStyle", "string", ["full", "long", "medium", "short"], undefined); + lazyDateTimeFormatData.dateStyle = dateStyle; + let timeStyle = GetOption(options, "timeStyle", "string", ["full", "long", "medium", "short"], undefined); + lazyDateTimeFormatData.timeStyle = timeStyle; + } + // Step 19. var i, prop; for (i = 0; i < dateTimeComponents.length; i++) { @@ -832,8 +875,18 @@ function Intl_DateTimeFormat_resolvedOptions() { locale: internals.locale, calendar: internals.calendar, numberingSystem: internals.numberingSystem, - timeZone: internals.timeZone + timeZone: internals.timeZone, }; + + if (internals.mozExtensions) { + if (internals.patternOption !== undefined) { + result.pattern = internals.pattern; + } else if (internals.dateStyle || internals.timeStyle) { + result.dateStyle = internals.dateStyle; + result.timeStyle = internals.timeStyle; + } + } + resolveICUPattern(internals.pattern, result); return result; } diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp index 893eb5ce40..c415079ae3 100644 --- a/js/src/builtin/intl/IntlObject.cpp +++ b/js/src/builtin/intl/IntlObject.cpp @@ -30,6 +30,7 @@ using namespace js; using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; @@ -455,7 +456,7 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) if (!collatorProto) return false; RootedObject dateTimeFormatProto(cx), dateTimeFormat(cx); - dateTimeFormatProto = CreateDateTimeFormatPrototype(cx, intl, global, &dateTimeFormat); + dateTimeFormatProto = CreateDateTimeFormatPrototype(cx, intl, global, &dateTimeFormat, DateTimeFormatOptions::Standard); if (!dateTimeFormatProto) return false; RootedObject numberFormatProto(cx), numberFormat(cx); diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp index 52c5747c99..b921fd780e 100644 --- a/js/src/builtin/intl/NumberFormat.cpp +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -35,6 +35,7 @@ using mozilla::IsFinite; using mozilla::IsNaN; using mozilla::IsNegativeZero; using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; @@ -119,7 +120,7 @@ NumberFormat(JSContext* cx, const CallArgs& args, bool construct) // Step 3. return intl::LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, - locales, options, args.rval()); + locales, options, DateTimeFormatOptions::Standard, args.rval()); } static bool diff --git a/js/src/jsfriendapi.h b/js/src/jsfriendapi.h index 71c9deb94e..6a13d4343c 100644 --- a/js/src/jsfriendapi.h +++ b/js/src/jsfriendapi.h @@ -2862,6 +2862,20 @@ ToWindowProxyIfWindow(JSObject* obj); extern JS_FRIEND_API(JSObject*) ToWindowIfWindowProxy(JSObject* obj); +/* + * This custom date/time formatter constructor gives users the ability + * to specify a custom format pattern. This pattern is passed *directly* + * to ICU with NO SYNTAX PARSING OR VALIDATION WHATSOEVER. ICU appears to + * have a a modicum of testing of this, and it won't fall over completely + * if passed bad input. But the current behavior is entirely under-specified + * and emphatically not shippable on the web, and it *must* be fixed before + * this functionality can be exposed in the real world. (There are also some + * questions about whether the format exposed here is the *right* one to + * standardize, that will also need to be resolved to ship this.) + */ +extern bool +AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl); + } /* namespace js */ class NativeProfiler diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index eaac23b537..a77e128a52 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -927,6 +927,9 @@ AddIntlExtras(JSContext* cx, unsigned argc, Value* vp) if (!JS_DefineFunctions(cx, intl, funcs)) return false; + if (!js::AddMozDateTimeFormatConstructor(cx, intl)) + return false; + args.rval().setUndefined(); return true; } diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 98e29b25a4..e4695332cf 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2480,6 +2480,7 @@ 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_patternForStyle", intl_patternForStyle, 3,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),