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