Source code

Revision control

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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "builtin/intl/ListFormat.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/PodOperations.h"
#include "mozilla/Unused.h"
#include <stddef.h>
#include <stdint.h>
#include "builtin/Array.h"
#include "builtin/intl/CommonFunctions.h"
#include "builtin/intl/ScopedICUObject.h"
#include "gc/FreeOp.h"
#include "js/Utility.h"
#include "js/Vector.h"
#include "unicode/uformattedvalue.h"
#include "unicode/ulistformatter.h"
#include "unicode/utypes.h"
#include "vm/JSContext.h"
#include "vm/PlainObject.h" // js::PlainObject
#include "vm/Runtime.h" // js::ReportAllocationOverflow
#include "vm/SelfHosting.h"
#include "vm/Stack.h"
#include "vm/StringType.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
#include "vm/ObjectOperations-inl.h"
using namespace js;
using mozilla::CheckedInt;
using js::intl::CallICU;
using js::intl::IcuLocale;
const JSClassOps ListFormatObject::classOps_ = {
nullptr, // addProperty
nullptr, // delProperty
nullptr, // enumerate
nullptr, // newEnumerate
nullptr, // resolve
nullptr, // mayResolve
ListFormatObject::finalize, // finalize
nullptr, // call
nullptr, // hasInstance
nullptr, // construct
nullptr, // trace
};
const JSClass ListFormatObject::class_ = {
"Intl.ListFormat",
JSCLASS_HAS_RESERVED_SLOTS(ListFormatObject::SLOT_COUNT) |
JSCLASS_HAS_CACHED_PROTO(JSProto_ListFormat) |
JSCLASS_FOREGROUND_FINALIZE,
&ListFormatObject::classOps_, &ListFormatObject::classSpec_};
const JSClass& ListFormatObject::protoClass_ = PlainObject::class_;
static bool listFormat_toSource(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
args.rval().setString(cx->names().ListFormat);
return true;
}
static const JSFunctionSpec listFormat_static_methods[] = {
JS_SELF_HOSTED_FN("supportedLocalesOf",
"Intl_ListFormat_supportedLocalesOf", 1, 0),
JS_FS_END};
static const JSFunctionSpec listFormat_methods[] = {
JS_SELF_HOSTED_FN("resolvedOptions", "Intl_ListFormat_resolvedOptions", 0,
0),
JS_SELF_HOSTED_FN("format", "Intl_ListFormat_format", 1, 0),
JS_SELF_HOSTED_FN("formatToParts", "Intl_ListFormat_formatToParts", 1, 0),
JS_FN(js_toSource_str, listFormat_toSource, 0, 0), JS_FS_END};
static const JSPropertySpec listFormat_properties[] = {
JS_STRING_SYM_PS(toStringTag, "Intl.ListFormat", JSPROP_READONLY),
JS_PS_END};
static bool ListFormat(JSContext* cx, unsigned argc, Value* vp);
const ClassSpec ListFormatObject::classSpec_ = {
GenericCreateConstructor<ListFormat, 0, gc::AllocKind::FUNCTION>,
GenericCreatePrototype<ListFormatObject>,
listFormat_static_methods,
nullptr,
listFormat_methods,
listFormat_properties,
nullptr,
ClassSpec::DontDefineConstructor};
enum class ListFormatOptions {
SupportsTypeAndStyle,
NoTypeAndStyle,
};
/**
* Initialize a new Intl.ListFormat object using the named self-hosted function.
*/
static bool InitializeListFormatObject(JSContext* cx, HandleObject obj,
HandlePropertyName initializer,
HandleValue locales, HandleValue options,
ListFormatOptions lfoptions) {
FixedInvokeArgs<4> args(cx);
args[0].setObject(*obj);
args[1].set(locales);
args[2].set(options);
args[3].setBoolean(lfoptions == ListFormatOptions::SupportsTypeAndStyle);
RootedValue ignored(cx);
if (!CallSelfHostedFunction(cx, initializer, JS::NullHandleValue, args,
&ignored)) {
return false;
}
MOZ_ASSERT(ignored.isUndefined(),
"Unexpected return value from non-legacy Intl object initializer");
return true;
}
/**
* Intl.ListFormat([ locales [, options]])
*/
static bool ListFormat(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
// Step 1.
if (!ThrowIfNotConstructing(cx, args, "Intl.ListFormat")) {
return false;
}
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
RootedObject proto(cx);
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ListFormat,
&proto)) {
return false;
}
Rooted<ListFormatObject*> listFormat(
cx, NewObjectWithClassProto<ListFormatObject>(cx, proto));
if (!listFormat) {
return false;
}
HandleValue locales = args.get(0);
HandleValue options = args.get(1);
constexpr ListFormatOptions lfoptions =
#ifndef U_HIDE_DRAFT_API
ListFormatOptions::SupportsTypeAndStyle
#else
ListFormatOptions::NoTypeAndStyle
#endif
;
// Step 3.
if (!InitializeListFormatObject(cx, listFormat,
cx->names().InitializeListFormat, locales,
options, lfoptions)) {
return false;
}
args.rval().setObject(*listFormat);
return true;
}
void js::ListFormatObject::finalize(JSFreeOp* fop, JSObject* obj) {
MOZ_ASSERT(fop->onMainThread());
if (UListFormatter* lf = obj->as<ListFormatObject>().getListFormatter()) {
intl::RemoveICUCellMemory(fop, obj, ListFormatObject::EstimatedMemoryUse);
ulistfmt_close(lf);
}
}
/**
* Returns a new UListFormatter with the locale and list formatting options
* of the given ListFormat.
*/
static UListFormatter* NewUListFormatter(JSContext* cx,
Handle<ListFormatObject*> listFormat) {
RootedObject internals(cx, intl::GetInternalsObject(cx, listFormat));
if (!internals) {
return nullptr;
}
RootedValue value(cx);
if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) {
return nullptr;
}
UniqueChars locale = intl::EncodeLocale(cx, value.toString());
if (!locale) {
return nullptr;
}
enum class ListFormatType { Conjunction, Disjunction, Unit };
ListFormatType type;
if (!GetProperty(cx, internals, internals, cx->names().type, &value)) {
return nullptr;
}
{
JSLinearString* strType = value.toString()->ensureLinear(cx);
if (!strType) {
return nullptr;
}
if (StringEqualsLiteral(strType, "conjunction")) {
type = ListFormatType::Conjunction;
} else if (StringEqualsLiteral(strType, "disjunction")) {
type = ListFormatType::Disjunction;
} else {
MOZ_ASSERT(StringEqualsLiteral(strType, "unit"));
type = ListFormatType::Unit;
}
}
enum class ListFormatStyle { Long, Short, Narrow };
ListFormatStyle style;
if (!GetProperty(cx, internals, internals, cx->names().style, &value)) {
return nullptr;
}
{
JSLinearString* strStyle = value.toString()->ensureLinear(cx);
if (!strStyle) {
return nullptr;
}
if (StringEqualsLiteral(strStyle, "long")) {
style = ListFormatStyle::Long;
} else if (StringEqualsLiteral(strStyle, "short")) {
style = ListFormatStyle::Short;
} else {
MOZ_ASSERT(StringEqualsLiteral(strStyle, "narrow"));
style = ListFormatStyle::Narrow;
}
}
UErrorCode status = U_ZERO_ERROR;
UListFormatter* lf;
#ifndef U_HIDE_DRAFT_API
UListFormatterType utype;
switch (type) {
case ListFormatType::Conjunction:
utype = ULISTFMT_TYPE_AND;
break;
case ListFormatType::Disjunction:
utype = ULISTFMT_TYPE_OR;
break;
case ListFormatType::Unit:
utype = ULISTFMT_TYPE_UNITS;
break;
}
UListFormatterWidth uwidth;
switch (style) {
case ListFormatStyle::Long:
uwidth = ULISTFMT_WIDTH_WIDE;
break;
case ListFormatStyle::Short:
uwidth = ULISTFMT_WIDTH_SHORT;
break;
case ListFormatStyle::Narrow:
uwidth = ULISTFMT_WIDTH_NARROW;
break;
}
lf = ulistfmt_openForType(IcuLocale(locale.get()), utype, uwidth, &status);
#else
MOZ_ASSERT(type == ListFormatType::Conjunction);
MOZ_ASSERT(style == ListFormatStyle::Long);
mozilla::Unused << type;
mozilla::Unused << style;
lf = ulistfmt_open(IcuLocale(locale.get()), &status);
#endif
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return nullptr;
}
return lf;
}
static constexpr size_t DEFAULT_LIST_LENGTH = 8;
using ListFormatStringVector = Vector<UniqueTwoByteChars, DEFAULT_LIST_LENGTH>;
using ListFormatStringLengthVector = Vector<int32_t, DEFAULT_LIST_LENGTH>;
static_assert(sizeof(UniqueTwoByteChars) == sizeof(char16_t*),
"UniqueTwoByteChars are stored efficiently and are held in "
"continuous memory");
/**
* FormatList ( listFormat, list )
*/
static bool FormatList(JSContext* cx, UListFormatter* lf,
const ListFormatStringVector& strings,
const ListFormatStringLengthVector& stringLengths,
MutableHandleValue result) {
MOZ_ASSERT(strings.length() == stringLengths.length());
MOZ_ASSERT(strings.length() <= INT32_MAX);
JSString* str = intl::CallICU(cx, [lf, &strings, &stringLengths](
UChar* chars, int32_t size,
UErrorCode* status) {
return ulistfmt_format(
lf, reinterpret_cast<char16_t* const*>(strings.begin()),
stringLengths.begin(), int32_t(strings.length()), chars, size, status);
});
if (!str) {
return false;
}
result.setString(str);
return true;
}
/**
* FormatListToParts ( listFormat, list )
*/
static bool FormatListToParts(JSContext* cx, UListFormatter* lf,
const ListFormatStringVector& strings,
const ListFormatStringLengthVector& stringLengths,
MutableHandleValue result) {
MOZ_ASSERT(strings.length() == stringLengths.length());
MOZ_ASSERT(strings.length() <= INT32_MAX);
UErrorCode status = U_ZERO_ERROR;
UFormattedList* formatted = ulistfmt_openResult(&status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
ScopedICUObject<UFormattedList, ulistfmt_closeResult> toClose(formatted);
ulistfmt_formatStringsToResult(
lf, reinterpret_cast<char16_t* const*>(strings.begin()),
stringLengths.begin(), int32_t(strings.length()), formatted, &status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
const UFormattedValue* formattedValue =
ulistfmt_resultAsValue(formatted, &status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
RootedString overallResult(cx,
intl::FormattedValueToString(cx, formattedValue));
if (!overallResult) {
return false;
}
RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
if (!partsArray) {
return false;
}
using FieldType = js::ImmutablePropertyNamePtr JSAtomState::*;
size_t lastEndIndex = 0;
RootedObject singlePart(cx);
RootedValue val(cx);
auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) {
singlePart = NewBuiltinClassInstance<PlainObject>(cx);
if (!singlePart) {
return false;
}
val = StringValue(cx->names().*type);
if (!DefineDataProperty(cx, singlePart, cx->names().type, val)) {
return false;
}
JSLinearString* partSubstr = NewDependentString(
cx, overallResult, beginIndex, endIndex - beginIndex);
if (!partSubstr) {
return false;
}
val = StringValue(partSubstr);
if (!DefineDataProperty(cx, singlePart, cx->names().value, val)) {
return false;
}
if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) {
return false;
}
lastEndIndex = endIndex;
return true;
};
UConstrainedFieldPosition* fpos = ucfpos_open(&status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos);
// We're only interested in ULISTFMT_ELEMENT_FIELD fields.
ucfpos_constrainField(fpos, UFIELD_CATEGORY_LIST, ULISTFMT_ELEMENT_FIELD,
&status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
while (true) {
bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
if (!hasMore) {
break;
}
int32_t beginIndexInt, endIndexInt;
ucfpos_getIndexes(fpos, &beginIndexInt, &endIndexInt, &status);
if (U_FAILURE(status)) {
intl::ReportInternalError(cx);
return false;
}
MOZ_ASSERT(beginIndexInt >= 0);
MOZ_ASSERT(endIndexInt >= 0);
MOZ_ASSERT(beginIndexInt <= endIndexInt,
"field iterator returning invalid range");
size_t beginIndex = size_t(beginIndexInt);
size_t endIndex = size_t(endIndexInt);
// Indices are guaranteed to be returned in order (from left to right).
MOZ_ASSERT(lastEndIndex <= beginIndex,
"field iteration didn't return fields in order start to "
"finish as expected");
if (lastEndIndex < beginIndex) {
if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) {
return false;
}
}
if (!AppendPart(&JSAtomState::element, beginIndex, endIndex)) {
return false;
}
}
// Append any final literal.
if (lastEndIndex < overallResult->length()) {
if (!AppendPart(&JSAtomState::literal, lastEndIndex,
overallResult->length())) {
return false;
}
}
result.setObject(*partsArray);
return true;
}
bool js::intl_FormatList(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 3);
Rooted<ListFormatObject*> listFormat(
cx, &args[0].toObject().as<ListFormatObject>());
bool formatToParts = args[2].toBoolean();
// Obtain a cached UListFormatter object.
UListFormatter* lf = listFormat->getListFormatter();
if (!lf) {
lf = NewUListFormatter(cx, listFormat);
if (!lf) {
return false;
}
listFormat->setListFormatter(lf);
intl::AddICUCellMemory(listFormat, ListFormatObject::EstimatedMemoryUse);
}
// Collect all strings and their lengths.
ListFormatStringVector strings(cx);
ListFormatStringLengthVector stringLengths(cx);
// Keep a conservative running count of overall length.
CheckedInt<int32_t> stringLengthTotal(0);
RootedArrayObject list(cx, &args[1].toObject().as<ArrayObject>());
RootedValue value(cx);
uint32_t listLen = list->length();
for (uint32_t i = 0; i < listLen; i++) {
if (!GetElement(cx, list, list, i, &value)) {
return false;
}
JSLinearString* linear = value.toString()->ensureLinear(cx);
if (!linear) {
return false;
}
size_t linearLength = linear->length();
if (!stringLengths.append(linearLength)) {
return false;
}
stringLengthTotal += linearLength;
UniqueTwoByteChars chars = cx->make_pod_array<char16_t>(linearLength);
if (!chars) {
return false;
}
CopyChars(chars.get(), *linear);
if (!strings.append(std::move(chars))) {
return false;
}
}
// Add space for N unrealistically large conjunctions.
constexpr int32_t MaxConjunctionLen = 100;
stringLengthTotal += CheckedInt<int32_t>(listLen) * MaxConjunctionLen;
// If the overestimate exceeds ICU length limits, don't try to format.
if (!stringLengthTotal.isValid()) {
ReportAllocationOverflow(cx);
return false;
}
// Use the UListFormatter to actually format the strings.
if (formatToParts) {
return FormatListToParts(cx, lf, strings, stringLengths, args.rval());
}
return FormatList(cx, lf, strings, stringLengths, args.rval());
}