use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::map::Table;
use crate::value::Value;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Style {
Plain,
Pretty,
}
pub fn to_string(table: &Table) -> String {
let mut out = String::new();
write_table(&mut out, table, &mut Vec::new(), Style::Plain);
out
}
pub fn to_string_pretty(table: &Table) -> String {
let mut out = String::new();
write_table(&mut out, table, &mut Vec::new(), Style::Pretty);
out
}
fn is_section(value: &Value) -> bool {
match value {
Value::Table(_) => true,
Value::Array(items) => !items.is_empty() && items.iter().all(Value::is_table),
_ => false,
}
}
fn needs_header(table: &Table) -> bool {
table.is_empty() || table.values().any(|value| !is_section(value))
}
fn write_table(out: &mut String, table: &Table, path: &mut Vec<String>, style: Style) {
for (key, value) in table {
if !is_section(value) {
out.push_str(&key_to_string(key));
out.push_str(" = ");
write_styled(out, value, style, 0);
out.push('\n');
}
}
for (key, value) in table {
if !is_section(value) {
continue;
}
path.push(key_to_string(key));
match value {
Value::Table(child) => {
if needs_header(child) {
write_header(out, path, false);
}
write_table(out, child, path, style);
}
Value::Array(items) => {
for item in items {
write_header(out, path, true);
if let Value::Table(child) = item {
write_table(out, child, path, style);
}
}
}
_ => unreachable!("only tables and arrays of tables are sections"),
}
path.pop();
}
}
fn write_header(out: &mut String, path: &[String], array: bool) {
if !out.is_empty() {
out.push('\n');
}
out.push_str(if array { "[[" } else { "[" });
out.push_str(&path.join("."));
out.push_str(if array { "]]\n" } else { "]\n" });
}
pub(crate) fn write_value(out: &mut String, value: &Value) {
write_styled(out, value, Style::Plain, 0);
}
fn write_styled(out: &mut String, value: &Value, style: Style, depth: usize) {
match value {
Value::String(s) if style == Style::Pretty && s.contains('\n') => {
write_multiline_string(out, s);
}
Value::String(s) => write_string(out, s),
Value::Integer(i) => out.push_str(&i.to_string()),
Value::Float(f) => write_float(out, *f),
Value::Boolean(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Datetime(dt) => out.push_str(&dt.to_string()),
Value::Array(items) if style == Style::Pretty && !items.is_empty() => {
out.push_str("[\n");
for item in items {
for _ in 0..=depth {
out.push_str(" ");
}
write_styled(out, item, style, depth + 1);
out.push_str(",\n");
}
for _ in 0..depth {
out.push_str(" ");
}
out.push(']');
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_styled(out, item, style, depth);
}
out.push(']');
}
Value::Table(table) => {
if table.is_empty() {
out.push_str("{}");
return;
}
out.push_str("{ ");
for (i, (key, value)) in table.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&key_to_string(key));
out.push_str(" = ");
write_styled(out, value, Style::Plain, 0);
}
out.push_str(" }");
}
}
}
fn write_float(out: &mut String, value: f64) {
if value.is_nan() {
out.push_str(if value.is_sign_negative() {
"-nan"
} else {
"nan"
});
} else if value.is_infinite() {
out.push_str(if value < 0.0 { "-inf" } else { "inf" });
} else {
out.push_str(&format!("{value:?}"));
}
}
fn write_string(out: &mut String, value: &str) {
out.push('"');
for c in value.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
c if c < ' ' || c == '\u{7f}' => out.push_str(&format!("\\u{:04X}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
fn write_multiline_string(out: &mut String, value: &str) {
out.push_str("\"\"\"\n");
for c in value.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' | '\t' => out.push(c),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
c if c < ' ' || c == '\u{7f}' => out.push_str(&format!("\\u{:04X}", c as u32)),
c => out.push(c),
}
}
out.push_str("\"\"\"");
}
pub(crate) fn key_to_string(key: &str) -> String {
if !key.is_empty()
&& key
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'-')
{
key.to_owned()
} else {
let mut out = String::with_capacity(key.len() + 2);
write_string(&mut out, key);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_are_quoted_only_when_needed() {
assert_eq!(key_to_string("plain-key_1"), "plain-key_1");
assert_eq!(key_to_string(""), "\"\"");
assert_eq!(key_to_string("with space"), "\"with space\"");
assert_eq!(key_to_string("a.b"), "\"a.b\"");
assert_eq!(key_to_string("clé"), "\"clé\"");
}
#[test]
fn floats_stay_floats() {
let mut out = String::new();
write_float(&mut out, 1.0);
assert_eq!(out, "1.0");
for (value, expected) in [
(f64::INFINITY, "inf"),
(f64::NEG_INFINITY, "-inf"),
(f64::NAN, "nan"),
] {
let mut out = String::new();
write_float(&mut out, value);
assert_eq!(out, expected);
}
}
#[test]
fn strings_escape_control_characters() {
let mut out = String::new();
write_string(&mut out, "a\tb\nc\u{1}\u{7f}\"\\");
assert_eq!(out, "\"a\\tb\\nc\\u0001\\u007F\\\"\\\\\"");
}
}