Browse Source

String.prototype.replace() (#217)

* String Replace addition

* Function argument now fully implemented

* adding substitutions

* finish off String.prototype.replace

* use is_some()

* fixing string

* clippy
pull/232/head
Jason Williams 5 years ago committed by GitHub
parent
commit
495f0a4868
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      README.md
  2. 156
      src/lib/builtins/string.rs

4
README.md

@ -11,6 +11,10 @@ https://jasonwilliams.github.io/boa/
You can get more verbose errors when running from the command line
## Benchmarks
https://jasonwilliams.github.io/boa/dev/bench/
## Contributing
If you don't already have Rust installed rustup is the recommended tool to use. It will install Rust and allow you to switch between nightly, stable and beta. You can also install additional components.

156
src/lib/builtins/string.rs

@ -9,9 +9,11 @@ use crate::{
exec::Interpreter,
};
use gc::Gc;
use regex::Regex;
use std::{
cmp::{max, min},
f64::NAN,
ops::Deref,
};
/// Create new string [[Construct]]
@ -322,6 +324,116 @@ pub fn includes(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultVa
Ok(to_value(this_string.contains(&search_string)))
}
/// Return either the string itself or the string of the regex equivalent
fn get_regex_string(value: &Value) -> String {
match value.deref() {
ValueData::String(ref body) => body.into(),
ValueData::Object(ref obj) => {
let slots = &*obj.borrow().internal_slots;
if slots.get("RegExpMatcher").is_some() {
// first argument is another `RegExp` object, so copy its pattern and flags
if let Some(body) = slots.get("OriginalSource") {
return from_value(r#body.clone())
.expect("unable to get body from regex value");
}
}
"undefined".to_string()
}
_ => "undefined".to_string(),
}
}
/// <https://tc39.es/ecma262/#sec-string.prototype.replace>
pub fn replace(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
// TODO: Support Symbol replacer
let primitive_val: String = ctx.value_to_rust_string(this);
if args.is_empty() {
return Ok(to_value(primitive_val));
}
let regex_body = get_regex_string(args.get(0).expect("Value needed"));
let re = Regex::new(&regex_body).expect("unable to convert regex to regex object");
let mat = re.find(&primitive_val).expect("unable to find value");
let caps = re
.captures(&primitive_val)
.expect("unable to get capture groups from text");
let replace_value = if args.len() > 1 {
// replace_object could be a string or function or not exist at all
let replace_object: &Value = args.get(1).expect("second argument expected");
match replace_object.deref() {
ValueData::String(val) => {
// https://tc39.es/ecma262/#table-45
let mut result: String = val.to_string();
let re = Regex::new(r"\$(\d)").unwrap();
if val.find("$$").is_some() {
result = val.replace("$$", "$")
}
if val.find("$`").is_some() {
let start_of_match = mat.start();
let slice = &primitive_val[..start_of_match];
result = val.replace("$`", slice);
}
if val.find("$'").is_some() {
let end_of_match = mat.end();
let slice = &primitive_val[end_of_match..];
result = val.replace("$'", slice);
}
if val.find("$&").is_some() {
// get matched value
let matched = caps.get(0).expect("cannot get matched value");
result = val.replace("$&", matched.as_str());
}
// Capture $1, $2, $3 etc
if re.is_match(&result) {
let mat_caps = re.captures(&result).unwrap();
let group_str = mat_caps.get(1).unwrap().as_str();
let group_int = group_str.parse::<usize>().unwrap();
result = re
.replace(result.as_str(), caps.get(group_int).unwrap().as_str())
.to_string()
}
result
}
ValueData::Function(_) => {
// This will return the matched substring first, then captured parenthesized groups later
let mut results: Vec<Value> = caps
.iter()
.map(|capture| to_value(capture.unwrap().as_str()))
.collect();
// Returns the starting byte offset of the match
let start = caps
.get(0)
.expect("Unable to get Byte offset from string for match")
.start();
results.push(to_value(start));
// Push the whole string being examined
results.push(to_value(primitive_val.to_string()));
let result = ctx.call(&replace_object, &this, results).unwrap();
ctx.value_to_rust_string(&result)
}
_ => "undefined".to_string(),
}
} else {
"undefined".to_string()
};
Ok(to_value(primitive_val.replacen(
&mat.as_str(),
&replace_value,
1,
)))
}
/// If searchString appears as a substring of the result of converting this
/// object to a String, at one or more indices that are greater than or equal to
/// position, then the smallest such index is returned; otherwise, -1 is
@ -757,6 +869,7 @@ pub fn create_constructor(global: &Value) -> Value {
make_builtin_fn!(substr, named "substr", with length 2, of proto);
make_builtin_fn!(value_of, named "valueOf", of proto);
make_builtin_fn!(match_all, named "matchAll", with length 1, of proto);
make_builtin_fn!(replace, named "replace", with length 2, of proto);
let string = to_value(string_constructor);
proto.set_field_slice("constructor", string.clone());
@ -872,6 +985,49 @@ mod tests {
);
}
#[test]
fn replace() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var a = "abc";
a = a.replace("a", "2");
a
"#;
forward(&mut engine, init);
let empty = String::from("2bc");
assert_eq!(forward(&mut engine, "a"), empty);
}
#[test]
fn replace_with_function() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var a = "ecmascript is cool";
var p1, p2, p3;
var replacer = (match, cap1, cap2, cap3) => {
p1 = cap1;
p2 = cap2;
p3 = cap3;
return "awesome!";
};
a = a.replace(/c(o)(o)(l)/, replacer);
a;
"#;
forward(&mut engine, init);
assert_eq!(
forward(&mut engine, "a"),
String::from("ecmascript is awesome!")
);
assert_eq!(forward(&mut engine, "p1"), String::from("o"));
assert_eq!(forward(&mut engine, "p2"), String::from("o"));
assert_eq!(forward(&mut engine, "p3"), String::from("l"));
}
#[test]
fn starts_with() {
let realm = Realm::create();

Loading…
Cancel
Save