mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: 修复 String 原型方法边界条件
This commit is contained in:
@@ -297,16 +297,22 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let s = this_string(vm, args);
|
||||
let n = char_len(&s) as i32;
|
||||
let mut start = if args.len() > 1 {
|
||||
(vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32)
|
||||
.max(0)
|
||||
.min(n)
|
||||
let v = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1]));
|
||||
if v.is_nan() || v < 0.0 {
|
||||
0
|
||||
} else {
|
||||
(v as i32).min(n)
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut end = if args.len() > 2 {
|
||||
(vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as i32)
|
||||
.max(0)
|
||||
.min(n)
|
||||
let v = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
|
||||
if v.is_nan() || v < 0.0 {
|
||||
0
|
||||
} else {
|
||||
(v as i32).min(n)
|
||||
}
|
||||
} else {
|
||||
n
|
||||
};
|
||||
@@ -317,6 +323,84 @@ pub fn string_substring<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
NativeResult::Ok(vm.new_string(result))
|
||||
}
|
||||
|
||||
pub fn string_substr<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.substr called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
let len = char_len(&s) as isize;
|
||||
let start = if args.len() > 1 {
|
||||
let n = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as isize;
|
||||
if n < 0 {
|
||||
(len + n).max(0)
|
||||
} else {
|
||||
n.min(len)
|
||||
}
|
||||
} else {
|
||||
0
|
||||
} as usize;
|
||||
let length = if args.len() > 2 {
|
||||
(oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as isize).max(0) as usize
|
||||
} else {
|
||||
len as usize - start
|
||||
};
|
||||
let result = take_chars(&s[byte_index_at_char(&s, start)..], length.min(len as usize - start));
|
||||
NativeResult::Ok(vm.new_string(&result))
|
||||
}
|
||||
|
||||
pub fn string_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.at called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
let len = char_len(&s) as i32;
|
||||
let idx = if args.len() > 1 {
|
||||
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[1])) as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let idx = if idx < 0 { len + idx } else { idx };
|
||||
if idx < 0 || idx >= len {
|
||||
return NativeResult::Ok(JsValue::undefined());
|
||||
}
|
||||
let ch = s.chars().nth(idx as usize).unwrap().to_string();
|
||||
NativeResult::Ok(vm.new_string(&ch))
|
||||
}
|
||||
|
||||
pub fn string_last_index_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
let n = char_len(&s);
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::int(-1));
|
||||
}
|
||||
let search = as_string(vm, vm.reg(args[1]));
|
||||
let pos = if args.len() > 2 {
|
||||
let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
|
||||
if p.is_nan() {
|
||||
n
|
||||
} else {
|
||||
(p as usize).min(n)
|
||||
}
|
||||
} else {
|
||||
n
|
||||
};
|
||||
|
||||
if search.is_empty() {
|
||||
return NativeResult::Ok(JsValue::int(pos as i32));
|
||||
}
|
||||
|
||||
let end_byte = byte_index_at_char(&s, (pos + 1).min(n));
|
||||
let haystack = &s[..end_byte];
|
||||
|
||||
if search.len() == 1 {
|
||||
if let Some(idx) = memchr::memrchr(search.as_bytes()[0], haystack.as_bytes()) {
|
||||
let result = char_len(&s[..idx]);
|
||||
return NativeResult::Ok(JsValue::int(result as i32));
|
||||
}
|
||||
} else if let Some(idx) = haystack.rfind(&search) {
|
||||
let result = char_len(&s[..idx]);
|
||||
return NativeResult::Ok(JsValue::int(result as i32));
|
||||
}
|
||||
NativeResult::Ok(JsValue::int(-1))
|
||||
}
|
||||
|
||||
pub fn string_to_upper_case<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.toUpperCase called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
@@ -437,7 +521,7 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
}
|
||||
let sep_val = vm.reg(args[1]);
|
||||
let limit = if args.len() > 2 {
|
||||
vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize
|
||||
oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])) as usize
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
@@ -452,10 +536,30 @@ pub fn string_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
return NativeResult::Ok(make_string_array(vm, &parts));
|
||||
}
|
||||
};
|
||||
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
|
||||
let raw: Vec<&str> = regex.split(&s).collect();
|
||||
let parts: Vec<String> = raw.iter().map(|p| p.to_string()).take(limit).collect();
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
let mut last_end = 0;
|
||||
for caps in regex.captures_iter(&s) {
|
||||
if parts.len() >= limit {
|
||||
break;
|
||||
}
|
||||
let full = caps.get(0).unwrap();
|
||||
parts.push(s[last_end..full.start()].to_string());
|
||||
if parts.len() >= limit {
|
||||
break;
|
||||
}
|
||||
for i in 1..caps.len() {
|
||||
if parts.len() >= limit {
|
||||
break;
|
||||
}
|
||||
let cap = caps.get(i).map(|m| m.as_str()).unwrap_or("");
|
||||
parts.push(cap.to_string());
|
||||
}
|
||||
last_end = full.end();
|
||||
}
|
||||
if last_end <= s.len() && parts.len() < limit {
|
||||
parts.push(s[last_end..].to_string());
|
||||
}
|
||||
return NativeResult::Ok(make_string_array(vm, &parts));
|
||||
}
|
||||
|
||||
@@ -475,7 +579,6 @@ pub fn string_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
return NativeResult::Ok(vm.new_string(&s));
|
||||
}
|
||||
let pattern_val = vm.reg(args[1]);
|
||||
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
|
||||
|
||||
if is_regexp_obj(pattern_val, vm) {
|
||||
let re_ptr = pattern_val.as_js_object_ptr();
|
||||
@@ -484,12 +587,53 @@ pub fn string_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
Some(p) => p,
|
||||
None => return NativeResult::Ok(vm.new_string(&s)),
|
||||
};
|
||||
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
|
||||
let result = regex.replace_all(&s, replacement.as_str()).to_string();
|
||||
|
||||
if args.len() > 2 {
|
||||
let replacer_val = vm.reg(args[2]);
|
||||
if replacer_val.is_object() {
|
||||
let o = unsafe { &*replacer_val.as_js_object_ptr() };
|
||||
if o.is_function() {
|
||||
builtins_debug!("string_replace: function replacer path");
|
||||
if let Some(captures) = regex.captures(&s) {
|
||||
let full = captures.get(0).unwrap();
|
||||
let mut cb_args: Vec<JsValue> = Vec::with_capacity(captures.len() + 2);
|
||||
cb_args.push(vm.new_string(full.as_str()));
|
||||
for i in 1..captures.len() {
|
||||
cb_args.push(vm.new_string(captures.get(i).map(|m| m.as_str()).unwrap_or("")));
|
||||
}
|
||||
cb_args.push(JsValue::int(full.start() as i32));
|
||||
cb_args.push(vm.new_string(&s));
|
||||
match vm.call_function_sync(replacer_val, JsValue::undefined(), &cb_args) {
|
||||
Ok(result) => {
|
||||
let result_str = oxide_runtime_api::to_string(result);
|
||||
let output = format!("{}{}{}", &s[..full.start()], &result_str, &s[full.end()..]);
|
||||
return NativeResult::Ok(vm.new_string(&output));
|
||||
}
|
||||
Err(err) => {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
&format!("replace replacer: {}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return NativeResult::Ok(vm.new_string(&s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
|
||||
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
|
||||
let result = if is_global {
|
||||
regex.replace_all(&s, replacement.as_str()).to_string()
|
||||
} else {
|
||||
regex.replacen(&s, 1, replacement.as_str()).to_string()
|
||||
};
|
||||
return NativeResult::Ok(vm.new_string(&result));
|
||||
}
|
||||
|
||||
let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() };
|
||||
let pattern = as_string(vm, pattern_val);
|
||||
let result = s.replacen(&pattern, &replacement, 1);
|
||||
NativeResult::Ok(vm.new_string(&result))
|
||||
@@ -499,7 +643,7 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.match called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
if args.len() < 2 {
|
||||
return NativeResult::Ok(JsValue::undefined());
|
||||
return NativeResult::Ok(JsValue::null());
|
||||
}
|
||||
let pattern_val = vm.reg(args[1]);
|
||||
|
||||
@@ -508,19 +652,30 @@ pub fn string_match_fn<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let re = unsafe { &*re_ptr };
|
||||
let fn_ptr = match re.native_fn() {
|
||||
Some(p) => p,
|
||||
None => return NativeResult::Ok(JsValue::undefined()),
|
||||
None => return NativeResult::Ok(JsValue::null()),
|
||||
};
|
||||
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
|
||||
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
|
||||
return NativeResult::Ok(make_string_array(vm, &matches));
|
||||
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
|
||||
if is_global {
|
||||
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
|
||||
return NativeResult::Ok(make_string_array(vm, &matches));
|
||||
}
|
||||
if let Some(captures) = regex.captures(&s) {
|
||||
let mut parts: Vec<String> = Vec::with_capacity(captures.len());
|
||||
parts.push(captures.get(0).unwrap().as_str().to_string());
|
||||
for i in 1..captures.len() {
|
||||
parts.push(captures.get(i).map(|m| m.as_str()).unwrap_or("").to_string());
|
||||
}
|
||||
return NativeResult::Ok(make_string_array(vm, &parts));
|
||||
}
|
||||
return NativeResult::Ok(JsValue::null());
|
||||
}
|
||||
|
||||
let pattern = as_string(vm, pattern_val);
|
||||
if let Some(pos) = s.find(&pattern) {
|
||||
return NativeResult::Ok(make_string_array(vm, &[s[pos..pos + pattern.len()].to_string()]));
|
||||
}
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
NativeResult::Ok(JsValue::null())
|
||||
}
|
||||
|
||||
pub fn string_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
@@ -602,6 +757,10 @@ pub fn string_normalize<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
NativeResult::Ok(vm.new_string(&result))
|
||||
}
|
||||
|
||||
const MALL_INPUT: &str = "__mal_input__";
|
||||
const MALL_INDEX: &str = "__mal_index__";
|
||||
const MALL_RE: &str = "__mal_re__";
|
||||
|
||||
pub fn string_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
builtins_debug!("String.prototype.matchAll called with {} args", args.len());
|
||||
let s = this_string(vm, args);
|
||||
@@ -610,19 +769,138 @@ pub fn string_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
return NativeResult::Err(JsValue::undefined());
|
||||
}
|
||||
let pattern_val = vm.reg(args[1]);
|
||||
if is_regexp_obj(pattern_val, vm) {
|
||||
|
||||
let re_obj = if is_regexp_obj(pattern_val, vm) {
|
||||
let re_ptr = pattern_val.as_js_object_ptr();
|
||||
let re = unsafe { &*re_ptr };
|
||||
let fn_ptr = match re.native_fn() {
|
||||
Some(p) => p,
|
||||
None => return NativeResult::Ok(JsValue::undefined()),
|
||||
let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false);
|
||||
if !is_global {
|
||||
return NativeResult::Err(crate::error::create_type_error(
|
||||
vm,
|
||||
"String.prototype.matchAll: regex must have global flag",
|
||||
));
|
||||
}
|
||||
pattern_val
|
||||
} else {
|
||||
let pattern_str = as_string(vm, pattern_val);
|
||||
let escaped = regex::escape(&pattern_str);
|
||||
let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) };
|
||||
let compiled = match regex::Regex::new(&rx_str) {
|
||||
Ok(rx) => rx,
|
||||
Err(e) => {
|
||||
return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("Invalid regex: {}", e)));
|
||||
}
|
||||
};
|
||||
// SAFETY: fn_ptr holds a Box<regex::Regex> pointer stored by regexp_constructor.
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
|
||||
let matches: Vec<String> = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect();
|
||||
return NativeResult::Ok(make_string_array(vm, &matches));
|
||||
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let mut stub = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
|
||||
let boxed = Box::new(compiled);
|
||||
let raw = Box::into_raw(boxed) as *const u8;
|
||||
stub.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(raw as *const ()) }));
|
||||
let stub_ptr = vm.alloc_object(stub);
|
||||
JsValue::from_js_object(stub_ptr)
|
||||
};
|
||||
|
||||
// Build wrapper object
|
||||
builder_wrapper(vm, &s, re_obj)
|
||||
}
|
||||
|
||||
fn builder_wrapper<H: VmHost>(vm: &mut H, input: &str, re_obj: JsValue) -> NativeResult {
|
||||
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let wrapper = vm
|
||||
.epoch()
|
||||
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
|
||||
|
||||
let wrapper_obj = unsafe { &mut *wrapper };
|
||||
let input_si = vm.kernel_core().perm_interner().intern(MALL_INPUT).0;
|
||||
let index_si = vm.kernel_core().perm_interner().intern(MALL_INDEX).0;
|
||||
let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0;
|
||||
let next_si = vm.kernel_core().perm_interner().intern("next").0;
|
||||
|
||||
let input_val = vm.new_string(input);
|
||||
vm.set_or_create_prop_value(wrapper_obj, input_si, input_val);
|
||||
vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(0));
|
||||
vm.set_or_create_prop_value(wrapper_obj, re_si, re_obj);
|
||||
|
||||
let next_fn = make_public_native_fn(vm, "next", string_match_all_next::<H> as *const (), 0);
|
||||
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
|
||||
|
||||
NativeResult::Ok(JsValue::from_js_object(wrapper))
|
||||
}
|
||||
|
||||
fn make_public_native_fn<H: VmHost>(vm: &mut H, name: &str, native_fn: *const (), arg_count: u8) -> JsValue {
|
||||
let function_proto = vm.session().builtin_world().function_proto.as_ptr() as *mut JsObject;
|
||||
let mut func = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(function_proto));
|
||||
func.set_function(true);
|
||||
func.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(native_fn) }));
|
||||
func.set_native_arg_count(arg_count);
|
||||
let func = vm.alloc_object(func);
|
||||
let name_si = vm.kernel_core().perm_interner().intern("name").0;
|
||||
let value = vm.new_string(name);
|
||||
let func_ref = unsafe { &mut *func };
|
||||
vm.set_or_create_prop_value(func_ref, name_si, value);
|
||||
JsValue::from_js_object(func)
|
||||
}
|
||||
|
||||
pub fn string_match_all_next<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] });
|
||||
if !this_val.is_object() {
|
||||
return NativeResult::Err(crate::error::create_type_error(vm, "matchAll next called on non-object"));
|
||||
}
|
||||
NativeResult::Ok(JsValue::undefined())
|
||||
let wrapper = unsafe { &mut *this_val.as_js_object_ptr() };
|
||||
let input_si = vm.kernel_core().perm_interner().intern(MALL_INPUT).0;
|
||||
let index_si = vm.kernel_core().perm_interner().intern(MALL_INDEX).0;
|
||||
let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0;
|
||||
|
||||
let input_val = match vm.ordinary_get(wrapper, input_si, this_val) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return make_match_done_result(vm, JsValue::undefined()),
|
||||
};
|
||||
let input_str = oxide_runtime_api::to_string(input_val);
|
||||
let idx_val = match vm.ordinary_get(wrapper, index_si, this_val) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return make_match_done_result(vm, JsValue::undefined()),
|
||||
};
|
||||
let mut idx = if idx_val.is_int() { idx_val.as_int().max(0) as usize } else { 0 };
|
||||
let re_val = match vm.ordinary_get(wrapper, re_si, this_val) {
|
||||
Ok(v) if v.is_object() => v,
|
||||
_ => return make_match_done_result(vm, JsValue::undefined()),
|
||||
};
|
||||
let re_obj = unsafe { &*re_val.as_js_object_ptr() };
|
||||
let fn_ptr = match re_obj.native_fn() {
|
||||
Some(p) => p,
|
||||
None => return make_match_done_result(vm, JsValue::undefined()),
|
||||
};
|
||||
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) };
|
||||
|
||||
if let Some(m) = regex.find_at(&input_str, idx) {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if let Some(caps) = regex.captures_at(&input_str, idx) {
|
||||
parts.push(caps.get(0).unwrap().as_str().to_string());
|
||||
for i in 1..caps.len() {
|
||||
parts.push(caps.get(i).map(|c| c.as_str()).unwrap_or("").to_string());
|
||||
}
|
||||
}
|
||||
idx = m.end();
|
||||
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32));
|
||||
let arr_val = make_string_array(vm, &parts);
|
||||
make_match_done_result(vm, arr_val)
|
||||
} else {
|
||||
make_match_done_result(vm, JsValue::undefined())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_match_done_result<H: VmHost>(vm: &mut H, value: JsValue) -> NativeResult {
|
||||
let done = value.is_undefined();
|
||||
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
|
||||
let obj = vm
|
||||
.epoch()
|
||||
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
|
||||
let value_si = vm.kernel_core().perm_interner().intern("value").0;
|
||||
let done_si = vm.kernel_core().perm_interner().intern("done").0;
|
||||
let obj_ref = unsafe { &mut *obj };
|
||||
vm.set_or_create_prop_value(obj_ref, value_si, value);
|
||||
vm.set_or_create_prop_value(obj_ref, done_si, JsValue::bool(done));
|
||||
NativeResult::Ok(JsValue::from_js_object(obj))
|
||||
}
|
||||
|
||||
pub fn string_replace_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
|
||||
|
||||
@@ -137,6 +137,9 @@ pub struct StringMethods {
|
||||
pub match_all: *const (),
|
||||
pub replace_all: *const (),
|
||||
pub value_of: *const (),
|
||||
pub substr: *const (),
|
||||
pub at: *const (),
|
||||
pub last_index_of: *const (),
|
||||
}
|
||||
|
||||
pub struct RegExpMethods {
|
||||
@@ -1048,6 +1051,9 @@ impl BuiltinWorld {
|
||||
("matchAll", methods.match_all, 1),
|
||||
("replaceAll", methods.replace_all, 2),
|
||||
("valueOf", methods.value_of, 0),
|
||||
("substr", methods.substr, 2),
|
||||
("at", methods.at, 1),
|
||||
("lastIndexOf", methods.last_index_of, 1),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ pub fn bind_string(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
|
||||
match_all: oxide_builtins::string::string_match_all::<crate::vm::Vm> as *const (),
|
||||
replace_all: oxide_builtins::string::string_replace_all::<crate::vm::Vm> as *const (),
|
||||
value_of: oxide_builtins::string::string_value_of::<crate::vm::Vm> as *const (),
|
||||
substr: oxide_builtins::string::string_substr::<crate::vm::Vm> as *const (),
|
||||
at: oxide_builtins::string::string_at::<crate::vm::Vm> as *const (),
|
||||
last_index_of: oxide_builtins::string::string_last_index_of::<crate::vm::Vm> as *const (),
|
||||
};
|
||||
session.builtin_world().bind_string_methods(
|
||||
&string_methods,
|
||||
|
||||
@@ -267,3 +267,259 @@ fn string_call_conversion_stays_primitive() {
|
||||
assert!(v.is_string());
|
||||
assert_eq!(to_str(&vm, v), "123");
|
||||
}
|
||||
|
||||
// ── Plan 01: replace tests ──
|
||||
|
||||
#[test]
|
||||
fn string_replace_non_global_first_only() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'aaa'.replace(/a/, 'b')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "baa");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_replace_global_all() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'aaa'.replace(/a/g, 'b')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "bbb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_replace_function_replacer_match() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.replace(/l/, function(m){return m.toUpperCase()})").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "heLlo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_replace_function_replacer_offset() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'abc'.replace(/b/, function(m,o){return o})").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "a1c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_replace_string_pattern() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.replace('l', 'L')").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "heLlo");
|
||||
}
|
||||
|
||||
// ── Plan 02: split tests ──
|
||||
|
||||
#[test]
|
||||
fn string_split_regex_capture_groups() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'a1b2c'.split(/(\\d)/)").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 5);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "1");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "b");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(3)), "2");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(4)), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_split_regex_capture_with_limit() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'a1b2c'.split(/(\\d)/, 3)").unwrap();
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_split_regex_no_capture() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'a,b,c'.split(/,/)").unwrap();
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 3);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "b");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_split_string_separator() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'a,b,c'.split(',')").unwrap();
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 3);
|
||||
}
|
||||
|
||||
// ── Plan 03: match tests ──
|
||||
|
||||
#[test]
|
||||
fn string_match_non_global_captures() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'12-34'.match(/(\\d+)-(\\d+)/)").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 3);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "12-34");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "12");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(2)), "34");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_match_no_match_null() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'abc'.match(/x/)").unwrap();
|
||||
assert!(result.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_match_global_flat() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'aba'.match(/a/g)").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 2);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "a");
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_match_string_pattern() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.match('ll')").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
assert_eq!(obj.prop_count(), 1);
|
||||
assert_eq!(to_str(&vm, obj.get_prop_at(0)), "ll");
|
||||
}
|
||||
|
||||
// ── Plan 04: matchAll tests ──
|
||||
|
||||
#[test]
|
||||
fn string_match_all_returns_iterator() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'aba'.matchAll(/a/g)").unwrap();
|
||||
assert!(result.is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_match_all_next_returns_match() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "var it = 'ab'.matchAll(/a/g); it.next()").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
let done = obj.get_prop_at(1).as_bool();
|
||||
assert!(!done);
|
||||
assert!(obj.get_prop_at(0).is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_match_all_next_exhausted() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "var it = 'a'.matchAll(/x/g); it.next(); it.next()").unwrap();
|
||||
assert!(result.is_object());
|
||||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||||
let done = obj.get_prop_at(1).as_bool();
|
||||
assert!(done);
|
||||
}
|
||||
|
||||
// ── Plan 05: substring tests ──
|
||||
|
||||
#[test]
|
||||
fn string_substring_nan_index() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substring(NaN, 2)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "he");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_substring_negative_index() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substring(-3, 2)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "he");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_substring_swap() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substring(3, 1)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "el");
|
||||
}
|
||||
|
||||
// ── Plan 06: substr + at tests ──
|
||||
|
||||
#[test]
|
||||
fn string_substr_positive() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substr(1, 3)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "ell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_substr_negative_start() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substr(-3, 2)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "ll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_substr_negative_length() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.substr(1, -1)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_at_positive() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.at(1)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "e");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_at_negative() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.at(-1)").unwrap();
|
||||
assert_eq!(to_str(&vm, s), "o");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_at_out_of_range() {
|
||||
let mut vm = Vm::new();
|
||||
let s = eval(&mut vm, "'hello'.at(10)").unwrap();
|
||||
assert!(s.is_undefined());
|
||||
}
|
||||
|
||||
// ── Plan 07: lastIndexOf tests ──
|
||||
|
||||
#[test]
|
||||
fn string_last_index_of_basic() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.lastIndexOf('l')").unwrap();
|
||||
assert_eq!(result.as_int(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_last_index_of_not_found() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.lastIndexOf('x')").unwrap();
|
||||
assert_eq!(result.as_int(), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_last_index_of_empty_string() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.lastIndexOf('')").unwrap();
|
||||
assert_eq!(result.as_int(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_last_index_of_with_position() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.lastIndexOf('l', 2)").unwrap();
|
||||
assert_eq!(result.as_int(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_last_index_of_empty_with_position() {
|
||||
let mut vm = Vm::new();
|
||||
let result = eval(&mut vm, "'hello'.lastIndexOf('', 3)").unwrap();
|
||||
assert_eq!(result.as_int(), 3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user