1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 11:57:35 +00:00

LibJS: Implement most of String.prototype.replace

This commit is contained in:
Timothy Flynn 2021-04-01 16:33:51 -04:00 committed by Andreas Kling
parent 96121ddb11
commit 77a601d52e
5 changed files with 308 additions and 0 deletions

View file

@ -50,6 +50,7 @@ void RegExpPrototype::initialize(GlobalObject& global_object)
define_native_function(vm.names.exec, exec, 1, attr); define_native_function(vm.names.exec, exec, 1, attr);
define_native_function(vm.well_known_symbol_match(), symbol_match, 1, attr); define_native_function(vm.well_known_symbol_match(), symbol_match, 1, attr);
define_native_function(vm.well_known_symbol_replace(), symbol_replace, 2, attr);
u8 readable_attr = Attribute::Configurable; u8 readable_attr = Attribute::Configurable;
define_native_property(vm.names.flags, flags, {}, readable_attr); define_native_property(vm.names.flags, flags, {}, readable_attr);
@ -281,4 +282,151 @@ JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_match)
return vm.call(*exec, rx, js_string(vm, s)); return vm.call(*exec, rx, js_string(vm, s));
} }
JS_DEFINE_NATIVE_FUNCTION(RegExpPrototype::symbol_replace)
{
auto string_value = vm.argument(0);
auto replace_value = vm.argument(1);
// https://tc39.es/ecma262/#sec-regexp.prototype-@@replace
auto rx = regexp_object_from(vm, global_object);
if (!rx)
return {};
auto string = string_value.to_string(global_object);
if (vm.exception())
return {};
auto global_value = rx->get(vm.names.global).value_or(js_undefined());
if (vm.exception())
return {};
bool global = global_value.to_boolean();
if (global)
rx->regex().start_offset = 0;
// FIXME: Implement and use RegExpExec - https://tc39.es/ecma262/#sec-regexpexec
auto* exec = get_method(global_object, rx, vm.names.exec);
if (!exec)
return {};
Vector<Object*> results;
while (true) {
auto result = vm.call(*exec, rx, string_value);
if (vm.exception())
return {};
if (result.is_null())
break;
auto* result_object = result.to_object(global_object);
if (!result_object)
return {};
results.append(result_object);
if (!global)
break;
auto match_object = result_object->get(0);
if (vm.exception())
return {};
String match_str = match_object.to_string(global_object);
if (vm.exception())
return {};
if (match_str.is_empty()) {
// FIXME: Implement AdvanceStringIndex to take Unicode code points into account - https://tc39.es/ecma262/#sec-advancestringindex
// Once implemented, step (8a) of the @@replace algorithm must also be implemented.
rx->regex().start_offset += 1;
}
}
String accumulated_result;
size_t next_source_position = 0;
for (auto* result : results) {
size_t result_length = length_of_array_like(global_object, *result);
size_t n_captures = result_length == 0 ? 0 : result_length - 1;
auto matched = result->get(0).value_or(js_undefined());
if (vm.exception())
return {};
auto position_value = result->get(vm.names.index).value_or(js_undefined());
if (vm.exception())
return {};
double position = position_value.to_integer_or_infinity(global_object);
if (vm.exception())
return {};
position = clamp(position, static_cast<double>(0), static_cast<double>(string.length()));
Vector<Value> captures;
for (size_t n = 1; n <= n_captures; ++n) {
auto capture = result->get(n).value_or(js_undefined());
if (vm.exception())
return {};
if (!capture.is_undefined()) {
auto capture_string = capture.to_string(global_object);
if (vm.exception())
return {};
capture = Value(js_string(vm, capture_string));
if (vm.exception())
return {};
}
captures.append(move(capture));
}
auto named_captures = result->get(vm.names.groups).value_or(js_undefined());
if (vm.exception())
return {};
String replacement;
if (replace_value.is_function()) {
Vector<Value> replacer_args { matched };
replacer_args.append(move(captures));
replacer_args.append(Value(position));
replacer_args.append(js_string(vm, string));
if (!named_captures.is_undefined()) {
replacer_args.append(move(named_captures));
}
auto replace_result = vm.call(replace_value.as_function(), js_undefined(), move(replacer_args));
if (vm.exception())
return {};
replacement = replace_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 {};
}
if (position >= next_source_position) {
StringBuilder builder;
builder.append(accumulated_result);
builder.append(string.substring(next_source_position, position - next_source_position));
builder.append(replacement);
accumulated_result = builder.build();
next_source_position = position + matched.as_string().string().length();
}
}
if (next_source_position >= string.length())
return js_string(vm, accumulated_result);
StringBuilder builder;
builder.append(accumulated_result);
builder.append(string.substring(next_source_position));
return js_string(vm, builder.build());
}
} }

View file

@ -48,6 +48,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(test); JS_DECLARE_NATIVE_FUNCTION(test);
JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(symbol_match); JS_DECLARE_NATIVE_FUNCTION(symbol_match);
JS_DECLARE_NATIVE_FUNCTION(symbol_replace);
#define __JS_ENUMERATE(_, flag_name, ...) \ #define __JS_ENUMERATE(_, flag_name, ...) \
JS_DECLARE_NATIVE_GETTER(flag_name); JS_DECLARE_NATIVE_GETTER(flag_name);

