use crate::{
exec::Interpreter,
js::{
function::NativeFunctionData,
object::{ObjectKind, Property, PROTOTYPE},
value::{from_value, to_value, ResultValue, Value, ValueData},
},
};
use gc::Gc;
use gc_derive::{Finalize, Trace};
use std::{
cmp::{max, min},
f64::NAN,
};
pub fn make_string(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
this.set_kind(ObjectKind::String);
this.set_internal_slot("PrimitiveValue", args[0].clone());
Ok(this.clone())
}
pub fn get_string_length(this: &Value, _: &[Value], _: &Interpreter) -> ResultValue {
let this_str: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
Ok(to_value::<i32>(this_str.chars().count() as i32))
}
pub fn to_string(this: &Value, _: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val = this.get_internal_slot("PrimitiveValue");
Ok(to_value(format!("{}", primitive_val).to_string()))
}
pub fn char_at(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let pos: i32 = from_value(args[0].clone()).unwrap();
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).unwrap(),
))
}
pub fn char_code_at(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let length = primitive_val.chars().count();
let pos: i32 = from_value(args[0].clone()).unwrap();
if pos >= length as i32 || pos < 0 {
return Ok(to_value(NAN));
}
let utf16_val = primitive_val.encode_utf16().nth(pos as usize).unwrap();
Ok(to_value(f64::from(utf16_val)))
}
pub fn concat(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let mut new_str = primitive_val.clone();
for arg in args {
let concat_str: String = from_value(arg.clone()).unwrap();
new_str.push_str(&concat_str);
}
Ok(to_value(new_str))
}
pub fn repeat(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let repeat_times: usize = from_value(args[0].clone()).unwrap();
Ok(to_value(primitive_val.repeat(repeat_times)))
}
pub fn slice(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let start: i32 = from_value(args[0].clone()).unwrap();
let end: i32 = from_value(args[1].clone()).unwrap();
let length: i32 = primitive_val.chars().count() as i32;
let from: i32 = if start < 0 {
max(length + start, 0)
} else {
min(start, length)
};
let to: i32 = if end < 0 {
max(length + end, 0)
} else {
min(end, length)
};
let span = max(to - from, 0);
let mut new_str = String::new();
for i in from..from + span {
new_str.push(primitive_val.chars().nth(i as usize).unwrap());
}
Ok(to_value(new_str))
}
pub fn starts_with(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let search_string: String = from_value(args[0].clone()).unwrap();
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[1].clone()).unwrap()
};
let start = min(max(position, 0), length);
let end = start + 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], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let search_string: String = from_value(args[0].clone()).unwrap();
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[1].clone()).unwrap()
};
let end = min(max(end_position, 0), length);
let start = end - 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], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let search_string: String = from_value(args[0].clone()).unwrap();
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args[1].clone()).unwrap()
};
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], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let search_string: String = from_value(args[0].clone()).unwrap();
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args[1].clone()).unwrap()
};
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], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
let search_string: String = from_value(args[0].clone()).unwrap();
let length: i32 = primitive_val.chars().count() as i32;
let position: i32 = if args.len() < 2 {
0
} else {
from_value(args[1].clone()).unwrap()
};
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))
}
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 - 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(concat_fill_str + &primitive))
} else {
Ok(to_value(primitive + &concat_fill_str))
}
}
pub fn pad_end(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
if args.is_empty() {
return Err(to_value("padEnd requires maxLength argument"));
}
let max_length = from_value(args[0].clone()).unwrap();
let fill_string: Option<String> = match args.len() {
1 => None,
_ => Some(from_value(args[1].clone()).unwrap()),
};
string_pad(primitive_val, max_length, fill_string, false)
}
pub fn pad_start(this: &Value, args: &[Value], _: &Interpreter) -> ResultValue {
let primitive_val: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
if args.is_empty() {
return Err(to_value("padStart requires maxLength argument"));
}
let max_length = from_value(args[0].clone()).unwrap();
let fill_string: Option<String> = match args.len() {
1 => None,
_ => Some(from_value(args[1].clone()).unwrap()),
};
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], _: &Interpreter) -> ResultValue {
let this_str: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
Ok(to_value(this_str.trim_matches(is_trimmable_whitespace)))
}
pub fn trim_start(this: &Value, _: &[Value], _: &Interpreter) -> ResultValue {
let this_str: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
Ok(to_value(
this_str.trim_start_matches(is_trimmable_whitespace),
))
}
pub fn trim_end(this: &Value, _: &[Value], _: &Interpreter) -> ResultValue {
let this_str: String = from_value(this.get_internal_slot("PrimitiveValue")).unwrap();
Ok(to_value(this_str.trim_end_matches(is_trimmable_whitespace)))
}
pub fn _create(global: &Value) -> Value {
let string = to_value(make_string as NativeFunctionData);
let proto = ValueData::new_obj(Some(global));
let prop = Property {
configurable: false,
enumerable: false,
writable: false,
value: Gc::new(ValueData::Undefined),
get: to_value(get_string_length as NativeFunctionData),
set: Gc::new(ValueData::Undefined),
};
proto.set_prop_slice("length", prop);
proto.set_field_slice("charAt", to_value(char_at as NativeFunctionData));
proto.set_field_slice("charCodeAt", to_value(char_code_at as NativeFunctionData));
proto.set_field_slice("toString", to_value(to_string as NativeFunctionData));
proto.set_field_slice("concat", to_value(concat as NativeFunctionData));
proto.set_field_slice("repeat", to_value(repeat as NativeFunctionData));
proto.set_field_slice("slice", to_value(slice as NativeFunctionData));
proto.set_field_slice("startsWith", to_value(starts_with as NativeFunctionData));
proto.set_field_slice("endsWith", to_value(ends_with as NativeFunctionData));
proto.set_field_slice("includes", to_value(includes as NativeFunctionData));
proto.set_field_slice("indexOf", to_value(index_of as NativeFunctionData));
proto.set_field_slice("lastIndexOf", to_value(last_index_of as NativeFunctionData));
proto.set_field_slice("padEnd", to_value(pad_end as NativeFunctionData));
proto.set_field_slice("padStart", to_value(pad_start as NativeFunctionData));
proto.set_field_slice("trim", to_value(trim as NativeFunctionData));
proto.set_field_slice("trimStart", to_value(trim_start as NativeFunctionData));
string.set_field_slice(PROTOTYPE, proto);
string
}
pub fn init(global: &Value) {
global.set_field_slice("String", _create(global));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::Executor;
use crate::forward;
#[test]
fn check_string_constructor_is_function() {
let global = ValueData::new_obj(None);
let string_constructor = _create(&global);
assert_eq!(string_constructor.is_function(), true);
}
#[test]
#[test]
fn concat() {
let mut engine = Executor::new();
let init = r#"
const hello = new String('Hello, ');
const world = new String('world! ');
const 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");
}
#[test]
fn repeat() {
let mut engine = Executor::new();
let init = r#"
const empty = new String('');
const en = new String('english');
const 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 mut engine = Executor::new();
let init = r#"
const empty = new String('');
const en = new String('english');
const zh = new String('中文');
"#;
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);
}
#[test]
fn ends_with() {
let mut engine = Executor::new();
let init = r#"
const empty = new String('');
const en = new String('english');
const zh = new String('中文');
"#;
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);
}
}