use std::borrow::Cow;
use std::fmt::{self, Write};
use deser_core::ext::{BigInt, Datetime, Decimal, ExtValue, Number, Timestamp};
use deser_core::hints::Layout;
use deser_core::ser::EventSink;
use deser_core::ser::SerializeRef;
use deser_core::{Atom, BytesFormat, Error, ErrorKind, Event, State};
use crate::quote::{
BlockScalar, MAX_SIMPLE_KEY_LEN, PushSmall, is_plain_safe, is_single_quote_safe, push_indent,
write_double_quoted, write_float, write_single_quoted, write_tag,
};
use crate::resolve::{Version, is_plain_str, writes_as_plain};
use crate::ser::{FlowPolicy, Indent, MultilineStyle, NullStyle, QuoteStyle, SerializerConfig};
use crate::style::{ScalarStyle, StyleHint};
use crate::tag::NodeTag;
const BINARY_LINE_LEN: usize = 76;
const DEFAULT_FOLD_WIDTH: usize = 80;
fn wrap_binary(encoded: &str) -> BlockScalar<'static> {
let mut lines = String::with_capacity(encoded.len() + encoded.len() / BINARY_LINE_LEN + 1);
for (idx, chunk) in encoded.as_bytes().chunks(BINARY_LINE_LEN).enumerate() {
if idx > 0 {
lines.push('\n');
}
lines.push_str(std::str::from_utf8(chunk).unwrap());
}
BlockScalar::from_lines(lines)
}
#[derive(Debug, Clone, Default)]
struct Hints {
tag: Option<String>,
layout: Layout,
style: Option<ScalarStyle>,
}
#[derive(Debug, Clone, Copy)]
enum Pos {
Root,
Inline(usize),
Value(usize),
Flow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Context {
Block,
Key,
Flow,
}
struct Frame {
is_map: bool,
flow: bool,
indent: usize,
first: bool,
value: bool,
explicit: bool,
}
#[derive(Clone)]
struct Pending {
is_map: bool,
hints: Hints,
pos: Pos,
}
struct Attempt {
pending: Pending,
out_len: usize,
depth: usize,
space: bool,
line_done: bool,
events: Vec<(Event<'static>, Hints)>,
width: usize,
}
enum Scalar<'a> {
Text(Cow<'a, str>),
Short(ShortText),
Block(BlockScalar<'a>),
Empty,
}
impl Scalar<'_> {
fn into_owned(self) -> Scalar<'static> {
match self {
Scalar::Text(text) => Scalar::Text(Cow::Owned(text.into_owned())),
Scalar::Short(text) => Scalar::Short(text),
Scalar::Empty => Scalar::Empty,
Scalar::Block(block) => Scalar::Block(block.into_owned()),
}
}
}
struct ShortText {
buf: [u8; 48],
len: usize,
}
impl ShortText {
fn new() -> ShortText {
ShortText {
buf: [0; 48],
len: 0,
}
}
fn as_str(&self) -> &str {
unsafe { std::str::from_utf8_unchecked(&self.buf[..self.len]) }
}
}
impl fmt::Write for ShortText {
fn write_str(&mut self, s: &str) -> fmt::Result {
let end = self.len + s.len();
let dst = self.buf.get_mut(self.len..end).ok_or(fmt::Error)?;
unsafe { crate::copy::copy_small(s.as_ptr(), dst.as_mut_ptr(), s.len()) };
self.len = end;
Ok(())
}
}
impl<'a> Scalar<'a> {
fn text(&self) -> Option<&str> {
match self {
Scalar::Text(text) => Some(text),
Scalar::Short(text) => Some(text.as_str()),
_ => None,
}
}
fn short_text(value: &str) -> Scalar<'a> {
let mut text = ShortText::new();
text.write_str(value).expect("short text");
Scalar::Short(text)
}
fn short(value: impl fmt::Display) -> Scalar<'a> {
let mut text = ShortText::new();
write!(text, "{}", value).expect("short text");
Scalar::Short(text)
}
}
pub(crate) struct Emitter {
config: SerializerConfig,
bytes: BytesFormat,
pub(crate) out: String,
stack: Vec<Frame>,
pending: Option<Pending>,
attempt: Option<Attempt>,
space: bool,
done: bool,
line_done: bool,
column: usize,
column_offset: usize,
base_column: usize,
spare_events: Vec<(Event<'static>, Hints)>,
pub(crate) limit: usize,
}
impl EventSink for Emitter {
fn event(
&mut self,
event: Event<'_>,
_value: SerializeRef<'_>,
state: &mut State,
) -> Result<(), Error> {
Emitter::event(self, event, state)
}
fn pause(&mut self) -> bool {
self.attempt.is_none() && self.out.len() >= self.limit
}
}
impl Emitter {
pub(crate) fn new(config: &SerializerConfig, out: String, bytes: BytesFormat) -> Emitter {
Emitter {
config: config.clone(),
bytes,
out,
stack: Vec::new(),
pending: None,
attempt: None,
space: false,
done: false,
line_done: false,
column: 0,
column_offset: 0,
base_column: 0,
spare_events: Vec::new(),
limit: usize::MAX,
}
}
pub(crate) fn finish(&mut self) -> Result<(), Error> {
if !self.done || !self.stack.is_empty() || self.pending.is_some() {
return Err(Error::new(ErrorKind::InvalidState, "incomplete document"));
}
if !self.line_done {
self.out.push('\n');
}
Ok(())
}
pub(crate) fn take_output(&mut self) -> String {
debug_assert!(self.attempt.is_none());
self.base_column = self.column();
self.column_offset = 0;
std::mem::take(&mut self.out)
}
pub(crate) fn event(&mut self, event: Event, state: &State) -> Result<(), Error> {
let hints = Hints {
tag: state.event::<NodeTag>().and_then(|x| x.0.clone()),
layout: Layout::of(state),
style: state.event::<StyleHint>().and_then(|x| x.0),
};
self.emit(event, hints)
}
fn emit(&mut self, event: Event, hints: Hints) -> Result<(), Error> {
if let Some(ref mut attempt) = self.attempt {
attempt.events.push((event.to_static(), hints.clone()));
if matches!(event, Event::MapStart(_) | Event::SeqStart(_)) {
return self.abort_attempt();
}
}
if let Some(pending) = self.pending.take() {
match event {
Event::MapEnd if pending.is_map => return self.write_empty(pending),
Event::SeqEnd if !pending.is_map => return self.write_empty(pending),
ref event => {
let is_start = matches!(event, Event::MapStart(_) | Event::SeqStart(_));
if !is_start && self.may_attempt(&pending) {
self.start_attempt(pending, event, &hints);
} else {
self.open(pending);
}
}
}
}
match event {
Event::Atom(atom) => self.atom(atom, hints)?,
Event::MapStart(_) => self.start(true, hints)?,
Event::SeqStart(_) => self.start(false, hints)?,
Event::MapEnd => self.end(true)?,
Event::SeqEnd => self.end(false)?,
}
if let Some(ref attempt) = self.attempt {
let width = attempt.width;
if self.stack.len() <= attempt.depth {
let mut events = self.attempt.take().unwrap().events;
events.clear();
self.spare_events = events;
} else if self.column() > width {
return self.abort_attempt();
}
}
Ok(())
}
fn may_attempt(&self, pending: &Pending) -> bool {
matches!(self.config.flow, FlowPolicy::LeafIfFits(_))
&& self.config.indent != Indent::None
&& pending.hints.layout == Layout::Auto
&& !matches!(pending.pos, Pos::Flow)
}
fn start_attempt(&mut self, pending: Pending, event: &Event, hints: &Hints) {
let FlowPolicy::LeafIfFits(width) = self.config.flow else {
unreachable!();
};
self.attempt = Some(Attempt {
pending: pending.clone(),
out_len: self.out.len(),
depth: self.stack.len(),
space: self.space,
line_done: self.line_done,
events: {
let mut events = std::mem::take(&mut self.spare_events);
events.push((event.to_static(), hints.clone()));
events
},
width,
});
self.open_flow(pending);
}
#[cold]
fn abort_attempt(&mut self) -> Result<(), Error> {
let attempt = self.attempt.take().unwrap();
self.out.truncate(attempt.out_len);
if self.column_offset > attempt.out_len {
match self.out.rfind('\n') {
Some(idx) => {
self.column = 0;
self.column_offset = idx + 1;
}
None => {
self.column = self.base_column;
self.column_offset = 0;
}
}
}
self.stack.truncate(attempt.depth);
self.space = attempt.space;
self.line_done = attempt.line_done;
self.pending = None;
self.open_block(attempt.pending);
let mut events = attempt.events;
for (event, hints) in events.drain(..) {
self.emit(event, hints)?;
}
if events.capacity() > self.spare_events.capacity() {
self.spare_events = events;
}
Ok(())
}
fn column(&mut self) -> usize {
let new = &self.out[self.column_offset..];
match new.rfind('\n') {
Some(idx) => self.column = new[idx + 1..].chars().count(),
None => self.column += new.chars().count(),
}
self.column_offset = self.out.len();
self.column
}
fn in_flow(&self) -> bool {
self.stack.last().is_some_and(|x| x.flow)
}
fn start(&mut self, is_map: bool, hints: Hints) -> Result<(), Error> {
let pos = match self.begin_node()? {
Some(pos) => pos,
None => self.begin_explicit_key(),
};
self.pending = Some(Pending { is_map, hints, pos });
Ok(())
}
fn end(&mut self, is_map: bool) -> Result<(), Error> {
match self.stack.pop() {
Some(frame) if frame.is_map == is_map && (!is_map || !frame.value) => {
if frame.flow {
self.out.push(if is_map { '}' } else { ']' });
}
}
_ => return Err(Error::new(ErrorKind::InvalidState, "unexpected end event")),
}
self.complete();
Ok(())
}
fn write_empty(&mut self, pending: Pending) -> Result<(), Error> {
self.write_inline_start(pending.hints.tag.as_deref());
self.out
.push_small(if pending.is_map { "{}" } else { "[]" });
self.complete();
Ok(())
}
fn open(&mut self, pending: Pending) {
if matches!(pending.pos, Pos::Flow)
|| pending.hints.layout == Layout::Compact
|| self.config.indent == Indent::None
{
self.open_flow(pending);
} else {
self.open_block(pending);
}
}
fn open_flow(&mut self, pending: Pending) {
self.write_inline_start(pending.hints.tag.as_deref());
self.out.push(if pending.is_map { '{' } else { '[' });
self.stack.push(Frame {
is_map: pending.is_map,
flow: true,
indent: 0,
first: true,
value: false,
explicit: false,
});
}
fn open_block(&mut self, pending: Pending) {
let (indent, first) = match pending.pos {
Pos::Root => {
if let Some(ref tag) = pending.hints.tag {
write_tag(&mut self.out, tag);
self.out.push('\n');
}
(0, true)
}
Pos::Inline(column) => match pending.hints.tag {
Some(ref tag) => {
self.flush_space();
write_tag(&mut self.out, tag);
(column, false)
}
None => (column, true),
},
Pos::Value(key_column) => {
if let Some(ref tag) = pending.hints.tag {
self.flush_space();
write_tag(&mut self.out, tag);
}
self.space = false;
let indent = if pending.is_map || self.config.indent_sequences {
key_column + self.config.indent_width()
} else {
key_column
};
(indent, false)
}
Pos::Flow => unreachable!("collections in flow collections are flow"),
};
self.stack.push(Frame {
is_map: pending.is_map,
flow: false,
indent,
first,
value: false,
explicit: false,
});
}
fn begin_node(&mut self) -> Result<Option<Pos>, Error> {
let Some(frame) = self.stack.last_mut() else {
if self.done {
return Err(Error::new(
ErrorKind::InvalidState,
"a document can only contain one value",
));
}
return Ok(Some(Pos::Root));
};
if frame.flow {
if frame.is_map && frame.value {
if frame.explicit {
self.out.push(':');
self.space = true;
}
return Ok(Some(Pos::Flow));
}
if !std::mem::replace(&mut frame.first, false) {
self.out.push(',');
self.space = true;
}
return Ok(if frame.is_map { None } else { Some(Pos::Flow) });
}
let indent = frame.indent;
if !frame.is_map {
self.begin_entry();
self.out.push('-');
self.space = true;
return Ok(Some(Pos::Inline(indent + 2)));
}
if !frame.value {
return Ok(None);
}
if frame.explicit {
self.newline(indent);
self.out.push(':');
self.space = true;
Ok(Some(Pos::Inline(indent + 2)))
} else {
Ok(Some(Pos::Value(indent)))
}
}
fn begin_entry(&mut self) {
let frame = self.stack.last_mut().unwrap();
if frame.flow {
self.flush_space();
return;
}
let indent = frame.indent;
if std::mem::replace(&mut frame.first, false) {
self.flush_space();
} else {
self.newline(indent);
}
}
fn begin_explicit_key(&mut self) -> Pos {
self.begin_entry();
let frame = self.stack.last_mut().unwrap();
frame.explicit = true;
let pos = if frame.flow {
Pos::Flow
} else {
Pos::Inline(frame.indent + 2)
};
self.out.push('?');
self.space = true;
pos
}
fn complete(&mut self) {
match self.stack.last_mut() {
Some(frame) if frame.is_map => {
if frame.value {
frame.explicit = false;
}
frame.value = !frame.value;
}
Some(_) => {}
None => self.done = true,
}
}
fn newline(&mut self, indent: usize) {
if !std::mem::take(&mut self.line_done) {
self.out.push('\n');
}
push_indent(&mut self.out, indent);
self.space = false;
}
fn flush_space(&mut self) {
if self.space {
self.out.push(' ');
self.space = false;
}
}
fn write_inline_start(&mut self, tag: Option<&str>) {
self.flush_space();
if let Some(tag) = tag {
write_tag(&mut self.out, tag);
self.out.push(' ');
}
}
fn atom(&mut self, atom: Atom, hints: Hints) -> Result<(), Error> {
if let Atom::Bytes(ref bytes) = atom
&& !self.config.binary
&& bytes.fallback.copied().unwrap_or(self.bytes) == BytesFormat::SEQ
{
self.emit(
Event::seq_start(),
Hints {
layout: hints.layout,
tag: hints.tag,
style: None,
},
)?;
for &byte in bytes.iter() {
self.emit(Event::Atom(Atom::U64(byte.into())), Hints::default())?;
}
return self.emit(Event::SeqEnd, Hints::default());
}
let pos = match self.begin_node()? {
Some(pos) => pos,
None => return self.key(atom, hints),
};
let context = match pos {
Pos::Flow => Context::Flow,
_ if self.config.indent == Indent::None => Context::Key,
_ => Context::Block,
};
let (scalar, implicit_tag) = self.render(&atom, context, hints.style)?;
let tag = hints.tag.as_deref().or(implicit_tag);
match scalar {
Scalar::Empty if !matches!(pos, Pos::Root) && tag.is_none() => {
self.space = false;
}
Scalar::Empty => {
self.write_inline_start(tag);
self.out.push_small("null");
}
Scalar::Text(_) | Scalar::Short(_) => {
self.write_inline_start(tag);
self.out.push_small(scalar.text().unwrap());
}
Scalar::Block(block) => {
self.write_inline_start(tag);
let (column, parent) = match pos {
Pos::Root => (self.config.indent_width(), 0),
Pos::Inline(column) => (column, column - 2),
Pos::Value(key_column) => (key_column + self.config.indent_width(), key_column),
Pos::Flow => unreachable!("no block scalars in flow collections"),
};
block.write_header(&mut self.out, column - parent);
self.out.push('\n');
block.write_body(&mut self.out, column);
self.line_done = true;
}
}
self.complete();
Ok(())
}
fn key(&mut self, atom: Atom, hints: Hints) -> Result<(), Error> {
let context = if self.in_flow() {
Context::Flow
} else {
Context::Key
};
let (scalar, implicit_tag) = self.render(&atom, context, hints.style)?;
let tag = hints.tag.as_deref().or(implicit_tag);
let text = match scalar {
Scalar::Empty => "null",
Scalar::Block(_) => unreachable!("keys are never block scalars"),
ref scalar => scalar.text().unwrap(),
};
if text.len() > MAX_SIMPLE_KEY_LEN {
self.begin_explicit_key();
self.write_inline_start(tag);
self.out.push_small(text);
} else {
self.begin_entry();
self.write_inline_start(tag);
self.out.push_small(text);
self.out.push(':');
self.space = true;
}
self.complete();
Ok(())
}
fn render<'a>(
&self,
atom: &'a Atom,
context: Context,
style: Option<ScalarStyle>,
) -> Result<(Scalar<'a>, Option<&'static str>), Error> {
Ok(match *atom {
Atom::Null => match (self.config.null_style, context) {
(NullStyle::Tilde, _) => (Scalar::Text("~".into()), None),
(NullStyle::Empty, Context::Block) => (Scalar::Empty, None),
_ => (Scalar::Text("null".into()), None),
},
Atom::Bool(value) => (
Scalar::Text(if value { "true" } else { "false" }.into()),
None,
),
Atom::U64(value) => (Scalar::short_text(itoa::Buffer::new().format(value)), None),
Atom::I64(value) => (Scalar::short_text(itoa::Buffer::new().format(value)), None),
Atom::F64(value) => {
let mut text = ShortText::new();
write_float(&mut text, value);
(Scalar::Short(text), None)
}
Atom::F32(value) => {
let mut text = ShortText::new();
write_float(&mut text, value);
(Scalar::Short(text), None)
}
Atom::Char(value) => (
self.render_owned_str(value.to_string(), context, style),
None,
),
Atom::Str(ref value) | Atom::Lexical(ref value) => {
(self.render_str(value, context, style), None)
}
Atom::Bytes(ref bytes) => {
let format = if self.config.binary {
BytesFormat::BASE64
} else {
bytes.fallback.copied().unwrap_or(self.bytes)
};
let encoded = format
.encode(bytes)
.or_else(|| BytesFormat::BASE64.encode(bytes))
.unwrap_or_default();
if self.config.binary {
let tag = Some("tag:yaml.org,2002:binary");
if encoded.is_empty() {
(Scalar::Text("\"\"".into()), tag)
} else if encoded.len() > BINARY_LINE_LEN && context == Context::Block {
(Scalar::Block(wrap_binary(&encoded)), tag)
} else {
(Scalar::Text(encoded.into()), tag)
}
} else {
(self.render_owned_str(encoded, context, style), None)
}
}
Atom::Ext(ref ext) => return self.render_ext(ext, context, style),
Atom::Implicit(ref value) if writes_as_plain(value, self.config.compat) => {
(Scalar::Text(Cow::Borrowed(value.text().as_str())), None)
}
Atom::Implicit(ref value) => {
let value = value.value().to_atom();
let (scalar, tag) = self.render(&value, context, style)?;
(scalar.into_owned(), tag)
}
_ => return Err(Error::new(ErrorKind::UnsupportedType, "unknown atom")),
})
}
#[cold]
fn render_ext<'a>(
&self,
ext: &'a ExtValue,
context: Context,
style: Option<ScalarStyle>,
) -> Result<(Scalar<'a>, Option<&'static str>), Error> {
if let Some(value) = ext.downcast_ref::<u128>() {
return Ok((Scalar::short(value), None));
}
if let Some(value) = ext.downcast_ref::<i128>() {
return Ok((Scalar::short(value), None));
}
if let Some(value) = ext.downcast_ref::<BigInt>() {
return Ok((Scalar::Text(value.to_string().into()), None));
}
if let Some(value) = ext.downcast_value_ref::<Number>()
&& self.is_number(value.as_str())
{
return Ok((Scalar::Text(value.as_str().into()), None));
}
if let Some(value) = ext.downcast_ref::<Decimal>()
&& self.is_number(value.as_str())
{
return Ok((Scalar::Text(value.as_str().into()), None));
}
let datetime = ext.downcast_ref::<Datetime>().copied().or_else(|| {
ext.downcast_ref::<Timestamp>()
.and_then(|x| x.to_datetime())
});
if let Some(value) = datetime {
return Ok(self.render_datetime(value, context, style));
}
match ext.fallback() {
Atom::Ext(_) => Err(Error::new(
ErrorKind::UnsupportedType,
format!("YAML does not support {}", ext.name()),
)),
Atom::Str(value) | Atom::Lexical(value) => Ok((
self.render_owned_str(value.into_owned(), context, style),
None,
)),
fallback => {
let (scalar, tag) = self.render(&fallback, context, style)?;
Ok((scalar.into_owned(), tag))
}
}
}
fn render_datetime<'a>(
&self,
value: Datetime,
context: Context,
style: Option<ScalarStyle>,
) -> (Scalar<'a>, Option<&'static str>) {
let text = value.to_string();
let is_timestamp = value.date.is_some() && (value.time.is_none() || value.offset.is_some());
if !is_timestamp {
return (self.render_owned_str(text, context, style), None);
}
let tag = self
.config
.timestamp_tag
.then_some("tag:yaml.org,2002:timestamp");
(Scalar::Text(text.into()), tag)
}
fn is_number(&self, s: &str) -> bool {
!is_plain_str(s, Version::V1_2)
&& (self.config.compat == Version::V1_2 || !is_plain_str(s, Version::V1_1))
&& s.parse::<f64>().is_ok()
}
fn render_owned_str<'a>(
&self,
value: String,
context: Context,
style: Option<ScalarStyle>,
) -> Scalar<'a> {
let context = match context {
Context::Block => Context::Key,
other => other,
};
match self.render_str(&value, context, style) {
Scalar::Text(text) => Scalar::Text(Cow::Owned(text.into_owned())),
_ => unreachable!("strings outside of blocks are text"),
}
}
fn render_str<'a>(
&self,
value: &'a str,
context: Context,
style: Option<ScalarStyle>,
) -> Scalar<'a> {
let block = context == Context::Block;
let flow = context == Context::Flow;
let fold_width = self.config.fold_width.unwrap_or(DEFAULT_FOLD_WIDTH);
match style {
Some(ScalarStyle::Plain) if is_plain_safe(value, self.config.compat, flow) => {
return Scalar::Text(value.into());
}
Some(ScalarStyle::SingleQuoted) if is_single_quote_safe(value) => {
let mut out = String::with_capacity(value.len() + 2);
write_single_quoted(&mut out, value);
return Scalar::Text(out.into());
}
Some(ScalarStyle::DoubleQuoted) => {
let mut out = String::with_capacity(value.len() + 2);
write_double_quoted(&mut out, value);
return Scalar::Text(out.into());
}
Some(ScalarStyle::Literal) if block => {
if let Some(block) = BlockScalar::literal(value) {
return Scalar::Block(block);
}
}
Some(ScalarStyle::Folded) if block => {
if let Some(block) =
BlockScalar::folded(value, fold_width).or_else(|| BlockScalar::literal(value))
{
return Scalar::Block(block);
}
}
_ => {}
}
if block && !self.config.quote_all {
if value.contains('\n') {
if self.config.multiline == MultilineStyle::Literal
&& let Some(block) = BlockScalar::literal(value)
{
return Scalar::Block(block);
}
} else if let Some(width) = self.config.fold_width
&& BlockScalar::should_fold(value, width)
&& let Some(block) = BlockScalar::folded(value, width)
{
return Scalar::Block(block);
}
}
if !self.config.quote_all && is_plain_safe(value, self.config.compat, flow) {
return Scalar::Text(Cow::Borrowed(value));
}
let mut out = String::with_capacity(value.len() + 2);
if self.config.quote_style == QuoteStyle::Single && is_single_quote_safe(value) {
write_single_quoted(&mut out, value);
} else {
write_double_quoted(&mut out, value);
}
Scalar::Text(out.into())
}
}