summaryrefslogtreecommitdiff
path: root/Userland
diff options
context:
space:
mode:
authorTimothy Flynn <trflynn89@pm.me>2021-07-01 16:28:33 -0400
committerLinus Groh <mail@linusgroh.de>2021-07-05 01:10:43 +0100
commit9f0aef60513328242ca4fb1399f58f48b8727bc9 (patch)
tree2084fc92989777d5c94ebc3d506bd1aa72d7c4c8 /Userland
parentcb20baebaef4aeb53e3914dafa5699f365f14925 (diff)
downloadserenity-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')
-rw-r--r--Userland/Libraries/LibJS/Forward.h1
-rw-r--r--Userland/Libraries/LibJS/Runtime/ErrorTypes.h2
-rw-r--r--Userland/Libraries/LibJS/Runtime/StringPrototype.cpp87
-rw-r--r--Userland/Libraries/LibJS/Runtime/StringPrototype.h1
-rw-r--r--Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replaceAll.js101
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");
+});