diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-07-01 16:28:33 -0400 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-07-05 01:10:43 +0100 |
commit | 9f0aef60513328242ca4fb1399f58f48b8727bc9 (patch) | |
tree | 2084fc92989777d5c94ebc3d506bd1aa72d7c4c8 /Userland | |
parent | cb20baebaef4aeb53e3914dafa5699f365f14925 (diff) | |
download | serenity-9f0aef60513328242ca4fb1399f58f48b8727bc9.zip |
LibJS: Implement most of String.prototype.replaceAll
This also renames ErrorType::StringMatchAllNonGlobalRegExp to
ErrorType::StringNonGlobalRegExp (removes "MatchAll") because this error
is now used in the same way from multiple operations.
Diffstat (limited to 'Userland')
5 files changed, 190 insertions, 2 deletions
diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index 1c39061d03..e5676a9bc5 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -94,6 +94,7 @@ __JS_ENUMERATE(match, match) \ __JS_ENUMERATE(matchAll, match_all) \ __JS_ENUMERATE(replace, replace) \ + __JS_ENUMERATE(replaceAll, replace_all) \ __JS_ENUMERATE(search, search) \ __JS_ENUMERATE(split, split) \ __JS_ENUMERATE(hasInstance, has_instance) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index ef41a6deab..12acdf9988 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -157,7 +157,7 @@ "not be accessed in strict mode") \ M(SpeciesConstructorDidNotCreate, "Species constructor did not create {}") \ M(SpeciesConstructorReturned, "Species constructor returned {}") \ - M(StringMatchAllNonGlobalRegExp, "RegExp argument is non-global") \ + M(StringNonGlobalRegExp, "RegExp argument is non-global") \ M(StringRawCannotConvert, "Cannot convert property 'raw' to object from {}") \ M(StringRepeatCountMustBe, "repeat count must be a {} number") \ M(ThisHasNotBeenInitialized, "|this| has not been initialized") \ diff --git a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp index 4ef4af6d1d..0d48b7c3c3 100644 --- a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp @@ -82,6 +82,7 @@ void StringPrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.match, match, 1, attr); define_native_function(vm.names.matchAll, match_all, 1, attr); define_native_function(vm.names.replace, replace, 2, attr); + define_native_function(vm.names.replaceAll, replace_all, 2, attr); define_native_function(vm.names.search, search, 1, attr); define_native_function(vm.names.anchor, anchor, 1, attr); define_native_function(vm.names.big, big, 0, attr); @@ -765,7 +766,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::match_all) if (vm.exception()) return {}; if (!flags_string.contains("g")) { - vm.throw_exception<TypeError>(global_object, ErrorType::StringMatchAllNonGlobalRegExp); + vm.throw_exception<TypeError>(global_object, ErrorType::StringNonGlobalRegExp); return {}; } } @@ -835,6 +836,90 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) return js_string(vm, builder.build()); } +// 22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue ), https://tc39.es/ecma262/#sec-string.prototype.replaceall +JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) +{ + auto this_object = require_object_coercible(global_object, vm.this_value(global_object)); + if (vm.exception()) + return {}; + auto search_value = vm.argument(0); + auto replace_value = vm.argument(1); + + if (!search_value.is_nullish()) { + bool is_regexp = search_value.is_regexp(global_object); + if (vm.exception()) + return {}; + + if (is_regexp) { + auto flags = search_value.as_object().get(vm.names.flags); + if (vm.exception()) + return {}; + auto flags_object = require_object_coercible(global_object, flags); + if (vm.exception()) + return {}; + auto flags_string = flags_object.to_string(global_object); + if (vm.exception()) + return {}; + if (!flags_string.contains("g")) { + vm.throw_exception<TypeError>(global_object, ErrorType::StringNonGlobalRegExp); + return {}; + } + } + + auto* replacer = search_value.get_method(global_object, *vm.well_known_symbol_replace()); + if (vm.exception()) + return {}; + if (replacer) { + auto result = vm.call(*replacer, search_value, this_object, replace_value); + if (vm.exception()) + return {}; + return result; + } + } + + auto string = this_object.to_string(global_object); + if (vm.exception()) + return {}; + auto search_string = search_value.to_string(global_object); + if (vm.exception()) + return {}; + + Vector<size_t> match_positions = string.find_all(search_string); + size_t end_of_last_match = 0; + + StringBuilder result; + + for (auto position : match_positions) { + auto preserved = string.substring_view(end_of_last_match, position - end_of_last_match); + String replacement; + + if (replace_value.is_function()) { + auto result = vm.call(replace_value.as_function(), js_undefined(), search_value, Value(position), js_string(vm, string)); + if (vm.exception()) + return {}; + + replacement = result.to_string(global_object); + if (vm.exception()) + return {}; + } else { + // FIXME: Implement the GetSubstituion algorithm for substituting placeholder '$' characters - https://tc39.es/ecma262/#sec-getsubstitution + replacement = replace_value.to_string(global_object); + if (vm.exception()) + return {}; + } + + result.append(preserved); + result.append(replacement); + + end_of_last_match = position + search_string.length(); + } + + if (end_of_last_match < string.length()) + result.append(string.substring_view(end_of_last_match)); + + return js_string(vm, result.build()); +} + // 22.1.3.19 String.prototype.search ( regexp ), https://tc39.es/ecma262/#sec-string.prototype.search JS_DEFINE_NATIVE_FUNCTION(StringPrototype::search) { diff --git a/Userland/Libraries/LibJS/Runtime/StringPrototype.h b/Userland/Libraries/LibJS/Runtime/StringPrototype.h index e40706e11a..bea193bd2e 100644 --- a/Userland/Libraries/LibJS/Runtime/StringPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/StringPrototype.h @@ -46,6 +46,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(match); JS_DECLARE_NATIVE_FUNCTION(match_all); JS_DECLARE_NATIVE_FUNCTION(replace); + JS_DECLARE_NATIVE_FUNCTION(replace_all); JS_DECLARE_NATIVE_FUNCTION(search); JS_DECLARE_NATIVE_FUNCTION(anchor); JS_DECLARE_NATIVE_FUNCTION(big); diff --git a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replaceAll.js b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replaceAll.js new file mode 100644 index 0000000000..eab1d9d1ea --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replaceAll.js @@ -0,0 +1,101 @@ +test("invariants", () => { + expect(String.prototype.replaceAll).toHaveLength(2); +}); + +test("error cases", () => { + [null, undefined].forEach(value => { + expect(() => { + value.replace("", ""); + }).toThrow(TypeError); + }); + + expect(() => { + "".replaceAll(/abc/, ""); + }).toThrow(TypeError); +}); + +test("basic string replacement", () => { + expect("".replaceAll("", "")).toBe(""); + expect("".replaceAll("a", "")).toBe(""); + expect("".replaceAll("", "a")).toBe("a"); + expect("abc".replaceAll("", "x")).toBe("xaxbxcx"); + + expect("a".replaceAll("a", "")).toBe(""); + expect("a".replaceAll("a", "b")).toBe("b"); + expect("aa".replaceAll("a", "b")).toBe("bb"); + expect("ca".replaceAll("a", "b")).toBe("cb"); + expect("aca".replaceAll("a", "b")).toBe("bcb"); + expect("aca".replaceAll("ca", "b")).toBe("ab"); + expect("aca".replaceAll("ac", "b")).toBe("ba"); +}); + +test("convertible string replacement", () => { + expect("1223".replaceAll(2, "x")).toBe("1xx3"); + expect("1223".replaceAll("2", 4)).toBe("1443"); + expect("1223".replaceAll(2, 4)).toBe("1443"); +}); + +test("functional string replacement", () => { + expect( + "aba".replaceAll("a", function () { + return "c"; + }) + ).toBe("cbc"); + expect("aba".replaceAll("a", () => "c")).toBe("cbc"); + + expect( + "aba".replaceAll("a", (search, position, string) => { + expect(search).toBe("a"); + expect(position <= 2).toBeTrue(); + expect(string).toBe("aba"); + return "x"; + }) + ).toBe("xbx"); +}); + +test("basic regex replacement", () => { + expect("".replaceAll(/a/g, "")).toBe(""); + expect("a".replaceAll(/a/g, "")).toBe(""); + + expect("abc123def".replaceAll(/\D/g, "*")).toBe("***123***"); + expect("123abc456".replaceAll(/\D/g, "*")).toBe("123***456"); +}); + +test("functional regex replacement", () => { + expect( + "a".replace(/a/g, function () { + return "b"; + }) + ).toBe("b"); + expect("a".replace(/a/g, () => "b")).toBe("b"); + + expect( + "abc".replace(/\D/g, (matched, position, string) => { + expect(matched).toBe(string[position]); + expect(position <= 2).toBeTrue(); + expect(string).toBe("abc"); + return "x"; + }) + ).toBe("xxx"); + + expect( + "abc".replace(/(\D)/g, (matched, capture1, position, string) => { + expect(matched).toBe(string[position]); + expect(capture1).toBe(string[position]); + expect(position <= 2).toBeTrue(); + expect(string).toBe("abc"); + return "x"; + }) + ).toBe("xxx"); + + expect( + "abcd".replace(/(\D)b(\D)/g, (matched, capture1, capture2, position, string) => { + expect(matched).toBe("abc"); + expect(capture1).toBe("a"); + expect(capture2).toBe("c"); + expect(position).toBe(0); + expect(string).toBe("abcd"); + return "x"; + }) + ).toBe("xd"); +}); |