Menu
Public repository / master
AkurAI-Framework / crates/toml/src/strings.rs
Latest change d7fe19be2f456eff31bbb2b789e79c235c51dddb - akurai-toml: literal and multi-line strings, full escapes, inline tables by Ólafur Búi Ólafsson
//! The four TOML string forms: basic (`"..."`), literal (`'...'`), and their
//! multi-line variants (`"""..."""`, `'''...'''`). Literal forms take the text
//! verbatim, which is what content files with HTML fragments need; basic forms
//! apply escapes. A newline right after a multi-line opener is trimmed.
use crate::error::TomlError;
use crate::lexer::Cursor;
/// Read any string form; the cursor sits on the opening quote.
pub(crate) fn read_string(cur: &mut Cursor) -> Result<String, TomlError> {
let quote = cur.peek().unwrap_or('"');
let multiline = cur.peek_at(1) == Some(quote) && cur.peek_at(2) == Some(quote);
match (quote, multiline) {
('\'', false) => read_literal(cur),
('\'', true) => read_multiline(cur, '\''),
(_, false) => read_basic(cur),
(_, true) => read_multiline(cur, '"'),
}
}
fn read_literal(cur: &mut Cursor) -> Result<String, TomlError> {
let open_line = cur.line();
cur.bump();
let mut s = String::new();
loop {
match cur.bump() {
None => return Err(TomlError::new("unterminated string", open_line)),
Some('\'') => return Ok(s),
Some('\n') => return Err(TomlError::new("newline inside literal string", open_line)),
Some(c) => s.push(checked(c, cur)?),
}
}
}
fn read_basic(cur: &mut Cursor) -> Result<String, TomlError> {
let open_line = cur.line();
cur.bump();
let mut s = String::new();
loop {
match cur.bump() {
None => return Err(TomlError::new("unterminated string", open_line)),
Some('"') => return Ok(s),
Some('\\') => read_escape(cur, &mut s, open_line)?,
Some('\n') => return Err(TomlError::new("newline inside basic string", open_line)),
Some(c) => s.push(checked(c, cur)?),
}
}
}
/// `'''...'''` or `"""..."""`. Up to two quote characters directly before the
/// closing delimiter belong to the content (TOML's rule).
fn read_multiline(cur: &mut Cursor, quote: char) -> Result<String, TomlError> {
let open_line = cur.line();
for _ in 0..3 {
cur.bump();
}
if cur.peek() == Some('\n') {
cur.bump();
} else if cur.peek() == Some('\r') && cur.peek_at(1) == Some('\n') {
cur.bump();
cur.bump();
}
let mut s = String::new();
loop {
if is_closing(cur, quote) {
for _ in 0..3 {
cur.bump();
}
return Ok(s);
}
match cur.bump() {
None => return Err(TomlError::new("unterminated multi-line string", open_line)),
Some('\\') if quote == '"' => read_multiline_escape(cur, &mut s, open_line)?,
Some(c) if c == '\n' || c == '\t' || c == '\r' => s.push(c),
Some(c) => s.push(checked(c, cur)?),
}
}
}
/// True when the next three characters close the string and no fourth quote
/// follows (so `''''` closes one character later, keeping a quote).
fn is_closing(cur: &Cursor, quote: char) -> bool {
(0..3).all(|i| cur.peek_at(i) == Some(quote)) && cur.peek_at(3) != Some(quote)
}
/// In `"""` strings a backslash at the end of a line swallows the newline and
/// all whitespace that follows; otherwise it starts a normal escape.
fn read_multiline_escape(
cur: &mut Cursor,
s: &mut String,
open_line: usize,
) -> Result<(), TomlError> {
let mut ahead = 0;
while matches!(cur.peek_at(ahead), Some(' ' | '\t' | '\r')) {
ahead += 1;
}
if cur.peek_at(ahead) != Some('\n') {
return read_escape(cur, s, open_line);
}
while matches!(cur.peek(), Some(' ' | '\t' | '\r' | '\n')) {
cur.bump();
}
Ok(())
}
fn read_escape(cur: &mut Cursor, s: &mut String, open_line: usize) -> Result<(), TomlError> {
let c = match cur.bump() {
Some('"') => '"',
Some('\\') => '\\',
Some('n') => '\n',
Some('t') => '\t',
Some('r') => '\r',
Some('b') => '\u{8}',
Some('f') => '\u{c}',
Some('u') => read_unicode(cur, 4)?,
Some('U') => read_unicode(cur, 8)?,
Some(other) => {
return Err(TomlError::new(
format!("unsupported escape '\\{other}'"),
cur.line(),
))
}
None => return Err(TomlError::new("unterminated string", open_line)),
};
s.push(c);
Ok(())
}
fn read_unicode(cur: &mut Cursor, digits: usize) -> Result<char, TomlError> {
let mut hex = String::with_capacity(digits);
for _ in 0..digits {
match cur.bump() {
Some(c) if c.is_ascii_hexdigit() => hex.push(c),
_ => return Err(TomlError::new("invalid unicode escape", cur.line())),
}
}
u32::from_str_radix(&hex, 16)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| TomlError::new(format!("invalid unicode scalar '{hex}'"), cur.line()))
}
fn checked(c: char, cur: &Cursor) -> Result<char, TomlError> {
if (c as u32) < 0x20 || c == '\u{7f}' {
Err(TomlError::new("control character in string", cur.line()))
} else {
Ok(c)
}
}