use crate::input::{
code_points::{LF, REVERSE_SOLIDUS},
Filtered, FilteredChar,
};
use super::{errors::Eof, escaped_code_point::EscapedCodePoint};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct StringToken<'a> {
full: &'a str,
starting_code_point_str: &'a str,
value_original_str: &'a str,
has_filtered_chars_or_escaped_code_points: bool,
ending_code_point_str: &'a str,
}
impl<'a> std::fmt::Debug for StringToken<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("StringToken").field(&self.full).finish()
}
}
#[derive(Debug)]
pub enum StringTokenParseError<'a> {
Eof {
full: &'a str,
starting_code_point_str: &'a str,
expected_ending_code_point: FilteredChar,
},
Newline {
before_new_line: &'a str,
stream_starting_with_new_line: Filtered<'a>,
},
}
impl<'a> StringTokenParseError<'a> {
pub(crate) const fn to_message(&self) -> &'static str {
match self {
StringTokenParseError::Eof { .. } => "unexpected eof when parsing string token",
StringTokenParseError::Newline { .. } => "unexpected newline when parsing string token",
}
}
}
impl<'a> StringToken<'a> {
pub const fn consume(
stream: Filtered<'a>,
) -> Result<(Option<Self>, Filtered<'a>), StringTokenParseError> {
let original = stream.copy();
let (fc, stream) = stream.consume_n_code_points::<1>();
if !matches!(fc.to_chars_padding_zero(), ['"' | '\'']) {
return Ok((None, original));
}
let Some(fc) = fc.first() else { unreachable!() };
let starting_code_point_str = original.str_before(&stream);
match Self::consume_after_starting_code_point(stream, fc) {
Ok(ConsumeAfterStartingCodePoint {
value_original_str,
has_filtered_chars_or_escaped_code_points,
ending_code_point_str,
remaining: stream,
}) => Ok((
Some(Self {
full: original.str_before(&stream),
value_original_str,
has_filtered_chars_or_escaped_code_points,
starting_code_point_str,
ending_code_point_str,
}),
stream,
)),
Err(err) => Err(match err {
ConsumeAfterStartingCodePointError::Eof => StringTokenParseError::Eof {
full: original.0.as_str(),
starting_code_point_str,
expected_ending_code_point: fc,
},
ConsumeAfterStartingCodePointError::Newline {
stream_starting_with_new_line,
} => StringTokenParseError::Newline {
before_new_line: original.str_before(&stream_starting_with_new_line),
stream_starting_with_new_line,
},
}),
}
}
const fn consume_after_starting_code_point(
mut stream: Filtered<'a>,
ending_code_point: FilteredChar,
) -> Result<ConsumeAfterStartingCodePoint<'a>, ConsumeAfterStartingCodePointError> {
let after_starting_code_point = stream.copy();
let mut has_filtered_chars_or_escaped_code_points = false;
loop {
let current_stream = stream.copy();
if let Some((fc, has_filtered, new_stream)) = stream.next_and_report() {
if has_filtered {
has_filtered_chars_or_escaped_code_points = true;
}
match fc.to_char() {
LF => {
return Err(ConsumeAfterStartingCodePointError::Newline {
stream_starting_with_new_line: current_stream,
});
}
REVERSE_SOLIDUS => {
match EscapedCodePoint::consume_after_reverse_solidus(new_stream.copy()) {
Ok((None, new_stream)) => {
has_filtered_chars_or_escaped_code_points = true;
stream = new_stream;
}
Ok((Some(_), new_stream)) => {
has_filtered_chars_or_escaped_code_points = true;
stream = new_stream;
}
Err(Eof) => {
stream = new_stream
}
}
}
u if u == ending_code_point.to_char() => {
let value_original_str =
after_starting_code_point.str_before(¤t_stream);
let ending_code_point_str = current_stream.str_before(&new_stream);
return Ok(ConsumeAfterStartingCodePoint {
value_original_str,
has_filtered_chars_or_escaped_code_points,
ending_code_point_str,
remaining: new_stream,
});
}
_ => {
stream = new_stream
}
}
} else {
return Err(ConsumeAfterStartingCodePointError::Eof);
}
}
}
fn chars(&self) -> Chars<'a> {
Chars {
string_token_value: Filtered::new(self.value_original_str),
}
}
}
pub(crate) enum ConsumeAfterStartingCodePointError<'a> {
Eof,
Newline {
stream_starting_with_new_line: Filtered<'a>,
},
}
struct ConsumeAfterStartingCodePoint<'a> {
value_original_str: &'a str,
has_filtered_chars_or_escaped_code_points: bool,
ending_code_point_str: &'a str,
remaining: Filtered<'a>,
}
struct Chars<'a> {
string_token_value: Filtered<'a>,
}
impl<'a> Chars<'a> {
const fn into_next(self) -> Option<(char, Self)> {
let mut stream = self.string_token_value;
loop {
stream = if let Some((fc, new_stream)) = stream.next() {
match fc.to_char() {
LF => {
unreachable!()
}
REVERSE_SOLIDUS => {
match EscapedCodePoint::consume_after_reverse_solidus(new_stream.copy()) {
Ok((None, new_stream)) => {
new_stream
}
Ok((Some(e), new_stream)) => {
return Some((
e.to_code_point(),
Self {
string_token_value: new_stream,
},
));
}
Err(Eof) => {
unreachable!()
}
}
}
ch => {
return Some((
ch,
Self {
string_token_value: new_stream,
},
));
}
}
} else {
return None;
}
}
}
}
#[cfg(feature = "alloc")]
mod alloc {
use alloc::{borrow::Cow, string::String};
use super::StringToken;
impl<'a> StringToken<'a> {
pub fn value_unescape(&self) -> Cow<'a, str> {
if self.has_filtered_chars_or_escaped_code_points {
let mut chars = self.chars();
let mut res = String::with_capacity(self.value_original_str.len());
while let Some((ch, new_chars)) = chars.into_next() {
chars = new_chars;
res.push(ch);
}
Cow::Owned(res)
} else {
Cow::Borrowed(self.value_original_str)
}
}
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "alloc")]
#[test]
fn unescape() {
use crate::input::Filtered;
use super::StringToken;
let (s, remaining) = StringToken::consume(Filtered::new("'\\\n'")).unwrap();
remaining.assert_empty();
let s = s.unwrap();
assert_eq!(s.full, "'\\\n'");
assert_eq!(s.starting_code_point_str, "'");
assert_eq!(s.value_original_str, "\\\n");
assert!(s.has_filtered_chars_or_escaped_code_points);
assert_eq!(s.ending_code_point_str, "'");
assert_eq!(s.value_unescape(), "");
}
}