From 207319ecf1d5b6ba6e5c0df820bb542e9c2dce92 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 5 Sep 2021 15:27:11 -0400 Subject: [PATCH] LibJS: Implement ECMA-402 String.prototype.toLocale{Lower,Upper}Case --- .../LibJS/Runtime/Intl/AbstractOperations.cpp | 2 +- .../LibJS/Runtime/Intl/AbstractOperations.h | 1 + .../LibJS/Runtime/StringPrototype.cpp | 59 +++++++++++++++++-- .../String.prototype.toLocaleLowerCase.js | 21 +++++++ .../String.prototype.toLocaleUpperCase.js | 15 +++++ 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index 1f2d7a0774..0cb86bdaca 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -233,7 +233,7 @@ Vector canonicalize_locale_list(GlobalObject& global_object, Value local } // 9.2.2 BestAvailableLocale ( availableLocales, locale ), https://tc39.es/ecma402/#sec-bestavailablelocale -static Optional best_available_locale(StringView const& locale) +Optional best_available_locale(StringView const& locale) { // 1. Let candidate be locale. StringView candidate = locale; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index 03a4853fad..3f089ff33c 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -29,6 +29,7 @@ struct LocaleResult { Optional is_structurally_valid_language_tag(StringView locale); String canonicalize_unicode_locale_id(Unicode::LocaleID& locale); Vector canonicalize_locale_list(GlobalObject&, Value locales); +Optional best_available_locale(StringView const& locale); Vector best_fit_supported_locales(Vector const& requested_locales); Vector lookup_supported_locales(Vector const& requested_locales); Array* supported_locales(GlobalObject&, Vector const& requested_locales, Value options); diff --git a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp index 0d48cd9b3b..7321180343 100644 --- a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include namespace JS { @@ -372,27 +374,72 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::index_of) return index.has_value() ? Value(*index) : Value(-1); } -// 22.1.3.24 String.prototype.toLocaleLowerCase ( [ reserved1 [ , reserved2 ] ] ), https://tc39.es/ecma262/#sec-string.prototype.tolocalelowercase -// NOTE: This is the minimum toLocaleLowerCase implementation for engines without ECMA-402. +static Optional resolve_best_locale(GlobalObject& global_object, Value locales) +{ + // For details on these steps, see https://tc39.es/ecma402/#sup-string.prototype.tolocalelowercase + auto& vm = global_object.vm(); + + // 3. Let requestedLocales be ? CanonicalizeLocaleList(locales). + auto requested_locales = Intl::canonicalize_locale_list(global_object, locales); + if (vm.exception()) + return {}; + + Optional requested_locale; + + // 4. If requestedLocales is not an empty List, then + if (!requested_locales.is_empty()) { + // a. Let requestedLocale be requestedLocales[0]. + requested_locale = Unicode::parse_unicode_locale_id(requested_locales[0]); + } + // 5. Else, + else { + // a. Let requestedLocale be DefaultLocale(). + requested_locale = Unicode::parse_unicode_locale_id(Unicode::default_locale()); + } + VERIFY(requested_locale.has_value()); + + // 6. Let noExtensionsLocale be the String value that is requestedLocale with any Unicode locale extension sequences (6.2.1) removed. + requested_locale->remove_extension_type(); + auto no_extensions_locale = requested_locale->to_string(); + + // 7. Let availableLocales be a List with language tags that includes the languages for which the Unicode Character Database contains language sensitive case mappings. Implementations may add additional language tags if they support case mapping for additional locales. + // 8. Let locale be BestAvailableLocale(availableLocales, noExtensionsLocale). + auto locale = Intl::best_available_locale(no_extensions_locale); + + // 9. If locale is undefined, let locale be "und". + if (!locale.has_value()) + locale = "und"sv; + + return locale; +} + +// 18.1.2 String.prototype.toLocaleLowerCase ( [ locales ] ), https://tc39.es/ecma402/#sup-string.prototype.tolocalelowercase JS_DEFINE_NATIVE_FUNCTION(StringPrototype::to_locale_lowercase) { auto string = ak_string_from(vm, global_object); if (!string.has_value()) return {}; - auto lowercase = Unicode::to_unicode_lowercase_full(*string); + auto locale = resolve_best_locale(global_object, vm.argument(0)); + if (!locale.has_value()) + return {}; + + auto lowercase = Unicode::to_unicode_lowercase_full(*string, *locale); return js_string(vm, move(lowercase)); } -// 22.1.3.25 String.prototype.toLocaleUpperCase ( [ reserved1 [ , reserved2 ] ] ), https://tc39.es/ecma262/#sec-string.prototype.tolocaleuppercase -// NOTE: This is the minimum toLocaleUpperCase implementation for engines without ECMA-402. +// 18.1.3 String.prototype.toLocaleUpperCase ( [ locales ] ), https://tc39.es/ecma402/#sup-string.prototype.tolocaleuppercase JS_DEFINE_NATIVE_FUNCTION(StringPrototype::to_locale_uppercase) { auto string = ak_string_from(vm, global_object); if (!string.has_value()) return {}; - auto uppercase = Unicode::to_unicode_uppercase_full(*string); + auto locale = resolve_best_locale(global_object, vm.argument(0)); + if (!locale.has_value()) + return {}; + + auto uppercase = Unicode::to_unicode_uppercase_full(*string, *locale); return js_string(vm, move(uppercase)); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleLowerCase.js b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleLowerCase.js index 730cb31aec..941944cbb4 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleLowerCase.js +++ b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleLowerCase.js @@ -25,4 +25,25 @@ test("special case folding", () => { expect("\u1FB7".toLocaleLowerCase()).toBe("\u1FB7"); expect("\u1FC7".toLocaleLowerCase()).toBe("\u1FC7"); expect("\u1FF7".toLocaleLowerCase()).toBe("\u1FF7"); + + expect("I".toLocaleLowerCase()).toBe("i"); + expect("I".toLocaleLowerCase("az")).toBe("\u0131"); + expect("I".toLocaleLowerCase("tr")).toBe("\u0131"); + + expect("\u0130".toLocaleLowerCase()).toBe("\u0069\u0307"); + expect("\u0130".toLocaleLowerCase("az")).toBe("i"); + expect("\u0130".toLocaleLowerCase("tr")).toBe("i"); + + expect("I\u0307".toLocaleLowerCase()).toBe("i\u0307"); + expect("I\u0307".toLocaleLowerCase("az")).toBe("i"); + expect("I\u0307".toLocaleLowerCase("tr")).toBe("i"); + + expect("\u012e".toLocaleLowerCase()).toBe("\u012f"); + expect("\u012e".toLocaleLowerCase("lt")).toBe("\u012f"); + + expect("\u012e\u0300".toLocaleLowerCase()).toBe("\u012f\u0300"); + expect("\u012e\u0300".toLocaleLowerCase("lt")).toBe("\u012f\u0307\u0300"); + + expect("\u012e\u0300".toLocaleLowerCase(["en", "lt"])).toBe("\u012f\u0300"); + expect("\u012e\u0300".toLocaleLowerCase(["lt", "en"])).toBe("\u012f\u0307\u0300"); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleUpperCase.js b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleUpperCase.js index ec70301b74..39e992df6e 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleUpperCase.js +++ b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.toLocaleUpperCase.js @@ -27,4 +27,19 @@ test("special case folding", () => { expect("\u1FB7".toLocaleUpperCase()).toBe("\u0391\u0342\u0399"); expect("\u1FC7".toLocaleUpperCase()).toBe("\u0397\u0342\u0399"); expect("\u1FF7".toLocaleUpperCase()).toBe("\u03A9\u0342\u0399"); + + expect("i".toLocaleUpperCase()).toBe("I"); + expect("i".toLocaleUpperCase("lt")).toBe("I"); + + expect("i\u0307".toLocaleUpperCase()).toBe("I\u0307"); + expect("i\u0307".toLocaleUpperCase("lt")).toBe("I"); + + expect("j".toLocaleUpperCase()).toBe("J"); + expect("j".toLocaleUpperCase("lt")).toBe("J"); + + expect("j\u0307".toLocaleUpperCase()).toBe("J\u0307"); + expect("j\u0307".toLocaleUpperCase("lt")).toBe("J"); + + expect("j\u0307".toLocaleUpperCase(["en", "lt"])).toBe("J\u0307"); + expect("j\u0307".toLocaleUpperCase(["lt", "en"])).toBe("J"); });