use oxc_allocator::{ArenaVec, GetAllocator};
use crate::{JSChar, JSStr, wtf8::*};
pub struct JSStrBuilder<'a> {
bytes: ArenaVec<'a, u8>,
pending_lead_surrogate: Option<u16>,
has_lone_surrogate: bool,
}
impl<'a> JSStrBuilder<'a> {
#[inline]
pub fn new_in(allocator: &impl GetAllocator<'a>) -> Self {
Self {
bytes: ArenaVec::new_in(allocator),
pending_lead_surrogate: None,
has_lone_surrogate: false,
}
}
#[inline]
pub fn with_capacity_in(capacity: usize, allocator: &impl GetAllocator<'a>) -> Self {
Self {
bytes: ArenaVec::with_capacity_in(capacity, allocator),
pending_lead_surrogate: None,
has_lone_surrogate: false,
}
}
#[inline]
pub fn push_str(&mut self, value: &str) {
if value.is_empty() {
return;
}
if self.pending_lead_surrogate.is_some() {
self.bytes.reserve(value.len() + SURROGATE_BYTE_LEN);
self.flush_pending();
}
self.bytes.extend_from_slice_copy(value.as_bytes());
}
#[inline]
pub fn push(&mut self, value: char) {
self.push_str(value.encode_utf8(&mut [0; char::MAX_LEN_UTF8]));
}
#[inline]
#[expect(clippy::cast_possible_truncation, reason = "lone surrogates fit in `u16`")]
pub fn push_js_char(&mut self, value: JSChar) {
if let Some(lead) = self.pending_lead_surrogate
&& value.is_trail_surrogate()
{
self.bytes.reserve(char::MAX_LEN_UTF8);
self.append_pair(lead, value.to_u32() as u16);
self.pending_lead_surrogate = None;
return;
}
let mut buffer = [0; char::MAX_LEN_UTF8];
let bytes = value.encode(&mut buffer);
let additional = self.pending_bytes() + bytes.len();
self.bytes.reserve(additional);
self.flush_pending();
if value.is_lead_surrogate() {
self.pending_lead_surrogate = Some(value.to_u32() as u16);
} else {
self.bytes.extend_from_slice_copy(bytes);
self.has_lone_surrogate |= value.is_surrogate();
}
}
#[inline]
pub fn push_code_unit(&mut self, unit: u16) {
self.push_js_char(JSChar::from_code_unit(unit));
}
#[inline]
pub fn push_utf16(&mut self, units: &[u16]) {
for &unit in units {
self.push_code_unit(unit);
}
}
#[inline]
pub fn push_js_str(&mut self, value: JSStr<'_>) {
if let Some(value) = value.as_str() {
self.push_str(value);
} else {
self.push_js_str_slow(value);
}
}
#[cold]
#[inline(never)]
fn push_js_str_slow(&mut self, value: JSStr<'_>) {
fn decode_surrogate(second: u8, third: u8) -> u16 {
(u16::from(SURROGATE_FIRST_BYTE & THREE_BYTE_MASK) << 12)
| (u16::from(second & CONT_MASK) << 6)
| u16::from(third & CONT_MASK)
}
debug_assert!(value.has_lone_surrogate());
let mut bytes = value.as_bytes();
let additional = bytes.len() + self.pending_bytes();
self.bytes.reserve(additional);
let mut trimmed = false;
if let Some(lead) = self.pending_lead_surrogate
&& let [SURROGATE_FIRST_BYTE, second @ (TRAIL_SURROGATE_SECOND_BYTE_MIN..), third, ..] =
bytes
{
self.append_pair(lead, decode_surrogate(*second, *third));
self.pending_lead_surrogate = None;
bytes = &bytes[SURROGATE_BYTE_LEN..];
trimmed = true;
} else {
self.flush_pending();
}
if let [
..,
SURROGATE_FIRST_BYTE,
second @ LEAD_SURROGATE_SECOND_BYTE_MIN..=LEAD_SURROGATE_SECOND_BYTE_MAX,
third,
] = bytes
{
self.pending_lead_surrogate = Some(decode_surrogate(*second, *third));
bytes = &bytes[..bytes.len() - SURROGATE_BYTE_LEN];
trimmed = true;
}
self.has_lone_surrogate = self.has_lone_surrogate
|| !trimmed
|| bytes.windows(SURROGATE_BYTE_LEN).any(|bytes| {
bytes[0] == SURROGATE_FIRST_BYTE && bytes[1] >= SURROGATE_SECOND_BYTE_MIN
});
self.bytes.extend_from_slice_copy(bytes);
}
#[inline]
pub fn into_js_str(mut self) -> JSStr<'a> {
self.flush_pending();
let bytes = self.bytes.into_arena_slice();
unsafe { JSStr::from_bytes_unchecked(bytes, self.has_lone_surrogate) }
}
#[inline]
fn pending_bytes(&self) -> usize {
if self.pending_lead_surrogate.is_some() { SURROGATE_BYTE_LEN } else { 0 }
}
#[inline]
fn flush_pending(&mut self) {
if let Some(lead) = self.pending_lead_surrogate.take() {
self.bytes.extend_from_slice_copy(
JSChar::from_code_unit(lead).encode(&mut [0; char::MAX_LEN_UTF8]),
);
self.has_lone_surrogate = true;
}
}
#[inline]
fn append_pair(&mut self, lead: u16, trail: u16) {
let value = SUPPLEMENTARY_MIN
+ ((u32::from(lead) - LEAD_SURROGATE_MIN) << 10)
+ (u32::from(trail) - TRAIL_SURROGATE_MIN);
let c = char::from_u32(value).unwrap();
self.bytes.extend_from_slice_copy(c.encode_utf8(&mut [0; char::MAX_LEN_UTF8]).as_bytes());
}
}