use std::borrow::Cow;
use std::cmp::Ordering;
use crate::push_number;
#[must_use]
pub fn js_length(s: &str) -> i64 {
if s.is_ascii() {
return s.len() as i64;
}
s.chars().map(char::len_utf16).sum::<usize>() as i64
}
#[must_use]
pub fn js_trim(s: &str) -> &str {
s.trim_matches(is_js_space)
}
#[must_use]
pub fn js_trim_start(s: &str) -> &str {
s.trim_start_matches(is_js_space)
}
#[must_use]
pub fn js_trim_end(s: &str) -> &str {
s.trim_end_matches(is_js_space)
}
pub(crate) fn is_js_space(c: char) -> bool {
matches!(
c,
'\u{9}' | '\u{A}' | '\u{B}' | '\u{C}' | '\u{D}' | ' ' | '\u{A0}' | '\u{1680}' | '\u{2000}'
..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
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
}
#[must_use]
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))
}
#[must_use]
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()
}
#[must_use]
pub fn js_item_at<T>(items: impl Iterator<Item = T>, index: f64) -> Option<T> {
let mut all: Vec<T> = items.collect();
let len = all.len() as f64;
let n = to_integer(index);
let k = if n < 0.0 { len + n } else { n };
if 0.0 <= k && k < len {
Some(all.swap_remove(k as usize))
} else {
None
}
}
#[must_use]
pub fn js_reduce<T, A>(items: impl Iterator<Item = T>, init: A, step: impl FnMut(A, T) -> A) -> A {
items.fold(init, step)
}
#[must_use]
pub fn js_sorted<T>(
items: impl Iterator<Item = T>,
compare: impl FnMut(&T, &T) -> Ordering,
) -> std::vec::IntoIter<T> {
let mut all: Vec<T> = items.collect();
all.sort_by(compare);
all.into_iter()
}
#[must_use]
pub fn js_sorted_by_string<T>(
items: impl Iterator<Item = T>,
mut key: impl FnMut(&T) -> String,
) -> std::vec::IntoIter<T> {
let mut keyed: Vec<(String, T)> = items.map(|v| (key(&v), v)).collect();
keyed.sort_by(|a, b| js_cmp(&a.0, &b.0));
keyed
.into_iter()
.map(|(_, v)| v)
.collect::<Vec<T>>()
.into_iter()
}
#[must_use]
pub fn js_order(a: f64, b: f64) -> Ordering {
let d = a - b;
if d < 0.0 {
Ordering::Less
} else if d > 0.0 {
Ordering::Greater
} else {
Ordering::Equal
}
}
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
}
#[must_use]
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)
}
#[must_use]
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))
}
#[must_use]
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))
}
#[must_use]
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)
}
}
#[must_use]
pub fn js_index_of(s: &str, search: &str) -> i64 {
s.find(search).map_or(-1, |byte| js_length(&s[..byte]))
}
#[must_use]
pub fn js_last_index_of(s: &str, search: &str) -> i64 {
s.rfind(search).map_or(-1, |byte| js_length(&s[..byte]))
}
fn clamped_unit(s: &str, x: f64) -> usize {
to_integer(x).clamp(0.0, length(s) as f64) as usize
}
fn char_start(s: &str, unit: usize) -> usize {
match boundary(s, unit) {
(byte, true) => byte - 4,
(byte, false) => byte,
}
}
#[must_use]
pub fn js_includes_from(s: &str, search: &str, position: f64) -> bool {
let (byte, _) = boundary(s, clamped_unit(s, position));
s[byte..].contains(search)
}
#[must_use]
pub fn js_starts_with_at(s: &str, search: &str, position: f64) -> bool {
match boundary(s, clamped_unit(s, position)) {
(_, true) => search.is_empty(),
(byte, false) => s[byte..].starts_with(search),
}
}
#[must_use]
pub fn js_ends_with_at(s: &str, search: &str, end_position: f64) -> bool {
match boundary(s, clamped_unit(s, end_position)) {
(_, true) => search.is_empty(),
(byte, false) => s[..byte].ends_with(search),
}
}
#[must_use]
pub fn js_index_of_from(s: &str, search: &str, position: f64) -> i64 {
let from = clamped_unit(s, position);
if search.is_empty() {
return from as i64;
}
let (byte, _) = boundary(s, from);
s[byte..]
.find(search)
.map_or(-1, |at| js_length(&s[..byte + at]))
}
#[must_use]
pub fn js_last_index_of_from(s: &str, search: &str, position: f64) -> i64 {
let len = length(s);
let searched = length(search);
if searched > len {
return -1;
}
let pos = if position.is_nan() {
f64::INFINITY
} else {
to_integer(position)
};
let start = pos.clamp(0.0, (len - searched) as f64) as usize;
if search.is_empty() {
return start as i64;
}
let mut end = (char_start(s, start) + search.len()).min(s.len());
while !s.is_char_boundary(end) {
end -= 1;
}
s[..end].rfind(search).map_or(-1, |at| js_length(&s[..at]))
}
#[must_use]
pub fn js_substr(s: &str, start: f64, length: Option<f64>) -> Cow<'_, str> {
let len = self::length(s);
let from = relative(start, len);
let count = length.map_or(len, |l| to_integer(l).clamp(0.0, len as f64) as usize);
units(s, from, (from + count).min(len))
}
#[must_use]
pub fn js_char_code_at(s: &str, index: f64) -> f64 {
let n = to_integer(index);
if n < 0.0 || n >= length(s) as f64 {
return f64::NAN;
}
f64::from(
s.encode_utf16()
.nth(n as usize)
.expect("a code unit in range"),
)
}
#[must_use]
pub fn js_code_point_at(s: &str, index: f64) -> Option<i64> {
let n = to_integer(index);
if n < 0.0 || n >= length(s) as f64 {
return None;
}
let mut rest = s.encode_utf16().skip(n as usize);
let first = rest.next().expect("a code unit in range");
let point = match (first, rest.next()) {
(0xD800..=0xDBFF, Some(second @ 0xDC00..=0xDFFF)) => {
0x10000 + ((u32::from(first) - 0xD800) << 10) + (u32::from(second) - 0xDC00)
}
_ => u32::from(first),
};
Some(i64::from(point))
}
#[must_use]
pub fn js_split_limit<'s>(s: &'s str, separator: &str, limit: f64) -> Vec<Cow<'s, str>> {
let lim = if limit.is_finite() {
limit.trunc().rem_euclid(4_294_967_296.0) as usize
} else {
0
};
if lim == 0 {
return Vec::new();
}
let mut pieces = js_split(s, separator);
pieces.truncate(lim);
pieces
}
#[derive(Debug, Clone, Copy)]
pub struct JsOr<T>(pub Option<T>, pub &'static str);
impl<T: std::fmt::Display> std::fmt::Display for JsOr<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
Some(v) => v.fmt(f),
None => f.write_str(self.1),
}
}
}
#[must_use]
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);
}
#[must_use]
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)
}
#[must_use]
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}")
})
}
#[must_use]
pub fn js_pad_start<'s>(s: &'s str, max_length: f64, fill: &str) -> Cow<'s, str> {
pad(s, max_length, fill, true)
}
#[must_use]
pub fn js_pad_end<'s>(s: &'s str, max_length: f64, fill: &str) -> Cow<'s, str> {
pad(s, max_length, fill, false)
}
#[must_use]
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)
}
#[must_use]
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"),
}
}
#[must_use]
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,
}
}
#[must_use]
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 }
}
#[must_use]
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]))
}
#[must_use]
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
}
#[must_use]
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
}
#[cfg(test)]
mod tests;