View file

@ -108,6 +108,7 @@ void StringPrototype::initialize(GlobalObject& global_object)
define_native_function(vm.names.lastIndexOf, last_index_of, 1, attr); define_native_function(vm.names.lastIndexOf, last_index_of, 1, attr);
define_native_function(vm.names.at, at, 1, attr); define_native_function(vm.names.at, at, 1, attr);
define_native_function(vm.names.match, match, 1, attr); define_native_function(vm.names.match, match, 1, attr);
define_native_function(vm.names.replace, replace, 2, attr);
define_native_function(vm.well_known_symbol_iterator(), symbol_iterator, 0, attr); define_native_function(vm.well_known_symbol_iterator(), symbol_iterator, 0, attr);
} }
@ -675,4 +676,57 @@ JS_DEFINE_NATIVE_FUNCTION(StringPrototype::match)
return rx->invoke(vm.well_known_symbol_match(), js_string(vm, s)); return rx->invoke(vm.well_known_symbol_match(), js_string(vm, s));
} }
JS_DEFINE_NATIVE_FUNCTION(StringPrototype::replace)
{
// https://tc39.es/ecma262/#sec-string.prototype.replace
auto this_object = vm.this_value(global_object);
if (this_object.is_nullish()) {
vm.throw_exception<TypeError>(global_object, ErrorType::ToObjectNullOrUndefined);
return {};
}
auto search_value = vm.argument(0);
auto replace_value = vm.argument(1);
if (!search_value.is_nullish()) {
if (auto* replacer = get_method(global_object, search_value, vm.well_known_symbol_replace()))
return vm.call(*replacer, search_value, this_object, replace_value);
}
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 {};
Optional<size_t> position = string.index_of(search_string);
if (!position.has_value())
return js_string(vm, string);
auto preserved = string.substring(0, position.value());
String replacement;
if (replace_value.is_function()) {
auto result = vm.call(replace_value.as_function(), js_undefined(), search_value, Value(position.value()), 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 {};
}
StringBuilder builder;
builder.append(preserved);
builder.append(replacement);
builder.append(string.substring(position.value() + search_string.length()));
return js_string(vm, builder.build());
}
} }

View file

@ -64,6 +64,7 @@ private:
JS_DECLARE_NATIVE_FUNCTION(last_index_of); JS_DECLARE_NATIVE_FUNCTION(last_index_of);
JS_DECLARE_NATIVE_FUNCTION(at); JS_DECLARE_NATIVE_FUNCTION(at);
JS_DECLARE_NATIVE_FUNCTION(match); JS_DECLARE_NATIVE_FUNCTION(match);
JS_DECLARE_NATIVE_FUNCTION(replace);
JS_DECLARE_NATIVE_FUNCTION(symbol_iterator); JS_DECLARE_NATIVE_FUNCTION(symbol_iterator);
}; };

View file

@ -0,0 +1,104 @@
test("invariants", () => {
expect(String.prototype.replace).toHaveLength(2);
});
test("error cases", () => {
[null, undefined].forEach(value => {
expect(() => {
value.replace("", "");
}).toThrow(TypeError);
});
});
test("basic string replacement", () => {
expect("".replace("", "")).toBe("");
expect("".replace("a", "")).toBe("");
expect("".replace("", "a")).toBe("a");
expect("a".replace("a", "")).toBe("");
expect("a".replace("a", "b")).toBe("b");
expect("aa".replace("a", "b")).toBe("ba");
expect("ca".replace("a", "b")).toBe("cb");
});
test("convertible string replacement", () => {
expect("123".replace(2, "x")).toBe("1x3");
expect("123".replace("2", 4)).toBe("143");
expect("123".replace(2, 4)).toBe("143");
});
test("functional string replacement", () => {
expect(
"a".replace("a", function () {
return "b";
})
).toBe("b");
expect("a".replace("a", () => "b")).toBe("b");
expect(
"abc".replace("b", (search, position, string) => {
expect(search).toBe("b");
expect(position).toBe(1);
expect(string).toBe("abc");
return "x";
})
).toBe("axc");
});
test("basic regex replacement", () => {
expect("".replace(/a/, "")).toBe("");
expect("a".replace(/a/, "")).toBe("");
expect("abc123def".replace(/\D/, "*")).toBe("*bc123def");
expect("123abc456".replace(/\D/, "*")).toBe("123*bc456");
expect("abc123def".replace(/\D/g, "*")).toBe("***123***");
expect("123abc456".replace(/\D/g, "*")).toBe("123***456");
});
test("functional regex replacement", () => {
expect(
"a".replace(/a/, function () {
return "b";
})
).toBe("b");
expect("a".replace(/a/, () => "b")).toBe("b");
expect(
"abc".replace(/\D/, (matched, position, string) => {
expect(matched).toBe("a");
expect(position).toBe(0);
expect(string).toBe("abc");
return "x";
})
).toBe("xbc");
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");
});