use std::slice;
use oxc_ast::ast::StringLiteral;
use oxc_data_structures::assert_unchecked;
use oxc_syntax::identifier::{LS, NBSP, PS};
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.as_slice().as_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.clone().next().copied()
}
#[inline]
unsafe fn consume_byte_unchecked(&mut self) {
unsafe { assert_unchecked!(!self.bytes.as_slice().is_empty()) };
self.bytes.next().unwrap();
}
#[inline]
unsafe fn consume_bytes_unchecked<const N: usize>(&mut self) {
unsafe { assert_unchecked!(self.bytes.as_slice().len() >= N) };
for _i in 0..N {
self.bytes.next().unwrap();
}
}
#[inline]
fn start_chunk(&mut self) {
self.chunk_start = self.bytes.as_slice().as_ptr();
}
#[inline]
unsafe fn flush_and_consume_byte(&mut self, codegen: &mut Codegen) {
unsafe { self.flush_and_consume_bytes::<1>(codegen) };
}
#[inline]
unsafe fn flush_and_consume_bytes<const N: usize>(&mut self, codegen: &mut Codegen) {
self.flush(codegen);
debug_assert!(self.bytes.as_slice().len() >= N);
unsafe { self.consume_bytes_unchecked::<N>() };
self.start_chunk();
}
fn flush(&mut self, codegen: &mut Codegen) {
self.calculate_quote(codegen);
let len = unsafe {
let bytes_ptr = self.bytes.as_slice().as_ptr();
let offset = bytes_ptr.offset_from(self.chunk_start);
usize::try_from(offset).unwrap_unchecked()
};
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: i64 = 0;
let mut double_cost: i64 = 0;
let mut backtick_cost: i64 = 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.clone().next() == Some(&b'{') {
backtick_cost += 1;
}
}
_ => {}
}
}
#[rustfmt::skip]
let quote = if double_cost >= backtick_cost {
if backtick_cost > single_cost {
Quote::Single
} else {
Quote::Backtick
}
} else if double_cost > single_cost {
Quote::Single
} else {
Quote::Double
};
quote
}
fn calculate_quote_no_backtick(&self) -> Quote {
let mut single_cost: i64 = 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] {
let mut bytes = [0u8; N];
ch.encode_utf8(&mut bytes);
bytes
}
const LS_BYTES: [u8; 3] = to_bytes(LS);
const PS_BYTES: [u8; 3] = to_bytes(PS);
const _: () = assert!(LS_BYTES[0] == 0xE2);
const _: () = assert!(PS_BYTES[0] == 0xE2);
const LS_LAST_2_BYTES: [u8; 2] = [LS_BYTES[1], LS_BYTES[2]];
const PS_LAST_2_BYTES: [u8; 2] = [PS_BYTES[1], PS_BYTES[2]];
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, LS = 14, NB = 15, LO = 16, }
#[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, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, BQ, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, NB, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, LS, __, __, __, __, __, __, __, __, __, __, __, __, LO, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, ])
};
type ByteHandler = unsafe fn(&mut Codegen, &mut PrintStringState);
static BYTE_HANDLERS: Aligned128<[ByteHandler; 16]> = 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_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_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::<3>(codegen) };
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::<2>(codegen) };
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()
}