use std::slice;
use oxc_ast::ast::StringLiteral;
use oxc_data_structures::{assert_unchecked, slice_iter::SliceIter};
use oxc_syntax::{
identifier::NBSP,
line_terminator::{LS_LAST_2_BYTES, PS_LAST_2_BYTES},
};
use crate::Codegen;
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Quote {
Single = b'\'',
Double = b'"',
Backtick = b'`',
}
impl Quote {
#[inline]
pub fn print(self, codegen: &mut Codegen<'_>) {
unsafe { codegen.code.print_byte_unchecked(self as u8) };
}
}
impl Codegen<'_> {
pub(crate) fn print_string_literal(&mut self, s: &StringLiteral<'_>, allow_backtick: bool) {
self.add_source_mapping(s.span);
let quote = if self.options.minify {
None
} else {
let quote = self.quote;
quote.print(self);
Some(quote)
};
let bytes = s.value.as_bytes().iter();
let mut state = PrintStringState {
chunk_start: bytes.ptr(),
bytes,
quote,
lone_surrogates: s.lone_surrogates,
allow_backtick,
};
while let Some(b) = state.peek() {
let escape = ESCAPES.0[b as usize];
if escape == Escape::__ {
unsafe { state.consume_byte_unchecked() };
} else {
cold_branch(|| {
unsafe { handle_escape(escape, self, &mut state) };
});
}
}
state.flush(self);
let quote = unsafe { state.quote.unwrap_unchecked() };
quote.print(self);
}
}
struct PrintStringState<'s> {
chunk_start: *const u8,
bytes: slice::Iter<'s, u8>,
quote: Option<Quote>,
lone_surrogates: bool,
allow_backtick: bool,
}
impl PrintStringState<'_> {
#[inline]
fn peek(&self) -> Option<u8> {
self.bytes.peek_copy()
}
#[inline]
unsafe fn consume_byte_unchecked(&mut self) {
unsafe { self.bytes.next_unchecked() };
}
#[inline]
unsafe fn consume_bytes_unchecked(&mut self, count: usize) {
unsafe { self.bytes.advance_unchecked(count) };
}
#[inline]
fn start_chunk(&mut self) {
self.chunk_start = self.bytes.ptr();
}
#[inline]
unsafe fn flush_and_consume_byte(&mut self, codegen: &mut Codegen) {
unsafe { self.flush_and_consume_bytes(codegen, 1) };
}
#[inline]
unsafe fn flush_and_consume_bytes(&mut self, codegen: &mut Codegen, count: usize) {
self.flush(codegen);
unsafe { self.consume_bytes_unchecked(count) };
self.start_chunk();
}
fn flush(&mut self, codegen: &mut Codegen) {
self.calculate_quote(codegen);
let len = unsafe {
let bytes_ptr = self.bytes.ptr();
bytes_ptr.offset_from_unsigned(self.chunk_start)
};
unsafe {
let slice = slice::from_raw_parts(self.chunk_start, len);
codegen.code.print_bytes_unchecked(slice);
}
}
#[inline]
fn calculate_quote(&mut self, codegen: &mut Codegen) -> Quote {
if let Some(quote) = self.quote { quote } else { self.calculate_quote_impl(codegen) }
}
fn calculate_quote_impl(&mut self, codegen: &mut Codegen) -> Quote {
let quote = if self.allow_backtick {
self.calculate_quote_maybe_backtick()
} else {
self.calculate_quote_no_backtick()
};
quote.print(codegen);
self.quote = Some(quote);
quote
}
fn calculate_quote_maybe_backtick(&self) -> Quote {
let mut single_cost: isize = 0;
let mut double_cost: isize = 0;
let mut backtick_cost: isize = 0;
let mut bytes = self.bytes.clone();
while let Some(b) = bytes.next() {
match b {
b'\n' => backtick_cost -= 1,
b'\'' => single_cost += 1,
b'"' => double_cost += 1,
b'`' => backtick_cost += 1,
b'$' if bytes.peek() == Some(&b'{') => {
backtick_cost += 1;
}
_ => {}
}
}
#[rustfmt::skip]
let quote = if backtick_cost <= double_cost {
if backtick_cost <= single_cost {
Quote::Backtick
} else {
Quote::Single
}
} else if double_cost <= single_cost {
Quote::Double
} else {
Quote::Single
};
quote
}
fn calculate_quote_no_backtick(&self) -> Quote {
let mut single_cost: isize = 0;
for &b in self.bytes.clone() {
match b {
b'\'' => single_cost += 1,
b'"' => single_cost -= 1,
_ => {}
}
}
if single_cost < 0 { Quote::Single } else { Quote::Double }
}
}
const fn to_bytes<const N: usize>(ch: char) -> [u8; N] {
assert!(ch.len_utf8() == N);
let mut bytes = [0u8; N];
ch.encode_utf8(&mut bytes);
bytes
}
const NBSP_BYTES: [u8; 2] = to_bytes(NBSP);
const _: () = assert!(NBSP_BYTES[0] == 0xC2);
const NBSP_LAST_BYTE: u8 = NBSP_BYTES[1];
const LOSSY_REPLACEMENT_CHAR_BYTES: [u8; 3] = to_bytes('\u{FFFD}');
const _: () = assert!(LOSSY_REPLACEMENT_CHAR_BYTES[0] == 0xEF);
const LOSSY_REPLACEMENT_CHAR_LAST_2_BYTES: [u8; 2] =
[LOSSY_REPLACEMENT_CHAR_BYTES[1], LOSSY_REPLACEMENT_CHAR_BYTES[2]];
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum Escape {
__ = 0, NU = 1, BE = 2, BK = 3, VT = 4, FF = 5, NL = 6, CR = 7, ES = 8, BS = 9, SQ = 10, DQ = 11, BQ = 12, DO = 13, LT = 14, LS = 15, NB = 16, LO = 17, }
#[repr(C, align(128))]
struct Aligned128<T>(T);
static ESCAPES: Aligned128<[Escape; 256]> = {
#[allow(clippy::enum_glob_use, clippy::allow_attributes)]
use Escape::*;
Aligned128([
NU, __, __, __, __, __, __, BE, BK, __, NL, VT, FF, CR, __, __, __, __, __, __, __, __, __, __, __, __, __, ES, __, __, __, __, __, __, DQ, __, DO, __, __, SQ, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, LT, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, BQ, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, NB, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, LS, __, __, __, __, __, __, __, __, __, __, __, __, LO, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, ])
};
type ByteHandler = unsafe fn(&mut Codegen, &mut PrintStringState);
static BYTE_HANDLERS: Aligned128<[ByteHandler; 17]> = Aligned128([
print_null,
print_bell,
print_backspace,
print_vertical_tab,
print_form_field,
print_new_line,
print_carriage_return,
print_escape,
print_backslash,
print_single_quote,
print_double_quote,
print_backtick,
print_dollar,
print_less_than,
print_ls_or_ps,
print_non_breaking_space,
print_lossy_replacement,
]);
unsafe fn handle_escape(escape: Escape, codegen: &mut Codegen, state: &mut PrintStringState) {
unsafe { assert_unchecked!(escape != Escape::__) };
let byte_handler = BYTE_HANDLERS.0[escape as usize - 1];
unsafe { byte_handler(codegen, state) };
}
unsafe fn print_null(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x00));
unsafe { state.flush_and_consume_byte(codegen) };
if state.peek().is_some_and(|b| b.is_ascii_digit()) {
codegen.print_str("\\x00");
} else {
codegen.print_str("\\0");
}
}
unsafe fn print_bell(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x07));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\x07");
}
unsafe fn print_backspace(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x08));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\b");
}
unsafe fn print_vertical_tab(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x0B));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\v");
}
unsafe fn print_form_field(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x0C));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\f");
}
unsafe fn print_new_line(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'\n'));
if state.calculate_quote(codegen) == Quote::Backtick {
unsafe { state.consume_byte_unchecked() };
} else {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\n");
}
}
unsafe fn print_carriage_return(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'\r'));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\r");
}
unsafe fn print_escape(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0x1B));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\x1B");
}
unsafe fn print_backslash(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'\\'));
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\\\");
}
unsafe fn print_single_quote(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'\''));
if state.calculate_quote(codegen) == Quote::Single {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\'");
} else {
unsafe { state.consume_byte_unchecked() };
}
}
unsafe fn print_double_quote(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'"'));
if state.calculate_quote(codegen) == Quote::Double {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\\"");
} else {
unsafe { state.consume_byte_unchecked() };
}
}
unsafe fn print_backtick(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'`'));
if state.calculate_quote(codegen) == Quote::Backtick {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\`");
} else {
unsafe { state.consume_byte_unchecked() };
}
}
unsafe fn print_dollar(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'$'));
let next = state.bytes.as_slice().get(1);
if next == Some(&b'{') && state.calculate_quote(codegen) == Quote::Backtick {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\$");
} else {
unsafe { state.consume_byte_unchecked() };
}
}
unsafe fn print_less_than(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(b'<'));
let slice = state.bytes.as_slice();
unsafe { state.consume_byte_unchecked() };
if slice.len() >= 8 && is_script_close_tag(&slice[0..8]) {
unsafe { state.flush_and_consume_byte(codegen) };
codegen.print_str("\\/");
unsafe { state.consume_bytes_unchecked(6) };
}
}
unsafe fn print_ls_or_ps(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0xE2));
let next2: [u8; 2] = {
let next2 = unsafe { state.bytes.as_slice().get_unchecked(1..3) };
next2.try_into().unwrap()
};
let replacement = match next2 {
LS_LAST_2_BYTES => "\\u2028",
PS_LAST_2_BYTES => "\\u2029",
_ => {
unsafe { state.consume_bytes_unchecked(3) };
return;
}
};
unsafe { state.flush_and_consume_bytes(codegen, 3) };
codegen.print_str(replacement);
}
unsafe fn print_non_breaking_space(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0xC2));
let next = unsafe { *state.bytes.as_slice().get_unchecked(1) };
if next == NBSP_LAST_BYTE {
unsafe { state.flush_and_consume_bytes(codegen, 2) };
codegen.print_str("\\xA0");
} else {
unsafe { state.consume_bytes_unchecked(2) };
}
}
unsafe fn print_lossy_replacement(codegen: &mut Codegen, state: &mut PrintStringState) {
debug_assert_eq!(state.peek(), Some(0xEF));
if state.lone_surrogates {
let next2: [u8; 2] = {
let next2 = unsafe { state.bytes.as_slice().get_unchecked(1..3) };
next2.try_into().unwrap()
};
if next2 == LOSSY_REPLACEMENT_CHAR_LAST_2_BYTES {
let bytes = &mut state.bytes;
let hex: [u8; 4] = bytes.as_slice()[3..7].try_into().unwrap();
if hex == *b"fffd" {
unsafe { state.consume_bytes_unchecked(3) };
state.flush(codegen);
unsafe { state.consume_bytes_unchecked(4) };
state.start_chunk();
return;
}
state.flush(codegen);
assert_eq!(u32::from_ne_bytes(hex) & 0x8080_8080, 0);
unsafe { state.consume_bytes_unchecked(7) };
state.start_chunk();
codegen.print_str("\\u");
unsafe { codegen.code.print_bytes_unchecked(&hex) };
return;
}
}
unsafe { state.consume_bytes_unchecked(3) };
}
#[cold]
pub fn cold_branch<F: FnOnce() -> T, T>(f: F) -> T {
f()
}
#[expect(clippy::inline_always)]
#[inline(always)]
pub fn is_script_close_tag(slice: &[u8]) -> bool {
let mut bytes: [u8; 8] = slice.try_into().unwrap();
for byte in bytes.iter_mut().skip(2) {
*byte |= 32;
}
bytes == *b"</script"
}