Skip to content

Commit 9f0aef6

Browse files
trflynn89linusg
authored andcommitted
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.
1 parent cb20bae commit 9f0aef6

File tree

5 files changed

+190
-2
lines changed

5 files changed

+190
-2
lines changed

Userland/Libraries/LibJS/Forward.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
__JS_ENUMERATE(match, match) \
9595
__JS_ENUMERATE(matchAll, match_all) \
9696
__JS_ENUMERATE(replace, replace) \
97+
__JS_ENUMERATE(replaceAll, replace_all) \
9798
__JS_ENUMERATE(search, search) \
9899
__JS_ENUMERATE(split, split) \
99100
__JS_ENUMERATE(hasInstance, has_instance) \

Userland/Libraries/LibJS/Runtime/ErrorTypes.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
"not be accessed in strict mode") \
158158
M(SpeciesConstructorDidNotCreate, "Species constructor did not create {}") \
159159
M(SpeciesConstructorReturned, "Species constructor returned {}") \
160-
M(StringMatchAllNonGlobalRegExp, "RegExp argument is non-global") \
160+
M(StringNonGlobalRegExp, "RegExp argument is non-global") \
161161
M(StringRawCannotConvert, "Cannot convert property 'raw' to object from {}") \
162162
M(StringRepeatCountMustBe, "repeat count must be a {} number") \
163163
M(ThisHasNotBeenInitialized, "|this| has not been initialized") \

Userland/Libraries/LibJS/Runtime/StringPrototype.cpp

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ void StringPrototype::initialize(GlobalObject& global_object)
8282
define_native_function(vm.names.match, match, 1, attr);
8383
define_native_function(vm.names.matchAll, match_all, 1, attr);
8484
define_native_function(vm.names.replace, replace, 2, attr);
85+
define_native_function(vm.names.replaceAll, replace_all, 2, attr);
8586
define_native_function(vm.names.search, search, 1, attr);
8687
define_native_function(vm.names.anchor, anchor, 1, attr);
8788
define_native_function(vm.names.big, big, 0, attr);
@@ -765,7 +766,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::match_all)
765766
if (vm.exception())
766767
return {};
767768
if (!flags_string.contains("g")) {
768-
vm.throw_exception<TypeError>(global_object, ErrorType::StringMatchAllNonGlobalRegExp);
769+
vm.throw_exception<TypeError>(global_object, ErrorType::StringNonGlobalRegExp);
769770
return {};
770771
}
771772
}
@@ -835,6 +836,90 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace)
835836
return js_string(vm, builder.build());
836837
}
837838

