diff options
author | Timothy Flynn <trflynn89@pm.me> | 2021-07-05 16:16:48 -0400 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-07-05 23:26:41 +0100 |
commit | e0c9f58b0c5e7093e652b81f38e20aae9509ab3d (patch) | |
tree | c82c82fb29b42875d7db2c06c73cd4faa3ad882f /Userland/Libraries/LibJS | |
parent | 4985d3ef42d3a280f2d525612da9a715bedf3804 (diff) | |
download | serenity-e0c9f58b0c5e7093e652b81f38e20aae9509ab3d.zip |
LibJS: Implement and use the GetSubstitution abstract operation
Used by String.prototype.replace, String.prototype.replaceAll, and
RegExp.prototype [ @@replace ].
Diffstat (limited to 'Userland/Libraries/LibJS')
5 files changed, 129 insertions, 6 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp index 0f73cef6a7..cfa0d90eaf 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include <AK/CharacterTypes.h> #include <AK/Function.h> #include <AK/Optional.h> #include <AK/Result.h> @@ -574,4 +575,91 @@ Value canonical_numeric_index_string(GlobalObject& global_object, PropertyName c return n; } +// 22.1.3.17.1 GetSubstitution ( matched, str, position, captures, namedCaptures, replacement ), https://tc39.es/ecma262/#sec-getsubstitution +String get_substitution(GlobalObject& global_object, String const& matched, String const& str, size_t position, Vector<Value> const& captures, Value named_captures, Value replacement) +{ + auto& vm = global_object.vm(); + + auto replace_string = replacement.to_string(global_object); + if (vm.exception()) + return {}; + + StringBuilder result; + + for (size_t i = 0; i < replace_string.length(); ++i) { + char curr = replace_string[i]; + + if ((curr != '$') || (i + 1 >= replace_string.length())) { + result.append(curr); + continue; + } + + char next = replace_string[i + 1]; + + if (next == '$') { + result.append(next); + ++i; + } else if (next == '&') { + result.append(matched); + ++i; + } else if (next == '`') { + result.append(str.substring_view(0, position)); + ++i; + } else if (next == '\'') { + auto tail_pos = position + matched.length(); + if (tail_pos < str.length()) + result.append(str.substring_view(tail_pos)); + ++i; + } else if (is_ascii_digit(next)) { + bool is_two_digits = (i + 2 < replace_string.length()) && is_ascii_digit(replace_string[i + 2]); + + auto capture_postition_string = replace_string.substring_view(i + 1, is_two_digits ? 2 : 1); + auto capture_position = capture_postition_string.to_uint(); + + if (capture_position.has_value() && (*capture_position > 0) && (*capture_position <= captures.size())) { + auto& value = captures[*capture_position - 1]; + + if (!value.is_undefined()) { + auto value_string = value.to_string(global_object); + if (vm.exception()) + return {}; + + result.append(value_string); + } + + i += is_two_digits ? 2 : 1; + } else { + result.append(curr); + } + } else if (next == '<') { + auto start_position = i + 2; + auto end_position = replace_string.find('>', start_position); + + if (named_captures.is_undefined() || !end_position.has_value()) { + result.append(curr); + } else { + auto group_name = replace_string.substring(start_position, *end_position - start_position); + + auto capture = named_captures.as_object().get(group_name); + if (vm.exception()) + return {}; + + if (!capture.is_undefined()) { + auto capture_string = capture.to_string(global_object); + if (vm.exception()) + return {}; + + result.append(capture_string); + } + + i = *end_position; + } + } else { + result.append(curr); + } + } + + return result.build(); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h index 3c7ecdbb68..f70f51654b 100644 --- a/Userland/Libraries/LibJS/Runtime/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/AbstractOperations.h @@ -30,6 +30,7 @@ Object* get_prototype_from_constructor(GlobalObject&, FunctionObject const& cons Object* create_unmapped_arguments_object(GlobalObject&, Vector<Value> const& arguments); Object* create_mapped_arguments_object(GlobalObject&, FunctionObject&, Vector<FunctionNode::Parameter> const&, Vector<Value> const& arguments, Environment&); Value canonical_numeric_index_string(GlobalObject&, PropertyName const&); +String get_substitution(GlobalObject&, String const& matched, String const& str, size_t position, Vector<Value> const& captures, Value named_captures, Value replacement); enum class CallerMode { Strict, diff --git a/Userland/Libraries/LibJS/Runtime/RegExpPrototype.cpp b/Userland/Libraries/LibJS/Runtime/RegExpPrototype.cpp index 4c9759fd6e..29d20760d0 100644 --- a/Userland/Libraries/LibJS/Runtime/RegExpPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/RegExpPrototype.cpp @@ -399,8 +399,7 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace) 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); + replacement = get_substitution(global_object, matched, string, position, captures, named_captures, replace_value); if (vm.exception()) return {}; } diff --git a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp index a7a3510400..2e1b423cae 100644 --- a/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/StringPrototype.cpp @@ -822,8 +822,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace) 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); + replacement = get_substitution(global_object, search_string, string, *position, {}, js_undefined(), replace_value); if (vm.exception()) return {}; } @@ -902,8 +901,7 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace_all) 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); + replacement = get_substitution(global_object, search_string, string, position, {}, js_undefined(), replace_value); if (vm.exception()) return {}; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replace.js b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replace.js index 5cce84c653..a570a68ad0 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replace.js +++ b/Userland/Libraries/LibJS/Tests/builtins/String/String.prototype.replace.js @@ -102,3 +102,40 @@ test("functional regex replacement", () => { }) ).toBe("xd"); }); + +test("replacement with substitution", () => { + expect("abc".replace("b", "$")).toBe("a$c"); + expect("abc".replace("b", "$.")).toBe("a$.c"); + + expect("abc".replace("b", "$$")).toBe("a$c"); + expect("abc".replace("b", ">$$<")).toBe("a>$<c"); + expect("abc".replace("b", "$$$$")).toBe("a$$c"); + + expect("abc".replace("b", "$&")).toBe("abc"); + expect("a123c".replace(/\d+/, "$&")).toBe("a123c"); + + expect("abc".replace("b", "$`")).toBe("aac"); + expect("aabc".replace("b", "$`")).toBe("aaaac"); + expect("a123c".replace(/\d+/, "$`")).toBe("aac"); + + expect("abc".replace("b", "$'")).toBe("acc"); + expect("abcc".replace("b", "$'")).toBe("acccc"); + expect("a123c".replace(/\d+/, "$'")).toBe("acc"); + + expect("abc".replace("b", "$0")).toBe("a$0c"); + expect("abc".replace("b", "$99")).toBe("a$99c"); + expect("abc".replace("b", "$100")).toBe("a$100c"); + expect("abc".replace(/(a)b(c)/, "$0")).toBe("$0"); + expect("abc".replace(/(a)b(c)/, "$1")).toBe("a"); + expect("abc".replace(/(a)b(c)/, "$2")).toBe("c"); + expect("abc".replace(/(a)b(c)/, "$3")).toBe("$3"); + expect("abc".replace(/(a)b(c)/, "$2b$1")).toBe("cba"); + + expect("abc".replace("b", "$<val>")).toBe("a$<val>c"); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<")).toBe("$<"); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<not_terminated")).toBe("$<not_terminated"); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<not_found>")).toBe(""); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<val1>")).toBe("a"); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<val2>")).toBe("c"); + expect("abc".replace(/(?<val1>a)b(?<val2>c)/, "$<val2>b$<val1>")).toBe("cba"); +}); |