use crate::cst::document::{
can_use_block_literal, format_block_literal, format_double_quoted, format_number,
format_single_quoted, is_plain_safe,
};
use crate::error::{Error, Result};
use crate::prelude::*;
use crate::value::Value;
use crate::{FlowStyle, ScalarStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmitCtx {
quote: ScalarStyle,
flow: FlowStyle,
indent_unit: usize,
column: usize,
}
impl EmitCtx {
#[must_use]
pub fn new(quote: ScalarStyle, flow: FlowStyle, indent_unit: usize, column: usize) -> Self {
Self {
quote,
flow,
indent_unit,
column,
}
}
#[must_use]
pub fn quote_style(&self) -> ScalarStyle {
self.quote
}
#[must_use]
pub fn flow_style(&self) -> FlowStyle {
self.flow
}
#[must_use]
pub fn indent_unit(&self) -> usize {
self.indent_unit
}
#[must_use]
pub fn column(&self) -> usize {
self.column
}
}
pub trait Emit {
fn emit(&self, ctx: &EmitCtx) -> Result<String>;
fn expected_value(&self) -> Result<Value>;
}
impl Emit for str {
fn emit(&self, ctx: &EmitCtx) -> Result<String> {
Ok(emit_string(self, ctx))
}
fn expected_value(&self) -> Result<Value> {
Ok(Value::String(self.to_owned()))
}
}
impl Emit for String {
fn emit(&self, ctx: &EmitCtx) -> Result<String> {
Ok(emit_string(self, ctx))
}
fn expected_value(&self) -> Result<Value> {
Ok(Value::String(self.clone()))
}
}
impl Emit for bool {
fn emit(&self, _ctx: &EmitCtx) -> Result<String> {
Ok(if *self {
"true".to_owned()
} else {
"false".to_owned()
})
}
fn expected_value(&self) -> Result<Value> {
Ok(Value::Bool(*self))
}
}
macro_rules! impl_emit_via_value {
($($t:ty),* $(,)?) => {
$(
impl Emit for $t {
fn emit(&self, ctx: &EmitCtx) -> Result<String> {
Value::from(*self).emit(ctx)
}
fn expected_value(&self) -> Result<Value> {
Ok(Value::from(*self))
}
}
)*
};
}
impl_emit_via_value!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
impl Emit for Value {
fn emit(&self, ctx: &EmitCtx) -> Result<String> {
match self {
Value::Null => Ok("null".to_owned()),
Value::Bool(b) => b.emit(ctx),
Value::Number(n) => Ok(emit_number(n)),
Value::String(s) => Ok(emit_string(s, ctx)),
Value::Sequence(_) | Value::Mapping(_) => emit_collection(self, ctx),
Value::Tagged(_) => Err(Error::Parse(
"emit: tagged values are not auto-formatted yet — the scalar emitter would \
drop the tag; splice the `!tag value` spelling with `set` / `insert_entry` \
instead"
.into(),
)),
}
}
fn expected_value(&self) -> Result<Value> {
if matches!(self, Value::Tagged(_)) {
return Err(Error::Parse(
"emit: tagged values are not auto-formatted yet — the scalar emitter would \
drop the tag; splice the `!tag value` spelling with `set` / `insert_entry` \
instead"
.into(),
));
}
Ok(self.clone())
}
}
impl<T: Emit + ?Sized> Emit for &T {
fn emit(&self, ctx: &EmitCtx) -> Result<String> {
(**self).emit(ctx)
}
fn expected_value(&self) -> Result<Value> {
(**self).expected_value()
}
}
fn emit_number(n: &crate::value::Number) -> String {
match crate::to_string_value_with_config(&Value::Number(*n), &crate::SerializerConfig::new()) {
Ok(s) => s.trim_end_matches('\n').to_owned(),
Err(_) => format_number(n),
}
}
fn emit_string(s: &str, ctx: &EmitCtx) -> String {
if s.contains('\n') && can_use_block_literal(s) {
return format_block_literal(s, 0);
}
quote_for_site(s, ctx.quote)
}
fn quote_for_site(s: &str, style: ScalarStyle) -> String {
let single_representable = !s.bytes().any(|b| b < 0x20 || b == 0x7F);
match style {
ScalarStyle::SingleQuoted if single_representable => format_single_quoted(s),
ScalarStyle::DoubleQuoted | ScalarStyle::SingleQuoted => format_double_quoted(s),
_ if is_plain_safe(s) => s.to_owned(),
_ => format_double_quoted(s),
}
}
fn emit_collection(value: &Value, ctx: &EmitCtx) -> Result<String> {
let cfg = crate::SerializerConfig::new()
.indent(ctx.indent_unit)
.flow_style(ctx.flow);
let emitted = crate::to_string_value_with_config(value, &cfg)?;
Ok(emitted.trim_end_matches('\n').to_owned())
}
pub(super) fn emit_key(key: &str, ctx: &EmitCtx) -> String {
if is_plain_safe(key) {
return key.to_owned();
}
let single_representable = !key.bytes().any(|b| b < 0x20 || b == 0x7F);
if ctx.quote == ScalarStyle::SingleQuoted && single_representable {
format_single_quoted(key)
} else {
format_double_quoted(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plain_ctx() -> EmitCtx {
EmitCtx::new(ScalarStyle::Plain, FlowStyle::Block, 2, 0)
}
#[test]
fn plain_safe_string_stays_plain() {
assert_eq!(emit_string("noyalib", &plain_ctx()), "noyalib");
}
#[test]
fn type_changing_spellings_are_quoted() {
let ctx = plain_ctx();
for s in ["true", "null", "8080", "- x", "a: b", "#lead", "~"] {
let out = emit_string(s, &ctx);
assert!(
out.starts_with('"'),
"{s:?} must be quoted, emitted {out:?}"
);
}
}
#[test]
fn dominant_single_quote_is_honoured() {
let ctx = EmitCtx::new(ScalarStyle::SingleQuoted, FlowStyle::Block, 2, 0);
assert_eq!(emit_string("plain", &ctx), "'plain'");
}
#[test]
fn control_characters_defeat_single_quoting() {
let ctx = EmitCtx::new(ScalarStyle::SingleQuoted, FlowStyle::Block, 2, 0);
assert_eq!(emit_string("a\tb", &ctx), "\"a\\tb\"");
}
#[test]
fn multiline_string_becomes_a_block_literal() {
let out = emit_string("one\ntwo\n", &plain_ctx());
assert!(
out.starts_with("|\n"),
"expected a block literal, got {out:?}"
);
}
#[test]
fn key_spelling_matches_value_rules() {
let ctx = plain_ctx();
assert_eq!(emit_key("name", &ctx), "name");
assert_eq!(emit_key("a: b", &ctx), "\"a: b\"");
assert_eq!(emit_key("true", &ctx), "\"true\"");
}
#[test]
fn emit_and_oracle_agree_for_primitives() {
let ctx = plain_ctx();
assert_eq!(true.emit(&ctx).unwrap(), "true");
assert_eq!(true.expected_value().unwrap(), Value::Bool(true));
assert_eq!(7_i64.emit(&ctx).unwrap(), "7");
assert_eq!(7_i64.expected_value().unwrap(), Value::from(7_i64));
}
#[test]
fn tagged_values_are_refused_on_both_halves() {
let ctx = plain_ctx();
let tagged = crate::from_str::<Value>("!custom 1").unwrap();
assert!(tagged.emit(&ctx).is_err());
assert!(tagged.expected_value().is_err());
}
}