#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TomlTable {
pub name: String,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TomlKeyValue {
pub key: String,
pub value: String,
pub line: usize,
}
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())
})
}
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()
}
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()
}
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
}
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
}
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
}
pub fn quote_toml_string(input: &str) -> String {
format!("\"{}\"", escape_toml_basic(input))
}
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)
}