use crate::{
context::intrinsics::Intrinsics, js_string, realm::Realm, Context, JsArgs, JsObject, JsResult,
JsValue,
};
use super::{BuiltInBuilder, BuiltInObject, IntrinsicObject};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Escape;
impl IntrinsicObject for Escape {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, escape)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().escape().into()
}
}
impl BuiltInObject for Escape {
const NAME: &'static str = "escape";
}
fn escape(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
fn is_unescaped(cp: u16) -> bool {
let Ok(cp) = TryInto::<u8>::try_into(cp) else {
return false;
};
cp.is_ascii_alphanumeric() || [b'_', b'@', b'*', b'+', b'-', b'.', b'/'].contains(&cp)
}
let string = args.get_or_undefined(0).to_string(context)?;
let mut vec = Vec::with_capacity(string.len());
for &cp in &*string {
if is_unescaped(cp) {
vec.push(cp);
continue;
}
let c = if cp < 256 {
format!("%{cp:02X}")
}
else {
format!("%u{cp:04X}")
};
vec.extend(c.encode_utf16());
}
Ok(js_string!(vec).into())
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Unescape;
impl IntrinsicObject for Unescape {
fn init(realm: &Realm) {
BuiltInBuilder::callable_with_intrinsic::<Self>(realm, unescape)
.name(Self::NAME)
.length(1)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().unescape().into()
}
}
impl BuiltInObject for Unescape {
const NAME: &'static str = "unescape";
}
fn unescape(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
fn to_hex_digit(cp: u16) -> Option<u16> {
char::from_u32(u32::from(cp))
.and_then(|c| c.to_digit(16))
.and_then(|d| d.try_into().ok())
}
let string = args.get_or_undefined(0).to_string(context)?;
let mut vec = Vec::with_capacity(string.len());
let mut codepoints = <PeekableN<_, 6>>::new(string.iter().copied());
loop {
let Some(cp) = codepoints.next() else {
break;
};
if cp != u16::from(b'%') {
vec.push(cp);
continue;
}
let Some(unescaped_cp) = (|| match *codepoints.peek_n(5) {
[u, n1, n2, n3, n4] if u == u16::from(b'u') => {
let n1 = to_hex_digit(n1)?;
let n2 = to_hex_digit(n2)?;
let n3 = to_hex_digit(n3)?;
let n4 = to_hex_digit(n4)?;
for _ in 0..5 {
codepoints.next();
}
Some((n1 << 12) + (n2 << 8) + (n3 << 4) + n4)
}
[n1, n2, ..] => {
let n1 = to_hex_digit(n1)?;
let n2 = to_hex_digit(n2)?;
for _ in 0..2 {
codepoints.next();
}
Some((n1 << 4) + n2)
}
_ => None
})() else {
vec.push(u16::from(b'%'));
continue;
};
vec.push(unescaped_cp);
}
Ok(js_string!(vec).into())
}
struct PeekableN<I, const N: usize>
where
I: Iterator,
{
iterator: I,
buffer: [I::Item; N],
buffered_end: usize,
}
impl<I, const N: usize> PeekableN<I, N>
where
I: Iterator,
I::Item: Default + Copy,
{
fn new(iterator: I) -> Self {
Self {
iterator,
buffer: [I::Item::default(); N],
buffered_end: 0,
}
}
fn peek_n(&mut self, count: usize) -> &[I::Item] {
if count <= self.buffered_end {
return &self.buffer[..count];
}
for _ in 0..(count - self.buffered_end) {
let Some(next) = self.iterator.next() else {
return &self.buffer[..self.buffered_end];
};
self.buffer[self.buffered_end] = next;
self.buffered_end += 1;
}
&self.buffer[..count]
}
}
impl<I, const N: usize> Iterator for PeekableN<I, N>
where
I: Iterator,
I::Item: Copy,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
if self.buffered_end > 0 {
let item = self.buffer[0];
self.buffer.rotate_left(1);
self.buffered_end -= 1;
return Some(item);
}
self.iterator.next()
}
}