diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-09-06 14:37:23 -0400 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-09-06 23:49:56 +0100 |
commit | cdba40f7ea12aa3c84832df7a761b5d0a53dfa4b (patch) | |
tree | 4b1b36705048752d7b9761d068cb02828e18a826 | |
parent | 3b410742ab5bdf8a8a5cee313075f8c5eff1a4a7 (diff) | |
download | serenity-cdba40f7ea12aa3c84832df7a761b5d0a53dfa4b.zip |
LibJS: Implement Intl.ListFormat.prototype.format
7 files changed, 517 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index b5faf7e375..54e3373825 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -167,6 +167,7 @@ namespace JS { P(fontcolor) \ P(fontsize) \ P(forEach) \ + P(format) \ P(fractionalSecondDigits) \ P(freeze) \ P(from) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 17f13df4ef..0fa9093796 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -62,6 +62,7 @@ M(NotAnObject, "{} is not an object") \ M(NotAnObjectOrNull, "{} is neither an object nor null") \ M(NotAnObjectOrString, "{} is neither an object nor a string") \ + M(NotAString, "{} is not a string") \ M(NotASymbol, "{} is not a symbol") \ M(NotIterable, "{} is not iterable") \ M(NotObjectCoercible, "{} cannot be converted to an object") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index 0cb86bdaca..5c4a7373f2 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -536,6 +536,65 @@ Value get_option(GlobalObject& global_object, Value options, PropertyName const& return value; } +// 9.2.16 PartitionPattern ( pattern ), https://tc39.es/ecma402/#sec-partitionpattern +Vector<PatternPartition> partition_pattern(StringView pattern) +{ + // 1. Let result be a new empty List. + Vector<PatternPartition> result; + + // 2. Let beginIndex be ! StringIndexOf(pattern, "{", 0). + auto begin_index = pattern.find('{', 0); + + // 3. Let endIndex be 0. + size_t end_index = 0; + + // 4. Let nextIndex be 0. + size_t next_index = 0; + + // 5. Let length be the number of code units in pattern. + // 6. Repeat, while beginIndex is an integer index into pattern, + while (begin_index.has_value()) { + // a. Set endIndex to ! StringIndexOf(pattern, "}", beginIndex). + end_index = pattern.find('}', *begin_index).value(); + + // b. Assert: endIndex is greater than beginIndex. + VERIFY(end_index > *begin_index); + + // c. If beginIndex is greater than nextIndex, then + if (*begin_index > next_index) { + // i. Let literal be a substring of pattern from position nextIndex, inclusive, to position beginIndex, exclusive. + auto literal = pattern.substring_view(next_index, *begin_index - next_index); + + // ii. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result. + result.append({ "literal"sv, literal }); + } + + // d. Let p be the substring of pattern from position beginIndex, exclusive, to position endIndex, exclusive. + auto partition = pattern.substring_view(*begin_index + 1, end_index - *begin_index - 1); + + // e. Append a new Record { [[Type]]: p, [[Value]]: undefined } as the last element of the list result. + result.append({ partition, {} }); + + // f. Set nextIndex to endIndex + 1. + next_index = end_index + 1; + + // g. Set beginIndex to ! StringIndexOf(pattern, "{", nextIndex). + begin_index = pattern.find('{', next_index); + } + + // 7. If nextIndex is less than length, then + if (next_index < pattern.length()) { + // a. Let literal be the substring of pattern from position nextIndex, inclusive, to position length, exclusive. + auto literal = pattern.substring_view(next_index); + + // b. Append a new Record { [[Type]]: "literal", [[Value]]: literal } as the last element of the list result. + result.append({ "literal"sv, literal }); + } + + // 8. Return result. + return result; +} + // 12.1.1 CanonicalCodeForDisplayNames ( type, code ), https://tc39.es/ecma402/#sec-canonicalcodefordisplaynames Value canonical_code_for_display_names(GlobalObject& global_object, DisplayNames::Type type, StringView code) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index 3f089ff33c..b2c7167c26 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -26,6 +26,11 @@ struct LocaleResult { String locale; }; +struct PatternPartition { + StringView type; + StringView value; +}; + Optional<Unicode::LocaleID> is_structurally_valid_language_tag(StringView locale); String canonicalize_unicode_locale_id(Unicode::LocaleID& locale); Vector<String> canonicalize_locale_list(GlobalObject&, Value locales); @@ -35,6 +40,7 @@ Vector<String> lookup_supported_locales(Vector<String> const& requested_locales) Array* supported_locales(GlobalObject&, Vector<String> const& requested_locales, Value options); Object* coerce_options_to_object(GlobalObject& global_object, Value options); Value get_option(GlobalObject& global_object, Value options, PropertyName const& property, Value::Type type, Vector<StringView> const& values, Fallback fallback); +Vector<PatternPartition> partition_pattern(StringView pattern); String insert_unicode_extension_and_canonicalize(Unicode::LocaleID locale_id, Unicode::LocaleExtension extension); LocaleResult resolve_locale(Vector<String> const& requested_locales, LocaleOptions const& options, Vector<StringView> relevant_extension_keys); Value canonical_code_for_display_names(GlobalObject&, DisplayNames::Type type, StringView code); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.cpp index 6bde36f714..769424e0f7 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.cpp @@ -4,11 +4,248 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include <AK/HashMap.h> +#include <AK/StringBuilder.h> +#include <AK/TypeCasts.h> +#include <AK/Variant.h> +#include <AK/Vector.h> #include <LibJS/Runtime/GlobalObject.h> +#include <LibJS/Runtime/Intl/AbstractOperations.h> +#include <LibJS/Runtime/Intl/ListFormat.h> #include <LibJS/Runtime/Intl/ListFormatPrototype.h> +#include <LibJS/Runtime/IteratorOperations.h> +#include <LibUnicode/Locale.h> namespace JS::Intl { +static ListFormat* typed_this(GlobalObject& global_object) +{ + auto& vm = global_object.vm(); + + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) + return nullptr; + + if (!is<ListFormat>(this_object)) { + vm.throw_exception<TypeError>(global_object, ErrorType::NotA, "Intl.ListFormat"); + return nullptr; + } + + return static_cast<ListFormat*>(this_object); +} + +using Placeables = HashMap<StringView, Variant<PatternPartition, Vector<PatternPartition>>>; + +// 13.1.1 DeconstructPattern ( pattern, placeables ), https://tc39.es/ecma402/#sec-deconstructpattern +static Vector<PatternPartition> deconstruct_pattern(StringView pattern, Placeables placeables) +{ + // 1. Let patternParts be PartitionPattern(pattern). + auto pattern_parts = partition_pattern(pattern); + + // 2. Let result be a new empty List. + Vector<PatternPartition> result {}; + + // 3. For each Record { [[Type]], [[Value]] } patternPart of patternParts, do + for (auto const& pattern_part : pattern_parts) { + // a. Let part be patternPart.[[Type]]. + auto part = pattern_part.type; + + // b. If part is "literal", then + if (part == "literal"sv) { + // i. Append Record { [[Type]]: "literal", [[Value]]: patternPart.[[Value]] } to result. + result.append({ part, pattern_part.value }); + } + // c. Else, + else { + // i. Assert: placeables has a field [[<part>]]. + // ii. Let subst be placeables.[[<part>]]. + auto subst = placeables.get(part); + VERIFY(subst.has_value()); + + subst.release_value().visit( + // iii. If Type(subst) is List, then + [&](Vector<PatternPartition>& partition) { + // 1. For each element s of subst, do + for (auto& element : partition) { + // a. Append s to result. + result.append(move(element)); + } + }, + // iv. Else, + [&](PatternPartition& partition) { + // 1. Append subst to result. + result.append(move(partition)); + }); + } + } + + // 4. Return result. + return result; +} + +// 13.1.2 CreatePartsFromList ( listFormat, list ), https://tc39.es/ecma402/#sec-createpartsfromlist +static Vector<PatternPartition> create_parts_from_list(ListFormat const& list_format, Vector<String> const& list) +{ + auto list_patterns = Unicode::get_locale_list_patterns(list_format.locale(), list_format.type_string(), list_format.style_string()); + if (!list_patterns.has_value()) + return {}; + + // 1. Let size be the number of elements of list. + auto size = list.size(); + + // 2. If size is 0, then + if (size == 0) { + // a. Return a new empty List. + return {}; + } + + // 3. If size is 2, then + if (size == 2) { + // a. Let n be an index into listFormat.[[Templates]] based on listFormat.[[Locale]], list[0], and list[1]. + // b. Let pattern be listFormat.[[Templates]][n].[[Pair]]. + auto pattern = list_patterns->pair; + + // c. Let first be a new Record { [[Type]]: "element", [[Value]]: list[0] }. + PatternPartition first { "element"sv, list[0] }; + + // d. Let second be a new Record { [[Type]]: "element", [[Value]]: list[1] }. + PatternPartition second { "element"sv, list[1] }; + + // e. Let placeables be a new Record { [[0]]: first, [[1]]: second }. + Placeables placeables; + placeables.set("0"sv, move(first)); + placeables.set("1"sv, move(second)); + + // f. Return DeconstructPattern(pattern, placeables). + return deconstruct_pattern(pattern, move(placeables)); + } + + // 4. Let last be a new Record { [[Type]]: "element", [[Value]]: list[size - 1] }. + PatternPartition last { "element"sv, list[size - 1] }; + + // 5. Let parts be ยซ last ยป. + Vector<PatternPartition> parts { move(last) }; + + // The spec does not say to do this, but because size_t is unsigned, we need to take care not to wrap around 0. + if (size == 1) + return parts; + + // 6. Let i be size - 2. + size_t i = size - 2; + + // 7. Repeat, while i โฅ 0, + do { + // a. Let head be a new Record { [[Type]]: "element", [[Value]]: list[i] }. + PatternPartition head { "element"sv, list[i] }; + + // b. Let n be an implementation-defined index into listFormat.[[Templates]] based on listFormat.[[Locale]], head, and parts. + StringView pattern; + + // c. If i is 0, then + if (i == 0) { + // i. Let pattern be listFormat.[[Templates]][n].[[Start]]. + pattern = list_patterns->start; + } + // d. Else if i is less than size - 2, then + else if (i < (size - 2)) { + // i. Let pattern be listFormat.[[Templates]][n].[[Middle]]. + pattern = list_patterns->middle; + } + // e. Else, + else { + // i. Let pattern be listFormat.[[Templates]][n].[[End]]. + pattern = list_patterns->end; + } + + // f. Let placeables be a new Record { [[0]]: head, [[1]]: parts }. + Placeables placeables; + placeables.set("0"sv, move(head)); + placeables.set("1"sv, move(parts)); + + // g. Set parts to DeconstructPattern(pattern, placeables). + parts = deconstruct_pattern(pattern, move(placeables)); + + // h. Decrement i by 1. + } while (i-- != 0); + + // 8. Return parts. + return parts; +} + +// 13.1.3 FormatList ( listFormat, list ) +static String format_list(ListFormat const& list_format, Vector<String> const& list) +{ + // 1. Let parts be CreatePartsFromList(listFormat, list). + auto parts = create_parts_from_list(list_format, list); + + // 2. Let result be an empty String. + StringBuilder result; + + // 3. For each Record { [[Type]], [[Value]] } part in parts, do + for (auto const& part : parts) { + // a. Set result to the string-concatenation of result and part.[[Value]]. + result.append(part.value); + } + + // 4. Return result. + return result.build(); +} + +// 13.1.5 StringListFromIterable ( iterable ), https://tc39.es/ecma402/#sec-createstringlistfromiterable +static Vector<String> string_list_from_iterable(GlobalObject& global_object, Value iterable) +{ + auto& vm = global_object.vm(); + + // 1. If iterable is undefined, then + if (iterable.is_undefined()) { + // a. Return a new empty List. + return {}; + } + + // 2. Let iteratorRecord be ? GetIterator(iterable). + auto* iterator_record = get_iterator(global_object, iterable); + if (vm.exception()) + return {}; + + // 3. Let list be a new empty List. + Vector<String> list; + + // 4. Let next be true. + Object* next = nullptr; + + // 5. Repeat, while next is not false, + do { + // a. Set next to ? IteratorStep(iteratorRecord). + next = iterator_step(global_object, *iterator_record); + if (vm.exception()) + return {}; + + // b. If next is not false, then + if (next != nullptr) { + // i. Let nextValue be ? IteratorValue(next). + auto next_value = iterator_value(global_object, *next); + if (vm.exception()) + return {}; + + // ii. If Type(nextValue) is not String, then + if (!next_value.is_string()) { + // 1. Let error be ThrowCompletion(a newly created TypeError object). + vm.throw_exception<TypeError>(global_object, ErrorType::NotAString, next_value); + + // 2. Return ? IteratorClose(iteratorRecord, error). + iterator_close(*iterator_record); + return {}; + } + + // iii. Append nextValue to the end of the List list. + list.append(next_value.as_string().string()); + } + } while (next != nullptr); + + // 6. Return list. + return list; +} + // 13.4 Properties of the Intl.ListFormat Prototype Object, https://tc39.es/ecma402/#sec-properties-of-intl-listformat-prototype-object ListFormatPrototype::ListFormatPrototype(GlobalObject& global_object) : Object(*global_object.object_prototype()) @@ -23,6 +260,30 @@ void ListFormatPrototype::initialize(GlobalObject& global_object) // 13.4.2 Intl.ListFormat.prototype [ @@toStringTag ], https://tc39.es/ecma402/#sec-Intl.ListFormat.prototype-toStringTag define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, "Intl.ListFormat"), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.format, format, 1, attr); +} + +// 13.4.3 Intl.ListFormat.prototype.format ( list ), https://tc39.es/ecma402/#sec-Intl.ListFormat.prototype.format +JS_DEFINE_NATIVE_FUNCTION(ListFormatPrototype::format) +{ + auto list = vm.argument(0); + + // 1. Let lf be the this value. + // 2. Perform ? RequireInternalSlot(lf, [[InitializedListFormat]]). + auto* list_format = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Let stringList be ? StringListFromIterable(list). + auto string_list = string_list_from_iterable(global_object, list); + if (vm.exception()) + return {}; + + // 4. Return FormatList(lf, stringList). + auto formatted = format_list(*list_format, string_list); + return js_string(vm, move(formatted)); } } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.h index 6bcbe6cf2f..605db99c11 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/ListFormatPrototype.h @@ -17,6 +17,9 @@ public: explicit ListFormatPrototype(GlobalObject&); virtual void initialize(GlobalObject&) override; virtual ~ListFormatPrototype() override = default; + +private: + JS_DECLARE_NATIVE_FUNCTION(format); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/ListFormat/ListFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/ListFormat/ListFormat.prototype.format.js new file mode 100644 index 0000000000..de5ad3563e --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/ListFormat/ListFormat.prototype.format.js @@ -0,0 +1,186 @@ +describe("errors", () => { + function SomeError() {} + + test("called on non-ListFormat object", () => { + expect(() => { + Intl.ListFormat.prototype.format([]); + }).toThrowWithMessage(TypeError, "Not a Intl.ListFormat object"); + }); + + test("called with non-string iterable", () => { + expect(() => { + new Intl.ListFormat().format([1]); + }).toThrowWithMessage(TypeError, "1 is not a string"); + }); + + test("called with iterable that throws immediately", () => { + let iterable = { + [Symbol.iterator]() { + throw new SomeError(); + }, + }; + + expect(() => { + new Intl.ListFormat().format(iterable); + }).toThrow(SomeError); + }); + + test("called with iterable that throws on step", () => { + let iterable = { + [Symbol.iterator]() { + return this; + }, + next() { + throw new SomeError(); + }, + }; + + expect(() => { + new Intl.ListFormat().format(iterable); + }).toThrow(SomeError); + }); + + test("called with iterable that throws on value resolution", () => { + let iterable = { + [Symbol.iterator]() { + return this; + }, + next() { + return { + done: false, + get value() { + throw new SomeError(); + }, + }; + }, + }; + + expect(() => { + new Intl.ListFormat().format(iterable); + }).toThrow(SomeError); + }); +}); + +describe("correct behavior", () => { + test("length is 1", () => { + expect(Intl.ListFormat.prototype.format).toHaveLength(1); + }); + + test("undefined list returns empty string", () => { + expect(new Intl.ListFormat().format(undefined)).toBe(""); + }); +}); + +describe("type=conjunction", () => { + test("style=long", () => { + let en = new Intl.ListFormat("en", { type: "conjunction", style: "long" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a and b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, and c"); + + let es = new Intl.ListFormat("es-419", { type: "conjunction", style: "long" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a y b"); + expect(es.format(["a", "b", "c"])).toBe("a, b y c"); + }); + + test("style=short", () => { + let en = new Intl.ListFormat("en", { type: "conjunction", style: "short" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a & b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, & c"); + + let es = new Intl.ListFormat("es-419", { type: "conjunction", style: "short" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a y b"); + expect(es.format(["a", "b", "c"])).toBe("a, b y c"); + }); + + test("style=narrow", () => { + let en = new Intl.ListFormat("en", { type: "conjunction", style: "narrow" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a, b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, c"); + + let es = new Intl.ListFormat("es-419", { type: "conjunction", style: "narrow" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a y b"); + expect(es.format(["a", "b", "c"])).toBe("a, b y c"); + }); +}); + +describe("type=disjunction", () => { + test("style=long", () => { + let en = new Intl.ListFormat("en", { type: "disjunction", style: "long" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a or b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, or c"); + + let es = new Intl.ListFormat("es-419", { type: "disjunction", style: "long" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a o b"); + expect(es.format(["a", "b", "c"])).toBe("a, b o c"); + }); + + test("style=short", () => { + let en = new Intl.ListFormat("en", { type: "disjunction", style: "short" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a or b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, or c"); + + let es = new Intl.ListFormat("es-419", { type: "disjunction", style: "short" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a o b"); + expect(es.format(["a", "b", "c"])).toBe("a, b o c"); + }); + + test("style=narrow", () => { + let en = new Intl.ListFormat("en", { type: "disjunction", style: "narrow" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a or b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, or c"); + + let es = new Intl.ListFormat("es-419", { type: "disjunction", style: "narrow" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a o b"); + expect(es.format(["a", "b", "c"])).toBe("a, b o c"); + }); +}); + +describe("type=unit", () => { + test("style=long", () => { + let en = new Intl.ListFormat("en", { type: "unit", style: "long" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a, b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, c"); + + let es = new Intl.ListFormat("es-419", { type: "unit", style: "long" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a y b"); + expect(es.format(["a", "b", "c"])).toBe("a, b y c"); + }); + + test("style=short", () => { + let en = new Intl.ListFormat("en", { type: "unit", style: "short" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a, b"); + expect(en.format(["a", "b", "c"])).toBe("a, b, c"); + + let es = new Intl.ListFormat("es-419", { type: "unit", style: "short" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a y b"); + expect(es.format(["a", "b", "c"])).toBe("a, b, c"); + }); + + test("style=narrow", () => { + let en = new Intl.ListFormat("en", { type: "unit", style: "narrow" }); + expect(en.format(["a"])).toBe("a"); + expect(en.format(["a", "b"])).toBe("a b"); + expect(en.format(["a", "b", "c"])).toBe("a b c"); + + let es = new Intl.ListFormat("es-419", { type: "unit", style: "narrow" }); + expect(es.format(["a"])).toBe("a"); + expect(es.format(["a", "b"])).toBe("a b"); + expect(es.format(["a", "b", "c"])).toBe("a b c"); + }); +}); |