diff options
Diffstat (limited to 'js/src/builtin/intl/Collator.cpp')
-rw-r--r-- | js/src/builtin/intl/Collator.cpp | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp new file mode 100644 index 0000000000..449de1dc45 --- /dev/null +++ b/js/src/builtin/intl/Collator.cpp @@ -0,0 +1,472 @@ +/* -*- 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/. */ + +/* Intl.Collator implementation. */ + +#include "builtin/intl/Collator.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/Locale.h" +#include "mozilla/Span.h" + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/SharedIntlData.h" +#include "gc/GCContext.h" +#include "js/PropertySpec.h" +#include "js/StableStringChars.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/Runtime.h" +#include "vm/StringType.h" +#include "vm/WellKnownAtom.h" // js_*_str + +#include "vm/GeckoProfiler-inl.h" +#include "vm/JSObject-inl.h" + +using namespace js; + +using JS::AutoStableStringChars; + +using js::intl::ReportInternalError; +using js::intl::SharedIntlData; + +const JSClassOps CollatorObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + CollatorObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass CollatorObject::class_ = { + "Intl.Collator", + JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Collator) | + JSCLASS_FOREGROUND_FINALIZE, + &CollatorObject::classOps_, &CollatorObject::classSpec_}; + +const JSClass& CollatorObject::protoClass_ = PlainObject::class_; + +static bool collator_toSource(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Collator); + return true; +} + +static const JSFunctionSpec collator_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", + 1, 0), + JS_FS_END}; + +static const JSFunctionSpec collator_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), + JS_FN(js_toSource_str, collator_toSource, 0, 0), JS_FS_END}; + +static const JSPropertySpec collator_properties[] = { + JS_SELF_HOSTED_GET("compare", "$Intl_Collator_compare_get", 0), + JS_STRING_SYM_PS(toStringTag, "Intl.Collator", JSPROP_READONLY), JS_PS_END}; + +static bool Collator(JSContext* cx, unsigned argc, Value* vp); + +const ClassSpec CollatorObject::classSpec_ = { + GenericCreateConstructor<Collator, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<CollatorObject>, + collator_static_methods, + nullptr, + collator_methods, + collator_properties, + nullptr, + ClassSpec::DontDefineConstructor}; + +/** + * 10.1.2 Intl.Collator([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool Collator(JSContext* cx, const CallArgs& args) { + AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.Collator"); + + // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). + + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Collator, &proto)) { + return false; + } + + Rooted<CollatorObject*> collator( + cx, NewObjectWithClassProto<CollatorObject>(cx, proto)); + if (!collator) { + return false; + } + + HandleValue locales = args.get(0); + HandleValue options = args.get(1); + + // Step 6. + if (!intl::InitializeObject(cx, collator, cx->names().InitializeCollator, + locales, options)) { + return false; + } + + args.rval().setObject(*collator); + return true; +} + +static bool Collator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + return Collator(cx, args); +} + +bool js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + + return Collator(cx, args); +} + +void js::CollatorObject::finalize(JS::GCContext* gcx, JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (mozilla::intl::Collator* coll = obj->as<CollatorObject>().getCollator()) { + intl::RemoveICUCellMemory(gcx, obj, CollatorObject::EstimatedMemoryUse); + delete coll; + } +} + +bool js::intl_availableCollations(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); + if (!locale) { + return false; + } + auto keywords = + mozilla::intl::Collator::GetBcp47KeywordValuesForLocale(locale.get()); + if (keywords.isErr()) { + ReportInternalError(cx, keywords.unwrapErr()); + return false; + } + + RootedObject collations(cx, NewDenseEmptyArray(cx)); + if (!collations) { + return false; + } + + // The first element of the collations array must be |null| per + // ES2017 Intl, 10.2.3 Internal Slots. + if (!NewbornArrayPush(cx, collations, NullValue())) { + return false; + } + + for (auto result : keywords.unwrap()) { + if (result.isErr()) { + ReportInternalError(cx); + return false; + } + mozilla::Span<const char> collation = result.unwrap(); + + // Per ECMA-402, 10.2.3, we don't include standard and search: + // "The values 'standard' and 'search' must not be used as elements in + // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co + // array." + static constexpr auto standard = mozilla::MakeStringSpan("standard"); + static constexpr auto search = mozilla::MakeStringSpan("search"); + if (collation == standard || collation == search) { + continue; + } + + JSString* jscollation = NewStringCopy<CanGC>(cx, collation); + if (!jscollation) { + return false; + } + if (!NewbornArrayPush(cx, collations, StringValue(jscollation))) { + return false; + } + } + + args.rval().setObject(*collations); + return true; +} + +/** + * Returns a new mozilla::intl::Collator with the locale and collation options + * of the given Collator. + */ +static mozilla::intl::Collator* NewIntlCollator( + JSContext* cx, Handle<CollatorObject*> collator) { + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); + if (!internals) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + mozilla::intl::Locale tag; + { + Rooted<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + using mozilla::intl::Collator; + + Collator::Options options{}; + + if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) { + return nullptr; + } + + enum class Usage { Search, Sort }; + + Usage usage; + { + JSLinearString* str = value.toString()->ensureLinear(cx); + if (!str) { + return nullptr; + } + + if (StringEqualsLiteral(str, "search")) { + usage = Usage::Search; + } else { + MOZ_ASSERT(StringEqualsLiteral(str, "sort")); + usage = Usage::Sort; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> keywords(cx); + + // ICU expects collation as Unicode locale extensions on locale. + if (usage == Usage::Search) { + if (!keywords.emplaceBack("co", cx->names().search)) { + return nullptr; + } + + // Search collations can't select a different collation, so the collation + // property is guaranteed to be "default". +#ifdef DEBUG + if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { + return nullptr; + } + + JSLinearString* collation = value.toString()->ensureLinear(cx); + if (!collation) { + return nullptr; + } + + MOZ_ASSERT(StringEqualsLiteral(collation, "default")); +#endif + } else { + if (!GetProperty(cx, internals, internals, cx->names().collation, &value)) { + return nullptr; + } + + JSLinearString* collation = value.toString()->ensureLinear(cx); + if (!collation) { + return nullptr; + } + + // Set collation as a Unicode locale extension when it was specified. + if (!StringEqualsLiteral(collation, "default")) { + if (!keywords.emplaceBack("co", collation)) { + 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; + } + + intl::FormatBuffer<char> buffer(cx); + if (auto result = tag.ToString(buffer); result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + UniqueChars locale = buffer.extractStringZ(); + if (!locale) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) { + return nullptr; + } + + { + JSLinearString* sensitivity = value.toString()->ensureLinear(cx); + if (!sensitivity) { + return nullptr; + } + if (StringEqualsLiteral(sensitivity, "base")) { + options.sensitivity = Collator::Sensitivity::Base; + } else if (StringEqualsLiteral(sensitivity, "accent")) { + options.sensitivity = Collator::Sensitivity::Accent; + } else if (StringEqualsLiteral(sensitivity, "case")) { + options.sensitivity = Collator::Sensitivity::Case; + } else { + MOZ_ASSERT(StringEqualsLiteral(sensitivity, "variant")); + options.sensitivity = Collator::Sensitivity::Variant; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, + &value)) { + return nullptr; + } + options.ignorePunctuation = value.toBoolean(); + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) { + return nullptr; + } + if (!value.isUndefined()) { + options.numeric = value.toBoolean(); + } + + if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) { + return nullptr; + } + if (!value.isUndefined()) { + JSLinearString* caseFirst = value.toString()->ensureLinear(cx); + if (!caseFirst) { + return nullptr; + } + if (StringEqualsLiteral(caseFirst, "upper")) { + options.caseFirst = Collator::CaseFirst::Upper; + } else if (StringEqualsLiteral(caseFirst, "lower")) { + options.caseFirst = Collator::CaseFirst::Lower; + } else { + MOZ_ASSERT(StringEqualsLiteral(caseFirst, "false")); + options.caseFirst = Collator::CaseFirst::False; + } + } + + auto collResult = Collator::TryCreate(locale.get()); + if (collResult.isErr()) { + ReportInternalError(cx, collResult.unwrapErr()); + return nullptr; + } + auto coll = collResult.unwrap(); + + auto optResult = coll->SetOptions(options); + if (optResult.isErr()) { + ReportInternalError(cx, optResult.unwrapErr()); + return nullptr; + } + + return coll.release(); +} + +static mozilla::intl::Collator* GetOrCreateCollator( + JSContext* cx, Handle<CollatorObject*> collator) { + // Obtain a cached mozilla::intl::Collator object. + mozilla::intl::Collator* coll = collator->getCollator(); + if (coll) { + return coll; + } + + coll = NewIntlCollator(cx, collator); + if (!coll) { + return nullptr; + } + collator->setCollator(coll); + + intl::AddICUCellMemory(collator, CollatorObject::EstimatedMemoryUse); + return coll; +} + +static bool intl_CompareStrings(JSContext* cx, mozilla::intl::Collator* coll, + HandleString str1, HandleString str2, + MutableHandleValue result) { + MOZ_ASSERT(str1); + MOZ_ASSERT(str2); + + if (str1 == str2) { + result.setInt32(0); + return true; + } + + AutoStableStringChars stableChars1(cx); + if (!stableChars1.initTwoByte(cx, str1)) { + return false; + } + + AutoStableStringChars stableChars2(cx); + if (!stableChars2.initTwoByte(cx, str2)) { + return false; + } + + mozilla::Range<const char16_t> chars1 = stableChars1.twoByteRange(); + mozilla::Range<const char16_t> chars2 = stableChars2.twoByteRange(); + + result.setInt32(coll->CompareStrings(chars1, chars2)); + return true; +} + +bool js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + MOZ_ASSERT(args[0].isObject()); + MOZ_ASSERT(args[1].isString()); + MOZ_ASSERT(args[2].isString()); + + Rooted<CollatorObject*> collator(cx, + &args[0].toObject().as<CollatorObject>()); + + mozilla::intl::Collator* coll = GetOrCreateCollator(cx, collator); + if (!coll) { + return false; + } + + // Use the UCollator to actually compare the strings. + RootedString str1(cx, args[1].toString()); + RootedString str2(cx, args[2].toString()); + return intl_CompareStrings(cx, coll, str1, str2, args.rval()); +} + +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->runtime()->sharedIntlData.ref(); + + RootedString locale(cx, args[0].toString()); + bool isUpperFirst; + if (!sharedIntlData.isUpperCaseFirst(cx, locale, &isUpperFirst)) { + return false; + } + + args.rval().setBoolean(isUpperFirst); + return true; +} |