use oxc_syntax::identifier::is_identifier_start;
pub fn skip_quoted(bytes: &[u8], mut i: usize, quote: u8) -> usize {
while let Some(&byte) = bytes.get(i) {
match byte {
b'\\' => {
if bytes.get(i + 1) == Some(&b'\r') && bytes.get(i + 2) == Some(&b'\n') {
i = i.saturating_add(3);
} else {
i = i.saturating_add(2);
}
}
b'\n' | b'\r' => return i,
b if b == quote => return i + 1,
_ => i += 1,
}
}
i
}
pub(super) fn skip_template_text(bytes: &[u8], mut i: usize) -> (usize, bool) {
while let Some(&byte) = bytes.get(i) {
match byte {
b'\\' => i = i.saturating_add(2),
b'`' => return (i + 1, false),
b'$' if bytes.get(i + 1) == Some(&b'{') => return (i + 2, true),
_ => i += 1,
}
}
(i, false)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum SpeculativeTypeAngleOpen {
Regular,
MalformedIdentifierEscape,
}
pub(super) fn speculative_arrow_default_paren(content: &str, open: usize) -> Option<usize> {
let after = content.get(open + 1..)?;
let marker = skip_type_angle_trivia(after);
if after.as_bytes().get(marker) != Some(&b'(') {
return None;
}
let paren = open + 1 + marker;
paren_starts_arrow_param_default(content, paren).then_some(paren)
}
fn paren_starts_arrow_param_default(content: &str, paren: usize) -> bool {
let after = content.get(paren + 1..).unwrap_or_default();
let bytes = after.as_bytes();
let start = skip_type_angle_trivia(after);
if !matches!(
bytes.get(start),
Some(b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$')
) {
return false;
}
let after_ident = skip_identifier(bytes, start + 1);
let rest = after.get(after_ident..).unwrap_or_default();
let eq = after_ident + skip_type_angle_trivia(rest);
bytes.get(eq) == Some(&b'=') && !matches!(bytes.get(eq + 1), Some(b'=' | b'>'))
}
pub(super) fn speculative_type_angle_open_kind(
content: &str,
open: usize,
) -> Option<SpeculativeTypeAngleOpen> {
let after = content.get(open + 1..).unwrap_or_default();
let marker = skip_type_angle_trivia(after);
match after.as_bytes().get(marker) {
Some(b'{' | b'[' | b'!' | b'(' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'$') => {
Some(SpeculativeTypeAngleOpen::Regular)
}
Some(b'\\') if starts_valid_identifier_escape(after.as_bytes(), marker) => {
Some(SpeculativeTypeAngleOpen::Regular)
}
Some(b'\\') => Some(SpeculativeTypeAngleOpen::MalformedIdentifierEscape),
Some(byte) if !byte.is_ascii() => Some(SpeculativeTypeAngleOpen::Regular),
Some(_) | None => None,
}
}
pub(super) fn starts_valid_identifier_escape(bytes: &[u8], marker: usize) -> bool {
decode_identifier_escape(bytes, marker).is_some_and(is_identifier_start)
}
fn decode_identifier_escape(bytes: &[u8], marker: usize) -> Option<char> {
if bytes.get(marker) != Some(&b'\\') || bytes.get(marker + 1) != Some(&b'u') {
return None;
}
if bytes.get(marker + 2) == Some(&b'{') {
decode_braced_identifier_escape(bytes, marker + 3)
} else {
decode_fixed_identifier_escape(bytes, marker + 2)
}
}
fn decode_braced_identifier_escape(bytes: &[u8], mut i: usize) -> Option<char> {
let mut code_point = 0u32;
let mut digits = 0usize;
while digits < 6 {
let Some(value) = bytes.get(i).and_then(|byte| hex_value(*byte)) else {
break;
};
code_point = (code_point << 4) | value;
i += 1;
digits += 1;
}
if digits == 0 || bytes.get(i) != Some(&b'}') {
return None;
}
char::from_u32(code_point)
}
fn decode_fixed_identifier_escape(bytes: &[u8], start: usize) -> Option<char> {
let mut code_point = 0u32;
for byte in bytes.get(start..start + 4)? {
code_point = (code_point << 4) | hex_value(*byte)?;
}
char::from_u32(code_point)
}
fn hex_value(byte: u8) -> Option<u32> {
match byte {
b'0'..=b'9' => Some(u32::from(byte - b'0')),
b'a'..=b'f' => Some(u32::from(byte - b'a' + 10)),
b'A'..=b'F' => Some(u32::from(byte - b'A' + 10)),
_ => None,
}
}
fn skip_type_angle_trivia(s: &str) -> usize {
let bytes = s.as_bytes();
let mut i = 0;
loop {
match bytes.get(i) {
Some(b'/') if bytes.get(i + 1) == Some(&b'/') => i = skip_line_comment(bytes, i + 2),
Some(b'/') if bytes.get(i + 1) == Some(&b'*') => i = skip_block_comment(bytes, i + 2),
Some(&b) if b < 0x80 => {
if matches!(b, b' ' | b'\t' | 0x0b | 0x0c | b'\n' | b'\r') {
i += 1;
} else {
return i;
}
}
Some(_) => {
let Some(c) = s.get(i..).and_then(|rest| rest.chars().next()) else {
return i;
};
if is_ecmascript_whitespace(c) {
i += c.len_utf8();
} else {
return i;
}
}
None => return i,
}
}
}
fn is_ecmascript_whitespace(c: char) -> bool {
c == '\u{feff}' || (c.is_whitespace() && c != '\u{0085}')
}
pub fn is_expression_trailing_trivia(content: &str) -> bool {
let bytes = content.as_bytes();
let mut i = 0;
loop {
match bytes.get(i) {
Some(b'/') if bytes.get(i + 1) == Some(&b'*') => {
let Some(next) = skip_closed_block_comment(bytes, i + 2) else {
return false;
};
i = next;
}
Some(&b) if b < 0x80 => {
if matches!(b, b' ' | b'\t' | 0x0b | 0x0c | b'\n' | b'\r') {
i += 1;
} else {
return false;
}
}
Some(_) => {
let Some(c) = content.get(i..).and_then(|rest| rest.chars().next()) else {
return false;
};
if is_ecmascript_whitespace(c) {
i += c.len_utf8();
} else {
return false;
}
}
None => return true,
}
}
}
pub fn skip_line_comment(bytes: &[u8], mut i: usize) -> usize {
while let Some(&byte) = bytes.get(i) {
match byte {
b'\n' | b'\r' => break,
0xe2 if bytes.get(i + 1) == Some(&0x80)
&& matches!(bytes.get(i + 2), Some(&0xa8) | Some(&0xa9)) =>
{
break;
}
_ => i += 1,
}
}
i
}
fn skip_closed_block_comment(bytes: &[u8], i: usize) -> Option<usize> {
let at = bytes.get(i..)?.windows(2).position(|pair| pair == b"*/")?;
Some(i + at + 2)
}
mod number;
pub use number::{keyword_allows_regex_after, skip_number};
pub(super) fn skip_block_comment(bytes: &[u8], i: usize) -> usize {
skip_closed_block_comment(bytes, i).unwrap_or(bytes.len())
}
pub fn skip_regex(bytes: &[u8], mut i: usize) -> Option<usize> {
let mut in_character_class = false;
while let Some(&byte) = bytes.get(i) {
match byte {
b'\\' => match bytes.get(i + 1) {
Some(&b'\n' | &b'\r') => return None,
Some(&0xe2)
if bytes.get(i + 2) == Some(&0x80)
&& matches!(bytes.get(i + 3), Some(&0xa8) | Some(&0xa9)) =>
{
return None;
}
_ => i = i.saturating_add(2),
},
b'[' => {
in_character_class = true;
i += 1;
}
b']' => {
in_character_class = false;
i += 1;
}
b'/' if !in_character_class => return Some(skip_identifier(bytes, i + 1)),
b'\n' | b'\r' => return None,
0xe2 if bytes.get(i + 1) == Some(&0x80)
&& matches!(bytes.get(i + 2), Some(&0xa8) | Some(&0xa9)) =>
{
return None;
}
_ => i += 1,
}
}
None
}
pub fn skip_identifier(bytes: &[u8], i: usize) -> usize {
i + bytes.get(i..).map_or(0, |rest| {
rest.iter()
.take_while(
|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'$'),
)
.count()
})
}