AkurAI Build
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)
    }
}