diff options
author | Konstantin Tokarev <annulen@yandex.ru> | 2016-08-25 19:20:41 +0300 |
---|---|---|
committer | Konstantin Tokarev <annulen@yandex.ru> | 2017-02-02 12:30:55 +0000 |
commit | 6882a04fb36642862b11efe514251d32070c3d65 (patch) | |
tree | b7959826000b061fd5ccc7512035c7478742f7b0 /Source/JavaScriptCore/runtime/IntlObject.cpp | |
parent | ab6df191029eeeb0b0f16f127d553265659f739e (diff) | |
download | qtwebkit-6882a04fb36642862b11efe514251d32070c3d65.tar.gz |
Imported QtWebKit TP3 (git b57bc6801f1876c3220d5a4bfea33d620d477443)
Change-Id: I3b1d8a2808782c9f34d50240000e20cb38d3680f
Reviewed-by: Konstantin Tokarev <annulen@yandex.ru>
Diffstat (limited to 'Source/JavaScriptCore/runtime/IntlObject.cpp')
-rw-r--r-- | Source/JavaScriptCore/runtime/IntlObject.cpp | 998 |
1 files changed, 998 insertions, 0 deletions
diff --git a/Source/JavaScriptCore/runtime/IntlObject.cpp b/Source/JavaScriptCore/runtime/IntlObject.cpp new file mode 100644 index 000000000..395a0430c --- /dev/null +++ b/Source/JavaScriptCore/runtime/IntlObject.cpp @@ -0,0 +1,998 @@ +/* + * Copyright (C) 2015 Andy VanWagoner (thetalecrafter@gmail.com) + * Copyright (C) 2015 Sukolsak Sakshuwong (sukolsak@gmail.com) + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "IntlObject.h" + +#if ENABLE(INTL) + +#include "Error.h" +#include "FunctionPrototype.h" +#include "IntlCollator.h" +#include "IntlCollatorConstructor.h" +#include "IntlCollatorPrototype.h" +#include "IntlDateTimeFormat.h" +#include "IntlDateTimeFormatConstructor.h" +#include "IntlDateTimeFormatPrototype.h" +#include "IntlNumberFormat.h" +#include "IntlNumberFormatConstructor.h" +#include "IntlNumberFormatPrototype.h" +#include "JSCInlines.h" +#include "JSCJSValueInlines.h" +#include "Lookup.h" +#include "ObjectPrototype.h" +#include <unicode/uloc.h> +#include <unicode/unumsys.h> +#include <wtf/Assertions.h> +#include <wtf/NeverDestroyed.h> + +namespace JSC { + +STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(IntlObject); + +} + +namespace JSC { + +struct MatcherResult { + String locale; + String extension; + size_t extensionIndex; +}; + +const ClassInfo IntlObject::s_info = { "Object", &Base::s_info, 0, CREATE_METHOD_TABLE(IntlObject) }; + +IntlObject::IntlObject(VM& vm, Structure* structure) + : JSNonFinalObject(vm, structure) +{ +} + +IntlObject* IntlObject::create(VM& vm, JSGlobalObject* globalObject, Structure* structure) +{ + IntlObject* object = new (NotNull, allocateCell<IntlObject>(vm.heap)) IntlObject(vm, structure); + object->finishCreation(vm, globalObject); + return object; +} + +void IntlObject::finishCreation(VM& vm, JSGlobalObject* globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + + // Set up Collator. + IntlCollatorPrototype* collatorPrototype = IntlCollatorPrototype::create(vm, globalObject, IntlCollatorPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); + Structure* collatorStructure = IntlCollator::createStructure(vm, globalObject, collatorPrototype); + IntlCollatorConstructor* collatorConstructor = IntlCollatorConstructor::create(vm, IntlCollatorConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), collatorPrototype, collatorStructure); + + collatorPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, collatorConstructor, DontEnum); + + // Set up NumberFormat. + IntlNumberFormatPrototype* numberFormatPrototype = IntlNumberFormatPrototype::create(vm, globalObject, IntlNumberFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); + Structure* numberFormatStructure = IntlNumberFormat::createStructure(vm, globalObject, numberFormatPrototype); + IntlNumberFormatConstructor* numberFormatConstructor = IntlNumberFormatConstructor::create(vm, IntlNumberFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), numberFormatPrototype, numberFormatStructure); + + numberFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, numberFormatConstructor, DontEnum); + + // Set up DateTimeFormat. + IntlDateTimeFormatPrototype* dateTimeFormatPrototype = IntlDateTimeFormatPrototype::create(vm, globalObject, IntlDateTimeFormatPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); + Structure* dateTimeFormatStructure = IntlDateTimeFormat::createStructure(vm, globalObject, dateTimeFormatPrototype); + IntlDateTimeFormatConstructor* dateTimeFormatConstructor = IntlDateTimeFormatConstructor::create(vm, IntlDateTimeFormatConstructor::createStructure(vm, globalObject, globalObject->functionPrototype()), dateTimeFormatPrototype, dateTimeFormatStructure); + + dateTimeFormatPrototype->putDirectWithoutTransition(vm, vm.propertyNames->constructor, dateTimeFormatConstructor, DontEnum); + + // 8.1 Properties of the Intl Object (ECMA-402 2.0) + putDirectWithoutTransition(vm, vm.propertyNames->Collator, collatorConstructor, DontEnum); + putDirectWithoutTransition(vm, vm.propertyNames->NumberFormat, numberFormatConstructor, DontEnum); + putDirectWithoutTransition(vm, vm.propertyNames->DateTimeFormat, dateTimeFormatConstructor, DontEnum); +} + +Structure* IntlObject::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +String defaultLocale() +{ + // 6.2.4 DefaultLocale () + String locale = uloc_getDefault(); + convertICULocaleToBCP47LanguageTag(locale); + return locale; +} + +void convertICULocaleToBCP47LanguageTag(String& locale) +{ + locale.replace('_', '-'); +} + +bool intlBooleanOption(ExecState& state, JSValue options, PropertyName property, bool& usesFallback) +{ + // 9.2.9 GetOption (options, property, type, values, fallback) + // For type="boolean". values is always undefined. + + // 1. Let opts be ToObject(options). + JSObject* opts = options.toObject(&state); + + // 2. ReturnIfAbrupt(opts). + if (state.hadException()) + return false; + + // 3. Let value be Get(opts, property). + JSValue value = opts->get(&state, property); + + // 4. ReturnIfAbrupt(value). + if (state.hadException()) + return false; + + // 5. If value is not undefined, then + if (!value.isUndefined()) { + // a. Assert: type is "boolean" or "string". + // Function dedicated to "boolean". + + // b. If type is "boolean", then + // i. Let value be ToBoolean(value). + bool booleanValue = value.toBoolean(&state); + + // e. Return value. + usesFallback = false; + return booleanValue; + } + + // 6. Else return fallback. + // Because fallback can be undefined, we let the caller handle it instead. + usesFallback = true; + return false; +} + +String intlStringOption(ExecState& state, JSValue options, PropertyName property, std::initializer_list<const char*> values, const char* notFound, const char* fallback) +{ + // 9.2.9 GetOption (options, property, type, values, fallback) + // For type="string". + + // 1. Let opts be ToObject(options). + JSObject* opts = options.toObject(&state); + + // 2. ReturnIfAbrupt(opts). + if (state.hadException()) + return { }; + + // 3. Let value be Get(opts, property). + JSValue value = opts->get(&state, property); + + // 4. ReturnIfAbrupt(value). + if (state.hadException()) + return { }; + + // 5. If value is not undefined, then + if (!value.isUndefined()) { + // a. Assert: type is "boolean" or "string". + // Function dedicated to "string". + + // c. If type is "string", then + // i. Let value be ToString(value). + String stringValue = value.toWTFString(&state); + + // ii. ReturnIfAbrupt(value). + if (state.hadException()) + return { }; + + // d. If values is not undefined, then + // i. If values does not contain an element equal to value, throw a RangeError exception. + if (values.size() && std::find(values.begin(), values.end(), stringValue) == values.end()) { + state.vm().throwException(&state, createRangeError(&state, notFound)); + return { }; + } + + // e. Return value. + return stringValue; + } + + // 6. Else return fallback. + return fallback; +} + +unsigned intlNumberOption(ExecState& state, JSValue options, PropertyName property, unsigned minimum, unsigned maximum, unsigned fallback) +{ + // 9.2.9 GetNumberOption (options, property, minimum, maximum, fallback) (ECMA-402 2.0) + // 1. Let opts be ToObject(options). + JSObject* opts = options.toObject(&state); + + // 2. ReturnIfAbrupt(opts). + if (state.hadException()) + return 0; + + // 3. Let value be Get(opts, property). + JSValue value = opts->get(&state, property); + + // 4. ReturnIfAbrupt(value). + if (state.hadException()) + return 0; + + // 5. If value is not undefined, then + if (!value.isUndefined()) { + // a. Let value be ToNumber(value). + double doubleValue = value.toNumber(&state); + // b. ReturnIfAbrupt(value). + if (state.hadException()) + return 0; + // 1. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception. + if (!(doubleValue >= minimum && doubleValue <= maximum)) { + state.vm().throwException(&state, createRangeError(&state, *property.publicName() + " is out of range")); + return 0; + } + + // c. Return floor(value). + return static_cast<unsigned>(doubleValue); + } + + // 6. Else return fallback. + return fallback; +} + +static String privateUseLangTag(const Vector<String>& parts, size_t startIndex) +{ + size_t numParts = parts.size(); + size_t currentIndex = startIndex; + + // Check for privateuse. + // privateuse = "x" 1*("-" (2*8alphanum)) + StringBuilder privateuse; + while (currentIndex < numParts) { + const String& singleton = parts[currentIndex]; + unsigned singletonLength = singleton.length(); + bool isValid = (singletonLength == 1 && (singleton == "x" || singleton == "X")); + if (!isValid) + break; + + if (currentIndex != startIndex) + privateuse.append('-'); + + ++currentIndex; + unsigned numExtParts = 0; + privateuse.append('x'); + while (currentIndex < numParts) { + const String& extPart = parts[currentIndex]; + unsigned extPartLength = extPart.length(); + + bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>()); + if (!isValid) + break; + + ++currentIndex; + ++numExtParts; + privateuse.append('-'); + privateuse.append(extPart.convertToASCIILowercase()); + } + + // Requires at least one production. + if (!numExtParts) + return String(); + } + + // Leftovers makes it invalid. + if (currentIndex < numParts) + return String(); + + return privateuse.toString(); +} + +static String canonicalLangTag(const Vector<String>& parts) +{ + ASSERT(!parts.isEmpty()); + + // Follows the grammar at https://www.rfc-editor.org/rfc/bcp/bcp47.txt + // langtag = language ["-" script] ["-" region] *("-" variant) *("-" extension) ["-" privateuse] + + size_t numParts = parts.size(); + // Check for language. + // language = 2*3ALPHA ["-" extlang] / 4ALPHA / 5*8ALPHA + size_t currentIndex = 0; + const String& language = parts[currentIndex]; + unsigned languageLength = language.length(); + bool canHaveExtlang = languageLength >= 2 && languageLength <= 3; + bool isValidLanguage = languageLength >= 2 && languageLength <= 8 && language.isAllSpecialCharacters<isASCIIAlpha>(); + if (!isValidLanguage) + return String(); + + ++currentIndex; + StringBuilder canonical; + canonical.append(language.convertToASCIILowercase()); + + // Check for extlang. + // extlang = 3ALPHA *2("-" 3ALPHA) + if (canHaveExtlang) { + for (unsigned times = 0; times < 3 && currentIndex < numParts; ++times) { + const String& extlang = parts[currentIndex]; + unsigned extlangLength = extlang.length(); + if (extlangLength == 3 && extlang.isAllSpecialCharacters<isASCIIAlpha>()) { + ++currentIndex; + canonical.append('-'); + canonical.append(extlang.convertToASCIILowercase()); + } else + break; + } + } + + // Check for script. + // script = 4ALPHA + if (currentIndex < numParts) { + const String& script = parts[currentIndex]; + unsigned scriptLength = script.length(); + if (scriptLength == 4 && script.isAllSpecialCharacters<isASCIIAlpha>()) { + ++currentIndex; + canonical.append('-'); + canonical.append(toASCIIUpper(script[0])); + canonical.append(script.substring(1, 3).convertToASCIILowercase()); + } + } + + // Check for region. + // region = 2ALPHA / 3DIGIT + if (currentIndex < numParts) { + const String& region = parts[currentIndex]; + unsigned regionLength = region.length(); + bool isValidRegion = ( + (regionLength == 2 && region.isAllSpecialCharacters<isASCIIAlpha>()) + || (regionLength == 3 && region.isAllSpecialCharacters<isASCIIDigit>()) + ); + if (isValidRegion) { + ++currentIndex; + canonical.append('-'); + canonical.append(region.convertToASCIIUppercase()); + } + } + + // Check for variant. + // variant = 5*8alphanum / (DIGIT 3alphanum) + HashSet<String> subtags; + while (currentIndex < numParts) { + const String& variant = parts[currentIndex]; + unsigned variantLength = variant.length(); + bool isValidVariant = ( + (variantLength >= 5 && variantLength <= 8 && variant.isAllSpecialCharacters<isASCIIAlphanumeric>()) + || (variantLength == 4 && isASCIIDigit(variant[0]) && variant.substring(1, 3).isAllSpecialCharacters<isASCIIAlphanumeric>()) + ); + if (!isValidVariant) + break; + + // Cannot include duplicate subtags (case insensitive). + String lowerVariant = variant.convertToASCIILowercase(); + if (!subtags.add(lowerVariant).isNewEntry) + return String(); + + ++currentIndex; + + // Reordering variant subtags is not required in the spec. + canonical.append('-'); + canonical.append(lowerVariant); + } + + // Check for extension. + // extension = singleton 1*("-" (2*8alphanum)) + // singleton = alphanum except x or X + subtags.clear(); + Vector<String> extensions; + while (currentIndex < numParts) { + const String& possibleSingleton = parts[currentIndex]; + unsigned singletonLength = possibleSingleton.length(); + bool isValidSingleton = (singletonLength == 1 && possibleSingleton != "x" && possibleSingleton != "X" && isASCIIAlphanumeric(possibleSingleton[0])); + if (!isValidSingleton) + break; + + // Cannot include duplicate singleton (case insensitive). + String singleton = possibleSingleton.convertToASCIILowercase(); + if (!subtags.add(singleton).isNewEntry) + return String(); + + ++currentIndex; + int numExtParts = 0; + StringBuilder extension; + extension.append(singleton); + while (currentIndex < numParts) { + const String& extPart = parts[currentIndex]; + unsigned extPartLength = extPart.length(); + + bool isValid = (extPartLength >= 2 && extPartLength <= 8 && extPart.isAllSpecialCharacters<isASCIIAlphanumeric>()); + if (!isValid) + break; + + ++currentIndex; + ++numExtParts; + extension.append('-'); + extension.append(extPart.convertToASCIILowercase()); + } + + // Requires at least one production. + if (!numExtParts) + return String(); + + extensions.append(extension.toString()); + } + + // Add extensions to canonical sorted by singleton. + std::sort( + extensions.begin(), + extensions.end(), + [] (const String& a, const String& b) -> bool { + return a[0] < b[0]; + } + ); + size_t numExtenstions = extensions.size(); + for (size_t i = 0; i < numExtenstions; ++i) { + canonical.append('-'); + canonical.append(extensions[i]); + } + + // Check for privateuse. + if (currentIndex < numParts) { + String privateuse = privateUseLangTag(parts, currentIndex); + if (privateuse.isNull()) + return String(); + canonical.append('-'); + canonical.append(privateuse); + } + + // FIXME: Replace subtags with their preferred values. + + return canonical.toString(); +} + +static String grandfatheredLangTag(const String& locale) +{ + // grandfathered = irregular / regular + // FIXME: convert to a compile time hash table if this is causing performance issues. + HashMap<String, String> tagMap = { + // Irregular. + { ASCIILiteral("en-gb-oed"), ASCIILiteral("en-GB-oed") }, + { ASCIILiteral("i-ami"), ASCIILiteral("ami") }, + { ASCIILiteral("i-bnn"), ASCIILiteral("bnn") }, + { ASCIILiteral("i-default"), ASCIILiteral("i-default") }, + { ASCIILiteral("i-enochian"), ASCIILiteral("i-enochian") }, + { ASCIILiteral("i-hak"), ASCIILiteral("hak") }, + { ASCIILiteral("i-klingon"), ASCIILiteral("tlh") }, + { ASCIILiteral("i-lux"), ASCIILiteral("lb") }, + { ASCIILiteral("i-mingo"), ASCIILiteral("i-mingo") }, + { ASCIILiteral("i-navajo"), ASCIILiteral("nv") }, + { ASCIILiteral("i-pwn"), ASCIILiteral("pwn") }, + { ASCIILiteral("i-tao"), ASCIILiteral("tao") }, + { ASCIILiteral("i-tay"), ASCIILiteral("tay") }, + { ASCIILiteral("i-tsu"), ASCIILiteral("tsu") }, + { ASCIILiteral("sgn-be-fr"), ASCIILiteral("sfb") }, + { ASCIILiteral("sgn-be-nl"), ASCIILiteral("vgt") }, + { ASCIILiteral("sgn-ch-de"), ASCIILiteral("sgg") }, + // Regular. + { ASCIILiteral("art-lojban"), ASCIILiteral("jbo") }, + { ASCIILiteral("cel-gaulish"), ASCIILiteral("cel-gaulish") }, + { ASCIILiteral("no-bok"), ASCIILiteral("nb") }, + { ASCIILiteral("no-nyn"), ASCIILiteral("nn") }, + { ASCIILiteral("zh-guoyu"), ASCIILiteral("cmn") }, + { ASCIILiteral("zh-hakka"), ASCIILiteral("hak") }, + { ASCIILiteral("zh-min"), ASCIILiteral("zh-min") }, + { ASCIILiteral("zh-min-nan"), ASCIILiteral("nan") }, + { ASCIILiteral("zh-xiang"), ASCIILiteral("hsn") } + }; + + return tagMap.get(locale.convertToASCIILowercase()); +} + +static String canonicalizeLanguageTag(const String& locale) +{ + // 6.2.2 IsStructurallyValidLanguageTag (locale) + // 6.2.3 CanonicalizeLanguageTag (locale) + // These are done one after another in CanonicalizeLocaleList, so they are combined here to reduce duplication. + // https://www.rfc-editor.org/rfc/bcp/bcp47.txt + + // Language-Tag = langtag / privateuse / grandfathered + String grandfather = grandfatheredLangTag(locale); + if (!grandfather.isNull()) + return grandfather; + + // FIXME: Replace redundant tags [RFC4647]. + + Vector<String> parts; + locale.split('-', true, parts); + if (!parts.isEmpty()) { + String langtag = canonicalLangTag(parts); + if (!langtag.isNull()) + return langtag; + + String privateuse = privateUseLangTag(parts, 0); + if (!privateuse.isNull()) + return privateuse; + } + + return String(); +} + +Vector<String> canonicalizeLocaleList(ExecState& state, JSValue locales) +{ + // 9.2.1 CanonicalizeLocaleList (locales) + VM& vm = state.vm(); + JSGlobalObject* globalObject = state.callee()->globalObject(); + Vector<String> seen; + + // 1. If locales is undefined, then a. Return a new empty List. + if (locales.isUndefined()) + return seen; + + // 2. Let seen be an empty List. + // Done before to also return in step 1, if needed. + + // 3. If Type(locales) is String, then + JSObject* localesObject; + if (locales.isString()) { + // a. Let aLocales be CreateArrayFromList(«locales»). + JSArray* localesArray = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithContiguous), 1); + localesArray->initializeIndex(vm, 0, locales); + // 4. Let O be ToObject(aLocales). + localesObject = localesArray; + } else { + // 4. Let O be ToObject(aLocales). + localesObject = locales.toObject(&state); + } + + // 5. ReturnIfAbrupt(O). + if (state.hadException()) + return Vector<String>(); + + // 6. Let len be ToLength(Get(O, "length")). + JSValue lengthProperty = localesObject->get(&state, vm.propertyNames->length); + if (state.hadException()) + return Vector<String>(); + + double length = lengthProperty.toLength(&state); + if (state.hadException()) + return Vector<String>(); + + // Keep track of locales that have been added to the list. + HashSet<String> seenSet; + + // 7. Let k be 0. + // 8. Repeat, while k < len + for (double k = 0; k < length; ++k) { + // a. Let Pk be ToString(k). + // Not needed because hasProperty and get take an int for numeric key. + + // b. Let kPresent be HasProperty(O, Pk). + bool kPresent = localesObject->hasProperty(&state, k); + + // c. ReturnIfAbrupt(kPresent). + if (state.hadException()) + return Vector<String>(); + + // d. If kPresent is true, then + if (kPresent) { + // i. Let kValue be Get(O, Pk). + JSValue kValue = localesObject->get(&state, k); + + // ii. ReturnIfAbrupt(kValue). + if (state.hadException()) + return Vector<String>(); + + // iii. If Type(kValue) is not String or Object, throw a TypeError exception. + if (!kValue.isString() && !kValue.isObject()) { + throwTypeError(&state, ASCIILiteral("locale value must be a string or object")); + return Vector<String>(); + } + + // iv. Let tag be ToString(kValue). + JSString* tag = kValue.toString(&state); + + // v. ReturnIfAbrupt(tag). + if (state.hadException()) + return Vector<String>(); + + // vi. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception. + // vii. Let canonicalizedTag be CanonicalizeLanguageTag(tag). + String canonicalizedTag = canonicalizeLanguageTag(tag->value(&state)); + if (canonicalizedTag.isNull()) { + state.vm().throwException(&state, createRangeError(&state, String::format("invalid language tag: %s", tag->value(&state).utf8().data()))); + return Vector<String>(); + } + + // viii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen. + if (seenSet.add(canonicalizedTag).isNewEntry) + seen.append(canonicalizedTag); + } + // e. Increase k by 1. + } + + return seen; +} + +String bestAvailableLocale(const HashSet<String>& availableLocales, const String& locale) +{ + // 9.2.2 BestAvailableLocale (availableLocales, locale) + // 1. Let candidate be locale. + String candidate = locale; + + // 2. Repeat + while (!candidate.isEmpty()) { + // a. If availableLocales contains an element equal to candidate, then return candidate. + if (availableLocales.contains(candidate)) + return candidate; + + // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined. + size_t pos = candidate.reverseFind('-'); + if (pos == notFound) + return String(); + + // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, then decrease pos by 2. + if (pos >= 2 && candidate[pos - 2] == '-') + pos -= 2; + + // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive. + candidate = candidate.substring(0, pos); + } + + return String(); +} + +String removeUnicodeLocaleExtension(const String& locale) +{ + Vector<String> parts; + locale.split('-', parts); + StringBuilder builder; + size_t partsSize = parts.size(); + if (partsSize > 0) + builder.append(parts[0]); + for (size_t p = 1; p < partsSize; ++p) { + if (parts[p] == "u") { + // Skip the u- and anything that follows until another singleton. + // While the next part is part of the unicode extension, skip it. + while (p + 1 < partsSize && parts[p + 1].length() > 1) + ++p; + } else { + builder.append('-'); + builder.append(parts[p]); + } + } + return builder.toString(); +} + +static MatcherResult lookupMatcher(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales) +{ + // 9.2.3 LookupMatcher (availableLocales, requestedLocales) (ECMA-402 2.0) + String locale; + String noExtensionsLocale; + String availableLocale; + for (size_t i = 0; i < requestedLocales.size() && availableLocale.isNull(); ++i) { + locale = requestedLocales[i]; + noExtensionsLocale = removeUnicodeLocaleExtension(locale); + availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); + } + + MatcherResult result; + if (!availableLocale.isNull()) { + result.locale = availableLocale; + if (locale != noExtensionsLocale) { + // i. Let extension be the String value consisting of the first substring of locale that is a Unicode locale extension sequence. + // ii. Let extensionIndex be the character position of the initial "-" extension sequence within locale. + size_t extensionIndex = locale.find("-u-"); + RELEASE_ASSERT(extensionIndex != notFound); + + size_t extensionLength = locale.length() - extensionIndex; + size_t end = extensionIndex + 3; + while (end < locale.length()) { + end = locale.find('-', end); + if (end == notFound) + break; + if (end + 2 < locale.length() && locale[end + 2] == '-') { + extensionLength = end - extensionIndex; + break; + } + end++; + } + result.extension = locale.substring(extensionIndex, extensionLength); + result.extensionIndex = extensionIndex; + } + } else + result.locale = defaultLocale(); + return result; +} + +static MatcherResult bestFitMatcher(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales) +{ + // 9.2.4 BestFitMatcher (availableLocales, requestedLocales) (ECMA-402 2.0) + // FIXME: Implement something better than lookup. + return lookupMatcher(availableLocales, requestedLocales); +} + +HashMap<String, String> resolveLocale(const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, const HashMap<String, String>& options, const char* const relevantExtensionKeys[], size_t relevantExtensionKeyCount, Vector<String> (*localeData)(const String&, size_t)) +{ + // 9.2.5 ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) (ECMA-402 2.0) + // 1. Let matcher be the value of options.[[localeMatcher]]. + const String& matcher = options.get(ASCIILiteral("localeMatcher")); + + // 2. If matcher is "lookup", then + MatcherResult (*matcherOperation)(const HashSet<String>&, const Vector<String>&); + if (matcher == "lookup") { + // a. Let MatcherOperation be the abstract operation LookupMatcher. + matcherOperation = lookupMatcher; + } else { // 3. Else + // a. Let MatcherOperation be the abstract operation BestFitMatcher. + matcherOperation = bestFitMatcher; + } + + // 4. Let r be MatcherOperation(availableLocales, requestedLocales). + MatcherResult matcherResult = matcherOperation(availableLocales, requestedLocales); + + // 5. Let foundLocale be the value of r.[[locale]]. + String foundLocale = matcherResult.locale; + + // 6. If r has an [[extension]] field, then + Vector<String> extensionSubtags; + if (!matcherResult.extension.isNull()) { + // a. Let extension be the value of r.[[extension]]. + // b. Let extensionIndex be the value of r.[[extensionIndex]]. + // c. Let extensionSubtags be Call(%StringProto_split%, extension, «"-"») . + // d. Let extensionSubtagsLength be Get(CreateArrayFromList(extensionSubtags), "length"). + matcherResult.extension.split('-', extensionSubtags); + } + + // 7. Let result be a new Record. + HashMap<String, String> result; + + // 8. Set result.[[dataLocale]] to foundLocale. + result.add(ASCIILiteral("dataLocale"), foundLocale); + + // 9. Let supportedExtension be "-u". + String supportedExtension = ASCIILiteral("-u"); + + // 10. Let k be 0. + // 11. Let rExtensionKeys be ToObject(CreateArrayFromList(relevantExtensionKeys)). + // 12. ReturnIfAbrupt(rExtensionKeys). + // 13. Let len be ToLength(Get(rExtensionKeys, "length")). + // 14. Repeat while k < len + for (size_t keyIndex = 0; keyIndex < relevantExtensionKeyCount; ++keyIndex) { + // a. Let key be Get(rExtensionKeys, ToString(k)). + // b. ReturnIfAbrupt(key). + const char* key = relevantExtensionKeys[keyIndex]; + + // c. Let foundLocaleData be Get(localeData, foundLocale). + // d. ReturnIfAbrupt(foundLocaleData). + // e. Let keyLocaleData be ToObject(Get(foundLocaleData, key)). + // f. ReturnIfAbrupt(keyLocaleData). + Vector<String> keyLocaleData = localeData(foundLocale, keyIndex); + + // g. Let value be ToString(Get(keyLocaleData, "0")). + // h. ReturnIfAbrupt(value). + ASSERT(!keyLocaleData.isEmpty()); + String value = keyLocaleData[0]; + + // i. Let supportedExtensionAddition be "". + String supportedExtensionAddition; + + // j. If extensionSubtags is not undefined, then + if (!extensionSubtags.isEmpty()) { + // i. Let keyPos be Call(%ArrayProto_indexOf%, extensionSubtags, «key») . + size_t keyPos = extensionSubtags.find(key); + // ii. If keyPos != -1, then + if (keyPos != notFound) { + // FIXME: https://github.com/tc39/ecma402/issues/59 + // 1. If keyPos + 1 < extensionSubtagsLength and the length of the result of Get(extensionSubtags, ToString(keyPos +1)) is greater than 2, then + if (keyPos + 1 < extensionSubtags.size() && extensionSubtags[keyPos + 1].length() > 2) { + const String& requestedValue = extensionSubtags[keyPos + 1]; + if (keyLocaleData.contains(requestedValue)) { + value = requestedValue; + supportedExtensionAddition = makeString('-', key, '-', value); + } + } else if (keyLocaleData.contains(static_cast<String>(ASCIILiteral("true")))) { + // 2. Else, if the result of Call(%StringProto_includes%, keyLocaleData, «"true"») is true, then + value = ASCIILiteral("true"); + } + } + } + + // k. If options has a field [[<key>]], then + HashMap<String, String>::const_iterator iterator = options.find(key); + if (iterator != options.end()) { + // i. Let optionsValue be the value of ToString(options.[[<key>]]). + // ii. ReturnIfAbrupt(optionsValue). + const String& optionsValue = iterator->value; + // iii. If the result of Call(%StringProto_includes%, keyLocaleData, «optionsValue») is true, then + if (!optionsValue.isNull() && keyLocaleData.contains(optionsValue)) { + // 1. If optionsValue is not equal to value, then + if (optionsValue != value) { + value = optionsValue; + supportedExtensionAddition = String(); + } + } + } + + // l. Set result.[[<key>]] to value. + result.add(key, value); + + // m. Append supportedExtensionAddition to supportedExtension. + supportedExtension.append(supportedExtensionAddition); + + // n. Increase k by 1. + } + + // 15. If the number of elements in supportedExtension is greater than 2, then + if (supportedExtension.length() > 2) { + // a. Let preExtension be the substring of foundLocale from position 0, inclusive, to position extensionIndex, exclusive. + // b. Let postExtension be the substring of foundLocale from position extensionIndex to the end of the string. + // c. Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension. + String preExtension = foundLocale.substring(0, matcherResult.extensionIndex); + String postExtension = foundLocale.substring(matcherResult.extensionIndex); + foundLocale = preExtension + supportedExtension + postExtension; + } + + // 16. Set result.[[locale]] to foundLocale. + result.add(ASCIILiteral("locale"), foundLocale); + + // 17. Return result. + return result; +} + +static JSArray* lookupSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales) +{ + // 9.2.6 LookupSupportedLocales (availableLocales, requestedLocales) + + // 1. Let rLocales be CreateArrayFromList(requestedLocales). + // Already an array. + + // 2. Let len be ToLength(Get(rLocales, "length")). + size_t len = requestedLocales.size(); + + // 3. Let subset be an empty List. + VM& vm = state.vm(); + JSGlobalObject* globalObject = state.callee()->globalObject(); + JSArray* subset = JSArray::tryCreateUninitialized(vm, globalObject->arrayStructureForIndexingTypeDuringAllocation(ArrayWithUndecided), 0); + if (!subset) { + throwOutOfMemoryError(&state); + return nullptr; + } + + // 4. Let k be 0. + // 5. Repeat while k < len + for (size_t k = 0; k < len; ++k) { + // a. Let Pk be ToString(k). + // b. Let locale be Get(rLocales, Pk). + // c. ReturnIfAbrupt(locale). + const String& locale = requestedLocales[k]; + + // d. Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed. + String noExtensionsLocale = removeUnicodeLocaleExtension(locale); + + // e. Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale). + String availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); + + // f. If availableLocale is not undefined, then append locale to the end of subset. + if (!availableLocale.isNull()) + subset->push(&state, jsString(&state, locale)); + + // g. Increment k by 1. + } + + // 6. Return subset. + return subset; +} + +static JSArray* bestFitSupportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales) +{ + // 9.2.7 BestFitSupportedLocales (availableLocales, requestedLocales) + // FIXME: Implement something better than lookup. + return lookupSupportedLocales(state, availableLocales, requestedLocales); +} + +JSValue supportedLocales(ExecState& state, const HashSet<String>& availableLocales, const Vector<String>& requestedLocales, JSValue options) +{ + // 9.2.8 SupportedLocales (availableLocales, requestedLocales, options) + VM& vm = state.vm(); + String matcher; + + // 1. If options is not undefined, then + if (!options.isUndefined()) { + // a. Let matcher be GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit"). + matcher = intlStringOption(state, options, vm.propertyNames->localeMatcher, { "lookup", "best fit" }, "localeMatcher must be either \"lookup\" or \"best fit\"", "best fit"); + // b. ReturnIfAbrupt(matcher). + if (state.hadException()) + return jsUndefined(); + } else { + // 2. Else, let matcher be "best fit". + matcher = ASCIILiteral("best fit"); + } + + JSArray* supportedLocales; + // 3. If matcher is "best fit", + if (matcher == "best fit") { + // a. Let MatcherOperation be the abstract operation BestFitSupportedLocales. + // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales). + supportedLocales = bestFitSupportedLocales(state, availableLocales, requestedLocales); + } else { + // 4. Else + // a. Let MatcherOperation be the abstract operation LookupSupportedLocales. + // 5. Let supportedLocales be MatcherOperation(availableLocales, requestedLocales). + supportedLocales = lookupSupportedLocales(state, availableLocales, requestedLocales); + } + + if (state.hadException()) + return jsUndefined(); + + // 6. Let subset be CreateArrayFromList(supportedLocales). + // Already an array. + + // 7. Let keys be subset.[[OwnPropertyKeys]](). + PropertyNameArray keys(&state, PropertyNameMode::Strings); + supportedLocales->getOwnPropertyNames(supportedLocales, &state, keys, EnumerationMode()); + + PropertyDescriptor desc; + desc.setConfigurable(false); + desc.setWritable(false); + + // 8. Repeat for each element P of keys in List order, + size_t len = keys.size(); + for (size_t i = 0; i < len; ++i) { + // a. Let desc be PropertyDescriptor { [[Configurable]]: false, [[Writable]]: false }. + // Created above for reuse. + + // b. Let status be DefinePropertyOrThrow(subset, P, desc). + supportedLocales->defineOwnProperty(supportedLocales, &state, keys[i], desc, true); + + // c. Assert: status is not abrupt completion. + if (state.hadException()) + return jsUndefined(); + } + + // 9. Return subset. + return supportedLocales; +} + +Vector<String> numberingSystemsForLocale(const String& locale) +{ + static NeverDestroyed<Vector<String>> cachedNumberingSystems; + Vector<String>& availableNumberingSystems = cachedNumberingSystems.get(); + if (availableNumberingSystems.isEmpty()) { + UErrorCode status(U_ZERO_ERROR); + UEnumeration* numberingSystemNames = unumsys_openAvailableNames(&status); + ASSERT(U_SUCCESS(status)); + status = U_ZERO_ERROR; + + int32_t resultLength; + // Numbering system names are always ASCII, so use char[]. + while (const char* result = uenum_next(numberingSystemNames, &resultLength, &status)) { + ASSERT(U_SUCCESS(status)); + status = U_ZERO_ERROR; + availableNumberingSystems.append(String(result, resultLength)); + } + uenum_close(numberingSystemNames); + } + + UErrorCode status(U_ZERO_ERROR); + UNumberingSystem* defaultSystem = unumsys_open(locale.utf8().data(), &status); + ASSERT(U_SUCCESS(status)); + String defaultSystemName(unumsys_getName(defaultSystem)); + unumsys_close(defaultSystem); + + Vector<String> numberingSystems({ defaultSystemName }); + numberingSystems.appendVector(availableNumberingSystems); + return numberingSystems; +} + +} // namespace JSC + +#endif // ENABLE(INTL) |