From 495f0a48686b362613b0befc8a6e8a91563a81f6 Mon Sep 17 00:00:00 2001 From: Jason Williams <936006+jasonwilliams@users.noreply.github.com> Date: Mon, 20 Jan 2020 23:57:18 +0000 Subject: [PATCH] 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 --- README.md | 4 + src/lib/builtins/string.rs | 156 +++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/README.md b/README.md index 3949e67dcc..13672cc670 100644 --- a/README.md +++ b/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. diff --git a/src/lib/builtins/string.rs b/src/lib/builtins/string.rs index b746e3266b..3135e44102 100644 --- a/src/lib/builtins/string.rs +++ b/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(), + } +} + +/// +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(®ex_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::().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 = 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();