use crate::common::Span;
use crate::error::{Error, Result};
use crate::wtf8;
use alloc::string::String;
use alloc::vec::Vec;
#[must_use]
pub(super) fn number(text: &str) -> f64 {
let cleaned: String = text.chars().filter(|&c| c != '_').collect();
if let Some(rest) = strip_radix(&cleaned, ['x', 'X']) {
return from_radix(rest, 16);
}
if let Some(rest) = strip_radix(&cleaned, ['o', 'O']) {
return from_radix(rest, 8);
}
if let Some(rest) = strip_radix(&cleaned, ['b', 'B']) {
return from_radix(rest, 2);
}
if let Some(rest) = cleaned.strip_prefix('0')
&& !rest.is_empty()
&& rest.bytes().all(|b| (b'0'..=b'7').contains(&b))
{
return from_radix(rest, 8);
}
let mut s = cleaned;
if s.starts_with('.') {
s.insert(0, '0');
}
if let Some(dot) = s.find('.') {
let after = &s[dot + 1..];
if after.is_empty() || after.starts_with(['e', 'E']) {
s.insert(dot + 1, '0');
}
}
s.parse::<f64>().unwrap_or(f64::NAN)
}
pub(super) fn is_legacy_octal_literal(text: &str) -> bool {
let b = text.as_bytes();
b.len() >= 2 && b[0] == b'0' && b[1].is_ascii_digit()
}
fn strip_radix(s: &str, markers: [char; 2]) -> Option<&str> {
let bytes = s.as_bytes();
if bytes.len() >= 2
&& bytes[0] == b'0'
&& (s[1..2].starts_with(markers[0]) || s[1..2].starts_with(markers[1]))
{
Some(&s[2..])
} else {
None
}
}
fn from_radix(digits: &str, radix: u32) -> f64 {
let mut acc = 0.0_f64;
let r = f64::from(radix);
for c in digits.chars() {
if let Some(d) = c.to_digit(radix) {
acc = acc * r + f64::from(d);
}
}
acc
}
#[must_use]
pub(super) fn identifier_name(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut chars = raw.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
chars.next(); let mut value: u32 = 0;
if chars.peek() == Some(&'{') {
chars.next(); while let Some(&d) = chars.peek() {
if d == '}' {
break;
}
value = value
.saturating_mul(16)
.saturating_add(d.to_digit(16).unwrap_or(0));
chars.next();
}
chars.next(); } else {
for _ in 0..4 {
if let Some(d) = chars.next().and_then(|d| d.to_digit(16)) {
value = value * 16 + d;
}
}
}
if let Some(ch) = char::from_u32(value) {
out.push(ch);
}
}
out
}
#[must_use]
pub(super) fn bigint(text: &str) -> String {
text.chars().filter(|&c| c != '_' && c != 'n').collect()
}
pub(super) fn string(raw: &str, span: Span) -> Result<Vec<u8>> {
let inner = &raw[1..raw.len() - 1];
decode_escapes(inner, span)
}
#[must_use]
pub(super) fn string_has_legacy_octal_escape(raw: &str) -> bool {
let b = raw.as_bytes();
let mut i = 0;
while i < b.len() {
if b[i] != b'\\' {
i += 1;
continue;
}
match b.get(i + 1) {
Some(d @ b'1'..=b'9') => {
let _ = d;
return true;
}
Some(b'0') => {
if b.get(i + 2).is_some_and(u8::is_ascii_digit) {
return true;
}
i += 2; }
Some(_) => i += 2,
None => i += 1,
}
}
false
}
pub(super) fn string_key(raw: &str, span: Span) -> Result<String> {
Ok(wtf8::to_string_lossy(&string(raw, span)?))
}
pub(super) fn decode_escapes(body: &str, span: Span) -> Result<Vec<u8>> {
let mut out = Vec::with_capacity(body.len());
let mut chars = body.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
push_char(&mut out, c);
continue;
}
let Some(esc) = chars.next() else {
return Err(Error::syntax("unterminated escape sequence", span));
};
match esc {
'n' => out.push(b'\n'),
't' => out.push(b'\t'),
'r' => out.push(b'\r'),
'b' => out.push(0x08),
'f' => out.push(0x0C),
'v' => out.push(0x0B),
'0' if !chars.peek().is_some_and(|c| c.is_ascii_digit()) => out.push(0),
'x' => {
let hi = hex_digit(chars.next(), span)?;
let lo = hex_digit(chars.next(), span)?;
push_char(&mut out, char::from(hi * 16 + lo));
}
'u' => decode_unicode_escape(&mut chars, &mut out, span)?,
'\n' | '\u{2028}' | '\u{2029}' => {}
'\r' => {
if chars.peek() == Some(&'\n') {
chars.next();
}
}
other => push_char(&mut out, other),
}
}
Ok(out)
}
pub(super) fn validate_template_escapes(body: &str, span: Span) -> Result<()> {
let mut chars = body.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
continue;
}
let Some(esc) = chars.next() else {
return Err(Error::syntax("invalid template escape sequence", span));
};
match esc {
'0' => {
if chars.peek().is_some_and(|c| c.is_ascii_digit()) {
return Err(Error::syntax(
"octal escape sequences are not allowed in template literals",
span,
));
}
}
'1'..='9' => {
return Err(Error::syntax(
"octal escape sequences are not allowed in template literals",
span,
));
}
'x' => {
if !next_is_hex(&mut chars) || !next_is_hex(&mut chars) {
return Err(Error::syntax(
"invalid hexadecimal escape sequence in template literal",
span,
));
}
}
'u' => validate_template_unicode_escape(&mut chars, span)?,
_ => {}
}
}
Ok(())
}
fn validate_template_unicode_escape(
chars: &mut core::iter::Peekable<core::str::Chars<'_>>,
span: Span,
) -> Result<()> {
let invalid = || Error::syntax("invalid Unicode escape sequence in template literal", span);
if chars.peek() == Some(&'{') {
chars.next(); let mut value: u32 = 0;
let mut any = false;
loop {
match chars.peek() {
Some('}') => break,
Some(&d) if d.is_ascii_hexdigit() => {
any = true;
value = value
.saturating_mul(16)
.saturating_add(d.to_digit(16).unwrap_or(0));
chars.next();
}
_ => return Err(invalid()),
}
}
chars.next(); if !any || value > 0x10_FFFF {
return Err(invalid());
}
} else {
for _ in 0..4 {
if !next_is_hex(chars) {
return Err(invalid());
}
}
}
Ok(())
}
fn next_is_hex(chars: &mut core::iter::Peekable<core::str::Chars<'_>>) -> bool {
if chars.peek().is_some_and(char::is_ascii_hexdigit) {
chars.next();
true
} else {
false
}
}
fn push_char(out: &mut Vec<u8>, c: char) {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
fn decode_unicode_escape(
chars: &mut core::iter::Peekable<core::str::Chars<'_>>,
out: &mut Vec<u8>,
span: Span,
) -> Result<()> {
if chars.peek() == Some(&'{') {
chars.next(); let mut value: u32 = 0;
while let Some(&c) = chars.peek() {
if c == '}' {
break;
}
value = value
.saturating_mul(16)
.saturating_add(u32::from(hex_digit(chars.next(), span)?));
}
chars.next(); if value > 0x10_FFFF {
return Err(Error::syntax("invalid escape sequence", span));
}
wtf8::encode_code_point(value, out);
return Ok(());
}
let hi = read_u16_hex(chars, span)?;
if (0xD800..=0xDBFF).contains(&hi) {
let mut clone = chars.clone();
if clone.next() == Some('\\') && clone.next() == Some('u') {
let lo = read_u16_hex(&mut clone, span)?;
if (0xDC00..=0xDFFF).contains(&lo) {
*chars = clone;
let cp = 0x10000 + ((u32::from(hi) - 0xD800) << 10) + (u32::from(lo) - 0xDC00);
wtf8::encode_code_point(cp, out);
return Ok(());
}
}
}
wtf8::encode_utf16_unit(hi, out);
Ok(())
}
fn read_u16_hex(chars: &mut core::iter::Peekable<core::str::Chars<'_>>, span: Span) -> Result<u16> {
let mut v: u16 = 0;
for _ in 0..4 {
v = v * 16 + u16::from(hex_digit(chars.next(), span)?);
}
Ok(v)
}
fn hex_digit(c: Option<char>, span: Span) -> Result<u8> {
match c.and_then(|c| c.to_digit(16)) {
Some(d) => Ok(d as u8),
None => Err(Error::syntax("invalid escape sequence", span)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sp() -> Span {
Span::new(0, 0)
}
#[test]
fn decimal_numbers() {
assert_eq!(number("0"), 0.0);
assert_eq!(number("42"), 42.0);
assert_eq!(number("2.5"), 2.5);
assert_eq!(number(".5"), 0.5);
assert_eq!(number("5."), 5.0);
assert_eq!(number("1e3"), 1000.0);
assert_eq!(number("1.5E-3"), 0.0015);
assert_eq!(number("1_000_000"), 1_000_000.0);
}
#[test]
fn radix_numbers() {
assert_eq!(number("0xFF"), 255.0);
assert_eq!(number("0xdead_beef"), 0xDEAD_BEEFu32 as f64);
assert_eq!(number("0o17"), 15.0);
assert_eq!(number("0b1010"), 10.0);
}
#[test]
fn bigint_normalization() {
assert_eq!(bigint("123n"), "123");
assert_eq!(bigint("1_000n"), "1000");
assert_eq!(bigint("0xFFn"), "0xFF");
}
#[test]
fn template_escape_validation() {
for ok in [
"",
"abc",
r"\n\t\\",
r"\x41",
r"A",
r"\u{1F600}",
r"\u{D800}",
r"\0",
r"\w",
] {
assert!(
validate_template_escapes(ok, sp()).is_ok(),
"should accept: {ok:?}"
);
}
for bad in [
r"\x0", r"\xZZ", r"\u0", r"\u", r"\u{}", r"\u{110000}", r"\u{1F_639}", r"\00", r"\1", r"\8", r"\9", ] {
assert!(
validate_template_escapes(bad, sp()).is_err(),
"should reject: {bad:?}"
);
}
}
#[test]
fn string_escapes() {
assert_eq!(string(r#""hello""#, sp()).unwrap(), b"hello");
assert_eq!(string(r#""a\tb\nc""#, sp()).unwrap(), b"a\tb\nc");
assert_eq!(string(r#""\x41\x42""#, sp()).unwrap(), b"AB");
assert_eq!(string(r#""A""#, sp()).unwrap(), b"A");
assert_eq!(
string(r#""\u{1F600}""#, sp()).unwrap(),
"\u{1F600}".as_bytes()
);
assert_eq!(string(r"'it\'s'", sp()).unwrap(), b"it's");
assert_eq!(string(r#""😀""#, sp()).unwrap(), "\u{1F600}".as_bytes());
assert_eq!(string("\"a\\\nb\"", sp()).unwrap(), b"ab");
}
#[test]
fn lone_surrogates_preserved() {
assert_eq!(
string(r#""\uD800""#, sp()).unwrap(),
wtf8::from_utf16(&[0xD800])
);
assert_eq!(
string(r#""\u{D800}""#, sp()).unwrap(),
wtf8::from_utf16(&[0xD800])
);
assert_eq!(
string(r#""\uDC00""#, sp()).unwrap(),
wtf8::from_utf16(&[0xDC00])
);
assert_eq!(string(r#""😀""#, sp()).unwrap(), "😀".as_bytes());
let mut expected = wtf8::from_utf16(&[0xD800]);
expected.push(b'x');
assert_eq!(string(r#""\uD800x""#, sp()).unwrap(), expected);
}
}