use super::{Mapping, Value};
use std::fmt::Write as _;
const INDENT_STEP: usize = 2;
const INDICATORS: &[char] = &[
'-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`',
' ',
];
pub fn emit(value: &Value) -> String {
let mut out = String::new();
match value {
Value::Mapping(m) if !m.is_empty() => emit_mapping(m, 0, &mut out),
Value::Sequence(s) if !s.is_empty() => emit_sequence(s, 0, &mut out),
scalar => {
out.push_str(&emit_scalar(scalar));
out.push('\n');
}
}
out
}
fn emit_mapping(map: &Mapping, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
for (k, v) in map.iter() {
let key = emit_scalar(k);
match v {
Value::Mapping(m) if !m.is_empty() => {
let _ = writeln!(out, "{pad}{key}:");
emit_mapping(m, indent + INDENT_STEP, out);
}
Value::Sequence(s) if !s.is_empty() => {
let _ = writeln!(out, "{pad}{key}:");
emit_sequence(s, indent + INDENT_STEP, out);
}
_ => {
let _ = writeln!(out, "{pad}{key}: {}", emit_scalar(v));
}
}
}
}
fn emit_sequence(seq: &[Value], indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
for item in seq {
match item {
Value::Mapping(m) if !m.is_empty() => {
let mut block = String::new();
emit_mapping(m, indent + INDENT_STEP, &mut block);
out.push_str(&bullet(&block, indent));
}
Value::Sequence(s) if !s.is_empty() => {
let mut block = String::new();
emit_sequence(s, indent + INDENT_STEP, &mut block);
out.push_str(&bullet(&block, indent));
}
_ => {
let _ = writeln!(out, "{pad}- {}", emit_scalar(item));
}
}
}
}
fn bullet(block: &str, indent: usize) -> String {
block
.strip_prefix(&" ".repeat(indent + INDENT_STEP))
.map_or_else(
|| block.to_string(),
|rest| format!("{}- {rest}", " ".repeat(indent)),
)
}
fn emit_scalar(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(true) => "true".to_string(),
Value::Bool(false) => "false".to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => format_float(*f),
Value::String(s) => emit_string(s),
Value::Sequence(s) if s.is_empty() => "[]".to_string(),
Value::Mapping(m) if m.is_empty() => "{}".to_string(),
Value::Sequence(_) | Value::Mapping(_) => "[]".to_string(),
}
}
fn format_float(f: f64) -> String {
if f.is_nan() {
return ".nan".to_string();
}
if f.is_infinite() {
return if f > 0.0 {
".inf".to_string()
} else {
"-.inf".to_string()
};
}
let s = format!("{f:?}");
if s.contains('.') {
s
} else if let Some(e) = s.find(['e', 'E']) {
format!("{}.0{}", &s[..e], &s[e..])
} else {
format!("{s}.0")
}
}
fn emit_string(s: &str) -> String {
if is_safe_plain(s) {
s.to_string()
} else {
double_quote(s)
}
}
fn is_safe_plain(s: &str) -> bool {
if s.is_empty() {
return false;
}
if super::Value::parse(s).map_or(true, |v| v != Value::String(s.to_string())) {
return false;
}
if s.starts_with(' ') || s.ends_with(' ') {
return false;
}
if resolves_as_datetime(s) {
return false;
}
let first = s.chars().next().unwrap();
if INDICATORS.contains(&first) {
return false;
}
let bytes: Vec<char> = s.chars().collect();
for (i, &c) in bytes.iter().enumerate() {
match c {
'\n' | '\t' | '\r' => return false,
':' if bytes.get(i + 1).is_none_or(|n| *n == ' ') => return false,
'#' if i > 0 && bytes[i - 1] == ' ' => return false,
_ => {}
}
}
true
}
fn resolves_as_datetime(s: &str) -> bool {
let b = s.as_bytes();
let mut i = 0;
if !(take_digits(b, &mut i, 4, 4)
&& take_byte(b, &mut i, b'-')
&& take_digits(b, &mut i, 1, 2)
&& take_byte(b, &mut i, b'-')
&& take_digits(b, &mut i, 1, 2))
{
return false;
}
match b.get(i) {
Some(b'T' | b't') => i += 1,
Some(b' ' | b'\t') => {
while matches!(b.get(i), Some(b' ' | b'\t')) {
i += 1;
}
}
_ => return false,
}
if !(take_digits(b, &mut i, 1, 2)
&& take_byte(b, &mut i, b':')
&& take_digits(b, &mut i, 2, 2)
&& take_byte(b, &mut i, b':')
&& take_digits(b, &mut i, 2, 2))
{
return false;
}
if take_byte(b, &mut i, b'.') {
take_digits(b, &mut i, 0, usize::MAX);
}
let before_blanks = i;
while matches!(b.get(i), Some(b' ' | b'\t')) {
i += 1;
}
if i == b.len() {
return before_blanks == i;
}
if !take_byte(b, &mut i, b'Z') {
if !(take_byte(b, &mut i, b'+') || take_byte(b, &mut i, b'-')) {
return false;
}
if !take_digits(b, &mut i, 1, 2) {
return false;
}
if take_byte(b, &mut i, b':') && !take_digits(b, &mut i, 2, 2) {
return false;
}
}
i == b.len()
}
fn take_digits(b: &[u8], i: &mut usize, min: usize, max: usize) -> bool {
let start = *i;
while *i - start < max && b.get(*i).is_some_and(u8::is_ascii_digit) {
*i += 1;
}
*i - start >= min
}
fn take_byte(b: &[u8], i: &mut usize, want: u8) -> bool {
let found = b.get(*i) == Some(&want);
if found {
*i += 1;
}
found
}
fn double_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
'\0' => out.push_str("\\0"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}