Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/* Implementation of the Intl object and its non-constructor properties. */
#include "builtin/intl/IntlObject.h"
#include "mozilla/Assertions.h"
#include "mozilla/intl/Calendar.h"
#include "mozilla/intl/Collator.h"
#include "mozilla/intl/Currency.h"
#include "mozilla/intl/MeasureUnitGenerated.h"
#include "mozilla/intl/TimeZone.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iterator>
#include <string_view>
#include "builtin/Array.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/LocaleNegotiation.h"
#include "builtin/intl/NumberingSystemsGenerated.h"
#include "builtin/intl/SharedIntlData.h"
#include "ds/Sort.h"
#include "js/Class.h"
#include "js/experimental/Intl.h"
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
#include "js/GCAPI.h"
#include "js/GCVector.h"
#include "js/PropertyAndElement.h"
#include "js/PropertySpec.h"
#include "vm/GlobalObject.h"
#include "vm/JSAtomUtils.h" // ClassName
#include "vm/JSContext.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/StringType.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
using namespace js;
using namespace js::intl;
/******************** Mozilla Intl extensions ********************/
/**
* Returns a plain object with calendar information for a single valid locale
* (callers must perform this validation). The object will have these
* properties:
*
* firstDayOfWeek
* an integer in the range 1=Monday to 7=Sunday indicating the day
* considered the first day of the week in calendars, e.g. 7 for en-US,
* 1 for en-GB, 7 for bn-IN
* minDays
* an integer in the range of 1 to 7 indicating the minimum number
* of days required in the first week of the year, e.g. 1 for en-US,
* 4 for de
* weekend
* an array with values in the range 1=Monday to 7=Sunday indicating the
* days of the week considered as part of the weekend, e.g. [6, 7] for en-US
* and en-GB, [7] for bn-IN (note that "weekend" is *not* necessarily two
* days)
*
* NOTE: "calendar" and "locale" properties are *not* added to the object.
*/
static PlainObject* GetCalendarInfo(JSContext* cx,
Handle<JSLinearString*> loc) {
auto locale = EncodeLocale(cx, loc);
if (!locale) {
return nullptr;
}
auto result = mozilla::intl::Calendar::TryCreate(locale.get());
if (result.isErr()) {
ReportInternalError(cx, result.unwrapErr());
return nullptr;
}
auto calendar = result.unwrap();
Rooted<IdValueVector> properties(cx, cx);
if (!properties.emplaceBack(NameToId(cx->names().locale), StringValue(loc))) {
return nullptr;
}
auto type = calendar->GetBcp47Type();
if (type.isErr()) {
ReportInternalError(cx, type.unwrapErr());
return nullptr;
}
auto* calendarType = NewStringCopy<CanGC>(cx, type.unwrap());
if (!calendarType) {
return nullptr;
}
if (!properties.emplaceBack(NameToId(cx->names().calendar),
StringValue(calendarType))) {
return nullptr;
}
if (!properties.emplaceBack(
NameToId(cx->names().firstDayOfWeek),
Int32Value(static_cast<int32_t>(calendar->GetFirstDayOfWeek())))) {
return nullptr;
}
if (!properties.emplaceBack(
NameToId(cx->names().minDays),
Int32Value(calendar->GetMinimalDaysInFirstWeek()))) {
return nullptr;
}
auto weekend = calendar->GetWeekend();
if (weekend.isErr()) {
ReportInternalError(cx, weekend.unwrapErr());
return nullptr;
}
auto weekendSet = weekend.unwrap();
auto* weekendArray = NewDenseFullyAllocatedArray(cx, weekendSet.size());
if (!weekendArray) {
return nullptr;
}
weekendArray->setDenseInitializedLength(weekendSet.size());
size_t index = 0;
for (auto day : weekendSet) {
weekendArray->initDenseElement(index++,
Int32Value(static_cast<int32_t>(day)));
}
MOZ_ASSERT(index == weekendSet.size());
if (!properties.emplaceBack(NameToId(cx->names().weekend),
ObjectValue(*weekendArray))) {
return nullptr;
}
return NewPlainObjectWithUniqueNames(cx, properties);
}
/**
* 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.
*
* weekend:
* The days of the week considered as the weekend for the resolved locale.
*
* Days are encoded as integers in the range 1=Monday to 7=Sunday.
*/
static bool intl_getCalendarInfo(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
Rooted<LocalesList> requestedLocales(cx, cx);
if (!CanonicalizeLocaleList(cx, args.get(0), &requestedLocales)) {
return false;
}
Rooted<ArrayObject*> reqLocales(cx, LocalesListToArray(cx, requestedLocales));
if (!reqLocales) {
return false;
}
// 2. Let localeOptions be a new Record.
// 3. Set localeOptions.[[localeMatcher]] to "best fit".
Rooted<LocaleOptions> localeOptions(cx);
// 4. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]],
// requestedLocales, localeOpt).
auto localeData = LocaleData::Default;
mozilla::EnumSet<UnicodeExtensionKey> relevantExtensionKeys{
UnicodeExtensionKey::Calendar,
};
Rooted<ResolvedLocale> resolved(cx);
if (!ResolveLocale(cx, AvailableLocaleKind::DateTimeFormat, reqLocales,
localeOptions, relevantExtensionKeys, localeData,
&resolved)) {
return false;
}
Rooted<JSLinearString*> locale(cx, resolved.toLocale(cx));
if (!locale) {
return false;
}
// 5. Let result be GetCalendarInfo(r.[[locale]]).
auto* result = GetCalendarInfo(cx, locale);
if (!result) {
return false;
}
// 6. Return result.
args.rval().setObject(*result);
return true;
}
static const JSFunctionSpec intl_extensions[] = {
JS_FN("getCalendarInfo", intl_getCalendarInfo, 1, 0),
JS_FS_END,
};
bool JS::AddMozGetCalendarInfo(JSContext* cx, Handle<JSObject*> intl) {
return JS_DefineFunctions(cx, intl, intl_extensions);
}
/******************** Intl ********************/
using StringList = GCVector<JSLinearString*>;
/**
* Create a sorted array from a list of strings.
*/
static ArrayObject* CreateArrayFromList(JSContext* cx,
MutableHandle<StringList> list) {
// Reserve scratch space for MergeSort().
size_t initialLength = list.length();
if (!list.growBy(initialLength)) {
return nullptr;
}
// Sort all strings in alphabetical order.
MOZ_ALWAYS_TRUE(
MergeSort(list.begin(), initialLength, list.begin() + initialLength,
[](const auto* a, const auto* b, bool* lessOrEqual) {
*lessOrEqual = CompareStrings(a, b) <= 0;
return true;
}));
// Ensure we don't add duplicate entries to the array.
auto* end = std::unique(
list.begin(), list.begin() + initialLength,
[](const auto* a, const auto* b) { return EqualStrings(a, b); });
// std::unique leaves the elements after |end| with an unspecified value, so
// remove them first. And also delete the elements in the scratch space.
list.shrinkBy(std::distance(end, list.end()));
// And finally copy the strings into the result array.
auto* array = NewDenseFullyAllocatedArray(cx, list.length());
if (!array) {
return nullptr;
}
array->setDenseInitializedLength(list.length());
for (size_t i = 0; i < list.length(); ++i) {
array->initDenseElement(i, StringValue(list[i]));
}
return array;
}
/**
* Create an array from a sorted list of strings.
*/
template <size_t N>
static ArrayObject* CreateArrayFromSortedList(
JSContext* cx, const std::array<const char*, N>& list) {
// Ensure the list is sorted and doesn't contain duplicates.
MOZ_ASSERT(std::adjacent_find(std::begin(list), std::end(list),
[](const auto& a, const auto& b) {
return std::strcmp(a, b) >= 0;
}) == std::end(list));
size_t length = std::size(list);
Rooted<ArrayObject*> array(cx, NewDenseFullyAllocatedArray(cx, length));
if (!array) {
return nullptr;
}
array->ensureDenseInitializedLength(0, length);
for (size_t i = 0; i < length; ++i) {
auto* str = NewStringCopyZ<CanGC>(cx, list[i]);
if (!str) {
return nullptr;
}
array->initDenseElement(i, StringValue(str));
}
return array;
}
/**
* Create an array from an intl::Enumeration.
*/
template <const auto& unsupported>
static bool EnumerationIntoList(JSContext* cx, auto values,
MutableHandle<StringList> list) {
for (auto value : values) {
if (value.isErr()) {
ReportInternalError(cx);
return false;
}
auto span = value.unwrap();
// Skip over known, unsupported values.
std::string_view sv(span.data(), span.size());
if (std::any_of(std::begin(unsupported), std::end(unsupported),
[sv](const auto& e) { return sv == e; })) {
continue;
}
auto* string = NewStringCopy<CanGC>(cx, span);
if (!string) {
return false;
}
if (!list.append(string)) {
return false;
}
}
return true;
}
/**
* Returns the list of calendar types which mustn't be returned by
* |Intl.supportedValuesOf()|.
*/
static constexpr auto UnsupportedCalendars() {
return std::array{
"islamic",
"islamic-rgsa",
};
}
/**
* AvailableCalendars ( )
*/
static ArrayObject* AvailableCalendars(JSContext* cx) {
Rooted<StringList> list(cx, StringList(cx));
{
// Hazard analysis complains that the mozilla::Result destructor calls a
// GC function, which is unsound when returning an unrooted value. Work
// around this issue by restricting the lifetime of |keywords| to a
// separate block.
auto keywords = mozilla::intl::Calendar::GetBcp47KeywordValuesForLocale("");
if (keywords.isErr()) {
ReportInternalError(cx, keywords.unwrapErr());
return nullptr;
}
static constexpr auto unsupported = UnsupportedCalendars();
if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
return nullptr;
}
}
return CreateArrayFromList(cx, &list);
}
/**
* Returns the list of collation types which mustn't be returned by
* |Intl.supportedValuesOf()|.
*/
static constexpr auto UnsupportedCollations() {
return std::array{
"search",
"standard",
};
}
/**
* AvailableCollations ( )
*/
static ArrayObject* AvailableCollations(JSContext* cx) {
Rooted<StringList> list(cx, StringList(cx));
{
// Hazard analysis complains that the mozilla::Result destructor calls a
// GC function, which is unsound when returning an unrooted value. Work
// around this issue by restricting the lifetime of |keywords| to a
// separate block.
auto keywords = mozilla::intl::Collator::GetBcp47KeywordValues();
if (keywords.isErr()) {
ReportInternalError(cx, keywords.unwrapErr());
return nullptr;
}
static constexpr auto unsupported = UnsupportedCollations();
if (!EnumerationIntoList<unsupported>(cx, keywords.unwrap(), &list)) {
return nullptr;
}
}
return CreateArrayFromList(cx, &list);
}
/**
* Returns a list of known, unsupported currencies which are returned by
* |Currency::GetISOCurrencies()|.
*/
static constexpr auto UnsupportedCurrencies() {
// "MVP" is also marked with "questionable, remove?" in ucurr.cpp, but only
// this single currency code isn't supported by |Intl.DisplayNames| and
// therefore must be excluded by |Intl.supportedValuesOf|.
return std::array{
};
}
/**
* AvailableCurrencies ( )
*/
static ArrayObject* AvailableCurrencies(JSContext* cx) {
Rooted<StringList> list(cx, StringList(cx));
{
// Hazard analysis complains that the mozilla::Result destructor calls a
// GC function, which is unsound when returning an unrooted value. Work
// around this issue by restricting the lifetime of |currencies| to a
// separate block.
auto currencies = mozilla::intl::Currency::GetISOCurrencies();
if (currencies.isErr()) {
ReportInternalError(cx, currencies.unwrapErr());
return nullptr;
}
static constexpr auto unsupported = UnsupportedCurrencies();
if (!EnumerationIntoList<unsupported>(cx, currencies.unwrap(), &list)) {
return nullptr;
}
}
return CreateArrayFromList(cx, &list);
}
/**
* AvailableNumberingSystems ( )
*/
static ArrayObject* AvailableNumberingSystems(JSContext* cx) {
static constexpr std::array numberingSystems = {
NUMBERING_SYSTEMS_WITH_SIMPLE_DIGIT_MAPPINGS};
return CreateArrayFromSortedList(cx, numberingSystems);
}
/**
* AvailableTimeZones ( )
*/
static ArrayObject* AvailableTimeZones(JSContext* cx) {
// Unsorted list of canonical time zone names, possibly containing duplicates.
Rooted<StringList> timeZones(cx, StringList(cx));
auto& sharedIntlData = cx->runtime()->sharedIntlData.ref();
auto iterResult = sharedIntlData.availableTimeZonesIteration(cx);
if (iterResult.isErr()) {
return nullptr;
}
auto iter = iterResult.unwrap();
Rooted<JSAtom*> validatedTimeZone(cx);
for (; !iter.done(); iter.next()) {
validatedTimeZone = iter.get();
// Canonicalize the time zone before adding it to the result array.
auto* timeZone = sharedIntlData.canonicalizeTimeZone(cx, validatedTimeZone);
if (!timeZone) {
return nullptr;
}
if (!timeZones.append(timeZone)) {
return nullptr;
}
}
return CreateArrayFromList(cx, &timeZones);
}
template <size_t N>
constexpr auto MeasurementUnitNames(
const mozilla::intl::SimpleMeasureUnit (&units)[N]) {
std::array<const char*, N> array = {};
for (size_t i = 0; i < N; ++i) {
array[i] = units[i].name;
}
return array;
}
/**
* AvailableUnits ( )
*/
static ArrayObject* AvailableUnits(JSContext* cx) {
static constexpr auto simpleMeasureUnitNames =
MeasurementUnitNames(mozilla::intl::simpleMeasureUnits);
return CreateArrayFromSortedList(cx, simpleMeasureUnitNames);
}
/**
* Intl.getCanonicalLocales ( locales )
*/
static bool intl_getCanonicalLocales(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1.
Rooted<LocalesList> locales(cx, cx);
if (!CanonicalizeLocaleList(cx, args.get(0), &locales)) {
return false;
}
// Step 2.
auto* array = LocalesListToArray(cx, locales);
if (!array) {
return false;
}
args.rval().setObject(*array);
return true;
}
/**
* Intl.supportedValuesOf ( key )
*/
static bool intl_supportedValuesOf(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1.
auto* key = ToString(cx, args.get(0));
if (!key) {
return false;
}
auto* linearKey = key->ensureLinear(cx);
if (!linearKey) {
return false;
}
// Steps 2-8.
ArrayObject* list;
if (StringEqualsLiteral(linearKey, "calendar")) {
list = AvailableCalendars(cx);
} else if (StringEqualsLiteral(linearKey, "collation")) {
list = AvailableCollations(cx);
} else if (StringEqualsLiteral(linearKey, "currency")) {
list = AvailableCurrencies(cx);
} else if (StringEqualsLiteral(linearKey, "numberingSystem")) {
list = AvailableNumberingSystems(cx);
} else if (StringEqualsLiteral(linearKey, "timeZone")) {
list = AvailableTimeZones(cx);
} else if (StringEqualsLiteral(linearKey, "unit")) {
list = AvailableUnits(cx);
} else {
if (UniqueChars chars = QuoteString(cx, linearKey, '"')) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY,
chars.get());
}
return false;
}
if (!list) {
return false;
}
// Step 9.
args.rval().setObject(*list);
return true;
}
static bool intl_toSource(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setString(cx->names().Intl);
return true;
}
static const JSFunctionSpec intl_static_methods[] = {
JS_FN("toSource", intl_toSource, 0, 0),
JS_FN("getCanonicalLocales", intl_getCanonicalLocales, 1, 0),
JS_FN("supportedValuesOf", intl_supportedValuesOf, 1, 0),
JS_FS_END,
};
static const JSPropertySpec intl_static_properties[] = {
JS_STRING_SYM_PS(toStringTag, "Intl", JSPROP_READONLY),
JS_PS_END,
};
static JSObject* CreateIntlObject(JSContext* cx, JSProtoKey key) {
Rooted<JSObject*> proto(cx, &cx->global()->getObjectPrototype());
// The |Intl| object is just a plain object with some "static" function
// properties and some constructor properties.
return NewTenuredObjectWithGivenProto(cx, &IntlClass, proto);
}
/**
* Initializes the Intl Object and its standard built-in properties.
* Spec: ECMAScript Internationalization API Specification, 8.0, 8.1
*/
static bool IntlClassFinish(JSContext* cx, Handle<JSObject*> intl,
Handle<JSObject*> proto) {
// Add the constructor properties.
Rooted<JS::PropertyKey> ctorId(cx);
Rooted<JS::Value> ctorValue(cx);
for (const auto& protoKey : {
JSProto_Collator,
JSProto_DateTimeFormat,
JSProto_DisplayNames,
JSProto_DurationFormat,
JSProto_ListFormat,
JSProto_Locale,
JSProto_NumberFormat,
JSProto_PluralRules,
JSProto_RelativeTimeFormat,
JSProto_Segmenter,
}) {
if (GlobalObject::skipDeselectedConstructor(cx, protoKey)) {
continue;
}
JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey);
if (!ctor) {
return false;
}
ctorId = NameToId(ClassName(protoKey, cx));
ctorValue.setObject(*ctor);
if (!DefineDataProperty(cx, intl, ctorId, ctorValue, 0)) {
return false;
}
}
return true;
}
static const ClassSpec IntlClassSpec = {
CreateIntlObject, nullptr, intl_static_methods, intl_static_properties,
nullptr, nullptr, IntlClassFinish,
};
const JSClass js::intl::IntlClass = {
"Intl",
JSCLASS_HAS_CACHED_PROTO(JSProto_Intl),
JS_NULL_CLASS_OPS,
&IntlClassSpec,
};