use std::borrow::Cow;
use std::cmp::Ordering;
use crate::{js_length, js_trim, js_trim_start, push_number};
const MAX_STRING_LENGTH: f64 = ((1 << 29) - 24) as f64;
fn to_integer(x: f64) -> f64 {
if x.is_nan() { 0.0 } else { x.trunc() + 0.0 }
}
fn relative(x: f64, len: usize) -> usize {
let n = to_integer(x);
let len = len as f64;
(if n < 0.0 {
(len + n).max(0.0)
} else {
n.min(len)
}) as usize
}
pub fn js_slice_range(len: usize, start: f64, end: Option<f64>) -> (usize, usize) {
let from = relative(start, len);
let to = end.map_or(len, |e| relative(e, len));
(from, to.max(from))
}
pub fn js_slice_items<T>(
items: impl Iterator<Item = T>,
start: f64,
end: Option<f64>,
) -> std::vec::IntoIter<T> {
let mut all: Vec<T> = items.collect();
let (from, to) = js_slice_range(all.len(), start, end);
all.truncate(to);
all.drain(..from);
all.into_iter()
}
fn boundary(s: &str, i: usize) -> (usize, bool) {
let mut unit = 0;
for (byte, c) in s.char_indices() {
if unit >= i {
return (byte, false);
}
let width = c.len_utf16();
if unit + width > i {
return (byte + c.len_utf8(), true);
}
unit += width;
}
(s.len(), false)
}
fn units(s: &str, from: usize, to: usize) -> Cow<'_, str> {
if from >= to {
return Cow::Borrowed("");
}
if s.is_ascii() {
return Cow::Borrowed(&s[from..to]);
}
let (start, low_half) = boundary(s, from);
let (end, high_half) = boundary(s, to);
if !low_half && !high_half {
return Cow::Borrowed(&s[start..end]);
}
let whole_end = if high_half { end - 4 } else { end };
let mut out = String::with_capacity(whole_end.saturating_sub(start) + 6);
if low_half {
out.push('\u{FFFD}');
}
if whole_end > start {
out.push_str(&s[start..whole_end]);
}
if high_half {
out.push('\u{FFFD}');
}
Cow::Owned(out)
}
fn unit(s: &str, k: usize) -> &str {
if s.is_ascii() {
return &s[k..k + 1];
}
let mut at = 0;
for (byte, c) in s.char_indices() {
let width = c.len_utf16();
if k < at + width {
return if width == 1 {
&s[byte..byte + c.len_utf8()]
} else {
"\u{FFFD}"
};
}
at += width;
}
unreachable!("code unit {k} is in range")
}
fn length(s: &str) -> usize {
js_length(s) as usize
}
pub fn js_slice(s: &str, start: f64, end: Option<f64>) -> Cow<'_, str> {
let (from, to) = js_slice_range(length(s), start, end);
units(s, from, to)
}
pub fn js_substring(s: &str, start: f64, end: Option<f64>) -> Cow<'_, str> {
let len = length(s);
let clamp = |x: f64| to_integer(x).clamp(0.0, len as f64) as usize;
let (a, b) = (clamp(start), end.map_or(len, clamp));
units(s, a.min(b), a.max(b))
}
pub fn js_at(s: &str, index: f64) -> Option<&str> {
let len = length(s) as f64;
let n = to_integer(index);
let k = if n < 0.0 { len + n } else { n };
(0.0 <= k && k < len).then(|| unit(s, k as usize))
}
pub fn js_char_at(s: &str, index: f64) -> &str {
let n = to_integer(index);
if n < 0.0 || n >= length(s) as f64 {
""
} else {
unit(s, n as usize)
}
}
pub fn js_index_of(s: &str, search: &str) -> i64 {
s.find(search).map_or(-1, |byte| js_length(&s[..byte]))
}
pub fn js_last_index_of(s: &str, search: &str) -> i64 {
s.rfind(search).map_or(-1, |byte| js_length(&s[..byte]))
}
pub fn js_split<'s>(s: &'s str, separator: &str) -> Vec<Cow<'s, str>> {
if !separator.is_empty() {
return s.split(separator).map(Cow::Borrowed).collect();
}
let mut out = Vec::with_capacity(s.len());
for (byte, c) in s.char_indices() {
if c.len_utf16() == 1 {
out.push(Cow::Borrowed(&s[byte..byte + c.len_utf8()]));
} else {
out.push(Cow::Borrowed("\u{FFFD}"));
out.push(Cow::Borrowed("\u{FFFD}"));
}
}
out
}
fn substitute(out: &mut String, replacement: &str, s: &str, start: usize, end: usize) {
let mut rest = replacement;
while let Some(i) = rest.find('$') {
out.push_str(&rest[..i]);
let (text, used) = match rest.as_bytes().get(i + 1) {
Some(b'$') => ("$", 2),
Some(b'&') => (&s[start..end], 2),
Some(b'`') => (&s[..start], 2),
Some(b'\'') => (&s[end..], 2),
_ => ("$", 1),
};
out.push_str(text);
rest = &rest[i + used..];
}
out.push_str(rest);
}
pub fn js_replace<'s>(s: &'s str, pattern: &str, replacement: &str) -> Cow<'s, str> {
let Some(start) = s.find(pattern) else {
return Cow::Borrowed(s);
};
let end = start + pattern.len();
let mut out = String::with_capacity(s.len() + replacement.len());
out.push_str(&s[..start]);
substitute(&mut out, replacement, s, start, end);
out.push_str(&s[end..]);
Cow::Owned(out)
}
pub fn js_replace_all<'s>(s: &'s str, pattern: &str, replacement: &str) -> Cow<'s, str> {
if pattern.is_empty() {
return Cow::Owned(replace_between_units(s, replacement));
}
let mut matches = s.match_indices(pattern).peekable();
if matches.peek().is_none() {
return Cow::Borrowed(s);
}
let mut out = String::with_capacity(s.len() + replacement.len());
let mut last = 0;
for (start, _) in matches {
out.push_str(&s[last..start]);
substitute(&mut out, replacement, s, start, start + pattern.len());
last = start + pattern.len();
}
out.push_str(&s[last..]);
Cow::Owned(out)
}
fn replace_between_units(s: &str, replacement: &str) -> String {
let units: Vec<u16> = s.encode_utf16().collect();
let repl: Vec<u16> = replacement.encode_utf16().collect();
let mut out: Vec<u16> = Vec::with_capacity(units.len() * (repl.len() + 1) + repl.len());
for at in 0..=units.len() {
let mut i = 0;
while i < repl.len() {
let next = repl.get(i + 1).copied();
if repl[i] == u16::from(b'$') {
match next.and_then(|c| u8::try_from(c).ok()) {
Some(b'$') => out.push(u16::from(b'$')),
Some(b'&') => {}
Some(b'`') => out.extend_from_slice(&units[..at]),
Some(b'\'') => out.extend_from_slice(&units[at..]),
_ => {
out.push(repl[i]);
i += 1;
continue;
}
}
i += 2;
} else {
out.push(repl[i]);
i += 1;
}
}
if let Some(&u) = units.get(at) {
out.push(u);
}
}
String::from_utf16_lossy(&out)
}
fn filler(fill: &str, n: usize) -> String {
if fill.is_ascii() {
return fill.bytes().cycle().take(n).map(char::from).collect();
}
let units: Vec<u16> = fill.encode_utf16().cycle().take(n).collect();
String::from_utf16_lossy(&units)
}
fn pad<'s>(s: &'s str, max_length: f64, fill: &str, at_start: bool) -> Cow<'s, str> {
let len = length(s);
let target = to_integer(max_length).clamp(0.0, 9_007_199_254_740_991.0);
if target <= len as f64 || fill.is_empty() {
return Cow::Borrowed(s);
}
if target > MAX_STRING_LENGTH {
panic!("RangeError: Invalid string length (padding to {target} code units)");
}
let pad = filler(fill, target as usize - len);
Cow::Owned(if at_start {
pad + s
} else {
format!("{s}{pad}")
})
}
pub fn js_pad_start<'s>(s: &'s str, max_length: f64, fill: &str) -> Cow<'s, str> {
pad(s, max_length, fill, true)
}
pub fn js_pad_end<'s>(s: &'s str, max_length: f64, fill: &str) -> Cow<'s, str> {
pad(s, max_length, fill, false)
}
pub fn js_repeat(s: &str, count: f64) -> String {
let n = to_integer(count);
if n < 0.0 || n == f64::INFINITY {
let mut shown = String::new();
push_number(&mut shown, count);
panic!("RangeError: Invalid count value: {shown}");
}
if n == 0.0 || s.is_empty() {
return String::new();
}
if length(s) as f64 * n > MAX_STRING_LENGTH {
panic!("RangeError: Invalid string length (repeating {n} times)");
}
s.repeat(n as usize)
}
pub fn js_cmp(a: &str, b: &str) -> Ordering {
if a.is_ascii() || b.is_ascii() {
return a.as_bytes().cmp(b.as_bytes());
}
a.encode_utf16().cmp(b.encode_utf16())
}
fn digit(b: u8, radix: u32) -> Option<u32> {
char::from(b).to_digit(radix)
}
fn power_of_two_value(digits: &[u8], radix: u32) -> f64 {
let bits = radix.trailing_zeros();
let mut number: u64 = 0;
let mut exponent: i32 = 0;
for (i, &b) in digits.iter().enumerate() {
number = number * u64::from(radix) + u64::from(digit(b, radix).expect("a digit"));
let overflow = number >> 53;
if overflow == 0 {
continue;
}
let dropped_count = 64 - overflow.leading_zeros();
let dropped = number & ((1 << dropped_count) - 1);
number >>= dropped_count;
exponent = dropped_count as i32;
let rest = &digits[i + 1..];
let zero_tail = rest.iter().all(|&d| d == b'0');
exponent += (rest.len() as i32) * bits as i32;
let middle = 1 << (dropped_count - 1);
if dropped > middle || (dropped == middle && (number & 1 == 1 || !zero_tail)) {
number += 1;
}
if number & (1 << 53) != 0 {
exponent += 1;
number >>= 1;
}
break;
}
(number as f64) * 2f64.powi(exponent)
}
fn decimal_prefix(s: &str) -> Option<usize> {
let b = s.as_bytes();
let digits_from = |mut i: usize| {
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
i
};
let mut i = usize::from(matches!(b.first(), Some(b'+' | b'-')));
if s[i..].starts_with("Infinity") {
return Some(i + "Infinity".len());
}
let int_end = digits_from(i);
let whole = int_end > i;
i = int_end;
if b.get(i) == Some(&b'.') {
let frac_end = digits_from(i + 1);
if whole || frac_end > i + 1 {
i = frac_end;
} else {
return None;
}
} else if !whole {
return None;
}
if matches!(b.get(i), Some(b'e' | b'E')) {
let mut j = i + 1;
if matches!(b.get(j), Some(b'+' | b'-')) {
j += 1;
}
let exp_end = digits_from(j);
if exp_end > j {
i = exp_end;
}
}
Some(i)
}
fn decimal_value(literal: &str) -> f64 {
match literal {
"Infinity" | "+Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
_ => literal.parse().expect("a decimal literal"),
}
}
pub fn js_number(s: &str) -> f64 {
let t = js_trim(s);
if t.is_empty() {
return 0.0;
}
let b = t.as_bytes();
if b.len() > 2 && b[0] == b'0' {
let radix = match b[1] {
b'x' | b'X' => 16,
b'o' | b'O' => 8,
b'b' | b'B' => 2,
_ => 0,
};
if radix != 0 {
let digits = &b[2..];
return if digits.iter().all(|&d| digit(d, radix).is_some()) {
power_of_two_value(digits, radix)
} else {
f64::NAN
};
}
}
match decimal_prefix(t) {
Some(n) if n == t.len() => decimal_value(t),
_ => f64::NAN,
}
}
pub fn js_parse_int(s: &str, radix: u32) -> f64 {
let t = js_trim_start(s);
let negative = t.starts_with('-');
let t = t.strip_prefix(['-', '+']).unwrap_or(t);
let (radix, t) = match radix {
0 | 16 => match t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
Some(rest) => (16, rest),
None => (if radix == 0 { 10 } else { 16 }, t),
},
2..=36 => (radix, t),
_ => return f64::NAN,
};
let n = t.bytes().take_while(|&b| digit(b, radix).is_some()).count();
if n == 0 {
return f64::NAN;
}
let digits = &t.as_bytes()[..n];
let value = if radix == 10 {
t[..n].parse().expect("decimal digits")
} else if radix.is_power_of_two() {
power_of_two_value(digits, radix)
} else {
digits.iter().fold(0.0, |acc, &d| {
acc * f64::from(radix) + f64::from(digit(d, radix).expect("a digit"))
})
};
if negative { -value } else { value }
}
pub fn js_parse_float(s: &str) -> f64 {
let t = js_trim_start(s);
decimal_prefix(t).map_or(f64::NAN, |n| decimal_value(&t[..n]))
}
pub fn js_json_string(s: &str) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
pub fn js_json_number(x: f64) -> String {
if !x.is_finite() {
return "null".to_owned();
}
let mut out = String::new();
push_number(&mut out, x);
out
}