diff --git a/config/check_spidermonkey_style.py b/config/check_spidermonkey_style.py index cb9e2418f2..5f06e6ad93 100644 --- a/config/check_spidermonkey_style.py +++ b/config/check_spidermonkey_style.py @@ -82,6 +82,7 @@ included_inclnames_to_ignore = set([ 'unicode/plurrule.h', # ICU 'unicode/timezone.h', # ICU 'unicode/ucal.h', # ICU + 'unicode/uchar.h', # ICU 'unicode/uclean.h', # ICU 'unicode/ucol.h', # ICU 'unicode/udat.h', # ICU diff --git a/js/public/Class.h b/js/public/Class.h index d7e2ab40db..f1d7739718 100644 --- a/js/public/Class.h +++ b/js/public/Class.h @@ -913,7 +913,7 @@ struct JSClass { // application. #define JSCLASS_GLOBAL_APPLICATION_SLOTS 5 #define JSCLASS_GLOBAL_SLOT_COUNT \ - (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 47) + (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 50) #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/public/GCVector.h b/js/public/GCVector.h index 4acf0d1fc5..a92969b576 100644 --- a/js/public/GCVector.h +++ b/js/public/GCVector.h @@ -130,6 +130,17 @@ class GCVector } }; +// AllocPolicy is optional. It has a default value declared in TypeDecls.h +template +class MOZ_STACK_CLASS StackGCVector : public GCVector { + public: + using Base = GCVector; + + private: + // Inherit constructor from GCVector. + using Base::Base; +}; + } // namespace JS namespace js { @@ -191,7 +202,7 @@ class MutableWrappedPtrOperations, Wrappe void clearAndFree() { vec().clearAndFree(); } template bool append(U&& aU) { return vec().append(mozilla::Forward(aU)); } template bool emplaceBack(Args&&... aArgs) { - return vec().emplaceBack(mozilla::Forward(aArgs...)); + return vec().emplaceBack(mozilla::Forward(aArgs)...); } template bool appendAll(const mozilla::Vector& aU) { return vec().appendAll(aU); } @@ -223,6 +234,29 @@ class MutableWrappedPtrOperations, Wrappe void erase(T* aBegin, T* aEnd) { vec().erase(aBegin, aEnd); } }; +template +class WrappedPtrOperations, Wrapper> : + public WrappedPtrOperations::Base, + Wrapper> {}; + +template +class MutableWrappedPtrOperations, Wrapper> : + public MutableWrappedPtrOperations::Base, + Wrapper> {}; + } // namespace js +namespace JS { + +// An automatically rooted GCVector for stack use. +template +class RootedVector : public Rooted> { + using Vec = StackGCVector; + using Base = Rooted; + + public: + explicit RootedVector(JSContext* cx) : Base(cx, Vec(cx)) {} +}; + +} // namespace JS #endif // js_GCVector_h diff --git a/js/public/Result.h b/js/public/Result.h new file mode 100644 index 0000000000..631fa366d5 --- /dev/null +++ b/js/public/Result.h @@ -0,0 +1,223 @@ +/* -*- 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/. */ + +/* + * `Result` is used as the return type of many SpiderMonkey functions that + * can either succeed or fail. See "/mfbt/Result.h". + * + * + * ## Which return type to use + * + * `Result` is for return values. Obviously, if you're writing a function that + * can't fail, don't use Result. Otherwise: + * + * JS::Result<> - function can fail, doesn't return anything on success + * (defaults to `JS::Result`) + * JS::Result - like JS::Result<>, but fails only on OOM + * + * JS::Result - function can fail, returns Data on success + * JS::Result - returns Data, fails only on OOM + * + * mozilla::GenericErrorResult - always fails + * + * That last type is like a Result with no success type. It's used for + * functions like `js::ReportNotFunction` that always return an error + * result. `GenericErrorResult` implicitly converts to `Result`, + * regardless of V. + * + * + * ## Checking Results when your return type is Result + * + * When you call a function that returns a `Result`, use the `MOZ_TRY` macro to + * check for errors: + * + * MOZ_TRY(DefenestrateObject(cx, obj)); + * + * If `DefenestrateObject` returns a success result, `MOZ_TRY` is done, and + * control flows to the next statement. If `DefenestrateObject` returns an + * error result, `MOZ_TRY` will immediately return it, propagating the error to + * your caller. It's kind of like exceptions, but more explicit -- you can see + * in the code exactly where errors can happen. + * + * You can do a tail call instead of using `MOZ_TRY`: + * + * return DefenestrateObject(cx, obj); + * + * Indicate success with `return Ok();`. + * + * If the function returns a value on success, use `MOZ_TRY_VAR` to get it: + * + * RootedValue thrug(cx); + * MOZ_TRY_VAR(thrug, GetObjectThrug(cx, obj)); + * + * This behaves the same as `MOZ_TRY` on error. On success, the success + * value of `GetObjectThrug(cx, obj)` is assigned to the variable `thrug`. + * + * + * ## Checking Results when your return type is not Result + * + * This header defines alternatives to MOZ_TRY and MOZ_TRY_VAR for when you + * need to call a `Result` function from a function that uses false or nullptr + * to indicate errors: + * + * JS_TRY_OR_RETURN_FALSE(cx, DefenestrateObject(cx, obj)); + * JS_TRY_VAR_OR_RETURN_FALSE(cx, v, GetObjectThrug(cx, obj)); + * + * JS_TRY_OR_RETURN_NULL(cx, DefenestrateObject(cx, obj)); + * JS_TRY_VAR_OR_RETURN_NULL(cx, v, GetObjectThrug(cx, obj)); + * + * When TRY is not what you want, because you need to do some cleanup or + * recovery on error, use this idiom: + * + * if (!cx->resultToBool(expr_that_is_a_Result)) { + * ... your recovery code here ... + * } + * + * In place of a tail call, you can use one of these methods: + * + * return cx->resultToBool(expr); // false on error + * return cx->resultToPtr(expr); // null on error + * + * Once we are using `Result` everywhere, including in public APIs, all of + * these will go away. + * + * + * ## GC safety + * + * When a function returns a `JS::Result`, it is the program's + * responsibility to check for errors and root the object before continuing: + * + * RootedObject wrapper(cx); + * MOZ_TRY_VAR(wrapper, Enwrapify(cx, thing)); + * + * This is ideal. On error, there is no object to root; on success, the + * assignment to wrapper roots it. GC safety is ensured. + * + * `Result` has methods .isOk(), .isErr(), .unwrap(), and .unwrapErr(), but if + * you're actually using them, it's possible to create a GC hazard. The static + * analysis will catch it if so, but that's hardly convenient. So try to stick + * to the idioms shown above. + * + * + * ## Future directions + * + * At present, JS::Error and JS::OOM are empty structs. The plan is to make them + * GC things that contain the actual error information (including the exception + * value and a saved stack). + * + * The long-term plan is to remove JS_IsExceptionPending and + * JS_GetPendingException in favor of JS::Error. Exception state will no longer + * exist. + */ + +#ifndef js_Result_h +#define js_Result_h + +#include "mozilla/Result.h" + +struct JSContext; + +/** + * Evaluate the boolean expression expr. If it's true, do nothing. + * If it's false, return an error result. + */ +#define JS_TRY_BOOL_TO_RESULT(cx, expr) \ + do { \ + bool ok_ = (expr); \ + if (!ok_) \ + return (cx)->boolToResult(ok_); \ + } while (0) + +/** + * JS_TRY_OR_RETURN_FALSE(cx, expr) runs expr to compute a Result value. + * On success, nothing happens; on error, it returns false immediately. + * + * Implementation note: this involves cx because this may eventually + * do the work of setting a pending exception or reporting OOM. + */ +#define JS_TRY_OR_RETURN_FALSE(cx, expr) \ + do { \ + auto tmpResult_ = (expr); \ + if (tmpResult_.isErr()) \ + return (cx)->resultToBool(tmpResult_); \ + } while (0) + +/** + * Like JS_TRY_OR_RETURN_FALSE, but returning nullptr on error, + * rather than false. + */ +#define JS_TRY_OR_RETURN_NULL(cx, expr) \ + do { \ + auto tmpResult_ = (expr); \ + if (tmpResult_.isErr()) { \ + JS_ALWAYS_FALSE((cx)->resultToBool(tmpResult_)); \ + return nullptr; \ + } \ + } while (0) + +#define JS_TRY_VAR_OR_RETURN_FALSE(cx, target, expr) \ + do { \ + auto tmpResult_ = (expr); \ + if (tmpResult_.isErr()) \ + return (cx)->resultToBool(tmpResult_); \ + (target) = tmpResult_.unwrap(); \ + } while (0) + +#define JS_TRY_VAR_OR_RETURN_NULL(cx, target, expr) \ + do { \ + auto tmpResult_ = (expr); \ + if (tmpResult_.isErr()) { \ + JS_ALWAYS_FALSE((cx)->resultToBool(tmpResult_)); \ + return nullptr; \ + } \ + (target) = tmpResult_.unwrap(); \ + } while (0) + +namespace JS { + +using mozilla::Ok; + +/** + * Type representing a JS error or exception. At the moment this only "represents" + * an error in a rather abstract way. + */ +struct Error +{ + // Ensure sizeof(Error) > 1 so that Result can use pointer + // tagging. + int dummy; +}; + +struct OOM : public Error +{ +}; + +/** + * `Result` is intended to be the return type of JSAPI calls and internal + * functions that can run JS code or allocate memory from the JS GC heap. Such + * functions can: + * + * - succeed, possibly returning a value; + * + * - fail with a JS exception (out-of-memory falls in this category); or + * + * - fail because JS execution was terminated, which occurs when e.g. a + * user kills a script from the "slow script" UI. This is also how we + * unwind the stack when the debugger forces the current function to + * return. JS `catch` blocks can't catch this kind of failure, + * and JS `finally` blocks don't execute. + */ +template +using Result = mozilla::Result; + +static_assert(sizeof(Result<>) == sizeof(uintptr_t), + "Result<> should be pointer-sized"); + +static_assert(sizeof(Result) == sizeof(uintptr_t), + "Result should be pointer-sized"); + +} // namespace JS + +#endif // js_Result_h diff --git a/js/public/TypeDecls.h b/js/public/TypeDecls.h index 40d6f1f8a4..2b36ed95b9 100644 --- a/js/public/TypeDecls.h +++ b/js/public/TypeDecls.h @@ -30,6 +30,10 @@ class JSAddonId; struct jsid; +namespace js { +class TempAllocPolicy; +}; // namespace js + namespace JS { typedef unsigned char Latin1Char; @@ -40,6 +44,8 @@ template class Handle; template class MutableHandle; template class Rooted; template class PersistentRooted; +template class RootedVector; +template class StackGCVector; typedef Handle HandleFunction; typedef Handle HandleId; @@ -48,6 +54,7 @@ typedef Handle HandleScript; typedef Handle HandleString; typedef Handle HandleSymbol; typedef Handle HandleValue; +typedef Handle> HandleValueVector; typedef MutableHandle MutableHandleFunction; typedef MutableHandle MutableHandleId; @@ -56,6 +63,7 @@ typedef MutableHandle MutableHandleScript; typedef MutableHandle MutableHandleString; typedef MutableHandle MutableHandleSymbol; typedef MutableHandle MutableHandleValue; +typedef MutableHandle> MutableHandleValueVector; typedef Rooted RootedObject; typedef Rooted RootedFunction; @@ -65,6 +73,8 @@ typedef Rooted RootedSymbol; typedef Rooted RootedId; typedef Rooted RootedValue; +typedef RootedVector RootedValueVector; + typedef PersistentRooted PersistentRootedFunction; typedef PersistentRooted PersistentRootedId; typedef PersistentRooted PersistentRootedObject; @@ -73,6 +83,11 @@ typedef PersistentRooted PersistentRootedString; typedef PersistentRooted PersistentRootedSymbol; typedef PersistentRooted PersistentRootedValue; + +template +using HandleVector = Handle>; +template +using MutableHandleVector = MutableHandle>; } // namespace JS #endif /* js_TypeDecls_h */ diff --git a/js/src/NamespaceImports.h b/js/src/NamespaceImports.h index 2b7a1f0e8b..a1d8bca1c3 100644 --- a/js/src/NamespaceImports.h +++ b/js/src/NamespaceImports.h @@ -82,10 +82,18 @@ using JS::UTF8CharsZ; using JS::UniqueChars; using JS::UniqueTwoByteChars; +using JS::Result; +using JS::Ok; +using JS::OOM; + using JS::AutoValueVector; using JS::AutoIdVector; using JS::AutoObjectVector; +using JS::RootedValueVector; +using JS::HandleValueVector; +using JS::MutableHandleValueVector; + using JS::ValueVector; using JS::IdVector; using JS::ScriptVector; diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index f97e1df571..54446d2578 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -195,17 +195,42 @@ function ArrayStaticSome(list, callbackfn/*, thisArg*/) { return callFunction(ArraySome, list, callbackfn, T); } -/* ES6 draft 2016-1-15 22.1.3.25 Array.prototype.sort (comparefn) */ +// ES2018 draft rev 3bbc87cd1b9d3bf64c3e68ca2fe9c5a3f2c304c0 +// 22.1.3.25 Array.prototype.sort ( comparefn ) function ArraySort(comparefn) { /* Step 1. */ - var O = ToObject(this); + if (comparefn !== undefined) { + if (!IsCallable(comparefn)) { + ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn)); + } + } /* Step 2. */ + var O = ToObject(this); + + /* Step 3. */ var len = ToLength(O.length); if (len <= 1) return this; + if (comparefn === undefined) { + // {Goanna} This implementation slightly breaks the standard. The default + // comparator function depends on the type of items in the Array + // (Strings, Numbers, etc.) and can be literal, lexicograpic, numeric, + // lexicograpic-number... + // Mozilla implements this correctly only in the native implementation. + // Note that this must be stable regardless of casting, so we can only + // use one of > or <, as the other may involve weird equality. + comparefn = function(x, y) { + /* Step 4.a. */ + if (x == y) + return 0; + if (x > y) + return 1; + return -1; + } + } /* 22.1.3.25.1 Runtime Semantics: SortCompare( x, y ) */ var wrappedCompareFn = comparefn; comparefn = function(x, y) { diff --git a/js/src/builtin/Date.js b/js/src/builtin/Date.js index 6983d26859..534deb0f6f 100644 --- a/js/src/builtin/Date.js +++ b/js/src/builtin/Date.js @@ -104,7 +104,7 @@ function Date_toLocaleString() { } // Step 7. - return intl_FormatDateTime(dateTimeFormat, x, false); + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); } @@ -137,7 +137,7 @@ function Date_toLocaleDateString() { } // Step 7. - return intl_FormatDateTime(dateTimeFormat, x, false); + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); } @@ -170,5 +170,5 @@ function Date_toLocaleTimeString() { } // Step 7. - return intl_FormatDateTime(dateTimeFormat, x, false); + return intl_FormatDateTime(dateTimeFormat, x, /* formatToParts = */ false); } diff --git a/js/src/builtin/RegExp.cpp b/js/src/builtin/RegExp.cpp index 46a2862909..f3d34762f6 100644 --- a/js/src/builtin/RegExp.cpp +++ b/js/src/builtin/RegExp.cpp @@ -974,8 +974,7 @@ IsTrailSurrogateWithLeadSurrogate(JSContext* cx, HandleLinearString input, int32 */ static RegExpRunStatus ExecuteRegExp(JSContext* cx, HandleObject regexp, HandleString string, - int32_t lastIndex, - MatchPairs* matches, size_t* endIndex, RegExpStaticsUpdate staticsUpdate) + int32_t lastIndex, MatchPairs* matches, size_t* endIndex) { /* * WARNING: Despite the presence of spec step comment numbers, this @@ -990,14 +989,9 @@ ExecuteRegExp(JSContext* cx, HandleObject regexp, HandleString string, if (!RegExpObject::getShared(cx, reobj, &re)) return RegExpRunStatus_Error; - RegExpStatics* res; - if (staticsUpdate == UpdateRegExpStatics) { - res = GlobalObject::getRegExpStatics(cx, cx->global()); - if (!res) - return RegExpRunStatus_Error; - } else { - res = nullptr; - } + RegExpStatics* res = GlobalObject::getRegExpStatics(cx, cx->global()); + if (!res) + return RegExpRunStatus_Error; RootedLinearString input(cx, string->ensureLinear(cx)); if (!input) @@ -1051,15 +1045,14 @@ ExecuteRegExp(JSContext* cx, HandleObject regexp, HandleString string, * steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. */ static bool -RegExpMatcherImpl(JSContext* cx, HandleObject regexp, HandleString string, - int32_t lastIndex, RegExpStaticsUpdate staticsUpdate, MutableHandleValue rval) +RegExpMatcherImpl(JSContext* cx, HandleObject regexp, HandleString string, int32_t lastIndex, + MutableHandleValue rval) { /* Execute regular expression and gather matches. */ ScopedMatchPairs matches(&cx->tempLifoAlloc()); /* Steps 3, 9-14, except 12.a.i, 12.c.i.1. */ - RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, - &matches, nullptr, staticsUpdate); + RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, &matches, nullptr); if (status == RegExpRunStatus_Error) return false; @@ -1099,8 +1092,7 @@ js::RegExpMatcher(JSContext* cx, unsigned argc, Value* vp) return false; /* Steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. */ - return RegExpMatcherImpl(cx, regexp, string, lastIndex, - UpdateRegExpStatics, args.rval()); + return RegExpMatcherImpl(cx, regexp, string, lastIndex, args.rval()); } /* @@ -1123,8 +1115,7 @@ js::RegExpMatcherRaw(JSContext* cx, HandleObject regexp, HandleString input, return false; return CreateRegExpMatchResult(cx, *shared, input, *maybeMatches, output); } - return RegExpMatcherImpl(cx, regexp, input, lastIndex, - UpdateRegExpStatics, output); + return RegExpMatcherImpl(cx, regexp, input, lastIndex, output); } /* @@ -1135,14 +1126,13 @@ js::RegExpMatcherRaw(JSContext* cx, HandleObject regexp, HandleString input, */ static bool RegExpSearcherImpl(JSContext* cx, HandleObject regexp, HandleString string, - int32_t lastIndex, RegExpStaticsUpdate staticsUpdate, int32_t* result) + int32_t lastIndex, int32_t* result) { /* Execute regular expression and gather matches. */ ScopedMatchPairs matches(&cx->tempLifoAlloc()); /* Steps 3, 9-14, except 12.a.i, 12.c.i.1. */ - RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, - &matches, nullptr, staticsUpdate); + RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, &matches, nullptr); if (status == RegExpRunStatus_Error) return false; @@ -1180,7 +1170,7 @@ js::RegExpSearcher(JSContext* cx, unsigned argc, Value* vp) /* Steps 3, 9-25, except 12.a.i, 12.c.i.1, 15. */ int32_t result = 0; - if (!RegExpSearcherImpl(cx, regexp, string, lastIndex, UpdateRegExpStatics, &result)) + if (!RegExpSearcherImpl(cx, regexp, string, lastIndex, &result)) return false; args.rval().setInt32(result); @@ -1203,23 +1193,7 @@ js::RegExpSearcherRaw(JSContext* cx, HandleObject regexp, HandleString input, *result = CreateRegExpSearchResult(cx, *maybeMatches); return true; } - return RegExpSearcherImpl(cx, regexp, input, lastIndex, - UpdateRegExpStatics, result); -} - -bool -js::regexp_exec_no_statics(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(IsRegExpObject(args[0])); - MOZ_ASSERT(args[1].isString()); - - RootedObject regexp(cx, &args[0].toObject()); - RootedString string(cx, args[1].toString()); - - return RegExpMatcherImpl(cx, regexp, string, 0, - DontUpdateRegExpStatics, args.rval()); + return RegExpSearcherImpl(cx, regexp, input, lastIndex, result); } /* @@ -1245,8 +1219,7 @@ js::RegExpTester(JSContext* cx, unsigned argc, Value* vp) /* Steps 3, 9-14, except 12.a.i, 12.c.i.1. */ size_t endIndex = 0; - RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, - nullptr, &endIndex, UpdateRegExpStatics); + RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, lastIndex, nullptr, &endIndex); if (status == RegExpRunStatus_Error) return false; @@ -1271,8 +1244,7 @@ js::RegExpTesterRaw(JSContext* cx, HandleObject regexp, HandleString input, MOZ_ASSERT(lastIndex >= 0); size_t endIndexTmp = 0; - RegExpRunStatus status = ExecuteRegExp(cx, regexp, input, lastIndex, - nullptr, &endIndexTmp, UpdateRegExpStatics); + RegExpRunStatus status = ExecuteRegExp(cx, regexp, input, lastIndex, nullptr, &endIndexTmp); if (status == RegExpRunStatus_Success) { MOZ_ASSERT(endIndexTmp <= INT32_MAX); @@ -1287,24 +1259,6 @@ js::RegExpTesterRaw(JSContext* cx, HandleObject regexp, HandleString input, return false; } -bool -js::regexp_test_no_statics(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(IsRegExpObject(args[0])); - MOZ_ASSERT(args[1].isString()); - - RootedObject regexp(cx, &args[0].toObject()); - RootedString string(cx, args[1].toString()); - - size_t ignored = 0; - RegExpRunStatus status = ExecuteRegExp(cx, regexp, string, 0, - nullptr, &ignored, DontUpdateRegExpStatics); - args.rval().setBoolean(status == RegExpRunStatus_Success); - return status != RegExpRunStatus_Error; -} - static void GetParen(JSLinearString* matched, const JS::Value& capture, JSSubString* out) { diff --git a/js/src/builtin/RegExp.h b/js/src/builtin/RegExp.h index f66c9b1b81..c0a7d59f77 100644 --- a/js/src/builtin/RegExp.h +++ b/js/src/builtin/RegExp.h @@ -18,10 +18,6 @@ namespace js { JSObject* InitRegExpClass(JSContext* cx, HandleObject obj); -// Whether RegExp statics should be updated with the input and results of a -// regular expression execution. -enum RegExpStaticsUpdate { UpdateRegExpStatics, DontUpdateRegExpStatics }; - /* * Legacy behavior of ExecuteRegExp(), which is baked into the JSAPI. * @@ -71,22 +67,6 @@ intrinsic_GetStringDataProperty(JSContext* cx, unsigned argc, Value* vp); * The following functions are for use by self-hosted code. */ -/* - * Behaves like regexp.exec(string), but doesn't set RegExp statics. - * - * Usage: match = regexp_exec_no_statics(regexp, string) - */ -extern MOZ_MUST_USE bool -regexp_exec_no_statics(JSContext* cx, unsigned argc, Value* vp); - -/* - * Behaves like regexp.test(string), but doesn't set RegExp statics. - * - * Usage: does_match = regexp_test_no_statics(regexp, string) - */ -extern MOZ_MUST_USE bool -regexp_test_no_statics(JSContext* cx, unsigned argc, Value* vp); - /* * Behaves like RegExp(pattern, flags). * |pattern| should be a RegExp object, |flags| should be a raw integer value. diff --git a/js/src/builtin/String.js b/js/src/builtin/String.js index b0928fe88c..e1c32482ae 100644 --- a/js/src/builtin/String.js +++ b/js/src/builtin/String.js @@ -731,6 +731,88 @@ function String_localeCompare(that) { return intl_CompareStrings(collator, S, That); } +/** + * 13.1.2 String.prototype.toLocaleLowerCase ( [ locales ] ) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +function String_toLocaleLowerCase() { + // Step 1. + RequireObjectCoercible(this); + + // Step 2. + var string = ToString(this); + + // Handle the common cases (no locales argument or a single string + // argument) first. + var locales = arguments.length > 0 ? arguments[0] : undefined; + var requestedLocale; + if (locales === undefined) { + // Steps 3, 6. + requestedLocale = undefined; + } else if (typeof locales === "string") { + // Steps 3, 5. + requestedLocale = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + } else { + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Steps 4-6. + requestedLocale = requestedLocales.length > 0 ? requestedLocales[0] : undefined; + } + + // Trivial case: When the input is empty, directly return the empty string. + if (string.length === 0) + return ""; + + if (requestedLocale === undefined) + requestedLocale = DefaultLocale(); + + // Steps 7-16. + return intl_toLocaleLowerCase(string, requestedLocale); +} + +/** + * 13.1.3 String.prototype.toLocaleUpperCase ( [ locales ] ) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +function String_toLocaleUpperCase() { + // Step 1. + RequireObjectCoercible(this); + + // Step 2. + var string = ToString(this); + + // Handle the common cases (no locales argument or a single string + // argument) first. + var locales = arguments.length > 0 ? arguments[0] : undefined; + var requestedLocale; + if (locales === undefined) { + // Steps 3, 6. + requestedLocale = undefined; + } else if (typeof locales === "string") { + // Steps 3, 5. + requestedLocale = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + } else { + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + + // Steps 4-6. + requestedLocale = requestedLocales.length > 0 ? requestedLocales[0] : undefined; + } + + // Trivial case: When the input is empty, directly return the empty string. + if (string.length === 0) + return ""; + + if (requestedLocale === undefined) + requestedLocale = DefaultLocale(); + + // Steps 7-16. + return intl_toLocaleUpperCase(string, requestedLocale); +} + /* ES6 Draft May 22, 2014 21.1.2.4 */ function String_static_raw(callSite, ...substitutions) { // Step 1 (implicit). @@ -1014,13 +1096,15 @@ _SetCanonicalName(String_static_trimEnd, "trimEnd"); function String_static_toLocaleLowerCase(string) { if (arguments.length < 1) ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toLocaleLowerCase'); - return callFunction(std_String_toLocaleLowerCase, string); + var locales = arguments.length > 1 ? arguments[1] : undefined; + return callFunction(String_toLocaleLowerCase, string, locales); } function String_static_toLocaleUpperCase(string) { if (arguments.length < 1) ThrowTypeError(JSMSG_MISSING_FUN_ARG, 0, 'String.toLocaleUpperCase'); - return callFunction(std_String_toLocaleUpperCase, string); + var locales = arguments.length > 1 ? arguments[1] : undefined; + return callFunction(String_toLocaleUpperCase, string, locales); } function String_static_normalize(string) { diff --git a/js/src/builtin/SymbolObject.cpp b/js/src/builtin/SymbolObject.cpp index effcf4f954..0f1e8164d5 100644 --- a/js/src/builtin/SymbolObject.cpp +++ b/js/src/builtin/SymbolObject.cpp @@ -50,7 +50,7 @@ const JSFunctionSpec SymbolObject::staticMethods[] = { }; JSObject* -SymbolObject::initClass(JSContext* cx, HandleObject obj) +SymbolObject::initClass(JSContext* cx, HandleObject obj, bool defineMembers) { Handle global = obj.as(); @@ -66,25 +66,33 @@ SymbolObject::initClass(JSContext* cx, HandleObject obj) if (!ctor) return nullptr; - // Define the well-known symbol properties, such as Symbol.iterator. - ImmutablePropertyNamePtr* names = cx->names().wellKnownSymbolNames(); - RootedValue value(cx); - unsigned attrs = JSPROP_READONLY | JSPROP_PERMANENT; - WellKnownSymbols* wks = cx->runtime()->wellKnownSymbols; - for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) { - value.setSymbol(wks->get(i)); - if (!NativeDefineProperty(cx, ctor, names[i], value, nullptr, nullptr, attrs)) - return nullptr; + if (defineMembers) { + // Define the well-known symbol properties, such as Symbol.iterator. + ImmutablePropertyNamePtr* names = cx->names().wellKnownSymbolNames(); + RootedValue value(cx); + unsigned attrs = JSPROP_READONLY | JSPROP_PERMANENT; + WellKnownSymbols* wks = cx->runtime()->wellKnownSymbols; + for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) { + value.setSymbol(wks->get(i)); + if (!NativeDefineProperty(cx, ctor, names[i], value, nullptr, nullptr, attrs)) + return nullptr; + } } - if (!LinkConstructorAndPrototype(cx, ctor, proto) || - !DefinePropertiesAndFunctions(cx, proto, properties, methods) || - !DefineToStringTag(cx, proto, cx->names().Symbol) || - !DefinePropertiesAndFunctions(cx, ctor, nullptr, staticMethods) || - !GlobalObject::initBuiltinConstructor(cx, global, JSProto_Symbol, ctor, proto)) - { + if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; + + if (defineMembers) { + if (!DefinePropertiesAndFunctions(cx, proto, properties, methods) || + !DefineToStringTag(cx, proto, cx->names().Symbol) || + !DefinePropertiesAndFunctions(cx, ctor, nullptr, staticMethods)) + { + return nullptr; + } } + + if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_Symbol, ctor, proto)) + return nullptr; return proto; } @@ -258,5 +266,11 @@ SymbolObject::descriptionGetter(JSContext* cx, unsigned argc, Value* vp) JSObject* js::InitSymbolClass(JSContext* cx, HandleObject obj) { - return SymbolObject::initClass(cx, obj); + return SymbolObject::initClass(cx, obj, true); +} + +JSObject* +js::InitBareSymbolCtor(JSContext* cx, HandleObject obj) +{ + return SymbolObject::initClass(cx, obj, false); } diff --git a/js/src/builtin/SymbolObject.h b/js/src/builtin/SymbolObject.h index 01099100b1..54d187fb59 100644 --- a/js/src/builtin/SymbolObject.h +++ b/js/src/builtin/SymbolObject.h @@ -21,7 +21,7 @@ class SymbolObject : public NativeObject static const Class class_; - static JSObject* initClass(JSContext* cx, js::HandleObject obj); + static JSObject* initClass(JSContext* cx, js::HandleObject obj, bool defineMembers); /* * Creates a new Symbol object boxing the given primitive Symbol. The @@ -63,6 +63,9 @@ class SymbolObject : public NativeObject extern JSObject* InitSymbolClass(JSContext* cx, HandleObject obj); +extern JSObject* +InitBareSymbolCtor(JSContext* cx, HandleObject obj); + } /* namespace js */ #endif /* builtin_SymbolObject_h */ diff --git a/js/src/builtin/Utilities.js b/js/src/builtin/Utilities.js index 259fef7ee0..51c5a574fd 100644 --- a/js/src/builtin/Utilities.js +++ b/js/src/builtin/Utilities.js @@ -50,6 +50,8 @@ // Do not create an alias to a self-hosted builtin, otherwise it will be cloned // twice. // +// Symbol is a bare constructor without properties or methods. +var std_Symbol = Symbol; // WeakMap is a bare constructor without properties or methods. var std_WeakMap = WeakMap; // StopIteration is a bare constructor without properties or methods. @@ -78,12 +80,6 @@ MakeConstructible(Record, {}); /********** Abstract operations defined in ECMAScript Language Specification **********/ -/* Spec: ECMAScript Language Specification, 5.1 edition, 8.12.6 and 11.8.7 */ -function HasProperty(o, p) { - return p in o; -} - - /* Spec: ECMAScript Language Specification, 5.1 edition, 9.2 and 11.4.9 */ function ToBoolean(v) { return !!v; diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp index 080974b066..aafb7535c7 100644 --- a/js/src/builtin/intl/Collator.cpp +++ b/js/src/builtin/intl/Collator.cpp @@ -8,12 +8,14 @@ #include "builtin/intl/Collator.h" #include "mozilla/Assertions.h" +#include "mozilla/Span.h" #include "jsapi.h" #include "jscntxt.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/intl/SharedIntlData.h" #include "js/TypeDecls.h" @@ -24,7 +26,6 @@ #include "jsobjinlines.h" using namespace js; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::ReportInternalError; using js::intl::SharedIntlData; @@ -79,64 +80,36 @@ static const JSFunctionSpec collator_methods[] = { * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b */ static bool -Collator(JSContext* cx, const CallArgs& args, bool construct) +Collator(JSContext* cx, const CallArgs& args) { - RootedObject obj(cx); + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - // 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)) + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) return false; - args.rval().setObject(*obj); + if (!proto) { + proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted collator(cx, NewObjectWithGivenProto(cx, proto)); + if (!collator) + return false; + + collator->setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue()); + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 6. + if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, locales, options)) + return false; + + args.rval().setObject(*collator); return true; } @@ -144,7 +117,7 @@ static bool Collator(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - return Collator(cx, args, args.isConstructing()); + return Collator(cx, args); } bool @@ -153,9 +126,8 @@ 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); + + return Collator(cx, args); } void @@ -163,15 +135,9 @@ 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); - } + const Value& slot = obj->as().getReservedSlot(CollatorObject::UCOLLATOR_SLOT); + if (UCollator* coll = static_cast(slot.toPrivate())) + ucol_close(coll); } JSObject* @@ -182,10 +148,9 @@ js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle(cx, global)); if (!proto) return nullptr; - proto->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; @@ -213,14 +178,6 @@ js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeCollator, UndefinedHandleValue, options)) - return nullptr; - // 8.1 RootedValue ctorValue(cx, ObjectValue(*ctor)); if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) @@ -229,19 +186,6 @@ js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle collator) { RootedValue value(cx); @@ -343,32 +285,33 @@ NewUCollator(JSContext* cx, HandleObject collator) 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) + intl::LanguageTag tag(cx); + if (!intl::LanguageTagParser::parse( + cx, mozilla::MakeCStringSpan(locale.ptr()), tag)) { return nullptr; - memcpy(newLocale, oldLocale, index); - memcpy(newLocale + index, insert, insertLen); - memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0' + } + + JS::RootedVector keywords(cx); + + if (!keywords.emplaceBack("co", cx->names().search)) { + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return nullptr; + } + locale.clear(); - locale.initBytes(newLocale); + locale.encodeLatin1(cx, tag.toString(cx)); + if (!locale) { + return nullptr; + } + } else { + MOZ_ASSERT(StringsAreEqual(usage, "sort")); } // We don't need to look at the collation property - it can only be set @@ -417,8 +360,10 @@ NewUCollator(JSContext* cx, HandleObject collator) uCaseFirst = UCOL_UPPER_FIRST; else if (StringsAreEqual(caseFirst, "lower")) uCaseFirst = UCOL_LOWER_FIRST; - else + else { MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); + uCaseFirst = UCOL_OFF; + } } UErrorCode status = U_ZERO_ERROR; @@ -490,40 +435,38 @@ js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args[2].isString()); Rooted collator(cx, &args[0].toObject().as()); - - // Obtain a UCollator object, cached if possible. + + // Obtain a cached UCollator object. // 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. + void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate(); + UCollator* coll = static_cast(priv); + if (!coll) { coll = NewUCollator(cx, collator); if (!coll) return false; + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll)); } // 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); + return intl_CompareStrings(cx, coll, str1, str2, args.rval()); +} - if (!isCollatorInstance) - ucol_close(coll); - if (!success) +bool +js::intl_isUpperCaseFirst(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 locale(cx, args[0].toString()); + bool isUpperFirst; + if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) return false; - args.rval().set(result); + + args.rval().setBoolean(isUpperFirst); return true; } diff --git a/js/src/builtin/intl/Collator.h b/js/src/builtin/intl/Collator.h index 23db1f1373..e8a4b741ef 100644 --- a/js/src/builtin/intl/Collator.h +++ b/js/src/builtin/intl/Collator.h @@ -52,17 +52,6 @@ CreateCollatorPrototype(JSContext* cx, JS::Handle Intl, 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 @@ -87,6 +76,15 @@ intl_availableCollations(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); +/** + * Returns true if the given locale sorts upper-case before lower-case + * characters. + * + * Usage: result = intl_isUpperCaseFirst(locale) + */ +extern MOZ_MUST_USE bool +intl_isUpperCaseFirst(JSContext* cx, unsigned argc, Value* vp); + } // namespace js diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js index 5fb85f8e6a..bae7ee76c9 100644 --- a/js/src/builtin/intl/Collator.js +++ b/js/src/builtin/intl/Collator.js @@ -5,18 +5,6 @@ /********** 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|. */ @@ -26,80 +14,64 @@ function resolveCollatorInternals(lazyCollatorData) var internalProps = std_Object_create(null); - // Step 7. - internalProps.usage = lazyCollatorData.usage; - - // Step 8. var Collator = collatorInternalProperties; - // Step 9. + // Step 5. + internalProps.usage = lazyCollatorData.usage; + + // Steps 6-7. var collatorIsSorting = lazyCollatorData.usage === "sort"; var localeData = collatorIsSorting ? Collator.sortLocaleData : Collator.searchLocaleData; // Compute effective locale. - // Step 14. + // Step 16. var relevantExtensionKeys = Collator.relevantExtensionKeys; - // Step 15. - var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), + // Step 17. + var r = ResolveLocale("Collator", lazyCollatorData.requestedLocales, lazyCollatorData.opt, relevantExtensionKeys, localeData); - // Step 16. + // Step 18. 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. + var collation = r.co; - // Step 19.d. - internalProps[property] = value; + // Step 20. + if (collation === null) + collation = "default"; - // Step 19.e. - i++; - } + // Step 21. + internalProps.collation = collation; + + // Step 22. + internalProps.numeric = r.kn === "true"; + + // Step 23. + internalProps.caseFirst = r.kf; // Compute remaining collation options. - // Steps 21-22. + // Step 25. 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; - } + // In theory the default sensitivity for the "search" collator is + // locale dependent; in reality the CLDR/ICU default strength is + // always tertiary. Therefore use "variant" as the default value for + // both collation modes. + s = "variant"; } + + // Step 26. internalProps.sensitivity = s; - // Step 24. + // Step 28. internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; - // Step 25. - internalProps.boundFormat = undefined; - // The caller is responsible for associating |internalProps| with the right // object using |setInternalProperties|. return internalProps; @@ -107,11 +79,13 @@ function resolveCollatorInternals(lazyCollatorData) /** - * Returns an object containing the Collator internal properties of |obj|, or - * throws a TypeError if |obj| isn't Collator-initialized. + * Returns an object containing the Collator internal properties of |obj|. */ -function getCollatorInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "Collator", methodName); +function getCollatorInternals(obj) { + assert(IsObject(obj), "getCollatorInternals called with non-object"); + assert(IsCollator(obj), "getCollatorInternals called with non-Collator"); + + var internals = getIntlObjectInternals(obj); assert(internals.type === "Collator", "bad type escaped getIntlObjectInternals"); // If internal properties have already been computed, use them. @@ -138,14 +112,8 @@ function getCollatorInternals(obj, methodName) { * 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); + assert(IsObject(collator), "InitializeCollator called with non-object"); + assert(IsCollator(collator), "InitializeCollator called with non-Collator"); // Lazy Collator data has the following structure: // @@ -167,11 +135,11 @@ function InitializeCollator(collator, locales, options) { // subset of them. var lazyCollatorData = std_Object_create(null); - // Step 3. + // Step 1. var requestedLocales = CanonicalizeLocaleList(locales); lazyCollatorData.requestedLocales = requestedLocales; - // Steps 4-5. + // Steps 2-3. // // 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 @@ -184,42 +152,43 @@ function InitializeCollator(collator, locales, options) { options = ToObject(options); // Compute options that impact interpretation of locale. - // Step 6. + // Step 4. var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); lazyCollatorData.usage = u; - // Step 10. + // Step 8. var opt = new Record(); lazyCollatorData.opt = opt; - // Steps 11-12. + // Steps 9-10. var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); opt.localeMatcher = matcher; - // Step 13, unrolled. + // Steps 11-13. var numericValue = GetOption(options, "numeric", "boolean", undefined, undefined); if (numericValue !== undefined) numericValue = numericValue ? 'true' : 'false'; opt.kn = numericValue; + // Steps 14-15. var caseFirstValue = GetOption(options, "caseFirst", "string", ["upper", "lower", "false"], undefined); opt.kf = caseFirstValue; // Compute remaining collation options. - // Step 20. + // Step 24. var s = GetOption(options, "sensitivity", "string", ["base", "accent", "case", "variant"], undefined); lazyCollatorData.rawSensitivity = s; - // Step 23. + // Step 27. var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); lazyCollatorData.ignorePunctuation = ip; - // Step 26. + // Step 29. // // We've done everything that must be done now: mark the lazy data as fully // computed and install it. - setLazyData(internals, "Collator", lazyCollatorData); + initializeIntlObject(collator, "Collator", lazyCollatorData); } @@ -233,9 +202,13 @@ function InitializeCollator(collator, locales, options) { function Intl_Collator_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; - var availableLocales = callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties); + // Step 1. + var availableLocales = "Collator"; + + // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. return SupportedLocales(availableLocales, requestedLocales, options); } @@ -248,46 +221,106 @@ function Intl_Collator_supportedLocalesOf(locales /*, options*/) { 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"] + relevantExtensionKeys: ["co", "kn", "kf"] }; -function collatorSortLocaleData(locale) { - var collations = intl_availableCollations(locale); - callFunction(std_Array_unshift, collations, null); - return { - co: collations, - kn: ["false", "true"] - }; -} +/** + * Returns the actual locale used when a collator for |locale| is constructed. + */ +function collatorActualLocale(locale) { + assert(typeof locale === "string", "locale should be string"); - -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" - }; + // If |locale| is the default locale (e.g. da-DK), but only supported + // through a fallback (da), we need to get the actual locale before we + // can call intl_isUpperCaseFirst. Also see intl_BestAvailableLocale. + return BestAvailableLocaleIgnoringDefault("Collator", locale); } /** - * Function to be bound and returned by Intl.Collator.prototype.format. + * Returns the default caseFirst values for the given locale. The first + * element in the returned array denotes the default value per ES2017 Intl, + * 9.1 Internal slots of Service Constructors. + */ +function collatorSortCaseFirst(locale) { + var actualLocale = collatorActualLocale(locale); + if (intl_isUpperCaseFirst(actualLocale)) + return ["upper", "false", "lower"]; + + // Default caseFirst values for all other languages. + return ["false", "lower", "upper"]; +} + + +/** + * Returns the default caseFirst value for the given locale. + */ +function collatorSortCaseFirstDefault(locale) { + var actualLocale = collatorActualLocale(locale); + if (intl_isUpperCaseFirst(actualLocale)) + return "upper"; + + // Default caseFirst value for all other languages. + return "false"; +} + +function collatorSortLocaleData() { + /* eslint-disable object-shorthand */ + return { + co: intl_availableCollations, + kn: function() { + return ["false", "true"]; + }, + kf: collatorSortCaseFirst, + default: { + co: function() { + // The first element of the collations array must be |null| + // per ES2017 Intl, 10.2.3 Internal Slots. + return null; + }, + kn: function() { + return "false"; + }, + kf: collatorSortCaseFirstDefault, + } + }; + /* eslint-enable object-shorthand */ +} + + +function collatorSearchLocaleData() { + /* eslint-disable object-shorthand */ + return { + co: function() { + return [null]; + }, + kn: function() { + return ["false", "true"]; + }, + kf: function() { + return ["false", "lower", "upper"]; + }, + default: { + co: function() { + return null; + }, + kn: function() { + return "false"; + }, + kf: function() { + return "false"; + }, + } + }; + /* eslint-enable object-shorthand */ +} + + +/** + * Function to be bound and returned by Intl.Collator.prototype.compare. * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * Spec: ECMAScript Internationalization API Specification, 10.3.3.1. */ function collatorCompareToBind(x, y) { // Steps 1.a.i-ii implemented by ECMAScript declaration binding instantiation, @@ -307,49 +340,60 @@ function collatorCompareToBind(x, y) { * 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. + * Spec: ECMAScript Internationalization API Specification, 10.3.3. */ 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; + var collator = this; - // Step 1.b-d. - var bc = callFunction(FunctionBind, F, this); - internals.boundCompare = bc; + // Steps 2-3. + if (!IsObject(collator) || !IsCollator(collator)) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "Collator", "compare", "Collator"); + + var internals = getCollatorInternals(collator); + + // Step 4. + if (internals.boundCompare === undefined) { + // Steps 4.a-b. + var F = callFunction(FunctionBind, collatorCompareToBind, collator); + + // Step 4.c. + internals.boundCompare = F; } - // Step 2. + // Step 5. return internals.boundCompare; } +_SetCanonicalName(Intl_Collator_compare_get, "get compare"); /** * Returns the resolved options for a Collator object. * - * Spec: ECMAScript Internationalization API Specification, 10.3.3 and 10.4. + * Spec: ECMAScript Internationalization API Specification, 10.3.4. */ function Intl_Collator_resolvedOptions() { - // Check "this Collator object" per introduction of section 10.3. - var internals = getCollatorInternals(this, "resolvedOptions"); + // Step 1. + var collator = this; + // Steps 2-3. + if (!IsObject(collator) || !IsCollator(collator)) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "Collator", "resolvedOptions", "Collator"); + + var internals = getCollatorInternals(collator); + + // Steps 4-5. var result = { locale: internals.locale, usage: internals.usage, sensitivity: internals.sensitivity, - ignorePunctuation: internals.ignorePunctuation + ignorePunctuation: internals.ignorePunctuation, + collation: internals.collation, + numeric: internals.numeric, + caseFirst: internals.caseFirst, }; - 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]); - } + // Step 6. return result; } diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp index b1e88851ab..3afb3ef55e 100644 --- a/js/src/builtin/intl/CommonFunctions.cpp +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -19,26 +19,10 @@ #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); @@ -47,7 +31,33 @@ js::intl::InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue thisValue, HandleValue locales, HandleValue options, + DateTimeFormatOptions dtfOptions, MutableHandleValue result) +{ + 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)) + return false; + + MOZ_ASSERT(result.isObject(), "Legacy Intl object initializer must return an object"); + return true; } /** @@ -56,21 +66,12 @@ js::intl::InitializeObject(JSContext* cx, HandleObject obj, Handleglobal(), 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)) + if (!js::CallSelfHostedFunction(cx, cx->names().getInternals, v, args, &v)) return nullptr; return &v.toObject(); @@ -82,34 +83,10 @@ 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; +const js::intl::OldStyleLanguageTagMapping + js::intl::oldStyleLanguageTagMappings[] = { + {"pa-PK", "pa-Arab-PK"}, {"zh-CN", "zh-Hans-CN"}, + {"zh-HK", "zh-Hant-HK"}, {"zh-SG", "zh-Hans-SG"}, + {"zh-TW", "zh-Hant-TW"}, +}; - 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 index 575597c0a2..66312ebc69 100644 --- a/js/src/builtin/intl/CommonFunctions.h +++ b/js/src/builtin/intl/CommonFunctions.h @@ -26,12 +26,6 @@ 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. */ @@ -39,6 +33,22 @@ extern bool InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, HandleValue locales, HandleValue options); +/** + * Initialize an existing object as an Intl.* object using the named + * 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, + DateTimeFormatOptions dtfOptions, MutableHandleValue result); /** * Returns the object holding the internal properties for obj. */ @@ -61,6 +71,32 @@ StringsAreEqual(JSAutoByteString& s1, const char* s2) return !strcmp(s1.ptr(), s2); } +/** + * The last-ditch locale is used if none of the available locales satisfies a + * request. "en-GB" is used based on the assumptions that English is the most + * common second language, that both en-GB and en-US are normally available in + * an implementation, and that en-GB is more representative of the English used + * in other locales. + */ +static inline const char* LastDitchLocale() { return "en-GB"; } + +/** + * Certain old, commonly-used language tags that lack a script, are expected to + * nonetheless imply one. This object maps these old-style tags to modern + * equivalents. + */ +struct OldStyleLanguageTagMapping { + const char* const oldStyle; + const char* const modernStyle; + + // Provide a constructor to catch missing initializers in the mappings array. + constexpr OldStyleLanguageTagMapping(const char* oldStyle, + const char* modernStyle) + : oldStyle(oldStyle), modernStyle(modernStyle) {} +}; + +extern const OldStyleLanguageTagMapping oldStyleLanguageTagMappings[5]; + static inline const char* IcuLocale(const char* locale) { @@ -79,9 +115,9 @@ 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 +template static int32_t -CallICU(JSContext* cx, Vector& chars, const ICUStringFunction& strFn) +CallICU(JSContext* cx, Vector& chars, const ICUStringFunction& strFn) { MOZ_ASSERT(chars.length() == 0); MOZ_ALWAYS_TRUE(chars.resize(InlineCapacity)); @@ -119,25 +155,6 @@ CallICU(JSContext* cx, const ICUStringFunction& strFn) 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 (*)(); -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 diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js index 48337e666a..effabf020f 100644 --- a/js/src/builtin/intl/CommonFunctions.js +++ b/js/src/builtin/intl/CommonFunctions.js @@ -5,36 +5,90 @@ /* Portions Copyright Norbert Lindenberg 2011-2012. */ -/** - * Holder object for encapsulating regexp instances. - * - * Regular expression instances should be created after the initialization of - * self-hosted global. - */ -var internalIntlRegExps = std_Object_create(null); -internalIntlRegExps.unicodeLocaleExtensionSequenceRE = null; -internalIntlRegExps.languageTagRE = null; -internalIntlRegExps.duplicateVariantRE = null; -internalIntlRegExps.duplicateSingletonRE = null; -internalIntlRegExps.isWellFormedCurrencyCodeRE = null; -internalIntlRegExps.currencyDigitsRE = null; /** - * Regular expression matching a "Unicode locale extension sequence", which the + * Shorthand for hasOwnProperty. + */ +function hasOwn(propName, object) { + return callFunction(std_Object_hasOwnProperty, object, propName); +} + +#ifdef DEBUG +#define assertIsValidAndCanonicalLanguageTag(locale, desc) \ + do { \ + let canonical = intl_TryValidateAndCanonicalizeLanguageTag(locale); \ + assert(canonical !== null, \ + `${desc} is a structurally valid language tag`); \ + assert(canonical === locale, \ + `${desc} is a canonicalized language tag`); \ + } while (false) +#else +#define assertIsValidAndCanonicalLanguageTag(locale, desc) ; // Elided assertion. +#endif + +/** + * Returns the start index of a "Unicode locale extension sequence", which the * specification defines as: "any substring of a language tag that starts with * a separator '-' and the singleton 'u' and includes the maximum sequence of * following non-singleton subtags and their preceding '-' separators." * * Alternatively, this may be defined as: the components of a language tag that - * match the extension production in RFC 5646, where the singleton component is - * "u". + * match the `unicode_locale_extensions` production in UTS 35. * * Spec: ECMAScript Internationalization API Specification, 6.2.1. */ -function getUnicodeLocaleExtensionSequenceRE() { - return internalIntlRegExps.unicodeLocaleExtensionSequenceRE || - (internalIntlRegExps.unicodeLocaleExtensionSequenceRE = - RegExpCreate("-u(?:-[a-z0-9]{2,8})+")); +function startOfUnicodeExtensions(locale) { + assert(typeof locale === "string", "locale is a string"); + + // Search for "-u-" marking the start of a Unicode extension sequence. + var start = callFunction(std_String_indexOf, locale, "-u-"); + if (start < 0) + return -1; + + // And search for "-x-" marking the start of any privateuse component to + // handle the case when "-u-" was only found within a privateuse subtag. + var privateExt = callFunction(std_String_indexOf, locale, "-x-"); + if (privateExt >= 0 && privateExt < start) + return -1; + + return start; +} + +/** + * Returns the end index of a Unicode locale extension sequence. + */ +function endOfUnicodeExtensions(locale, start) { + assert(typeof locale === "string", "locale is a string"); + assert(0 <= start && start < locale.length, "start is an index into locale"); + assert(Substring(locale, start, 3) === "-u-", "start points to Unicode extension sequence"); + + #define HYPHEN 0x2D + assert(std_String_fromCharCode(HYPHEN) === "-", + "code unit constant should match the expected character"); + + // Search for the start of the next singleton or privateuse subtag. + // + // Begin searching after the smallest possible Unicode locale extension + // sequence, namely |"-u-" 2alphanum|. End searching once the remaining + // characters can't fit the smallest possible singleton or privateuse + // subtag, namely |"-x-" alphanum|. Note the reduced end-limit means + // indexing inside the loop is always in-range. + for (var i = start + 5, end = locale.length - 4; i <= end; i++) { + if (callFunction(std_String_charCodeAt, locale, i) !== HYPHEN) + continue; + if (callFunction(std_String_charCodeAt, locale, i + 2) === HYPHEN) + return i; + + // Skip over (i + 1) and (i + 2) because we've just verified they + // aren't "-", so the next possible delimiter can only be at (i + 3). + i += 2; + } + + #undef HYPHEN + + // If no singleton or privateuse subtag was found, the Unicode extension + // sequence extends until the end of the string. + return locale.length; } @@ -42,497 +96,54 @@ function getUnicodeLocaleExtensionSequenceRE() { * Removes Unicode locale extension sequences from the given language tag. */ function removeUnicodeExtensions(locale) { - // A wholly-privateuse locale has no extension sequences. - if (callFunction(std_String_startsWith, locale, "x-")) + var start = startOfUnicodeExtensions(locale); + if (start < 0) return locale; - // Otherwise, split on "-x-" marking the start of any privateuse component. - // Replace Unicode locale extension sequences in the left half, and return - // the concatenation. - var pos = callFunction(std_String_indexOf, locale, "-x-"); - if (pos < 0) - pos = locale.length; - - var left = callFunction(String_substring, locale, 0, pos); - var right = callFunction(String_substring, locale, pos); - - var extensions; - var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); - while ((extensions = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, left)) !== null) { - left = StringReplaceString(left, extensions[0], ""); - unicodeLocaleExtensionSequenceRE.lastIndex = 0; - } + var end = endOfUnicodeExtensions(locale, start); + var left = Substring(locale, 0, start); + var right = Substring(locale, end, locale.length - end); var combined = left + right; - assert(IsStructurallyValidLanguageTag(combined), "recombination produced an invalid language tag"); - assert(function() { - var uindex = callFunction(std_String_indexOf, combined, "-u-"); - if (uindex < 0) - return true; - var xindex = callFunction(std_String_indexOf, combined, "-x-"); - return xindex > 0 && xindex < uindex; - }(), "recombination failed to remove all Unicode locale extension sequences"); + + assertIsValidAndCanonicalLanguageTag(combined, "the recombined locale"); + assert(startOfUnicodeExtensions(combined) < 0, + "recombination failed to remove all Unicode locale extension sequences"); return combined; } - /** - * Regular expression defining BCP 47 language tags. - * - * Spec: RFC 5646 section 2.1. + * Returns Unicode locale extension sequences from the given language tag. */ -function getLanguageTagRE() { - if (internalIntlRegExps.languageTagRE) - return internalIntlRegExps.languageTagRE; +function getUnicodeExtensions(locale) { + var start = startOfUnicodeExtensions(locale); + assert(start >= 0, "start of Unicode extension sequence not found"); + var end = endOfUnicodeExtensions(locale, start); - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // regular = "art-lojban" ; these tags match the 'langtag' - // / "cel-gaulish" ; production, but their subtags - // / "no-bok" ; are not extended language - // / "no-nyn" ; or variant subtags: their meaning - // / "zh-guoyu" ; is defined by their registration - // / "zh-hakka" ; and all of these are deprecated - // / "zh-min" ; in favor of a more modern - // / "zh-min-nan" ; subtag or sequence of subtags - // / "zh-xiang" - var regular = "(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)"; - // irregular = "en-GB-oed" ; irregular tags do not match - // / "i-ami" ; the 'langtag' production and - // / "i-bnn" ; would not otherwise be - // / "i-default" ; considered 'well-formed' - // / "i-enochian" ; These tags are all valid, - // / "i-hak" ; but most are deprecated - // / "i-klingon" ; in favor of more modern - // / "i-lux" ; subtags or subtag - // / "i-mingo" ; combination - // / "i-navajo" - // / "i-pwn" - // / "i-tao" - // / "i-tay" - // / "i-tsu" - // / "sgn-BE-FR" - // / "sgn-BE-NL" - // / "sgn-CH-DE" - var irregular = "(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)"; - // grandfathered = irregular ; non-redundant tags registered - // / regular ; during the RFC 3066 era - var grandfathered = "(?:" + irregular + "|" + regular + ")"; - // privateuse = "x" 1*("-" (1*8alphanum)) - var privateuse = "(?:x(?:-[a-z0-9]{1,8})+)"; - // singleton = DIGIT ; 0 - 9 - // / %x41-57 ; A - W - // / %x59-5A ; Y - Z - // / %x61-77 ; a - w - // / %x79-7A ; y - z - var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; - // extension = singleton 1*("-" (2*8alphanum)) - var extension = "(?:" + singleton + "(?:-" + alphanum + "{2,8})+)"; - // variant = 5*8alphanum ; registered variants - // / (DIGIT 3alphanum) - var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; - // region = 2ALPHA ; ISO 3166-1 code - // / 3DIGIT ; UN M.49 code - var region = "(?:" + ALPHA + "{2}|" + DIGIT + "{3})"; - // script = 4ALPHA ; ISO 15924 code - var script = "(?:" + ALPHA + "{4})"; - // extlang = 3ALPHA ; selected ISO 639 codes - // *2("-" 3ALPHA) ; permanently reserved - var extlang = "(?:" + ALPHA + "{3}(?:-" + ALPHA + "{3}){0,2})"; - // language = 2*3ALPHA ; shortest ISO 639 code - // ["-" extlang] ; sometimes followed by - // ; extended language subtags - // / 4ALPHA ; or reserved for future use - // / 5*8ALPHA ; or registered language subtag - var language = "(?:" + ALPHA + "{2,3}(?:-" + extlang + ")?|" + ALPHA + "{4}|" + ALPHA + "{5,8})"; - // langtag = language - // ["-" script] - // ["-" region] - // *("-" variant) - // *("-" extension) - // ["-" privateuse] - var langtag = language + "(?:-" + script + ")?(?:-" + region + ")?(?:-" + - variant + ")*(?:-" + extension + ")*(?:-" + privateuse + ")?"; - // Language-Tag = langtag ; normal language tags - // / privateuse ; private use tag - // / grandfathered ; grandfathered tags - var languageTag = "^(?:" + langtag + "|" + privateuse + "|" + grandfathered + ")$"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). - return (internalIntlRegExps.languageTagRE = RegExpCreate(languageTag, "i")); -} - - -function getDuplicateVariantRE() { - if (internalIntlRegExps.duplicateVariantRE) - return internalIntlRegExps.duplicateVariantRE; - - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // variant = 5*8alphanum ; registered variants - // / (DIGIT 3alphanum) - var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; - - // Match a langtag that contains a duplicate variant. - var duplicateVariant = - // Match everything in a langtag prior to any variants, and maybe some - // of the variants as well (which makes this pattern inefficient but - // not wrong, for our purposes); - "(?:" + alphanum + "{2,8}-)+" + - // a variant, parenthesised so that we can refer back to it later; - "(" + variant + ")-" + - // zero or more subtags at least two characters long (thus stopping - // before extension and privateuse components); - "(?:" + alphanum + "{2,8}-)*" + - // and the same variant again - "\\1" + - // ...but not followed by any characters that would turn it into a - // different subtag. - "(?!" + alphanum + ")"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). Using - // character classes covering both upper- and lower-case characters nearly - // addresses this -- but for the possibility of variant repetition with - // differing case, e.g. "en-variant-Variant". Use a case-insensitive - // regular expression to address this. (Note that there's no worry about - // case transformation accepting invalid characters here: users have - // already verified the string is alphanumeric Latin plus "-".) - return (internalIntlRegExps.duplicateVariantRE = RegExpCreate(duplicateVariant, "i")); -} - - -function getDuplicateSingletonRE() { - if (internalIntlRegExps.duplicateSingletonRE) - return internalIntlRegExps.duplicateSingletonRE; - - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // singleton = DIGIT ; 0 - 9 - // / %x41-57 ; A - W - // / %x59-5A ; Y - Z - // / %x61-77 ; a - w - // / %x79-7A ; y - z - var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; - - // Match a langtag that contains a duplicate singleton. - var duplicateSingleton = - // Match a singleton subtag, parenthesised so that we can refer back to - // it later; - "-(" + singleton + ")-" + - // then zero or more subtags; - "(?:" + alphanum + "+-)*" + - // and the same singleton again - "\\1" + - // ...but not followed by any characters that would turn it into a - // different subtag. - "(?!" + alphanum + ")"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). Using - // character classes covering both upper- and lower-case characters nearly - // addresses this -- but for the possibility of singleton repetition with - // differing case, e.g. "en-u-foo-U-foo". Use a case-insensitive regular - // expression to address this. (Note that there's no worry about case - // transformation accepting invalid characters here: users have already - // verified the string is alphanumeric Latin plus "-".) - return (internalIntlRegExps.duplicateSingletonRE = RegExpCreate(duplicateSingleton, "i")); -} - - -/** - * Verifies that the given string is a well-formed BCP 47 language tag - * with no duplicate variant or singleton subtags. - * - * Spec: ECMAScript Internationalization API Specification, 6.2.2. - */ -function IsStructurallyValidLanguageTag(locale) { - assert(typeof locale === "string", "IsStructurallyValidLanguageTag"); - var languageTagRE = getLanguageTagRE(); - if (!regexp_test_no_statics(languageTagRE, locale)) - return false; - - // Before checking for duplicate variant or singleton subtags with - // regular expressions, we have to get private use subtag sequences - // out of the picture. - if (callFunction(std_String_startsWith, locale, "x-")) - return true; - var pos = callFunction(std_String_indexOf, locale, "-x-"); - if (pos !== -1) - locale = callFunction(String_substring, locale, 0, pos); - - // Check for duplicate variant or singleton subtags. - var duplicateVariantRE = getDuplicateVariantRE(); - var duplicateSingletonRE = getDuplicateSingletonRE(); - return !regexp_test_no_statics(duplicateVariantRE, locale) && - !regexp_test_no_statics(duplicateSingletonRE, locale); + return Substring(locale, start, end - start); } /** - * Joins the array elements in the given range with the supplied separator. + * Returns true if the input contains only ASCII alphabetical characters. */ -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"); +function IsASCIIAlphaString(s) { + assert(typeof s === "string", "IsASCIIAlphaString"); - if (from === to) - return ""; - - var result = array[from]; - for (var i = from + 1; i < to; i++) { - result += separator + array[i]; + for (var i = 0; i < s.length; i++) { + var c = callFunction(std_String_charCodeAt, s, i); + if (!((0x41 <= c && c <= 0x5A) || (0x61 <= c && c <= 0x7A))) + return false } - return result; + return true; } -/** - * Canonicalizes the given structurally valid BCP 47 language tag, including - * regularized case of subtags. For example, the language tag - * Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, where - * - * Zh ; 2*3ALPHA - * -NAN ; ["-" extlang] - * -haNS ; ["-" script] - * -bu ; ["-" region] - * -variant2 ; *("-" variant) - * -Variant1 - * -u-ca-chinese ; *("-" extension) - * -t-Zh-laTN - * -x-PRIVATE ; ["-" privateuse] - * - * becomes nan-Hans-mm-variant2-variant1-t-zh-latn-u-ca-chinese-x-private - * - * Spec: ECMAScript Internationalization API Specification, 6.2.3. - * Spec: RFC 5646, section 4.5. - */ -function CanonicalizeLanguageTag(locale) { - assert(IsStructurallyValidLanguageTag(locale), "CanonicalizeLanguageTag"); - - // The input - // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" - // will be used throughout this method to illustrate how it works. - - // Language tags are compared and processed case-insensitively, so - // technically it's not necessary to adjust case. But for easier processing, - // and because the canonical form for most subtags is lower case, we start - // with lower case for all. - // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" -> - // "zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private" - locale = callFunction(std_String_toLowerCase, locale); - - // Handle mappings for complete tags. - if (callFunction(std_Object_hasOwnProperty, langTagMappings, locale)) - return langTagMappings[locale]; - - var subtags = StringSplitString(ToString(locale), "-"); - var i = 0; - - // Handle the standard part: All subtags before the first singleton or "x". - // "zh-nan-hans-bu-variant2-variant1" - while (i < subtags.length) { - var subtag = subtags[i]; - - // If we reach the start of an extension sequence or private use part, - // we're done with this loop. We have to check for i > 0 because for - // irregular language tags, such as i-klingon, the single-character - // subtag "i" is not the start of an extension sequence. - // In the example, we break at "u". - if (subtag.length === 1 && (i > 0 || subtag === "x")) - break; - - if (i !== 0) { - if (subtag.length === 4) { - // 4-character subtags that are not in initial position are - // script codes; their first character needs to be capitalized. - // "hans" -> "Hans" - subtag = callFunction(std_String_toUpperCase, subtag[0]) + - callFunction(String_substring, subtag, 1); - } else if (subtag.length === 2) { - // 2-character subtags that are not in initial position are - // region codes; they need to be upper case. "bu" -> "BU" - subtag = callFunction(std_String_toUpperCase, subtag); - } - } - if (callFunction(std_Object_hasOwnProperty, langSubtagMappings, subtag)) { - // Replace deprecated subtags with their preferred values. - // "BU" -> "MM" - // This has to come after we capitalize region codes because - // otherwise some language and region codes could be confused. - // For example, "in" is an obsolete language code for Indonesian, - // but "IN" is the country code for India. - // Note that the script generating langSubtagMappings makes sure - // that no regular subtag mapping will replace an extlang code. - subtag = langSubtagMappings[subtag]; - } else if (callFunction(std_Object_hasOwnProperty, extlangMappings, subtag)) { - // Replace deprecated extlang subtags with their preferred values, - // and remove the preceding subtag if it's a redundant prefix. - // "zh-nan" -> "nan" - // Note that the script generating extlangMappings makes sure that - // no extlang mapping will replace a normal language code. - subtag = extlangMappings[subtag].preferred; - if (i === 1 && extlangMappings[subtag].prefix === subtags[0]) { - callFunction(std_Array_shift, subtags); - i--; - } - } - subtags[i] = subtag; - i++; - } - var normal = ArrayJoinRange(subtags, "-", 0, i); - - // Extension sequences are sorted by their singleton characters. - // "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese" - var extensions = new List(); - while (i < subtags.length && subtags[i] !== "x") { - var extensionStart = i; - i++; - while (i < subtags.length && subtags[i].length > 1) - i++; - var extension = ArrayJoinRange(subtags, "-", extensionStart, i); - callFunction(std_Array_push, extensions, extension); - } - callFunction(std_Array_sort, extensions); - - // Private use sequences are left as is. "x-private" - var privateUse = ""; - if (i < subtags.length) - privateUse = ArrayJoinRange(subtags, "-", i); - - // Put everything back together. - var canonical = normal; - if (extensions.length > 0) - canonical += "-" + callFunction(std_Array_join, extensions, "-"); - if (privateUse.length > 0) { - // Be careful of a Language-Tag that is entirely privateuse. - if (canonical.length > 0) - canonical += "-" + privateUse; - else - canonical = privateUse; - } - - return canonical; -} - -function localeContainsNoUnicodeExtensions(locale) { - // No "-u-", no possible Unicode extension. - if (callFunction(std_String_indexOf, locale, "-u-") === -1) - return true; - - // "-u-" within privateuse also isn't one. - if (callFunction(std_String_indexOf, locale, "-u-") > callFunction(std_String_indexOf, locale, "-x-")) - return true; - - // An entirely-privateuse tag doesn't contain extensions. - if (callFunction(std_String_startsWith, locale, "x-")) - return true; - - // Otherwise, we have a Unicode extension sequence. - return false; -} - - -// The last-ditch locale is used if none of the available locales satisfies a -// request. "en-GB" is used based on the assumptions that English is the most -// common second language, that both en-GB and en-US are normally available in -// an implementation, and that en-GB is more representative of the English used -// in other locales. -function lastDitchLocale() { - // Per bug 1177929, strings don't clone out of self-hosted code as atoms, - // breaking IonBuilder::constant. Put this in a function for now. - return "en-GB"; -} - - -// Certain old, commonly-used language tags that lack a script, are expected to -// nonetheless imply one. This object maps these old-style tags to modern -// equivalents. -var oldStyleLanguageTagMappings = { - "pa-PK": "pa-Arab-PK", - "zh-CN": "zh-Hans-CN", - "zh-HK": "zh-Hant-HK", - "zh-SG": "zh-Hans-SG", - "zh-TW": "zh-Hant-TW", -}; - - -var localeCandidateCache = { - runtimeDefaultLocale: undefined, - candidateDefaultLocale: undefined, -}; - - var localeCache = { runtimeDefaultLocale: undefined, defaultLocale: undefined, }; -/** - * Compute the candidate default locale: the locale *requested* to be used as - * the default locale. We'll use it if and only if ICU provides support (maybe - * fallback support, e.g. supporting "de-ZA" through "de" support implied by a - * "de-DE" locale). - */ -function DefaultLocaleIgnoringAvailableLocales() { - const runtimeDefaultLocale = RuntimeDefaultLocale(); - if (runtimeDefaultLocale === localeCandidateCache.runtimeDefaultLocale) - return localeCandidateCache.candidateDefaultLocale; - - // If we didn't get a cache hit, compute the candidate default locale and - // cache it. Fall back on the last-ditch locale when necessary. - var candidate; - if (!IsStructurallyValidLanguageTag(runtimeDefaultLocale)) { - candidate = lastDitchLocale(); - } else { - candidate = CanonicalizeLanguageTag(runtimeDefaultLocale); - - // The default locale must be in [[availableLocales]], and that list - // must not contain any locales with Unicode extension sequences, so - // remove any present in the candidate. - candidate = removeUnicodeExtensions(candidate); - - if (callFunction(std_Object_hasOwnProperty, oldStyleLanguageTagMappings, candidate)) - candidate = oldStyleLanguageTagMappings[candidate]; - } - - // Cache the candidate locale until the runtime default locale changes. - localeCandidateCache.candidateDefaultLocale = candidate; - localeCandidateCache.runtimeDefaultLocale = runtimeDefaultLocale; - - assert(IsStructurallyValidLanguageTag(candidate), - "the candidate must be structurally valid"); - assert(localeContainsNoUnicodeExtensions(candidate), - "the candidate must not contain a Unicode extension sequence"); - - return candidate; -} - - /** * Returns the BCP 47 language tag for the host environment's current locale. * @@ -544,32 +155,13 @@ function DefaultLocale() { return localeCache.defaultLocale; // If we didn't have a cache hit, compute the candidate default locale. - // Then use it as the actual default locale if ICU supports that locale - // (perhaps via fallback). Otherwise use the last-ditch locale. - var candidate = DefaultLocaleIgnoringAvailableLocales(); - var locale; - if (BestAvailableLocaleIgnoringDefault(callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties), - candidate)) - { - locale = candidate; - } else { - locale = lastDitchLocale(); - } + var locale = intl_supportedLocaleOrFallback(runtimeDefaultLocale); - assert(IsStructurallyValidLanguageTag(locale), - "the computed default locale must be structurally valid"); - assert(locale === CanonicalizeLanguageTag(locale), - "the computed default locale must be canonical"); - assert(localeContainsNoUnicodeExtensions(locale), + assertIsValidAndCanonicalLanguageTag(locale, "the computed default locale"); + assert(startOfUnicodeExtensions(locale) < 0, "the computed default locale must not contain a Unicode extension sequence"); + // Cache the computed locale until the runtime default locale changes. localeCache.defaultLocale = locale; localeCache.runtimeDefaultLocale = runtimeDefaultLocale; @@ -577,109 +169,66 @@ function DefaultLocale() { } -/** - * 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 - * locale, in case ICU doesn't directly support it (but does support it through - * fallback, e.g. supporting "en-GB" indirectly using "en" support). - */ -function addSpecialMissingLanguageTags(availableLocales) { - // Certain old-style language tags lack a script code, but in current usage - // they *would* include a script code. Map these over to modern forms. - var oldStyleLocales = std_Object_getOwnPropertyNames(oldStyleLanguageTagMappings); - for (var i = 0; i < oldStyleLocales.length; i++) { - var oldStyleLocale = oldStyleLocales[i]; - if (availableLocales[oldStyleLanguageTagMappings[oldStyleLocale]]) - availableLocales[oldStyleLocale] = true; - } - - // Also forcibly provide the last-ditch locale. - var lastDitch = lastDitchLocale(); - assert(lastDitch === "en-GB" && availableLocales["en"], - "shouldn't be a need to add every locale implied by the last-" + - "ditch locale, merely just the last-ditch locale"); - availableLocales[lastDitch] = true; -} - - /** * Canonicalizes a locale list. * * Spec: ECMAScript Internationalization API Specification, 9.2.1. */ function CanonicalizeLocaleList(locales) { + // Step 1. if (locales === undefined) - return new List(); - var seen = new List(); - if (typeof locales === "string") - locales = [locales]; + return []; + + // Step 3 (and the remaining steps). + var tag = intl_ValidateAndCanonicalizeLanguageTag(locales, false); + if (tag !== null) { + assert(typeof tag === "string", + "intl_ValidateAndCanonicalizeLanguageTag returns a string value"); + return [tag]; + } + + // Step 2. + var seen = []; + + // Step 4. var O = ToObject(locales); + + // Step 5. var len = ToLength(O.length); + + // Step 6. var k = 0; + + // Step 7. while (k < len) { - // Don't call ToString(k) - SpiderMonkey is faster with integers. - var kPresent = HasProperty(O, k); - if (kPresent) { + // Steps 7.a-c. + if (k in O) { + // Step 7.c.i. var kValue = O[k]; + + // Step 7.c.ii. if (!(typeof kValue === "string" || IsObject(kValue))) ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT); - var tag = ToString(kValue); - if (!IsStructurallyValidLanguageTag(tag)) - ThrowRangeError(JSMSG_INVALID_LANGUAGE_TAG, tag); - tag = CanonicalizeLanguageTag(tag); + + // Steps 7.c.iii-iv. + var tag = intl_ValidateAndCanonicalizeLanguageTag(kValue, true); + assert(typeof tag === "string", + "ValidateAndCanonicalizeLanguageTag returns a string value"); + + // Step 7.c.v. if (callFunction(ArrayIndexOf, seen, tag) === -1) - callFunction(std_Array_push, seen, tag); + _DefineDataProperty(seen, seen.length, tag); } + + // Step 7.d. k++; } + + // Step 8. return seen; } -function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLocale) { - assert(IsStructurallyValidLanguageTag(locale), "invalid BestAvailableLocale locale structure"); - assert(locale === CanonicalizeLanguageTag(locale), "non-canonical BestAvailableLocale locale"); - assert(localeContainsNoUnicodeExtensions(locale), "locale must contain no Unicode extensions"); - - // In the spec, [[availableLocales]] is formally a list of all available - // locales. But in our implementation, it's an *incomplete* list, not - // necessarily including the default locale (and all locales implied by it, - // e.g. "de" implied by "de-CH"), if that locale isn't in every - // [[availableLocales]] list (because that locale is supported through - // fallback, e.g. "de-CH" supported through "de"). - // - // If we're considering the default locale, augment the spec loop with - // additional checks to also test whether the current prefix is a prefix of - // the default locale. - - var defaultLocale; - if (considerDefaultLocale) - defaultLocale = DefaultLocale(); - - var candidate = locale; - while (true) { - if (availableLocales[candidate]) - return candidate; - - if (considerDefaultLocale && candidate.length <= defaultLocale.length) { - if (candidate === defaultLocale) - return candidate; - if (callFunction(std_String_startsWith, defaultLocale, candidate + "-")) - return candidate; - } - - var pos = callFunction(std_String_lastIndexOf, candidate, "-"); - if (pos === -1) - return undefined; - - if (pos >= 2 && candidate[pos - 2] === "-") - pos -= 2; - - candidate = callFunction(String_substring, candidate, 0, pos); - } -} - - /** * Compares a BCP 47 language tag against the locales in availableLocales * and returns the best available match. Uses the fallback @@ -689,20 +238,17 @@ function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLoca * Spec: RFC 4647, section 3.4. */ function BestAvailableLocale(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, true); + return intl_BestAvailableLocale(availableLocales, locale, DefaultLocale()); } - /** * Identical to BestAvailableLocale, but does not consider the default locale * during computation. */ function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, false); + return intl_BestAvailableLocale(availableLocales, locale, null); } -var noRelevantExtensionKeys = []; - /** * Compares a BCP 47 language priority list against the set of locales in * availableLocales and determines the best available language to meet the @@ -716,31 +262,37 @@ var noRelevantExtensionKeys = []; * Spec: RFC 4647, section 3.4. */ function LookupMatcher(availableLocales, requestedLocales) { - var i = 0; - var len = requestedLocales.length; - var availableLocale; - var locale, noExtensionsLocale; - while (i < len && availableLocale === undefined) { - locale = requestedLocales[i]; - noExtensionsLocale = removeUnicodeExtensions(locale); - availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); - i++; + // Step 1. + var result = new Record(); + + // Step 2. + for (var i = 0; i < requestedLocales.length; i++) { + var locale = requestedLocales[i]; + + // Step 2.a. + var noExtensionsLocale = removeUnicodeExtensions(locale); + + // Step 2.b. + var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); + + // Step 2.c. + if (availableLocale !== undefined) { + // Step 2.c.i. + result.locale = availableLocale; + + // Step 2.c.ii. + if (locale !== noExtensionsLocale) + result.extension = getUnicodeExtensions(locale); + + // Step 2.c.iii. + return result; + } } - var result = new Record(); - if (availableLocale !== undefined) { - result.locale = availableLocale; - if (locale !== noExtensionsLocale) { - var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); - var extensionMatch = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, locale); - var extension = extensionMatch[0]; - var extensionIndex = extensionMatch.index; - result.extension = extension; - result.extensionIndex = extensionIndex; - } - } else { - result.locale = DefaultLocale(); - } + // Steps 3-4. + result.locale = DefaultLocale(); + + // Step 5. return result; } @@ -759,6 +311,77 @@ function BestFitMatcher(availableLocales, requestedLocales) { return LookupMatcher(availableLocales, requestedLocales); } +/** + * Returns the Unicode extension value subtags for the requested key subtag. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.5. + */ +function UnicodeExtensionValue(extension, key) { + assert(typeof extension === "string", "extension is a string value"); + assert(callFunction(std_String_startsWith, extension, "-u-") && + getUnicodeExtensions("und" + extension) === extension, + "extension is a Unicode extension subtag"); + assert(typeof key === "string", "key is a string value"); + + // Step 1. + assert(key.length === 2, "key is a Unicode extension key subtag"); + + // Step 2. + var size = extension.length; + + // Step 3. + var searchValue = "-" + key + "-"; + + // Step 4. + var pos = callFunction(std_String_indexOf, extension, searchValue); + + // Step 5. + if (pos !== -1) { + // Step 5.a. + var start = pos + 4; + + // Step 5.b. + var end = start; + + // Step 5.c. + var k = start; + + // Steps 5.d-e. + while (true) { + // Step 5.e.i. + var e = callFunction(std_String_indexOf, extension, "-", k); + + // Step 5.e.ii. + var len = e === -1 ? size - k : e - k; + + // Step 5.e.iii. + if (len === 2) + break; + + // Step 5.e.iv. + if (e === -1) { + end = size; + break; + } + + // Step 5.e.v. + end = e; + k = e + 1; + } + + // Step 5.f. + return callFunction(String_substring, extension, start, end); + } + + // Step 6. + searchValue = "-" + key; + + // Steps 7-8. + if (callFunction(std_String_endsWith, extension, searchValue)) + return ""; + + // Step 9 (implicit). +} /** * Compares a BCP 47 language priority list against availableLocales and @@ -767,11 +390,9 @@ function BestFitMatcher(availableLocales, requestedLocales) { * caller's relevant extensions and locale data as well as client-provided * options into consideration. * - * Spec: ECMAScript Internationalization API Specification, 9.2.5. + * Spec: ECMAScript Internationalization API Specification, 9.2.6. */ function ResolveLocale(availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) { - /*jshint laxbreak: true */ - // Steps 1-3. var matcher = options.localeMatcher; var r = (matcher === "lookup") @@ -780,121 +401,130 @@ function ResolveLocale(availableLocales, requestedLocales, options, relevantExte // Step 4. var foundLocale = r.locale; - - // Step 5.a. var extension = r.extension; - var extensionIndex, extensionSubtags, extensionSubtagsLength; // Step 5. - if (extension !== undefined) { - // Step 5.b. - extensionIndex = r.extensionIndex; - - // Steps 5.d-e. - extensionSubtags = StringSplitString(ToString(extension), "-"); - extensionSubtagsLength = extensionSubtags.length; - } - - // Steps 6-7. var result = new Record(); + + // Step 6. result.dataLocale = foundLocale; - // Step 8. + // Step 7. var supportedExtension = "-u"; - // Steps 9-11. - var i = 0; - var len = relevantExtensionKeys.length; - while (i < len) { - // Steps 11.a-c. + // In this implementation, localeData is a function, not an object. + var localeDataProvider = localeData(); + + // Step 8. + for (var i = 0; i < relevantExtensionKeys.length; i++) { var key = relevantExtensionKeys[i]; - // In this implementation, localeData is a function, not an object. - var foundLocaleData = localeData(foundLocale); - var keyLocaleData = foundLocaleData[key]; - - // Locale data provides default value. - // Step 11.d. - var value = keyLocaleData[0]; + // Steps 8.a-h (The locale data is only computed when needed). + var keyLocaleData = undefined; + var value = undefined; // Locale tag may override. - // Step 11.e. + // Step 8.g. var supportedExtensionAddition = ""; - // Step 11.f is implemented by Utilities.js. + // Step 8.h. + if (extension !== undefined) { + // Step 8.h.i. + var requestedValue = UnicodeExtensionValue(extension, key); - var valuePos; + // Step 8.h.ii. + if (requestedValue !== undefined) { + // Steps 8.a-d. + keyLocaleData = callFunction(localeDataProvider[key], null, foundLocale); - // Step 11.g. - if (extensionSubtags !== undefined) { - // Step 11.g.i. - var keyPos = callFunction(ArrayIndexOf, extensionSubtags, key); - - // Step 11.g.ii. - if (keyPos !== -1) { - // Step 11.g.ii.1. - if (keyPos + 1 < extensionSubtagsLength && - extensionSubtags[keyPos + 1].length > 2) - { - // Step 11.g.ii.1.a. - var requestedValue = extensionSubtags[keyPos + 1]; - - // Step 11.g.ii.1.b. - valuePos = callFunction(ArrayIndexOf, keyLocaleData, requestedValue); - - // Step 11.g.ii.1.c. - if (valuePos !== -1) { + // Step 8.h.ii.1. + if (requestedValue !== "") { + // Step 8.h.ii.1.a. + if (callFunction(ArrayIndexOf, keyLocaleData, requestedValue) !== -1) { value = requestedValue; supportedExtensionAddition = "-" + key + "-" + value; } } else { - // Step 11.g.ii.2. + // Step 8.h.ii.2. // According to the LDML spec, if there's no type value, // and true is an allowed value, it's used. - // Step 11.g.ii.2.a. - valuePos = callFunction(ArrayIndexOf, keyLocaleData, "true"); - - // Step 11.g.ii.2.b. - if (valuePos !== -1) + if (callFunction(ArrayIndexOf, keyLocaleData, "true") !== -1) { value = "true"; + supportedExtensionAddition = "-" + key; + } } } } // Options override all. - // Step 11.h.i. + // Step 8.i.i. var optionsValue = options[key]; - // Step 11.h, 11.h.ii. - if (optionsValue !== undefined && - callFunction(ArrayIndexOf, keyLocaleData, optionsValue) !== -1) - { - // Step 11.h.ii.1. - if (optionsValue !== value) { + // Step 8.i.ii. + assert(typeof optionsValue === "string" || + optionsValue === undefined || + optionsValue === null, + "unexpected type for options value"); + + // Steps 8.i, 8.i.iii.1. + if (optionsValue !== undefined && optionsValue !== value) { + // Steps 8.a-d. + if (keyLocaleData === undefined) + keyLocaleData = callFunction(localeDataProvider[key], null, foundLocale); + + // Step 8.i.iii. + if (callFunction(ArrayIndexOf, keyLocaleData, optionsValue) !== -1) { value = optionsValue; supportedExtensionAddition = ""; } } - // Steps 11.i-k. + // Locale data provides default value. + if (value === undefined) { + // Steps 8.a-f. + value = keyLocaleData === undefined + ? callFunction(localeDataProvider.default[key], null, foundLocale) + : keyLocaleData[0]; + } + + // Step 8.j. + assert(typeof value === "string" || value === null, "unexpected locale data value"); result[key] = value; + + // Step 8.k. supportedExtension += supportedExtensionAddition; - i++; } - // Step 12. + // Step 9. if (supportedExtension.length > 2) { - var preExtension = callFunction(String_substring, foundLocale, 0, extensionIndex); - var postExtension = callFunction(String_substring, foundLocale, extensionIndex); - foundLocale = preExtension + supportedExtension + postExtension; + assert(!callFunction(std_String_startsWith, foundLocale, "x-"), + "unexpected privateuse-only locale returned from ICU"); + + // Step 9.a. + var privateIndex = callFunction(std_String_indexOf, foundLocale, "-x-"); + + // Steps 9.b-c. + if (privateIndex === -1) { + foundLocale += supportedExtension; + } else { + var preExtension = callFunction(String_substring, foundLocale, 0, privateIndex); + var postExtension = callFunction(String_substring, foundLocale, privateIndex); + foundLocale = preExtension + supportedExtension + postExtension; + } + + // Step 9.d-e (Step 9.e is not required in this implementation, because we don't + // canonicalize Unicode extension subtags). + assertIsValidAndCanonicalLanguageTag(foundLocale, "same locale with extension"); } - // Steps 13-14. + // Step 10. result.locale = foundLocale; + + // Step 11. return result; } @@ -904,31 +534,29 @@ function ResolveLocale(availableLocales, requestedLocales, options, relevantExte * matching (possibly fallback) locale. Locales appear in the same order in the * returned list as in the input list. * - * Spec: ECMAScript Internationalization API Specification, 9.2.6. + * Spec: ECMAScript Internationalization API Specification, 9.2.7. */ function LookupSupportedLocales(availableLocales, requestedLocales) { - // Steps 1-2. - var len = requestedLocales.length; - var subset = new List(); + // Step 1. + var subset = []; - // Steps 3-4. - var k = 0; - while (k < len) { - // Steps 4.a-b. - var locale = requestedLocales[k]; + // Step 2. + for (var i = 0; i < requestedLocales.length; i++) { + var locale = requestedLocales[i]; + + // Step 2.a. var noExtensionsLocale = removeUnicodeExtensions(locale); - // Step 4.c-d. + // Step 2.b. var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); - if (availableLocale !== undefined) - callFunction(std_Array_push, subset, locale); - // Step 4.e. - k++; + // Step 2.c. + if (availableLocale !== undefined) + _DefineDataProperty(subset, subset.length, locale); } - // Steps 5-6. - return callFunction(std_Array_slice, subset, 0); + // Step 3. + return subset; } @@ -937,7 +565,7 @@ function LookupSupportedLocales(availableLocales, requestedLocales) { * matching (possibly fallback) locale. Locales appear in the same order in the * returned list as in the input list. * - * Spec: ECMAScript Internationalization API Specification, 9.2.7. + * Spec: ECMAScript Internationalization API Specification, 9.2.8. */ function BestFitSupportedLocales(availableLocales, requestedLocales) { // don't have anything better @@ -950,19 +578,17 @@ function BestFitSupportedLocales(availableLocales, requestedLocales) { * matching (possibly fallback) locale. Locales appear in the same order in the * returned list as in the input list. * - * Spec: ECMAScript Internationalization API Specification, 9.2.8. + * Spec: ECMAScript Internationalization API Specification, 9.2.9. */ function SupportedLocales(availableLocales, requestedLocales, options) { - /*jshint laxbreak: true */ - // Step 1. var matcher; if (options !== undefined) { - // Steps 1.a-b. + // Step 1.a. options = ToObject(options); - matcher = options.localeMatcher; - // Step 1.c. + // Step 1.b + matcher = options.localeMatcher; if (matcher !== undefined) { matcher = ToString(matcher); if (matcher !== "lookup" && matcher !== "best fit") @@ -970,12 +596,12 @@ function SupportedLocales(availableLocales, requestedLocales, options) { } } - // Steps 2-3. + // Steps 2-5. var subset = (matcher === undefined || matcher === "best fit") ? BestFitSupportedLocales(availableLocales, requestedLocales) : LookupSupportedLocales(availableLocales, requestedLocales); - // Step 4. + // Steps 6-7. for (var i = 0; i < subset.length; i++) { _DefineDataProperty(subset, i, subset[i], ATTR_ENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); @@ -983,7 +609,7 @@ function SupportedLocales(availableLocales, requestedLocales, options) { _DefineDataProperty(subset, "length", subset.length, ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); - // Step 5. + // Step 8. return subset; } @@ -993,7 +619,7 @@ function SupportedLocales(availableLocales, requestedLocales, options) { * the required type, checks whether it is one of a list of allowed values, * and fills in a fallback value if necessary. * - * Spec: ECMAScript Internationalization API Specification, 9.2.9. + * Spec: ECMAScript Internationalization API Specification, 9.2.10. */ function GetOption(options, property, type, values, fallback) { // Step 1. @@ -1049,26 +675,38 @@ function GetNumberOption(options, property, minimum, maximum, fallback) { } +// Symbols in the self-hosting compartment can't be cloned, use a separate +// object to hold the actual symbol value. +// TODO: Can we add support to clone symbols? +var intlFallbackSymbolHolder = { value: undefined }; + /** - * 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. - * Presence of an object as a key within this map indicates that the object has - * its [[initializedIntlObject]] internal property set to true. The associated - * value is an object whose structure is documented in |initializeIntlObject| - * below. + * The [[FallbackSymbol]] symbol of the %Intl% intrinsic object. * - * Ideally we'd be using private symbols for internal properties, but - * SpiderMonkey doesn't have those yet. + * This symbol is used to implement the legacy constructor semantics for + * Intl.DateTimeFormat and Intl.NumberFormat. */ -var internalsMap = new WeakMap(); +function intlFallbackSymbol() { + var fallbackSymbol = intlFallbackSymbolHolder.value; + if (!fallbackSymbol) + intlFallbackSymbolHolder.value = fallbackSymbol = std_Symbol(); + return fallbackSymbol; +} /** - * Set the [[initializedIntlObject]] internal property of |obj| to true. + * Initializes the INTL_INTERNALS_OBJECT_SLOT of the given object. */ -function initializeIntlObject(obj) { +function initializeIntlObject(obj, type, lazyData) { assert(IsObject(obj), "Non-object passed to initializeIntlObject"); + assert((type === "Collator" && IsCollator(obj)) || + (type === "DateTimeFormat" && IsDateTimeFormat(obj)) || + (type === "NumberFormat" && IsNumberFormat(obj)) || + (type === "PluralRules" && IsPluralRules(obj)) || + (type === "RelativeTimeFormat" && IsRelativeTimeFormat(obj)), + "type must match the object's class"); + assert(IsObject(lazyData), "non-object lazy data"); // Intl-initialized objects are weird. They have [[initializedIntlObject]] // set on them, but they don't *necessarily* have any other properties. @@ -1076,51 +714,26 @@ function initializeIntlObject(obj) { // The meaning of an internals object for an object |obj| is as follows. // - // If the .type is "partial", |obj| has [[initializedIntlObject]] set but - // nothing else. No other property of |internals| can be used. (This - // occurs when InitializeCollator or similar marks an object as - // [[initializedIntlObject]] but fails before marking it as the appropriate - // more-specific type ["Collator", "DateTimeFormat", "NumberFormat"].) + // The .type property indicates the type of Intl object that |obj| is: + // "Collator", "DateTimeFormat", "NumberFormat", or "PluralRules" (likely + // with more coming in future Intl specs). // - // Otherwise, the .type indicates the type of Intl object that |obj| is: - // "Collator", "DateTimeFormat", or "NumberFormat" (likely with more coming - // in future Intl specs). In these cases |obj| *conceptually* also has - // [[initializedCollator]] or similar set, and all the other properties - // implied by that. - // - // If |internals| doesn't have a "partial" .type, two additional properties - // have meaning. The .lazyData property stores information needed to - // compute -- without observable side effects -- the actual internal Intl - // properties of |obj|. If it is non-null, then the actual internal - // properties haven't been computed, and .lazyData must be processed by + // The .lazyData property stores information needed to compute -- without + // observable side effects -- the actual internal Intl properties of + // |obj|. If it is non-null, then the actual internal properties haven't + // been computed, and .lazyData must be processed by // |setInternalProperties| before internal Intl property values are // available. If it is null, then the .internalProps property contains an // object whose properties are the internal Intl properties of |obj|. - internals.type = "partial"; - internals.lazyData = null; + var internals = std_Object_create(null); + internals.type = type; + internals.lazyData = lazyData; internals.internalProps = null; - callFunction(std_WeakMap_set, internalsMap, obj, internals); - return internals; -} - - -/** - * Mark |internals| as having the given type and lazy data. - */ -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" || type === "PluralRules" || - type === "RelativeTimeFormat", - "bad type"); - assert(IsObject(lazyData), "non-object lazy data"); - - // Set in reverse order so that the .type change is a barrier. - internals.lazyData = lazyData; - internals.type = type; + assert(UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT) === null, + "Internal slot already initialized?"); + UnsafeSetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT, internals); } @@ -1128,9 +741,7 @@ function setLazyData(internals, type, lazyData) * Set the internal properties object for an |internals| object previously * associated with lazy data. */ -function setInternalProperties(internals, internalProps) -{ - assert(internals.type !== "partial", "newborn internals can't have computed internals"); +function setInternalProperties(internals, internalProps) { assert(IsObject(internals.lazyData), "lazy data must exist already"); assert(IsObject(internalProps), "internalProps argument should be an object"); @@ -1144,10 +755,8 @@ function setInternalProperties(internals, internalProps) * Get the existing internal properties out of a non-newborn |internals|, or * null if none have been computed. */ -function maybeInternalProperties(internals) -{ +function maybeInternalProperties(internals) { assert(IsObject(internals), "non-object passed to maybeInternalProperties"); - assert(internals.type !== "partial", "maybeInternalProperties must only be used on completely-initialized internals objects"); var lazyData = internals.lazyData; if (lazyData) return null; @@ -1157,48 +766,33 @@ function maybeInternalProperties(internals) /** - * Return whether |obj| has an[[initializedIntlObject]] property set to true. - */ -function isInitializedIntlObject(obj) { -#ifdef DEBUG - var internals = callFunction(std_WeakMap_get, internalsMap, 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" || - type === "PluralRules" || type === "RelativeTimeFormat", - "unexpected type"); - assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData"); - assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps"); - } else { - assert(internals === undefined, "bad mapping for |obj|"); - } -#endif - return callFunction(std_WeakMap_has, internalsMap, obj); -} - - -/** - * Check that |obj| meets the requirements for "this Collator object", "this - * NumberFormat object", or "this DateTimeFormat object" as used in the method - * with the given name. Throw a TypeError if |obj| doesn't meet these - * requirements. But if it does, return |obj|'s internals object (*not* the - * object holding its internal properties!), associated with it by - * |internalsMap|, with structure specified above. + * Return |obj|'s internals object (*not* the object holding its internal + * properties!), with structure specified above. * * Spec: ECMAScript Internationalization API Specification, 10.3. * Spec: ECMAScript Internationalization API Specification, 11.3. * Spec: ECMAScript Internationalization API Specification, 12.3. */ -function getIntlObjectInternals(obj, className, methodName) { - assert(typeof className === "string", "bad className for getIntlObjectInternals"); +function getIntlObjectInternals(obj) { + assert(IsObject(obj), "getIntlObjectInternals called with non-Object"); + assert(IsCollator(obj) || IsDateTimeFormat(obj) || IsNumberFormat(obj) || + IsPluralRules(obj) || IsRelativeTimeFormat(obj), + "getIntlObjectInternals called with non-Intl object"); - var internals = callFunction(std_WeakMap_get, internalsMap, obj); - assert(internals === undefined || isInitializedIntlObject(obj), "bad mapping in internalsMap"); + var internals = UnsafeGetReservedSlot(obj, INTL_INTERNALS_OBJECT_SLOT); - if (internals === undefined || internals.type !== className) - ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, className, methodName, className); + assert(IsObject(internals), "internals not an object"); + assert(hasOwn("type", internals), "missing type"); + assert((internals.type === "Collator" && IsCollator(obj)) || + (internals.type === "DateTimeFormat" && IsDateTimeFormat(obj)) || + (internals.type === "NumberFormat" && IsNumberFormat(obj)) || + (internals.type === "PluralRules" && IsPluralRules(obj)) || + (internals.type === "RelativeTimeFormat" && IsRelativeTimeFormat(obj)), + "type must match the object's class"); + assert(hasOwn("lazyData", internals), + "missing lazyData"); + assert(hasOwn("internalProps", internals), + "missing internalProps"); return internals; } @@ -1208,35 +802,32 @@ function getIntlObjectInternals(obj, className, methodName) { * Get the internal properties of known-Intl object |obj|. For use only by * C++ code that knows what it's doing! */ -function getInternals(obj) -{ - assert(isInitializedIntlObject(obj), "for use only on guaranteed Intl objects"); +function getInternals(obj) { + var internals = getIntlObjectInternals(obj); - var internals = callFunction(std_WeakMap_get, internalsMap, obj); + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; - assert(internals.type !== "partial", "must have been successfully initialized"); - var lazyData = internals.lazyData; - if (!lazyData) - return internals.internalProps; - - var internalProps; + // Otherwise it's time to fully create them. var type = internals.type; - + switch (type) { case "Collator": - internalProps = resolveCollatorInternals(lazyData); + internalProps = resolveCollatorInternals(internals.lazyData); break; case "DateTimeFormat": - internalProps = resolveDateTimeFormatInternals(lazyData); + internalProps = resolveDateTimeFormatInternals(internals.lazyData); break; case "PluralRules": - internalProps = resolvePluralRulesInternals(lazyData); + internalProps = resolvePluralRulesInternals(internals.lazyData); break; - case "RelativeTimeFormat": - internalProps = resolveRelativeTimeFormatInternals(lazyData); + case "NumberFormat": + internalProps = resolveNumberFormatInternals(internals.lazyData); break; - default: // type === "NumberFormat" - internalProps = resolveNumberFormatInternals(lazyData); + default: // type === "RelativeTimeFormat" + internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); break; } setInternalProperties(internals, internalProps); diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp index 511c788dcb..1cd18c87ec 100644 --- a/js/src/builtin/intl/DateTimeFormat.cpp +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -9,12 +9,14 @@ #include "mozilla/Assertions.h" #include "mozilla/Range.h" +#include "mozilla/Span.h" #include "jscntxt.h" #include "jsfriendapi.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" #include "builtin/intl/ScopedICUObject.h" #include "builtin/intl/SharedIntlData.h" #include "builtin/intl/TimeZoneDataGenerated.h" @@ -33,7 +35,7 @@ using JS::ClippedTime; using JS::TimeClip; using js::intl::CallICU; -using js::intl::GetAvailableLocales; +using js::intl::DateTimeFormatOptions; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::SharedIntlData; @@ -89,72 +91,58 @@ 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) { - RootedObject obj(cx); + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - // 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)) + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) return false; - args.rval().setObject(*obj); - return true; + if (!proto) { + proto = GlobalObject::getOrCreateDateTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted dateTimeFormat(cx); + dateTimeFormat = NewObjectWithGivenProto(cx, proto); + if (!dateTimeFormat) + return false; + + dateTimeFormat->setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); + dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, + PrivateValue(nullptr)); + + RootedValue thisValue(cx, construct ? ObjectValue(*dateTimeFormat) : args.thisv()); + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + return intl::LegacyIntlInitialize(cx, dateTimeFormat, cx->names().InitializeDateTimeFormat, + 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 @@ -166,7 +154,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 @@ -174,30 +162,25 @@ 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); - } + if (UDateFormat* df = static_cast(slot.toPrivate())) + udat_close(df); } JSObject* -js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + 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; - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &DateTimeFormatObject::class_)); + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); if (!proto) return nullptr; - proto->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; @@ -226,51 +209,60 @@ js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().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; + constructor.set(ctor); return proto; } bool -js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +js::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) { - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); + Handle global = cx->global(); - RootedValue result(cx); - if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) + RootedObject mozDateTimeFormat(cx); + JSObject* mozDateTimeFormatProto = + CreateDateTimeFormatPrototype(cx, intl, global, &mozDateTimeFormat, DateTimeFormatOptions::EnableMozExtensions); + return mozDateTimeFormatProto != nullptr; +} + +static bool +DefaultCalendar(JSContext* cx, const JSAutoByteString& locale, MutableHandleValue rval) +{ + UErrorCode status = U_ZERO_ERROR; + 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)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); return false; - args.rval().set(result); + } + + // ICU returns old-style keyword values; map them to BCP 47 equivalents + JSString* str = JS_NewStringCopyZ(cx, uloc_toUnicodeLocaleType("ca", calendar)); + if (!str) + return false; + + rval.setString(str); 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) +struct CalendarAlias { - if (StringsAreEqual(icuName, "ethiopic-amete-alem")) - return "ethioaa"; - if (StringsAreEqual(icuName, "gregorian")) - return "gregory"; - if (StringsAreEqual(icuName, "islamic-civil")) - return "islamicc"; - return icuName; -} + const char* const calendar; + const char* const alias; +}; + +const CalendarAlias calendarAliases[] = { + { "islamic-civil", "islamicc" }, + { "ethioaa", "ethiopic-amete-alem" } +}; bool js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) @@ -289,30 +281,15 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) 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); + RootedValue element(cx); + if (!DefaultCalendar(cx, locale, &element)) + return false; - // 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. + UErrorCode status = U_ZERO_ERROR; UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); @@ -333,18 +310,47 @@ js::intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp) return false; } - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + // ICU returns old-style keyword values; map them to BCP 47 equivalents + calendar = uloc_toUnicodeLocaleType("ca", calendar); + + JSString* jscalendar = JS_NewStringCopyZ(cx, calendar); if (!jscalendar) return false; element = StringValue(jscalendar); if (!DefineElement(cx, calendars, index++, element)) return false; + + // ICU doesn't return calendar aliases, append them here. + for (const auto& calendarAlias : calendarAliases) { + if (StringsAreEqual(calendar, calendarAlias.calendar)) { + JSString* jscalendar = JS_NewStringCopyZ(cx, calendarAlias.alias); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + } + } } args.rval().setObject(*calendars); return true; } +bool +js::intl_defaultCalendar(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; + + return DefaultCalendar(cx, locale, args.rval()); +} + bool js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) { @@ -448,13 +454,139 @@ js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { return true; } +enum class HourCycle { + // 12 hour cycle, from 0 to 11. + H11, + + // 12 hour cycle, from 1 to 12. + H12, + + // 24 hour cycle, from 0 to 23. + H23, + + // 24 hour cycle, from 1 to 24. + H24 +}; + +static bool +IsHour12(HourCycle hc) +{ + return hc == HourCycle::H11 || hc == HourCycle::H12; +} + +static char16_t +HourSymbol(HourCycle hc) +{ + switch (hc) { + case HourCycle::H11: + return 'K'; + case HourCycle::H12: + return 'h'; + case HourCycle::H23: + return 'H'; + case HourCycle::H24: + return 'k'; + } + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("unexpected hour cycle"); +} + +/** +* Parse a pattern according to the format specified in +* . +*/ +template +class PatternIterator { + CharT* iter_; + const CharT* const end_; + + public: + explicit PatternIterator(mozilla::Span pattern) + : iter_(pattern.data()), end_(pattern.data() + pattern.size()) {} + + CharT* next() { + MOZ_ASSERT(iter_ != nullptr); + + bool inQuote = false; + while (iter_ < end_) { + CharT* cur = iter_++; + if (*cur == '\'') { + inQuote = !inQuote; + } else if (!inQuote) { + return cur; + } + } + + iter_ = nullptr; + return nullptr; + } +}; + +/** +* Return the hour cycle for the given option string. +*/ +static HourCycle +HourCycleFromOption(JSLinearString* str) +{ + if (StringEqualsAscii(str, "h11")) { + return HourCycle::H11; + } + if (StringEqualsAscii(str, "h12")) { + return HourCycle::H12; + } + if (StringEqualsAscii(str, "h23")) { + return HourCycle::H23; + } + MOZ_ASSERT(StringEqualsAscii(str, "h24")); + return HourCycle::H24; +} + +/** +* Return the hour cycle used in the input pattern or Nothing if none was found. +*/ +static mozilla::Maybe +HourCycleFromPattern(mozilla::Span pattern) +{ + PatternIterator iter(pattern); + while (const auto* ptr = iter.next()) { + switch (*ptr) { + case 'K': + return mozilla::Some(HourCycle::H11); + case 'h': + return mozilla::Some(HourCycle::H12); + case 'H': + return mozilla::Some(HourCycle::H23); + case 'k': + return mozilla::Some(HourCycle::H24); + } + } + return mozilla::Nothing(); +} + +/** +* Replaces all hour pattern characters in |pattern| to use the matching hour +* representation for |hourCycle|. +*/ +static void +ReplaceHourSymbol(mozilla::Span pattern, HourCycle hc) +{ + char16_t replacement = HourSymbol(hc); + PatternIterator iter(pattern); + while (auto* ptr = iter.next()) { + char16_t ch = *ptr; + if (ch == 'K' || ch == 'h' || ch == 'H' || ch == 'k') { + *ptr = replacement; + } + } +} + bool js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args.length() == 3); MOZ_ASSERT(args[0].isString()); MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); JSAutoByteString locale(cx, args[0].toString()); if (!locale) @@ -468,6 +600,16 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) if (!stableChars.initTwoByte(cx, skeletonFlat)) return false; + mozilla::Maybe hourCycle; + if (args[2].isString()) { + JSLinearString* hourCycleStr = args[2].toString()->ensureLinear(cx); + if (!hourCycleStr) { + return false; + } + + hourCycle.emplace(HourCycleFromOption(hourCycleStr)); + } + mozilla::Range skeletonChars = stableChars.twoByteRange(); uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); @@ -479,11 +621,220 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) } 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); + Vector pattern(cx); + + int32_t patternSize = CallICU( + cx, + pattern, + [gen, &skeletonChars](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), + skeletonChars.length(), chars, size, status); }); + if (patternSize < 0) { + return false; + } + + // If the hourCycle option was set, adjust the resolved pattern to use the + // requested hour cycle representation. + if (hourCycle) { + ReplaceHourSymbol(pattern, hourCycle.value()); + } + + JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); + if (!str) { + return false; + } + args.rval().setString(str); + return true; +} + +/** + * Find a matching pattern using the requested hour-12 options. + * + * This function is needed to work around the following two issues. + * - https://unicode-org.atlassian.net/browse/ICU-21023 + * - https://unicode-org.atlassian.net/browse/CLDR-13425 + * + * We're currently using a relatively simple workaround, which doesn't give the + * most accurate results. For example: + * + * ``` + * var dtf = new Intl.DateTimeFormat("en", { + * timeZone: "UTC", + * dateStyle: "long", + * timeStyle: "long", + * hourCycle: "h12", + * }); + * print(dtf.format(new Date("2020-01-01T00:00Z"))); + * ``` + * + * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through + * |udatpg_getSkeleton| and then |udatpg_getBestPattern| to find an equivalent + * pattern for "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so + * the combinator element " 'at' " was lost in the process. + */ +template +static bool +FindPatternWithHourCycle(JSContext* cx, const char* locale, + Vector& pattern, bool hour12) +{ + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + if (!gen) { + return false; + } + + Vector skeleton(cx); + + int32_t skeletonSize = CallICU( + cx, + skeleton, + [&pattern](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getSkeleton(nullptr, pattern.begin(), pattern.length(), + chars, size, status); + }); + if (skeletonSize < 0) { + return false; + } + + // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". + ReplaceHourSymbol(skeleton, hour12 ? HourCycle::H12 : HourCycle::H23); + + MOZ_ALWAYS_TRUE(pattern.resize(0)); + + int32_t patternSize = CallICU( + cx, + pattern, + [gen, &skeleton](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeleton.begin(), skeleton.length(), + chars, size, status); + }); + if (patternSize < 0) { + return false; + } + + return true; +} + +bool +js::intl_patternForStyle(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 6); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString() || args[1].isUndefined()); + MOZ_ASSERT(args[2].isString() || args[2].isUndefined()); + MOZ_ASSERT(args[3].isString()); + MOZ_ASSERT(args[4].isBoolean() || args[4].isUndefined()); + MOZ_ASSERT(args[5].isString() || args[5].isUndefined()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + auto toDateFormatStyle = [](JSLinearString* str) { + if (StringEqualsAscii(str, "full")) { + return UDAT_FULL; + } + if (StringEqualsAscii(str, "long")) { + return UDAT_LONG; + } + if (StringEqualsAscii(str, "medium")) { + return UDAT_MEDIUM; + } + MOZ_ASSERT(StringEqualsAscii(str, "short")); + return UDAT_SHORT; + }; + + UDateFormatStyle dateStyle = UDAT_NONE; + + if (args[1].isString()) { + JSLinearString* dateStyleStr = args[1].toString()->ensureLinear(cx); + if (!dateStyleStr) + return false; + + dateStyle = toDateFormatStyle(dateStyleStr); + } + + UDateFormatStyle timeStyle = UDAT_NONE; + if (args[2].isString()) { + JSLinearString* timeStyleStr = args[2].toString()->ensureLinear(cx); + if (!timeStyleStr) + return false; + + timeStyle = toDateFormatStyle(timeStyleStr); + } + + AutoStableStringChars timeZone(cx); + if (!timeZone.initTwoByte(cx, args[3].toString())) + return false; + + mozilla::Maybe hour12; + if (args[4].isBoolean()) { + hour12.emplace(args[4].toBoolean()); + } + + mozilla::Maybe hourCycle; + if (args[5].isString()) { + JSLinearString* hourCycleStr = args[5].toString()->ensureLinear(cx); + if (!hourCycleStr) { + return false; + } + + hourCycle.emplace(HourCycleFromOption(hourCycleStr)); + } + + 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); + + Vector pattern(cx); + + int32_t patternSize = CallICU( + cx, + pattern, + [df](UChar* chars, uint32_t size, UErrorCode* status) { + return udat_toPattern(df, false, chars, size, status); + }); + if (patternSize < 0) { + return false; + } + + // If a specific hour cycle was requested and this hour cycle doesn't match + // the hour cycle used in the resolved pattern, find an equivalent pattern + // with the correct hour cycle. + if (timeStyle != UDAT_NONE && (hour12 || hourCycle)) { + if (auto hcPattern = HourCycleFromPattern(pattern)) { + bool wantHour12 = hour12 ? hour12.value() : IsHour12(hourCycle.value()); + if (wantHour12 != IsHour12(hcPattern.value())) { + if (!FindPatternWithHourCycle(cx, locale.ptr(), pattern, wantHour12)) { + return false; + } + } + } + } + + // If the hourCycle option was set, adjust the resolved pattern to use the + // requested hour cycle representation. + if (hourCycle) { + ReplaceHourSymbol(pattern, hourCycle.value()); + } + + JSString* str = NewStringCopyN(cx, pattern.begin(), pattern.length()); if (!str) return false; args.rval().setString(str); @@ -495,7 +846,7 @@ js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) * of the given DateTimeFormat. */ static UDateFormat* -NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) +NewUDateFormat(JSContext* cx, Handle dateTimeFormat) { RootedValue value(cx); @@ -505,13 +856,56 @@ NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) + + // ICU expects calendar and numberingSystem as Unicode locale extensions on + // locale. + + intl::LanguageTag tag(cx); + { + JSLinearString* locale = value.toString()->ensureLinear(cx); + if (!locale) + return nullptr; + + if (!intl::LanguageTagParser::parse(cx, locale, tag)) + return nullptr; + } + + JS::RootedVector keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().calendar, &value)) 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. + { + JSLinearString* calendar = value.toString()->ensureLinear(cx); + if (!calendar) + return nullptr; + + if (!keywords.emplaceBack("ca", calendar)) + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) + return nullptr; + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) + return nullptr; + + if (!keywords.emplaceBack("nu", numberingSystem)) + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) + return nullptr; + + UniqueChars locale = tag.toStringZ(cx); + if (!locale) + return nullptr; if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) return nullptr; @@ -537,7 +931,7 @@ 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.get()), uTimeZone, uTimeZoneLength, uPattern, uPatternLength, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); @@ -558,7 +952,8 @@ static bool intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) { if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, + "DateTimeFormat", "format"); return false; } @@ -664,7 +1059,8 @@ static bool intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) { if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, + "DateTimeFormat", "formatToParts"); return false; } @@ -777,42 +1173,23 @@ js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args[1].isNumber()); MOZ_ASSERT(args[2].isBoolean()); - RootedObject dateTimeFormat(cx, &args[0].toObject()); + Rooted dateTimeFormat(cx); + dateTimeFormat = &args[0].toObject().as(); - // 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. + // Obtain a cached UDateFormat object. + void* priv = + dateTimeFormat->getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); + UDateFormat* df = static_cast(priv); + if (!df) { df = NewUDateFormat(cx, dateTimeFormat); if (!df) return false; + dateTimeFormat->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); } // 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; + return args[2].toBoolean() + ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), args.rval()) + : intl_FormatDateTime(cx, df, args[1].toNumber(), args.rval()); } diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h index e9617c0e06..034f08cc82 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); + JS::Handle global, MutableHandleObject constructor, + intl::DateTimeFormatOptions dtfOptions); /** * Returns a new instance of the standard built-in DateTimeFormat constructor. @@ -53,17 +54,6 @@ CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, 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 @@ -75,6 +65,16 @@ intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); +/** + * Returns the calendar type identifier per Unicode Technical Standard 35, + * Unicode Locale Data Markup Language, for the default calendar for the given + * locale. + * + * Usage: calendar = intl_defaultCalendar(locale) + */ +extern MOZ_MUST_USE bool +intl_defaultCalendar(JSContext* cx, unsigned argc, Value* vp); + /** * 6.4.1 IsValidTimeZoneName ( timeZone ) * @@ -119,11 +119,45 @@ intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); * best-fit date-time format pattern corresponding to skeleton for the * given locale. * - * Usage: pattern = intl_patternForSkeleton(locale, skeleton) + * Usage: pattern = intl_patternForSkeleton(locale, skeleton, hourCycle) */ 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 six 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 + * hour12 + * A boolean to request hour12 representation, or `undefined` + * hourCycle + * A string with values: h11, h12, h23, or h24, or `undefined` + * + * 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, + * hour12, hourCycle) + */ +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 @@ -131,7 +165,7 @@ intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); * * Spec: ECMAScript Internationalization API Specification, 12.3.2. * - * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) + * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x, formatToParts) */ extern MOZ_MUST_USE bool intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js index 75d7ead61a..2d04dc9150 100644 --- a/js/src/builtin/intl/DateTimeFormat.js +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -20,7 +20,11 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { // { // localeMatcher: "lookup" / "best fit", // - // hour12: true / false, // optional + // ca: string matching a Unicode extension type, // optional + // + // nu: string matching a Unicode extension type, // optional + // + // hc: "h11" / "h12" / "h23" / "h24", // optional // } // // timeZone: IANA time zone name, @@ -29,9 +33,18 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { // { // // all the properties/values listed in Table 3 // // (weekday, era, year, month, day, &c.) + // + // hour12: true / false, // optional // } // // formatMatcher: "basic" / "best fit", + // + // 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, @@ -39,22 +52,22 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { // 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; + // Compute effective locale. // Step 10. - var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + var localeData = DateTimeFormat.localeData; + + // Step 11. + var r = ResolveLocale("DateTimeFormat", lazyDateTimeFormatData.requestedLocales, lazyDateTimeFormatData.localeOpt, DateTimeFormat.relevantExtensionKeys, localeData); - // Steps 11-13. + // Steps 12-13, 15. internalProps.locale = r.locale; internalProps.calendar = r.ca; internalProps.numberingSystem = r.nu; @@ -63,26 +76,43 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { // Step 14. var dataLocale = r.dataLocale; - // Steps 15-17. - var tz = lazyDateTimeFormatData.timeZone; - if (tz === undefined) { - // Step 16. - tz = DefaultTimeZone(); - } - internalProps.timeZone = tz; + // Steps 20. + internalProps.timeZone = lazyDateTimeFormatData.timeZone; - // Step 18. + // Step 21. var formatOpt = lazyDateTimeFormatData.formatOpt; - // Steps 27-28, more or less - see comment after this function. - var pattern = toBestICUPattern(dataLocale, formatOpt); + // Step 16. + // Copy the hourCycle setting, if present, to the format options. But + // only do this if no hour12 option is present, because the latter takes + // precedence over hourCycle. + if (r.hc !== null && formatOpt.hour12 === undefined) + formatOpt.hourCycle = r.hc; - // Step 29. + // Steps 26-30, more or less - see comment after this function. + var pattern; + if (lazyDateTimeFormatData.patternOption !== undefined) { + pattern = lazyDateTimeFormatData.patternOption; + + internalProps.patternOption = lazyDateTimeFormatData.patternOption; + } else if (lazyDateTimeFormatData.dateStyle !== undefined || + lazyDateTimeFormatData.timeStyle !== undefined) { + pattern = intl_patternForStyle(dataLocale, + lazyDateTimeFormatData.dateStyle, + lazyDateTimeFormatData.timeStyle, + lazyDateTimeFormatData.timeZone, + formatOpt.hour12, + formatOpt.hourCycle); + + internalProps.dateStyle = lazyDateTimeFormatData.dateStyle; + internalProps.timeStyle = lazyDateTimeFormatData.timeStyle; + } else { + pattern = toBestICUPattern(dataLocale, formatOpt); + } + + // Step 31. internalProps.pattern = pattern; - // Step 30. - internalProps.boundFormat = undefined; - // The caller is responsible for associating |internalProps| with the right // object using |setInternalProperties|. return internalProps; @@ -90,11 +120,13 @@ function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { /** - * Returns an object containing the DateTimeFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't DateTimeFormat-initialized. + * Returns an object containing the DateTimeFormat internal properties of |obj|. */ -function getDateTimeFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "DateTimeFormat", methodName); +function getDateTimeFormatInternals(obj) { + assert(IsObject(obj), "getDateTimeFormatInternals called with non-object"); + assert(IsDateTimeFormat(obj), "getDateTimeFormatInternals called with non-DateTimeFormat"); + + var internals = getIntlObjectInternals(obj); assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals"); // If internal properties have already been computed, use them. @@ -214,6 +246,31 @@ function DefaultTimeZone() { return defaultTimeZone; } + +/** + * 12.1.10 UnwrapDateTimeFormat( dtf ) + */ +function UnwrapDateTimeFormat(dtf, methodName) { + // Step 1 (not applicable in our implementation). + + // Step 2. + if ((!IsObject(dtf) || !IsDateTimeFormat(dtf)) && + dtf instanceof GetDateTimeFormatConstructor()) + { + dtf = dtf[intlFallbackSymbol()]; + } + + // Step 3. + if (!IsObject(dtf) || !IsDateTimeFormat(dtf)) { + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "DateTimeFormat", methodName, + "DateTimeFormat"); + } + + // Step 4. + return dtf; +} + + /** * Initializes an object as a DateTimeFormat. * @@ -225,15 +282,10 @@ function DefaultTimeZone() { * * 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); +function InitializeDateTimeFormat(dateTimeFormat, thisValue, locales, options, mozExtensions) { + assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat called with non-Object"); + assert(IsDateTimeFormat(dateTimeFormat), + "InitializeDateTimeFormat called with non-DateTimeFormat"); // Lazy DateTimeFormat data has the following structure: // @@ -243,6 +295,12 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { // localeOpt: // *first* opt computed in InitializeDateTimeFormat // { // localeMatcher: "lookup" / "best fit", + // + // ca: string matching a Unicode extension type, // optional + // + // nu: string matching a Unicode extension type, // optional + // + // hc: "h11" / "h12" / "h23" / "h24", // optional // } // // timeZone: IANA time zone name, @@ -252,7 +310,7 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { // // all the properties/values listed in Table 3 // // (weekday, era, year, month, day, &c.) // - // hour12: true / false // optional + // hour12: true / false, // optional // } // // formatMatcher: "basic" / "best fit", @@ -263,45 +321,89 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { // never a subset of them. var lazyDateTimeFormatData = std_Object_create(null); - // Step 3. + // Step 1. var requestedLocales = CanonicalizeLocaleList(locales); lazyDateTimeFormatData.requestedLocales = requestedLocales; - // Step 4. + // Step24. options = ToDateTimeOptions(options, "any", "date"); // Compute options that impact interpretation of locale. - // Step 5. + // Step 3. var localeOpt = new Record(); lazyDateTimeFormatData.localeOpt = localeOpt; - // Steps 6-7. + // Steps 4-5. var localeMatcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); localeOpt.localeMatcher = localeMatcher; - // Steps 15-17. + var calendar = GetOption(options, "calendar", "string", undefined, undefined); + + if (calendar !== undefined) { + calendar = intl_ValidateAndCanonicalizeUnicodeExtensionType(calendar, "calendar", "ca"); + } + + localeOpt.ca = calendar; + + var numberingSystem = GetOption(options, "numberingSystem", "string", undefined, undefined); + + if (numberingSystem !== undefined) { + numberingSystem = intl_ValidateAndCanonicalizeUnicodeExtensionType(numberingSystem, + "numberingSystem", + "nu"); + } + + localeOpt.nu = numberingSystem; + + // Step 6. + var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); + + // Step 7. + var hc = GetOption(options, "hourCycle", "string", ["h11", "h12", "h23", "h24"], undefined); + + // Step 8. + if (hr12 !== undefined) { + // The "hourCycle" option is ignored if "hr12" is also present. + hc = null; + } + + // Step 9. + localeOpt.hc = hc; + + // Steps 10-16 (see resolveDateTimeFormatInternals). + + // Steps 17-20. var tz = options.timeZone; if (tz !== undefined) { - // Step 15.a. + // Step 18.a. tz = ToString(tz); - // Step 15.b. + // Step 18.b. var timeZone = intl_IsValidTimeZoneName(tz); if (timeZone === null) ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); - // Step 15.c. + // Step 18.c. tz = CanonicalizeTimeZoneName(timeZone); + } else { + // Step 19. + tz = DefaultTimeZone(); } lazyDateTimeFormatData.timeZone = tz; - // Step 18. + // Step 21. var formatOpt = new Record(); lazyDateTimeFormatData.formatOpt = formatOpt; - // Step 19. + if (mozExtensions) { + let pattern = GetOption(options, "pattern", "string", undefined, undefined); + lazyDateTimeFormatData.patternOption = pattern; + } + + // Step 22. + // 12.1, Table 5: Components of date and time formats. var i, prop; for (i = 0; i < dateTimeComponents.length; i++) { prop = dateTimeComponents[i]; @@ -309,9 +411,9 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { formatOpt[prop] = value; } - // Steps 20-21 provided by ICU - see comment after this function. + // Steps 23-24 provided by ICU - see comment after this function. - // Step 22. + // Step 25. // // For some reason (ICU not exposing enough interface?) we drop the // requested format matcher on the floor after this. In any case, even if @@ -321,20 +423,58 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { GetOption(options, "formatMatcher", "string", ["basic", "best fit"], "best fit"); - // Steps 23-25 provided by ICU, more or less - see comment after this function. + // "DateTimeFormat dateStyle & timeStyle" propsal + // https://github.com/tc39/proposal-intl-datetime-style + var dateStyle = GetOption(options, "dateStyle", "string", ["full", "long", "medium", "short"], + undefined); + lazyDateTimeFormatData.dateStyle = dateStyle; - // Step 26. - var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); + var timeStyle = GetOption(options, "timeStyle", "string", ["full", "long", "medium", "short"], + undefined); + lazyDateTimeFormatData.timeStyle = timeStyle; + if (dateStyle !== undefined || timeStyle !== undefined) { + var optionsList = [ + "weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName", + ]; + + for (var i = 0; i < optionsList.length; i++) { + var option = optionsList[i]; + if (formatOpt[option] !== undefined) { + ThrowTypeError(JSMSG_INVALID_DATETIME_OPTION, option, + dateStyle !== undefined ? "dateStyle" : "timeStyle"); + } + } + } + + // Steps 26-28 provided by ICU, more or less - see comment after this function. + + // Steps 29-30. // Pass hr12 on to ICU. if (hr12 !== undefined) formatOpt.hour12 = hr12; - // Step 31. + // Step 32. // // We've done everything that must be done now: mark the lazy data as fully // computed and install it. - setLazyData(internals, "DateTimeFormat", lazyDateTimeFormatData); + initializeIntlObject(dateTimeFormat, "DateTimeFormat", lazyDateTimeFormatData); + + // 12.2.1, steps 4-5. + // TODO: spec issue - The current spec doesn't have the IsObject check, + // which means |Intl.DateTimeFormat.call(null)| is supposed to throw here. + if (dateTimeFormat !== thisValue && thisValue instanceof GetDateTimeFormatConstructor()) { + if (!IsObject(thisValue)) + ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, typeof thisValue); + + _DefineDataProperty(thisValue, intlFallbackSymbol(), dateTimeFormat, + ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); + + return thisValue; + } + + // 12.2.1, step 6. + return dateTimeFormat; } @@ -396,6 +536,7 @@ function InitializeDateTimeFormat(dateTimeFormat, locales, options) { // - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], // [[second]], [[timeZoneName]] // - [[hour12]] +// - [[hourCycle]] // - [[hourNo0]] // When needed for the resolvedOptions method, the resolveICUPattern function // maps the instance's ICU pattern back to the specified properties of the @@ -469,12 +610,24 @@ function toBestICUPattern(locale, options) { skeleton += "d"; break; } + // If hour12 and hourCycle are both present, hour12 takes precedence. var hourSkeletonChar = "j"; if (options.hour12 !== undefined) { if (options.hour12) hourSkeletonChar = "h"; else hourSkeletonChar = "H"; + } else { + switch (options.hourCycle) { + case "h11": + case "h12": + hourSkeletonChar = "h"; + break; + case "h23": + case "h24": + hourSkeletonChar = "H"; + break; + } } switch (options.hour) { case "2-digit": @@ -510,7 +663,7 @@ function toBestICUPattern(locale, options) { } // Let ICU convert the ICU skeleton to an ICU pattern for the given locale. - return intl_patternForSkeleton(locale, skeleton); + return intl_patternForSkeleton(locale, skeleton, options.hourCycle); } @@ -526,17 +679,19 @@ function ToDateTimeOptions(options, required, defaults) { assert(typeof required === "string", "ToDateTimeOptions"); assert(typeof defaults === "string", "ToDateTimeOptions"); - // Steps 1-3. + // Steps 1-2. if (options === undefined) options = null; else options = ToObject(options); options = std_Object_create(options); - // Step 4. + // Step 3. var needDefaults = true; - // Step 5. + // Step 4. + // TODO: spec issue - The spec requires to retrieve all options, so using + // the ||-operator with its lazy evaluation semantics is incorrect. if ((required === "date" || required === "any") && (options.weekday !== undefined || options.year !== undefined || options.month !== undefined || options.day !== undefined)) @@ -544,7 +699,9 @@ function ToDateTimeOptions(options, required, defaults) { needDefaults = false; } - // Step 6. + // Step 5. + // TODO: spec issue - The spec requires to retrieve all options, so using + // the ||-operator with its lazy evaluation semantics is incorrect. if ((required === "time" || required === "any") && (options.hour !== undefined || options.minute !== undefined || options.second !== undefined)) @@ -552,7 +709,21 @@ function ToDateTimeOptions(options, required, defaults) { needDefaults = false; } - // Step 7. + // "DateTimeFormat dateStyle & timeStyle" propsal + // https://github.com/tc39/proposal-intl-datetime-style + var dateStyle = options.dateStyle; + var timeStyle = options.timeStyle; + + if (dateStyle !== undefined || timeStyle !== undefined) + needDefaults = false; + + if (required === "date" && timeStyle !== undefined) + ThrowTypeError(JSMSG_INVALID_DATETIME_STYLE, "timeStyle", "toLocaleDateString"); + + if (required === "time" && dateStyle !== undefined) + ThrowTypeError(JSMSG_INVALID_DATETIME_STYLE, "dateStyle", "toLocaleTimeString"); + + // Step 6. 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 @@ -563,7 +734,7 @@ function ToDateTimeOptions(options, required, defaults) { _DefineDataProperty(options, "day", "numeric"); } - // Step 8. + // Step 7. if (needDefaults && (defaults === "time" || defaults === "all")) { // See comment for step 7. _DefineDataProperty(options, "hour", "numeric"); @@ -571,7 +742,7 @@ function ToDateTimeOptions(options, required, defaults) { _DefineDataProperty(options, "second", "numeric"); } - // Step 9. + // Step 8. return options; } @@ -623,7 +794,7 @@ function BasicFormatMatcher(options, formats) { formatProp = undefined; // Steps 11.c.ii-iii. - if (callFunction(std_Object_hasOwnProperty, format, property)) + if (hasOwn(property, format)) formatProp = format[property]; if (optionsProp === undefined && formatProp !== undefined) { @@ -681,14 +852,18 @@ function BestFitFormatMatcher(options, formats) { * 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. + * Spec: ECMAScript Internationalization API Specification, 12.3.2. */ function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; - var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties); + // Step 1. + var availableLocales = "DateTimeFormat"; + + // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. return SupportedLocales(availableLocales, requestedLocales, options); } @@ -696,29 +871,28 @@ function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { /** * DateTimeFormat internal properties. * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.2.3. + * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.3.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"] + relevantExtensionKeys: ["ca", "nu", "hc"] }; -function dateTimeFormatLocaleData(locale) { +function dateTimeFormatLocaleData() { return { - ca: intl_availableCalendars(locale), - nu: getNumberingSystems(locale) + ca: intl_availableCalendars, + nu: getNumberingSystems, + hc: () => { + return [null, "h11", "h12", "h23", "h24"]; + }, + default: { + ca: intl_defaultCalendar, + nu: intl_numberingSystem, + hc: () => { + return null; + } + } }; } @@ -726,7 +900,7 @@ function dateTimeFormatLocaleData(locale) { /** * Function to be bound and returned by Intl.DateTimeFormat.prototype.format. * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * Spec: ECMAScript Internationalization API Specification, 12.1.5. */ function dateTimeFormatFormatToBind() { // Steps 1.a.i-ii @@ -734,7 +908,7 @@ function dateTimeFormatFormatToBind() { var x = (date === undefined) ? std_Date_now() : ToNumber(date); // Step 1.a.iii. - return intl_FormatDateTime(this, x, false); + return intl_FormatDateTime(this, x, /* formatToParts = */ false); } /** @@ -742,82 +916,97 @@ function dateTimeFormatFormatToBind() { * 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. + * Spec: ECMAScript Internationalization API Specification, 12.4.3. */ function Intl_DateTimeFormat_format_get() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "format"); + // Steps 1-3. + var dtf = UnwrapDateTimeFormat(this, "format"); - // Step 1. + var internals = getDateTimeFormatInternals(dtf); + + // Step 4. if (internals.boundFormat === undefined) { - // Step 1.a. - var F = dateTimeFormatFormatToBind; + // Steps 4.a-b. + var F = callFunction(FunctionBind, dateTimeFormatFormatToBind, dtf); - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; + // Step 4.c. + internals.boundFormat = F; } - // Step 2. + // Step 5. return internals.boundFormat; } +_SetCanonicalName(Intl_DateTimeFormat_format_get, "get format"); +/** + * Intl.DateTimeFormat.prototype.formatToParts ( date ) + * + * Spec: ECMAScript Internationalization API Specification, 12.4.4. + */ function Intl_DateTimeFormat_formatToParts() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - getDateTimeFormatInternals(this, "formatToParts"); + // Steps 1-3. + var dtf = UnwrapDateTimeFormat(this, "formatToParts"); - // Steps 1.a.i-ii + // Ensure the DateTimeFormat internals are resolved. + getDateTimeFormatInternals(dtf); + + // Steps 4-5. 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); + // Step 6. + return intl_FormatDateTime(dtf, x, /* formatToParts = */ true); } /** * Returns the resolved options for a DateTimeFormat object. * - * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4. + * Spec: ECMAScript Internationalization API Specification, 12.4.5. */ function Intl_DateTimeFormat_resolvedOptions() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "resolvedOptions"); + // Steps 1-3. + var dtf = UnwrapDateTimeFormat(this, "resolvedOptions"); + var internals = getDateTimeFormatInternals(dtf); + + // Steps 4-5. var result = { locale: internals.locale, calendar: internals.calendar, numberingSystem: internals.numberingSystem, - timeZone: internals.timeZone + timeZone: internals.timeZone, }; - resolveICUPattern(internals.pattern, result); + + if (internals.patternOption !== undefined) { + _DefineDataProperty(result, "pattern", internals.pattern); + } + + var hasDateStyle = internals.dateStyle !== undefined; + var hasTimeStyle = internals.timeStyle !== undefined; + + if (hasDateStyle || hasTimeStyle) { + if (hasTimeStyle) { + // timeStyle (unlike dateStyle) requires resolving the pattern to + // ensure "hourCycle" and "hour12" properties are added to |result|. + resolveICUPattern(internals.pattern, result, /* includeDateTimeFields = */ false); + } + if (hasDateStyle) { + _DefineDataProperty(result, "dateStyle", internals.dateStyle); + } + if (hasTimeStyle) { + _DefineDataProperty(result, "timeStyle", internals.timeStyle); + } + } else { + resolveICUPattern(internals.pattern, result, /* includeDateTimeFields = */ true); + } + + // Step 6. 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 @@ -825,8 +1014,12 @@ var icuPatternCharToComponent = { * interpretation of ICU pattern characters, see * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table */ -function resolveICUPattern(pattern, result) { +function resolveICUPattern(pattern, result, includeDateTimeFields) { assert(IsObject(result), "resolveICUPattern"); + + var hourCycle, weekday, era, year, month, day, hour, minute, second, + timeZoneName; + var i = 0; while (i < pattern.length) { var c = pattern[i++]; @@ -886,13 +1079,91 @@ function resolveICUPattern(pattern, result) { 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); + + // Map 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 + switch (c) { + case "E": + case "c": + weekday = value; + break; + case "G": + era = value; + break; + case "y": + year = value; + break; + case "M": + case "L": + month = value; + break; + case "d": + day = value; + break; + case "h": + hourCycle = "h12"; + hour = value; + break; + case "H": + hourCycle = "h23"; + hour = value; + break; + case "k": + hourCycle = "h24"; + hour = value; + break; + case "K": + hourCycle = "h11"; + hour = value; + break; + case "m": + minute = value; + break; + case "s": + second = value; + break; + case "z": + case "v": + case "V": + timeZoneName = value; + break; + } } } + if (hourCycle) { + _DefineDataProperty(result, "hourCycle", hourCycle); + _DefineDataProperty(result, "hour12", hourCycle === "h11" || hourCycle === "h12"); + } + if (!includeDateTimeFields) { + return; + } + if (weekday) { + _DefineDataProperty(result, "weekday", weekday); + } + if (era) { + _DefineDataProperty(result, "era", era); + } + if (year) { + _DefineDataProperty(result, "year", year); + } + if (month) { + _DefineDataProperty(result, "month", month); + } + if (day) { + _DefineDataProperty(result, "day", day); + } + if (hour) { + _DefineDataProperty(result, "hour", hour); + } + if (minute) { + _DefineDataProperty(result, "minute", minute); + } + if (second) { + _DefineDataProperty(result, "second", second); + } + if (timeZoneName) { + _DefineDataProperty(result, "timeZoneName", timeZoneName); + } } diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h index 57a353c8c9..c75df47402 100644 --- a/js/src/builtin/intl/ICUHeader.h +++ b/js/src/builtin/intl/ICUHeader.h @@ -13,6 +13,7 @@ #include "unicode/udatpg.h" #include "unicode/udisplaycontext.h" #include "unicode/uenum.h" +#include "unicode/uloc.h" #include "unicode/unum.h" #include "unicode/unumsys.h" #include "unicode/upluralrules.h" diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp index 6bb57adf41..2f42e1df76 100644 --- a/js/src/builtin/intl/IntlObject.cpp +++ b/js/src/builtin/intl/IntlObject.cpp @@ -11,6 +11,9 @@ #include "mozilla/Likely.h" #include "mozilla/Range.h" +#include +#include + #include "jsapi.h" #include "jscntxt.h" #include "jsobj.h" @@ -18,11 +21,15 @@ #include "builtin/intl/Collator.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/LanguageTag.h" #include "builtin/intl/ICUHeader.h" +#include "builtin/intl/Locale.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/PluralRules.h" #include "builtin/intl/RelativeTimeFormat.h" #include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "js/Result.h" #include "vm/GlobalObject.h" #include "jsobjinlines.h" @@ -30,7 +37,7 @@ using namespace js; using js::intl::CallICU; -using js::intl::GetAvailableLocales; +using js::intl::DateTimeFormatOptions; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -405,6 +412,272 @@ js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) return true; } +using SupportedLocaleKind = js::intl::SharedIntlData::SupportedLocaleKind; + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +static JS::Result +BestAvailableLocale(JSContext* cx, SupportedLocaleKind kind, HandleLinearString locale, + HandleLinearString defaultLocale) +{ + // In the spec, [[availableLocales]] is formally a list of all available + // locales. But in our implementation, it's an *incomplete* list, not + // necessarily including the default locale (and all locales implied by it, + // e.g. "de" implied by "de-CH"), if that locale isn't in every + // [[availableLocales]] list (because that locale is supported through + // fallback, e.g. "de-CH" supported through "de"). + // + // If we're considering the default locale, augment the spec loop with + // additional checks to also test whether the current prefix is a prefix of + // the default locale. + + intl::SharedIntlData& sharedIntlData = cx->sharedIntlData; + + auto findLast = [](const auto* chars, size_t length) { + auto rbegin = std::make_reverse_iterator(chars + length); + auto rend = std::make_reverse_iterator(chars); + auto p = std::find(rbegin, rend, '-'); + + // |dist(chars, p.base())| is equal to |dist(p, rend)|, pick whichever you + // find easier to reason about when using reserve iterators. + ptrdiff_t r = std::distance(chars, p.base()); + MOZ_ASSERT(r == std::distance(p, rend)); + + // But always subtract one to convert from the reverse iterator result to + // the corresponding forward iterator value, because reserve iterators point + // to one element past the forward iterator value. + return r - 1; + }; + + // Step 1. + RootedLinearString candidate(cx, locale); + + // Step 2. + while (true) { + // Step 2.a. + bool supported = false; + if (!sharedIntlData.isSupportedLocale(cx, kind, candidate, &supported)) { + return cx->alreadyReportedError(); + } + if (supported) { + return candidate.get(); + } + + if (defaultLocale && candidate->length() <= defaultLocale->length()) { + if (EqualStrings(candidate, defaultLocale)) { + return candidate.get(); + } + + if (candidate->length() < defaultLocale->length() && + HasSubstringAt(defaultLocale, candidate, 0) && + defaultLocale->latin1OrTwoByteChar(candidate->length()) == '-') { + return candidate.get(); + } + } + + // Step 2.b. + ptrdiff_t pos; + if (candidate->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->latin1Chars(nogc), candidate->length()); + } else { + JS::AutoCheckCannotGC nogc; + pos = findLast(candidate->twoByteChars(nogc), candidate->length()); + } + + if (pos < 0) { + return nullptr; + } + + // Step 2.c. + size_t length = size_t(pos); + if (length >= 2 && candidate->latin1OrTwoByteChar(length - 2) == '-') { + length -= 2; + } + + // Step 2.d. + candidate = NewDependentString(cx, candidate, 0, length); + if (!candidate) { + return cx->alreadyReportedError(); + } + } +} + +// 9.2.2 BestAvailableLocale ( availableLocales, locale ) +// +// Carries an additional third argument in our implementation to provide the +// default locale. See the doc-comment in the header file. +bool +js::intl_BestAvailableLocale(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + SupportedLocaleKind kind; + { + JSLinearString* typeStr = args[0].toString()->ensureLinear(cx); + if (!typeStr) { + return false; + } + + if (StringEqualsAscii(typeStr, "Collator")) { + kind = SupportedLocaleKind::Collator; + } else if (StringEqualsAscii(typeStr, "DateTimeFormat")) { + kind = SupportedLocaleKind::DateTimeFormat; + } else if (StringEqualsAscii(typeStr, "NumberFormat")) { + kind = SupportedLocaleKind::NumberFormat; + } else if (StringEqualsAscii(typeStr, "PluralRules")) { + kind = SupportedLocaleKind::PluralRules; + } else { + MOZ_ASSERT(StringEqualsAscii(typeStr, "RelativeTimeFormat")); + kind = SupportedLocaleKind::RelativeTimeFormat; + } + } + + RootedLinearString locale(cx, args[1].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + #ifdef DEBUG + { + intl::LanguageTag tag(cx); + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, intl::LanguageTagParser::tryParse(cx, locale, tag)); + MOZ_ASSERT(ok, "locale is a structurally valid language tag"); + + MOZ_ASSERT(!tag.unicodeExtension(), + "locale must contain no Unicode extensions"); + + if (!tag.canonicalize(cx)) { + return false; + } + + JSString* tagStr = tag.toString(cx); + if (!tagStr) { + return false; + } + + bool canonical; + if (!EqualStrings(cx, locale, tagStr, &canonical)) { + return false; + } + MOZ_ASSERT(canonical, "locale is a canonicalized language tag"); + } + #endif + + MOZ_ASSERT(args[2].isNull() || args[2].isString()); + + RootedLinearString defaultLocale(cx); + if (args[2].isString()) { + defaultLocale = args[2].toString()->ensureLinear(cx); + if (!defaultLocale) { + return false; + } + } + + JSString* result; + JS_TRY_VAR_OR_RETURN_FALSE(cx, result, BestAvailableLocale(cx, kind, locale, defaultLocale)); + + if (result) { + args.rval().setString(result); + } else { + args.rval().setUndefined(); + } + return true; +} + +bool +js::intl_supportedLocaleOrFallback(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + RootedLinearString locale(cx, args[0].toString()->ensureLinear(cx)); + if (!locale) { + return false; + } + + intl::LanguageTag tag(cx); + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, intl::LanguageTagParser::tryParse(cx, locale, tag)); + + RootedLinearString candidate(cx); + if (!ok) { + candidate = NewStringCopyZ(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } else { + if (!tag.canonicalize(cx)) { + return false; + } + + // The default locale must be in [[AvailableLocales]], and that list must + // not contain any locales with Unicode extension sequences, so remove any + // present in the candidate. + tag.clearUnicodeExtension(); + + JSString* canonical = tag.toString(cx); + if (!canonical) { + return false; + } + + candidate = canonical->ensureLinear(cx); + if (!candidate) { + return false; + } + + for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) { + const char* oldStyle = mapping.oldStyle; + const char* modernStyle = mapping.modernStyle; + + if (StringEqualsAscii(candidate, oldStyle)) { + candidate = NewStringCopyZ(cx, modernStyle); + if (!candidate) { + return false; + } + break; + } + } + } + + // 9.1 Internal slots of Service Constructors + // + // - [[AvailableLocales]] is a List [...]. The list must include the value + // returned by the DefaultLocale abstract operation (6.2.4), [...]. + // + // That implies we must ignore any candidate which isn't supported by all Intl + // service constructors. + // + // Note: We don't test the supported locales of either Intl.PluralRules or + // Intl.RelativeTimeFormat, because ICU doesn't provide the necessary API to + // return actual set of supported locales for these constructors. Instead it + // returns the complete set of available locales for ULocale, which is a + // superset of the locales supported by Collator, NumberFormat, and + // DateTimeFormat. + bool isSupported = true; + for (auto kind : {SupportedLocaleKind::Collator, SupportedLocaleKind::DateTimeFormat, + SupportedLocaleKind::NumberFormat}) { + JSString* supported; + JS_TRY_VAR_OR_RETURN_FALSE(cx, supported, BestAvailableLocale(cx, kind, candidate, nullptr)); + + if (!supported) { + isSupported = false; + break; + } + } + + if (!isSupported) { + candidate = NewStringCopyZ(cx, intl::LastDitchLocale()); + if (!candidate) { + return false; + } + } + + args.rval().setString(candidate); + return true; +} + const Class js::IntlClass = { js_Object_str, JSCLASS_HAS_CACHED_PROTO(JSProto_Intl) @@ -454,10 +727,16 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) RootedObject collatorProto(cx, CreateCollatorPrototype(cx, intl, global)); if (!collatorProto) return false; - RootedObject dateTimeFormatProto(cx, CreateDateTimeFormatPrototype(cx, intl, global)); + RootedObject dateTimeFormatProto(cx), dateTimeFormat(cx); + dateTimeFormatProto = CreateDateTimeFormatPrototype(cx, intl, global, &dateTimeFormat, DateTimeFormatOptions::Standard); if (!dateTimeFormatProto) return false; - RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); + RootedObject localeProto(cx); + localeProto = CreateLocalePrototype(cx, intl, global); + if (!localeProto) + return false; + RootedObject numberFormatProto(cx), numberFormat(cx); + numberFormatProto = CreateNumberFormatPrototype(cx, intl, global, &numberFormat); if (!numberFormatProto) return false; RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global)); @@ -487,7 +766,10 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) // |getPrototype(JSProto_*)|, but that has global-object-property-related // baggage we don't need or want, so we use one-off reserved slots. global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto)); + global->setReservedSlot(DATE_TIME_FORMAT, ObjectValue(*dateTimeFormat)); global->setReservedSlot(DATE_TIME_FORMAT_PROTO, ObjectValue(*dateTimeFormatProto)); + global->setReservedSlot(LOCALE_PROTO, ObjectValue(*localeProto)); + global->setReservedSlot(NUMBER_FORMAT, ObjectValue(*numberFormat)); global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto)); global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto)); global->setReservedSlot(RELATIVE_TIME_FORMAT_PROTO, ObjectValue(*relativeTimeFmtProto)); diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h index 1f0b26c545..eb5b4c69f8 100644 --- a/js/src/builtin/intl/IntlObject.h +++ b/js/src/builtin/intl/IntlObject.h @@ -95,6 +95,35 @@ intl_GetCalendarInfo(JSContext* cx, unsigned argc, JS::Value* vp); extern MOZ_MUST_USE bool intl_ComputeDisplayNames(JSContext* cx, unsigned argc, JS::Value* vp); +/** + * Compares a BCP 47 language tag against the locales in availableLocales and + * returns the best available match -- or |undefined| if no match was found. + * Uses the fallback mechanism of RFC 4647, section 3.4. + * + * The set of available locales consulted doesn't necessarily include the + * default locale or any generalized forms of it (e.g. "de" is a more-general + * form of "de-CH"). If you want to be sure to consider the default local and + * its generalized forms (you usually will), pass the default locale as the + * value of |defaultOrNull|; otherwise pass null. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.2. + * Spec: RFC 4647, section 3.4. + * + * Usage: result = intl_BestAvailableLocale("Collator", locale, defaultOrNull) + */ +extern MOZ_MUST_USE bool +intl_BestAvailableLocale(JSContext* cx, unsigned argc, JS::Value* vp); + +/** + * Returns the input locale in its canonicalized form if ICU supports that + * locale (perhaps via fallback, e.g. supporting "de-ZA" through "de" support + * implied by a "de-DE" locale). Otherwise uses the last-ditch locale. + * + * Usage: result = intl_supportedLocaleOrFallback(locale) + */ +extern MOZ_MUST_USE bool +intl_supportedLocaleOrFallback(JSContext* cx, unsigned argc, JS::Value* vp); + } // namespace js #endif /* builtin_intl_IntlObject_h */ diff --git a/js/src/builtin/intl/IntlObject.js b/js/src/builtin/intl/IntlObject.js index 826ad27ff0..54e4699dd9 100644 --- a/js/src/builtin/intl/IntlObject.js +++ b/js/src/builtin/intl/IntlObject.js @@ -3,44 +3,79 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function Intl_getCanonicalLocales(locales) { - let codes = CanonicalizeLocaleList(locales); - let result = []; + // Step 1. + var localeList = CanonicalizeLocaleList(locales); - let len = codes.length; - let k = 0; + // Step 2 (Inlined CreateArrayFromList). + var array = []; - while (k < len) { - _DefineDataProperty(result, k, codes[k]); - k++; - } - return result; -} + for (var n = 0, len = localeList.length; n < len; n++) + _DefineDataProperty(array, n, localeList[n]); -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; + return array; } /** - * This function is a custom method designed after Intl API, but currently - * not part of the spec or spec proposal. + * This function is a custom function in the style of the standard Intl.* + * functions, that isn't part of any spec or proposal yet. + * + * Returns an object with the following properties: + * locale: + * The actual resolved locale. + * + * calendar: + * The default calendar of the resolved locale. + * + * firstDayOfWeek: + * The first day of the week for the resolved locale. + * + * minDays: + * The minimum number of days in a week for the resolved locale. + * + * weekendStart: + * The day considered the beginning of a weekend for the resolved locale. + * + * weekendEnd: + * The day considered the end of a weekend for the resolved locale. + * + * Days are encoded as integers in the range 1=Sunday to 7=Saturday. + */ +function Intl_getCalendarInfo(locales) { + // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). + const requestedLocales = CanonicalizeLocaleList(locales); + + const DateTimeFormat = dateTimeFormatInternalProperties; + + // 2. Let localeData be %DateTimeFormat%.[[localeData]]. + const localeData = DateTimeFormat.localeData; + + // 3. Let localeOpt be a new Record. + const localeOpt = new Record(); + + // 4. Set localeOpt.[[localeMatcher]] to "best fit". + localeOpt.localeMatcher = "best fit"; + + // 5. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], + // requestedLocales, localeOpt, + // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). + const r = ResolveLocale("DateTimeFormat", + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // 6. Let result be GetCalendarInfo(r.[[locale]]). + const result = intl_GetCalendarInfo(r.locale); + _DefineDataProperty(result, "calendar", r.ca); + _DefineDataProperty(result, "locale", r.locale); + + // 7. Return result. + return result; +} + +/** + * This function is a custom function in the style of the standard Intl.* + * functions, that isn't part of any spec or proposal yet. * We want to use it internally to retrieve translated values from CLDR in * order to ensure they're aligned with what Intl API returns. * @@ -86,21 +121,23 @@ function Intl_getDisplayNames(locales, options) { // 4. Let localeData be %DateTimeFormat%.[[localeData]]. const localeData = DateTimeFormat.localeData; - // 5. Let opt be a new Record. + // 5. Let localeOpt 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); + const r = ResolveLocale("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; @@ -119,8 +156,10 @@ function Intl_getDisplayNames(locales, options) { // |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++) { diff --git a/js/src/builtin/intl/LangTagMappingsGenerated.js b/js/src/builtin/intl/LangTagMappingsGenerated.js deleted file mode 100644 index 269cf9f93a..0000000000 --- a/js/src/builtin/intl/LangTagMappingsGenerated.js +++ /dev/null @@ -1,382 +0,0 @@ -// Generated by make_intl_data.py. DO NOT EDIT. - -// Mappings from complete tags to preferred values. -// Derived from IANA Language Subtag Registry, file date 2016-10-12. -// http://www.iana.org/assignments/language-subtag-registry -var langTagMappings = { - "art-lojban": "jbo", - "cel-gaulish": "cel-gaulish", - "en-gb-oed": "en-GB-oxendict", - "i-ami": "ami", - "i-bnn": "bnn", - "i-default": "i-default", - "i-enochian": "i-enochian", - "i-hak": "hak", - "i-klingon": "tlh", - "i-lux": "lb", - "i-mingo": "i-mingo", - "i-navajo": "nv", - "i-pwn": "pwn", - "i-tao": "tao", - "i-tay": "tay", - "i-tsu": "tsu", - "ja-latn-hepburn-heploc": "ja-Latn-alalc97", - "no-bok": "nb", - "no-nyn": "nn", - "sgn-be-fr": "sfb", - "sgn-be-nl": "vgt", - "sgn-br": "bzs", - "sgn-ch-de": "sgg", - "sgn-co": "csn", - "sgn-de": "gsg", - "sgn-dk": "dsl", - "sgn-es": "ssp", - "sgn-fr": "fsl", - "sgn-gb": "bfi", - "sgn-gr": "gss", - "sgn-ie": "isg", - "sgn-it": "ise", - "sgn-jp": "jsl", - "sgn-mx": "mfs", - "sgn-ni": "ncs", - "sgn-nl": "dse", - "sgn-no": "nsl", - "sgn-pt": "psr", - "sgn-se": "swl", - "sgn-us": "ase", - "sgn-za": "sfs", - "zh-cmn": "cmn", - "zh-cmn-hans": "cmn-Hans", - "zh-cmn-hant": "cmn-Hant", - "zh-gan": "gan", - "zh-guoyu": "cmn", - "zh-hakka": "hak", - "zh-min": "zh-min", - "zh-min-nan": "nan", - "zh-wuu": "wuu", - "zh-xiang": "hsn", - "zh-yue": "yue", -}; - -// Mappings from non-extlang subtags to preferred values. -// Derived from IANA Language Subtag Registry, file date 2016-10-12. -// http://www.iana.org/assignments/language-subtag-registry -var langSubtagMappings = { - "BU": "MM", - "DD": "DE", - "FX": "FR", - "TP": "TL", - "YD": "YE", - "ZR": "CD", - "aam": "aas", - "adp": "dz", - "aue": "ktz", - "ayx": "nun", - "bgm": "bcg", - "bjd": "drl", - "ccq": "rki", - "cjr": "mom", - "cka": "cmr", - "cmk": "xch", - "coy": "pij", - "cqu": "quh", - "drh": "khk", - "drw": "prs", - "gav": "dev", - "gfx": "vaj", - "ggn": "gvr", - "gti": "nyc", - "guv": "duz", - "hrr": "jal", - "ibi": "opa", - "ilw": "gal", - "in": "id", - "iw": "he", - "ji": "yi", - "jw": "jv", - "kgc": "tdf", - "kgh": "kml", - "koj": "kwv", - "ktr": "dtp", - "kvs": "gdj", - "kwq": "yam", - "kxe": "tvd", - "kzj": "dtp", - "kzt": "dtp", - "lii": "raq", - "lmm": "rmx", - "meg": "cir", - "mo": "ro", - "mst": "mry", - "mwj": "vaj", - "myt": "mry", - "nad": "xny", - "nnx": "ngv", - "nts": "pij", - "oun": "vaj", - "pcr": "adx", - "pmc": "huw", - "pmu": "phr", - "ppa": "bfy", - "ppr": "lcq", - "pry": "prt", - "puz": "pub", - "sca": "hle", - "tdu": "dtp", - "thc": "tpo", - "thx": "oyb", - "tie": "ras", - "tkk": "twm", - "tlw": "weo", - "tmp": "tyj", - "tne": "kak", - "tnf": "prs", - "tsf": "taj", - "uok": "ema", - "xba": "cax", - "xia": "acn", - "xkh": "waw", - "xsj": "suj", - "ybd": "rki", - "yma": "lrr", - "ymt": "mtm", - "yos": "zom", - "yuu": "yug", -}; - -// Mappings from extlang subtags to preferred values. -// Derived from IANA Language Subtag Registry, file date 2016-10-12. -// http://www.iana.org/assignments/language-subtag-registry -var extlangMappings = { - "aao": {preferred: "aao", prefix: "ar"}, - "abh": {preferred: "abh", prefix: "ar"}, - "abv": {preferred: "abv", prefix: "ar"}, - "acm": {preferred: "acm", prefix: "ar"}, - "acq": {preferred: "acq", prefix: "ar"}, - "acw": {preferred: "acw", prefix: "ar"}, - "acx": {preferred: "acx", prefix: "ar"}, - "acy": {preferred: "acy", prefix: "ar"}, - "adf": {preferred: "adf", prefix: "ar"}, - "ads": {preferred: "ads", prefix: "sgn"}, - "aeb": {preferred: "aeb", prefix: "ar"}, - "aec": {preferred: "aec", prefix: "ar"}, - "aed": {preferred: "aed", prefix: "sgn"}, - "aen": {preferred: "aen", prefix: "sgn"}, - "afb": {preferred: "afb", prefix: "ar"}, - "afg": {preferred: "afg", prefix: "sgn"}, - "ajp": {preferred: "ajp", prefix: "ar"}, - "apc": {preferred: "apc", prefix: "ar"}, - "apd": {preferred: "apd", prefix: "ar"}, - "arb": {preferred: "arb", prefix: "ar"}, - "arq": {preferred: "arq", prefix: "ar"}, - "ars": {preferred: "ars", prefix: "ar"}, - "ary": {preferred: "ary", prefix: "ar"}, - "arz": {preferred: "arz", prefix: "ar"}, - "ase": {preferred: "ase", prefix: "sgn"}, - "asf": {preferred: "asf", prefix: "sgn"}, - "asp": {preferred: "asp", prefix: "sgn"}, - "asq": {preferred: "asq", prefix: "sgn"}, - "asw": {preferred: "asw", prefix: "sgn"}, - "auz": {preferred: "auz", prefix: "ar"}, - "avl": {preferred: "avl", prefix: "ar"}, - "ayh": {preferred: "ayh", prefix: "ar"}, - "ayl": {preferred: "ayl", prefix: "ar"}, - "ayn": {preferred: "ayn", prefix: "ar"}, - "ayp": {preferred: "ayp", prefix: "ar"}, - "bbz": {preferred: "bbz", prefix: "ar"}, - "bfi": {preferred: "bfi", prefix: "sgn"}, - "bfk": {preferred: "bfk", prefix: "sgn"}, - "bjn": {preferred: "bjn", prefix: "ms"}, - "bog": {preferred: "bog", prefix: "sgn"}, - "bqn": {preferred: "bqn", prefix: "sgn"}, - "bqy": {preferred: "bqy", prefix: "sgn"}, - "btj": {preferred: "btj", prefix: "ms"}, - "bve": {preferred: "bve", prefix: "ms"}, - "bvl": {preferred: "bvl", prefix: "sgn"}, - "bvu": {preferred: "bvu", prefix: "ms"}, - "bzs": {preferred: "bzs", prefix: "sgn"}, - "cdo": {preferred: "cdo", prefix: "zh"}, - "cds": {preferred: "cds", prefix: "sgn"}, - "cjy": {preferred: "cjy", prefix: "zh"}, - "cmn": {preferred: "cmn", prefix: "zh"}, - "coa": {preferred: "coa", prefix: "ms"}, - "cpx": {preferred: "cpx", prefix: "zh"}, - "csc": {preferred: "csc", prefix: "sgn"}, - "csd": {preferred: "csd", prefix: "sgn"}, - "cse": {preferred: "cse", prefix: "sgn"}, - "csf": {preferred: "csf", prefix: "sgn"}, - "csg": {preferred: "csg", prefix: "sgn"}, - "csl": {preferred: "csl", prefix: "sgn"}, - "csn": {preferred: "csn", prefix: "sgn"}, - "csq": {preferred: "csq", prefix: "sgn"}, - "csr": {preferred: "csr", prefix: "sgn"}, - "czh": {preferred: "czh", prefix: "zh"}, - "czo": {preferred: "czo", prefix: "zh"}, - "doq": {preferred: "doq", prefix: "sgn"}, - "dse": {preferred: "dse", prefix: "sgn"}, - "dsl": {preferred: "dsl", prefix: "sgn"}, - "dup": {preferred: "dup", prefix: "ms"}, - "ecs": {preferred: "ecs", prefix: "sgn"}, - "esl": {preferred: "esl", prefix: "sgn"}, - "esn": {preferred: "esn", prefix: "sgn"}, - "eso": {preferred: "eso", prefix: "sgn"}, - "eth": {preferred: "eth", prefix: "sgn"}, - "fcs": {preferred: "fcs", prefix: "sgn"}, - "fse": {preferred: "fse", prefix: "sgn"}, - "fsl": {preferred: "fsl", prefix: "sgn"}, - "fss": {preferred: "fss", prefix: "sgn"}, - "gan": {preferred: "gan", prefix: "zh"}, - "gds": {preferred: "gds", prefix: "sgn"}, - "gom": {preferred: "gom", prefix: "kok"}, - "gse": {preferred: "gse", prefix: "sgn"}, - "gsg": {preferred: "gsg", prefix: "sgn"}, - "gsm": {preferred: "gsm", prefix: "sgn"}, - "gss": {preferred: "gss", prefix: "sgn"}, - "gus": {preferred: "gus", prefix: "sgn"}, - "hab": {preferred: "hab", prefix: "sgn"}, - "haf": {preferred: "haf", prefix: "sgn"}, - "hak": {preferred: "hak", prefix: "zh"}, - "hds": {preferred: "hds", prefix: "sgn"}, - "hji": {preferred: "hji", prefix: "ms"}, - "hks": {preferred: "hks", prefix: "sgn"}, - "hos": {preferred: "hos", prefix: "sgn"}, - "hps": {preferred: "hps", prefix: "sgn"}, - "hsh": {preferred: "hsh", prefix: "sgn"}, - "hsl": {preferred: "hsl", prefix: "sgn"}, - "hsn": {preferred: "hsn", prefix: "zh"}, - "icl": {preferred: "icl", prefix: "sgn"}, - "iks": {preferred: "iks", prefix: "sgn"}, - "ils": {preferred: "ils", prefix: "sgn"}, - "inl": {preferred: "inl", prefix: "sgn"}, - "ins": {preferred: "ins", prefix: "sgn"}, - "ise": {preferred: "ise", prefix: "sgn"}, - "isg": {preferred: "isg", prefix: "sgn"}, - "isr": {preferred: "isr", prefix: "sgn"}, - "jak": {preferred: "jak", prefix: "ms"}, - "jax": {preferred: "jax", prefix: "ms"}, - "jcs": {preferred: "jcs", prefix: "sgn"}, - "jhs": {preferred: "jhs", prefix: "sgn"}, - "jls": {preferred: "jls", prefix: "sgn"}, - "jos": {preferred: "jos", prefix: "sgn"}, - "jsl": {preferred: "jsl", prefix: "sgn"}, - "jus": {preferred: "jus", prefix: "sgn"}, - "kgi": {preferred: "kgi", prefix: "sgn"}, - "knn": {preferred: "knn", prefix: "kok"}, - "kvb": {preferred: "kvb", prefix: "ms"}, - "kvk": {preferred: "kvk", prefix: "sgn"}, - "kvr": {preferred: "kvr", prefix: "ms"}, - "kxd": {preferred: "kxd", prefix: "ms"}, - "lbs": {preferred: "lbs", prefix: "sgn"}, - "lce": {preferred: "lce", prefix: "ms"}, - "lcf": {preferred: "lcf", prefix: "ms"}, - "liw": {preferred: "liw", prefix: "ms"}, - "lls": {preferred: "lls", prefix: "sgn"}, - "lsg": {preferred: "lsg", prefix: "sgn"}, - "lsl": {preferred: "lsl", prefix: "sgn"}, - "lso": {preferred: "lso", prefix: "sgn"}, - "lsp": {preferred: "lsp", prefix: "sgn"}, - "lst": {preferred: "lst", prefix: "sgn"}, - "lsy": {preferred: "lsy", prefix: "sgn"}, - "ltg": {preferred: "ltg", prefix: "lv"}, - "lvs": {preferred: "lvs", prefix: "lv"}, - "lzh": {preferred: "lzh", prefix: "zh"}, - "max": {preferred: "max", prefix: "ms"}, - "mdl": {preferred: "mdl", prefix: "sgn"}, - "meo": {preferred: "meo", prefix: "ms"}, - "mfa": {preferred: "mfa", prefix: "ms"}, - "mfb": {preferred: "mfb", prefix: "ms"}, - "mfs": {preferred: "mfs", prefix: "sgn"}, - "min": {preferred: "min", prefix: "ms"}, - "mnp": {preferred: "mnp", prefix: "zh"}, - "mqg": {preferred: "mqg", prefix: "ms"}, - "mre": {preferred: "mre", prefix: "sgn"}, - "msd": {preferred: "msd", prefix: "sgn"}, - "msi": {preferred: "msi", prefix: "ms"}, - "msr": {preferred: "msr", prefix: "sgn"}, - "mui": {preferred: "mui", prefix: "ms"}, - "mzc": {preferred: "mzc", prefix: "sgn"}, - "mzg": {preferred: "mzg", prefix: "sgn"}, - "mzy": {preferred: "mzy", prefix: "sgn"}, - "nan": {preferred: "nan", prefix: "zh"}, - "nbs": {preferred: "nbs", prefix: "sgn"}, - "ncs": {preferred: "ncs", prefix: "sgn"}, - "nsi": {preferred: "nsi", prefix: "sgn"}, - "nsl": {preferred: "nsl", prefix: "sgn"}, - "nsp": {preferred: "nsp", prefix: "sgn"}, - "nsr": {preferred: "nsr", prefix: "sgn"}, - "nzs": {preferred: "nzs", prefix: "sgn"}, - "okl": {preferred: "okl", prefix: "sgn"}, - "orn": {preferred: "orn", prefix: "ms"}, - "ors": {preferred: "ors", prefix: "ms"}, - "pel": {preferred: "pel", prefix: "ms"}, - "pga": {preferred: "pga", prefix: "ar"}, - "pgz": {preferred: "pgz", prefix: "sgn"}, - "pks": {preferred: "pks", prefix: "sgn"}, - "prl": {preferred: "prl", prefix: "sgn"}, - "prz": {preferred: "prz", prefix: "sgn"}, - "psc": {preferred: "psc", prefix: "sgn"}, - "psd": {preferred: "psd", prefix: "sgn"}, - "pse": {preferred: "pse", prefix: "ms"}, - "psg": {preferred: "psg", prefix: "sgn"}, - "psl": {preferred: "psl", prefix: "sgn"}, - "pso": {preferred: "pso", prefix: "sgn"}, - "psp": {preferred: "psp", prefix: "sgn"}, - "psr": {preferred: "psr", prefix: "sgn"}, - "pys": {preferred: "pys", prefix: "sgn"}, - "rms": {preferred: "rms", prefix: "sgn"}, - "rsi": {preferred: "rsi", prefix: "sgn"}, - "rsl": {preferred: "rsl", prefix: "sgn"}, - "rsm": {preferred: "rsm", prefix: "sgn"}, - "sdl": {preferred: "sdl", prefix: "sgn"}, - "sfb": {preferred: "sfb", prefix: "sgn"}, - "sfs": {preferred: "sfs", prefix: "sgn"}, - "sgg": {preferred: "sgg", prefix: "sgn"}, - "sgx": {preferred: "sgx", prefix: "sgn"}, - "shu": {preferred: "shu", prefix: "ar"}, - "slf": {preferred: "slf", prefix: "sgn"}, - "sls": {preferred: "sls", prefix: "sgn"}, - "sqk": {preferred: "sqk", prefix: "sgn"}, - "sqs": {preferred: "sqs", prefix: "sgn"}, - "ssh": {preferred: "ssh", prefix: "ar"}, - "ssp": {preferred: "ssp", prefix: "sgn"}, - "ssr": {preferred: "ssr", prefix: "sgn"}, - "svk": {preferred: "svk", prefix: "sgn"}, - "swc": {preferred: "swc", prefix: "sw"}, - "swh": {preferred: "swh", prefix: "sw"}, - "swl": {preferred: "swl", prefix: "sgn"}, - "syy": {preferred: "syy", prefix: "sgn"}, - "tmw": {preferred: "tmw", prefix: "ms"}, - "tse": {preferred: "tse", prefix: "sgn"}, - "tsm": {preferred: "tsm", prefix: "sgn"}, - "tsq": {preferred: "tsq", prefix: "sgn"}, - "tss": {preferred: "tss", prefix: "sgn"}, - "tsy": {preferred: "tsy", prefix: "sgn"}, - "tza": {preferred: "tza", prefix: "sgn"}, - "ugn": {preferred: "ugn", prefix: "sgn"}, - "ugy": {preferred: "ugy", prefix: "sgn"}, - "ukl": {preferred: "ukl", prefix: "sgn"}, - "uks": {preferred: "uks", prefix: "sgn"}, - "urk": {preferred: "urk", prefix: "ms"}, - "uzn": {preferred: "uzn", prefix: "uz"}, - "uzs": {preferred: "uzs", prefix: "uz"}, - "vgt": {preferred: "vgt", prefix: "sgn"}, - "vkk": {preferred: "vkk", prefix: "ms"}, - "vkt": {preferred: "vkt", prefix: "ms"}, - "vsi": {preferred: "vsi", prefix: "sgn"}, - "vsl": {preferred: "vsl", prefix: "sgn"}, - "vsv": {preferred: "vsv", prefix: "sgn"}, - "wuu": {preferred: "wuu", prefix: "zh"}, - "xki": {preferred: "xki", prefix: "sgn"}, - "xml": {preferred: "xml", prefix: "sgn"}, - "xmm": {preferred: "xmm", prefix: "ms"}, - "xms": {preferred: "xms", prefix: "sgn"}, - "ygs": {preferred: "ygs", prefix: "sgn"}, - "yhs": {preferred: "yhs", prefix: "sgn"}, - "ysl": {preferred: "ysl", prefix: "sgn"}, - "yue": {preferred: "yue", prefix: "zh"}, - "zib": {preferred: "zib", prefix: "sgn"}, - "zlm": {preferred: "zlm", prefix: "ms"}, - "zmi": {preferred: "zmi", prefix: "ms"}, - "zsl": {preferred: "zsl", prefix: "sgn"}, - "zsm": {preferred: "zsm", prefix: "ms"}, -}; diff --git a/js/src/builtin/intl/LanguageTag.cpp b/js/src/builtin/intl/LanguageTag.cpp new file mode 100644 index 0000000000..1049e91bc2 --- /dev/null +++ b/js/src/builtin/intl/LanguageTag.cpp @@ -0,0 +1,1728 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +#include "builtin/intl/LanguageTag.h" + +#include "mozilla/Assertions.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Variant.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "ds/Sort.h" +#include "gc/Tracer.h" +#include "js/Result.h" +#include "js/TracingAPI.h" +#include "js/Utility.h" +#include "js/Vector.h" +#include "unicode/uloc.h" +#include "unicode/utypes.h" +#include "vm/String.h" +#include "vm/StringBuffer.h" + +namespace js { +namespace intl { + +using namespace js::intl::LanguageTagLimits; + +template +bool IsStructurallyValidLanguageTag(mozilla::Span language) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + // unicode_language_subtag = alpha{2,3} | alpha{5,8}; + size_t length = language.size(); + const CharT* str = language.data(); + return ((2 <= length && length <= 3) || (5 <= length && length <= 8)) && + std::all_of(str, str + length, mozilla::IsAsciiAlpha); +} + +template bool IsStructurallyValidLanguageTag( + mozilla::Span language); +template bool IsStructurallyValidLanguageTag( + mozilla::Span language); +template bool IsStructurallyValidLanguageTag( + mozilla::Span language); + +template +bool IsStructurallyValidScriptTag(mozilla::Span script) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + // unicode_script_subtag = alpha{4} ; + size_t length = script.size(); + const CharT* str = script.data(); + return length == 4 && + std::all_of(str, str + length, mozilla::IsAsciiAlpha); +} + +template bool IsStructurallyValidScriptTag( + mozilla::Span script); +template bool IsStructurallyValidScriptTag( + mozilla::Span script); +template bool IsStructurallyValidScriptTag( + mozilla::Span script); + +template +bool IsStructurallyValidRegionTag(mozilla::Span region) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + // unicode_region_subtag = (alpha{2} | digit{3}) ; + size_t length = region.size(); + const CharT* str = region.data(); + return (length == 2 && + std::all_of(str, str + length, mozilla::IsAsciiAlpha)) || + (length == 3 && + std::all_of(str, str + length, mozilla::IsAsciiDigit)); +} + +template bool IsStructurallyValidRegionTag( + mozilla::Span region); +template bool IsStructurallyValidRegionTag( + mozilla::Span region); +template bool IsStructurallyValidRegionTag( + mozilla::Span region); + +#ifdef DEBUG +bool IsStructurallyValidVariantTag(mozilla::Span variant) { + // unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3}) ; + size_t length = variant.size(); + const char* str = variant.data(); + return ((5 <= length && length <= 8) || + (length == 4 && mozilla::IsAsciiDigit(str[0]))) && + std::all_of(str, str + length, mozilla::IsAsciiAlphanumeric); +} + +bool IsStructurallyValidUnicodeExtensionTag( + mozilla::Span extension) { + return LanguageTagParser::canParseUnicodeExtension(extension); +} + +static bool IsStructurallyValidExtensionTag( + mozilla::Span extension) { + // other_extensions = sep [alphanum-[tTuUxX]] (sep alphanum{2,8})+ ; + // NB: Allow any extension, including Unicode and Transform here, because + // this function is only used for an assertion. + + size_t length = extension.size(); + const char* str = extension.data(); + const char* const end = extension.data() + length; + if (length <= 2) { + return false; + } + if (!mozilla::IsAsciiAlphanumeric(str[0]) || str[0] == 'x' || str[0] == 'X') { + return false; + } + str++; + if (*str++ != '-') { + return false; + } + while (true) { + const char* sep = + reinterpret_cast(memchr(str, '-', end - str)); + size_t len = (sep ? sep : end) - str; + if (len < 2 || len > 8 || + !std::all_of(str, str + len, mozilla::IsAsciiAlphanumeric)) { + return false; + } + if (!sep) { + return true; + } + str = sep + 1; + } +} + +bool IsStructurallyValidPrivateUseTag(mozilla::Span privateUse) { + // pu_extensions = sep [xX] (sep alphanum{1,8})+ ; + + size_t length = privateUse.size(); + const char* str = privateUse.data(); + const char* const end = privateUse.data() + length; + if (length <= 2) { + return false; + } + if (str[0] != 'x' && str[0] != 'X') { + return false; + } + str++; + if (*str++ != '-') { + return false; + } + while (true) { + const char* sep = + reinterpret_cast(memchr(str, '-', end - str)); + size_t len = (sep ? sep : end) - str; + if (len == 0 || len > 8 || + !std::all_of(str, str + len, mozilla::IsAsciiAlphanumeric)) { + return false; + } + if (!sep) { + return true; + } + str = sep + 1; + } +} +#endif + +ptrdiff_t LanguageTag::unicodeExtensionIndex() const { + // The extension subtags aren't necessarily sorted, so we can't use binary + // search here. + auto p = std::find_if( + extensions().begin(), extensions().end(), + [](const auto& ext) { return ext[0] == 'u' || ext[0] == 'U'; }); + if (p != extensions().end()) { + return std::distance(extensions().begin(), p); + } + return -1; +} + +const char* LanguageTag::unicodeExtension() const { + ptrdiff_t index = unicodeExtensionIndex(); + if (index >= 0) { + return extensions()[index].get(); + } + return nullptr; +} + +bool LanguageTag::setUnicodeExtension(UniqueChars extension) { + MOZ_ASSERT(IsStructurallyValidUnicodeExtensionTag( + mozilla::MakeCStringSpan(extension.get()))); + + // Replace the existing Unicode extension subtag or append a new one. + ptrdiff_t index = unicodeExtensionIndex(); + if (index >= 0) { + extensions_[index] = std::move(extension); + return true; + } + return extensions_.append(std::move(extension)); +} + +void LanguageTag::clearUnicodeExtension() { + ptrdiff_t index = unicodeExtensionIndex(); + if (index >= 0) { + extensions_.erase(extensions_.begin() + index); + } +} + +template +static bool SortAlphabetically(JSContext* cx, + Vector& subtags) { + size_t length = subtags.length(); + + // Zero or one element lists are already sorted. + if (length < 2) { + return true; + } + + // Handle two element lists inline. + if (length == 2) { + if (strcmp(subtags[0].get(), subtags[1].get()) > 0) { + subtags[0].swap(subtags[1]); + } + return true; + } + + Vector scratch(cx); + if (!scratch.resizeUninitialized(length * 2)) { + return false; + } + for (size_t i = 0; i < length; i++) { + scratch[i] = subtags[i].release(); + } + + MOZ_ALWAYS_TRUE( + MergeSort(scratch.begin(), length, scratch.begin() + length, + [](const char* a, const char* b, bool* lessOrEqualp) { + *lessOrEqualp = strcmp(a, b) <= 0; + return true; + })); + + for (size_t i = 0; i < length; i++) { + subtags[i] = UniqueChars(scratch[i]); + } + return true; +} + +bool LanguageTag::canonicalizeBaseName(JSContext* cx, + DuplicateVariants duplicateVariants) { + // Per 6.2.3 CanonicalizeUnicodeLocaleId, the very first step is to + // canonicalize the syntax by normalizing the case and ordering all subtags. + // The canonical syntax form is specified in UTS 35, 3.2.1. + + // Language codes need to be in lower case. "JA" -> "ja" + language_.toLowerCase(); + MOZ_ASSERT(IsStructurallyValidLanguageTag(language().span())); + + // The first character of a script code needs to be capitalized. + // "hans" -> "Hans" + script_.toTitleCase(); + MOZ_ASSERT(script().missing() || + IsStructurallyValidScriptTag(script().span())); + + // Region codes need to be in upper case. "bu" -> "BU" + region_.toUpperCase(); + MOZ_ASSERT(region().missing() || + IsStructurallyValidRegionTag(region().span())); + + // The canonical case for variant subtags is lowercase. + for (UniqueChars& variant : variants_) { + char* variantChars = variant.get(); + size_t variantLength = strlen(variantChars); + AsciiToLowerCase(variantChars, variantLength, variantChars); + + MOZ_ASSERT(IsStructurallyValidVariantTag({variantChars, variantLength})); + } + + // Extensions and privateuse subtags are case normalized in the + // |canonicalizeExtensions| method. + + // The second step in UTS 35, 3.2.1, is to order all subtags. + + if (variants_.length() > 1) { + // 1. Any variants are in alphabetical order. + if (!SortAlphabetically(cx, variants_)) { + return false; + } + + if (duplicateVariants == DuplicateVariants::Reject) { + // Reject the Locale identifier if a duplicate variant was found, e.g. + // "en-variant-Variant". + const UniqueChars* duplicate = + std::adjacent_find(variants().begin(), variants().end(), + [](const auto& a, const auto& b) { + return strcmp(a.get(), b.get()) == 0; + }); + if (duplicate != variants().end()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_DUPLICATE_VARIANT_SUBTAG, + duplicate->get()); + return false; + } + } + } + + // 2. Any extensions are in alphabetical order by their singleton. + // 3. All attributes are sorted in alphabetical order. + // 4. All keywords and tfields are sorted by alphabetical order of their keys, + // within their respective extensions. + // 5. Any type or tfield value "true" is removed. + // - A subsequent call to canonicalizeExtensions() will perform these steps. + + // 6.2.3 CanonicalizeUnicodeLocaleId, step 2 transforms the locale identifier + // into its canonical form per UTS 3.2.1. + + // 1. Use the bcp47 data to replace keys, types, tfields, and tvalues by their + // canonical forms. + // - A subsequent call to canonicalizeExtensions() will perform this step. + + // 2. Replace aliases in the unicode_language_id and tlang (if any). + // - tlang is handled in canonicalizeExtensions(). + + // Replace deprecated language, region, and variant subtags with their + // preferred mappings. + + if (!updateGrandfatheredMappings(cx)) { + return false; + } + + // Replace deprecated language subtags with their preferred values. + if (!languageMapping(language_) && complexLanguageMapping(language_)) { + performComplexLanguageMappings(); + } + + // No script replacements are currently present. + + // Replace deprecated region subtags with their preferred values. + if (region().present()) { + if (!regionMapping(region_) && complexRegionMapping(region_)) { + performComplexRegionMappings(); + } + } + + // Replace deprecated variant subtags with their preferred values. + if (!performVariantMappings(cx)) { + return false; + } + + // No extension replacements are currently present. + // Private use sequences are left as is. + + // 3. Replace aliases in special key values. + // - A subsequent call to canonicalizeExtensions() will perform this step. + + return true; +} + +#ifdef DEBUG +template +static bool IsAsciiLowercaseAlphanumericOrDash( + mozilla::Span span) { + const CharT* ptr = span.data(); + size_t length = span.size(); + return std::all_of(ptr, ptr + length, [](auto c) { + return mozilla::IsAsciiLowercaseAlpha(c) || mozilla::IsAsciiDigit(c) || + c == '-'; + }); +} +#endif + +bool LanguageTag::canonicalizeExtensions(JSContext* cx) { + // The canonical case for all extension subtags is lowercase. + for (UniqueChars& extension : extensions_) { + char* extensionChars = extension.get(); + size_t extensionLength = strlen(extensionChars); + AsciiToLowerCase(extensionChars, extensionLength, extensionChars); + + MOZ_ASSERT( + IsStructurallyValidExtensionTag({extensionChars, extensionLength})); + } + + // Any extensions are in alphabetical order by their singleton. + // "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese" + if (!SortAlphabetically(cx, extensions_)) { + return false; + } + + for (UniqueChars& extension : extensions_) { + if (extension[0] == 'u') { + if (!canonicalizeUnicodeExtension(cx, extension)) { + return false; + } + } else if (extension[0] == 't') { + if (!canonicalizeTransformExtension(cx, extension)) { + return false; + } + } + + MOZ_ASSERT(IsAsciiLowercaseAlphanumericOrDash( + mozilla::MakeCStringSpan(extension.get()))); + } + + // The canonical case for privateuse subtags is lowercase. + if (char* privateuse = privateuse_.get()) { + size_t privateuseLength = strlen(privateuse); + AsciiToLowerCase(privateuse, privateuseLength, privateuse); + + MOZ_ASSERT( + IsStructurallyValidPrivateUseTag({privateuse, privateuseLength})); + } + return true; +} + +/** + * CanonicalizeUnicodeExtension( attributes, keywords ) + * + * Canonical syntax per + * : + * + * - All attributes and keywords are in lowercase. + * - Note: The parser already converted keywords to lowercase. + * - All attributes are sorted in alphabetical order. + * - All keywords are sorted by alphabetical order of their keys. + * - Any type value "true" is removed. + * + * Canonical form: + * - All keys and types use the canonical form (from the name attribute; + * see Section 3.6.4 U Extension Data Files). + */ +bool LanguageTag::canonicalizeUnicodeExtension( + JSContext* cx, JS::UniqueChars& unicodeExtension) { + const char* const extension = unicodeExtension.get(); + MOZ_ASSERT(extension[0] == 'u'); + MOZ_ASSERT(extension[1] == '-'); + MOZ_ASSERT( + IsStructurallyValidExtensionTag(mozilla::MakeCStringSpan(extension))); + + size_t length = strlen(extension); + + LanguageTagParser::AttributesVector attributes(cx); + LanguageTagParser::KeywordsVector keywords(cx); + + using Attribute = LanguageTagParser::AttributesVector::ElementType; + using Keyword = LanguageTagParser::KeywordsVector::ElementType; + + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, ok, + LanguageTagParser::parseUnicodeExtension( + cx, mozilla::MakeSpan(extension, length), attributes, keywords)); + MOZ_ASSERT(ok, "unexpected invalid Unicode extension subtag"); + + auto attributesLessOrEqual = [extension](const Attribute& a, + const Attribute& b) { + const char* astr = a.begin(extension); + const char* bstr = b.begin(extension); + size_t alen = a.length(); + size_t blen = b.length(); + + if (int r = + std::char_traits::compare(astr, bstr, std::min(alen, blen))) { + return r < 0; + } + return alen <= blen; + }; + + // All attributes are sorted in alphabetical order. + size_t attributesLength = attributes.length(); + if (attributesLength > 1) { + if (!attributes.growByUninitialized(attributesLength)) { + return false; + } + + MOZ_ALWAYS_TRUE( + MergeSort(attributes.begin(), attributesLength, + attributes.begin() + attributesLength, + [&](const auto& a, const auto& b, bool* lessOrEqualp) { + *lessOrEqualp = attributesLessOrEqual(a, b); + return true; + })); + + attributes.shrinkBy(attributesLength); + } + + auto keywordsLessOrEqual = [extension](const Keyword& a, const Keyword& b) { + const char* astr = a.begin(extension); + const char* bstr = b.begin(extension); + MOZ_ASSERT(a.length() >= UnicodeKeyLength); + MOZ_ASSERT(b.length() >= UnicodeKeyLength); + + return std::char_traits::compare(astr, bstr, UnicodeKeyLength) <= 0; + }; + + // All keywords are sorted by alphabetical order of keys. + size_t keywordsLength = keywords.length(); + if (keywordsLength > 1) { + if (!keywords.growByUninitialized(keywordsLength)) { + return false; + } + + // Using merge sort, being a stable sort algorithm, guarantees that two + // keywords using the same key are never reordered. That means for example + // when we have the input "u-nu-thai-kf-false-nu-latn", we are guaranteed to + // get the result "u-kf-false-nu-thai-nu-latn", i.e. "nu-thai" still occurs + // before "nu-latn". + // This is required so that deduplication below preserves the first keyword + // for a given key and discards the rest. + MOZ_ALWAYS_TRUE(MergeSort( + keywords.begin(), keywordsLength, keywords.begin() + keywordsLength, + [&](const auto& a, const auto& b, bool* lessOrEqualp) { + *lessOrEqualp = keywordsLessOrEqual(a, b); + return true; + })); + + keywords.shrinkBy(keywordsLength); + } + + Vector sb(cx); + if (!sb.append('u')) { + return false; + } + + // Append all Unicode extension attributes. + for (size_t i = 0; i < attributes.length(); i++) { + const auto& attribute = attributes[i]; + + // Skip duplicate attributes. + if (i > 0) { + const auto& lastAttribute = attributes[i - 1]; + if (attribute.length() == lastAttribute.length() && + std::char_traits::compare(attribute.begin(extension), + lastAttribute.begin(extension), + attribute.length()) == 0) { + continue; + } + MOZ_ASSERT(!attributesLessOrEqual(attribute, lastAttribute)); + } + + if (!sb.append('-')) { + return false; + } + if (!sb.append(attribute.begin(extension), attribute.length())) { + return false; + } + } + + static constexpr size_t UnicodeKeyWithSepLength = UnicodeKeyLength + 1; + + using StringSpan = mozilla::Span; + + static auto isTrue = [](StringSpan type) { + constexpr char True[] = "true"; + const size_t TrueLength = strlen(True); + return type.size() == TrueLength && + std::char_traits::compare(type.data(), True, TrueLength) == 0; + }; + + auto appendKey = [&sb, extension](const Keyword& keyword) { + MOZ_ASSERT(keyword.length() == UnicodeKeyLength); + return sb.append(keyword.begin(extension), UnicodeKeyLength); + }; + + auto appendKeyword = [&sb, extension](const Keyword& keyword, + StringSpan type) { + MOZ_ASSERT(keyword.length() > UnicodeKeyLength); + + // Elide the Unicode extension type "true". + if (isTrue(type)) { + return sb.append(keyword.begin(extension), UnicodeKeyLength); + } + // Otherwise append the complete Unicode extension keyword. + return sb.append(keyword.begin(extension), keyword.length()); + }; + + auto appendReplacement = [&sb, extension](const Keyword& keyword, + StringSpan replacement) { + MOZ_ASSERT(keyword.length() > UnicodeKeyLength); + + // Elide the type "true" if present in the replacement. + if (isTrue(replacement)) { + return sb.append(keyword.begin(extension), UnicodeKeyLength); + } + // Otherwise append the Unicode key (including the separator) and the + // replaced type. + return sb.append(keyword.begin(extension), UnicodeKeyWithSepLength) && + sb.append(replacement.data(), replacement.size()); + }; + + // Append all Unicode extension keywords. + for (size_t i = 0; i < keywords.length(); i++) { + const auto& keyword = keywords[i]; + + // Skip duplicate keywords. + if (i > 0) { + const auto& lastKeyword = keywords[i - 1]; + if (std::char_traits::compare(keyword.begin(extension), + lastKeyword.begin(extension), + UnicodeKeyLength) == 0) { + continue; + } + MOZ_ASSERT(!keywordsLessOrEqual(keyword, lastKeyword)); + } + + if (!sb.append('-')) { + return false; + } + + if (keyword.length() == UnicodeKeyLength) { + // Keyword without type value. + if (!appendKey(keyword)) { + return false; + } + } else { + StringSpan key(keyword.begin(extension), UnicodeKeyLength); + StringSpan type(keyword.begin(extension) + UnicodeKeyWithSepLength, + keyword.length() - UnicodeKeyWithSepLength); + + // Search if there's a replacement for the current Unicode keyword. + if (const char* replacement = replaceUnicodeExtensionType(key, type)) { + if (!appendReplacement(keyword, mozilla::MakeCStringSpan(replacement))) { + return false; + } + } else { + if (!appendKeyword(keyword, type)) { + return false; + } + } + } + } + + // We can keep the previous extension when canonicalization didn't modify it. + if (sb.length() != length || + std::char_traits::compare(sb.begin(), extension, length) != 0) { + // Null-terminate the new string and replace the previous extension. + if (!sb.append('\0')) { + return false; + } + UniqueChars canonical(sb.extractOrCopyRawBuffer()); + if (!canonical) { + return false; + } + unicodeExtension = std::move(canonical); + } + + return true; +} + +template +static bool LanguageTagToString(JSContext* cx, const LanguageTag& tag, + Buffer& sb) { + auto appendSubtag = [&sb](const auto& subtag) { + auto span = subtag.span(); + MOZ_ASSERT(span.size() > 0); + return sb.append(span.data(), span.size()); + }; + + auto appendSubtagZ = [&sb](const char* subtag) { + MOZ_ASSERT(strlen(subtag) > 0); + return sb.append(subtag, strlen(subtag)); + }; + + auto appendSubtagsZ = [&sb, &appendSubtagZ](const auto& subtags) { + for (const auto& subtag : subtags) { + if (!sb.append('-') || !appendSubtagZ(subtag.get())) { + return false; + } + } + return true; + }; + + // Append the language subtag. + if (!appendSubtag(tag.language())) { + return false; + } + + // Append the script subtag if present. + if (tag.script().present()) { + if (!sb.append('-') || !appendSubtag(tag.script())) { + return false; + } + } + + // Append the region subtag if present. + if (tag.region().present()) { + if (!sb.append('-') || !appendSubtag(tag.region())) { + return false; + } + } + + // Append the variant subtags if present. + if (!appendSubtagsZ(tag.variants())) { + return false; + } + + // Append the extensions subtags if present. + if (!appendSubtagsZ(tag.extensions())) { + return false; + } + + // Append the private-use subtag if present. + if (tag.privateuse()) { + if (!sb.append('-') || !appendSubtagZ(tag.privateuse())) { + return false; + } + } + + return true; +} + +/** + * CanonicalizeTransformExtension + * + * Canonical form per : + * + * - These subtags are all in lowercase (that is the canonical casing for these + * subtags), [...]. + * + * And per + * : + * + * - All keywords and tfields are sorted by alphabetical order of their keys, + * within their respective extensions. + */ +bool LanguageTag::canonicalizeTransformExtension( + JSContext* cx, JS::UniqueChars& transformExtension) { + const char* const extension = transformExtension.get(); + MOZ_ASSERT(extension[0] == 't'); + MOZ_ASSERT(extension[1] == '-'); + MOZ_ASSERT( + IsStructurallyValidExtensionTag(mozilla::MakeCStringSpan(extension))); + + size_t length = strlen(extension); + + LanguageTag tag(cx); + LanguageTagParser::TFieldVector fields(cx); + + using TField = LanguageTagParser::TFieldVector::ElementType; + + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, ok, + LanguageTagParser::parseTransformExtension( + cx, mozilla::MakeSpan(extension, length), tag, fields)); + MOZ_ASSERT(ok, "unexpected invalid transform extension subtag"); + + auto tfieldLessOrEqual = [extension](const TField& a, const TField& b) { + MOZ_ASSERT(a.length() > TransformKeyLength); + MOZ_ASSERT(b.length() > TransformKeyLength); + const char* astr = a.begin(extension); + const char* bstr = b.begin(extension); + return std::char_traits::compare(astr, bstr, TransformKeyLength) <= 0; + }; + + // All tfields are sorted by alphabetical order of their keys. + size_t fieldsLength = fields.length(); + if (fieldsLength > 1) { + if (!fields.growByUninitialized(fieldsLength)) { + return false; + } + + MOZ_ALWAYS_TRUE( + MergeSort(fields.begin(), fieldsLength, fields.begin() + fieldsLength, + [&](const auto& a, const auto& b, bool* lessOrEqualp) { + *lessOrEqualp = tfieldLessOrEqual(a, b); + return true; + })); + + fields.shrinkBy(fieldsLength); + } + + Vector sb(cx); + if (!sb.append('t')) { + return false; + } + + // Append the language subtag if present. + // + // Replace aliases in tlang per + // . + if (tag.language().present()) { + if (!sb.append('-')) { + return false; + } + + // ECMA-402 is unclear whether or not duplicate variants are allowed in + // transform extensions. Tentatively allow duplicates until + // https://github.com/tc39/ecma402/issues/330 has been addressed. + if (!tag.canonicalizeBaseName(cx, DuplicateVariants::Accept)) { + return false; + } + + // The canonical case for Transform extensions is lowercase per + // . Convert the two + // subtags which don't use lowercase for their canonical syntax. + tag.script_.toLowerCase(); + tag.region_.toLowerCase(); + + if (!LanguageTagToString(cx, tag, sb)) { + return false; + } + } + + static constexpr size_t TransformKeyWithSepLength = TransformKeyLength + 1; + + using StringSpan = mozilla::Span; + + // Append all fields. + // + // UTS 35, 3.2.1 specifies: + // - Any type or tfield value "true" is removed. + // + // But the `tvalue` subtag is mandatory in `tfield: tkey tvalue`, so ignore + // this apparently invalid part of the UTS 35 specification and simply + // append all `tfield` subtags. + for (const auto& field : fields) { + if (!sb.append('-')) { + return false; + } + + StringSpan key(field.begin(extension), TransformKeyLength); + StringSpan value(field.begin(extension) + TransformKeyWithSepLength, + field.length() - TransformKeyWithSepLength); + + // Search if there's a replacement for the current transform keyword. + if (const char* replacement = replaceTransformExtensionType(key, value)) { + if (!sb.append(field.begin(extension), TransformKeyWithSepLength)) { + return false; + } + if (!sb.append(replacement, strlen(replacement))) { + return false; + } + } else { + if (!sb.append(field.begin(extension), field.length())) { + return false; + } + } + } + + // We can keep the previous extension when canonicalization didn't modify it. + if (sb.length() != length || + std::char_traits::compare(sb.begin(), extension, length) != 0) { + // Null-terminate the new string and replace the previous extension. + if (!sb.append('\0')) { + return false; + } + UniqueChars canonical(sb.extractOrCopyRawBuffer()); + if (!canonical) { + return false; + } + transformExtension = std::move(canonical); + } + + return true; +} + +JSString* LanguageTag::toString(JSContext* cx) const { + StringBuffer sb(cx); + if (!LanguageTagToString(cx, *this, sb)) { + return nullptr; + } + + return sb.finishString(); +} + +UniqueChars LanguageTag::toStringZ(JSContext* cx) const { + Vector sb(cx); + if (!LanguageTagToString(cx, *this, sb)) { + return nullptr; + } + if (!sb.append('\0')) { + return nullptr; + } + + return UniqueChars(sb.extractOrCopyRawBuffer()); +} + +// Zero-terminated ICU Locale ID. +using LocaleId = + js::Vector; + +enum class LikelySubtags : bool { Add, Remove }; + +// Return true iff the language tag is already maximized resp. minimized. +static bool HasLikelySubtags(LikelySubtags likelySubtags, + const LanguageTag& tag) { + // The language tag is already maximized if the language, script, and region + // subtags are present and no placeholder subtags ("und", "Zzzz", "ZZ") are + // used. + if (likelySubtags == LikelySubtags::Add) { + return !tag.language().equalTo("und") && + (tag.script().present() && !tag.script().equalTo("Zzzz")) && + (tag.region().present() && !tag.region().equalTo("ZZ")); + } + + // The language tag is already minimized if it only contains a language + // subtag whose value is not the placeholder value "und". + return !tag.language().equalTo("und") && tag.script().missing() && + tag.region().missing(); +} + +// Create an ICU locale ID from the given language tag. +static bool CreateLocaleForLikelySubtags(const LanguageTag& tag, + LocaleId& locale) { + MOZ_ASSERT(locale.length() == 0); + + auto appendSubtag = [&locale](const auto& subtag) { + auto span = subtag.span(); + MOZ_ASSERT(span.size() > 0); + return locale.append(span.data(), span.size()); + }; + + // Append the language subtag. + if (!appendSubtag(tag.language())) { + return false; + } + + // Append the script subtag if present. + if (tag.script().present()) { + if (!locale.append('_') || !appendSubtag(tag.script())) { + return false; + } + } + + // Append the region subtag if present. + if (tag.region().present()) { + if (!locale.append('_') || !appendSubtag(tag.region())) { + return false; + } + } + + // Zero-terminated for use with ICU. + return locale.append('\0'); +} + +// Assign the language, script, and region subtags from an ICU locale ID. +// +// ICU provides |uloc_getLanguage|, |uloc_getScript|, and |uloc_getCountry| to +// retrieve these subtags, but unfortunately these functions are rather slow, so +// we use our own implementation. +static bool AssignFromLocaleId(JSContext* cx, LocaleId& localeId, + LanguageTag& tag) { + MOZ_ASSERT(localeId.back() == '\0', + "Locale ID should be zero-terminated for ICU"); + + // Replace the ICU locale ID separator. + std::replace(localeId.begin(), localeId.end(), '_', '-'); + + // ICU replaces "und" with the empty string, which means "und" becomes "" and + // "und-Latn" becomes "-Latn". Handle this case separately. + if (localeId[0] == '\0' || localeId[0] == '-') { + static constexpr char und[] = "und"; + size_t length = strlen(und); + + // Insert "und" in front of the locale ID. + if (!localeId.growBy(length)) { + return false; + } + memmove(localeId.begin() + length, localeId.begin(), localeId.length()); + memmove(localeId.begin(), und, length); + } + + mozilla::Span localeSpan(localeId.begin(), localeId.length() - 1); + + // Retrieve the language, script, and region subtags from the locale ID, but + // ignore any other subtags. + LanguageTag localeTag(cx); + if (!LanguageTagParser::parseBaseName(cx, localeSpan, localeTag)) { + return false; + } + + tag.setLanguage(localeTag.language()); + tag.setScript(localeTag.script()); + tag.setRegion(localeTag.region()); + + return true; +} + +template +static bool CallLikelySubtags(JSContext* cx, const LocaleId& localeId, + LocaleId& result) { + // Locale ID must be zero-terminated before passing it to ICU. + MOZ_ASSERT(localeId.back() == '\0'); + MOZ_ASSERT(result.length() == 0); + + int32_t length = intl::CallICU( + cx, + result, + [&localeId](char* chars, int32_t size, UErrorCode* status) { + return likelySubtagsFn(localeId.begin(), chars, size, status); + }); + if (length < 0) { + return false; + } + + MOZ_ASSERT( + size_t(length) <= LocaleId::InlineLength, + "Unexpected extra subtags were added by ICU. If this assertion ever " + "fails, simply remove it and move on like nothing ever happended."); + + // Resize the vector to the actual string length. + result.shrinkTo(length); + + // Zero-terminated for use with ICU. + return result.append('\0'); +} + +// The canonical way to compute the Unicode BCP 47 locale identifier with likely +// subtags is as follows: +// +// 1. Call uloc_forLanguageTag() to transform the locale identifer into an ICU +// locale ID. +// 2. Call uloc_addLikelySubtags() to add the likely subtags to the locale ID. +// 3. Call uloc_toLanguageTag() to transform the resulting locale ID back into +// a Unicode BCP 47 locale identifier. +// +// Since uloc_forLanguageTag() and uloc_toLanguageTag() are both kind of slow +// and we know, by construction, that the input Unicode BCP 47 locale identifier +// only contains valid language, script, and region subtags, we can avoid both +// calls if we implement them ourselves, see CreateLocaleForLikelySubtags() and +// AssignFromLocaleId(). (Where "slow" means about 50% of the execution time of +// |Intl.Locale.prototype.maximize|.) +static bool LikelySubtags(JSContext* cx, LikelySubtags likelySubtags, + LanguageTag& tag) { + // Return early if the input is already maximized/minimized. + if (HasLikelySubtags(likelySubtags, tag)) { + return true; + } + + // Create the locale ID for the input argument. + LocaleId locale(cx); + if (!CreateLocaleForLikelySubtags(tag, locale)) { + return false; + } + + // UTS #35 requires that locale ID is maximized before its likely subtags are + // removed, so we need to call uloc_addLikelySubtags() for both cases. + // See and + // . + + LocaleId localeLikelySubtags(cx); + + // Add likely subtags to the locale ID. When minimizing we can skip adding the + // likely subtags for already maximized tags. (When maximizing we've already + // verified above that the tag is missing likely subtags.) + bool addLikelySubtags = likelySubtags == LikelySubtags::Add || + !HasLikelySubtags(LikelySubtags::Add, tag); + + if (addLikelySubtags) { + if (!CallLikelySubtags(cx, locale, + localeLikelySubtags)) { + return false; + } + } + + // Now that we've succesfully maximized the locale, we can minimize it. + if (likelySubtags == LikelySubtags::Remove) { + if (addLikelySubtags) { + // Copy the maximized subtags back into |locale|. + locale = std::move(localeLikelySubtags); + localeLikelySubtags = LocaleId(cx); + } + + // Remove likely subtags from the locale ID. + if (!CallLikelySubtags(cx, locale, + localeLikelySubtags)) { + return false; + } + } + + // Assign the language, script, and region subtags from the locale ID. + if (!AssignFromLocaleId(cx, localeLikelySubtags, tag)) { + return false; + } + + // Update mappings in case ICU returned a non-canonical locale. + return tag.canonicalizeBaseName(cx); +} + +bool LanguageTag::addLikelySubtags(JSContext* cx) { + return LikelySubtags(cx, LikelySubtags::Add, *this); +} + +bool LanguageTag::removeLikelySubtags(JSContext* cx) { + return LikelySubtags(cx, LikelySubtags::Remove, *this); +} + +LanguageTagParser::Token LanguageTagParser::nextToken() { + MOZ_ASSERT(index_ <= length_ + 1, "called after 'None' token was read"); + + TokenKind kind = TokenKind::None; + size_t tokenLength = 0; + for (size_t i = index_; i < length_; i++) { + // UTS 35, section 3.1. + // alpha = [A-Z a-z] ; + // digit = [0-9] ; + char16_t c = charAtUnchecked(i); + if (mozilla::IsAsciiAlpha(c)) { + kind |= TokenKind::Alpha; + } else if (mozilla::IsAsciiDigit(c)) { + kind |= TokenKind::Digit; + } else if (c == '-' && i > index_ && i + 1 < length_) { + break; + } else { + return {TokenKind::Error, 0, 0}; + } + tokenLength += 1; + } + + Token token{kind, index_, tokenLength}; + index_ += tokenLength + 1; + return token; +} + +UniqueChars LanguageTagParser::chars(JSContext* cx, size_t index, + size_t length) const { + // Add +1 to null-terminate the string. + auto chars = cx->make_pod_array(length + 1); + if (chars) { + char* dest = chars.get(); + if (locale_.is()) { + std::copy_n(locale_.as() + index, length, dest); + } else { + std::copy_n(locale_.as() + index, length, dest); + } + dest[length] = '\0'; + } + return chars; +} + +// Parse the `unicode_language_id` production. +// +// unicode_language_id = unicode_language_subtag +// (sep unicode_script_subtag)? +// (sep unicode_region_subtag)? +// (sep unicode_variant_subtag)* ; +// +// sep = "-" +// +// Note: Unicode CLDR locale identifier backward compatibility extensions +// removed from `unicode_language_id`. +// +// |tok| is the current token from |ts|. +// +// All subtags will be added unaltered to |tag|, without canonicalizing their +// case or, in the case of variant subtags, detecting and rejecting duplicate +// variants. Users must subsequently |canonicalizeBaseName| to perform these +// actions. +// +// Do not use this function directly: use |parseBaseName| or +// |parseTlangFromTransformExtension| instead. +JS::Result LanguageTagParser::internalParseBaseName(JSContext* cx, + LanguageTagParser& ts, + LanguageTag& tag, + Token& tok) { + if (ts.isLanguage(tok)) { + ts.copyChars(tok, tag.language_); + + tok = ts.nextToken(); + } else { + // The language subtag is mandatory. + return false; + } + + if (ts.isScript(tok)) { + ts.copyChars(tok, tag.script_); + + tok = ts.nextToken(); + } + + if (ts.isRegion(tok)) { + ts.copyChars(tok, tag.region_); + + tok = ts.nextToken(); + } + + auto& variants = tag.variants_; + MOZ_ASSERT(variants.length() == 0); + while (ts.isVariant(tok)) { + auto variant = ts.chars(cx, tok); + if (!variant) { + return cx->alreadyReportedOOM(); + } + if (!variants.append(std::move(variant))) { + return cx->alreadyReportedOOM(); + } + + tok = ts.nextToken(); + } + + return true; +} + +static mozilla::Variant StringChars( + const char* locale) { + return mozilla::AsVariant(reinterpret_cast(locale)); +} + +static mozilla::Variant StringChars( + JSLinearString* linear, JS::AutoCheckCannotGC& nogc) { + if (linear->hasLatin1Chars()) { + return mozilla::AsVariant(linear->latin1Chars(nogc)); + } + return mozilla::AsVariant(linear->twoByteChars(nogc)); +} + +JS::Result LanguageTagParser::tryParse(JSContext* cx, + JSLinearString* locale, + LanguageTag& tag) { + JS::AutoCheckCannotGC nogc; + LocaleChars localeChars = StringChars(locale, nogc); + return tryParse(cx, localeChars, locale->length(), tag); +} + +JS::Result LanguageTagParser::tryParse(JSContext* cx, + mozilla::Span locale, + LanguageTag& tag) { + LocaleChars localeChars = StringChars(locale.data()); + return tryParse(cx, localeChars, locale.size(), tag); +} + +JS::Result LanguageTagParser::tryParse(JSContext* cx, + LocaleChars& localeChars, + size_t localeLength, + LanguageTag& tag) { + // unicode_locale_id = unicode_language_id + // extensions* + // pu_extensions? ; + + LanguageTagParser ts(localeChars, localeLength); + Token tok = ts.nextToken(); + + bool ok; + MOZ_TRY_VAR(ok, parseBaseName(cx, ts, tag, tok)); + if (!ok) { + return false; + } + + // extensions = unicode_locale_extensions + // | transformed_extensions + // | other_extensions ; + + // Bit set of seen singletons. + uint64_t seenSingletons = 0; + + auto& extensions = tag.extensions_; + while (ts.isExtensionStart(tok)) { + char singleton = ts.singletonKey(tok); + + // Reject the input if a duplicate singleton was found. + uint64_t hash = 1ULL << (mozilla::AsciiAlphanumericToNumber(singleton) + 1); + if (seenSingletons & hash) { + return false; + } + seenSingletons |= hash; + + Token start = tok; + tok = ts.nextToken(); + + // We'll check for missing non-singleton subtags after this block by + // comparing |startValue| with the then-current position. + size_t startValue = tok.index(); + + if (singleton == 'u') { + while (ts.isUnicodeExtensionPart(tok)) { + tok = ts.nextToken(); + } + } else if (singleton == 't') { + // transformed_extensions = sep [tT] + // ((sep tlang (sep tfield)*) + // | (sep tfield)+) ; + + // tlang = unicode_language_subtag + // (sep unicode_script_subtag)? + // (sep unicode_region_subtag)? + // (sep unicode_variant_subtag)* ; + if (ts.isLanguage(tok)) { + tok = ts.nextToken(); + + if (ts.isScript(tok)) { + tok = ts.nextToken(); + } + + if (ts.isRegion(tok)) { + tok = ts.nextToken(); + } + + while (ts.isVariant(tok)) { + tok = ts.nextToken(); + } + } + + // tfield = tkey tvalue; + while (ts.isTransformExtensionKey(tok)) { + tok = ts.nextToken(); + + size_t startTValue = tok.index(); + while (ts.isTransformExtensionPart(tok)) { + tok = ts.nextToken(); + } + + // `tfield` requires at least one `tvalue`. + if (tok.index() <= startTValue) { + return false; + } + } + } else { + while (ts.isOtherExtensionPart(tok)) { + tok = ts.nextToken(); + } + } + + // Singletons must be followed by a non-singleton subtag, "en-a-b" is not + // allowed. + if (tok.index() <= startValue) { + return false; + } + + UniqueChars extension = ts.extension(cx, start, tok); + if (!extension) { + return cx->alreadyReportedOOM(); + } + if (!extensions.append(std::move(extension))) { + return cx->alreadyReportedOOM(); + } + } + + // Trailing `pu_extension` component of the `unicode_locale_id` production. + if (ts.isPrivateUseStart(tok)) { + Token start = tok; + tok = ts.nextToken(); + + size_t startValue = tok.index(); + while (ts.isPrivateUsePart(tok)) { + tok = ts.nextToken(); + } + + // There must be at least one subtag after the "-x-". + if (tok.index() <= startValue) { + return false; + } + + UniqueChars privateUse = ts.extension(cx, start, tok); + if (!privateUse) { + return cx->alreadyReportedOOM(); + } + tag.privateuse_ = std::move(privateUse); + } + + // Return true if the complete input was successfully parsed. + return tok.isNone(); +} + +bool LanguageTagParser::parse(JSContext* cx, JSLinearString* locale, + LanguageTag& tag) { + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, tryParse(cx, locale, tag)); + if (ok) { + return true; + } + if (UniqueChars localeChars = StringToNewUTF8CharsZ(cx, *locale)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG, localeChars.get()); + } + return false; +} + +bool LanguageTagParser::parse(JSContext* cx, mozilla::Span locale, + LanguageTag& tag) { + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, tryParse(cx, locale, tag)); + if (ok) { + return true; + } + if (UniqueChars localeChars = DuplicateString(cx, locale.data())) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG, localeChars.get()); + } + return false; +} + +bool LanguageTagParser::parseBaseName(JSContext* cx, + mozilla::Span locale, + LanguageTag& tag) { + LocaleChars localeChars = StringChars(locale.data()); + LanguageTagParser ts(localeChars, locale.size()); + Token tok = ts.nextToken(); + + // Parse only the base-name part and ignore any trailing characters. + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, parseBaseName(cx, ts, tag, tok)); + if (ok) { + return true; + } + if (UniqueChars localeChars = DuplicateString(cx, locale.data())) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LANGUAGE_TAG, localeChars.get()); + } + return false; +} + +// Parse |extension|, which must be a valid `transformed_extensions` subtag, and +// fill |tag| and |fields| from the `tlang` and `tfield` components. +JS::Result LanguageTagParser::parseTransformExtension( + JSContext* cx, mozilla::Span extension, LanguageTag& tag, + TFieldVector& fields) { + LocaleChars extensionChars = StringChars(extension.data()); + LanguageTagParser ts(extensionChars, extension.size()); + Token tok = ts.nextToken(); + + if (!ts.isExtensionStart(tok) || ts.singletonKey(tok) != 't') { + return false; + } + + tok = ts.nextToken(); + + if (tok.isNone()) { + return false; + } + + if (ts.isLanguage(tok)) { + // We're parsing a possible `tlang` in a known-valid transform extension, so + // use the special-purpose function that takes advantage of this to compute + // lowercased |tag| contents in an optimal manner. + MOZ_TRY(parseTlangInTransformExtension(cx, ts, tag, tok)); + + // After `tlang` we must have a `tfield` and its `tkey`, or we're at the end + // of the transform extension. + MOZ_ASSERT(ts.isTransformExtensionKey(tok) || tok.isNone()); + } else { + // If there's no `tlang` subtag, at least one `tfield` must be present. + MOZ_ASSERT(ts.isTransformExtensionKey(tok)); + } + + // Trailing `tfield` subtags. (Any other trailing subtags are an error, + // because we're guaranteed to only see a valid tranform extension here.) + while (ts.isTransformExtensionKey(tok)) { + size_t begin = tok.index(); + tok = ts.nextToken(); + + size_t startTValue = tok.index(); + while (ts.isTransformExtensionPart(tok)) { + tok = ts.nextToken(); + } + + // `tfield` requires at least one `tvalue`. + if (tok.index() <= startTValue) { + return false; + } + + size_t length = tok.index() - 1 - begin; + if (!fields.emplaceBack(begin, length)) { + return cx->alreadyReportedOOM(); + } + } + + // Return true if the complete input was successfully parsed. + return tok.isNone(); +} + +// Parse |extension|, which must be a valid `unicode_locale_extensions` subtag, +// and fill |attributes| and |keywords| from the `attribute` and `keyword` +// components. +JS::Result LanguageTagParser::parseUnicodeExtension( + JSContext* cx, mozilla::Span extension, + AttributesVector& attributes, KeywordsVector& keywords) { + LocaleChars extensionChars = StringChars(extension.data()); + LanguageTagParser ts(extensionChars, extension.size()); + Token tok = ts.nextToken(); + + // unicode_locale_extensions = sep [uU] ((sep keyword)+ | + // (sep attribute)+ (sep keyword)*) ; + + if (!ts.isExtensionStart(tok) || ts.singletonKey(tok) != 'u') { + return false; + } + + tok = ts.nextToken(); + + if (tok.isNone()) { + return false; + } + + while (ts.isUnicodeExtensionAttribute(tok)) { + if (!attributes.emplaceBack(tok.index(), tok.length())) { + return cx->alreadyReportedOOM(); + } + + tok = ts.nextToken(); + } + + // keyword = key (sep type)? ; + while (ts.isUnicodeExtensionKey(tok)) { + size_t begin = tok.index(); + tok = ts.nextToken(); + + while (ts.isUnicodeExtensionType(tok)) { + tok = ts.nextToken(); + } + + if (tok.isError()) { + return false; + } + + size_t length = tok.index() - 1 - begin; + if (!keywords.emplaceBack(begin, length)) { + return cx->alreadyReportedOOM(); + } + } + + // Return true if the complete input was successfully parsed. + return tok.isNone(); +} + +bool LanguageTagParser::canParseUnicodeExtension( + mozilla::Span extension) { + LocaleChars extensionChars = StringChars(extension.data()); + LanguageTagParser ts(extensionChars, extension.size()); + Token tok = ts.nextToken(); + + // unicode_locale_extensions = sep [uU] ((sep keyword)+ | + // (sep attribute)+ (sep keyword)*) ; + + if (!ts.isExtensionStart(tok) || ts.singletonKey(tok) != 'u') { + return false; + } + + tok = ts.nextToken(); + + if (tok.isNone()) { + return false; + } + + while (ts.isUnicodeExtensionAttribute(tok)) { + tok = ts.nextToken(); + } + + // keyword = key (sep type)? ; + while (ts.isUnicodeExtensionKey(tok)) { + tok = ts.nextToken(); + + while (ts.isUnicodeExtensionType(tok)) { + tok = ts.nextToken(); + } + + if (tok.isError()) { + return false; + } + } + + // Return true if the complete input was successfully parsed. + return tok.isNone(); +} + +bool LanguageTagParser::canParseUnicodeExtensionType( + JSLinearString* unicodeType) { + MOZ_ASSERT(unicodeType->length() > 0, "caller must exclude empty strings"); + + JS::AutoCheckCannotGC nogc; + LocaleChars unicodeTypeChars = StringChars(unicodeType, nogc); + + LanguageTagParser ts(unicodeTypeChars, unicodeType->length()); + Token tok = ts.nextToken(); + + while (ts.isUnicodeExtensionType(tok)) { + tok = ts.nextToken(); + } + + // Return true if the complete input was successfully parsed. + return tok.isNone(); +} + +bool ParseStandaloneLanguageTag(HandleLinearString str, + LanguageSubtag& result) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + if (!IsStructurallyValidLanguageTag(str->latin1Range(nogc))) { + return false; + } + result.set(str->latin1Range(nogc)); + } else { + if (!IsStructurallyValidLanguageTag(str->twoByteRange(nogc))) { + return false; + } + result.set(str->twoByteRange(nogc)); + } + return true; +} + +bool ParseStandaloneScriptTag(HandleLinearString str, ScriptSubtag& result) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + if (!IsStructurallyValidScriptTag(str->latin1Range(nogc))) { + return false; + } + result.set(str->latin1Range(nogc)); + } else { + if (!IsStructurallyValidScriptTag(str->twoByteRange(nogc))) { + return false; + } + result.set(str->twoByteRange(nogc)); + } + return true; +} + +bool ParseStandaloneRegionTag(HandleLinearString str, RegionSubtag& result) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + if (!IsStructurallyValidRegionTag(str->latin1Range(nogc))) { + return false; + } + result.set(str->latin1Range(nogc)); + } else { + if (!IsStructurallyValidRegionTag(str->twoByteRange(nogc))) { + return false; + } + result.set(str->twoByteRange(nogc)); + } + return true; +} + +template +static bool IsAsciiLowercaseAlpha(mozilla::Span span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + const CharT* ptr = span.data(); + size_t length = span.size(); + return std::all_of(ptr, ptr + length, mozilla::IsAsciiLowercaseAlpha); +} + +static bool IsAsciiLowercaseAlpha(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return IsAsciiLowercaseAlpha(str->latin1Range(nogc)); + } + return IsAsciiLowercaseAlpha(str->twoByteRange(nogc)); +} + +template +static bool IsAsciiAlpha(mozilla::Span span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + const CharT* ptr = span.data(); + size_t length = span.size(); + return std::all_of(ptr, ptr + length, mozilla::IsAsciiAlpha); +} + +static bool IsAsciiAlpha(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return IsAsciiAlpha(str->latin1Range(nogc)); + } + return IsAsciiAlpha(str->twoByteRange(nogc)); +} + +JS::Result ParseStandaloneISO639LanguageTag(JSContext* cx, + HandleLinearString str) { + // ISO-639 language codes contain either two or three characters. + size_t length = str->length(); + if (length != 2 && length != 3) { + return nullptr; + } + + // We can directly the return the input below if it's in the correct case. + bool isLowerCase = IsAsciiLowercaseAlpha(str); + if (!isLowerCase) { + // Must be an ASCII alpha string. + if (!IsAsciiAlpha(str)) { + return nullptr; + } + } + + LanguageSubtag languageTag; + if (str->hasLatin1Chars()) { + JS::AutoCheckCannotGC nogc; + languageTag.set(str->latin1Range(nogc)); + } else { + JS::AutoCheckCannotGC nogc; + languageTag.set(str->twoByteRange(nogc)); + } + + if (!isLowerCase) { + // The language subtag is canonicalized to lower case. + languageTag.toLowerCase(); + } + + // Reject the input if the canonical tag contains more than just a single + // language subtag. + if (LanguageTag::complexLanguageMapping(languageTag)) { + return nullptr; + } + + // Take care to replace deprecated subtags with their preferred values. + JSString* result; + if (LanguageTag::languageMapping(languageTag) || !isLowerCase) { + auto span = languageTag.span(); + result = NewStringCopyN(cx, span.data(), span.size()); + } else { + result = str; + } + if (!result) { + return cx->alreadyReportedOOM(); + } + return result; +} + +void js::intl::UnicodeExtensionKeyword::trace(JSTracer* trc) { + TraceRoot(trc, &type_, "UnicodeExtensionKeyword::type"); +} + +} // namespace intl +} // namespace js diff --git a/js/src/builtin/intl/LanguageTag.h b/js/src/builtin/intl/LanguageTag.h new file mode 100644 index 0000000000..3c2ecb1553 --- /dev/null +++ b/js/src/builtin/intl/LanguageTag.h @@ -0,0 +1,770 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +/* Structured representation of Unicode locale IDs used with Intl functions. */ + +#ifndef builtin_intl_LanguageTag_h +#define builtin_intl_LanguageTag_h + +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/Variant.h" + +#include +#include +#include +#include +#include + +#include "jsalloc.h" +#include "js/Result.h" + +#include "js/GCAPI.h" +#include "js/Utility.h" +#include "js/Vector.h" + +struct JSContext; +class JSLinearString; +class JSString; +class JSTracer; + +namespace js { + +namespace intl { + +/** + * Return true if |language| is a valid language subtag. + */ +template +bool IsStructurallyValidLanguageTag(mozilla::Span language); + +/** + * Return true if |script| is a valid script subtag. + */ +template +bool IsStructurallyValidScriptTag(mozilla::Span script); + +/** + * Return true if |region| is a valid region subtag. + */ +template +bool IsStructurallyValidRegionTag(mozilla::Span region); + +#ifdef DEBUG +/** + * Return true if |variant| is a valid variant subtag. + */ +bool IsStructurallyValidVariantTag(mozilla::Span variant); + +/** + * Return true if |extension| is a valid Unicode extension subtag. + */ +bool IsStructurallyValidUnicodeExtensionTag( + mozilla::Span extension); + +/** + * Return true if |privateUse| is a valid private-use subtag. + */ +bool IsStructurallyValidPrivateUseTag(mozilla::Span privateUse); + +#endif + +template +char AsciiToLowerCase(CharT c) { + MOZ_ASSERT(mozilla::IsAscii(c)); + return mozilla::IsAsciiUppercaseAlpha(c) ? (c + 0x20) : c; +} + +template +char AsciiToUpperCase(CharT c) { + MOZ_ASSERT(mozilla::IsAscii(c)); + return mozilla::IsAsciiLowercaseAlpha(c) ? (c - 0x20) : c; +} + +template +void AsciiToLowerCase(CharT* chars, size_t length, char* dest) { + // Tell the analysis the |std::transform| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + char (&fn)(CharT) = AsciiToLowerCase; + std::transform(chars, chars + length, dest, fn); +} + +template +void AsciiToUpperCase(CharT* chars, size_t length, char* dest) { + // Tell the analysis the |std::transform| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + char (&fn)(CharT) = AsciiToUpperCase; + std::transform(chars, chars + length, dest, fn); +} + +template +void AsciiToTitleCase(CharT* chars, size_t length, char* dest) { + if (length > 0) { + AsciiToUpperCase(chars, 1, dest); + AsciiToLowerCase(chars + 1, length - 1, dest + 1); + } +} + +// Constants for language subtag lengths. +namespace LanguageTagLimits { + +// unicode_language_subtag = alpha{2,3} | alpha{5,8} ; +static constexpr size_t LanguageLength = 8; + +// unicode_script_subtag = alpha{4} ; +static constexpr size_t ScriptLength = 4; + +// unicode_region_subtag = (alpha{2} | digit{3}) ; +static constexpr size_t RegionLength = 3; +static constexpr size_t AlphaRegionLength = 2; +static constexpr size_t DigitRegionLength = 3; + +// key = alphanum alpha ; +static constexpr size_t UnicodeKeyLength = 2; + +// tkey = alpha digit ; +static constexpr size_t TransformKeyLength = 2; + +} // namespace LanguageTagLimits + +// Fixed size language subtag which is stored inline in LanguageTag. +template +class LanguageTagSubtag final { + uint8_t length_ = 0; + char chars_[Length] = {}; // zero initialize + + public: + LanguageTagSubtag() = default; + + LanguageTagSubtag(const LanguageTagSubtag&) = delete; + LanguageTagSubtag& operator=(const LanguageTagSubtag&) = delete; + + size_t length() const { return length_; } + bool missing() const { return length_ == 0; } + bool present() const { return length_ > 0; } + + mozilla::Span span() const { return {chars_, length_}; } + + template + void set(mozilla::Span str) { + MOZ_ASSERT(str.size() <= Length); + std::copy_n(str.data(), str.size(), chars_); + length_ = str.size(); + } + + // The toXYZCase() methods are using |Length| instead of |length()|, because + // current compilers (tested GCC and Clang) can't infer the maximum string + // length - even when using hints like |std::min| - and instead are emitting + // SIMD optimized code. Using a fixed sized length avoids emitting the SIMD + // code. (Emitting SIMD code doesn't make sense here, because the SIMD code + // only kicks in for long strings.) A fixed length will additionally ensure + // the compiler unrolls the loop in the case conversion code. + + void toLowerCase() { AsciiToLowerCase(chars_, Length, chars_); } + + void toUpperCase() { AsciiToUpperCase(chars_, Length, chars_); } + + void toTitleCase() { AsciiToTitleCase(chars_, Length, chars_); } + + template + bool equalTo(const char (&str)[N]) const { + static_assert(N - 1 <= Length, + "subtag literals must not exceed the maximum subtag length"); + + return length_ == N - 1 && memcmp(chars_, str, N - 1) == 0; + } +}; + +using LanguageSubtag = LanguageTagSubtag; +using ScriptSubtag = LanguageTagSubtag; +using RegionSubtag = LanguageTagSubtag; + +/** + * Object representing a language tag. + * + * All subtags are already in canonicalized case. + */ +class MOZ_STACK_CLASS LanguageTag final { + LanguageSubtag language_ = {}; + ScriptSubtag script_ = {}; + RegionSubtag region_ = {}; + + using VariantsVector = Vector; + using ExtensionsVector = Vector; + + VariantsVector variants_; + ExtensionsVector extensions_; + JS::UniqueChars privateuse_ = nullptr; + + friend class LanguageTagParser; + + bool canonicalizeUnicodeExtension(JSContext* cx, + JS::UniqueChars& unicodeExtension); + + bool canonicalizeTransformExtension(JSContext* cx, + JS::UniqueChars& transformExtension); + + public: + static bool languageMapping(LanguageSubtag& language); + static bool complexLanguageMapping(const LanguageSubtag& language); + + private: + static bool regionMapping(RegionSubtag& region); + static bool complexRegionMapping(const RegionSubtag& region); + + void performComplexLanguageMappings(); + void performComplexRegionMappings(); + MOZ_MUST_USE bool performVariantMappings(JSContext* cx); + + MOZ_MUST_USE bool updateGrandfatheredMappings(JSContext* cx); + + static const char* replaceTransformExtensionType( + mozilla::Span key, mozilla::Span type); + + public: + /** + * Given a Unicode key and type, return the null-terminated preferred + * replacement for that type if there is one, or null if there is none, e.g. + * in effect + * |replaceUnicodeExtensionType("ca", "islamicc") == "islamic-civil"| + * and + * |replaceUnicodeExtensionType("ca", "islamic-civil") == nullptr|. + */ + static const char* replaceUnicodeExtensionType( + mozilla::Span key, mozilla::Span type); + + public: + explicit LanguageTag(JSContext* cx) : variants_(cx), extensions_(cx) {} + + LanguageTag(const LanguageTag&) = delete; + LanguageTag& operator=(const LanguageTag&) = delete; + + const LanguageSubtag& language() const { return language_; } + const ScriptSubtag& script() const { return script_; } + const RegionSubtag& region() const { return region_; } + const auto& variants() const { return variants_; } + const auto& extensions() const { return extensions_; } + const char* privateuse() const { return privateuse_.get(); } + + /** + * Return the Unicode extension subtag or nullptr if not present. + */ + const char* unicodeExtension() const; + + private: + ptrdiff_t unicodeExtensionIndex() const; + + public: + /** + * Set the language subtag. The input must be a valid language subtag. + */ + template + void setLanguage(const char (&language)[N]) { + mozilla::Span span(language, N - 1); + MOZ_ASSERT(IsStructurallyValidLanguageTag(span)); + language_.set(span); + } + + /** + * Set the language subtag. The input must be a valid language subtag. + */ + void setLanguage(const LanguageSubtag& language) { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language.span())); + language_.set(language.span()); + } + + /** + * Set the script subtag. The input must be a valid script subtag. + */ + template + void setScript(const char (&script)[N]) { + mozilla::Span span(script, N - 1); + MOZ_ASSERT(IsStructurallyValidScriptTag(span)); + script_.set(span); + } + + /** + * Set the script subtag. The input must be a valid script subtag or the empty + * string. + */ + void setScript(const ScriptSubtag& script) { + MOZ_ASSERT(script.missing() || IsStructurallyValidScriptTag(script.span())); + script_.set(script.span()); + } + + /** + * Set the region subtag. The input must be a valid region subtag. + */ + template + void setRegion(const char (®ion)[N]) { + mozilla::Span span(region, N - 1); + MOZ_ASSERT(IsStructurallyValidRegionTag(span)); + region_.set(span); + } + + /** + * Set the region subtag. The input must be a valid region subtag or the empty + * empty string. + */ + void setRegion(const RegionSubtag& region) { + MOZ_ASSERT(region.missing() || IsStructurallyValidRegionTag(region.span())); + region_.set(region.span()); + } + + /** + * Removes all variant subtags. + */ + void clearVariants() { variants_.clearAndFree(); } + + /** + * Set the Unicode extension subtag. The input must be a valid Unicode + * extension subtag. + */ + bool setUnicodeExtension(JS::UniqueChars extension); + + /** + * Remove any Unicode extension subtag if present. + */ + void clearUnicodeExtension(); + + /** + * Set the private-use subtag. The input must be a valid private-use subtag + * or nullptr. + */ + void setPrivateuse(JS::UniqueChars privateuse) { + MOZ_ASSERT(!privateuse || + IsStructurallyValidPrivateUseTag( + {privateuse.get(), strlen(privateuse.get())})); + privateuse_ = std::move(privateuse); + } + + private: + enum class DuplicateVariants { Reject, Accept }; + + bool canonicalizeBaseName(JSContext* cx, DuplicateVariants duplicateVariants); + + public: + /** + * Canonicalize the base-name subtags, that means the language, script, + * region, and variant subtags. + */ + bool canonicalizeBaseName(JSContext* cx) { + return canonicalizeBaseName(cx, DuplicateVariants::Reject); + } + + /** + * Canonicalize all extension subtags. + */ + bool canonicalizeExtensions(JSContext* cx); + + /** + * Canonicalizes the given structurally valid Unicode BCP 47 locale + * identifier, including regularized case of subtags. For example, the + * language tag Zh-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, + * where + * + * Zh ; 2*3ALPHA + * -haNS ; ["-" script] + * -bu ; ["-" region] + * -variant2 ; *("-" variant) + * -Variant1 + * -u-ca-chinese ; *("-" extension) + * -t-Zh-laTN + * -x-PRIVATE ; ["-" privateuse] + * + * becomes zh-Hans-MM-variant1-variant2-t-zh-latn-u-ca-chinese-x-private + * + * Spec: ECMAScript Internationalization API Specification, 6.2.3. + */ + bool canonicalize(JSContext* cx) { + return canonicalizeBaseName(cx) && canonicalizeExtensions(cx); + } + + /** + * Return the string representation of this language tag. + */ + JSString* toString(JSContext* cx) const; + + /** + * Return the string representation of this language tag as a null-terminated + * C-string. + */ + JS::UniqueChars toStringZ(JSContext* cx) const; + + /** + * Add likely-subtags to the language tag. + * + * Spec: + */ + bool addLikelySubtags(JSContext* cx); + + /** + * Remove likely-subtags from the language tag. + * + * Spec: + */ + bool removeLikelySubtags(JSContext* cx); +}; + +/** + * Parser for Unicode BCP 47 locale identifiers. + * + * + */ +class MOZ_STACK_CLASS LanguageTagParser final { + public: + // Exposed as |public| for |MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS|. + enum class TokenKind : uint8_t { + None = 0b000, + Alpha = 0b001, + Digit = 0b010, + AlphaDigit = 0b011, + Error = 0b100 + }; + + private: + class Token final { + size_t index_; + size_t length_; + TokenKind kind_; + + public: + Token(TokenKind kind, size_t index, size_t length) + : index_(index), length_(length), kind_(kind) {} + + TokenKind kind() const { return kind_; } + size_t index() const { return index_; } + size_t length() const { return length_; } + + bool isError() const { return kind_ == TokenKind::Error; } + bool isNone() const { return kind_ == TokenKind::None; } + bool isAlpha() const { return kind_ == TokenKind::Alpha; } + bool isDigit() const { return kind_ == TokenKind::Digit; } + bool isAlphaDigit() const { return kind_ == TokenKind::AlphaDigit; } + }; + + using LocaleChars = mozilla::Variant; + + const LocaleChars& locale_; + size_t length_; + size_t index_ = 0; + + LanguageTagParser(const LocaleChars& locale, size_t length) + : locale_(locale), length_(length) {} + + char16_t charAtUnchecked(size_t index) const { + if (locale_.is()) { + return locale_.as()[index]; + } + return locale_.as()[index]; + } + + char charAt(size_t index) const { + char16_t c = charAtUnchecked(index); + MOZ_ASSERT(mozilla::IsAscii(c)); + return c; + } + + // Copy the token characters into |subtag|. + template + void copyChars(const Token& tok, LanguageTagSubtag& subtag) const { + size_t index = tok.index(); + size_t length = tok.length(); + if (locale_.is()) { + using T = const JS::Latin1Char; + subtag.set(mozilla::MakeSpan(locale_.as() + index, length)); + } else { + using T = const char16_t; + subtag.set(mozilla::MakeSpan(locale_.as() + index, length)); + } + } + + // Create a string copy of |length| characters starting at |index|. + JS::UniqueChars chars(JSContext* cx, size_t index, size_t length) const; + + // Create a string copy of the token characters. + JS::UniqueChars chars(JSContext* cx, const Token& tok) const { + return chars(cx, tok.index(), tok.length()); + } + + JS::UniqueChars extension(JSContext* cx, const Token& start, + const Token& end) const { + MOZ_ASSERT(start.index() < end.index()); + + size_t length = end.index() - 1 - start.index(); + return chars(cx, start.index(), length); + } + + Token nextToken(); + + // unicode_language_subtag = alpha{2,3} | alpha{5,8} ; + // + // Four character language subtags are not allowed in Unicode BCP 47 locale + // identifiers. Also see the comparison to Unicode CLDR locale identifiers in + // . + bool isLanguage(const Token& tok) const { + return tok.isAlpha() && ((2 <= tok.length() && tok.length() <= 3) || + (5 <= tok.length() && tok.length() <= 8)); + } + + // unicode_script_subtag = alpha{4} ; + bool isScript(const Token& tok) const { + return tok.isAlpha() && tok.length() == 4; + } + + // unicode_region_subtag = (alpha{2} | digit{3}) ; + bool isRegion(const Token& tok) const { + return (tok.isAlpha() && tok.length() == 2) || + (tok.isDigit() && tok.length() == 3); + } + + // unicode_variant_subtag = (alphanum{5,8} | digit alphanum{3}) ; + bool isVariant(const Token& tok) const { + return (5 <= tok.length() && tok.length() <= 8) || + (tok.length() == 4 && mozilla::IsAsciiDigit(charAt(tok.index()))); + } + + // Returns the code unit of the first character at the given singleton token. + // Always returns the lower case form of an alphabetical character. + char singletonKey(const Token& tok) const { + MOZ_ASSERT(tok.length() == 1); + return AsciiToLowerCase(charAt(tok.index())); + } + + // extensions = unicode_locale_extensions | + // transformed_extensions | + // other_extensions ; + // + // unicode_locale_extensions = sep [uU] ((sep keyword)+ | + // (sep attribute)+ (sep keyword)*) ; + // + // transformed_extensions = sep [tT] ((sep tlang (sep tfield)*) | + // (sep tfield)+) ; + // + // other_extensions = sep [alphanum-[tTuUxX]] (sep alphanum{2,8})+ ; + bool isExtensionStart(const Token& tok) const { + return tok.length() == 1 && singletonKey(tok) != 'x'; + } + + // other_extensions = sep [alphanum-[tTuUxX]] (sep alphanum{2,8})+ ; + bool isOtherExtensionPart(const Token& tok) const { + return 2 <= tok.length() && tok.length() <= 8; + } + + // unicode_locale_extensions = sep [uU] ((sep keyword)+ | + // (sep attribute)+ (sep keyword)*) ; + // keyword = key (sep type)? ; + bool isUnicodeExtensionPart(const Token& tok) const { + return isUnicodeExtensionKey(tok) || isUnicodeExtensionType(tok) || + isUnicodeExtensionAttribute(tok); + } + + // attribute = alphanum{3,8} ; + bool isUnicodeExtensionAttribute(const Token& tok) const { + return 3 <= tok.length() && tok.length() <= 8; + } + + // key = alphanum alpha ; + bool isUnicodeExtensionKey(const Token& tok) const { + return tok.length() == 2 && mozilla::IsAsciiAlpha(charAt(tok.index() + 1)); + } + + // type = alphanum{3,8} (sep alphanum{3,8})* ; + bool isUnicodeExtensionType(const Token& tok) const { + return 3 <= tok.length() && tok.length() <= 8; + } + + // tkey = alpha digit ; + bool isTransformExtensionKey(const Token& tok) const { + return tok.length() == 2 && mozilla::IsAsciiAlpha(charAt(tok.index())) && + mozilla::IsAsciiDigit(charAt(tok.index() + 1)); + } + + // tvalue = (sep alphanum{3,8})+ ; + bool isTransformExtensionPart(const Token& tok) const { + return 3 <= tok.length() && tok.length() <= 8; + } + + // pu_extensions = sep [xX] (sep alphanum{1,8})+ ; + bool isPrivateUseStart(const Token& tok) const { + return tok.length() == 1 && singletonKey(tok) == 'x'; + } + + // pu_extensions = sep [xX] (sep alphanum{1,8})+ ; + bool isPrivateUsePart(const Token& tok) const { + return 1 <= tok.length() && tok.length() <= 8; + } + + // Helper function for use in |parseBaseName| and + // |parseTlangInTransformExtension|. Do not use this directly! + static JS::Result internalParseBaseName(JSContext* cx, + LanguageTagParser& ts, + LanguageTag& tag, Token& tok); + + // Parse the `unicode_language_id` production, i.e. the + // language/script/region/variants portion of a language tag, into |tag|. + // |tok| must be the current token. + static JS::Result parseBaseName(JSContext* cx, LanguageTagParser& ts, + LanguageTag& tag, Token& tok) { + return internalParseBaseName(cx, ts, tag, tok); + } + + // Parse the `tlang` production within a parsed 't' transform extension. + // The precise requirements for "previously parsed" are: + // + // * the input begins from current token |tok| with a valid `tlang` + // * the `tlang` is wholly lowercase (*not* canonical case) + // * variant subtags in the `tlang` may contain duplicates and be + // unordered + // + // Return an error on internal failure. Otherwise, return a success value. If + // there was no `tlang`, then |tag.language().missing()|. But if there was a + // `tlang`, then |tag| is filled with subtags exactly as they appeared in the + // parse input. + static JS::Result parseTlangInTransformExtension( + JSContext* cx, LanguageTagParser& ts, LanguageTag& tag, Token& tok) { + MOZ_ASSERT(ts.isLanguage(tok)); + return internalParseBaseName(cx, ts, tag, tok).map([](bool parsed) { + MOZ_ASSERT(parsed); + return JS::Ok(); + }); + } + + friend class LanguageTag; + + class Range final { + size_t begin_; + size_t length_; + + public: + Range(size_t begin, size_t length) : begin_(begin), length_(length) {} + + template + T* begin(T* ptr) const { + return ptr + begin_; + } + + size_t length() const { return length_; } + }; + + using TFieldVector = js::Vector; + using AttributesVector = js::Vector; + using KeywordsVector = js::Vector; + + // Parse |extension|, which must be a validated, fully lowercase + // `transformed_extensions` subtag, and fill |tag| and |fields| from the + // `tlang` and `tfield` components. Data in |tag| is lowercase, consistent + // with |extension|. + static JS::Result parseTransformExtension( + JSContext* cx, mozilla::Span extension, LanguageTag& tag, + TFieldVector& fields); + + // Parse |extension|, which must be a validated, fully lowercase + // `unicode_locale_extensions` subtag, and fill |attributes| and |keywords| + // from the `attribute` and `keyword` components. + static JS::Result parseUnicodeExtension( + JSContext* cx, mozilla::Span extension, + AttributesVector& attributes, KeywordsVector& keywords); + + static JS::Result tryParse(JSContext* cx, LocaleChars& localeChars, + size_t localeLength, LanguageTag& tag); + + public: + // Parse the input string as a language tag. Reports an error to the context + // if the input can't be parsed completely. + static bool parse(JSContext* cx, JSLinearString* locale, LanguageTag& tag); + + // Parse the input string as a language tag. Reports an error to the context + // if the input can't be parsed completely. + static bool parse(JSContext* cx, mozilla::Span locale, + LanguageTag& tag); + + // Parse the input string as a language tag. Returns Ok(true) if the input + // could be completely parsed, Ok(false) if the input couldn't be parsed, + // or Err() in case of internal error. + static JS::Result tryParse(JSContext* cx, JSLinearString* locale, + LanguageTag& tag); + + // Parse the input string as a language tag. Returns Ok(true) if the input + // could be completely parsed, Ok(false) if the input couldn't be parsed, + // or Err() in case of internal error. + static JS::Result tryParse(JSContext* cx, + mozilla::Span locale, + LanguageTag& tag); + + // Parse the input string as the base-name parts (language, script, region, + // variants) of a language tag. Ignores any trailing characters. + static bool parseBaseName(JSContext* cx, mozilla::Span locale, + LanguageTag& tag); + + // Return true iff |extension| can be parsed as a Unicode extension subtag. + static bool canParseUnicodeExtension(mozilla::Span extension); + + // Return true iff |unicodeType| can be parsed as a Unicode extension type. + static bool canParseUnicodeExtensionType(JSLinearString* unicodeType); +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(LanguageTagParser::TokenKind) + +/** + * Parse a string as a standalone |language| tag. If |str| is a standalone + * language tag, store it in |result| and return true. Otherwise return false. + */ +MOZ_MUST_USE bool ParseStandaloneLanguageTag(JS::Handle str, + LanguageSubtag& result); + +/** + * Parse a string as a standalone |script| tag. If |str| is a standalone script + * tag, store it in |result| and return true. Otherwise return false. + */ +MOZ_MUST_USE bool ParseStandaloneScriptTag(JS::Handle str, + ScriptSubtag& result); + +/** + * Parse a string as a standalone |region| tag. If |str| is a standalone region + * tag, store it in |result| and return true. Otherwise return false. + */ +MOZ_MUST_USE bool ParseStandaloneRegionTag(JS::Handle str, + RegionSubtag& result); + +/** + * Parse a string as an ISO-639 language code. Return |nullptr| in the result if + * the input could not be parsed or the canonical form of the resulting language + * tag contains more than a single language subtag. + */ +JS::Result ParseStandaloneISO639LanguageTag( + JSContext* cx, JS::Handle str); + +class UnicodeExtensionKeyword final { + char key_[LanguageTagLimits::UnicodeKeyLength]; + JSLinearString* type_; + + public: + using UnicodeKey = const char (&)[LanguageTagLimits::UnicodeKeyLength + 1]; + using UnicodeKeySpan = + mozilla::Span; + + UnicodeExtensionKeyword(UnicodeKey key, JSLinearString* type) + : key_{key[0], key[1]}, type_(type) {} + + UnicodeKeySpan key() const { return {key_, sizeof(key_)}; } + JSLinearString* type() const { return type_; } + + void trace(JSTracer* trc); +}; + +extern MOZ_MUST_USE bool ApplyUnicodeExtensionToTag( + JSContext* cx, LanguageTag& tag, + JS::HandleVector keywords); + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_LanguageTag_h */ diff --git a/js/src/builtin/intl/LanguageTagGenerated.cpp b/js/src/builtin/intl/LanguageTagGenerated.cpp new file mode 100644 index 0000000000..bd99140ace --- /dev/null +++ b/js/src/builtin/intl/LanguageTagGenerated.cpp @@ -0,0 +1,950 @@ +// Generated by make_intl_data.py. DO NOT EDIT. +// Version: CLDR-35.1 +// URL: https://unicode.org/Public/cldr/35.1/core.zip + +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include +#include +#include +#include +#include +#include + +#include "jscntxt.h" +#include "jsstr.h" + +#include "builtin/intl/LanguageTag.h" + +using namespace js::intl::LanguageTagLimits; + +template +static inline bool HasReplacement( + const char (&subtags)[Length][TagLength], + const js::intl::LanguageTagSubtag& subtag) { + MOZ_ASSERT(subtag.length() == TagLength - 1, + "subtag must have the same length as the list of subtags"); + + const char* ptr = subtag.span().data(); + return std::binary_search(std::begin(subtags), std::end(subtags), ptr, + [](const char* a, const char* b) { + return memcmp(a, b, TagLength - 1) < 0; + }); +} + +template +static inline const char* SearchReplacement( + const char (&subtags)[Length][TagLength], + const char* (&aliases)[Length], + const js::intl::LanguageTagSubtag& subtag) { + MOZ_ASSERT(subtag.length() == TagLength - 1, + "subtag must have the same length as the list of subtags"); + + const char* ptr = subtag.span().data(); + auto p = std::lower_bound(std::begin(subtags), std::end(subtags), ptr, + [](const char* a, const char* b) { + return memcmp(a, b, TagLength - 1) < 0; + }); + if (p != std::end(subtags) && memcmp(*p, ptr, TagLength - 1) == 0) { + return aliases[std::distance(std::begin(subtags), p)]; + } + return nullptr; +} + +#ifdef DEBUG +static bool IsAsciiLowercaseAlphanumeric(char c) { + return mozilla::IsAsciiLowercaseAlpha(c) || mozilla::IsAsciiDigit(c); +} + +static bool IsAsciiLowercaseAlphanumericOrDash(char c) { + return IsAsciiLowercaseAlphanumeric(c) || c == '-'; +} + +static bool IsCanonicallyCasedLanguageTag(mozilla::Span span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + return std::all_of(span.begin(), span.end(), mozilla::IsAsciiLowercaseAlpha); +} + +static bool IsCanonicallyCasedRegionTag(mozilla::Span span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + return std::all_of(span.begin(), span.end(), mozilla::IsAsciiUppercaseAlpha) || + std::all_of(span.begin(), span.end(), mozilla::IsAsciiDigit); +} + +static bool IsCanonicallyCasedVariantTag(mozilla::Span span) { + // Tell the analysis the |std::all_of| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + return std::all_of(span.begin(), span.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedUnicodeKey(mozilla::Span key) { + return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedUnicodeType(mozilla::Span type) { + return std::all_of(type.begin(), type.end(), IsAsciiLowercaseAlphanumericOrDash); +} + +static bool IsCanonicallyCasedTransformKey(mozilla::Span key) { + return std::all_of(key.begin(), key.end(), IsAsciiLowercaseAlphanumeric); +} + +static bool IsCanonicallyCasedTransformType(mozilla::Span type) { + return std::all_of(type.begin(), type.end(), IsAsciiLowercaseAlphanumericOrDash); +} +#endif + +// Mappings from language subtags to preferred values. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::languageMapping(LanguageSubtag& language) { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language.span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language.span())); + + if (language.length() == 2) { + static const char languages[9][3] = { + "bh", "in", "iw", "ji", "jw", "mo", "no", "tl", "tw", + }; + static const char* aliases[9] = { + "bho", "id", "he", "yi", "jv", "ro", "nb", "fil", "ak", + }; + + if (const char* replacement = SearchReplacement(languages, aliases, language)) { + language.set(mozilla::MakeCStringSpan(replacement)); + return true; + } + return false; + } + + if (language.length() == 3) { + static const char languages[340][4] = { + "aam", "aar", "abk", "adp", "afr", "aju", "aka", "alb", "als", "amh", + "ara", "arb", "arg", "arm", "asm", "aue", "ava", "ave", "aym", "ayr", + "ayx", "aze", "azj", "bak", "bam", "baq", "bcc", "bcl", "bel", "ben", + "bgm", "bih", "bis", "bjd", "bod", "bos", "bre", "bul", "bur", "bxk", + "bxr", "cat", "ccq", "ces", "cha", "che", "chi", "chu", "chv", "cjr", + "cka", "cld", "cmk", "cmn", "cor", "cos", "coy", "cqu", "cre", "cwd", + "cym", "cze", "dan", "deu", "dgo", "dhd", "dik", "diq", "div", "drh", + "dut", "dzo", "ekk", "ell", "emk", "eng", "epo", "esk", "est", "eus", + "ewe", "fao", "fas", "fat", "fij", "fin", "fra", "fre", "fry", "fuc", + "ful", "gav", "gaz", "gbo", "geo", "ger", "gfx", "ggn", "gla", "gle", + "glg", "glv", "gno", "gre", "grn", "gti", "gug", "guj", "guv", "gya", + "hat", "hau", "hdn", "hea", "heb", "her", "him", "hin", "hmo", "hrr", + "hrv", "hun", "hye", "ibi", "ibo", "ice", "ido", "iii", "ike", "iku", + "ile", "ilw", "ina", "ind", "ipk", "isl", "ita", "jav", "jeg", "jpn", + "kal", "kan", "kas", "kat", "kau", "kaz", "kgc", "kgh", "khk", "khm", + "kik", "kin", "kir", "kmr", "knc", "kng", "knn", "koj", "kom", "kon", + "kor", "kpv", "krm", "ktr", "kua", "kur", "kvs", "kwq", "kxe", "kzj", + "kzt", "lao", "lat", "lav", "lbk", "lii", "lim", "lin", "lit", "lmm", + "ltz", "lub", "lug", "lvs", "mac", "mah", "mal", "mao", "mar", "may", + "meg", "mhr", "mkd", "mlg", "mlt", "mnk", "mol", "mon", "mri", "msa", + "mst", "mup", "mwj", "mya", "myt", "nad", "nau", "nav", "nbl", "ncp", + "nde", "ndo", "nep", "nld", "nno", "nnx", "nob", "nor", "npi", "nts", + "nya", "oci", "ojg", "oji", "ori", "orm", "ory", "oss", "oun", "pan", + "pbu", "pcr", "per", "pes", "pli", "plt", "pmc", "pmu", "pnb", "pol", + "por", "ppa", "ppr", "pry", "pus", "puz", "que", "quz", "rmy", "roh", + "ron", "rum", "run", "rus", "sag", "san", "sca", "scc", "scr", "sin", + "skk", "slk", "slo", "slv", "sme", "smo", "sna", "snd", "som", "sot", + "spa", "spy", "sqi", "src", "srd", "srp", "ssw", "sun", "swa", "swe", + "swh", "tah", "tam", "tat", "tdu", "tel", "tgk", "tgl", "tha", "thc", + "thx", "tib", "tie", "tir", "tkk", "tlw", "tmp", "tne", "ton", "tsf", + "tsn", "tso", "ttq", "tuk", "tur", "twi", "uig", "ukr", "umu", "uok", + "urd", "uzb", "uzn", "ven", "vie", "vol", "wel", "wln", "wol", "xba", + "xho", "xia", "xkh", "xpe", "xsj", "xsl", "ybd", "ydd", "yid", "yma", + "ymt", "yor", "yos", "yuu", "zai", "zha", "zho", "zsm", "zul", "zyb", + }; + static const char* aliases[340] = { + "aas", "aa", "ab", "dz", "af", "jrb", "ak", "sq", "sq", "am", + "ar", "ar", "an", "hy", "as", "ktz", "av", "ae", "ay", "ay", + "nun", "az", "az", "ba", "bm", "eu", "bal", "bik", "be", "bn", + "bcg", "bho", "bi", "drl", "bo", "bs", "br", "bg", "my", "luy", + "bua", "ca", "rki", "cs", "ch", "ce", "zh", "cu", "cv", "mom", + "cmr", "syr", "xch", "zh", "kw", "co", "pij", "quh", "cr", "cr", + "cy", "cs", "da", "de", "doi", "mwr", "din", "zza", "dv", "mn", + "nl", "dz", "et", "el", "man", "en", "eo", "ik", "et", "eu", + "ee", "fo", "fa", "ak", "fj", "fi", "fr", "fr", "fy", "ff", + "ff", "dev", "om", "grb", "ka", "de", "vaj", "gvr", "gd", "ga", + "gl", "gv", "gon", "el", "gn", "nyc", "gn", "gu", "duz", "gba", + "ht", "ha", "hai", "hmn", "he", "hz", "srx", "hi", "ho", "jal", + "hr", "hu", "hy", "opa", "ig", "is", "io", "ii", "iu", "iu", + "ie", "gal", "ia", "id", "ik", "is", "it", "jv", "oyb", "ja", + "kl", "kn", "ks", "ka", "kr", "kk", "tdf", "kml", "mn", "km", + "ki", "rw", "ky", "ku", "kr", "kg", "kok", "kwv", "kv", "kg", + "ko", "kv", "bmf", "dtp", "kj", "ku", "gdj", "yam", "tvd", "dtp", + "dtp", "lo", "la", "lv", "bnc", "raq", "li", "ln", "lt", "rmx", + "lb", "lu", "lg", "lv", "mk", "mh", "ml", "mi", "mr", "ms", + "cir", "chm", "mk", "mg", "mt", "man", "ro", "mn", "mi", "ms", + "mry", "raj", "vaj", "my", "mry", "xny", "na", "nv", "nr", "kdz", + "nd", "ng", "ne", "nl", "nn", "ngv", "nb", "nb", "ne", "pij", + "ny", "oc", "oj", "oj", "or", "om", "or", "os", "vaj", "pa", + "ps", "adx", "fa", "fa", "pi", "mg", "huw", "phr", "lah", "pl", + "pt", "bfy", "lcq", "prt", "ps", "pub", "qu", "qu", "rom", "rm", + "ro", "ro", "rn", "ru", "sg", "sa", "hle", "sr", "hr", "si", + "oyb", "sk", "sk", "sl", "se", "sm", "sn", "sd", "so", "st", + "es", "kln", "sq", "sc", "sc", "sr", "ss", "su", "sw", "sv", + "sw", "ty", "ta", "tt", "dtp", "te", "tg", "fil", "th", "tpo", + "oyb", "bo", "ras", "ti", "twm", "weo", "tyj", "kak", "to", "taj", + "tn", "ts", "tmh", "tk", "tr", "ak", "ug", "uk", "del", "ema", + "ur", "uz", "uz", "ve", "vi", "vo", "cy", "wa", "wo", "cax", + "xh", "acn", "waw", "kpe", "suj", "den", "rki", "yi", "yi", "lrr", + "mtm", "yo", "zom", "yug", "zap", "za", "zh", "ms", "zu", "za", + }; + + if (const char* replacement = SearchReplacement(languages, aliases, language)) { + language.set(mozilla::MakeCStringSpan(replacement)); + return true; + } + return false; + } + + return false; +} + +// Language subtags with complex mappings. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::complexLanguageMapping(const LanguageSubtag& language) { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language.span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language.span())); + + if (language.length() == 2) { + return language.equalTo("sh"); + } + + if (language.length() == 3) { + static const char languages[6][4] = { + "cnr", "drw", "hbs", "prs", "swc", "tnf", + }; + + return HasReplacement(languages, language); + } + + return false; +} + +// Mappings from region subtags to preferred values. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::regionMapping(RegionSubtag& region) { + MOZ_ASSERT(IsStructurallyValidRegionTag(region.span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(region.span())); + + if (region.length() == 2) { + static const char regions[23][3] = { + "BU", "CS", "CT", "DD", "DY", "FQ", "FX", "HV", "JT", "MI", + "NH", "NQ", "PU", "PZ", "QU", "RH", "TP", "UK", "VD", "WK", + "YD", "YU", "ZR", + }; + static const char* aliases[23] = { + "MM", "RS", "KI", "DE", "BJ", "AQ", "FR", "BF", "UM", "UM", + "VU", "AQ", "UM", "PA", "EU", "ZW", "TL", "GB", "VN", "UM", + "YE", "RS", "CD", + }; + + if (const char* replacement = SearchReplacement(regions, aliases, region)) { + region.set(mozilla::MakeCStringSpan(replacement)); + return true; + } + return false; + } + + { + static const char regions[300][4] = { + "004", "008", "010", "012", "016", "020", "024", "028", "031", "032", + "036", "040", "044", "048", "050", "051", "052", "056", "060", "062", + "064", "068", "070", "072", "074", "076", "084", "086", "090", "092", + "096", "100", "104", "108", "112", "116", "120", "124", "132", "136", + "140", "144", "148", "152", "156", "158", "162", "166", "170", "174", + "175", "178", "180", "184", "188", "191", "192", "196", "203", "204", + "208", "212", "214", "218", "222", "226", "230", "231", "232", "233", + "234", "238", "239", "242", "246", "248", "249", "250", "254", "258", + "260", "262", "266", "268", "270", "275", "276", "278", "280", "288", + "292", "296", "300", "304", "308", "312", "316", "320", "324", "328", + "332", "334", "336", "340", "344", "348", "352", "356", "360", "364", + "368", "372", "376", "380", "384", "388", "392", "398", "400", "404", + "408", "410", "414", "417", "418", "422", "426", "428", "430", "434", + "438", "440", "442", "446", "450", "454", "458", "462", "466", "470", + "474", "478", "480", "484", "492", "496", "498", "499", "500", "504", + "508", "512", "516", "520", "524", "528", "531", "533", "534", "535", + "540", "548", "554", "558", "562", "566", "570", "574", "578", "580", + "581", "583", "584", "585", "586", "591", "598", "600", "604", "608", + "612", "616", "620", "624", "626", "630", "634", "638", "642", "643", + "646", "652", "654", "659", "660", "662", "663", "666", "670", "674", + "678", "682", "686", "688", "690", "694", "702", "703", "704", "705", + "706", "710", "716", "720", "724", "728", "729", "732", "736", "740", + "744", "748", "752", "756", "760", "762", "764", "768", "772", "776", + "780", "784", "788", "792", "795", "796", "798", "800", "804", "807", + "818", "826", "830", "831", "832", "833", "834", "840", "850", "854", + "858", "860", "862", "876", "882", "886", "887", "891", "894", "958", + "959", "960", "962", "963", "964", "965", "966", "967", "968", "969", + "970", "971", "972", "973", "974", "975", "976", "977", "978", "979", + "980", "981", "982", "983", "984", "985", "986", "987", "988", "989", + "990", "991", "992", "993", "994", "995", "996", "997", "998", "999", + }; + static const char* aliases[300] = { + "AF", "AL", "AQ", "DZ", "AS", "AD", "AO", "AG", "AZ", "AR", + "AU", "AT", "BS", "BH", "BD", "AM", "BB", "BE", "BM", "034", + "BT", "BO", "BA", "BW", "BV", "BR", "BZ", "IO", "SB", "VG", + "BN", "BG", "MM", "BI", "BY", "KH", "CM", "CA", "CV", "KY", + "CF", "LK", "TD", "CL", "CN", "TW", "CX", "CC", "CO", "KM", + "YT", "CG", "CD", "CK", "CR", "HR", "CU", "CY", "CZ", "BJ", + "DK", "DM", "DO", "EC", "SV", "GQ", "ET", "ET", "ER", "EE", + "FO", "FK", "GS", "FJ", "FI", "AX", "FR", "FR", "GF", "PF", + "TF", "DJ", "GA", "GE", "GM", "PS", "DE", "DE", "DE", "GH", + "GI", "KI", "GR", "GL", "GD", "GP", "GU", "GT", "GN", "GY", + "HT", "HM", "VA", "HN", "HK", "HU", "IS", "IN", "ID", "IR", + "IQ", "IE", "IL", "IT", "CI", "JM", "JP", "KZ", "JO", "KE", + "KP", "KR", "KW", "KG", "LA", "LB", "LS", "LV", "LR", "LY", + "LI", "LT", "LU", "MO", "MG", "MW", "MY", "MV", "ML", "MT", + "MQ", "MR", "MU", "MX", "MC", "MN", "MD", "ME", "MS", "MA", + "MZ", "OM", "NA", "NR", "NP", "NL", "CW", "AW", "SX", "BQ", + "NC", "VU", "NZ", "NI", "NE", "NG", "NU", "NF", "NO", "MP", + "UM", "FM", "MH", "PW", "PK", "PA", "PG", "PY", "PE", "PH", + "PN", "PL", "PT", "GW", "TL", "PR", "QA", "RE", "RO", "RU", + "RW", "BL", "SH", "KN", "AI", "LC", "MF", "PM", "VC", "SM", + "ST", "SA", "SN", "RS", "SC", "SL", "SG", "SK", "VN", "SI", + "SO", "ZA", "ZW", "YE", "ES", "SS", "SD", "EH", "SD", "SR", + "SJ", "SZ", "SE", "CH", "SY", "TJ", "TH", "TG", "TK", "TO", + "TT", "AE", "TN", "TR", "TM", "TC", "TV", "UG", "UA", "MK", + "EG", "GB", "JE", "GG", "JE", "IM", "TZ", "US", "VI", "BF", + "UY", "UZ", "VE", "WF", "WS", "YE", "YE", "RS", "ZM", "AA", + "QM", "QN", "QP", "QQ", "QR", "QS", "QT", "EU", "QV", "QW", + "QX", "QY", "QZ", "XA", "XB", "XC", "XD", "XE", "XF", "XG", + "XH", "XI", "XJ", "XK", "XL", "XM", "XN", "XO", "XP", "XQ", + "XR", "XS", "XT", "XU", "XV", "XW", "XX", "XY", "XZ", "ZZ", + }; + + if (const char* replacement = SearchReplacement(regions, aliases, region)) { + region.set(mozilla::MakeCStringSpan(replacement)); + return true; + } + return false; + } +} + +// Region subtags with complex mappings. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::complexRegionMapping(const RegionSubtag& region) { + MOZ_ASSERT(IsStructurallyValidRegionTag(region.span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(region.span())); + + if (region.length() == 2) { + return region.equalTo("AN") || + region.equalTo("NT") || + region.equalTo("PC") || + region.equalTo("SU"); + } + + { + static const char regions[8][4] = { + "172", "200", "530", "532", "536", "582", "810", "890", + }; + + return HasReplacement(regions, region); + } +} + +// Language subtags with complex mappings. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +void js::intl::LanguageTag::performComplexLanguageMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); + + if (language().equalTo("cnr")) { + setLanguage("sr"); + if (region().missing()) { + setRegion("ME"); + } + } + else if (language().equalTo("drw") || + language().equalTo("prs") || + language().equalTo("tnf")) { + setLanguage("fa"); + if (region().missing()) { + setRegion("AF"); + } + } + else if (language().equalTo("hbs") || + language().equalTo("sh")) { + setLanguage("sr"); + if (script().missing()) { + setScript("Latn"); + } + } + else if (language().equalTo("swc")) { + setLanguage("sw"); + if (region().missing()) { + setRegion("CD"); + } + } +} + +// Region subtags with complex mappings. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +void js::intl::LanguageTag::performComplexRegionMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); + MOZ_ASSERT(IsStructurallyValidRegionTag(region().span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(region().span())); + + if (region().equalTo("172")) { + if (language().equalTo("hy") || + (language().equalTo("und") && script().equalTo("Armn"))) { + setRegion("AM"); + } + else if (language().equalTo("az") || + language().equalTo("tkr") || + language().equalTo("tly") || + language().equalTo("ttt")) { + setRegion("AZ"); + } + else if (language().equalTo("be")) { + setRegion("BY"); + } + else if (language().equalTo("ab") || + language().equalTo("ka") || + language().equalTo("os") || + (language().equalTo("und") && script().equalTo("Geor")) || + language().equalTo("xmf")) { + setRegion("GE"); + } + else if (language().equalTo("ky")) { + setRegion("KG"); + } + else if (language().equalTo("kk") || + (language().equalTo("ug") && script().equalTo("Cyrl"))) { + setRegion("KZ"); + } + else if (language().equalTo("gag")) { + setRegion("MD"); + } + else if (language().equalTo("tg")) { + setRegion("TJ"); + } + else if (language().equalTo("tk")) { + setRegion("TM"); + } + else if (language().equalTo("crh") || + language().equalTo("got") || + language().equalTo("ji") || + language().equalTo("rue") || + language().equalTo("uk") || + (language().equalTo("und") && script().equalTo("Goth"))) { + setRegion("UA"); + } + else if (language().equalTo("kaa") || + language().equalTo("sog") || + (language().equalTo("und") && script().equalTo("Sogd")) || + (language().equalTo("und") && script().equalTo("Sogo")) || + language().equalTo("uz")) { + setRegion("UZ"); + } + else { + setRegion("RU"); + } + } + else if (region().equalTo("200")) { + if (language().equalTo("sk")) { + setRegion("SK"); + } + else { + setRegion("CZ"); + } + } + else if (region().equalTo("530") || + region().equalTo("532") || + region().equalTo("AN")) { + if (language().equalTo("vic")) { + setRegion("SX"); + } + else { + setRegion("CW"); + } + } + else if (region().equalTo("536") || + region().equalTo("NT")) { + if (language().equalTo("akk") || + language().equalTo("ckb") || + (language().equalTo("ku") && script().equalTo("Arab")) || + language().equalTo("mis") || + language().equalTo("syr") || + (language().equalTo("und") && script().equalTo("Syrc")) || + (language().equalTo("und") && script().equalTo("Xsux")) || + (language().equalTo("und") && script().equalTo("Hatr"))) { + setRegion("IQ"); + } + else { + setRegion("SA"); + } + } + else if (region().equalTo("582") || + region().equalTo("PC")) { + if (language().equalTo("mh")) { + setRegion("MH"); + } + else if (language().equalTo("pau")) { + setRegion("PW"); + } + else { + setRegion("FM"); + } + } + else if (region().equalTo("810") || + region().equalTo("SU")) { + if (language().equalTo("hy") || + (language().equalTo("und") && script().equalTo("Armn"))) { + setRegion("AM"); + } + else if (language().equalTo("az") || + language().equalTo("tkr") || + language().equalTo("tly") || + language().equalTo("ttt")) { + setRegion("AZ"); + } + else if (language().equalTo("be")) { + setRegion("BY"); + } + else if (language().equalTo("et") || + language().equalTo("vro")) { + setRegion("EE"); + } + else if (language().equalTo("ab") || + language().equalTo("ka") || + language().equalTo("os") || + (language().equalTo("und") && script().equalTo("Geor")) || + language().equalTo("xmf")) { + setRegion("GE"); + } + else if (language().equalTo("ky")) { + setRegion("KG"); + } + else if (language().equalTo("kk") || + (language().equalTo("ug") && script().equalTo("Cyrl"))) { + setRegion("KZ"); + } + else if (language().equalTo("lt") || + language().equalTo("sgs")) { + setRegion("LT"); + } + else if (language().equalTo("ltg") || + language().equalTo("lv")) { + setRegion("LV"); + } + else if (language().equalTo("gag")) { + setRegion("MD"); + } + else if (language().equalTo("tg")) { + setRegion("TJ"); + } + else if (language().equalTo("tk")) { + setRegion("TM"); + } + else if (language().equalTo("crh") || + language().equalTo("got") || + language().equalTo("ji") || + language().equalTo("rue") || + language().equalTo("uk") || + (language().equalTo("und") && script().equalTo("Goth"))) { + setRegion("UA"); + } + else if (language().equalTo("kaa") || + language().equalTo("sog") || + (language().equalTo("und") && script().equalTo("Sogd")) || + (language().equalTo("und") && script().equalTo("Sogo")) || + language().equalTo("uz")) { + setRegion("UZ"); + } + else { + setRegion("RU"); + } + } + else if (region().equalTo("890")) { + if (language().equalTo("bs")) { + setRegion("BA"); + } + else if (language().equalTo("hr")) { + setRegion("HR"); + } + else if (language().equalTo("mk")) { + setRegion("MK"); + } + else if (language().equalTo("sl")) { + setRegion("SI"); + } + else { + setRegion("RS"); + } + } +} + +static const char* ToCharPointer(const char* str) { + return str; +} + +static const char* ToCharPointer(const js::UniqueChars& str) { + return str.get(); +} + +template +static bool IsLessThan(const T& a, const U& b) { + return strcmp(ToCharPointer(a), ToCharPointer(b)) < 0; +} + +// Mappings from variant subtags to preferred values. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::performVariantMappings(JSContext* cx) { + // The variant subtags need to be sorted for binary search. + MOZ_ASSERT(std::is_sorted(variants_.begin(), variants_.end(), + IsLessThan)); + + auto insertVariantSortedIfNotPresent = [&](const char* variant) { + auto* p = std::lower_bound(variants_.begin(), variants_.end(), variant, + IsLessThan); + + // Don't insert the replacement when already present. + if (p != variants_.end() && strcmp(p->get(), variant) == 0) { + return true; + } + + // Insert the preferred variant in sort order. + auto preferred = DuplicateString(cx, variant); + if (!preferred) { + return false; + } + return !!variants_.insert(p, std::move(preferred)); + }; + + for (size_t i = 0; i < variants_.length(); ) { + auto& variant = variants_[i]; + MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeCStringSpan(variant.get()))); + + if (strcmp(variant.get(), "aaland") == 0) { + variants_.erase(variants_.begin() + i); + setRegion("AX"); + } + else if (strcmp(variant.get(), "arevela") == 0) { + variants_.erase(variants_.begin() + i); + setLanguage("hy"); + } + else if (strcmp(variant.get(), "arevmda") == 0) { + variants_.erase(variants_.begin() + i); + setLanguage("hyw"); + } + else if (strcmp(variant.get(), "heploc") == 0) { + variants_.erase(variants_.begin() + i); + if (!insertVariantSortedIfNotPresent("alalc97")) { + return false; + } + } + else if (strcmp(variant.get(), "polytoni") == 0) { + variants_.erase(variants_.begin() + i); + if (!insertVariantSortedIfNotPresent("polyton")) { + return false; + } + } + else { + i++; + } + } + return true; +} + +// Canonicalize grandfathered locale identifiers. +// Derived from CLDR Supplemental Data, version 35.1. +// https://unicode.org/Public/cldr/35.1/core.zip +bool js::intl::LanguageTag::updateGrandfatheredMappings(JSContext* cx) { + // We're mapping regular grandfathered tags to non-grandfathered form here. + // Other tags remain unchanged. + // + // regular = "art-lojban" + // / "cel-gaulish" + // / "no-bok" + // / "no-nyn" + // / "zh-guoyu" + // / "zh-hakka" + // / "zh-min" + // / "zh-min-nan" + // / "zh-xiang" + // + // Therefore we can quickly exclude most tags by checking every + // |unicode_locale_id| subcomponent for characteristics not shared by any of + // the regular grandfathered (RG) tags: + // + // * Real-world |unicode_language_subtag|s are all two or three letters, + // so don't waste time running a useless |language.length > 3| fast-path. + // * No RG tag has a "script"-looking component. + // * No RG tag has a "region"-looking component. + // * The RG tags that match |unicode_locale_id| (art-lojban, cel-gaulish, + // zh-guoyu, zh-hakka, zh-xiang) have exactly one "variant". (no-bok, + // no-nyn, zh-min, and zh-min-nan require BCP47's extlang subtag + // that |unicode_locale_id| doesn't support.) + // * No RG tag contains |extensions| or |pu_extensions|. + if (script().present() || + region().present() || + variants().length() != 1 || + extensions().length() != 0 || + privateuse()) { + return true; + } + + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeCStringSpan(variants()[0].get()))); + + auto variantEqualTo = [this](const char* variant) { + return strcmp(variants()[0].get(), variant) == 0; + }; + + // art-lojban -> jbo + if (language().equalTo("art") && variantEqualTo("lojban")) { + setLanguage("jbo"); + clearVariants(); + return true; + } + + // cel-gaulish -> xtg-x-cel-gaulish + else if (language().equalTo("cel") && variantEqualTo("gaulish")) { + setLanguage("xtg"); + clearVariants(); + + auto privateuse = DuplicateString(cx, "x-cel-gaulish"); + if (!privateuse) { + return false; + } + setPrivateuse(std::move(privateuse)); + return true; + } + + // zh-guoyu -> zh + else if (language().equalTo("zh") && variantEqualTo("guoyu")) { + setLanguage("zh"); + clearVariants(); + return true; + } + + // zh-hakka -> hak + else if (language().equalTo("zh") && variantEqualTo("hakka")) { + setLanguage("hak"); + clearVariants(); + return true; + } + + // zh-xiang -> hsn + else if (language().equalTo("zh") && variantEqualTo("xiang")) { + setLanguage("hsn"); + clearVariants(); + return true; + } + + return true; +} + +template +static inline bool IsUnicodeKey( + mozilla::Span key, const char (&str)[Length]) { + static_assert(Length == UnicodeKeyLength + 1, + "Unicode extension key is two characters long"); + return memcmp(key.data(), str, Length - 1) == 0; +} + +template +static inline bool IsUnicodeType( + mozilla::Span type, const char (&str)[Length]) { + static_assert(Length > UnicodeKeyLength + 1, + "Unicode extension type contains more than two characters"); + return type.size() == (Length - 1) && + memcmp(type.data(), str, Length - 1) == 0; +} + +static int32_t CompareUnicodeType(const char* a, mozilla::Span b) { + MOZ_ASSERT(!std::char_traits::find(b.data(), b.size(), '\0'), + "unexpected null-character in string"); + + using UnsignedChar = unsigned char; + for (size_t i = 0; i < b.size(); i++) { + // |a| is zero-terminated and |b| doesn't contain a null-terminator. So if + // we've reached the end of |a|, the below if-statement will always be true. + // That ensures we don't read past the end of |a|. + if (int32_t r = UnsignedChar(a[i]) - UnsignedChar(b[i])) { + return r; + } + } + + // Return zero if both strings are equal or a negative number if |b| is a + // prefix of |a|. + return -int32_t(UnsignedChar(a[b.size()])); +} + +template +static inline const char* SearchUnicodeReplacement( + const char* (&types)[Length], const char* (&aliases)[Length], + mozilla::Span type) { + + auto p = std::lower_bound(std::begin(types), std::end(types), type, + [](const auto& a, const auto& b) { + return CompareUnicodeType(a, b) < 0; + }); + if (p != std::end(types) && CompareUnicodeType(*p, type) == 0) { + return aliases[std::distance(std::begin(types), p)]; + } + return nullptr; +} + +/** + * Mapping from deprecated BCP 47 Unicode extension types to their preferred + * values. + * + * Spec: https://www.unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files + * Spec: https://www.unicode.org/reports/tr35/#t_Extension + */ +const char* js::intl::LanguageTag::replaceUnicodeExtensionType( + mozilla::Span key, mozilla::Span type) { + MOZ_ASSERT(key.size() == UnicodeKeyLength); + MOZ_ASSERT(IsCanonicallyCasedUnicodeKey(key)); + + MOZ_ASSERT(type.size() > UnicodeKeyLength); + MOZ_ASSERT(IsCanonicallyCasedUnicodeType(type)); + + if (IsUnicodeKey(key, "ca")) { + if (IsUnicodeType(type, "ethiopic-amete-alem")) { + return "ethioaa"; + } + if (IsUnicodeType(type, "islamicc")) { + return "islamic-civil"; + } + } + else if (IsUnicodeKey(key, "kb") || + IsUnicodeKey(key, "kc") || + IsUnicodeKey(key, "kh") || + IsUnicodeKey(key, "kk") || + IsUnicodeKey(key, "kn")) { + if (IsUnicodeType(type, "yes")) { + return "true"; + } + } + else if (IsUnicodeKey(key, "ks")) { + if (IsUnicodeType(type, "primary")) { + return "level1"; + } + if (IsUnicodeType(type, "tertiary")) { + return "level3"; + } + } + else if (IsUnicodeKey(key, "ms")) { + if (IsUnicodeType(type, "imperial")) { + return "uksystem"; + } + } + else if (IsUnicodeKey(key, "rg") || + IsUnicodeKey(key, "sd")) { + static const char* types[116] = { + "cn11", "cn12", "cn13", "cn14", "cn15", "cn21", "cn22", "cn23", + "cn31", "cn32", "cn33", "cn34", "cn35", "cn36", "cn37", "cn41", + "cn42", "cn43", "cn44", "cn45", "cn46", "cn50", "cn51", "cn52", + "cn53", "cn54", "cn61", "cn62", "cn63", "cn64", "cn65", "cz10a", + "cz10b", "cz10c", "cz10d", "cz10e", "cz10f", "cz611", "cz612", "cz613", + "cz614", "cz615", "cz621", "cz622", "cz623", "cz624", "cz626", "cz627", + "czjc", "czjm", "czka", "czkr", "czli", "czmo", "czol", "czpa", + "czpl", "czpr", "czst", "czus", "czvy", "czzl", "fra", "frb", + "frc", "frd", "fre", "frf", "frg", "frh", "fri", "frj", + "frk", "frl", "frm", "frn", "fro", "frp", "frq", "frr", + "frs", "frt", "fru", "frv", "laxn", "lud", "lug", "lul", + "mrnkc", "nzn", "nzs", "omba", "omsh", "plds", "plkp", "pllb", + "plld", "pllu", "plma", "plmz", "plop", "plpd", "plpk", "plpm", + "plsk", "plsl", "plwn", "plwp", "plzp", "tteto", "ttrcm", "ttwto", + "twkhq", "twtnq", "twtpq", "twtxq", + }; + static const char* aliases[116] = { + "cnbj", "cntj", "cnhe", "cnsx", "cnmn", "cnln", "cnjl", "cnhl", + "cnsh", "cnjs", "cnzj", "cnah", "cnfj", "cnjx", "cnsd", "cnha", + "cnhb", "cnhn", "cngd", "cngx", "cnhi", "cncq", "cnsc", "cngz", + "cnyn", "cnxz", "cnsn", "cngs", "cnqh", "cnnx", "cnxj", "cz110", + "cz111", "cz112", "cz113", "cz114", "cz115", "cz663", "cz632", "cz633", + "cz634", "cz635", "cz641", "cz642", "cz643", "cz644", "cz646", "cz647", + "cz31", "cz64", "cz41", "cz52", "cz51", "cz80", "cz71", "cz53", + "cz32", "cz10", "cz20", "cz42", "cz63", "cz72", "frges", "frnaq", + "frara", "frbfc", "frbre", "frcvl", "frges", "frcor", "frbfc", "fridf", + "frocc", "frnaq", "frges", "frocc", "frhdf", "frnor", "frnor", "frpdl", + "frhdf", "frnaq", "frpac", "frara", "laxs", "lucl", "luec", "luca", + "mr13", "nzauk", "nzcan", "ombj", "omsj", "pl02", "pl04", "pl08", + "pl10", "pl06", "pl12", "pl14", "pl16", "pl20", "pl18", "pl22", + "pl26", "pl24", "pl28", "pl30", "pl32", "tttob", "ttmrc", "tttob", + "twkhh", "twtnn", "twnwt", "twtxg", + }; + return SearchUnicodeReplacement(types, aliases, type); + } + else if (IsUnicodeKey(key, "tz")) { + static const char* types[28] = { + "aqams", "cnckg", "cnhrb", "cnkhg", "cuba", "egypt", + "eire", "est", "gmt0", "hongkong", "hst", "iceland", + "iran", "israel", "jamaica", "japan", "libya", "mst", + "navajo", "poland", "portugal", "prc", "roc", "rok", + "turkey", "uct", "usnavajo", "zulu", + }; + static const char* aliases[28] = { + "nzakl", "cnsha", "cnsha", "cnurc", "cuhav", "egcai", + "iedub", "utcw05", "gmt", "hkhkg", "utcw10", "isrey", + "irthr", "jeruslm", "jmkin", "jptyo", "lytip", "utcw07", + "usden", "plwaw", "ptlis", "cnsha", "twtpe", "krsel", + "trist", "utc", "usden", "utc", + }; + return SearchUnicodeReplacement(types, aliases, type); + } + return nullptr; +} + +template +static inline bool IsTransformKey( + mozilla::Span key, const char (&str)[Length]) { + static_assert(Length == TransformKeyLength + 1, + "Transform extension key is two characters long"); + return memcmp(key.data(), str, Length - 1) == 0; +} + +template +static inline bool IsTransformType( + mozilla::Span type, const char (&str)[Length]) { + static_assert(Length > TransformKeyLength + 1, + "Transform extension type contains more than two characters"); + return type.size() == (Length - 1) && + memcmp(type.data(), str, Length - 1) == 0; +} + +/** + * Mapping from deprecated BCP 47 Transform extension types to their preferred + * values. + * + * Spec: https://www.unicode.org/reports/tr35/#Unicode_Locale_Extension_Data_Files + * Spec: https://www.unicode.org/reports/tr35/#t_Extension + */ +const char* js::intl::LanguageTag::replaceTransformExtensionType( + mozilla::Span key, mozilla::Span type) { + MOZ_ASSERT(key.size() == TransformKeyLength); + MOZ_ASSERT(IsCanonicallyCasedTransformKey(key)); + + MOZ_ASSERT(type.size() > TransformKeyLength); + MOZ_ASSERT(IsCanonicallyCasedTransformType(type)); + + if (IsTransformKey(key, "d0")) { + if (IsTransformType(type, "name")) { + return "charname"; + } + } + else if (IsTransformKey(key, "m0")) { + if (IsTransformType(type, "names")) { + return "prprname"; + } + } + return nullptr; +} diff --git a/js/src/builtin/intl/Locale.cpp b/js/src/builtin/intl/Locale.cpp new file mode 100644 index 0000000000..980ab37f66 --- /dev/null +++ b/js/src/builtin/intl/Locale.cpp @@ -0,0 +1,1430 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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.Locale implementation. */ + +#include "builtin/intl/Locale.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include +#include +#include +#include +#include + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "jscntxt.h" +#include "jsobjinlines.h" +#include "jswrapper.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/LanguageTag.h" +#include "gc/Rooting.h" +#include "js/Conversions.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/String.h" +#include "vm/StringBuffer.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; +using namespace js::intl::LanguageTagLimits; + +using intl::LanguageTag; +using intl::LanguageTagParser; + +const Class LocaleObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(LocaleObject::SLOT_COUNT), +}; + +static inline bool IsLocale(HandleValue v) { + return v.isObject() && v.toObject().is(); +} + +// Return the length of the base-name subtags. +static size_t BaseNameLength(const LanguageTag& tag) { + size_t baseNameLength = tag.language().length(); + if (tag.script().present()) { + baseNameLength += 1 + tag.script().length(); + } + if (tag.region().present()) { + baseNameLength += 1 + tag.region().length(); + } + for (const auto& variant : tag.variants()) { + baseNameLength += 1 + strlen(variant.get()); + } + return baseNameLength; +} + +struct IndexAndLength { + size_t index; + size_t length; + + IndexAndLength(size_t index, size_t length) : index(index), length(length){}; + + template + mozilla::Span spanOf(const T* ptr) const { + return {ptr + index, length}; + } +}; + +// Compute the Unicode extension's index and length in the extension subtag. +static mozilla::Maybe UnicodeExtensionPosition( + const LanguageTag& tag) { + size_t index = 0; + for (const auto& extension : tag.extensions()) { + MOZ_ASSERT(!mozilla::IsAsciiUppercaseAlpha(extension[0]), + "extensions are case normalized to lowercase"); + + size_t extensionLength = strlen(extension.get()); + if (extension[0] == 'u') { + return mozilla::Some(IndexAndLength{index, extensionLength}); + } + + // Add +1 to skip over the preceding separator. + index += 1 + extensionLength; + } + return mozilla::Nothing(); +} + +static LocaleObject* CreateLocaleObject(JSContext* cx, HandleObject prototype, + const LanguageTag& tag) { + RootedObject proto(cx, prototype); + if (!proto) { + proto = GlobalObject::getOrCreateLocalePrototype(cx, cx->global()); + if (!proto) { + return nullptr; + } + } + + RootedString tagStr(cx, tag.toString(cx)); + if (!tagStr) { + return nullptr; + } + + size_t baseNameLength = BaseNameLength(tag); + + RootedString baseName(cx, NewDependentString(cx, tagStr, 0, baseNameLength)); + if (!baseName) { + return nullptr; + } + + RootedValue unicodeExtension(cx, UndefinedValue()); + if (auto result = UnicodeExtensionPosition(tag)) { + JSString* str = NewDependentString( + cx, tagStr, baseNameLength + 1 + result->index, result->length); + if (!str) { + return nullptr; + } + + unicodeExtension.setString(str); + } + + auto* locale = NewObjectWithGivenProto(cx, proto); + if (!locale) { + return nullptr; + } + + locale->setFixedSlot(LocaleObject::LANGUAGE_TAG_SLOT, StringValue(tagStr)); + locale->setFixedSlot(LocaleObject::BASENAME_SLOT, StringValue(baseName)); + locale->setFixedSlot(LocaleObject::UNICODE_EXTENSION_SLOT, unicodeExtension); + + return locale; +} + +static inline bool IsValidUnicodeExtensionValue(JSLinearString* linear) { + return linear->length() > 0 && + LanguageTagParser::canParseUnicodeExtensionType(linear); +} + +/** Iterate through (sep keyword) in a valid, lowercased Unicode extension. */ +template +class SepKeywordIterator { + const CharT* iter_; + const CharT* const end_; + + public: + SepKeywordIterator(const CharT* unicodeExtensionBegin, + const CharT* unicodeExtensionEnd) + : iter_(unicodeExtensionBegin), end_(unicodeExtensionEnd) {} + + /** + * Return (sep keyword) in the Unicode locale extension from begin to end. + * The first call after all (sep keyword) are consumed returns |nullptr|; no + * further calls are allowed. + */ + const CharT* next() { + MOZ_ASSERT(iter_ != nullptr, + "can't call next() once it's returned nullptr"); + + constexpr size_t SepKeyLength = 1 + UnicodeKeyLength; // "-co"/"-nu"/etc. + + MOZ_ASSERT(iter_ + SepKeyLength <= end_, + "overall Unicode locale extension or non-leading subtags must " + "be at least key-sized"); + + MOZ_ASSERT((iter_[0] == 'u' && iter_[1] == '-') || iter_[0] == '-'); + + while (true) { + // Skip past '-' so |std::char_traits::find| makes progress. Skipping + // 'u' is harmless -- skip or not, |find| returns the first '-'. + iter_++; + + // Find the next separator. + iter_ = std::char_traits::find( + iter_, mozilla::PointerRangeSize(iter_, end_), CharT('-')); + if (!iter_) { + return nullptr; + } + + MOZ_ASSERT(iter_ + SepKeyLength <= end_, + "non-leading subtags in a Unicode locale extension are all " + "at least as long as a key"); + + if (iter_ + SepKeyLength == end_ || // key is terminal subtag + iter_[SepKeyLength] == '-') { // key is followed by more subtags + break; + } + } + + MOZ_ASSERT(iter_[0] == '-'); + MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[1]) || + mozilla::IsAsciiDigit(iter_[1])); + MOZ_ASSERT(mozilla::IsAsciiLowercaseAlpha(iter_[2])); + MOZ_ASSERT_IF(iter_ + SepKeyLength < end_, iter_[SepKeyLength] == '-'); + return iter_; + } +}; + +/** + * 9.2.10 GetOption ( options, property, type, values, fallback ) + * + * If the requested property is present and not-undefined, set the result string + * to |ToString(value)|. Otherwise set the result string to nullptr. + */ +static bool GetStringOption(JSContext* cx, HandleObject options, + HandlePropertyName name, + MutableHandle string) { + // Step 1. + RootedValue option(cx); + if (!GetProperty(cx, options, options, name, &option)) { + return false; + } + + // Step 2. + JSLinearString* linear = nullptr; + if (!option.isUndefined()) { + // Steps 2.a-b, 2.d (not applicable). + + // Steps 2.c, 2.e. + JSString* str = ToString(cx, option); + if (!str) { + return false; + } + linear = str->ensureLinear(cx); + if (!linear) { + return false; + } + } + + // Step 3. + string.set(linear); + return true; +} + +/** + * 9.2.10 GetOption ( options, property, type, values, fallback ) + * + * If the requested property is present and not-undefined, set the result string + * to |ToString(ToBoolean(value))|. Otherwise set the result string to nullptr. + */ +static bool GetBooleanOption(JSContext* cx, HandleObject options, + HandlePropertyName name, + MutableHandle string) { + // Step 1. + RootedValue option(cx); + if (!GetProperty(cx, options, options, name, &option)) { + return false; + } + + // Step 2. + JSLinearString* linear = nullptr; + if (!option.isUndefined()) { + // Steps 2.a, 2.c-d (not applicable). + + // Steps 2.c, 2.e. + JSString* str = BooleanToString(cx, ToBoolean(option)); + MOZ_ALWAYS_TRUE(linear = str->ensureLinear(cx)); + } + + // Step 3. + string.set(linear); + return true; +} + +/** + * ApplyOptionsToTag ( tag, options ) + */ +static bool ApplyOptionsToTag(JSContext* cx, LanguageTag& tag, + HandleObject options) { + // Steps 1-2 (Already performed in caller). + + RootedLinearString option(cx); + + // Step 3. + if (!GetStringOption(cx, options, cx->names().language, &option)) { + return false; + } + + // Step 4. + intl::LanguageSubtag language; + if (option && !intl::ParseStandaloneLanguageTag(option, language)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "language", + str.get()); + } + return false; + } + + // Step 5. + if (!GetStringOption(cx, options, cx->names().script, &option)) { + return false; + } + + // Step 6. + intl::ScriptSubtag script; + if (option && !intl::ParseStandaloneScriptTag(option, script)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "script", str.get()); + } + return false; + } + + // Step 7. + if (!GetStringOption(cx, options, cx->names().region, &option)) { + return false; + } + + // Step 8. + intl::RegionSubtag region; + if (option && !intl::ParseStandaloneRegionTag(option, region)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *option)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "region", str.get()); + } + return false; + } + + // Step 9 (Already performed in caller). + + // Skip steps 10-13 when no subtags were modified. + if (language.present() || script.present() || region.present()) { + // Step 10. + if (language.present()) { + tag.setLanguage(language); + } + + // Step 11. + if (script.present()) { + tag.setScript(script); + } + + // Step 12. + if (region.present()) { + tag.setRegion(region); + } + + // Step 13. + // Optimized to only canonicalize the base-name subtags. All other + // canonicalization steps will happen later. + if (!tag.canonicalizeBaseName(cx)) { + return true; + } + } + + return true; +} + +/** + * ApplyUnicodeExtensionToTag( tag, options, relevantExtensionKeys ) + */ +bool js::intl::ApplyUnicodeExtensionToTag( + JSContext* cx, LanguageTag& tag, + JS::HandleVector keywords) { + // If no Unicode extensions were present in the options object, we can skip + // everything below and directly return. + if (keywords.length() == 0) { + return true; + } + + Vector newExtension(cx); + if (!newExtension.append('u')) { + return false; + } + + // Check if there's an existing Unicode extension subtag. + + const char* unicodeExtensionEnd = nullptr; + const char* unicodeExtensionKeywords = nullptr; + if (const char* unicodeExtension = tag.unicodeExtension()) { + unicodeExtensionEnd = unicodeExtension + strlen(unicodeExtension); + + SepKeywordIterator iter(unicodeExtension, unicodeExtensionEnd); + + // Find the start of the first keyword. + unicodeExtensionKeywords = iter.next(); + + // Copy any attributes present before the first keyword. + const char* attributesEnd = unicodeExtensionKeywords + ? unicodeExtensionKeywords + : unicodeExtensionEnd; + if (!newExtension.append(unicodeExtension + 1, attributesEnd)) { + return false; + } + } + + // Append the new keywords before any existing keywords. That way any previous + // keyword with the same key is detected as a duplicate when canonicalizing + // the Unicode extension subtag and gets discarded. + + for (const auto& keyword : keywords) { + UnicodeExtensionKeyword::UnicodeKeySpan key = keyword.key(); + if (!newExtension.append('-')) { + return false; + } + if (!newExtension.append(key.data(), key.size())) { + return false; + } + if (!newExtension.append('-')) { + return false; + } + + JS::AutoCheckCannotGC nogc; + JSLinearString* type = keyword.type(); + if (type->hasLatin1Chars()) { + if (!newExtension.append(type->latin1Chars(nogc), type->length())) { + return false; + } + } else { + if (!newExtension.append(type->twoByteChars(nogc), type->length())) { + return false; + } + } + } + + // Append the remaining keywords from the previous Unicode extension subtag. + if (unicodeExtensionKeywords) { + if (!newExtension.append(unicodeExtensionKeywords, unicodeExtensionEnd)) { + return false; + } + } + + // Null-terminate the new Unicode extension string. + if (!newExtension.append('\0')) { + return false; + } + + // Insert the new Unicode extension string into the language tag. + UniqueChars newExtensionChars(newExtension.extractOrCopyRawBuffer()); + if (!newExtensionChars) { + return false; + } + return tag.setUnicodeExtension(std::move(newExtensionChars)); +} + +static JS::Result LanguageTagFromMaybeWrappedLocale(JSContext* cx, + JSObject* obj) { + if (obj->is()) { + return obj->as().languageTag(); + } + + JSObject* unwrapped = CheckedUnwrap(obj); + if (!unwrapped) { + /* ReportAccessDenied(cx); */ + return cx->alreadyReportedError(); + } + + if (!unwrapped->is()) { + return nullptr; + } + + RootedString tagStr(cx, unwrapped->as().languageTag()); + if (!cx->compartment()->wrap(cx, &tagStr)) { + return cx->alreadyReportedError(); + } + return tagStr.get(); +} + +/** + * Intl.Locale( tag[, options] ) + */ +static bool Locale(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.Locale")) { + return false; + } + + // Steps 2-6 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromCallableConstructor(cx, args, &proto)) { + return false; + } + + // Steps 7-9. + HandleValue tagValue = args.get(0); + JSString* tagStr; + if (tagValue.isObject()) { + JS_TRY_VAR_OR_RETURN_FALSE( + cx, tagStr, + LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject())); + if (!tagStr) { + tagStr = ToString(cx, tagValue); + if (!tagStr) { + return false; + } + } + } else if (tagValue.isString()) { + tagStr = tagValue.toString(); + } else { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_LOCALES_ELEMENT); + return false; + } + + RootedLinearString tagLinearStr(cx, tagStr->ensureLinear(cx)); + if (!tagLinearStr) { + return false; + } + + // ApplyOptionsToTag, steps 2 and 9. + LanguageTag tag(cx); + if (!LanguageTagParser::parse(cx, tagLinearStr, tag)) { + return false; + } + + if (!tag.canonicalizeBaseName(cx)) { + return false; + } + + // Steps 10-11. + if (args.hasDefined(1)) { + RootedObject options(cx, ToObject(cx, args[1])); + if (!options) { + return false; + } + + // Step 12. + if (!ApplyOptionsToTag(cx, tag, options)) { + return false; + } + + // Step 13. + JS::RootedVector keywords(cx); + + // Step 14. + RootedLinearString calendar(cx); + if (!GetStringOption(cx, options, cx->names().calendar, &calendar)) { + return false; + } + + // Steps 15-16. + if (calendar) { + if (!IsValidUnicodeExtensionValue(calendar)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *calendar)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "calendar", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("ca", calendar)) { + return false; + } + } + + // Step 17. + RootedLinearString collation(cx); + if (!GetStringOption(cx, options, cx->names().collation, &collation)) { + return false; + } + + // Steps 18-19. + if (collation) { + if (!IsValidUnicodeExtensionValue(collation)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *collation)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "collation", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("co", collation)) { + return false; + } + } + + // Step 20 (without validation). + RootedLinearString hourCycle(cx); + if (!GetStringOption(cx, options, cx->names().hourCycle, &hourCycle)) { + return false; + } + + // Steps 20-21. + if (hourCycle) { + if (!StringEqualsAscii(hourCycle, "h11") && + !StringEqualsAscii(hourCycle, "h12") && + !StringEqualsAscii(hourCycle, "h23") && + !StringEqualsAscii(hourCycle, "h24")) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *hourCycle)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "hourCycle", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("hc", hourCycle)) { + return false; + } + } + + // Step 22 (without validation). + RootedLinearString caseFirst(cx); + if (!GetStringOption(cx, options, cx->names().caseFirst, &caseFirst)) { + return false; + } + + // Steps 22-23. + if (caseFirst) { + if (!StringEqualsAscii(caseFirst, "upper") && + !StringEqualsAscii(caseFirst, "lower") && + !StringEqualsAscii(caseFirst, "false")) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *caseFirst)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "caseFirst", + str.get()); + } + return false; + } + + if (!keywords.emplaceBack("kf", caseFirst)) { + return false; + } + } + + // Steps 24-25. + RootedLinearString numeric(cx); + if (!GetBooleanOption(cx, options, cx->names().numeric, &numeric)) { + return false; + } + + // Step 26. + if (numeric) { + if (!keywords.emplaceBack("kn", numeric)) { + return false; + } + } + + // Step 27. + RootedLinearString numberingSystem(cx); + if (!GetStringOption(cx, options, cx->names().numberingSystem, + &numberingSystem)) { + return false; + } + + // Steps 28-29. + if (numberingSystem) { + if (!IsValidUnicodeExtensionValue(numberingSystem)) { + if (UniqueChars str = StringToNewUTF8CharsZ(cx, *numberingSystem)) { + JS_ReportErrorNumberUTF8(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, + "numberingSystem", str.get()); + } + return false; + } + + if (!keywords.emplaceBack("nu", numberingSystem)) { + return false; + } + } + + // Step 30. + if (!ApplyUnicodeExtensionToTag(cx, tag, keywords)) { + return false; + } + } + + // ApplyOptionsToTag, steps 9 and 13. + // ApplyUnicodeExtensionToTag, step 9. + if (!tag.canonicalizeExtensions(cx)) { + return false; + } + + // Steps 6, 31-37. + JSObject* obj = CreateLocaleObject(cx, proto, tag); + if (!obj) { + return false; + } + + // Step 38. + args.rval().setObject(*obj); + return true; +} + +using UnicodeKey = const char (&)[UnicodeKeyLength + 1]; + +// Returns the tuple [index, length] of the `type` in the `keyword` in Unicode +// locale extension |extension| that has |key| as its `key`. If `keyword` lacks +// a type, the returned |index| will be where `type` would have been, and +// |length| will be set to zero. +template +static mozilla::Maybe FindUnicodeExtensionType( + const CharT* extension, size_t length, UnicodeKey key) { + MOZ_ASSERT(extension[0] == 'u'); + MOZ_ASSERT(extension[1] == '-'); + + const CharT* end = extension + length; + + SepKeywordIterator iter(extension, end); + + // Search all keywords until a match was found. + const CharT* beginKey; + while (true) { + beginKey = iter.next(); + if (!beginKey) { + return mozilla::Nothing(); + } + + // Add +1 to skip over the separator preceding the keyword. + MOZ_ASSERT(beginKey[0] == '-'); + beginKey++; + + // Exit the loop on the first match. + if (std::equal(beginKey, beginKey + UnicodeKeyLength, key)) { + break; + } + } + + // Skip over the key. + const CharT* beginType = beginKey + UnicodeKeyLength; + + // Find the start of the next keyword. + const CharT* endType = iter.next(); + + // No further keyword present, the current keyword ends the Unicode extension. + if (!endType) { + endType = end; + } + + // If the keyword has a type, skip over the separator preceding the type. + if (beginType != endType) { + MOZ_ASSERT(beginType[0] == '-'); + beginType++; + } + return mozilla::Some(IndexAndLength{size_t(beginType - extension), + size_t(endType - beginType)}); +} + +static inline auto FindUnicodeExtensionType(JSLinearString* unicodeExtension, + UnicodeKey key) { + JS::AutoCheckCannotGC nogc; + return unicodeExtension->hasLatin1Chars() + ? FindUnicodeExtensionType(unicodeExtension->latin1Chars(nogc), + unicodeExtension->length(), key) + : FindUnicodeExtensionType(unicodeExtension->twoByteChars(nogc), + unicodeExtension->length(), key); +} + +// Return the sequence of types for the Unicode extension keyword specified by +// key or undefined when the keyword isn't present. +static bool GetUnicodeExtension(JSContext* cx, LocaleObject* locale, + UnicodeKey key, MutableHandleValue value) { + // Return undefined when no Unicode extension subtag is present. + const Value& unicodeExtensionValue = locale->unicodeExtension(); + if (unicodeExtensionValue.isUndefined()) { + value.setUndefined(); + return true; + } + + JSLinearString* unicodeExtension = + unicodeExtensionValue.toString()->ensureLinear(cx); + if (!unicodeExtension) { + return false; + } + + // Find the type of the requested key in the Unicode extension subtag. + auto result = FindUnicodeExtensionType(unicodeExtension, key); + + // Return undefined if the requested key isn't present in the extension. + if (!result) { + value.setUndefined(); + return true; + } + + size_t index = result->index; + size_t length = result->length; + + // Otherwise return the type value of the found keyword. + JSString* str = NewDependentString(cx, unicodeExtension, index, length); + if (!str) { + return false; + } + value.setString(str); + return true; +} + +struct BaseNamePartsResult { + IndexAndLength language; + mozilla::Maybe script; + mozilla::Maybe region; +}; + +// Returns [language-length, script-index, region-index, region-length]. +template +static BaseNamePartsResult BaseNameParts(const CharT* baseName, size_t length) { + size_t languageLength; + size_t scriptIndex = 0; + size_t regionIndex = 0; + size_t regionLength = 0; + + // Search the first separator to find the end of the language subtag. + if (const CharT* sep = std::char_traits::find(baseName, length, '-')) { + languageLength = sep - baseName; + + // Add +1 to skip over the separator character. + size_t nextSubtag = languageLength + 1; + + // Script subtags are always four characters long, but take care for a four + // character long variant subtag. These start with a digit. + if ((nextSubtag + ScriptLength == length || + (nextSubtag + ScriptLength < length && + baseName[nextSubtag + ScriptLength] == '-')) && + mozilla::IsAsciiAlpha(baseName[nextSubtag])) { + scriptIndex = nextSubtag; + nextSubtag = scriptIndex + ScriptLength + 1; + } + + // Region subtags can be either two or three characters long. + if (nextSubtag < length) { + for (size_t rlen : {AlphaRegionLength, DigitRegionLength}) { + MOZ_ASSERT(nextSubtag + rlen <= length); + if (nextSubtag + rlen == length || baseName[nextSubtag + rlen] == '-') { + regionIndex = nextSubtag; + regionLength = rlen; + break; + } + } + } + } else { + // No separator found, the base-name consists of just a language subtag. + languageLength = length; + } + + IndexAndLength language{0, languageLength}; + MOZ_ASSERT(intl::IsStructurallyValidLanguageTag(language.spanOf(baseName))); + + mozilla::Maybe script{}; + if (scriptIndex) { + script.emplace(scriptIndex, ScriptLength); + MOZ_ASSERT(intl::IsStructurallyValidScriptTag(script->spanOf(baseName))); + } + + mozilla::Maybe region{}; + if (regionIndex) { + region.emplace(regionIndex, regionLength); + MOZ_ASSERT(intl::IsStructurallyValidRegionTag(region->spanOf(baseName))); + } + + return {language, script, region}; +} + +static inline auto BaseNameParts(JSLinearString* baseName) { + JS::AutoCheckCannotGC nogc; + return baseName->hasLatin1Chars() + ? BaseNameParts(baseName->latin1Chars(nogc), baseName->length()) + : BaseNameParts(baseName->twoByteChars(nogc), baseName->length()); +} + +// Intl.Locale.prototype.maximize () +static bool Locale_maximize(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx)); + if (!tagStr) { + return false; + } + + LanguageTag tag(cx); + if (!LanguageTagParser::parse(cx, tagStr, tag)) { + return false; + } + + if (!tag.addLikelySubtags(cx)) { + return false; + } + + // Step 4. + auto* result = CreateLocaleObject(cx, nullptr, tag); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +// Intl.Locale.prototype.maximize () +static bool Locale_maximize(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// Intl.Locale.prototype.minimize () +static bool Locale_minimize(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + RootedLinearString tagStr(cx, locale->languageTag()->ensureLinear(cx)); + if (!tagStr) { + return false; + } + + LanguageTag tag(cx); + if (!LanguageTagParser::parse(cx, tagStr, tag)) { + return false; + } + + if (!tag.removeLikelySubtags(cx)) { + return false; + } + + // Step 4. + auto* result = CreateLocaleObject(cx, nullptr, tag); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +// Intl.Locale.prototype.minimize () +static bool Locale_minimize(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// Intl.Locale.prototype.toString () +static bool Locale_toString(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + args.rval().setString(locale->languageTag()); + return true; +} + +// Intl.Locale.prototype.toString () +static bool Locale_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.baseName +static bool Locale_baseName(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Steps 3-4. + auto* locale = &args.thisv().toObject().as(); + args.rval().setString(locale->baseName()); + return true; +} + +// get Intl.Locale.prototype.baseName +static bool Locale_baseName(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.calendar +static bool Locale_calendar(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + return GetUnicodeExtension(cx, locale, "ca", args.rval()); +} + +// get Intl.Locale.prototype.calendar +static bool Locale_calendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.caseFirst +static bool Locale_caseFirst(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + return GetUnicodeExtension(cx, locale, "kf", args.rval()); +} + +// get Intl.Locale.prototype.caseFirst +static bool Locale_caseFirst(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.collation +static bool Locale_collation(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + return GetUnicodeExtension(cx, locale, "co", args.rval()); +} + +// get Intl.Locale.prototype.collation +static bool Locale_collation(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.hourCycle +static bool Locale_hourCycle(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + return GetUnicodeExtension(cx, locale, "hc", args.rval()); +} + +// get Intl.Locale.prototype.hourCycle +static bool Locale_hourCycle(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.numeric +static bool Locale_numeric(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + RootedValue value(cx); + if (!GetUnicodeExtension(cx, locale, "kn", &value)) { + return false; + } + + // Compare against the empty string per Intl.Locale, step 36.a. The Unicode + // extension is already canonicalized, so we don't need to compare against + // "true" at this point. + MOZ_ASSERT(value.isUndefined() || value.isString()); + MOZ_ASSERT_IF(value.isString(), + !StringEqualsAscii(&value.toString()->asLinear(), "true")); + + args.rval().setBoolean(value.isString() && value.toString()->empty()); + return true; +} + +// get Intl.Locale.prototype.numeric +static bool Locale_numeric(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.numberingSystem +static bool Intl_Locale_numberingSystem(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + return GetUnicodeExtension(cx, locale, "nu", args.rval()); +} + +// get Intl.Locale.prototype.numberingSystem +static bool Locale_numberingSystem(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.language +static bool Locale_language(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto language = BaseNameParts(baseName).language; + + size_t index = language.index; + size_t length = language.length; + + // Step 5. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.language +static bool Locale_language(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.script +static bool Locale_script(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto script = BaseNameParts(baseName).script; + + // Step 5. + if (!script) { + args.rval().setUndefined(); + return true; + } + + size_t index = script->index; + size_t length = script->length; + + // Step 6. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.script +static bool Locale_script(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +// get Intl.Locale.prototype.region +static bool Locale_region(JSContext* cx, const CallArgs& args) { + MOZ_ASSERT(IsLocale(args.thisv())); + + // Step 3. + auto* locale = &args.thisv().toObject().as(); + JSLinearString* baseName = locale->baseName()->ensureLinear(cx); + if (!baseName) { + return false; + } + + // Step 4 (Unnecessary assertion). + + auto region = BaseNameParts(baseName).region; + + // Step 5. + if (!region) { + args.rval().setUndefined(); + return true; + } + + size_t index = region->index; + size_t length = region->length; + + // Step 6. + JSString* str = NewDependentString(cx, baseName, index, length); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +// get Intl.Locale.prototype.region +static bool Locale_region(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod(cx, args); +} + +static bool Locale_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Locale); + return true; +} + +static const JSFunctionSpec locale_methods[] = { + JS_FN("maximize", Locale_maximize, 0, 0), + JS_FN("minimize", Locale_minimize, 0, 0), + JS_FN(js_toString_str, Locale_toString, 0, 0), + JS_FN(js_toSource_str, Locale_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec locale_properties[] = { + JS_PSG("baseName", Locale_baseName, 0), + JS_PSG("calendar", Locale_calendar, 0), + JS_PSG("caseFirst", Locale_caseFirst, 0), + JS_PSG("collation", Locale_collation, 0), + JS_PSG("hourCycle", Locale_hourCycle, 0), + JS_PSG("numeric", Locale_numeric, 0), + JS_PSG("numberingSystem", Locale_numberingSystem, 0), + JS_PSG("language", Locale_language, 0), + JS_PSG("script", Locale_script, 0), + JS_PSG("region", Locale_region, 0), + JS_STRING_SYM_PS(toStringTag, "Intl.Locale", JSPROP_READONLY), + JS_PS_END}; + +JSObject* js::CreateLocalePrototype(JSContext* cx, HandleObject Intl, + Handle global) { + RootedFunction ctor(cx, + GlobalObject::createConstructor(cx, &Locale, cx->names().Locale, 1)); + if (!ctor) { + return nullptr; + } + + RootedObject proto( + cx, GlobalObject::createBlankPrototype(cx, global)); + if (!proto) { + return nullptr; + } + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) { + return nullptr; + } + + if (!DefinePropertiesAndFunctions(cx, proto, locale_properties, locale_methods)) { + return nullptr; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().Locale, ctorValue, nullptr, nullptr, 0)) { + return nullptr; + } + + return proto; +} + +bool js::intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + HandleValue tagValue = args[0]; + bool applyToString = args[1].toBoolean(); + + if (tagValue.isObject()) { + JSString* tagStr; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, tagStr, + LanguageTagFromMaybeWrappedLocale(cx, &tagValue.toObject())); + if (tagStr) { + args.rval().setString(tagStr); + return true; + } + } + + if (!applyToString && !tagValue.isString()) { + args.rval().setNull(); + return true; + } + + JSString* tagStr = ToString(cx, tagValue); + if (!tagStr) { + return false; + } + + RootedLinearString tagLinearStr(cx, tagStr->ensureLinear(cx)); + if (!tagLinearStr) { + return false; + } + + // Handle the common case (a standalone language) first. + // Only the following Unicode BCP 47 locale identifier subset is accepted: + // unicode_locale_id = unicode_language_id + // unicode_language_id = unicode_language_subtag + // unicode_language_subtag = alpha{2,3} + JSString* language; + JS_TRY_VAR_OR_RETURN_FALSE( + cx, language, intl::ParseStandaloneISO639LanguageTag(cx, tagLinearStr)); + if (language) { + args.rval().setString(language); + return true; + } + + LanguageTag tag(cx); + if (!LanguageTagParser::parse(cx, tagLinearStr, tag)) { + return false; + } + + if (!tag.canonicalize(cx)) { + return false; + } + + JSString* resultStr = tag.toString(cx); + if (!resultStr) { + return false; + } + args.rval().setString(resultStr); + return true; +} + +bool js::intl_TryValidateAndCanonicalizeLanguageTag(JSContext* cx, + unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + RootedLinearString linear(cx, args[0].toString()->ensureLinear(cx)); + if (!linear) { + return false; + } + + LanguageTag tag(cx); + bool ok; + JS_TRY_VAR_OR_RETURN_FALSE(cx, ok, + LanguageTagParser::tryParse(cx, linear, tag)); + + // The caller handles invalid inputs. + if (!ok) { + args.rval().setNull(); + return true; + } + + if (!tag.canonicalize(cx)) { + return false; + } + + JSString* resultStr = tag.toString(cx); + if (!resultStr) { + return false; + } + args.rval().setString(resultStr); + return true; +} + +bool js::intl_ValidateAndCanonicalizeUnicodeExtensionType(JSContext* cx, + unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + HandleValue typeArg = args[0]; + MOZ_ASSERT(typeArg.isString(), "type must be a string"); + + HandleValue optionArg = args[1]; + MOZ_ASSERT(optionArg.isString(), "option name must be a string"); + + HandleValue keyArg = args[2]; + MOZ_ASSERT(keyArg.isString(), "key must be a string"); + + RootedLinearString unicodeType(cx, typeArg.toString()->ensureLinear(cx)); + if (!unicodeType) { + return false; + } + + if (!IsValidUnicodeExtensionValue(unicodeType)) { + JSAutoByteString optionStr(cx, optionArg.toString()); + if (!optionStr) { + return false; + } + + JSAutoByteString unicodeTypeQuot(cx, QuoteString(cx, unicodeType, '"')); + if (!unicodeTypeQuot) { + return false; + } + + JS_ReportErrorNumberASCII(cx, js::GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, optionStr.ptr(), + unicodeTypeQuot.ptr()); + return false; + } + + char unicodeKey[UnicodeKeyLength]; + { + JSLinearString* str = keyArg.toString()->ensureLinear(cx); + if (!str) { + return false; + } + MOZ_ASSERT(str->length() == UnicodeKeyLength); + + for (size_t i = 0; i < UnicodeKeyLength; i++) { + char16_t ch = str->latin1OrTwoByteChar(i); + MOZ_ASSERT(mozilla::IsAscii(ch)); + unicodeKey[i] = char(ch); + } + } + + JSAutoByteString unicodeTypeChars(cx, unicodeType); + if (!unicodeTypeChars) { + return false; + } + + size_t unicodeTypeLength = unicodeType->length(); + MOZ_ASSERT(strlen(unicodeTypeChars.ptr()) == unicodeTypeLength); + + // Convert into canonical case before searching for replacements. + intl::AsciiToLowerCase(unicodeTypeChars.ptr(), unicodeTypeLength, + unicodeTypeChars.ptr()); + + auto key = mozilla::MakeSpan(unicodeKey, UnicodeKeyLength); + auto type = mozilla::MakeSpan(unicodeTypeChars.ptr(), unicodeTypeLength); + + // Search if there's a replacement for the current Unicode keyword. + JSString* result; + if (const char* replacement = LanguageTag::replaceUnicodeExtensionType(key, type)) { + result = NewStringCopyZ(cx, replacement); + } else { + result = StringToLowerCase(cx, unicodeType); + } + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} diff --git a/js/src/builtin/intl/Locale.h b/js/src/builtin/intl/Locale.h new file mode 100644 index 0000000000..881906616b --- /dev/null +++ b/js/src/builtin/intl/Locale.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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_Locale_h +#define builtin_intl_Locale_h + +#include + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class GlobalObject; + +class LocaleObject : public NativeObject { + public: + static const Class class_; + + static constexpr uint32_t LANGUAGE_TAG_SLOT = 0; + static constexpr uint32_t BASENAME_SLOT = 1; + static constexpr uint32_t UNICODE_EXTENSION_SLOT = 2; + static constexpr uint32_t SLOT_COUNT = 3; + + /** + * Returns the complete language tag, including any extensions and privateuse + * subtags. + */ + JSString* languageTag() const { + return getFixedSlot(LANGUAGE_TAG_SLOT).toString(); + } + + /** + * Returns the basename subtags, i.e. excluding any extensions and privateuse + * subtags. + */ + JSString* baseName() const { return getFixedSlot(BASENAME_SLOT).toString(); } + + const Value& unicodeExtension() const { + return getFixedSlot(UNICODE_EXTENSION_SLOT); + } +}; + +extern JSObject* CreateLocalePrototype(JSContext* cx, + JS::Handle Intl, + JS::Handle global); + +extern MOZ_MUST_USE bool intl_ValidateAndCanonicalizeLanguageTag(JSContext* cx, + unsigned argc, + Value* vp); + +extern MOZ_MUST_USE bool intl_TryValidateAndCanonicalizeLanguageTag( + JSContext* cx, unsigned argc, Value* vp); + +extern MOZ_MUST_USE bool intl_ValidateAndCanonicalizeUnicodeExtensionType( + JSContext* cx, unsigned argc, Value* vp); + +} // namespace js + +#endif /* builtin_intl_Locale_h */ diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp index 0e6e367ae9..c659733bac 100644 --- a/js/src/builtin/intl/NumberFormat.cpp +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -18,6 +18,7 @@ #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/ICUHeader.h" +#include "builtin/intl/LanguageTag.h" #include "builtin/intl/ScopedICUObject.h" #include "ds/Sort.h" #include "js/RootingAPI.h" @@ -35,7 +36,7 @@ using mozilla::IsFinite; using mozilla::IsNaN; using mozilla::IsNegativeZero; using js::intl::CallICU; -using js::intl::GetAvailableLocales; +using js::intl::DateTimeFormatOptions; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -92,63 +93,34 @@ static const JSFunctionSpec numberFormat_methods[] = { static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) { - RootedObject obj(cx); + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). - // 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)) + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) return false; - args.rval().setObject(*obj); - return true; + if (!proto) { + proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + Rooted numberFormat(cx); + numberFormat = NewObjectWithGivenProto(cx, proto); + if (!numberFormat) + return false; + + numberFormat->setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); + + RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv()); + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + return intl::LegacyIntlInitialize(cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, + locales, options, DateTimeFormatOptions::Standard, args.rval()); } static bool @@ -175,30 +147,23 @@ 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); - } + const Value& slot = obj->as().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); + if (UNumberFormat* nf = static_cast(slot.toPrivate())) + unum_close(nf); } JSObject* -js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + MutableHandleObject constructor) { 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_)); + RootedObject proto(cx, GlobalObject::createBlankPrototype(cx, global)); if (!proto) return nullptr; - proto->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr)); if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; @@ -229,38 +194,15 @@ js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().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; + constructor.set(ctor); 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) { @@ -295,7 +237,7 @@ js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) * of the given NumberFormat. */ static UNumberFormat* -NewUNumberFormat(JSContext* cx, HandleObject numberFormat) +NewUNumberFormat(JSContext* cx, Handle numberFormat) { RootedValue value(cx); @@ -305,7 +247,41 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) return nullptr; - JSAutoByteString locale(cx, value.toString()); + + // ICU expects numberingSystem as a Unicode locale extensions on locale. + + intl::LanguageTag tag(cx); + { + JSLinearString* locale = value.toString()->ensureLinear(cx); + if (!locale) + return nullptr; + + if (!intl::LanguageTagParser::parse(cx, locale, tag)) + return nullptr; + } + + JS::RootedVector keywords(cx); + + if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) + return nullptr; + + { + JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); + if (!numberingSystem) + return nullptr; + + if (!keywords.emplaceBack("nu", numberingSystem)) + return nullptr; + } + + // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of + // the Unicode extension subtag. We're then relying on ICU to follow RFC + // 6067, which states that any trailing keywords using the same key + // should be ignored. + if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) + return nullptr; + + UniqueChars locale = tag.toStringZ(cx); if (!locale) return nullptr; @@ -323,9 +299,6 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) 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()); @@ -398,7 +371,7 @@ 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.get()), nullptr, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return nullptr; @@ -854,50 +827,23 @@ js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) MOZ_ASSERT(args[1].isNumber()); MOZ_ASSERT(args[2].isBoolean()); - RootedObject numberFormat(cx, &args[0].toObject()); + Rooted numberFormat(cx, &args[0].toObject().as()); - // 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. + // Obtain a cached UNumberFormat object. + void* priv = + numberFormat->getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + UNumberFormat* nf = static_cast(priv); + if (!nf) { nf = NewUNumberFormat(cx, numberFormat); if (!nf) return false; + numberFormat->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf)); } // 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); + return intl_FormatNumberToParts(cx, nf, args[1].toNumber(), args.rval()); } - - if (!isNumberFormatInstance) - unum_close(nf); - if (!success) - return false; - args.rval().set(result); - return true; + return intl_FormatNumber(cx, nf, args[1].toNumber(), args.rval()); } diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h index e3005b56b3..a6a25b07ec 100644 --- a/js/src/builtin/intl/NumberFormat.h +++ b/js/src/builtin/intl/NumberFormat.h @@ -37,7 +37,8 @@ class NumberFormatObject : public NativeObject }; extern JSObject* -CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global); +CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle global, + MutableHandleObject constructor); /** * Returns a new instance of the standard built-in NumberFormat constructor. @@ -49,17 +50,6 @@ CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle 1 ? arguments[1] : undefined; - var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties); + // Step 1. + var availableLocales = "NumberFormat"; + + // Step 2. var requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. return SupportedLocales(availableLocales, requestedLocales, options); } @@ -397,8 +415,8 @@ function getNumberingSystems(locale) { // 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 + // those decimal numbering systems explicitly listed in table 3 of + // the ECMAScript Internationalization API Specification, 11.1.6, 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. @@ -416,9 +434,12 @@ function getNumberingSystems(locale) { } -function numberFormatLocaleData(locale) { +function numberFormatLocaleData() { return { - nu: getNumberingSystems(locale) + nu: getNumberingSystems, + default: { + nu: intl_numberingSystem, + } }; } @@ -426,7 +447,7 @@ function numberFormatLocaleData(locale) { /** * Function to be bound and returned by Intl.NumberFormat.prototype.format. * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. + * Spec: ECMAScript Internationalization API Specification, 11.1.4. */ function numberFormatFormatToBind(value) { // Steps 1.a.i implemented by ECMAScript declaration binding instantiation, @@ -443,31 +464,37 @@ function numberFormatFormatToBind(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. + * Spec: ECMAScript Internationalization API Specification, 11.4.3. */ function Intl_NumberFormat_format_get() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "format"); + // Steps 1-3. + var nf = UnwrapNumberFormat(this, "format"); - // Step 1. + var internals = getNumberFormatInternals(nf); + + // Step 4. if (internals.boundFormat === undefined) { - // Step 1.a. - var F = numberFormatFormatToBind; + // Steps 4.a-b. + var F = callFunction(FunctionBind, numberFormatFormatToBind, nf); - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; + // Step 4.c. + internals.boundFormat = F; } - // Step 2. + + // Step 5. return internals.boundFormat; } +_SetCanonicalName(Intl_NumberFormat_format_get, "get format"); +/** + * 11.4.4 Intl.NumberFormat.prototype.formatToParts ( value ) + */ function Intl_NumberFormat_formatToParts(value) { - // Step 1. - var nf = this; + // Steps 1-3. + var nf = UnwrapNumberFormat(this, "formatToParts"); - // Steps 2-3. - getNumberFormatInternals(nf, "formatToParts"); + // Ensure the NumberFormat internals are resolved. + getNumberFormatInternals(nf); // Step 4. var x = ToNumber(value); @@ -479,12 +506,15 @@ function Intl_NumberFormat_formatToParts(value) { /** * Returns the resolved options for a NumberFormat object. * - * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. + * Spec: ECMAScript Internationalization API Specification, 11.4.5. */ function Intl_NumberFormat_resolvedOptions() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "resolvedOptions"); + // Steps 1-3. + var nf = UnwrapNumberFormat(this, "resolvedOptions"); + var internals = getNumberFormatInternals(nf); + + // Steps 4-5. var result = { locale: internals.locale, numberingSystem: internals.numberingSystem, @@ -494,17 +524,31 @@ function Intl_NumberFormat_resolvedOptions() { 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]); + + // currency and currencyDisplay are only present for currency formatters. + assert(hasOwn("currency", internals) === (internals.style === "currency"), + "currency is present iff style is 'currency'"); + assert(hasOwn("currencyDisplay", internals) === (internals.style === "currency"), + "currencyDisplay is present iff style is 'currency'"); + + if (hasOwn("currency", internals)) { + _DefineDataProperty(result, "currency", internals.currency); + _DefineDataProperty(result, "currencyDisplay", internals.currencyDisplay); } + + // Min/Max significant digits are either both present or not at all. + assert(hasOwn("minimumSignificantDigits", internals) === + hasOwn("maximumSignificantDigits", internals), + "minimumSignificantDigits is present iff maximumSignificantDigits is present"); + + if (hasOwn("minimumSignificantDigits", internals)) { + _DefineDataProperty(result, "minimumSignificantDigits", + internals.minimumSignificantDigits); + _DefineDataProperty(result, "maximumSignificantDigits", + internals.maximumSignificantDigits); + } + + // Step 6. return result; } diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp index c166a4b4de..e6ad27577b 100644 --- a/js/src/builtin/intl/PluralRules.cpp +++ b/js/src/builtin/intl/PluralRules.cpp @@ -28,7 +28,6 @@ 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; @@ -79,7 +78,7 @@ static const JSFunctionSpec pluralRules_methods[] = { /** * PluralRules constructor. - * Spec: ECMAScript 402 API, PluralRules, 1.1 + * Spec: ECMAScript 402 API, PluralRules, 13.2.1 */ static bool PluralRules(JSContext* cx, const CallArgs& args, bool construct) @@ -113,8 +112,8 @@ PluralRules(JSContext* cx, const CallArgs& args, bool construct) if (!obj) return false; - obj->as().setReservedSlot(PluralRulesObject::INTERNALS_SLOT, NullValue()); - obj->as().setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + obj->as().setReservedSlot(PluralRulesObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); } RootedValue locales(cx, args.get(0)); @@ -147,15 +146,9 @@ 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); - } + if (UPluralRules* pr = static_cast(slot.toPrivate())) + uplrules_close(pr); } JSObject* @@ -166,10 +159,9 @@ js::CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle(cx, global)); if (!proto) return nullptr; - proto->setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; @@ -180,16 +172,6 @@ js::CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializePluralRules, UndefinedHandleValue, - options)) - { - return nullptr; - } - RootedValue ctorValue(cx, ObjectValue(*ctor)); if (!DefineProperty(cx, Intl, cx->names().PluralRules, ctorValue, nullptr, nullptr, 0)) return nullptr; @@ -197,21 +179,6 @@ js::CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle pluralRules) { RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); if (!internals) @@ -299,7 +266,7 @@ js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); - RootedObject pluralRules(cx, &args[0].toObject()); + Rooted pluralRules(cx, &args[0].toObject().as()); UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules); if (!nf) diff --git a/js/src/builtin/intl/PluralRules.h b/js/src/builtin/intl/PluralRules.h index c9204bafd0..2b60a25b81 100644 --- a/js/src/builtin/intl/PluralRules.h +++ b/js/src/builtin/intl/PluralRules.h @@ -49,17 +49,6 @@ CreatePluralRulesPrototype(JSContext* cx, JS::Handle Intl, 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. diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js index 1fac1c9352..4522b5c4a3 100644 --- a/js/src/builtin/intl/PluralRules.js +++ b/js/src/builtin/intl/PluralRules.js @@ -7,22 +7,20 @@ /** * PluralRules internal properties. * - * Spec: ECMAScript 402 API, PluralRules, 1.3.3. + * Spec: ECMAScript 402 API, PluralRules, 13.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); - } + localeData: pluralRulesLocaleData, + relevantExtensionKeys: [], }; + +function pluralRulesLocaleData() { + // PluralRules don't support any extension keys. + return {}; +} + + /** * Compute an internal properties object from |lazyPluralRulesData|. */ @@ -35,20 +33,25 @@ function resolvePluralRulesInternals(lazyPluralRulesData) { var PluralRules = pluralRulesInternalProperties; - // Step 13. - const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules), - lazyPluralRulesData.requestedLocales, - lazyPluralRulesData.opt, - noRelevantExtensionKeys, undefined); + // Compute effective locale. - // Step 14. + // Step 10. + var localeData = PluralRules.localeData; + + // Step 11. + const r = ResolveLocale("PluralRules", + lazyPluralRulesData.requestedLocales, + lazyPluralRulesData.opt, + PluralRules.relevantExtensionKeys, + localeData); + + // Step 12. internalProps.locale = r.locale; + + // Step 8. internalProps.type = lazyPluralRulesData.type; - internalProps.pluralCategories = intl_GetPluralCategories( - internalProps.locale, - internalProps.type); - + // Step 9. internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits; internalProps.minimumFractionDigits = lazyPluralRulesData.minimumFractionDigits; internalProps.maximumFractionDigits = lazyPluralRulesData.maximumFractionDigits; @@ -59,15 +62,20 @@ function resolvePluralRulesInternals(lazyPluralRulesData) { internalProps.maximumSignificantDigits = lazyPluralRulesData.maximumSignificantDigits; } + // Step 13 (lazily computed on first access). + internalProps.pluralCategories = null; + return internalProps; } /** - * Returns an object containing the PluralRules internal properties of |obj|, - * or throws a TypeError if |obj| isn't PluralRules-initialized. + * Returns an object containing the PluralRules internal properties of |obj|. */ -function getPluralRulesInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "PluralRules", methodName); +function getPluralRulesInternals(obj) { + assert(IsObject(obj), "getPluralRulesInternals called with non-object"); + assert(IsPluralRules(obj), "getPluralRulesInternals called with non-PluralRules"); + + var internals = getIntlObjectInternals(obj); assert(internals.type === "PluralRules", "bad type escaped getIntlObjectInternals"); var internalProps = maybeInternalProperties(internals); @@ -88,16 +96,11 @@ function getPluralRulesInternals(obj, methodName) { * This later work occurs in |resolvePluralRulesInternals|; steps not noted * here occur there. * - * Spec: ECMAScript 402 API, PluralRules, 1.1.1. + * Spec: ECMAScript 402 API, PluralRules, 13.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); + assert(IsObject(pluralRules), "InitializePluralRules called with non-object"); + assert(IsPluralRules(pluralRules), "InitializePluralRules called with non-PluralRules"); // Lazy PluralRules data has the following structure: // @@ -124,30 +127,29 @@ function InitializePluralRules(pluralRules, locales, options) { // subset of them. const lazyPluralRulesData = std_Object_create(null); - // Step 3. + // Step 1. let requestedLocales = CanonicalizeLocaleList(locales); lazyPluralRulesData.requestedLocales = requestedLocales; - // Steps 4-5. + // Steps 2-3. if (options === undefined) options = {}; else options = ToObject(options); - // Step 6. - const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal"); - lazyPluralRulesData.type = type; - - // Step 8. + // Step 4. let opt = new Record(); lazyPluralRulesData.opt = opt; - // Steps 9-10. + // Steps 5-6. let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); opt.localeMatcher = matcher; + // Step 7. + const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal"); + lazyPluralRulesData.type = type; - // Step 11. + // Step 9. SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0); // Step 12. @@ -156,7 +158,11 @@ function InitializePluralRules(pluralRules, locales, options) { std_Math_max(lazyPluralRulesData.minimumFractionDigits, 3); } - setLazyData(internals, "PluralRules", lazyPluralRulesData) + // Step 15. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + initializeIntlObject(pluralRules, "PluralRules", lazyPluralRulesData) } /** @@ -164,14 +170,14 @@ function InitializePluralRules(pluralRules, locales, options) { * 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. + * Spec: ECMAScript 402 API, PluralRules, 13.3.2. */ function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales, - pluralRulesInternalProperties); + var availableLocales = "PluralRules"; + // Step 2. let requestedLocales = CanonicalizeLocaleList(locales); @@ -184,15 +190,20 @@ function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { * 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. + * Spec: ECMAScript 402 API, PluralRules, 13.4.3. */ function Intl_PluralRules_select(value) { // Step 1. let pluralRules = this; - // Step 2. - let internals = getPluralRulesInternals(pluralRules, "select"); - // Steps 3-4. + // Steps 2-3. + if (!IsObject(pluralRules) || !IsPluralRules(pluralRules)) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "PluralRules", "select", "PluralRules"); + + // Ensure the PluralRules internals are resolved. + getPluralRulesInternals(pluralRules); + + // Step 4. let n = ToNumber(value); // Step 5. @@ -202,11 +213,34 @@ function Intl_PluralRules_select(value) { /** * Returns the resolved options for a PluralRules object. * - * Spec: ECMAScript 402 API, PluralRules, 1.4.4. + * Spec: ECMAScript 402 API, PluralRules, 13.4.4. */ function Intl_PluralRules_resolvedOptions() { - var internals = getPluralRulesInternals(this, "resolvedOptions"); + // Step 1. + var pluralRules = this; + // Steps 2-3. + if (!IsObject(pluralRules) || !IsPluralRules(pluralRules)) { + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "PluralRules", "resolvedOptions", + "PluralRules"); + } + + var internals = getPluralRulesInternals(pluralRules); + + var internalsPluralCategories = internals.pluralCategories; + if (internalsPluralCategories === null) { + internalsPluralCategories = intl_GetPluralCategories(internals.locale, internals.type); + internals.pluralCategories = internalsPluralCategories; + } + + // TODO: The current spec actually requires to return the internal array + // object and not a copy of it. + // + var pluralCategories = []; + for (var i = 0; i < internalsPluralCategories.length; i++) + _DefineDataProperty(pluralCategories, i, internalsPluralCategories[i]); + + // Steps 4-5. var result = { locale: internals.locale, type: internals.type, @@ -216,16 +250,19 @@ function Intl_PluralRules_resolvedOptions() { maximumFractionDigits: internals.maximumFractionDigits, }; - var optionalProperties = [ - "minimumSignificantDigits", - "maximumSignificantDigits" - ]; + // Min/Max significant digits are either both present or not at all. + assert(hasOwn("minimumSignificantDigits", internals) === + hasOwn("maximumSignificantDigits", internals), + "minimumSignificantDigits is present iff maximumSignificantDigits is present"); - for (var i = 0; i < optionalProperties.length; i++) { - var p = optionalProperties[i]; - if (callFunction(std_Object_hasOwnProperty, internals, p)) - _DefineDataProperty(result, p, internals[p]); + if (hasOwn("minimumSignificantDigits", internals)) { + _DefineDataProperty(result, "minimumSignificantDigits", + internals.minimumSignificantDigits); + _DefineDataProperty(result, "maximumSignificantDigits", + internals.maximumSignificantDigits); } + + // Step 6. return result; } diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp index 22769af155..277baae4c4 100644 --- a/js/src/builtin/intl/RelativeTimeFormat.cpp +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -26,7 +26,6 @@ using mozilla::Range; using mozilla::RangedPtr; using js::intl::CallICU; -using js::intl::GetAvailableLocales; using js::intl::IcuLocale; using js::intl::INITIAL_CHAR_BUFFER_SIZE; using js::intl::StringsAreEqual; @@ -108,8 +107,8 @@ RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) if (!relativeTimeFormat) return false; - relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::INTERNALS_SLOT, NullValue()); - relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + 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)); @@ -127,15 +126,9 @@ 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); - } + if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) + ureldatefmt_close(rtf); } JSObject* @@ -146,10 +139,9 @@ js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle(cx, global)); if (!proto) return nullptr; - proto->setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); if (!LinkConstructorAndPrototype(cx, ctor, proto)) return nullptr; @@ -163,16 +155,6 @@ js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handlenames().InitializeRelativeTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - RootedValue ctorValue(cx, ObjectValue(*ctor)); if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { return nullptr; @@ -181,22 +163,6 @@ js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, 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. diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js index a37f067825..fe021c47e0 100644 --- a/js/src/builtin/intl/RelativeTimeFormat.js +++ b/js/src/builtin/intl/RelativeTimeFormat.js @@ -11,17 +11,6 @@ */ 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: [], }; @@ -41,7 +30,7 @@ function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { var RelativeTimeFormat = relativeTimeFormatInternalProperties; // Steps 7-8. - const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), + const r = ResolveLocale("RelativeTimeFormat", lazyRelativeTimeFormatData.requestedLocales, lazyRelativeTimeFormatData.opt, RelativeTimeFormat.relevantExtensionKeys, @@ -69,8 +58,11 @@ function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { * 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); +function getRelativeTimeFormatInternals(obj) { + assert(IsObject(obj), "getRelativeTimeFormatInternals called with non-object"); + assert(IsRelativeTimeFormat(obj), "getRelativeTimeFormatInternals called with non-RelativeTimeFormat"); + + var internals = getIntlObjectInternals(obj); assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals"); var internalProps = maybeInternalProperties(internals); @@ -94,12 +86,8 @@ function getRelativeTimeFormatInternals(obj, methodName) { * 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); + assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat called with non-object"); + assert(IsRelativeTimeFormat(relativeTimeFormat), "InitializeRelativeTimeFormat called with non-RelativeTimeFormat"); // Lazy RelativeTimeFormat data has the following structure: // @@ -146,7 +134,7 @@ function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { const numeric = GetOption(options, "numeric", "string", ["always", "auto"], "always"); lazyRelativeTimeFormatData.numeric = numeric; - setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData) + initializeIntlObject(relativeTimeFormat, "RelativeTimeFormat", lazyRelativeTimeFormatData) } /** @@ -160,8 +148,8 @@ function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { var options = arguments.length > 1 ? arguments[1] : undefined; // Step 1. - var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, - relativeTimeFormatInternalProperties); + var availableLocales = "RelativeTimeFormat"; + // Step 2. let requestedLocales = CanonicalizeLocaleList(locales); @@ -181,7 +169,11 @@ function Intl_RelativeTimeFormat_format(value, unit) { let relativeTimeFormat = this; // Step 2. - let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format"); + if (!IsObject(relativeTimeFormat) || !IsRelativeTimeFormat(relativeTimeFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "RelativeTimeFormat", "format", "RelativeTimeFormat"); + + // Ensure the RelativeTimeFormat internals are resolved. + let internals = getRelativeTimeFormatInternals(relativeTimeFormat); // Step 3. let t = ToNumber(value); @@ -191,7 +183,7 @@ function Intl_RelativeTimeFormat_format(value, unit) { // PartitionRelativeTimePattern, step 4. if (!Number_isFinite(t)) { - ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat"); + ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat", "format"); } // PartitionRelativeTimePattern, step 5. @@ -227,7 +219,13 @@ function Intl_RelativeTimeFormat_format(value, unit) { * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4. */ function Intl_RelativeTimeFormat_resolvedOptions() { - var internals = getRelativeTimeFormatInternals(this, "resolvedOptions"); + // Check "this RelativeTimeFormat object" per introduction of section 1.4. + if (!IsObject(this) || !IsRelativeTimeFormat(this)) { + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, "RelativeTimeFormat", "resolvedOptions", + "RelativeTimeFormat"); + } + + var internals = getRelativeTimeFormatInternals(this); // Steps 4-5. var result = { diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp index 12fe062346..45fa3621a0 100644 --- a/js/src/builtin/intl/SharedIntlData.cpp +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -10,6 +10,7 @@ #include "mozilla/Assertions.h" #include "mozilla/HashFunctions.h" +#include #include #include "jsatom.h" @@ -21,6 +22,7 @@ #include "builtin/intl/ScopedICUObject.h" #include "builtin/intl/TimeZoneDataGenerated.h" #include "js/Utility.h" +#include "js/Vector.h" using js::HashNumber; using js::intl::StringsAreEqual; @@ -29,9 +31,7 @@ template static constexpr Char ToUpperASCII(Char c) { - return ('a' <= c && c <= 'z') - ? (c & ~0x20) - : c; + return mozilla::IsAsciiLowercaseAlpha(c) ? (c - 0x20) : c; } static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); @@ -63,15 +63,12 @@ EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) } js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) - : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) + : js::intl::SharedIntlData::LinearStringLookup(timeZone) { - if (isLatin1) { - latin1Chars = timeZone->latin1Chars(nogc); + if (isLatin1) hash = HashStringIgnoreCaseASCII(latin1Chars, length); - } else { - twoByteChars = timeZone->twoByteChars(nogc); + else hash = HashStringIgnoreCaseASCII(twoByteChars, length); - } } bool @@ -110,7 +107,7 @@ js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) if (timeZoneDataInitialized) return true; - // If initTimeZones() was called previously, but didn't complete due to + // If ensureTimeZones() was called previously, but didn't complete due to // OOM, clear all sets/maps and start from scratch. if (availableTimeZones.initialized()) availableTimeZones.finish(); @@ -272,12 +269,307 @@ js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* c return true; } +js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(JSLinearString* locale) + : js::intl::SharedIntlData::LinearStringLookup(locale) +{ + if (isLatin1) + hash = mozilla::HashString(latin1Chars, length); + else + hash = mozilla::HashString(twoByteChars, length); +} + +js::intl::SharedIntlData::LocaleHasher::Lookup::Lookup(const char* chars, + size_t length) + : js::intl::SharedIntlData::LinearStringLookup(chars, length) +{ + hash = mozilla::HashString(latin1Chars, length); +} + +bool +js::intl::SharedIntlData::LocaleHasher::match(Locale key, const Lookup& lookup) +{ + if (key->length() != lookup.length) + return false; + + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) + return EqualChars(keyChars, lookup.latin1Chars, lookup.length); + return EqualChars(keyChars, lookup.twoByteChars, lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) + return EqualChars(lookup.latin1Chars, keyChars, lookup.length); + return EqualChars(keyChars, lookup.twoByteChars, lookup.length); +} + +bool +js::intl::SharedIntlData::getAvailableLocales(JSContext* cx, LocaleSet& locales, + CountAvailable countAvailable, + GetAvailable getAvailable) +{ + auto addLocale = [cx, &locales](const char* locale, size_t length) { + JSAtom* atom = Atomize(cx, locale, length); + if (!atom) + return false; + + LocaleHasher::Lookup lookup(atom); + LocaleSet::AddPtr p = locales.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate locales, but if it does, just + // ignore the duplicated locale. + if (!p && !locales.add(p, atom)) { + ReportOutOfMemory(cx); + return false; + } + + return true; + }; + + js::Vector lang(cx); + + int32_t count = countAvailable(); + for (int32_t i = 0; i < count; i++) { + const char* locale = getAvailable(i); + size_t length = strlen(locale); + + lang.clear(); + if (!lang.append(locale, length)) + return false; + + std::replace(lang.begin(), lang.end(), '_', '-'); + + if (!addLocale(lang.begin(), length)) + return false; + } + + // 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 + // locale, in case ICU doesn't directly support it (but does support it + // through fallback, e.g. supporting "en-GB" indirectly using "en" support). + + // Certain old-style language tags lack a script code, but in current usage + // they *would* include a script code. Map these over to modern forms. + for (const auto& mapping : js::intl::oldStyleLanguageTagMappings) { + const char* oldStyle = mapping.oldStyle; + const char* modernStyle = mapping.modernStyle; + + LocaleHasher::Lookup lookup(modernStyle, strlen(modernStyle)); + if (locales.has(lookup)) { + if (!addLocale(oldStyle, strlen(oldStyle))) + return false; + } + } + + // Also forcibly provide the last-ditch locale. + { + const char* lastDitch = intl::LastDitchLocale(); + MOZ_ASSERT(strcmp(lastDitch, "en-GB") == 0); + +#ifdef DEBUG + static constexpr char lastDitchParent[] = "en"; + + LocaleHasher::Lookup lookup(lastDitchParent, strlen(lastDitchParent)); + MOZ_ASSERT(locales.has(lookup), + "shouldn't be a need to add every locale implied by the " + "last-ditch locale, merely just the last-ditch locale"); +#endif + + if (!addLocale(lastDitch, strlen(lastDitch))) + return false; + } + + return true; +} + +#ifdef DEBUG +template +static bool +IsSameAvailableLocales(CountAvailable countAvailable1, + GetAvailable getAvailable1, + CountAvailable countAvailable2, + GetAvailable getAvailable2) +{ + int32_t count = countAvailable1(); + if (count != countAvailable2()) { + return false; + } + for (int32_t i = 0; i < count; i++) { + if (getAvailable1(i) != getAvailable2(i)) { + return false; + } + } + return true; +} +#endif + +bool +js::intl::SharedIntlData::ensureSupportedLocales(JSContext* cx) +{ + if (supportedLocalesInitialized) + return true; + + // If ensureSupportedLocales() was called previously, but didn't complete due + // to OOM, clear all data and start from scratch. + if (supportedLocales.initialized()) + supportedLocales.finish(); + if (collatorSupportedLocales.initialized()) + collatorSupportedLocales.finish(); + if (!supportedLocales.init() || + !collatorSupportedLocales.init()) { + ReportOutOfMemory(cx); + return false; + } + + if (!getAvailableLocales(cx, supportedLocales, uloc_countAvailable, uloc_getAvailable)) + return false; + + if (!getAvailableLocales(cx, collatorSupportedLocales, ucol_countAvailable, ucol_getAvailable)) + return false; + + MOZ_ASSERT(IsSameAvailableLocales(uloc_countAvailable, uloc_getAvailable, + udat_countAvailable, udat_getAvailable)); + + MOZ_ASSERT(IsSameAvailableLocales(uloc_countAvailable, uloc_getAvailable, + unum_countAvailable, unum_getAvailable)); + + MOZ_ASSERT(!supportedLocalesInitialized, "ensureSupportedLocales is neither reentrant nor thread-safe"); + supportedLocalesInitialized = true; + + return true; +} + +bool +js::intl::SharedIntlData::isSupportedLocale(JSContext* cx, + SupportedLocaleKind kind, + HandleString locale, + bool* supported) +{ + if (!ensureSupportedLocales(cx)) + return false; + + + RootedLinearString localeLinear(cx, locale->ensureLinear(cx)); + if (!localeLinear) + return false; + + LocaleHasher::Lookup lookup(localeLinear); + switch (kind) { + case SupportedLocaleKind::Collator: + *supported = collatorSupportedLocales.has(lookup); + return true; + case SupportedLocaleKind::DateTimeFormat: + case SupportedLocaleKind::NumberFormat: + case SupportedLocaleKind::PluralRules: + case SupportedLocaleKind::RelativeTimeFormat: + *supported = supportedLocales.has(lookup); + return true; + } + MOZ_CRASH("Invalid Intl constructor"); + + return true; +} + +bool +js::intl::SharedIntlData::ensureUpperCaseFirstLocales(JSContext* cx) +{ + if (upperCaseFirstInitialized) + return true; + + // If ensureUpperCaseFirstLocales() was called previously, but didn't + // complete due to OOM, clear all data and start from scratch. + if (upperCaseFirstLocales.initialized()) + upperCaseFirstLocales.finish(); + if (!upperCaseFirstLocales.init()) { + ReportOutOfMemory(cx); + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UEnumeration* available = ucol_openAvailableLocales(&status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject toClose(available); + + RootedAtom locale(cx); + while (true) { + int32_t size; + const char* rawLocale = uenum_next(available, &size, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + if (rawLocale == nullptr) + break; + + UCollator* collator = ucol_open(rawLocale, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + ScopedICUObject toCloseCollator(collator); + + UColAttributeValue caseFirst = ucol_getAttribute(collator, UCOL_CASE_FIRST, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + if (caseFirst != UCOL_UPPER_FIRST) + continue; + + MOZ_ASSERT(size >= 0); + locale = Atomize(cx, rawLocale, size_t(size)); + if (!locale) + return false; + + LocaleHasher::Lookup lookup(locale); + LocaleSet::AddPtr p = upperCaseFirstLocales.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate locales, but if it does, just + // ignore the duplicated locale. + if (!p && !upperCaseFirstLocales.add(p, locale)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT(!upperCaseFirstInitialized, + "ensureUpperCaseFirstLocales is neither reentrant nor thread-safe"); + upperCaseFirstInitialized = true; + + return true; +} + +bool +js::intl::SharedIntlData::isUpperCaseFirst(JSContext* cx, HandleString locale, bool* isUpperFirst) +{ + if (!ensureUpperCaseFirstLocales(cx)) + return false; + + RootedLinearString localeLinear(cx, locale->ensureLinear(cx)); + if (!localeLinear) + return false; + + LocaleHasher::Lookup lookup(localeLinear); + *isUpperFirst = upperCaseFirstLocales.has(lookup); + + return true; +} + void js::intl::SharedIntlData::destroyInstance() { availableTimeZones.finish(); ianaZonesTreatedAsLinksByICU.finish(); ianaLinksCanonicalizedDifferentlyByICU.finish(); + supportedLocales.finish(); + collatorSupportedLocales.finish(); + upperCaseFirstLocales.finish(); } void @@ -288,6 +580,9 @@ js::intl::SharedIntlData::trace(JSTracer* trc) availableTimeZones.trace(trc); ianaZonesTreatedAsLinksByICU.trace(trc); ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + supportedLocales.trace(trc); + collatorSupportedLocales.trace(trc); + upperCaseFirstLocales.trace(trc); } } @@ -296,5 +591,8 @@ js::intl::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf { return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + - ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); + ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf) + + supportedLocales.sizeOfExcludingThis(mallocSizeOf) + + collatorSupportedLocales.sizeOfExcludingThis(mallocSizeOf) + + upperCaseFirstLocales.sizeOfExcludingThis(mallocSizeOf); } diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h index 1414ba5a5c..81834804a1 100644 --- a/js/src/builtin/intl/SharedIntlData.h +++ b/js/src/builtin/intl/SharedIntlData.h @@ -30,6 +30,34 @@ namespace intl { */ class SharedIntlData { + struct LinearStringLookup + { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + }; + bool isLatin1; + size_t length; + JS::AutoCheckCannotGC nogc; + HashNumber hash = 0; + + explicit LinearStringLookup(JSLinearString* string) + : isLatin1(string->hasLatin1Chars()), length(string->length()) + { + if (isLatin1) + latin1Chars = string->latin1Chars(nogc); + else + twoByteChars = string->twoByteChars(nogc); + } + + LinearStringLookup(const char* chars, size_t length) + : isLatin1(true), length(length) + { + latin1Chars = reinterpret_cast(chars); + } + }; + + private: /** * Information tracking the set of the supported time zone names, derived * from the IANA time zone database . @@ -59,17 +87,8 @@ class SharedIntlData struct TimeZoneHasher { - struct Lookup + struct Lookup : LinearStringLookup { - union { - const JS::Latin1Char* latin1Chars; - const char16_t* twoByteChars; - }; - bool isLatin1; - size_t length; - JS::AutoCheckCannotGC nogc; - HashNumber hash; - explicit Lookup(JSFlatString* timeZone); }; @@ -148,7 +167,110 @@ class SharedIntlData */ bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, JS::MutableHandleString result); + private: + using Locale = JSAtom*; + struct LocaleHasher + { + struct Lookup : LinearStringLookup + { + explicit Lookup(JSLinearString* locale); + Lookup(const char* chars, size_t length); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(Locale key, const Lookup& lookup); + }; + + using LocaleSet = GCHashSet; + + // Set of supported locales for all Intl service constructors except Collator, + // which uses its own set. + // + // UDateFormat: + // udat_[count,get]Available() return the same results as their + // uloc_[count,get]Available() counterparts. + // + // UNumberFormatter: + // unum_[count,get]Available() return the same results as their + // uloc_[count,get]Available() counterparts. + // + // UPluralRules and URelativeDateTimeFormatter: + // We're going to use ULocale availableLocales as per ICU recommendation: + // https://unicode-org.atlassian.net/browse/ICU-12756 + LocaleSet supportedLocales; + + // ucol_[count,get]Available() return different results compared to + // uloc_[count,get]Available(), we can't use |supportedLocales| here. + LocaleSet collatorSupportedLocales; + + bool supportedLocalesInitialized = false; + + // 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); + + static bool getAvailableLocales(JSContext* cx, LocaleSet& locales, + CountAvailable countAvailable, + GetAvailable getAvailable); + + /** + * Precomputes the available locales sets. + */ + bool ensureSupportedLocales(JSContext* cx); + + public: + enum class SupportedLocaleKind { + Collator, + DateTimeFormat, + NumberFormat, + PluralRules, + RelativeTimeFormat + }; + + /** + * Sets |supported| to true if |locale| is supported by the requested Intl + * service constructor. Otherwise sets |supported| to false. + */ + MOZ_MUST_USE bool isSupportedLocale(JSContext* cx, SupportedLocaleKind kind, + JS::Handle locale, + bool* supported); + + private: + /** + * The case first parameter (BCP47 key "kf") allows to switch the order of + * upper- and lower-case characters. ICU doesn't directly provide an API + * to query the default case first value of a given locale, but instead + * requires to instantiate a collator object and then query the case first + * attribute (UCOL_CASE_FIRST). + * To avoid instantiating an additional collator object whenever we need + * to retrieve the default case first value of a specific locale, we + * compute the default case first value for every supported locale only + * once and then keep a list of all locales which don't use the default + * case first setting. + * There is almost no difference between lower-case first and when case + * first is disabled (UCOL_LOWER_FIRST resp. UCOL_OFF), so we only need to + * track locales which use upper-case first as their default setting. + */ + + LocaleSet upperCaseFirstLocales; + + bool upperCaseFirstInitialized = false; + + /** + * Precomputes the available locales which use upper-case first sorting. + */ + bool ensureUpperCaseFirstLocales(JSContext* cx); + + public: + /** + * Sets |isUpperFirst| to true if |locale| sorts upper-case characters + * before lower-case characters. + */ + bool isUpperCaseFirst(JSContext* cx, JS::HandleString locale, bool* isUpperFirst); + + public: void destroyInstance(); void trace(JSTracer* trc); diff --git a/js/src/builtin/intl/make_intl_data.py b/js/src/builtin/intl/make_intl_data.py index a81001e0f3..59ff14d76c 100644 --- a/js/src/builtin/intl/make_intl_data.py +++ b/js/src/builtin/intl/make_intl_data.py @@ -6,19 +6,15 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Usage: - make_intl_data.py langtags [language-subtag-registry.txt] + make_intl_data.py langtags [cldr_core.zip] make_intl_data.py tzdata + make_intl_data.py unicode-ext 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 - 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 - and uses the syntax specified in - https://tools.ietf.org/html/rfc5646#section-3 + This script extracts information about 1) mappings between deprecated and + current Unicode BCP 47 locale identifiers, and 2) deprecated and current + BCP 47 Unicode extension value from CLDR, and converts it to C++ mapping + code in LanguageTagGenerated.cpp. The code is used in LanguageTag.cpp. Target "tzdata": @@ -36,194 +32,1330 @@ import sys import tarfile import tempfile import urllib2 -import urlparse from contextlib import closing from functools import partial -from itertools import chain, ifilter, ifilterfalse, imap, tee +from itertools import chain, ifilter, ifilterfalse, imap, izip_longest, groupby, tee from operator import attrgetter, itemgetter +from urlparse import urlsplit +from zipfile import ZipFile -def readRegistryRecord(registry): - """ Yields the records of the IANA Language Subtag Registry as dictionaries. """ - record = {} - for line in registry: - line = line.strip() - if line == "": - continue - if line == "%%": - yield record - record = {} +# From https://docs.python.org/3/library/itertools.html +def grouper(iterable, n, fillvalue=None): + "Collect data into fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return izip_longest(*args, fillvalue=fillvalue) + +def writeMappingHeader(println, description, source, url): + if type(description) is not list: + description = [description] + for desc in description: + println(u"// {0}".format(desc)) + println(u"// Derived from {0}.".format(source)) + println(u"// {0}".format(url)) + +def writeMappingsVar(println, mapping, name, description, source, url): + """ Writes a variable definition with a mapping table. + + Writes the contents of dictionary |mapping| through the |println| + function with the given variable name and a comment with description, + source, and URL. + """ + println(u"") + writeMappingHeader(println, description, source, url) + println(u"var {0} = {{".format(name)) + for (key, value) in sorted(mapping.items(), key=itemgetter(0)): + println(u' "{0}": "{1}",'.format(key, value)) + println(u"};") + +def writeMappingsBinarySearch(println, fn_name, type_name, name, validate_fn, validate_case_fn, + mappings, tag_maxlength, description, source, url): + """ Emit code to perform a binary search on language tag subtags. + + Uses the contents of |mapping|, which can either be a dictionary or set, + to emit a mapping function to find subtag replacements. + """ + println(u"") + writeMappingHeader(println, description, source, url) + println(u""" +bool js::intl::LanguageTag::{0}({1} {2}) {{ + MOZ_ASSERT({3}({2}.span())); + MOZ_ASSERT({4}({2}.span())); +""".format(fn_name, type_name, name, validate_fn, validate_case_fn).strip()) + + def write_array(subtags, name, length, fixed): + if fixed: + println(u" static const char {}[{}][{}] = {{".format(name, len(subtags), + length + 1)) else: - if ":" in line: - key, value = line.split(":", 1) - key, value = key.strip(), value.strip() - record[key] = value + println(u" static const char* {}[{}] = {{".format(name, len(subtags))) + + # Group in pairs of ten to not exceed the 80 line column limit. + for entries in grouper(subtags, 10): + entries = (u"\"{}\"".format(tag).rjust(length + 2) + for tag in entries if tag is not None) + println(u" {},".format(u", ".join(entries))) + + println(u" };") + + trailing_return = True + + # Sort the subtags by length. That enables using an optimized comparator + # for the binary search, which only performs a single |memcmp| for multiple + # of two subtag lengths. + mappings_keys = mappings.keys() if type(mappings) == dict else mappings + for (length, subtags) in groupby(sorted(mappings_keys, key=len), len): + # Omit the length check if the current length is the maximum length. + if length != tag_maxlength: + println(u""" + if ({}.length() == {}) {{ +""".format(name, length).rstrip("\n")) + else: + trailing_return = False + println(u""" + { +""".rstrip("\n")) + + # The subtags need to be sorted for binary search to work. + subtags = sorted(subtags) + + def equals(subtag): + return u"""{}.equalTo("{}")""".format(name, subtag) + + # Don't emit a binary search for short lists. + if len(subtags) == 1: + if type(mappings) == dict: + println(u""" + if ({}) {{ + {}.set("{}"); + return true; + }} + return false; +""".format(equals(subtags[0]), name, mappings[subtags[0]]).strip("\n")) else: - # continuation line - record[key] += " " + line - if record: - yield record - return + println(u""" + return {}; +""".format(equals(subtags[0])).strip("\n")) + elif len(subtags) <= 4: + if type(mappings) == dict: + for subtag in subtags: + println(u""" + if ({}) {{ + {}.set("{}"); + return true; + }} +""".format(equals(subtag), name, mappings[subtag]).strip("\n")) + + println(u""" + return false; +""".strip("\n")) + else: + cond = (equals(subtag) for subtag in subtags) + cond = (u" ||\n" + u" " * (4 + len("return "))).join(cond) + println(u""" + return {}; +""".format(cond).strip("\n")) + else: + write_array(subtags, name + "s", length, True) + + if type(mappings) == dict: + write_array([mappings[k] for k in subtags], u"aliases", length, False) + + println(u""" + if (const char* replacement = SearchReplacement({0}s, aliases, {0})) {{ + {0}.set(mozilla::MakeCStringSpan(replacement)); + return true; + }} + return false; +""".format(name).rstrip()) + else: + println(u""" + return HasReplacement({0}s, {0}); +""".format(name).rstrip()) + + println(u""" + } +""".strip("\n")) + + if trailing_return: + println(u""" + return false;""") + + println(u""" +}""".lstrip("\n")) -def readRegistry(registry): - """ Reads IANA Language Subtag Registry and extracts information for Intl.js. +def writeComplexLanguageTagMappings(println, complex_language_mappings, + description, source, url): + println(u"") + writeMappingHeader(println, description, source, url) + println(u""" +void js::intl::LanguageTag::performComplexLanguageMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); +""".lstrip()) + + # Merge duplicate language entries. + language_aliases = {} + for (deprecated_language, (language, script, region)) in ( + sorted(complex_language_mappings.items(), key=itemgetter(0)) + ): + key = (language, script, region) + if key not in language_aliases: + language_aliases[key] = [] + else: + language_aliases[key].append(deprecated_language) + + first_language = True + for (deprecated_language, (language, script, region)) in ( + sorted(complex_language_mappings.items(), key=itemgetter(0)) + ): + key = (language, script, region) + if deprecated_language in language_aliases[key]: + continue + + if_kind = u"if" if first_language else u"else if" + first_language = False + + cond = (u"language().equalTo(\"{}\")".format(lang) + for lang in [deprecated_language] + language_aliases[key]) + cond = (u" ||\n" + u" " * (2 + len(if_kind) + 2)).join(cond) + + println(u""" + {} ({}) {{""".format(if_kind, cond).strip("\n")) + + println(u""" + setLanguage("{}");""".format(language).strip("\n")) + + if script is not None: + println(u""" + if (script().missing()) {{ + setScript("{}"); + }}""".format(script).strip("\n")) + if region is not None: + println(u""" + if (region().missing()) {{ + setRegion("{}"); + }}""".format(region).strip("\n")) + println(u""" + }""".strip("\n")) + + println(u""" +} +""".strip("\n")) + + +def writeComplexRegionTagMappings(println, complex_region_mappings, + description, source, url): + println(u"") + writeMappingHeader(println, description, source, url) + println(u""" +void js::intl::LanguageTag::performComplexRegionMappings() { + MOZ_ASSERT(IsStructurallyValidLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); + MOZ_ASSERT(IsStructurallyValidRegionTag(region().span())); + MOZ_ASSERT(IsCanonicallyCasedRegionTag(region().span())); +""".lstrip()) + + # |non_default_replacements| is a list and hence not hashable. Convert it + # to a string to get a proper hashable value. + def hash_key(default, non_default_replacements): + return (default, str(sorted(str(v) for v in non_default_replacements))) + + # Merge duplicate region entries. + region_aliases = {} + for (deprecated_region, (default, non_default_replacements)) in ( + sorted(complex_region_mappings.items(), key=itemgetter(0)) + ): + key = hash_key(default, non_default_replacements) + if key not in region_aliases: + region_aliases[key] = [] + else: + region_aliases[key].append(deprecated_region) + + first_region = True + for (deprecated_region, (default, non_default_replacements)) in ( + sorted(complex_region_mappings.items(), key=itemgetter(0)) + ): + key = hash_key(default, non_default_replacements) + if deprecated_region in region_aliases[key]: + continue + + if_kind = u"if" if first_region else u"else if" + first_region = False + + cond = (u"region().equalTo(\"{}\")".format(region) + for region in [deprecated_region] + region_aliases[key]) + cond = (u" ||\n" + u" " * (2 + len(if_kind) + 2)).join(cond) + + println(u""" + {} ({}) {{""".format(if_kind, cond).strip("\n")) + + replacement_regions = sorted({region for (_, _, region) in non_default_replacements}) + + first_case = True + for replacement_region in replacement_regions: + replacement_language_script = sorted(((language, script) + for (language, script, region) in ( + non_default_replacements + ) + if region == replacement_region), + key=itemgetter(0)) + + if_kind = u"if" if first_case else u"else if" + first_case = False + + def compare_tags(language, script): + if script is None: + return u"language().equalTo(\"{}\")".format(language) + return u"(language().equalTo(\"{}\") && script().equalTo(\"{}\"))".format( + language, script) + + cond = (compare_tags(language, script) + for (language, script) in replacement_language_script) + cond = (u" ||\n" + u" " * (4 + len(if_kind) + 2)).join(cond) + + println(u""" + {} ({}) {{ + setRegion("{}"); + }}""".format(if_kind, cond, replacement_region).rstrip().strip("\n")) + + println(u""" + else {{ + setRegion("{}"); + }} + }}""".format(default).rstrip().strip("\n")) + + println(u""" +} +""".strip("\n")) + + +def writeVariantTagMappings(println, variant_mappings, description, source, + url): + """ Writes a function definition that maps variant subtags. """ + println(u""" +static const char* ToCharPointer(const char* str) { + return str; +} + +static const char* ToCharPointer(const js::UniqueChars& str) { + return str.get(); +} + +template +static bool IsLessThan(const T& a, const U& b) { + return strcmp(ToCharPointer(a), ToCharPointer(b)) < 0; +} +""") + writeMappingHeader(println, description, source, url) + println(u""" +bool js::intl::LanguageTag::performVariantMappings(JSContext* cx) { + // The variant subtags need to be sorted for binary search. + MOZ_ASSERT(std::is_sorted(variants_.begin(), variants_.end(), + IsLessThan)); + + auto insertVariantSortedIfNotPresent = [&](const char* variant) { + auto* p = std::lower_bound(variants_.begin(), variants_.end(), variant, + IsLessThan); + + // Don't insert the replacement when already present. + if (p != variants_.end() && strcmp(p->get(), variant) == 0) { + return true; + } + + // Insert the preferred variant in sort order. + auto preferred = DuplicateString(cx, variant); + if (!preferred) { + return false; + } + return !!variants_.insert(p, std::move(preferred)); + }; + + for (size_t i = 0; i < variants_.length(); ) { + auto& variant = variants_[i]; + MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeCStringSpan(variant.get()))); +""".lstrip()) + + first_variant = True + + for (deprecated_variant, (type, replacement)) in ( + sorted(variant_mappings.items(), key=itemgetter(0)) + ): + if_kind = u"if" if first_variant else u"else if" + first_variant = False + + println(u""" + {} (strcmp(variant.get(), "{}") == 0) {{ + variants_.erase(variants_.begin() + i); +""".format(if_kind, deprecated_variant).strip("\n")) + + if type == "language": + println(u""" + setLanguage("{}"); +""".format(replacement).strip("\n")) + elif type == "region": + println(u""" + setRegion("{}"); +""".format(replacement).strip("\n")) + else: + assert type == "variant" + println(u""" + if (!insertVariantSortedIfNotPresent("{}")) {{ + return false; + }} +""".format(replacement).strip("\n")) + + println(u""" + } +""".strip("\n")) + + println(u""" + else { + i++; + } + } + return true; +} +""".strip("\n")) + + +def writeGrandfatheredMappingsFunction(println, grandfathered_mappings, + description, source, url): + """ Writes a function definition that maps grandfathered language tags. """ + println(u"") + writeMappingHeader(println, description, source, url) + println(u"""\ +bool js::intl::LanguageTag::updateGrandfatheredMappings(JSContext* cx) { + // We're mapping regular grandfathered tags to non-grandfathered form here. + // Other tags remain unchanged. + // + // regular = "art-lojban" + // / "cel-gaulish" + // / "no-bok" + // / "no-nyn" + // / "zh-guoyu" + // / "zh-hakka" + // / "zh-min" + // / "zh-min-nan" + // / "zh-xiang" + // + // Therefore we can quickly exclude most tags by checking every + // |unicode_locale_id| subcomponent for characteristics not shared by any of + // the regular grandfathered (RG) tags: + // + // * Real-world |unicode_language_subtag|s are all two or three letters, + // so don't waste time running a useless |language.length > 3| fast-path. + // * No RG tag has a "script"-looking component. + // * No RG tag has a "region"-looking component. + // * The RG tags that match |unicode_locale_id| (art-lojban, cel-gaulish, + // zh-guoyu, zh-hakka, zh-xiang) have exactly one "variant". (no-bok, + // no-nyn, zh-min, and zh-min-nan require BCP47's extlang subtag + // that |unicode_locale_id| doesn't support.) + // * No RG tag contains |extensions| or |pu_extensions|. + if (script().present() || + region().present() || + variants().length() != 1 || + extensions().length() != 0 || + privateuse()) { + return true; + } + + MOZ_ASSERT(IsCanonicallyCasedLanguageTag(language().span())); + MOZ_ASSERT(IsCanonicallyCasedVariantTag(mozilla::MakeCStringSpan(variants()[0].get()))); + + auto variantEqualTo = [this](const char* variant) { + return strcmp(variants()[0].get(), variant) == 0; + };""") + + # From Unicode BCP 47 locale identifier . + # + # Doesn't allow any 'extensions' subtags. + re_unicode_locale_id = re.compile( + r""" + ^ + # unicode_language_id = unicode_language_subtag + # unicode_language_subtag = alpha{2,3} | alpha{5,8} + (?P[a-z]{2,3}|[a-z]{5,8}) + + # (sep unicode_script_subtag)? + # unicode_script_subtag = alpha{4} + (?:-(?P