use-toml 0.1.0

Lightweight TOML table and key-value helpers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

/// A discovered TOML table header.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TomlTable {
    pub name: String,
    pub line: usize,
}

/// A discovered TOML key-value pair.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TomlKeyValue {
    pub key: String,
    pub value: String,
    pub line: usize,
}

/// Returns `true` when the input contains TOML-looking table or key-value lines.
pub fn looks_like_toml(input: &str) -> bool {
    input.lines().any(|line| {
        let trimmed = line.trim();
        !trimmed.is_empty()
            && !trimmed.starts_with('#')
            && (is_toml_table(trimmed)
                || is_toml_array_table(trimmed)
                || split_toml_key_value(trimmed).is_some())
    })
}

/// Returns `true` when a line is a TOML table header.
pub fn is_toml_table(line: &str) -> bool {
    let trimmed = line.trim();
    trimmed.len() >= 3
        && trimmed.starts_with('[')
        && trimmed.ends_with(']')
        && !trimmed.starts_with("[[")
        && !trimmed.ends_with("]]")
        && !trimmed[1..trimmed.len() - 1].trim().is_empty()
}

/// Returns `true` when a line is a TOML array-table header.
pub fn is_toml_array_table(line: &str) -> bool {
    let trimmed = line.trim();
    trimmed.len() >= 5
        && trimmed.starts_with("[[")
        && trimmed.ends_with("]]")
        && !trimmed[2..trimmed.len() - 2].trim().is_empty()
}

/// Extracts TOML table headers from the input.
pub fn extract_toml_tables(input: &str) -> Vec<TomlTable> {
    let mut tables = Vec::new();

    for (line_index, line) in input.lines().enumerate() {
        let trimmed = line.trim();
        if is_toml_table(trimmed) {
            tables.push(TomlTable {
                name: trimmed[1..trimmed.len() - 1].trim().to_string(),
                line: line_index + 1,
            });
        } else if is_toml_array_table(trimmed) {
            tables.push(TomlTable {
                name: trimmed[2..trimmed.len() - 2].trim().to_string(),
                line: line_index + 1,
            });
        }
    }

    tables
}

/// Extracts TOML key-value pairs from the input.
pub fn extract_toml_key_values(input: &str) -> Vec<TomlKeyValue> {
    let mut pairs = Vec::new();

    for (line_index, line) in input.lines().enumerate() {
        if let Some((key, value)) = split_toml_key_value(line) {
            pairs.push(TomlKeyValue {
                key,
                value,
                line: line_index + 1,
            });
        }
    }

    pairs
}

/// Splits a TOML key-value line on the first `=` outside quotes.
pub fn split_toml_key_value(line: &str) -> Option<(String, String)> {
    let content = strip_toml_comment(line).trim();
    if content.is_empty() || is_toml_table(content) || is_toml_array_table(content) {
        return None;
    }

    let mut in_single = false;
    let mut in_double = false;
    let mut escaped = false;

    for (index, ch) in content.char_indices() {
        if in_single {
            if ch == '\'' {
                in_single = false;
            }
            continue;
        }

        if in_double {
            if escaped {
                escaped = false;
            } else if ch == '\\' {
                escaped = true;
            } else if ch == '"' {
                in_double = false;
            }
            continue;
        }

        match ch {
            '\'' => in_single = true,
            '"' => in_double = true,
            '=' => {
                let key = content[..index].trim();
                let value = content[index + 1..].trim();
                if key.is_empty() || value.is_empty() {
                    return None;
                }
                return Some((key.to_string(), value.to_string()));
            }
            _ => {}
        }
    }

    None
}

/// Quotes a string as a TOML basic string.
pub fn quote_toml_string(input: &str) -> String {
    format!("\"{}\"", escape_toml_basic(input))
}

/// Unquotes a conservative TOML string literal.
pub fn unquote_toml_string(input: &str) -> Option<String> {
    let trimmed = input.trim();

    if trimmed.len() < 2 {
        return None;
    }

    if trimmed.starts_with('"') && trimmed.ends_with('"') {
        return unquote_toml_basic(trimmed);
    }

    if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
        let inner = &trimmed[1..trimmed.len() - 1];
        if inner.contains('\'') {
            return None;
        }
        return Some(inner.to_string());
    }

    None
}

fn strip_toml_comment(line: &str) -> &str {
    let mut in_single = false;
    let mut in_double = false;
    let mut escaped = false;

    for (index, ch) in line.char_indices() {
        if in_single {
            if ch == '\'' {
                in_single = false;
            }
            continue;
        }

        if in_double {
            if escaped {
                escaped = false;
            } else if ch == '\\' {
                escaped = true;
            } else if ch == '"' {
                in_double = false;
            }
            continue;
        }

        match ch {
            '\'' => in_single = true,
            '"' => in_double = true,
            '#' => return &line[..index],
            _ => {}
        }
    }

    line
}

fn escape_toml_basic(input: &str) -> String {
    let mut escaped = String::with_capacity(input.len());

    for ch in input.chars() {
        match ch {
            '"' => escaped.push_str("\\\""),
            '\\' => escaped.push_str("\\\\"),
            '\n' => escaped.push_str("\\n"),
            '\r' => escaped.push_str("\\r"),
            '\t' => escaped.push_str("\\t"),
            ch if ch.is_control() => escaped.push_str(&format!("\\u{:04X}", ch as u32)),
            _ => escaped.push(ch),
        }
    }

    escaped
}

fn unquote_toml_basic(input: &str) -> Option<String> {
    let inner = &input[1..input.len() - 1];
    let mut chars = inner.chars();
    let mut output = String::new();

    while let Some(ch) = chars.next() {
        if ch == '"' || ch.is_control() {
            return None;
        }

        if ch != '\\' {
            output.push(ch);
            continue;
        }

        match chars.next()? {
            '"' => output.push('"'),
            '\\' => output.push('\\'),
            'n' => output.push('\n'),
            'r' => output.push('\r'),
            't' => output.push('\t'),
            'u' => {
                let mut hex = String::with_capacity(4);
                for _ in 0..4 {
                    hex.push(chars.next()?);
                }
                let value = u32::from_str_radix(&hex, 16).ok()?;
                output.push(char::from_u32(value)?);
            }
            _ => return None,
        }
    }

    Some(output)
}