Browse Source

Implement `String.prototype.replaceAll` (#1469)

pull/1472/head
raskad 3 years ago committed by GitHub
parent
commit
17c412fa6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      boa/src/builtins/regexp/mod.rs
  2. 219
      boa/src/builtins/string/mod.rs
  3. 50
      boa/src/string.rs

6
boa/src/builtins/regexp/mod.rs

@ -1598,7 +1598,11 @@ impl RegExp {
let splitter = constructor
.as_object()
.expect("SpeciesConstructor returned non Object")
.construct(&[JsValue::from(rx), new_flags.into()], &constructor, context)?;
.construct(
&[JsValue::from(rx), new_flags.into()],
&constructor,
context,
)?;
// 11. Let A be ! ArrayCreate(0).
let a = Array::array_create(0, None, context).unwrap();

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

@ -131,6 +131,7 @@ impl BuiltIn for String {
.method(Self::value_of, "valueOf", 0)
.method(Self::match_all, "matchAll", 1)
.method(Self::replace, "replace", 2)
.method(Self::replace_all, "replaceAll", 2)
.method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0)
.method(Self::search, "search", 1)
.method(Self::at, "at", 1)
@ -744,15 +745,17 @@ impl String {
let search_length = search_str.len();
// 8. Let position be ! StringIndexOf(string, searchString, 0).
let position = this_str.find(search_str.as_str());
// 9. If position is -1, return string.
if position.is_none() {
let position = if let Some(p) = this_str.index_of(&search_str, 0) {
p
} else {
return Ok(this_str.into());
}
};
// 10. Let preserved be the substring of string from 0 to position.
let preserved = this_str.get(..position.unwrap());
let preserved = StdString::from_utf16_lossy(
&this_str.encode_utf16().take(position).collect::<Vec<u16>>(),
);
// 11. If functionalReplace is true, then
// 12. Else,
@ -762,11 +765,7 @@ impl String {
.call(
&replace_value,
&JsValue::undefined(),
&[
search_str.into(),
position.unwrap().into(),
this_str.clone().into(),
],
&[search_str.into(), position.into(), this_str.clone().into()],
)?
.to_string(context)?
} else {
@ -778,7 +777,7 @@ impl String {
get_substitution(
search_str.to_string(),
this_str.to_string(),
position.unwrap(),
position,
captures,
JsValue::undefined(),
replace_value.to_string(context)?,
@ -789,15 +788,189 @@ impl String {
// 13. Return the string-concatenation of preserved, replacement, and the substring of string from position + searchLength.
Ok(format!(
"{}{}{}",
preserved.unwrap_or_default(),
preserved,
replacement,
this_str
.get((position.unwrap() + search_length)..)
.unwrap_or_default()
StdString::from_utf16_lossy(
&this_str
.encode_utf16()
.skip(position + search_length)
.collect::<Vec<u16>>()
)
)
.into())
}
/// `22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue )`
///
/// The replaceAll() method returns a new string with all matches of a pattern replaced by a replacement.
///
/// The pattern can be a string or a RegExp, and the replacement can be a string or a function to be called for each match.
///
/// The original string is left unchanged.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replaceall
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
pub(crate) fn replace_all(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> Result<JsValue> {
// 1. Let O be ? RequireObjectCoercible(this value).
let o = this.require_object_coercible(context)?;
let search_value = args.get(0).cloned().unwrap_or_default();
let replace_value = args.get(1).cloned().unwrap_or_default();
// 2. If searchValue is neither undefined nor null, then
if !search_value.is_null_or_undefined() {
// a. Let isRegExp be ? IsRegExp(searchValue).
if let Some(obj) = search_value.as_object() {
// b. If isRegExp is true, then
if obj.is_regexp() {
// i. Let flags be ? Get(searchValue, "flags").
let flags = obj.get("flags", context)?;
// ii. Perform ? RequireObjectCoercible(flags).
flags.require_object_coercible(context)?;
// iii. If ? ToString(flags) does not contain "g", throw a TypeError exception.
if !flags.to_string(context)?.contains('g') {
return context.throw_type_error(
"String.prototype.replaceAll called with a non-global RegExp argument",
);
}
}
}
// c. Let replacer be ? GetMethod(searchValue, @@replace).
let replacer = search_value
.as_object()
.unwrap_or_default()
.get_method(context, WellKnownSymbols::replace())?;
// d. If replacer is not undefined, then
if let Some(replacer) = replacer {
// i. Return ? Call(replacer, searchValue, « O, replaceValue »).
return replacer.call(&search_value, &[o.into(), replace_value], context);
}
}
// 3. Let string be ? ToString(O).
let string = o.to_string(context)?;
// 4. Let searchString be ? ToString(searchValue).
let search_string = search_value.to_string(context)?;
// 5. Let functionalReplace be IsCallable(replaceValue).
let functional_replace = replace_value.is_function();
// 6. If functionalReplace is false, then
let replace_value_string = if !functional_replace {
// a. Set replaceValue to ? ToString(replaceValue).
replace_value.to_string(context)?
} else {
JsString::new("")
};
// 7. Let searchLength be the length of searchString.
let search_length = search_string.encode_utf16().count();
// 8. Let advanceBy be max(1, searchLength).
let advance_by = max(1, search_length);
// 9. Let matchPositions be a new empty List.
let mut match_positions = Vec::new();
// 10. Let position be ! StringIndexOf(string, searchString, 0).
let mut position = string.index_of(&search_string, 0);
// 11. Repeat, while position is not -1,
while let Some(p) = position {
// a. Append position to the end of matchPositions.
match_positions.push(p);
// b. Set position to ! StringIndexOf(string, searchString, position + advanceBy).
position = string.index_of(&search_string, p + advance_by);
}
// 12. Let endOfLastMatch be 0.
let mut end_of_last_match = 0;
// 13. Let result be the empty String.
let mut result = JsString::new("");
// 14. For each element p of matchPositions, do
for p in match_positions {
// a. Let preserved be the substring of string from endOfLastMatch to p.
let preserved = StdString::from_utf16_lossy(
&string
.clone()
.encode_utf16()
.skip(end_of_last_match)
.take(p - end_of_last_match)
.collect::<Vec<u16>>(),
);
// b. If functionalReplace is true, then
// c. Else,
let replacement = if functional_replace {
// i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)).
context
.call(
&replace_value,
&JsValue::undefined(),
&[
search_string.clone().into(),
p.into(),
string.clone().into(),
],
)?
.to_string(context)?
} else {
// i. Assert: Type(replaceValue) is String.
// ii. Let captures be a new empty List.
// iii. Let replacement be ! GetSubstitution(searchString, string, p, captures, undefined, replaceValue).
get_substitution(
search_string.to_string(),
string.to_string(),
p,
Vec::new(),
JsValue::undefined(),
replace_value_string.clone(),
context,
)
.expect("GetSubstitution should never fail here.")
};
// d. Set result to the string-concatenation of result, preserved, and replacement.
result = JsString::new(format!("{}{}{}", result.as_str(), &preserved, &replacement));
// e. Set endOfLastMatch to p + searchLength.
end_of_last_match = p + search_length;
}
// 15. If endOfLastMatch < the length of string, then
if end_of_last_match < string.encode_utf16().count() {
// a. Set result to the string-concatenation of result and the substring of string from endOfLastMatch.
result = JsString::new(format!(
"{}{}",
result.as_str(),
&StdString::from_utf16_lossy(
&string
.encode_utf16()
.skip(end_of_last_match)
.collect::<Vec<u16>>()
)
));
}
// 16. Return result.
Ok(result.into())
}
/// `String.prototype.indexOf( searchValue[, fromIndex] )`
///
/// The `indexOf()` method returns the index within the calling `String` object of the first occurrence
@ -1619,12 +1792,12 @@ pub(crate) fn get_substitution(
// 1. Assert: Type(matched) is String.
// 2. Let matchLength be the number of code units in matched.
let match_length = matched.chars().count();
let match_length = matched.encode_utf16().count();
// 3. Assert: Type(str) is String.
// 4. Let stringLength be the number of code units in str.
let str_length = str.chars().count();
let str_length = str.encode_utf16().count();
// 5. Assert: position ≤ stringLength.
// 6. Assert: captures is a possibly empty List of Strings.
@ -1665,16 +1838,18 @@ pub(crate) fn get_substitution(
// $`
(Some('`'), _) => {
// The replacement is the substring of str from 0 to position.
result.push_str(&str[..position]);
result.push_str(&StdString::from_utf16_lossy(
&str.encode_utf16().take(position).collect::<Vec<u16>>(),
));
}
// $'
(Some('\''), _) => {
// If tailPos ≥ stringLength, the replacement is the empty String.
// Otherwise the replacement is the substring of str from tailPos.
if tail_pos >= str_length {
result.push_str("");
} else {
result.push_str(&str[tail_pos..]);
if tail_pos < str_length {
result.push_str(&StdString::from_utf16_lossy(
&str.encode_utf16().skip(tail_pos).collect::<Vec<u16>>(),
));
}
}
// $nn

50
boa/src/string.rs

@ -186,6 +186,56 @@ impl JsString {
pub fn ptr_eq(x: &Self, y: &Self) -> bool {
x.inner == y.inner
}
/// `6.1.4.1 StringIndexOf ( string, searchValue, fromIndex )`
///
/// Note: Instead of returning an isize with `-1` as the "not found" value,
/// We make use of the type system and return Option<usize> with None as the "not found" value.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-stringindexof
pub(crate) fn index_of(&self, search_value: &Self, from_index: usize) -> Option<usize> {
// 1. Assert: Type(string) is String.
// 2. Assert: Type(searchValue) is String.
// 3. Assert: fromIndex is a non-negative integer.
// 4. Let len be the length of string.
let len = self.encode_utf16().count();
// 5. If searchValue is the empty String and fromIndex ≤ len, return fromIndex.
if search_value.is_empty() && from_index <= len {
return Some(from_index);
}
// 6. Let searchLen be the length of searchValue.
let search_len = search_value.encode_utf16().count();
// 7. For each integer i starting with fromIndex such that i ≤ len - searchLen, in ascending order, do
for i in from_index..=len {
if i as isize > (len as isize - search_len as isize) {
break;
}
// a. Let candidate be the substring of string from i to i + searchLen.
let candidate = String::from_utf16_lossy(
&self
.encode_utf16()
.skip(i)
.take(search_len)
.collect::<Vec<u16>>(),
);
// b. If candidate is the same sequence of code units as searchValue, return i.
if candidate == search_value.as_str() {
return Some(i);
}
}
// 8. Return -1.
None
}
}
impl Finalize for JsString {}

Loading…
Cancel
Save