AbstractOperations.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. /*
  2. * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/AllOf.h>
  7. #include <AK/AnyOf.h>
  8. #include <AK/CharacterTypes.h>
  9. #include <AK/Find.h>
  10. #include <AK/Function.h>
  11. #include <AK/QuickSort.h>
  12. #include <AK/TypeCasts.h>
  13. #include <LibJS/Runtime/Array.h>
  14. #include <LibJS/Runtime/GlobalObject.h>
  15. #include <LibJS/Runtime/Intl/AbstractOperations.h>
  16. #include <LibJS/Runtime/Intl/Locale.h>
  17. #include <LibLocale/Locale.h>
  18. namespace JS::Intl {
  19. // 6.2.2 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
  20. ThrowCompletionOr<Optional<::Locale::LocaleID>> is_structurally_valid_language_tag(VM& vm, StringView locale)
  21. {
  22. auto contains_duplicate_variant = [&](auto& variants) -> ThrowCompletionOr<bool> {
  23. if (variants.is_empty())
  24. return false;
  25. quick_sort(variants);
  26. for (size_t i = 0; i < variants.size() - 1; ++i) {
  27. if (TRY_OR_THROW_OOM(vm, variants[i].equals_ignoring_case(variants[i + 1])))
  28. return true;
  29. }
  30. return false;
  31. };
  32. // IsStructurallyValidLanguageTag returns true if all of the following conditions hold, false otherwise:
  33. // locale can be generated from the EBNF grammar for unicode_locale_id in Unicode Technical Standard #35 LDML § 3.2 Unicode Locale Identifier;
  34. auto locale_id = TRY_OR_THROW_OOM(vm, ::Locale::parse_unicode_locale_id(locale));
  35. if (!locale_id.has_value())
  36. return OptionalNone {};
  37. // locale does not use any of the backwards compatibility syntax described in Unicode Technical Standard #35 LDML § 3.3 BCP 47 Conformance;
  38. // https://unicode.org/reports/tr35/#BCP_47_Conformance
  39. if (locale.contains('_') || locale_id->language_id.is_root || !locale_id->language_id.language.has_value())
  40. return OptionalNone {};
  41. // the unicode_language_id within locale contains no duplicate unicode_variant_subtag subtags; and
  42. if (TRY(contains_duplicate_variant(locale_id->language_id.variants)))
  43. return OptionalNone {};
  44. // if locale contains an extensions* component, that component
  45. Vector<char> unique_keys;
  46. for (auto& extension : locale_id->extensions) {
  47. // does not contain any other_extensions components with duplicate [alphanum-[tTuUxX]] subtags,
  48. // contains at most one unicode_locale_extensions component,
  49. // contains at most one transformed_extensions component, and
  50. char key = extension.visit(
  51. [](::Locale::LocaleExtension const&) { return 'u'; },
  52. [](::Locale::TransformedExtension const&) { return 't'; },
  53. [](::Locale::OtherExtension const& ext) { return static_cast<char>(to_ascii_lowercase(ext.key)); });
  54. if (unique_keys.contains_slow(key))
  55. return OptionalNone {};
  56. TRY_OR_THROW_OOM(vm, unique_keys.try_append(key));
  57. // if a transformed_extensions component that contains a tlang component is present, then
  58. // the tlang component contains no duplicate unicode_variant_subtag subtags.
  59. if (auto* transformed = extension.get_pointer<::Locale::TransformedExtension>()) {
  60. auto& language = transformed->language;
  61. if (language.has_value() && TRY(contains_duplicate_variant(language->variants)))
  62. return Optional<::Locale::LocaleID> {};
  63. }
  64. }
  65. return locale_id;
  66. }
  67. // 6.2.3 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
  68. ThrowCompletionOr<String> canonicalize_unicode_locale_id(VM& vm, ::Locale::LocaleID& locale)
  69. {
  70. // Note: This implementation differs from the spec in how Step 3 is implemented. The spec assumes
  71. // the input to this method is a string, and is written such that operations are performed on parts
  72. // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
  73. // structure directly. From a footnote in the spec:
  74. //
  75. // The third step of this algorithm ensures that a Unicode locale extension sequence in the
  76. // returned language tag contains:
  77. // * only the first instance of any attribute duplicated in the input, and
  78. // * only the first keyword for a given key in the input.
  79. for (auto& extension : locale.extensions) {
  80. if (!extension.has<::Locale::LocaleExtension>())
  81. continue;
  82. auto& locale_extension = extension.get<::Locale::LocaleExtension>();
  83. auto attributes = move(locale_extension.attributes);
  84. for (auto& attribute : attributes) {
  85. if (!locale_extension.attributes.contains_slow(attribute))
  86. TRY_OR_THROW_OOM(vm, locale_extension.attributes.try_append(move(attribute)));
  87. }
  88. auto keywords = move(locale_extension.keywords);
  89. for (auto& keyword : keywords) {
  90. if (!any_of(locale_extension.keywords, [&](auto const& k) { return k.key == keyword.key; }))
  91. TRY_OR_THROW_OOM(vm, locale_extension.keywords.try_append(move(keyword)));
  92. }
  93. break;
  94. }
  95. // 1. Let localeId be the string locale after performing the algorithm to transform it to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers.
  96. // 2. Let localeId be the string localeId after performing the algorithm to transform it to canonical form.
  97. auto locale_id = TRY_OR_THROW_OOM(vm, ::Locale::canonicalize_unicode_locale_id(locale));
  98. VERIFY(locale_id.has_value());
  99. // 4. Return localeId.
  100. return locale_id.release_value();
  101. }
  102. // 6.3.1 IsWellFormedCurrencyCode ( currency ), https://tc39.es/ecma402/#sec-iswellformedcurrencycode
  103. bool is_well_formed_currency_code(StringView currency)
  104. {
  105. // 1. If the length of currency is not 3, return false.
  106. if (currency.length() != 3)
  107. return false;
  108. // 2. Let normalized be the ASCII-uppercase of currency.
  109. // 3. If normalized contains any code unit outside of 0x0041 through 0x005A (corresponding to Unicode characters LATIN CAPITAL LETTER A through LATIN CAPITAL LETTER Z), return false.
  110. if (!all_of(currency, is_ascii_alpha))
  111. return false;
  112. // 4. Return true.
  113. return true;
  114. }
  115. // 6.5.1 IsWellFormedUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-iswellformedunitidentifier
  116. bool is_well_formed_unit_identifier(StringView unit_identifier)
  117. {
  118. // 6.5.2 IsSanctionedSingleUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier
  119. constexpr auto is_sanctioned_single_unit_identifier = [](StringView unit_identifier) {
  120. // 1. If unitIdentifier is listed in Table 2 below, return true.
  121. // 2. Else, return false.
  122. static constexpr auto sanctioned_units = sanctioned_single_unit_identifiers();
  123. return find(sanctioned_units.begin(), sanctioned_units.end(), unit_identifier) != sanctioned_units.end();
  124. };
  125. // 1. If ! IsSanctionedSingleUnitIdentifier(unitIdentifier) is true, then
  126. if (is_sanctioned_single_unit_identifier(unit_identifier)) {
  127. // a. Return true.
  128. return true;
  129. }
  130. // 2. Let i be StringIndexOf(unitIdentifier, "-per-", 0).
  131. auto indices = unit_identifier.find_all("-per-"sv);
  132. // 3. If i is -1 or StringIndexOf(unitIdentifier, "-per-", i + 1) is not -1, then
  133. if (indices.size() != 1) {
  134. // a. Return false.
  135. return false;
  136. }
  137. // 4. Assert: The five-character substring "-per-" occurs exactly once in unitIdentifier, at index i.
  138. // NOTE: We skip this because the indices vector being of size 1 already verifies this invariant.
  139. // 5. Let numerator be the substring of unitIdentifier from 0 to i.
  140. auto numerator = unit_identifier.substring_view(0, indices[0]);
  141. // 6. Let denominator be the substring of unitIdentifier from i + 5.
  142. auto denominator = unit_identifier.substring_view(indices[0] + 5);
  143. // 7. If ! IsSanctionedSingleUnitIdentifier(numerator) and ! IsSanctionedSingleUnitIdentifier(denominator) are both true, then
  144. if (is_sanctioned_single_unit_identifier(numerator) && is_sanctioned_single_unit_identifier(denominator)) {
  145. // a. Return true.
  146. return true;
  147. }
  148. // 8. Return false.
  149. return false;
  150. }
  151. // 9.2.1 CanonicalizeLocaleList ( locales ), https://tc39.es/ecma402/#sec-canonicalizelocalelist
  152. ThrowCompletionOr<Vector<String>> canonicalize_locale_list(VM& vm, Value locales)
  153. {
  154. auto& realm = *vm.current_realm();
  155. // 1. If locales is undefined, then
  156. if (locales.is_undefined()) {
  157. // a. Return a new empty List.
  158. return Vector<String> {};
  159. }
  160. // 2. Let seen be a new empty List.
  161. Vector<String> seen;
  162. Object* object = nullptr;
  163. // 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then
  164. if (locales.is_string() || (locales.is_object() && is<Locale>(locales.as_object()))) {
  165. // a. Let O be CreateArrayFromList(« locales »).
  166. object = Array::create_from(realm, { locales });
  167. }
  168. // 4. Else,
  169. else {
  170. // a. Let O be ? ToObject(locales).
  171. object = TRY(locales.to_object(vm));
  172. }
  173. // 5. Let len be ? ToLength(? Get(O, "length")).
  174. auto length_value = TRY(object->get(vm.names.length));
  175. auto length = TRY(length_value.to_length(vm));
  176. // 6. Let k be 0.
  177. // 7. Repeat, while k < len,
  178. for (size_t k = 0; k < length; ++k) {
  179. // a. Let Pk be ToString(k).
  180. auto property_key = PropertyKey { k };
  181. // b. Let kPresent be ? HasProperty(O, Pk).
  182. auto key_present = TRY(object->has_property(property_key));
  183. // c. If kPresent is true, then
  184. if (key_present) {
  185. // i. Let kValue be ? Get(O, Pk).
  186. auto key_value = TRY(object->get(property_key));
  187. // ii. If Type(kValue) is not String or Object, throw a TypeError exception.
  188. if (!key_value.is_string() && !key_value.is_object())
  189. return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOrString, key_value);
  190. String tag;
  191. // iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
  192. if (key_value.is_object() && is<Locale>(key_value.as_object())) {
  193. // 1. Let tag be kValue.[[Locale]].
  194. tag = static_cast<Locale const&>(key_value.as_object()).locale();
  195. }
  196. // iv. Else,
  197. else {
  198. // 1. Let tag be ? ToString(kValue).
  199. tag = TRY(key_value.to_string(vm));
  200. }
  201. // v. If ! IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
  202. auto locale_id = TRY(is_structurally_valid_language_tag(vm, tag));
  203. if (!locale_id.has_value())
  204. return vm.throw_completion<RangeError>(ErrorType::IntlInvalidLanguageTag, tag);
  205. // vi. Let canonicalizedTag be ! CanonicalizeUnicodeLocaleId(tag).
  206. // NOTE: We TRY this operation only to propagate OOM errors.
  207. auto canonicalized_tag = TRY(canonicalize_unicode_locale_id(vm, *locale_id));
  208. // vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
  209. if (!seen.contains_slow(canonicalized_tag))
  210. TRY_OR_THROW_OOM(vm, seen.try_append(move(canonicalized_tag)));
  211. }
  212. // d. Increase k by 1.
  213. }
  214. return seen;
  215. }
  216. // 9.2.2 BestAvailableLocale ( availableLocales, locale ), https://tc39.es/ecma402/#sec-bestavailablelocale
  217. Optional<StringView> best_available_locale(StringView locale)
  218. {
  219. // 1. Let candidate be locale.
  220. StringView candidate = locale;
  221. // 2. Repeat,
  222. while (true) {
  223. // a. If availableLocales contains an element equal to candidate, return candidate.
  224. if (::Locale::is_locale_available(candidate))
  225. return candidate;
  226. // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined.
  227. auto pos = candidate.find_last('-');
  228. if (!pos.has_value())
  229. return {};
  230. // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2.
  231. if ((*pos >= 2) && (candidate[*pos - 2] == '-'))
  232. pos = *pos - 2;
  233. // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
  234. candidate = candidate.substring_view(0, *pos);
  235. }
  236. }
  237. struct MatcherResult {
  238. String locale;
  239. Vector<::Locale::Extension> extensions {};
  240. };
  241. // 9.2.3 LookupMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatcher
  242. static ThrowCompletionOr<MatcherResult> lookup_matcher(VM& vm, Vector<String> const& requested_locales)
  243. {
  244. // 1. Let result be a new Record.
  245. MatcherResult result {};
  246. // 2. For each element locale of requestedLocales, do
  247. for (auto const& locale : requested_locales) {
  248. auto locale_id = TRY_OR_THROW_OOM(vm, ::Locale::parse_unicode_locale_id(locale));
  249. VERIFY(locale_id.has_value());
  250. // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
  251. auto extensions = locale_id->remove_extension_type<::Locale::LocaleExtension>();
  252. auto no_extensions_locale = TRY_OR_THROW_OOM(vm, locale_id->to_string());
  253. // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
  254. auto available_locale = best_available_locale(no_extensions_locale);
  255. // c. If availableLocale is not undefined, then
  256. if (available_locale.has_value()) {
  257. // i. Set result.[[locale]] to availableLocale.
  258. result.locale = TRY_OR_THROW_OOM(vm, String::from_utf8(*available_locale));
  259. // ii. If locale and noExtensionsLocale are not the same String value, then
  260. if (locale != no_extensions_locale) {
  261. // 1. Let extension be the String value consisting of the substring of the Unicode locale extension sequence within locale.
  262. // 2. Set result.[[extension]] to extension.
  263. result.extensions.extend(move(extensions));
  264. }
  265. // iii. Return result.
  266. return result;
  267. }
  268. }
  269. // 3. Let defLocale be ! DefaultLocale().
  270. // 4. Set result.[[locale]] to defLocale.
  271. result.locale = TRY_OR_THROW_OOM(vm, String::from_utf8(::Locale::default_locale()));
  272. // 5. Return result.
  273. return result;
  274. }
  275. // 9.2.4 BestFitMatcher ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-bestfitmatcher
  276. static ThrowCompletionOr<MatcherResult> best_fit_matcher(VM& vm, Vector<String> const& requested_locales)
  277. {
  278. // The algorithm is implementation dependent, but should produce results that a typical user of the requested locales would
  279. // perceive as at least as good as those produced by the LookupMatcher abstract operation.
  280. return lookup_matcher(vm, requested_locales);
  281. }
  282. // 9.2.6 InsertUnicodeExtensionAndCanonicalize ( locale, extension ), https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize
  283. ThrowCompletionOr<String> insert_unicode_extension_and_canonicalize(VM& vm, ::Locale::LocaleID locale, ::Locale::LocaleExtension extension)
  284. {
  285. // Note: This implementation differs from the spec in how the extension is inserted. The spec assumes
  286. // the input to this method is a string, and is written such that operations are performed on parts
  287. // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
  288. // structure directly.
  289. TRY_OR_THROW_OOM(vm, locale.extensions.try_append(move(extension)));
  290. return canonicalize_unicode_locale_id(vm, locale);
  291. }
  292. template<typename T>
  293. static auto& find_key_in_value(T& value, StringView key)
  294. {
  295. if (key == "ca"sv)
  296. return value.ca;
  297. if (key == "co"sv)
  298. return value.co;
  299. if (key == "hc"sv)
  300. return value.hc;
  301. if (key == "kf"sv)
  302. return value.kf;
  303. if (key == "kn"sv)
  304. return value.kn;
  305. if (key == "nu"sv)
  306. return value.nu;
  307. // If you hit this point, you must add any missing keys from [[RelevantExtensionKeys]] to LocaleOptions and LocaleResult.
  308. VERIFY_NOT_REACHED();
  309. }
  310. // 9.2.7 ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData ), https://tc39.es/ecma402/#sec-resolvelocale
  311. ThrowCompletionOr<LocaleResult> resolve_locale(VM& vm, Vector<String> const& requested_locales, LocaleOptions const& options, Span<StringView const> relevant_extension_keys)
  312. {
  313. // 1. Let matcher be options.[[localeMatcher]].
  314. auto const& matcher = options.locale_matcher;
  315. MatcherResult matcher_result;
  316. // 2. If matcher is "lookup", then
  317. if (matcher.is_string() && (TRY(matcher.as_string().utf8_string_view()) == "lookup"sv)) {
  318. // a. Let r be ! LookupMatcher(availableLocales, requestedLocales).
  319. // NOTE: We TRY this operation only to propagate OOM errors.
  320. matcher_result = TRY(lookup_matcher(vm, requested_locales));
  321. }
  322. // 3. Else,
  323. else {
  324. // a. Let r be ! BestFitMatcher(availableLocales, requestedLocales).
  325. // NOTE: We TRY this operation only to propagate OOM errors.
  326. matcher_result = TRY(best_fit_matcher(vm, requested_locales));
  327. }
  328. // 4. Let foundLocale be r.[[locale]].
  329. auto found_locale = move(matcher_result.locale);
  330. // 5. Let result be a new Record.
  331. LocaleResult result {};
  332. // 6. Set result.[[dataLocale]] to foundLocale.
  333. result.data_locale = found_locale;
  334. // 7. If r has an [[extension]] field, then
  335. Vector<::Locale::Keyword> keywords;
  336. for (auto& extension : matcher_result.extensions) {
  337. if (!extension.has<::Locale::LocaleExtension>())
  338. continue;
  339. // a. Let components be ! UnicodeExtensionComponents(r.[[extension]]).
  340. auto& components = extension.get<::Locale::LocaleExtension>();
  341. // b. Let keywords be components.[[Keywords]].
  342. keywords = move(components.keywords);
  343. break;
  344. }
  345. // 8. Let supportedExtension be "-u".
  346. ::Locale::LocaleExtension supported_extension {};
  347. // 9. For each element key of relevantExtensionKeys, do
  348. for (auto const& key : relevant_extension_keys) {
  349. // a. Let foundLocaleData be localeData.[[<foundLocale>]].
  350. // b. Assert: Type(foundLocaleData) is Record.
  351. // c. Let keyLocaleData be foundLocaleData.[[<key>]].
  352. // d. Assert: Type(keyLocaleData) is List.
  353. auto key_locale_data = ::Locale::get_available_keyword_values(key);
  354. // e. Let value be keyLocaleData[0].
  355. // f. Assert: Type(value) is either String or Null.
  356. // NOTE: ECMA-402 assumes keyLocaleData is sorted by locale preference. Our list is sorted
  357. // alphabetically, so we get the locale's preferred value from LibUnicode.
  358. Optional<String> value;
  359. if (auto preference = ::Locale::get_preferred_keyword_value_for_locale(found_locale, key); preference.has_value())
  360. value = TRY_OR_THROW_OOM(vm, String::from_utf8(*preference));
  361. // g. Let supportedExtensionAddition be "".
  362. Optional<::Locale::Keyword> supported_extension_addition {};
  363. // h. If r has an [[extension]] field, then
  364. for (auto& entry : keywords) {
  365. // i. If keywords contains an element whose [[Key]] is the same as key, then
  366. if (entry.key != key)
  367. continue;
  368. // 1. Let entry be the element of keywords whose [[Key]] is the same as key.
  369. // 2. Let requestedValue be entry.[[Value]].
  370. auto requested_value = entry.value;
  371. // 3. If requestedValue is not the empty String, then
  372. if (!requested_value.is_empty()) {
  373. // a. If keyLocaleData contains requestedValue, then
  374. if (key_locale_data.contains_slow(requested_value)) {
  375. // i. Let value be requestedValue.
  376. value = move(requested_value);
  377. // ii. Let supportedExtensionAddition be the string-concatenation of "-", key, "-", and value.
  378. supported_extension_addition = ::Locale::Keyword { TRY_OR_THROW_OOM(vm, String::from_utf8(key)), move(entry.value) };
  379. }
  380. }
  381. // 4. Else if keyLocaleData contains "true", then
  382. else if (key_locale_data.contains_slow("true"sv)) {
  383. // a. Let value be "true".
  384. value = TRY_OR_THROW_OOM(vm, String::from_utf8("true"sv));
  385. // b. Let supportedExtensionAddition be the string-concatenation of "-" and key.
  386. supported_extension_addition = ::Locale::Keyword { TRY_OR_THROW_OOM(vm, String::from_utf8(key)), {} };
  387. }
  388. break;
  389. }
  390. // i. If options has a field [[<key>]], then
  391. // i. Let optionsValue be options.[[<key>]].
  392. // ii. Assert: Type(optionsValue) is either String, Undefined, or Null.
  393. auto options_value = find_key_in_value(options, key);
  394. // iii. If Type(optionsValue) is String, then
  395. if (options_value.has_value()) {
  396. // 1. Let optionsValue be the string optionsValue after performing the algorithm steps to transform Unicode extension values to canonical syntax per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions.
  397. // 2. Let optionsValue be the string optionsValue after performing the algorithm steps to replace Unicode extension values with their canonical form per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale Identifiers, treating key as ukey and optionsValue as uvalue productions.
  398. TRY_OR_THROW_OOM(vm, ::Locale::canonicalize_unicode_extension_values(key, *options_value, true));
  399. // 3. If optionsValue is the empty String, then
  400. if (options_value->is_empty()) {
  401. // a. Let optionsValue be "true".
  402. options_value = TRY_OR_THROW_OOM(vm, String::from_utf8("true"sv));
  403. }
  404. }
  405. // iv. If SameValue(optionsValue, value) is false and keyLocaleData contains optionsValue, then
  406. if (options_value.has_value() && (options_value != value) && key_locale_data.contains_slow(*options_value)) {
  407. // 1. Let value be optionsValue.
  408. value = move(options_value);
  409. // 2. Let supportedExtensionAddition be "".
  410. supported_extension_addition.clear();
  411. }
  412. // j. Set result.[[<key>]] to value.
  413. find_key_in_value(result, key) = move(value);
  414. // k. Set supportedExtension to the string-concatenation of supportedExtension and supportedExtensionAddition.
  415. if (supported_extension_addition.has_value())
  416. TRY_OR_THROW_OOM(vm, supported_extension.keywords.try_append(supported_extension_addition.release_value()));
  417. }
  418. // 10. If supportedExtension is not "-u", then
  419. if (!supported_extension.keywords.is_empty()) {
  420. auto locale_id = TRY_OR_THROW_OOM(vm, ::Locale::parse_unicode_locale_id(found_locale));
  421. VERIFY(locale_id.has_value());
  422. // a. Set foundLocale to InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension).
  423. found_locale = TRY(insert_unicode_extension_and_canonicalize(vm, locale_id.release_value(), move(supported_extension)));
  424. }
  425. // 11. Set result.[[locale]] to foundLocale.
  426. result.locale = move(found_locale);
  427. // 12. Return result.
  428. return result;
  429. }
  430. // 9.2.8 LookupSupportedLocales ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupsupportedlocales
  431. static ThrowCompletionOr<Vector<String>> lookup_supported_locales(VM& vm, Vector<String> const& requested_locales)
  432. {
  433. // 1. Let subset be a new empty List.
  434. Vector<String> subset;
  435. // 2. For each element locale of requestedLocales, do
  436. for (auto const& locale : requested_locales) {
  437. auto locale_id = TRY_OR_THROW_OOM(vm, ::Locale::parse_unicode_locale_id(locale));
  438. VERIFY(locale_id.has_value());
  439. // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale extension sequences removed.
  440. locale_id->remove_extension_type<::Locale::LocaleExtension>();
  441. auto no_extensions_locale = TRY_OR_THROW_OOM(vm, locale_id->to_string());
  442. // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
  443. auto available_locale = best_available_locale(no_extensions_locale);
  444. // c. If availableLocale is not undefined, append locale to the end of subset.
  445. if (available_locale.has_value())
  446. TRY_OR_THROW_OOM(vm, subset.try_append(locale));
  447. }
  448. // 3. Return subset.
  449. return subset;
  450. }
  451. // 9.2.9 BestFitSupportedLocales ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-bestfitsupportedlocales
  452. static ThrowCompletionOr<Vector<String>> best_fit_supported_locales(VM& vm, Vector<String> const& requested_locales)
  453. {
  454. // The BestFitSupportedLocales abstract operation returns the subset of the provided BCP 47
  455. // language priority list requestedLocales for which availableLocales has a matching locale
  456. // when using the Best Fit Matcher algorithm. Locales appear in the same order in the returned
  457. // list as in requestedLocales. The steps taken are implementation dependent.
  458. // :yakbrain:
  459. return lookup_supported_locales(vm, requested_locales);
  460. }
  461. // 9.2.10 SupportedLocales ( availableLocales, requestedLocales, options ), https://tc39.es/ecma402/#sec-supportedlocales
  462. ThrowCompletionOr<Array*> supported_locales(VM& vm, Vector<String> const& requested_locales, Value options)
  463. {
  464. auto& realm = *vm.current_realm();
  465. // 1. Set options to ? CoerceOptionsToObject(options).
  466. auto* options_object = TRY(coerce_options_to_object(vm, options));
  467. // 2. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
  468. auto matcher = TRY(get_option(vm, *options_object, vm.names.localeMatcher, OptionType::String, { "lookup"sv, "best fit"sv }, "best fit"sv));
  469. Vector<String> supported_locales;
  470. // 3. If matcher is "best fit", then
  471. if (TRY(matcher.as_string().utf8_string_view()) == "best fit"sv) {
  472. // a. Let supportedLocales be BestFitSupportedLocales(availableLocales, requestedLocales).
  473. supported_locales = TRY(best_fit_supported_locales(vm, requested_locales));
  474. }
  475. // 4. Else,
  476. else {
  477. // a. Let supportedLocales be LookupSupportedLocales(availableLocales, requestedLocales).
  478. supported_locales = TRY(lookup_supported_locales(vm, requested_locales));
  479. }
  480. // 5. Return CreateArrayFromList(supportedLocales).
  481. return Array::create_from<String>(realm, supported_locales, [&vm](auto& locale) { return PrimitiveString::create(vm, move(locale)); }).ptr();
  482. }
  483. // 9.2.12 CoerceOptionsToObject ( options ), https://tc39.es/ecma402/#sec-coerceoptionstoobject
  484. ThrowCompletionOr<Object*> coerce_options_to_object(VM& vm, Value options)
  485. {
  486. auto& realm = *vm.current_realm();
  487. // 1. If options is undefined, then
  488. if (options.is_undefined()) {
  489. // a. Return OrdinaryObjectCreate(null).
  490. return Object::create(realm, nullptr).ptr();
  491. }
  492. // 2. Return ? ToObject(options).
  493. return TRY(options.to_object(vm));
  494. }
  495. // NOTE: 9.2.13 GetOption has been removed and is being pulled in from ECMA-262 in the Temporal proposal.
  496. // 1.2.12 GetStringOrBooleanOption ( options, property, values, trueValue, falsyValue, fallback ), https://tc39.es/proposal-intl-numberformat-v3/out/negotiation/proposed.html#sec-getstringorbooleanoption
  497. ThrowCompletionOr<StringOrBoolean> get_string_or_boolean_option(VM& vm, Object const& options, PropertyKey const& property, Span<StringView const> values, StringOrBoolean true_value, StringOrBoolean falsy_value, StringOrBoolean fallback)
  498. {
  499. // 1. Let value be ? Get(options, property).
  500. auto value = TRY(options.get(property));
  501. // 2. If value is undefined, return fallback.
  502. if (value.is_undefined())
  503. return fallback;
  504. // 3. If value is true, return trueValue.
  505. if (value.is_boolean() && value.as_bool())
  506. return true_value;
  507. // 4. Let valueBoolean be ToBoolean(value).
  508. auto value_boolean = value.to_boolean();
  509. // 5. If valueBoolean is false, return falsyValue.
  510. if (!value_boolean)
  511. return falsy_value;
  512. // 6. Let value be ? ToString(value).
  513. auto value_string = TRY(value.to_string(vm));
  514. // 7. NOTE: For historical reasons, the strings "true" and "false" are treated the same as the boolean value true.
  515. // 8. If value is "true" or "false", return fallback.
  516. if (value_string.is_one_of("true"sv, "false"sv))
  517. return fallback;
  518. // 9. If values does not contain an element equal to value, throw a RangeError exception.
  519. auto it = find(values.begin(), values.end(), value_string.bytes_as_string_view());
  520. if (it == values.end())
  521. return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value_string, property.as_string());
  522. // 10. Return value.
  523. return StringOrBoolean { *it };
  524. }
  525. // 9.2.14 DefaultNumberOption ( value, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-defaultnumberoption
  526. ThrowCompletionOr<Optional<int>> default_number_option(VM& vm, Value value, int minimum, int maximum, Optional<int> fallback)
  527. {
  528. // 1. If value is undefined, return fallback.
  529. if (value.is_undefined())
  530. return fallback;
  531. // 2. Set value to ? ToNumber(value).
  532. value = TRY(value.to_number(vm));
  533. // 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
  534. if (value.is_nan() || (value.as_double() < minimum) || (value.as_double() > maximum))
  535. return vm.throw_completion<RangeError>(ErrorType::IntlNumberIsNaNOrOutOfRange, value, minimum, maximum);
  536. // 4. Return floor(value).
  537. return floor(value.as_double());
  538. }
  539. // 9.2.15 GetNumberOption ( options, property, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-getnumberoption
  540. ThrowCompletionOr<Optional<int>> get_number_option(VM& vm, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional<int> fallback)
  541. {
  542. // 1. Assert: Type(options) is Object.
  543. // 2. Let value be ? Get(options, property).
  544. auto value = TRY(options.get(property));
  545. // 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback).
  546. return default_number_option(vm, value, minimum, maximum, move(fallback));
  547. }
  548. // 9.2.16 PartitionPattern ( pattern ), https://tc39.es/ecma402/#sec-partitionpattern
  549. ThrowCompletionOr<Vector<PatternPartition>> partition_pattern(VM& vm, StringView pattern)
  550. {
  551. // 1. Let result be a new empty List.
  552. Vector<PatternPartition> result;
  553. // 2. Let beginIndex be StringIndexOf(pattern, "{", 0).
  554. auto begin_index = pattern.find('{', 0);
  555. // 3. Let endIndex be 0.
  556. size_t end_index = 0;
  557. // 4. Let nextIndex be 0.
  558. size_t next_index = 0;
  559. // 5. Let length be the number of code units in pattern.
  560. // 6. Repeat, while beginIndex is an integer index into pattern,
  561. while (begin_index.has_value()) {
  562. // a. Set endIndex to StringIndexOf(pattern, "}", beginIndex).
  563. end_index = pattern.find('}', *begin_index).value();
  564. // b. Assert: endIndex is greater than beginIndex.
  565. VERIFY(end_index > *begin_index);
  566. // c. If beginIndex is greater than nextIndex, then
  567. if (*begin_index > next_index) {
  568. // i. Let literal be a substring of pattern from position nextIndex, inclusive, to position beginIndex, exclusive.
  569. auto literal = pattern.substring_view(next_index, *begin_index - next_index);
  570. // ii. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result.
  571. TRY_OR_THROW_OOM(vm, result.try_append({ "literal"sv, literal }));
  572. }
  573. // d. Let p be the substring of pattern from position beginIndex, exclusive, to position endIndex, exclusive.
  574. auto partition = pattern.substring_view(*begin_index + 1, end_index - *begin_index - 1);
  575. // e. Append a new Record { [[Type]]: p, [[Value]]: undefined } as the last element of the list result.
  576. TRY_OR_THROW_OOM(vm, result.try_append({ partition, {} }));
  577. // f. Set nextIndex to endIndex + 1.
  578. next_index = end_index + 1;
  579. // g. Set beginIndex to StringIndexOf(pattern, "{", nextIndex).
  580. begin_index = pattern.find('{', next_index);
  581. }
  582. // 7. If nextIndex is less than length, then
  583. if (next_index < pattern.length()) {
  584. // a. Let literal be the substring of pattern from position nextIndex, inclusive, to position length, exclusive.
  585. auto literal = pattern.substring_view(next_index);
  586. // b. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result.
  587. TRY_OR_THROW_OOM(vm, result.try_append({ "literal"sv, literal }));
  588. }
  589. // 8. Return result.
  590. return result;
  591. }
  592. }