diff options
Diffstat (limited to 'js/src/builtin/intl/ListFormat.cpp')
-rw-r--r-- | js/src/builtin/intl/ListFormat.cpp | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/js/src/builtin/intl/ListFormat.cpp b/js/src/builtin/intl/ListFormat.cpp new file mode 100644 index 0000000000..57bc5ba7c0 --- /dev/null +++ b/js/src/builtin/intl/ListFormat.cpp @@ -0,0 +1,554 @@ +/* -*- 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/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()); +} |