use core::range::Range;
use std::iter::Peekable;
use crate::{
Error, ErrorKind, Result,
tokens::{CharWithContext, JsonChar, Token, TokenWithContext},
};
enum StringState {
Open,
CharOrEscapeOrEnd {
string_range: Range<usize>,
quote_range: Range<usize>,
},
Escape {
string_range: Range<usize>,
quote_range: Range<usize>,
slash_range: Range<usize>,
},
UEscape {
string_range: Range<usize>,
quote_range: Range<usize>,
u_range: Range<usize>,
slash_range: Range<usize>,
digits_seen: usize,
},
End(TokenWithContext),
}
impl StringState {
fn process(
self,
chars: &mut Peekable<impl Iterator<Item = CharWithContext>>,
input: &str,
) -> Result<Self> {
let res = match self {
StringState::Open => {
let Some(CharWithContext(starting_quote, JsonChar('"'))) = chars.next() else {
unreachable!("must start with a quote");
};
StringState::CharOrEscapeOrEnd {
string_range: starting_quote,
quote_range: starting_quote,
}
}
StringState::CharOrEscapeOrEnd {
string_range,
quote_range,
} => match chars.next() {
Some(CharWithContext(r, JsonChar('\\'))) => StringState::Escape {
string_range: string_range.start..r.end,
quote_range,
slash_range: r,
},
Some(CharWithContext(r, JsonChar('"'))) => StringState::End(TokenWithContext::new(
Token::String,
string_range.start..r.end,
)),
Some(CharWithContext(r, c)) if c.is_control() => {
return Err(Error::new(
ErrorKind::UnexpectedControlCharacterInString(c),
r,
input,
));
}
Some(CharWithContext(r, _)) => StringState::CharOrEscapeOrEnd {
string_range: string_range.start..r.end,
quote_range,
},
None => {
return Err(Error::from_unterminated(
ErrorKind::ExpectedQuote {
open_range: quote_range,
string_range,
},
input,
));
}
},
StringState::Escape {
string_range,
quote_range,
slash_range,
} => match chars.next() {
Some(CharWithContext(r, c)) if c.can_be_escaped_directly() => {
StringState::CharOrEscapeOrEnd {
string_range: string_range.start..r.end,
quote_range,
}
}
Some(CharWithContext(r, JsonChar('u'))) => StringState::UEscape {
string_range,
quote_range,
u_range: r,
slash_range,
digits_seen: 0,
},
maybe_c => {
return Err(Error::from_maybe_json_char_with_context(
|c| ErrorKind::ExpectedEscape {
maybe_c: c,
slash_range,
string_range,
quote_range,
},
maybe_c,
input,
));
}
},
StringState::UEscape {
string_range,
quote_range,
u_range,
slash_range,
digits_seen,
} => match chars.next() {
Some(CharWithContext(r, c)) if c.is_hexdigit() => {
let string_range = string_range.start..r.end;
let next_digits = digits_seen + 1;
if next_digits == 4 {
StringState::CharOrEscapeOrEnd {
string_range,
quote_range,
}
} else {
StringState::UEscape {
string_range,
quote_range,
u_range,
slash_range,
digits_seen: next_digits,
}
}
}
maybe_c => {
return Err(Error::from_maybe_json_char_with_context(
|c| ErrorKind::ExpectedHexDigit {
quote_range,
slash_range,
u_range,
maybe_c: c,
digit_idx: digits_seen + 1,
},
maybe_c,
input,
));
}
},
StringState::End(_) => self,
};
Ok(res)
}
}
pub fn parse_string(input: &str, pos: usize) -> Result<TokenWithContext> {
let mut chars = input[pos..]
.char_indices()
.map(|(i, c)| (i + pos, c).into())
.peekable();
let mut state = StringState::Open;
loop {
state = state.process(&mut chars, input)?;
if let StringState::End(tok) = state {
break Ok(tok);
}
}
}