use crate::Result;
use crate::Value;
use crate::patterns::has_ctrl_chars;
use crate::patterns::has_newline;
use crate::patterns::needs_quotes;
use std::fmt::Write;
pub fn emit(v: &Value<'_>) -> Result<String> {
let mut out = String::new();
emit_node(v, 0, &mut out)?;
if out.starts_with('\n') {
out.remove(0);
}
if !out.ends_with('\n') {
out.push('\n');
}
Ok(out)
}
fn emit_node(v: &Value<'_>, indent: usize, out: &mut String) -> Result<()> {
match v {
Value::Null => emit_null(out),
Value::Bool(b) => emit_bool(*b, out),
Value::Int(n) => emit_int(n, out),
Value::UInt(n) => emit_uint(n, out),
Value::Float(f) => emit_float(f, out),
Value::String(s) => emit_scalar(s, indent, out),
Value::Tagged(tag, v) => {
out.push_str(tag);
if is_inline(v) {
out.push(' ');
}
emit_node(v, indent, out)?;
}
Value::Seq(items) => emit_block_seq(items, indent, out)?,
Value::Map(pairs) => emit_block_map(pairs, indent, out)?,
}
Ok(())
}
fn emit_null(out: &mut String) {
out.push_str("null")
}
fn emit_bool(b: bool, out: &mut String) {
out.push_str(if b { "true" } else { "false" });
}
fn emit_int(n: &i64, out: &mut String) {
write!(out, "{n}").unwrap();
}
fn emit_uint(n: &u64, out: &mut String) {
write!(out, "{n}").unwrap();
}
fn emit_float(f: &f64, out: &mut String) {
if f.is_nan() {
out.push_str(".nan");
return;
}
if f.is_infinite() {
out.push_str(if f.is_sign_positive() {
".inf"
} else {
"-.inf"
});
return;
}
let s = format!("{}", f);
out.push_str(&s);
if !s.contains('.') && !s.contains('e') {
out.push_str(".0");
}
}
fn emit_scalar(s: &str, indent: usize, out: &mut String) {
if !s.is_empty() && has_newline(s) && !has_ctrl_chars(s) {
emit_block_scalar(s, indent, out);
} else if needs_quotes(s) {
emit_quoted_scalar(s, out);
} else {
out.push_str(s);
}
}
fn emit_block_scalar(s: &str, indent: usize, out: &mut String) {
out.push('|');
if !s.ends_with('\n') {
out.push('-');
}
let child = " ".repeat(indent + 2);
for line in s.split('\n') {
out.push('\n');
if !line.is_empty() {
out.push_str(&child);
out.push_str(line);
}
}
}
fn emit_quoted_scalar(s: &str, out: &mut String) {
if !has_ctrl_chars(s) && !s.contains('\'') {
out.push('\'');
out.push_str(s);
out.push('\'');
} else {
emit_double_quoted(s, out);
}
}
fn emit_double_quoted(s: &str, out: &mut String) {
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"),
'\0' => out.push_str("\\0"),
c if (c as u32) < 0x20 || c as u32 == 0x7f => {
write!(out, "\\x{:02x}", c as u32).unwrap()
}
c => out.push(c),
}
}
out.push('"');
}
fn emit_block_seq(items: &[Value<'_>], indent: usize, out: &mut String) -> Result<()> {
if items.is_empty() {
out.push_str("[]");
return Ok(());
}
for item in items {
out.push('\n');
push_indent(out, indent);
push_block_prefix(out);
match item {
Value::Map(pairs) if !pairs.is_empty() => {
emit_kv(&pairs[0], indent + 2, out)?;
for kv in &pairs[1..] {
out.push('\n');
push_indent(out, indent + 2);
emit_kv(kv, indent + 2, out)?;
}
}
_ => emit_node(item, indent + 2, out)?,
}
}
Ok(())
}
fn emit_block_map(pairs: &[(Value<'_>, Value<'_>)], indent: usize, out: &mut String) -> Result<()> {
if pairs.is_empty() {
out.push('{');
out.push('}');
return Ok(());
}
for kv in pairs {
out.push('\n');
push_indent(out, indent);
emit_kv(kv, indent, out)?;
}
Ok(())
}
fn emit_kv(kv: &(Value<'_>, Value<'_>), indent: usize, out: &mut String) -> Result<()> {
let (k, v) = kv;
emit_node(k, indent, out)?;
out.push(':');
if is_inline(v) {
out.push(' ');
}
emit_node(v, indent + 2, out)?;
Ok(())
}
fn push_indent(out: &mut String, indent: usize) {
out.push_str(&" ".repeat(indent))
}
fn push_block_prefix(out: &mut String) {
out.push('-');
out.push(' ');
}
fn is_inline(v: &Value<'_>) -> bool {
match v {
Value::Seq(items) if !items.is_empty() => false,
Value::Map(items) if !items.is_empty() => false,
Value::Tagged(_, value) => is_inline(value),
_ => true,
}
}