Browse Source

Fix: String.prototype.replace substitutions, closes #610 (#629)

pull/667/head
João Borges 4 years ago committed by GitHub
parent
commit
be20b65a9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 123
      boa/src/builtins/string/mod.rs
  2. 68
      boa/src/builtins/string/tests.rs

123
boa/src/builtins/string/mod.rs

@ -468,7 +468,10 @@ impl String {
let regex_body = Self::get_regex_string(args.get(0).expect("Value needed")); let regex_body = Self::get_regex_string(args.get(0).expect("Value needed"));
let re = Regex::new(&regex_body).expect("unable to convert regex to regex object"); 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 mat = match re.find(&primitive_val) {
Some(mat) => mat,
None => return Ok(Value::from(primitive_val)),
};
let caps = re let caps = re
.captures(&primitive_val) .captures(&primitive_val)
.expect("unable to get capture groups from text"); .expect("unable to get capture groups from text");
@ -479,39 +482,91 @@ impl String {
match replace_object { match replace_object {
Value::String(val) => { Value::String(val) => {
// https://tc39.es/ecma262/#table-45 // https://tc39.es/ecma262/#table-45
let mut result = val.to_string(); let mut result = StdString::new();
let re = Regex::new(r"\$(\d)").unwrap(); let mut chars = val.chars().peekable();
if val.find("$$").is_some() { let m = caps.len();
result = val.replace("$$", "$")
} while let Some(first) = chars.next() {
if first == '$' {
if val.find("$`").is_some() { let second = chars.next();
let start_of_match = mat.start(); let second_is_digit = second.map_or(false, |ch| ch.is_digit(10));
let slice = &primitive_val[..start_of_match]; // we use peek so that it is still in the iterator if not used
result = val.replace("$`", slice); let third = if second_is_digit { chars.peek() } else { None };
} let third_is_digit = third.map_or(false, |ch| ch.is_digit(10));
if val.find("$'").is_some() { match (second, third) {
let end_of_match = mat.end(); (Some('$'), _) => {
let slice = &primitive_val[end_of_match..]; // $$
result = val.replace("$'", slice); result.push('$');
} }
(Some('&'), _) => {
if val.find("$&").is_some() { // $&
// get matched value let matched = caps.get(0).expect("cannot get matched value");
let matched = caps.get(0).expect("cannot get matched value"); result.push_str(matched.as_str());
result = val.replace("$&", matched.as_str()); }
} (Some('`'), _) => {
// $`
// Capture $1, $2, $3 etc let start_of_match = mat.start();
if re.is_match(&result) { result.push_str(&primitive_val[..start_of_match]);
let mat_caps = re.captures(&result).unwrap(); }
let group_str = mat_caps.get(1).unwrap().as_str(); (Some('\''), _) => {
let group_int = group_str.parse::<usize>().unwrap(); // $'
result = re let end_of_match = mat.end();
.replace(result.as_str(), caps.get(group_int).unwrap().as_str()) result.push_str(&primitive_val[end_of_match..]);
.to_string() }
(Some(second), Some(third))
if second_is_digit && third_is_digit =>
{
// $nn
let tens = second.to_digit(10).unwrap() as usize;
let units = third.to_digit(10).unwrap() as usize;
let nn = 10 * tens + units;
if nn == 0 || nn > m {
result.push(first);
result.push(second);
if let Some(ch) = chars.next() {
result.push(ch);
}
} else {
let group = match caps.get(nn) {
Some(text) => text.as_str(),
None => "",
};
result.push_str(group);
chars.next(); // consume third
}
}
(Some(second), _) if second_is_digit => {
// $n
let n = second.to_digit(10).unwrap() as usize;
if n == 0 || n > m {
result.push(first);
result.push(second);
} else {
let group = match caps.get(n) {
Some(text) => text.as_str(),
None => "",
};
result.push_str(group);
}
}
(Some('<'), _) => {
// $<
todo!("named capture groups")
}
_ => {
// $?, ? is none of the above
// we can consume second because it isn't $
result.push(first);
if let Some(second) = second {
result.push(second);
}
}
}
} else {
result.push(first);
}
} }
result result

68
boa/src/builtins/string/tests.rs

@ -218,6 +218,74 @@ fn replace() {
assert_eq!(forward(&mut engine, "a"), "\"2bc\""); assert_eq!(forward(&mut engine, "a"), "\"2bc\"");
} }
#[test]
fn replace_no_match() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let init = r#"
var a = "abc";
a = a.replace(/d/, "$&$&");
"#;
forward(&mut engine, init);
assert_eq!(forward(&mut engine, "a"), "\"abc\"");
}
#[test]
fn replace_with_capture_groups() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let init = r#"
var re = /(\w+)\s(\w+)/;
var a = "John Smith";
a = a.replace(re, '$2, $1');
a
"#;
forward(&mut engine, init);
assert_eq!(forward(&mut engine, "a"), "\"Smith, John\"");
}
#[test]
fn replace_with_tenth_capture_group() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let init = r#"
var re = /(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)/;
var a = "0123456789";
let res = a.replace(re, '$10');
"#;
forward(&mut engine, init);
assert_eq!(forward(&mut engine, "res"), "\"9\"");
}
#[test]
fn replace_substitutions() {
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let init = r#"
var re = / two /;
var a = "one two three";
var dollar = a.replace(re, " $$ ");
var matched = a.replace(re, "$&$&");
var start = a.replace(re, " $` ");
var end = a.replace(re, " $' ");
var no_sub = a.replace(re, " $_ ");
"#;
forward(&mut engine, init);
assert_eq!(forward(&mut engine, "dollar"), "\"one $ three\"");
assert_eq!(forward(&mut engine, "matched"), "\"one two two three\"");
assert_eq!(forward(&mut engine, "start"), "\"one one three\"");
assert_eq!(forward(&mut engine, "end"), "\"one three three\"");
assert_eq!(forward(&mut engine, "no_sub"), "\"one $_ three\"");
}
#[test] #[test]
fn replace_with_function() { fn replace_with_function() {
let realm = Realm::create(); let realm = Realm::create();

Loading…
Cancel
Save