AbstractOperations.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. /*
  2. * Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/AllOf.h>
  7. #include <AK/CharacterTypes.h>
  8. #include <AK/Find.h>
  9. #include <AK/QuickSort.h>
  10. #include <AK/TypeCasts.h>
  11. #include <LibJS/Runtime/Array.h>
  12. #include <LibJS/Runtime/GlobalObject.h>
  13. #include <LibJS/Runtime/Intl/AbstractOperations.h>
  14. #include <LibJS/Runtime/Intl/Locale.h>
  15. #include <LibJS/Runtime/Intl/SingleUnitIdentifiers.h>
  16. #include <LibJS/Runtime/VM.h>
  17. #include <LibJS/Runtime/ValueInlines.h>
  18. #include <LibUnicode/TimeZone.h>
  19. #include <LibUnicode/UnicodeKeywords.h>
  20. namespace JS::Intl {
  21. Optional<LocaleKey> locale_key_from_value(Value value)
  22. {
  23. if (value.is_undefined())
  24. return OptionalNone {};
  25. if (value.is_null())
  26. return Empty {};
  27. if (value.is_string())
  28. return value.as_string().utf8_string();
  29. VERIFY_NOT_REACHED();
  30. }
  31. // 6.2.1 IsStructurallyValidLanguageTag ( locale ), https://tc39.es/ecma402/#sec-isstructurallyvalidlanguagetag
  32. bool is_structurally_valid_language_tag(StringView locale)
  33. {
  34. auto contains_duplicate_variant = [&](auto& variants) {
  35. if (variants.is_empty())
  36. return false;
  37. quick_sort(variants);
  38. for (size_t i = 0; i < variants.size() - 1; ++i) {
  39. if (variants[i].equals_ignoring_case(variants[i + 1]))
  40. return true;
  41. }
  42. return false;
  43. };
  44. // 1. Let lowerLocale be the ASCII-lowercase of locale.
  45. // NOTE: LibUnicode's parsing is case-insensitive.
  46. // 2. If lowerLocale cannot be matched by the unicode_locale_id Unicode locale nonterminal, return false.
  47. auto locale_id = Unicode::parse_unicode_locale_id(locale);
  48. if (!locale_id.has_value())
  49. return false;
  50. // 3. If lowerLocale uses any of the backwards compatibility syntax described in Unicode Technical Standard #35 Part 1 Core,
  51. // Section 3.3 BCP 47 Conformance, return false.
  52. // https://unicode.org/reports/tr35/#BCP_47_Conformance
  53. if (locale.contains('_') || locale_id->language_id.is_root || !locale_id->language_id.language.has_value())
  54. return false;
  55. // 4. Let languageId be the longest prefix of lowerLocale matched by the unicode_language_id Unicode locale nonterminal.
  56. auto& language_id = locale_id->language_id;
  57. // 5. Let variants be GetLocaleVariants(languageId).
  58. // 6. If variants is not undefined, then
  59. if (auto& variants = language_id.variants; !variants.is_empty()) {
  60. // a. If variants contains any duplicate subtags, return false.
  61. if (contains_duplicate_variant(variants))
  62. return false;
  63. }
  64. HashTable<char> unique_keys;
  65. // 7. Let allExtensions be the suffix of lowerLocale following languageId.
  66. // 8. If allExtensions contains a substring matched by the pu_extensions Unicode locale nonterminal, let extensions be
  67. // the prefix of allExtensions preceding the longest such substring. Otherwise, let extensions be allExtensions.
  68. // 9. If extensions is not the empty String, then
  69. for (auto& extension : locale_id->extensions) {
  70. char key = extension.visit(
  71. [](Unicode::LocaleExtension const&) { return 'u'; },
  72. [](Unicode::TransformedExtension const&) { return 't'; },
  73. [](Unicode::OtherExtension const& ext) { return static_cast<char>(to_ascii_lowercase(ext.key)); });
  74. // a. If extensions contains any duplicate singleton subtags, return false.
  75. if (unique_keys.set(key) != HashSetResult::InsertedNewEntry)
  76. return false;
  77. // b. Let transformExtension be the longest substring of extensions matched by the transformed_extensions Unicode
  78. // locale nonterminal. If there is no such substring, return true.
  79. if (auto* transformed = extension.get_pointer<Unicode::TransformedExtension>()) {
  80. // c. Assert: The substring of transformExtension from 0 to 3 is "-t-".
  81. // d. Let tPrefix be the substring of transformExtension from 3.
  82. // e. Let tlang be the longest prefix of tPrefix matched by the tlang Unicode locale nonterminal. If there is
  83. // no such prefix, return true.
  84. auto& transformed_language = transformed->language;
  85. if (!transformed_language.has_value())
  86. continue;
  87. // f. Let tlangRefinements be the longest suffix of tlang following a non-empty prefix matched by the
  88. // unicode_language_subtag Unicode locale nonterminal.
  89. auto& transformed_refinements = transformed_language->variants;
  90. // g. If tlangRefinements contains any duplicate substrings matched greedily by the unicode_variant_subtag
  91. // Unicode locale nonterminal, return false.
  92. if (contains_duplicate_variant(transformed_refinements))
  93. return false;
  94. }
  95. }
  96. // 10. Return true.
  97. return true;
  98. }
  99. // 6.2.2 CanonicalizeUnicodeLocaleId ( locale ), https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid
  100. String canonicalize_unicode_locale_id(StringView locale)
  101. {
  102. return Unicode::canonicalize_unicode_locale_id(locale);
  103. }
  104. // 6.3.1 IsWellFormedCurrencyCode ( currency ), https://tc39.es/ecma402/#sec-iswellformedcurrencycode
  105. bool is_well_formed_currency_code(StringView currency)
  106. {
  107. // 1. If the length of currency is not 3, return false.
  108. if (currency.length() != 3)
  109. return false;
  110. // 2. Let normalized be the ASCII-uppercase of currency.
  111. // 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.
  112. if (!all_of(currency, is_ascii_alpha))
  113. return false;
  114. // 4. Return true.
  115. return true;
  116. }
  117. // 6.5.1 AvailableNamedTimeZoneIdentifiers ( ), https://tc39.es/ecma402/#sup-availablenamedtimezoneidentifiers
  118. Vector<TimeZoneIdentifier> const& available_named_time_zone_identifiers()
  119. {
  120. // It is recommended that the result of AvailableNamedTimeZoneIdentifiers remains the same for the lifetime of the surrounding agent.
  121. static auto named_time_zone_identifiers = []() {
  122. // 1. Let identifiers be a List containing the String value of each Zone or Link name in the IANA Time Zone Database.
  123. auto const& identifiers = Unicode::available_time_zones();
  124. // 2. Assert: No element of identifiers is an ASCII-case-insensitive match for any other element.
  125. // 3. Assert: Every element of identifiers identifies a Zone or Link name in the IANA Time Zone Database.
  126. // 4. Sort identifiers according to lexicographic code unit order.
  127. // NOTE: All of the above is handled by LibUnicode.
  128. // 5. Let result be a new empty List.
  129. Vector<TimeZoneIdentifier> result;
  130. result.ensure_capacity(identifiers.size());
  131. bool found_utc = false;
  132. // 6. For each element identifier of identifiers, do
  133. for (auto const& identifier : identifiers) {
  134. // a. Let primary be identifier.
  135. auto primary = identifier;
  136. // b. If identifier is a Link name and identifier is not "UTC", then
  137. if (identifier != "UTC"sv) {
  138. if (auto resolved = Unicode::resolve_primary_time_zone(identifier); resolved.has_value() && identifier != resolved) {
  139. // i. Set primary to the Zone name that identifier resolves to, according to the rules for resolving Link
  140. // names in the IANA Time Zone Database.
  141. primary = resolved.release_value();
  142. // ii. NOTE: An implementation may need to resolve identifier iteratively.
  143. }
  144. }
  145. // c. If primary is one of "Etc/UTC", "Etc/GMT", or "GMT", set primary to "UTC".
  146. if (primary.is_one_of("Etc/UTC"sv, "Etc/GMT"sv, "GMT"sv))
  147. primary = "UTC"_string;
  148. // d. Let record be the Time Zone Identifier Record { [[Identifier]]: identifier, [[PrimaryIdentifier]]: primary }.
  149. TimeZoneIdentifier record { .identifier = identifier, .primary_identifier = primary };
  150. // e. Append record to result.
  151. result.unchecked_append(move(record));
  152. if (!found_utc && identifier == "UTC"sv && primary == "UTC"sv)
  153. found_utc = true;
  154. }
  155. // 7. Assert: result contains a Time Zone Identifier Record r such that r.[[Identifier]] is "UTC" and r.[[PrimaryIdentifier]] is "UTC".
  156. VERIFY(found_utc);
  157. // 8. Return result.
  158. return result;
  159. }();
  160. return named_time_zone_identifiers;
  161. }
  162. // 6.5.2 GetAvailableNamedTimeZoneIdentifier ( timeZoneIdentifier ), https://tc39.es/ecma402/#sec-getavailablenamedtimezoneidentifier
  163. Optional<TimeZoneIdentifier const&> get_available_named_time_zone_identifier(StringView time_zone_identifier)
  164. {
  165. // 1. For each element record of AvailableNamedTimeZoneIdentifiers(), do
  166. for (auto const& record : available_named_time_zone_identifiers()) {
  167. // a. If record.[[Identifier]] is an ASCII-case-insensitive match for timeZoneIdentifier, return record.
  168. if (record.identifier.equals_ignoring_ascii_case(time_zone_identifier))
  169. return record;
  170. }
  171. // 2. Return EMPTY.
  172. return {};
  173. }
  174. // 6.6.1 IsWellFormedUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-iswellformedunitidentifier
  175. bool is_well_formed_unit_identifier(StringView unit_identifier)
  176. {
  177. // 6.6.2 IsSanctionedSingleUnitIdentifier ( unitIdentifier ), https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier
  178. constexpr auto is_sanctioned_single_unit_identifier = [](StringView unit_identifier) {
  179. // 1. If unitIdentifier is listed in Table 2 below, return true.
  180. // 2. Else, return false.
  181. static constexpr auto sanctioned_units = sanctioned_single_unit_identifiers();
  182. return find(sanctioned_units.begin(), sanctioned_units.end(), unit_identifier) != sanctioned_units.end();
  183. };
  184. // 1. If ! IsSanctionedSingleUnitIdentifier(unitIdentifier) is true, then
  185. if (is_sanctioned_single_unit_identifier(unit_identifier)) {
  186. // a. Return true.
  187. return true;
  188. }
  189. // 2. Let i be StringIndexOf(unitIdentifier, "-per-", 0).
  190. auto indices = unit_identifier.find_all("-per-"sv);
  191. // 3. If i is -1 or StringIndexOf(unitIdentifier, "-per-", i + 1) is not -1, then
  192. if (indices.size() != 1) {
  193. // a. Return false.
  194. return false;
  195. }
  196. // 4. Assert: The five-character substring "-per-" occurs exactly once in unitIdentifier, at index i.
  197. // NOTE: We skip this because the indices vector being of size 1 already verifies this invariant.
  198. // 5. Let numerator be the substring of unitIdentifier from 0 to i.
  199. auto numerator = unit_identifier.substring_view(0, indices[0]);
  200. // 6. Let denominator be the substring of unitIdentifier from i + 5.
  201. auto denominator = unit_identifier.substring_view(indices[0] + 5);
  202. // 7. If ! IsSanctionedSingleUnitIdentifier(numerator) and ! IsSanctionedSingleUnitIdentifier(denominator) are both true, then
  203. if (is_sanctioned_single_unit_identifier(numerator) && is_sanctioned_single_unit_identifier(denominator)) {
  204. // a. Return true.
  205. return true;
  206. }
  207. // 8. Return false.
  208. return false;
  209. }
  210. // 9.2.1 CanonicalizeLocaleList ( locales ), https://tc39.es/ecma402/#sec-canonicalizelocalelist
  211. ThrowCompletionOr<Vector<String>> canonicalize_locale_list(VM& vm, Value locales)
  212. {
  213. auto& realm = *vm.current_realm();
  214. // 1. If locales is undefined, then
  215. if (locales.is_undefined()) {
  216. // a. Return a new empty List.
  217. return Vector<String> {};
  218. }
  219. // 2. Let seen be a new empty List.
  220. Vector<String> seen;
  221. Object* object = nullptr;
  222. // 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then
  223. if (locales.is_string() || (locales.is_object() && is<Locale>(locales.as_object()))) {
  224. // a. Let O be CreateArrayFromList(« locales »).
  225. object = Array::create_from(realm, { locales });
  226. }
  227. // 4. Else,
  228. else {
  229. // a. Let O be ? ToObject(locales).
  230. object = TRY(locales.to_object(vm));
  231. }
  232. // 5. Let len be ? ToLength(? Get(O, "length")).
  233. auto length_value = TRY(object->get(vm.names.length));
  234. auto length = TRY(length_value.to_length(vm));
  235. // 6. Let k be 0.
  236. // 7. Repeat, while k < len,
  237. for (size_t k = 0; k < length; ++k) {
  238. // a. Let Pk be ToString(k).
  239. auto property_key = PropertyKey { k };
  240. // b. Let kPresent be ? HasProperty(O, Pk).
  241. auto key_present = TRY(object->has_property(property_key));
  242. // c. If kPresent is true, then
  243. if (key_present) {
  244. // i. Let kValue be ? Get(O, Pk).
  245. auto key_value = TRY(object->get(property_key));
  246. // ii. If Type(kValue) is not String or Object, throw a TypeError exception.
  247. if (!key_value.is_string() && !key_value.is_object())
  248. return vm.throw_completion<TypeError>(ErrorType::NotAnObjectOrString, key_value);
  249. String tag;
  250. // iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then
  251. if (key_value.is_object() && is<Locale>(key_value.as_object())) {
  252. // 1. Let tag be kValue.[[Locale]].
  253. tag = static_cast<Locale const&>(key_value.as_object()).locale();
  254. }
  255. // iv. Else,
  256. else {
  257. // 1. Let tag be ? ToString(kValue).
  258. tag = TRY(key_value.to_string(vm));
  259. }
  260. // v. If ! IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception.
  261. if (!is_structurally_valid_language_tag(tag))
  262. return vm.throw_completion<RangeError>(ErrorType::IntlInvalidLanguageTag, tag);
  263. // vi. Let canonicalizedTag be ! CanonicalizeUnicodeLocaleId(tag).
  264. auto canonicalized_tag = canonicalize_unicode_locale_id(tag);
  265. // vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen.
  266. if (!seen.contains_slow(canonicalized_tag))
  267. seen.append(move(canonicalized_tag));
  268. }
  269. // d. Increase k by 1.
  270. }
  271. // 8. Return seen.
  272. return seen;
  273. }
  274. // 9.2.3 LookupMatchingLocaleByPrefix ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatchinglocalebyprefix
  275. Optional<MatchedLocale> lookup_matching_locale_by_prefix(ReadonlySpan<String> requested_locales)
  276. {
  277. // 1. For each element locale of requestedLocales, do
  278. for (auto locale : requested_locales) {
  279. auto locale_id = Unicode::parse_unicode_locale_id(locale);
  280. VERIFY(locale_id.has_value());
  281. // a. Let extension be empty.
  282. Optional<Unicode::Extension> extension;
  283. String locale_without_extension;
  284. // b. If locale contains a Unicode locale extension sequence, then
  285. if (auto extensions = locale_id->remove_extension_type<Unicode::LocaleExtension>(); !extensions.is_empty()) {
  286. VERIFY(extensions.size() == 1);
  287. // i. Set extension to the Unicode locale extension sequence of locale.
  288. extension = extensions.take_first();
  289. // ii. Set locale to the String value that is locale with any Unicode locale extension sequences removed.
  290. locale = locale_id->to_string();
  291. }
  292. // c. Let prefix be locale.
  293. StringView prefix { locale };
  294. // d. Repeat, while prefix is not the empty String,
  295. while (!prefix.is_empty()) {
  296. // i. If availableLocales contains prefix, return the Record { [[locale]]: prefix, [[extension]]: extension }.
  297. if (Unicode::is_locale_available(prefix))
  298. return MatchedLocale { MUST(String::from_utf8(prefix)), move(extension) };
  299. // ii. If prefix contains "-" (code unit 0x002D HYPHEN-MINUS), let pos be the index into prefix of the last
  300. // occurrence of "-"; else let pos be 0.
  301. auto position = prefix.find_last('-').value_or(0);
  302. // iii. Repeat, while pos ≥ 2 and the substring of prefix from pos - 2 to pos - 1 is "-",
  303. while (position >= 2 && prefix.substring_view(position - 2, 1) == '-') {
  304. // 1. Set pos to pos - 2.
  305. position -= 2;
  306. }
  307. // iv. Set prefix to the substring of prefix from 0 to pos.
  308. prefix = prefix.substring_view(0, position);
  309. }
  310. }
  311. // 2. Return undefined.
  312. return {};
  313. }
  314. // 9.2.4 LookupMatchingLocaleByBestFit ( availableLocales, requestedLocales ), https://tc39.es/ecma402/#sec-lookupmatchinglocalebybestfit
  315. Optional<MatchedLocale> lookup_matching_locale_by_best_fit(ReadonlySpan<String> requested_locales)
  316. {
  317. // The algorithm is implementation dependent, but should produce results that a typical user of the requested locales
  318. // would consider at least as good as those produced by the LookupMatchingLocaleByPrefix algorithm.
  319. return lookup_matching_locale_by_prefix(requested_locales);
  320. }
  321. // 9.2.6 InsertUnicodeExtensionAndCanonicalize ( locale, attributes, keywords ), https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize
  322. String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale, Vector<String> attributes, Vector<Unicode::Keyword> keywords)
  323. {
  324. // Note: This implementation differs from the spec in how the extension is inserted. The spec assumes
  325. // the input to this method is a string, and is written such that operations are performed on parts
  326. // of that string. LibUnicode gives us the parsed locale in a structure, so we can mutate that
  327. // structure directly.
  328. locale.extensions.append(Unicode::LocaleExtension { move(attributes), move(keywords) });
  329. // 10. Return CanonicalizeUnicodeLocaleId(newLocale).
  330. return JS::Intl::canonicalize_unicode_locale_id(locale.to_string());
  331. }
  332. template<typename T>
  333. static auto& find_key_in_value(T& value, StringView key)
  334. {
  335. if (key == "ca"sv)
  336. return value.ca;
  337. if (key == "co"sv)
  338. return value.co;
  339. if (key == "hc"sv)
  340. return value.hc;
  341. if (key == "kf"sv)
  342. return value.kf;
  343. if (key == "kn"sv)
  344. return value.kn;
  345. if (key == "nu"sv)
  346. return value.nu;
  347. // If you hit this point, you must add any missing keys from [[RelevantExtensionKeys]] to LocaleOptions and ResolvedLocale.
  348. VERIFY_NOT_REACHED();
  349. }
  350. static Vector<LocaleKey> available_keyword_values(StringView locale, StringView key)
  351. {
  352. auto key_locale_data = Unicode::available_keyword_values(locale, key);
  353. Vector<LocaleKey> result;
  354. result.ensure_capacity(key_locale_data.size());
  355. for (auto& keyword : key_locale_data)
  356. result.unchecked_append(move(keyword));
  357. if (key == "hc"sv) {
  358. // https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
  359. // [[LocaleData]].[[<locale>]].[[hc]] must be « null, "h11", "h12", "h23", "h24" ».
  360. result.prepend(Empty {});
  361. }
  362. return result;
  363. }
  364. // 9.2.7 ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData ), https://tc39.es/ecma402/#sec-resolvelocale
  365. ResolvedLocale resolve_locale(ReadonlySpan<String> requested_locales, LocaleOptions const& options, ReadonlySpan<StringView> relevant_extension_keys)
  366. {
  367. static auto true_string = "true"_string;
  368. // 1. Let matcher be options.[[localeMatcher]].
  369. auto const& matcher = options.locale_matcher;
  370. Optional<MatchedLocale> matcher_result;
  371. // 2. If matcher is "lookup", then
  372. if (matcher.is_string() && matcher.as_string().utf8_string_view() == "lookup"sv) {
  373. // a. Let r be LookupMatchingLocaleByPrefix(availableLocales, requestedLocales).
  374. matcher_result = lookup_matching_locale_by_prefix(requested_locales);
  375. }
  376. // 3. Else,
  377. else {
  378. // a. Let r be LookupMatchingLocaleByBestFit(availableLocales, requestedLocales).
  379. matcher_result = lookup_matching_locale_by_best_fit(requested_locales);
  380. }
  381. // 4. If r is undefined, set r to the Record { [[locale]]: DefaultLocale(), [[extension]]: empty }.
  382. if (!matcher_result.has_value())
  383. matcher_result = MatchedLocale { MUST(String::from_utf8(Unicode::default_locale())), {} };
  384. // 5. Let foundLocale be r.[[locale]].
  385. auto found_locale = move(matcher_result->locale);
  386. // 6. Let foundLocaleData be localeData.[[<foundLocale>]].
  387. // 7. Assert: Type(foundLocaleData) is Record.
  388. // 8. Let result be a new Record.
  389. // 9. Set result.[[LocaleData]] to foundLocaleData.
  390. ResolvedLocale result {};
  391. Vector<Unicode::Keyword> keywords;
  392. // 10. If r.[[extension]] is not empty, then
  393. if (matcher_result->extension.has_value()) {
  394. // a. Let components be UnicodeExtensionComponents(r.[[extension]]).
  395. auto& components = matcher_result->extension->get<Unicode::LocaleExtension>();
  396. // b. Let keywords be components.[[Keywords]].
  397. keywords = move(components.keywords);
  398. }
  399. // 11. Else,
  400. // a. Let keywords be a new empty List.
  401. // 12. Let supportedKeywords be a new empty List.
  402. Vector<Unicode::Keyword> supported_keywords;
  403. // 13. For each element key of relevantExtensionKeys, do
  404. for (auto const& key : relevant_extension_keys) {
  405. // a. Let keyLocaleData be foundLocaleData.[[<key>]].
  406. // b. Assert: keyLocaleData is a List.
  407. auto key_locale_data = available_keyword_values(found_locale, key);
  408. // c. Let value be keyLocaleData[0].
  409. // d. Assert: value is a String or value is null.
  410. auto value = key_locale_data[0];
  411. // e. Let supportedKeyword be empty.
  412. Optional<Unicode::Keyword> supported_keyword;
  413. // f. If keywords contains an element whose [[Key]] is key, then
  414. if (auto entry = keywords.find_if([&](auto const& entry) { return entry.key == key; }); entry != keywords.end()) {
  415. // i. Let entry be the element of keywords whose [[Key]] is key.
  416. // ii. Let requestedValue be entry.[[Value]].
  417. auto requested_value = entry->value;
  418. // iii. If requestedValue is not the empty String, then
  419. if (!requested_value.is_empty()) {
  420. // 1. If keyLocaleData contains requestedValue, then
  421. if (key_locale_data.contains_slow(requested_value)) {
  422. // a. Set value to requestedValue.
  423. value = move(requested_value);
  424. // b. Set supportedKeyword to the Record { [[Key]]: key, [[Value]]: value }.
  425. supported_keyword = Unicode::Keyword { MUST(String::from_utf8(key)), move(entry->value) };
  426. }
  427. }
  428. // iv. Else if keyLocaleData contains "true", then
  429. else if (key_locale_data.contains_slow(true_string)) {
  430. // 1. Set value to "true".
  431. value = true_string;
  432. // 2. Set supportedKeyword to the Record { [[Key]]: key, [[Value]]: "" }.
  433. supported_keyword = Unicode::Keyword { MUST(String::from_utf8(key)), {} };
  434. }
  435. }
  436. // g. Assert: options has a field [[<key>]].
  437. // h. Let optionsValue be options.[[<key>]].
  438. // i. Assert: optionsValue is a String, or optionsValue is either undefined or null.
  439. auto options_value = find_key_in_value(options, key);
  440. // j. If optionsValue is a String, then
  441. if (auto* options_string = options_value.has_value() ? options_value->get_pointer<String>() : nullptr) {
  442. // i. Let ukey be the ASCII-lowercase of key.
  443. // NOTE: `key` is always lowercase, and this step is likely to be removed:
  444. // https://github.com/tc39/ecma402/pull/846#discussion_r1428263375
  445. // ii. Set optionsValue to CanonicalizeUValue(ukey, optionsValue).
  446. *options_string = Unicode::canonicalize_unicode_extension_values(key, *options_string);
  447. // iii. If optionsValue is the empty String, then
  448. if (options_string->is_empty()) {
  449. // 1. Set optionsValue to "true".
  450. *options_string = true_string;
  451. }
  452. }
  453. // k. If SameValue(optionsValue, value) is false and keyLocaleData contains optionsValue, then
  454. if (options_value.has_value() && (options_value != value) && key_locale_data.contains_slow(*options_value)) {
  455. // i. Set value to optionsValue.
  456. value = options_value.release_value();
  457. // ii. Set supportedKeyword to empty.
  458. supported_keyword.clear();
  459. }
  460. // l. If supportedKeyword is not empty, append supportedKeyword to supportedKeywords.
  461. if (supported_keyword.has_value())
  462. supported_keywords.append(supported_keyword.release_value());
  463. // m. Set result.[[<key>]] to value.
  464. find_key_in_value(result, key) = move(value);
  465. }
  466. // 14. If supportedKeywords is not empty, then
  467. if (!supported_keywords.is_empty()) {
  468. auto locale_id = Unicode::parse_unicode_locale_id(found_locale);
  469. VERIFY(locale_id.has_value());
  470. // a. Let supportedAttributes be a new empty List.
  471. // b. Set foundLocale to InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedAttributes, supportedKeywords).
  472. found_locale = insert_unicode_extension_and_canonicalize(locale_id.release_value(), {}, move(supported_keywords));
  473. }
  474. // 15. Set result.[[Locale]] to foundLocale.
  475. result.locale = move(found_locale);
  476. // 16. Return result.
  477. return result;
  478. }
  479. // 9.2.8 FilterLocales ( availableLocales, requestedLocales, options ), https://tc39.es/ecma402/#sec-lookupsupportedlocales
  480. ThrowCompletionOr<Array*> filter_locales(VM& vm, ReadonlySpan<String> requested_locales, Value options)
  481. {
  482. auto& realm = *vm.current_realm();
  483. // 1. Set options to ? CoerceOptionsToObject(options).
  484. auto* options_object = TRY(coerce_options_to_object(vm, options));
  485. // 2. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
  486. auto matcher = TRY(get_option(vm, *options_object, vm.names.localeMatcher, OptionType::String, { "lookup"sv, "best fit"sv }, "best fit"sv));
  487. // 3. Let subset be a new empty List.
  488. Vector<String> subset;
  489. // 4. For each element locale of requestedLocales, do
  490. for (auto const& locale : requested_locales) {
  491. Optional<MatchedLocale> match;
  492. // a. If matcher is "lookup", then
  493. if (matcher.as_string().utf8_string_view() == "lookup"sv) {
  494. // i. Let match be LookupMatchingLocaleByPrefix(availableLocales, « locale »).
  495. match = lookup_matching_locale_by_prefix({ { locale } });
  496. }
  497. // b. Else,
  498. else {
  499. // i. Let match be LookupMatchingLocaleByBestFit(availableLocales, « locale »).
  500. match = lookup_matching_locale_by_best_fit({ { locale } });
  501. }
  502. // c. If match is not undefined, append locale to subset.
  503. if (match.has_value())
  504. subset.append(locale);
  505. }
  506. // 5. Return CreateArrayFromList(subset).
  507. return Array::create_from<String>(realm, subset, [&vm](auto& locale) { return PrimitiveString::create(vm, move(locale)); }).ptr();
  508. }
  509. // 9.2.10 CoerceOptionsToObject ( options ), https://tc39.es/ecma402/#sec-coerceoptionstoobject
  510. ThrowCompletionOr<Object*> coerce_options_to_object(VM& vm, Value options)
  511. {
  512. auto& realm = *vm.current_realm();
  513. // 1. If options is undefined, then
  514. if (options.is_undefined()) {
  515. // a. Return OrdinaryObjectCreate(null).
  516. return Object::create(realm, nullptr).ptr();
  517. }
  518. // 2. Return ? ToObject(options).
  519. return TRY(options.to_object(vm)).ptr();
  520. }
  521. // NOTE: 9.2.11 GetOption has been removed and is being pulled in from ECMA-262 in the Temporal proposal.
  522. // 9.2.12 GetBooleanOrStringNumberFormatOption ( options, property, stringValues, fallback ), https://tc39.es/ecma402/#sec-getbooleanorstringnumberformatoption
  523. ThrowCompletionOr<StringOrBoolean> get_boolean_or_string_number_format_option(VM& vm, Object const& options, PropertyKey const& property, ReadonlySpan<StringView> string_values, StringOrBoolean fallback)
  524. {
  525. // 1. Let value be ? Get(options, property).
  526. auto value = TRY(options.get(property));
  527. // 2. If value is undefined, return fallback.
  528. if (value.is_undefined())
  529. return fallback;
  530. // 3. If value is true, return true.
  531. if (value.is_boolean() && value.as_bool())
  532. return StringOrBoolean { true };
  533. // 4. If ToBoolean(value) is false, return false.
  534. if (!value.to_boolean())
  535. return StringOrBoolean { false };
  536. // 5. Let value be ? ToString(value).
  537. auto value_string = TRY(value.to_string(vm));
  538. // 6. If stringValues does not contain value, throw a RangeError exception.
  539. auto it = find(string_values.begin(), string_values.end(), value_string.bytes_as_string_view());
  540. if (it == string_values.end())
  541. return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, value_string, property.as_string());
  542. // 7. Return value.
  543. return StringOrBoolean { *it };
  544. }
  545. // 9.2.13 DefaultNumberOption ( value, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-defaultnumberoption
  546. ThrowCompletionOr<Optional<int>> default_number_option(VM& vm, Value value, int minimum, int maximum, Optional<int> fallback)
  547. {
  548. // 1. If value is undefined, return fallback.
  549. if (value.is_undefined())
  550. return fallback;
  551. // 2. Set value to ? ToNumber(value).
  552. value = TRY(value.to_number(vm));
  553. // 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception.
  554. if (value.is_nan() || (value.as_double() < minimum) || (value.as_double() > maximum))
  555. return vm.throw_completion<RangeError>(ErrorType::IntlNumberIsNaNOrOutOfRange, value, minimum, maximum);
  556. // 4. Return floor(value).
  557. return floor(value.as_double());
  558. }
  559. // 9.2.14 GetNumberOption ( options, property, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-getnumberoption
  560. ThrowCompletionOr<Optional<int>> get_number_option(VM& vm, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional<int> fallback)
  561. {
  562. // 1. Assert: Type(options) is Object.
  563. // 2. Let value be ? Get(options, property).
  564. auto value = TRY(options.get(property));
  565. // 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback).
  566. return default_number_option(vm, value, minimum, maximum, move(fallback));
  567. }
  568. }