use std::fmt::Write;
use super::{Item, text};
use crate::value::Value;
pub(crate) fn render(value: &Value) -> String {
let mut out = String::new();
match block(Item::Value(value)) {
Some(lines) => {
for line in lines {
out.push_str(&line);
out.push('\n');
}
}
None => {
out.push_str(&scalar(Item::Value(value)));
out.push('\n');
}
}
out
}
fn block(item: Item<'_>) -> Option<Vec<String>> {
if let Some(entries) = item.entries() {
if entries.len() == 0 {
return None;
}
let mut lines = Vec::new();
let _ = entries.each(|key, item| {
let key = string(key);
match block(item) {
Some(nested) => {
lines.push(format!("{key}:"));
lines.extend(nested.into_iter().map(|l| format!(" {l}")));
}
None => lines.push(format!("{key}: {}", scalar(item))),
}
Ok::<(), ()>(())
});
return Some(lines);
}
let (Value::List(items) | Value::Set(items)) = item.value()? else {
return None;
};
if items.is_empty() {
return None;
}
let mut lines = Vec::new();
for item in items {
match block(Item::Value(item)) {
Some(nested) => {
for (i, line) in nested.into_iter().enumerate() {
lines.push(if i == 0 {
format!("- {line}")
} else {
format!(" {line}")
});
}
}
None => lines.push(format!("- {}", scalar(Item::Value(item)))),
}
}
Some(lines)
}
fn scalar(item: Item<'_>) -> String {
if item.entries().is_some() {
return "{}".to_owned();
}
let Some(value) = item.value() else {
return "{}".to_owned();
};
match value {
Value::None => "null".to_owned(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => float(*f),
Value::List(_) | Value::Set(_) => "[]".to_owned(),
_ => text(value).map_or_else(|| "null".to_owned(), |t| string(&t)),
}
}
fn float(f: f64) -> String {
let text = format!("{f:?}");
match text.split_once('e') {
Some((mantissa, exponent)) if !mantissa.contains('.') => {
format!("{mantissa}.0e{exponent}")
}
_ => text,
}
}
fn string(text: &str) -> String {
if is_plain(text) {
return text.to_owned();
}
let mut out = String::from("\"");
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\0"),
c if c.is_control() || matches!(c, '\u{2028}' | '\u{2029}' | '\u{feff}') => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
const RESERVED: [&str; 22] = [
"null", "Null", "NULL", "true", "True", "TRUE", "false", "False", "FALSE", "y", "Y", "yes",
"Yes", "YES", "n", "N", "no", "No", "NO", "on", "On", "ON",
];
fn is_plain(text: &str) -> bool {
let Some(first) = text.chars().next() else {
return false;
};
if !(first.is_alphabetic() || first == '_' || first == '/') {
return false;
}
if text.ends_with(' ') || RESERVED.contains(&text) {
return false;
}
let lower = text.to_ascii_lowercase();
if lower == "off" || lower == "on" {
return false;
}
text.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | ' '))
}