839+
// 22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue ), https://tc39.es/ecma262/#sec-string.prototype.replaceall
840+
JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all)
841+
{
842+
auto this_object = require_object_coercible(global_object, vm.this_value(global_object));
843+
if (vm.exception())
844+
return {};
845+
auto search_value = vm.argument(0);
846+
auto replace_value = vm.argument(1);
847+
848+
if (!search_value.is_nullish()) {
849+
bool is_regexp = search_value.is_regexp(global_object);
850+
if (vm.exception())
851+
return {};
852+
853+
if (is_regexp) {
854+
auto flags = search_value.as_object().get(vm.names.flags);
855+
if (vm.exception())
856+
return {};
857+
auto flags_object = require_object_coercible(global_object, flags);
858+
if (vm.exception())
859+
return {};
860+
auto flags_string = flags_object.to_string(global_object);
861+
if (vm.exception())
862+
return {};
863+
if (!flags_string.contains("g")) {
864+
vm.throw_exception<TypeError>(global_object, ErrorType::StringNonGlobalRegExp);
865+
return {};
866+
}
867+
}
868+
869+
auto* replacer = search_value.get_method(global_object, *vm.well_known_symbol_replace());
870+
if (vm.exception())
871+
return {};
872+
if (replacer) {
873+
auto result = vm.call(*replacer, search_value, this_object, replace_value);
874+
if (vm.exception())
875+
return {};
876+
return result;
877+
}
878+
}
879+
880+
auto string = this_object.to_string(global_object);
881+
if (vm.exception())
882+
return {};
883+
auto search_string = search_value.to_string(global_object);
884+
if (vm.exception())
885+
return {};
886+
887+
Vector<size_t> match_positions = string.find_all(search_string);
888+
size_t end_of_last_match = 0;
889+
890+
StringBuilder result;
891+
892+
for (auto position : match_positions) {
893+
auto preserved = string.substring_view(end_of_last_match, position - end_of_last_match);
894+
String replacement;
895+
896+
if (replace_value.is_function()) {
897+
auto result = vm.call(replace_value.as_function(), js_undefined(), search_value, Value(position), js_string(vm, string));
898+
if (vm.exception())
899+
return {};
900+
901+
replacement = result.to_string(global_object);
902+
if (vm.exception())
903+
return {};
904+
} else {
905+
// FIXME: Implement the GetSubstituion algorithm for substituting placeholder '$' characters - https://tc39.es/ecma262/#sec-getsubstitution
906+
replacement = replace_value.to_string(global_object);
907+
if (vm.exception())
908+
return {};
909+
}
910+
911+
result.append(preserved);
912+
result.append(replacement);
913+
914+
end_of_last_match = position + search_string.length();
915+
}
916+
917+
if (end_of_last_match < string.length())
918+
result.append(string.substring_view(end_of_last_match));
919+
920+
return js_string(vm, result.build());
921+
}
922+
838923
// 22.1.3.19 String.prototype.search ( regexp ), https://tc39.es/ecma262/#sec-string.prototype.search
839924
JS_DEFINE_NATIVE_FUNCTION(StringPrototype::search)
840925
{

Userland/Libraries/LibJS/Runtime/StringPrototype.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class StringPrototype final : public StringObject {
4646
JS_DECLARE_NATIVE_FUNCTION(match);
4747
JS_DECLARE_NATIVE_FUNCTION(match_all);
4848
JS_DECLARE_NATIVE_FUNCTION(replace);
49+
JS_DECLARE_NATIVE_FUNCTION(replace_all);
4950
JS_DECLARE_NATIVE_FUNCTION(search);
5051
JS_DECLARE_NATIVE_FUNCTION(anchor);
5152
JS_DECLARE_NATIVE_FUNCTION(big);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
test("invariants", () => {
2+
expect(String.prototype.replaceAll).toHaveLength(2);
3+
});
4+
5+
test("error cases", () => {
6+
[null, undefined].forEach(value => {
7+
expect(() => {
8+
value.replace("", "");
9+
}).toThrow(TypeError);
10+
});
11+
12+
expect(() => {
13+
"".replaceAll(/abc/, "");
14+
}).toThrow(TypeError);
15+
});
16+
17+
test("basic string replacement", () => {
18+
expect("".replaceAll("", "")).toBe("");
19+
expect("".replaceAll("a", "")).toBe("");
20+
expect("".replaceAll("", "a")).toBe("a");
21+
expect("abc".replaceAll("", "x")).toBe("xaxbxcx");
22+
23+
expect("a".replaceAll("a", "")).toBe("");
24+
expect("a".replaceAll("a", "b")).toBe("b");
25+
expect("aa".replaceAll("a", "b")).toBe("bb");
26+
expect("ca".replaceAll("a", "b")).toBe("cb");
27+
expect("aca".replaceAll("a", "b")).toBe("bcb");
28+
expect("aca".replaceAll("ca", "b")).toBe("ab");
29+
expect("aca".replaceAll("ac", "b")).toBe("ba");
30+
});
31+
32+
test("convertible string replacement", () => {
33+
expect("1223".replaceAll(2, "x")).toBe("1xx3");
34+
expect("1223".replaceAll("2", 4)).toBe("1443");
35+
expect("1223".replaceAll(2, 4)).toBe("1443");
36+
});
37+
38+
test("functional string replacement", () => {
39+
expect(
40+
"aba".replaceAll("a", function () {
41+
return "c";
42+
})
43+
).toBe("cbc");
44+
expect("aba".replaceAll("a", () => "c")).toBe("cbc");
45+
46+
expect(
47+
"aba".replaceAll("a", (search, position, string) => {
48+
expect(search).toBe("a");
49+
expect(position <= 2).toBeTrue();
50+
expect(string).toBe("aba");
51+
return "x";
52+
})
53+
).toBe("xbx");
54+
});
55+
56+
test("basic regex replacement", () => {
57+
expect("".replaceAll(/a/g, "")).toBe("");
58+
expect("a".replaceAll(/a/g, "")).toBe("");
59+
60+
expect("abc123def".replaceAll(/\D/g, "*")).toBe("***123***");
61+
expect("123abc456".replaceAll(/\D/g, "*")).toBe("123***456");
62+
});
63+
64+
test("functional regex replacement", () => {
65+
expect(
66+
"a".replace(/a/g, function () {
67+
return "b";
68+
})
69+
).toBe("b");
70+
expect("a".replace(/a/g, () => "b")).toBe("b");
71+
72+
expect(
73+
"abc".replace(/\D/g, (matched, position, string) => {
74+
expect(matched).toBe(string[position]);
75+
expect(position <= 2).toBeTrue();
76+
expect(string).toBe("abc");
77+
return "x";
78+
})
79+
).toBe("xxx");
80+
81+
expect(
82+
"abc".replace(/(\D)/g, (matched, capture1, position, string) => {
83+
expect(matched).toBe(string[position]);
84+
expect(capture1).toBe(string[position]);
85+
expect(position <= 2).toBeTrue();
86+
expect(string).toBe("abc");
87+
return "x";
88+
})
89+
).toBe("xxx");
90+
91+
expect(
92+
"abcd".replace(/(\D)b(\D)/g, (matched, capture1, capture2, position, string) => {
93+
expect(matched).toBe("abc");
94+
expect(capture1).toBe("a");
95+
expect(capture2).toBe("c");
96+
expect(position).toBe(0);
97+
expect(string).toBe("abcd");
98+
return "x";
99+
})
100+
).toBe("xd");
101+
});

0 commit comments

Comments
 (0)