//! Lex TOML tokens //! //! To get started, see [`Source::lex`][crate::Source::lex]
#[cfg(test)] #[cfg(feature = "std")] mod test; mod token;
#[cfg(feature = "alloc")] use alloc::vec::Vec;
use winnow::stream::AsBStr as _; use winnow::stream::ContainsToken as _; use winnow::stream::FindSlice as _; use winnow::stream::Location; use winnow::stream::Stream as _;
usecrate::Span;
pubuse token::Token; pubuse token::TokenKind;
/// Lex TOML [tokens][Token] /// /// To get started, see [`Source::lex`][crate::Source::lex] pubstruct Lexer<'i> {
stream: Stream<'i>,
eof: bool,
}
impl<'i> Lexer<'i> { pub(crate) fn new(input: &'i str) -> Self { letmut stream = Stream::new(input); if input.as_bytes().starts_with(BOM) { let offset = BOM.len(); #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
}
Lexer { stream, eof: false }
}
#[cfg(feature = "alloc")] pubfn into_vec(self) -> Vec<Token> { #![allow(unused_qualifications)] // due to MSRV of 1.66 let capacity = core::cmp::min( self.stream.len(),
usize::MAX / core::mem::size_of::<Token>(),
); letmut vec = Vec::with_capacity(capacity);
vec.extend(self);
vec
}
}
/// Process an ASCII character token /// /// # Safety /// /// - `stream` must be UTF-8 /// - `stream` must be non-empty /// - `stream[0]` must be ASCII fn lex_ascii_char(stream: &mut Stream<'_>, kind: TokenKind) -> Token {
debug_assert!(!stream.is_empty()); let start = stream.current_token_start();
let offset = 1; // an ascii character #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(kind, span)
}
/// Process Whitespace /// /// ```bnf /// ;; Whitespace /// /// ws = *wschar /// wschar = %x20 ; Space /// wschar =/ %x09 ; Horizontal tab /// ``` /// /// # Safety /// /// - `stream` must be UTF-8 /// - `stream` must be non-empty fn lex_whitespace(stream: &mut Stream<'_>) -> Token {
debug_assert!(!stream.is_empty()); let start = stream.current_token_start();
let offset = stream
.as_bstr()
.offset_for(|b| !WSCHAR.contains_token(b))
.unwrap_or(stream.eof_offset()); #[cfg(feature = "unsafe")] // SAFETY: WSCHAR ensures `offset` will be at UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(TokenKind::Whitespace, span)
}
let offset = 1; // APOSTROPHE #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let offset = match stream.as_bstr().find_slice((APOSTROPHE, b'\n')) {
Some(span) => { if stream.as_bstr()[span.start] == APOSTROPHE {
span.end
} else {
span.start
}
}
None => stream.eof_offset(),
}; #[cfg(feature = "unsafe")] // SAFETY: `APOSTROPHE`/newline ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(TokenKind::LiteralString, span)
}
let offset = ML_LITERAL_STRING_DELIM.len(); #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let offset = match stream.as_bstr().find_slice(ML_LITERAL_STRING_DELIM) {
Some(span) => span.end,
None => stream.eof_offset(),
}; #[cfg(feature = "unsafe")] // SAFETY: `ML_LITERAL_STRING_DELIM` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
if stream.as_bstr().peek_token() == Some(APOSTROPHE) { let offset = 1; #[cfg(feature = "unsafe")] // SAFETY: `APOSTROPHE` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
if stream.as_bstr().peek_token() == Some(APOSTROPHE) { let offset = 1; #[cfg(feature = "unsafe")] // SAFETY: `APOSTROPHE` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
}
}
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(TokenKind::MlLiteralString, span)
}
let offset = 1; // QUOTATION_MARK #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
loop { // newline is present for error recovery match stream.as_bstr().find_slice((QUOTATION_MARK, ESCAPE, b'\n')) {
Some(span) => { let found = stream.as_bstr()[span.start]; if found == QUOTATION_MARK { let offset = span.end; #[cfg(feature = "unsafe")] // SAFETY: `QUOTATION_MARK` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset); break;
} elseif found == ESCAPE { let offset = span.end; #[cfg(feature = "unsafe")] // SAFETY: `ESCAPE` / newline ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let offset = ML_BASIC_STRING_DELIM.len(); #[cfg(feature = "unsafe")] // SAFETY: only called when next character is ASCII unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
loop { // newline is present for error recovery match stream.as_bstr().find_slice((ML_BASIC_STRING_DELIM, "\\")) {
Some(span) => { let found = stream.as_bstr()[span.start]; if found == QUOTATION_MARK { let offset = span.end; #[cfg(feature = "unsafe")] // SAFETY: `QUOTATION_MARK` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset); break;
} elseif found == ESCAPE { let offset = span.end; #[cfg(feature = "unsafe")] // SAFETY: `ESCAPE` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let peek = stream.as_bstr().peek_token(); match peek {
Some(ESCAPE) | Some(QUOTATION_MARK) => { let offset = 1; // ESCAPE / QUOTATION_MARK #[cfg(feature = "unsafe")] // SAFETY: `QUOTATION_MARK`/`ESCAPE` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
}
_ => {}
} continue;
} else {
unreachable!("found `{found}`");
}
}
None => {
stream.finish(); break;
}
}
} if stream.as_bstr().peek_token() == Some(QUOTATION_MARK) { let offset = 1; #[cfg(feature = "unsafe")] // SAFETY: `QUOTATION_MARK` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset); if stream.as_bstr().peek_token() == Some(QUOTATION_MARK) { let offset = 1; #[cfg(feature = "unsafe")] // SAFETY: `QUOTATION_MARK` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
}
}
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(TokenKind::MlBasicString, span)
}
/// Process Atom /// /// This is everything else /// /// # Safety /// /// - `stream` must be UTF-8 /// - `stream` must be non-empty fn lex_atom(stream: &mut Stream<'_>) -> Token { let start = stream.current_token_start();
// Intentionally leaves off quotes in case the opening quote was missing const TOKEN_START: &[u8] = b".=,[]{} \t#\r\n"; let offset = stream
.as_bstr()
.offset_for(|b| TOKEN_START.contains_token(b))
.unwrap_or_else(|| stream.eof_offset()); #[cfg(feature = "unsafe")] // SAFETY: `TOKEN_START` ensure `offset` is along UTF-8 boundary unsafe {
stream.next_slice_unchecked(offset)
}; #[cfg(not(feature = "unsafe"))]
stream.next_slice(offset);
let end = stream.previous_token_end(); let span = Span::new_unchecked(start, end);
Token::new(TokenKind::Atom, span)
}