use crate::{
builtins::{
function::NativeFunctionData,
object::{Object, ObjectKind, PROTOTYPE},
property::Property,
regexp::{make_regexp, match_all as regexp_match_all, r#match as regexp_match},
value::{from_value, to_value, ResultValue, Value, ValueData},
},
exec::Interpreter,
};
use gc::Gc;
use std::{
cmp::{max, min},
f64::NAN,
};
pub fn make_string(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.set_kind(ObjectKind::String);
this.set_internal_slot(
"StringData",
args.get(0)
.expect("failed to get StringData for make_string()")
.clone(),
);
Ok(this.clone())
}
pub fn call_string(_: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let arg = match args.get(0) {
Some(v) => v.clone(),
None => Gc::new(ValueData::Undefined),
};
if arg.is_undefined() {
return Ok(to_value(""));
}
Ok(to_value(arg.to_string()))
}
pub fn to_string(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue {
let primitive_val = this.get_internal_slot("StringData");
Ok(to_value(format!("{}", primitive_val).to_string()))
}
pub fn char_at(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val = ctx.value_to_rust_string(this);
let pos: i32 = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length = primitive_val.chars().count();
if pos >= length as i32 || pos < 0 {
return Ok(to_value::<String>(String::new()));
}
Ok(to_value::<char>(
primitive_val
.chars()
.nth(pos as usize)
.expect("failed to get value"),
))
}
pub fn char_code_at(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let length = primitive_val.chars().count();
let pos: i32 = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
if pos >= length as i32 || pos < 0 {
return Ok(to_value(NAN));
}
let utf16_val = primitive_val
.encode_utf16()
.nth(pos as usize)
.expect("failed to get utf16 value");
Ok(to_value(f64::from(utf16_val)))
}
pub fn concat(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let mut new_str = primitive_val.clone();
for arg in args {
let concat_str: String = from_value(arg.clone()).expect("failed to get argument value");
new_str.push_str(&concat_str);
}
Ok(to_value(new_str))
}
pub fn repeat(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let repeat_times: usize = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
Ok(to_value(primitive_val.repeat(repeat_times)))
}
pub fn slice(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let start: i32 = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let end: i32 = from_value(
args.get(1)
.expect("failed to get argument in slice")
.clone(),
)
.expect("failed to parse argument");
let length: i32 = primitive_val.chars().count() as i32;
let from: i32 = if start < 0 {
max(length.wrapping_add(start), 0)
} else {
min(start, length)
};
let to: i32 = if end < 0 {
max(length.wrapping_add(end), 0)
} else {
min(end, length)
};
let span = max(to.wrapping_sub(from), 0);
let mut new_str = String::new();
for i in from..from.wrapping_add(span) {
new_str.push(
primitive_val
.chars()
.nth(i as usize)
.expect("Could not get nth char"),
);
}
Ok(to_value(new_str))
}
pub fn starts_with(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let search_string: String = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length: i32 = primitive_val.chars().count() as i32;
let search_length: i32 = search_string.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args.get(1).expect("failed to get arg").clone()).expect("failed to get argument")
};
let start = min(max(position, 0), length);
let end = start.wrapping_add(search_length);
if end > length {
Ok(to_value(false))
} else {
let this_string: String = primitive_val.chars().skip(start as usize).collect();
Ok(to_value(this_string.starts_with(&search_string)))
}
}
pub fn ends_with(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let search_string: String = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length: i32 = primitive_val.chars().count() as i32;
let search_length: i32 = search_string.chars().count() as i32;
let end_position: i32 = if args.len() < 2 {
length
} else {
from_value(args.get(1).expect("Could not get argumetn").clone())
.expect("Could not convert value to i32")
};
let end = min(max(end_position, 0), length);
let start = end.wrapping_sub(search_length);
if start < 0 {
Ok(to_value(false))
} else {
let this_string: String = primitive_val.chars().take(end as usize).collect();
Ok(to_value(this_string.ends_with(&search_string)))
}
}
pub fn includes(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let search_string: String = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args.get(1).expect("Could not get argument").clone())
.expect("Could not convert value to i32")
};
let start = min(max(position, 0), length);
let this_string: String = primitive_val.chars().skip(start as usize).collect();
Ok(to_value(this_string.contains(&search_string)))
}
pub fn index_of(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let search_string: String = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args.get(1).expect("Could not get argument").clone())
.expect("Could not convert value to i32")
};
let start = min(max(position, 0), length);
for index in start..length {
let this_string: String = primitive_val.chars().skip(index as usize).collect();
if this_string.starts_with(&search_string) {
return Ok(to_value(index));
}
}
Ok(to_value(-1))
}
pub fn last_index_of(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let search_string: String = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args.get(1).expect("Could not get argument").clone())
.expect("Could not convert value to i32")
};
let start = min(max(position, 0), length);
let mut highest_index: i32 = -1;
for index in start..length {
let this_string: String = primitive_val.chars().skip(index as usize).collect();
if this_string.starts_with(&search_string) {
highest_index = index;
}
}
Ok(to_value(highest_index))
}
pub fn r#match(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let re = make_regexp(&to_value(Object::default()), &[args[0].clone()], ctx)?.clone();
regexp_match(&re, ctx.value_to_rust_string(this), ctx)
}
fn string_pad(
primitive: String,
max_length: i32,
fill_string: Option<String>,
at_start: bool,
) -> ResultValue {
let primitive_length = primitive.len() as i32;
if max_length <= primitive_length {
return Ok(to_value(primitive));
}
let filler = match fill_string {
Some(filler) => filler,
None => String::from(" "),
};
if filler == "" {
return Ok(to_value(primitive));
}
let fill_len = max_length.wrapping_sub(primitive_length);
let mut fill_str = String::new();
while fill_str.len() < fill_len as usize {
fill_str.push_str(&filler);
}
let concat_fill_str: String = fill_str.chars().take(fill_len as usize).collect();
if at_start {
Ok(to_value(format!("{}{}", concat_fill_str, &primitive)))
} else {
Ok(to_value(format!("{}{}", primitive, &concat_fill_str)))
}
}
pub fn pad_end(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
if args.is_empty() {
return Err(to_value("padEnd requires maxLength argument"));
}
let max_length = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let fill_string: Option<String> = match args.len() {
1 => None,
_ => Some(
from_value(args.get(1).expect("Could not get argument").clone())
.expect("Could not convert value to Option<String>"),
),
};
string_pad(primitive_val, max_length, fill_string, false)
}
pub fn pad_start(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
if args.is_empty() {
return Err(to_value("padStart requires maxLength argument"));
}
let max_length = from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method");
let fill_string: Option<String> = match args.len() {
1 => None,
_ => Some(
from_value(args.get(1).expect("Could not get argument").clone())
.expect("Could not convert value to Option<String>"),
),
};
string_pad(primitive_val, max_length, fill_string, true)
}
fn is_trimmable_whitespace(c: char) -> bool {
match c {
'\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' |
'\u{1680}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' |
'\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' => true,
_ => false,
}
}
pub fn trim(this: &Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue {
let this_str: String = ctx.value_to_rust_string(this);
Ok(to_value(this_str.trim_matches(is_trimmable_whitespace)))
}
pub fn trim_start(this: &Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue {
let this_str: String = ctx.value_to_rust_string(this);
Ok(to_value(
this_str.trim_start_matches(is_trimmable_whitespace),
))
}
pub fn trim_end(this: &Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue {
let this_str: String = ctx.value_to_rust_string(this);
Ok(to_value(this_str.trim_end_matches(is_trimmable_whitespace)))
}
pub fn to_lowercase(this: &Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue {
let this_str: String = ctx.value_to_rust_string(this);
Ok(to_value(this_str.to_lowercase()))
}
pub fn to_uppercase(this: &Value, _: &[Value], ctx: &mut Interpreter) -> ResultValue {
let this_str: String = ctx.value_to_rust_string(this);
Ok(to_value(this_str.to_uppercase()))
}
pub fn substring(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let start = if args.is_empty() {
0
} else {
from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method")
};
let length: i32 = primitive_val.chars().count() as i32;
let end = if args.len() < 2 {
length
} else {
from_value(args.get(1).expect("Could not get argument").clone())
.expect("failed to parse argument for String method")
};
let final_start = min(max(start, 0), length);
let final_end = min(max(end, 0), length);
let from = min(final_start, final_end) as usize;
let to = max(final_start, final_end) as usize;
let extracted_string: String = primitive_val
.chars()
.skip(from)
.take(to.wrapping_sub(from))
.collect();
Ok(to_value(extracted_string))
}
pub fn substr(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let primitive_val: String = ctx.value_to_rust_string(this);
let mut start = if args.is_empty() {
0
} else {
from_value(
args.get(0)
.expect("failed to get argument for String method")
.clone(),
)
.expect("failed to parse argument for String method")
};
let length: i32 = primitive_val.chars().count() as i32;
let end = if args.len() < 2 {
i32::max_value()
} else {
from_value(args.get(1).expect("Could not get argument").clone())
.expect("failed to parse argument for String method")
};
if start < 0 {
start = max(length.wrapping_add(start), 0);
}
let result_length = min(max(end, 0), length.wrapping_sub(start));
if result_length <= 0 {
Ok(to_value("".to_string()))
} else {
let extracted_string: String = primitive_val
.chars()
.skip(start as usize)
.take(result_length as usize)
.collect();
Ok(to_value(extracted_string))
}
}
pub fn value_of(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
to_string(this, args, ctx)
}
pub fn match_all(this: &Value, args: &[Value], ctx: &mut Interpreter) -> ResultValue {
let re: Value = match args.get(0) {
Some(arg) => {
if arg == &Gc::new(ValueData::Null) {
make_regexp(
&to_value(Object::default()),
&[
to_value(ctx.value_to_rust_string(arg)),
to_value(String::from("g")),
],
ctx,
)
} else if arg == &Gc::new(ValueData::Undefined) {
make_regexp(
&to_value(Object::default()),
&[Gc::new(ValueData::Undefined), to_value(String::from("g"))],
ctx,
)
} else {
from_value(arg.clone()).map_err(to_value)
}
}
None => make_regexp(
&to_value(Object::default()),
&[to_value(String::new()), to_value(String::from("g"))],
ctx,
),
}?
.clone();
regexp_match_all(&re, ctx.value_to_rust_string(this))
}
pub fn create_constructor(global: &Value) -> Value {
let mut string_constructor = Object::default();
string_constructor.kind = ObjectKind::Function;
string_constructor.set_internal_method("construct", make_string);
string_constructor.set_internal_method("call", call_string);
let proto = ValueData::new_obj(Some(global));
let prop = Property::default().value(to_value(0_i32));
proto.set_prop_slice("length", prop);
make_builtin_fn!(char_at, named "charAt", with length 1, of proto);
make_builtin_fn!(char_code_at, named "charCodeAt", with length 1, of proto);
make_builtin_fn!(to_string, named "toString", of proto);
make_builtin_fn!(concat, named "concat", with length 1, of proto);
make_builtin_fn!(repeat, named "repeat", with length 1, of proto);
make_builtin_fn!(slice, named "slice", with length 2, of proto);
make_builtin_fn!(starts_with, named "startsWith", with length 1, of proto);
make_builtin_fn!(ends_with, named "endsWith", with length 1, of proto);
make_builtin_fn!(includes, named "includes", with length 1, of proto);
make_builtin_fn!(index_of, named "indexOf", with length 1, of proto);
make_builtin_fn!(last_index_of, named "lastIndexOf", with length 1, of proto);
make_builtin_fn!(r#match, named "match", with length 1, of proto);
make_builtin_fn!(pad_end, named "padEnd", with length 1, of proto);
make_builtin_fn!(pad_start, named "padStart", with length 1, of proto);
make_builtin_fn!(trim, named "trim", of proto);
make_builtin_fn!(trim_start, named "trimStart", of proto);
make_builtin_fn!(to_lowercase, named "toLowerCase", of proto);
make_builtin_fn!(to_uppercase, named "toUpperCase", of proto);
make_builtin_fn!(substring, named "substring", with length 2, of proto);
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);
let string = to_value(string_constructor);
proto.set_field_slice("constructor", string.clone());
string.set_field_slice(PROTOTYPE, proto);
string
}
pub fn init(global: &Value) {
global.set_field_slice("String", create_constructor(global));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::Executor;
use crate::realm::Realm;
use crate::{forward, forward_val};
#[test]
fn check_string_constructor_is_function() {
let global = ValueData::new_obj(None);
let string_constructor = create_constructor(&global);
assert_eq!(string_constructor.is_function(), true);
}
#[test]
#[test]
fn concat() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var hello = new String('Hello, ');
var world = new String('world! ');
var nice = new String('Have a nice day.');
"#;
forward(&mut engine, init);
let _a = forward(&mut engine, "hello.concat(world, nice)");
let _b = forward(&mut engine, "hello + world + nice");
}
#[allow(clippy::result_unwrap_used)]
#[test]
fn construct_and_call() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var hello = new String('Hello');
var world = String('world');
"#;
forward(&mut engine, init);
let hello = forward_val(&mut engine, "hello").unwrap();
let world = forward_val(&mut engine, "world").unwrap();
assert_eq!(hello.is_object(), true);
assert_eq!(world.is_string(), true);
}
#[test]
fn repeat() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var empty = new String('');
var en = new String('english');
var zh = new String('中文');
"#;
forward(&mut engine, init);
let empty = String::from("");
assert_eq!(forward(&mut engine, "empty.repeat(0)"), empty);
assert_eq!(forward(&mut engine, "empty.repeat(1)"), empty);
assert_eq!(forward(&mut engine, "en.repeat(0)"), empty);
assert_eq!(forward(&mut engine, "zh.repeat(0)"), empty);
assert_eq!(
forward(&mut engine, "en.repeat(1)"),
String::from("english")
);
assert_eq!(
forward(&mut engine, "zh.repeat(2)"),
String::from("中文中文")
);
}
#[test]
fn starts_with() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var empty = new String('');
var en = new String('english');
var zh = new String('中文');
var emptyLiteral = '';
var enLiteral = 'english';
var zhLiteral = '中文';
"#;
forward(&mut engine, init);
let pass = String::from("true");
assert_eq!(forward(&mut engine, "empty.startsWith('')"), pass);
assert_eq!(forward(&mut engine, "en.startsWith('e')"), pass);
assert_eq!(forward(&mut engine, "zh.startsWith('中')"), pass);
assert_eq!(forward(&mut engine, "emptyLiteral.startsWith('')"), pass);
assert_eq!(forward(&mut engine, "enLiteral.startsWith('e')"), pass);
assert_eq!(forward(&mut engine, "zhLiteral.startsWith('中')"), pass);
}
#[test]
fn ends_with() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var empty = new String('');
var en = new String('english');
var zh = new String('中文');
var emptyLiteral = '';
var enLiteral = 'english';
var zhLiteral = '中文';
"#;
forward(&mut engine, init);
let pass = String::from("true");
assert_eq!(forward(&mut engine, "empty.endsWith('')"), pass);
assert_eq!(forward(&mut engine, "en.endsWith('h')"), pass);
assert_eq!(forward(&mut engine, "zh.endsWith('文')"), pass);
assert_eq!(forward(&mut engine, "emptyLiteral.endsWith('')"), pass);
assert_eq!(forward(&mut engine, "enLiteral.endsWith('h')"), pass);
assert_eq!(forward(&mut engine, "zhLiteral.endsWith('文')"), pass);
}
#[test]
fn match_all() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
assert_eq!(
forward(&mut engine, "'aa'.matchAll(null).length"),
String::from("0")
);
assert_eq!(
forward(&mut engine, "'aa'.matchAll(/b/).length"),
String::from("0")
);
assert_eq!(
forward(&mut engine, "'aa'.matchAll(/a/).length"),
String::from("1")
);
assert_eq!(
forward(&mut engine, "'aa'.matchAll(/a/g).length"),
String::from("2")
);
forward(
&mut engine,
"var groupMatches = 'test1test2'.matchAll(/t(e)(st(\\d?))/g)",
);
assert_eq!(
forward(&mut engine, "groupMatches.length"),
String::from("2")
);
assert_eq!(
forward(&mut engine, "groupMatches[0][1]"),
String::from("e")
);
assert_eq!(
forward(&mut engine, "groupMatches[0][2]"),
String::from("st1")
);
assert_eq!(
forward(&mut engine, "groupMatches[0][3]"),
String::from("1")
);
assert_eq!(
forward(&mut engine, "groupMatches[1][3]"),
String::from("2")
);
assert_eq!(
forward(
&mut engine,
"'test1test2'.matchAll(/t(e)(st(\\d?))/).length"
),
String::from("1")
);
let init = r#"
var regexp = RegExp('foo[a-z]*','g');
var str = 'table football, foosball';
var matches = str.matchAll(regexp);
"#;
forward(&mut engine, init);
assert_eq!(
forward(&mut engine, "matches[0][0]"),
String::from("football")
);
assert_eq!(forward(&mut engine, "matches[0].index"), String::from("6"));
assert_eq!(
forward(&mut engine, "matches[1][0]"),
String::from("foosball")
);
assert_eq!(forward(&mut engine, "matches[1].index"), String::from("16"));
}
#[test]
fn test_match() {
let realm = Realm::create();
let mut engine = Executor::new(realm);
let init = r#"
var str = new String('The Quick Brown Fox Jumps Over The Lazy Dog');
var result1 = str.match(/quick\s(brown).+?(jumps)/i);
var result2 = str.match(/[A-Z]/g);
var result3 = str.match("T");
var result4 = str.match(RegExp("B", 'g'));
"#;
forward(&mut engine, init);
assert_eq!(forward(&mut engine, "result1[0]"), "Quick Brown Fox Jumps");
assert_eq!(forward(&mut engine, "result1[1]"), "Brown");
assert_eq!(forward(&mut engine, "result1[2]"), "Jumps");
assert_eq!(forward(&mut engine, "result1.index"), "4");
assert_eq!(
forward(&mut engine, "result1.input"),
"The Quick Brown Fox Jumps Over The Lazy Dog"
);
assert_eq!(forward(&mut engine, "result2[0]"), "T");
assert_eq!(forward(&mut engine, "result2[1]"), "Q");
assert_eq!(forward(&mut engine, "result2[2]"), "B");
assert_eq!(forward(&mut engine, "result2[3]"), "F");
assert_eq!(forward(&mut engine, "result2[4]"), "J");
assert_eq!(forward(&mut engine, "result2[5]"), "O");
assert_eq!(forward(&mut engine, "result2[6]"), "T");
assert_eq!(forward(&mut engine, "result2[7]"), "L");
assert_eq!(forward(&mut engine, "result2[8]"), "D");
assert_eq!(forward(&mut engine, "result3[0]"), "T");
assert_eq!(forward(&mut engine, "result3.index"), "0");
assert_eq!(
forward(&mut engine, "result3.input"),
"The Quick Brown Fox Jumps Over The Lazy Dog"
);
assert_eq!(forward(&mut engine, "result4[0]"), "B");
}
}