use std::{
fmt::{self, Debug, Write},
hash::{Hash, Hasher},
iter::FusedIterator,
marker::PhantomData,
ptr::NonNull,
slice, str,
};
use oxc_allocator::{Allocator, CloneIn, CloneInSemanticIds, Dummy, GetAllocator};
use crate::{Ident, JSChar, JSStrBuilder, Str, wtf8::*};
#[derive(Clone, Copy)]
#[repr(C)]
pub struct JSStr<'a> {
ptr: NonNull<u8>,
len: u32,
has_lone_surrogate: bool,
_marker: PhantomData<&'a [u8]>,
}
impl JSStr<'static> {
#[inline]
pub const fn empty() -> Self {
Self::from_str("")
}
}
impl<'a> JSStr<'a> {
#[inline]
const fn from_str(value: &'a str) -> Self {
assert!(value.len() <= u32::MAX as usize, "JavaScript string exceeds u32::MAX bytes");
unsafe { Self::from_bytes_unchecked(value.as_bytes(), false) }
}
#[inline]
pub fn from_str_in(value: &str, allocator: &impl GetAllocator<'a>) -> Self {
JSStr::from(value).clone_in(allocator.allocator())
}
#[inline]
pub fn from_js_strs_array_in<const N: usize>(
strings: [JSStr<'_>; N],
allocator: &impl GetAllocator<'a>,
) -> Self {
let capacity = strings
.iter()
.try_fold(0usize, |len, value| len.checked_add(value.len()))
.expect("JavaScript string capacity overflow");
let mut builder = JSStrBuilder::with_capacity_in(capacity, allocator);
for value in strings {
builder.push_js_str(value);
}
builder.into_js_str()
}
#[inline]
pub fn as_str(self) -> Option<&'a str> {
if self.has_lone_surrogate() {
None
} else {
Some(unsafe { str::from_utf8_unchecked(self.as_bytes()) })
}
}
#[inline]
pub const fn len(self) -> usize {
self.len as usize
}
#[inline]
pub const fn is_empty(self) -> bool {
self.len == 0
}
#[inline]
pub const fn has_lone_surrogate(self) -> bool {
self.has_lone_surrogate
}
pub fn len_utf16(self) -> usize {
self.as_bytes()
.iter()
.map(|&byte| {
usize::from(byte & !CONT_MASK != CONT_TAG) + usize::from(byte >= FOUR_BYTE_TAG)
})
.sum()
}
#[inline]
pub fn chars(self) -> impl FusedIterator<Item = JSChar> + Clone + 'a {
JSChars { remaining: self.as_bytes() }
}
#[inline]
pub fn encode_utf16(self) -> impl FusedIterator<Item = u16> + Clone + 'a {
EncodeUtf16 { chars: JSChars { remaining: self.as_bytes() }, pending: 0 }
}
#[inline]
pub fn as_bytes(self) -> &'a [u8] {
unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len()) }
}
#[inline]
#[expect(clippy::cast_possible_truncation, reason = "the caller guarantees the length fits")]
pub const unsafe fn from_bytes_unchecked(bytes: &'a [u8], has_lone_surrogate: bool) -> Self {
Self {
ptr: NonNull::from_ref(bytes).cast(),
len: bytes.len() as u32,
has_lone_surrogate,
_marker: PhantomData,
}
}
}
unsafe impl Send for JSStr<'_> {}
unsafe impl Sync for JSStr<'_> {}
impl<'a> From<&'a str> for JSStr<'a> {
#[inline]
fn from(value: &'a str) -> Self {
Self::from_str(value)
}
}
impl<'a> From<Str<'a>> for JSStr<'a> {
#[inline]
fn from(value: Str<'a>) -> Self {
Self::from(value.as_str())
}
}
impl<'a> From<Ident<'a>> for JSStr<'a> {
#[inline]
fn from(value: Ident<'a>) -> Self {
Self::from(value.as_str())
}
}
impl PartialEq for JSStr<'_> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl Eq for JSStr<'_> {}
impl PartialEq<str> for JSStr<'_> {
#[inline]
fn eq(&self, other: &str) -> bool {
self.as_bytes() == other.as_bytes()
}
}
impl PartialEq<&str> for JSStr<'_> {
#[inline]
fn eq(&self, other: &&str) -> bool {
self == *other
}
}
impl PartialEq<JSStr<'_>> for str {
#[inline]
fn eq(&self, other: &JSStr<'_>) -> bool {
other == self
}
}
impl PartialEq<JSStr<'_>> for &str {
#[inline]
fn eq(&self, other: &JSStr<'_>) -> bool {
other == *self
}
}
impl PartialEq<Ident<'_>> for JSStr<'_> {
#[inline]
fn eq(&self, other: &Ident<'_>) -> bool {
self == other.as_str()
}
}
impl PartialEq<JSStr<'_>> for Ident<'_> {
#[inline]
fn eq(&self, other: &JSStr<'_>) -> bool {
other == self.as_str()
}
}
impl Hash for JSStr<'_> {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
match self.as_str() {
Some(value) => value.hash(state),
None => self.as_bytes().hash(state),
}
}
}
impl Debug for JSStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_char('"')?;
for c in self.chars() {
if let Some(c) = c.to_char() {
if c == '\'' {
f.write_char(c)?;
} else {
for escaped in c.escape_debug() {
f.write_char(escaped)?;
}
}
} else {
write!(f, "\\u{:04x}", c.to_u32())?;
}
}
f.write_char('"')
}
}
impl<'new_alloc> CloneIn<'new_alloc> for JSStr<'_> {
type Cloned = JSStr<'new_alloc>;
#[inline]
fn clone_in_impl(
&self,
_with_semantic_ids: CloneInSemanticIds,
allocator: &'new_alloc Allocator,
) -> Self::Cloned {
let bytes = allocator.alloc_slice_copy(self.as_bytes());
unsafe { JSStr::from_bytes_unchecked(bytes, self.has_lone_surrogate()) }
}
}
impl<'a> Dummy<'a> for JSStr<'a> {
#[inline]
fn dummy(_allocator: &'a Allocator) -> Self {
JSStr::empty()
}
}
#[derive(Clone)]
struct JSChars<'a> {
remaining: &'a [u8],
}
impl Iterator for JSChars<'_> {
type Item = JSChar;
#[inline]
fn next(&mut self) -> Option<JSChar> {
let (&first, rest) = self.remaining.split_first()?;
let (value, len) = if first.is_ascii() {
(u32::from(first), 0)
} else {
unsafe {
let second = u32::from(*rest.get_unchecked(0) & CONT_MASK);
if first < THREE_BYTE_TAG {
((u32::from(first & TWO_BYTE_MASK) << 6) | second, 1)
} else {
let third = u32::from(*rest.get_unchecked(1) & CONT_MASK);
if first < FOUR_BYTE_TAG {
((u32::from(first & THREE_BYTE_MASK) << 12) | (second << 6) | third, 2)
} else {
let fourth = u32::from(*rest.get_unchecked(2) & CONT_MASK);
let first = u32::from(first & FOUR_BYTE_MASK);
((first << 18) | (second << 12) | (third << 6) | fourth, 3)
}
}
}
};
self.remaining = unsafe { rest.get_unchecked(len..) };
Some(unsafe { JSChar::from_u32_unchecked(value) })
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.remaining.len();
(len.div_ceil(char::MAX_LEN_UTF8), Some(len))
}
}
impl FusedIterator for JSChars<'_> {}
#[derive(Clone)]
struct EncodeUtf16<'a> {
chars: JSChars<'a>,
pending: u16,
}
impl Iterator for EncodeUtf16<'_> {
type Item = u16;
#[inline]
#[expect(clippy::cast_possible_truncation, reason = "code units are masked or range-checked")]
fn next(&mut self) -> Option<u16> {
if self.pending != 0 {
let trail = self.pending;
self.pending = 0;
return Some(trail);
}
let value = self.chars.next()?.to_u32();
if value < SUPPLEMENTARY_MIN {
Some(value as u16)
} else {
let offset = value - SUPPLEMENTARY_MIN;
self.pending = (TRAIL_SURROGATE_MIN | (offset & SURROGATE_PAYLOAD_MASK)) as u16;
Some((LEAD_SURROGATE_MIN | (offset >> 10)) as u16)
}
}
}
impl FusedIterator for EncodeUtf16<'_> {}
#[cfg(test)]
#[path = "js_str/tests.rs"]
mod tests;