diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 9720f1b33d..e5a613702d 100644 --- a/boa/src/builtins/string/mod.rs +++ b/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 re = Regex::new(®ex_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 .captures(&primitive_val) .expect("unable to get capture groups from text"); @@ -479,39 +482,91 @@ impl String { match replace_object { Value::String(val) => { // https://tc39.es/ecma262/#table-45 - let mut result = 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::().unwrap(); - result = re - .replace(result.as_str(), caps.get(group_int).unwrap().as_str()) - .to_string() + let mut result = StdString::new(); + let mut chars = val.chars().peekable(); + + let m = caps.len(); + + while let Some(first) = chars.next() { + if first == '$' { + let second = chars.next(); + let second_is_digit = second.map_or(false, |ch| ch.is_digit(10)); + // we use peek so that it is still in the iterator if not used + let third = if second_is_digit { chars.peek() } else { None }; + let third_is_digit = third.map_or(false, |ch| ch.is_digit(10)); + + match (second, third) { + (Some('$'), _) => { + // $$ + result.push('$'); + } + (Some('&'), _) => { + // $& + let matched = caps.get(0).expect("cannot get matched value"); + result.push_str(matched.as_str()); + } + (Some('`'), _) => { + // $` + let start_of_match = mat.start(); + result.push_str(&primitive_val[..start_of_match]); + } + (Some('\''), _) => { + // $' + let end_of_match = mat.end(); + result.push_str(&primitive_val[end_of_match..]); + } + (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 diff --git a/boa/src/builtins/string/tests.rs b/boa/src/builtins/string/tests.rs index 2ba8b048d6..3666f4a1cb 100644 --- a/boa/src/builtins/string/tests.rs +++ b/boa/src/builtins/string/tests.rs @@ -218,6 +218,74 @@ fn replace() { 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] fn replace_with_function() { let realm = Realm::create();