use crate::{CommentedValue, Comments, Error, IndentStyle, QuoteStyle, Result, Value};
use std::collections::HashMap;
use std::io::Write;
pub trait Emitter {
fn emit<W: Write>(&mut self, value: &Value, writer: W) -> Result<()>;
fn emit_commented<W: Write>(&mut self, value: &CommentedValue, writer: W) -> Result<()>;
fn emit_with_style<W: Write>(
&mut self,
value: &CommentedValue,
indent_style: &IndentStyle,
writer: W,
) -> Result<()>;
fn reset(&mut self);
}
#[derive(Debug, Clone)]
struct ValueInfo {
anchor_name: String,
first_occurrence: bool,
}
#[derive(Debug)]
pub struct BasicEmitter {
indent: usize,
current_indent: usize,
shared_values: HashMap<Value, ValueInfo>,
anchor_counter: usize,
indent_style: IndentStyle,
yaml_version: Option<(u8, u8)>,
tag_directives: Vec<(String, String)>,
}
#[allow(dead_code)]
impl BasicEmitter {
pub fn new() -> Self {
Self {
indent: 2,
current_indent: 0,
shared_values: HashMap::new(),
anchor_counter: 0,
indent_style: IndentStyle::default(),
yaml_version: None,
tag_directives: Vec::new(),
}
}
pub fn with_indent(indent: usize) -> Self {
Self {
indent,
current_indent: 0,
shared_values: HashMap::new(),
anchor_counter: 0,
indent_style: IndentStyle::Spaces(indent),
yaml_version: None,
tag_directives: Vec::new(),
}
}
pub fn with_indent_style(indent_style: IndentStyle) -> Self {
let indent = match &indent_style {
IndentStyle::Spaces(width) => *width,
IndentStyle::Tabs => 1, };
Self {
indent,
current_indent: 0,
shared_values: HashMap::new(),
anchor_counter: 0,
indent_style,
yaml_version: None,
tag_directives: Vec::new(),
}
}
pub fn set_yaml_version(&mut self, major: u8, minor: u8) {
self.yaml_version = Some((major, minor));
}
pub fn add_tag_directive(&mut self, handle: String, prefix: String) {
self.tag_directives.push((handle, prefix));
}
pub fn clear_directives(&mut self) {
self.yaml_version = None;
self.tag_directives.clear();
}
fn emit_directives<W: Write>(&self, writer: &mut W) -> Result<()> {
if let Some((major, minor)) = self.yaml_version {
writeln!(writer, "%YAML {}.{}", major, minor).map_err(|e| Error::Emission {
message: format!("Failed to write YAML directive: {}", e),
})?;
}
for (handle, prefix) in &self.tag_directives {
writeln!(writer, "%TAG {} {}", handle, prefix).map_err(|e| Error::Emission {
message: format!("Failed to write TAG directive: {}", e),
})?;
}
if self.yaml_version.is_some() || !self.tag_directives.is_empty() {
writeln!(writer, "---").map_err(|e| Error::Emission {
message: format!("Failed to write document start marker: {}", e),
})?;
}
Ok(())
}
fn analyze_shared_values(&mut self, value: &Value) {
let mut value_counts = HashMap::new();
self.count_value_occurrences(value, &mut value_counts);
for (val, count) in value_counts {
if count > 1 && self.is_complex_value(&val) {
let anchor_name = format!("anchor{}", self.anchor_counter);
self.anchor_counter += 1;
self.shared_values.insert(
val,
ValueInfo {
anchor_name,
first_occurrence: true,
},
);
}
}
}
fn count_value_occurrences(&self, value: &Value, counts: &mut HashMap<Value, usize>) {
if self.is_complex_value(value) {
*counts.entry(value.clone()).or_insert(0) += 1;
}
match value {
Value::Sequence(seq) => {
for item in seq {
self.count_value_occurrences(item, counts);
}
}
Value::Mapping(map) => {
for (key, val) in map {
self.count_value_occurrences(key, counts);
self.count_value_occurrences(val, counts);
}
}
_ => {}
}
}
const fn is_complex_value(&self, value: &Value) -> bool {
matches!(value, Value::Sequence(_) | Value::Mapping(_))
}
fn next_anchor_name(&mut self) -> String {
let name = format!("anchor{}", self.anchor_counter);
self.anchor_counter += 1;
name
}
pub const fn set_indent_style(&mut self, indent_style: IndentStyle) {
self.indent = match &indent_style {
IndentStyle::Spaces(width) => *width,
IndentStyle::Tabs => 1,
};
self.indent_style = indent_style;
}
fn write_indent<W: Write>(&self, writer: &mut W) -> Result<()> {
match &self.indent_style {
IndentStyle::Spaces(_width) => {
let total_spaces = self.current_indent;
for _ in 0..total_spaces {
write!(writer, " ")?;
}
}
IndentStyle::Tabs => {
let indent_levels = self.current_indent / self.indent;
for _ in 0..indent_levels {
write!(writer, "\t")?;
}
}
}
Ok(())
}
fn emit_leading_comments<W: Write>(&self, comments: &[String], writer: &mut W) -> Result<()> {
for comment in comments {
self.write_indent(writer)?;
writeln!(writer, "# {}", comment)?;
}
Ok(())
}
fn emit_trailing_comment<W: Write>(&self, comment: &str, writer: &mut W) -> Result<()> {
write!(writer, " # {}", comment)?;
Ok(())
}
fn emit_inner_comments<W: Write>(&self, comments: &[String], writer: &mut W) -> Result<()> {
for comment in comments {
writeln!(writer)?;
self.write_indent(writer)?;
writeln!(writer, "# {}", comment)?;
}
Ok(())
}
fn emit_scalar<W: Write>(&self, value: &Value, writer: &mut W) -> Result<()> {
self.emit_scalar_with_comments(value, None, writer)
}
fn emit_scalar_with_comments<W: Write>(
&self,
value: &Value,
comments: Option<&Comments>,
writer: &mut W,
) -> Result<()> {
self.emit_scalar_with_comments_and_style(value, comments, None, writer)
}
fn emit_scalar_with_comments_and_style<W: Write>(
&self,
value: &Value,
comments: Option<&Comments>,
quote_style: Option<&QuoteStyle>,
writer: &mut W,
) -> Result<()> {
if let Some(comments) = comments {
self.emit_leading_comments(&comments.leading, writer)?;
}
match value {
Value::Null => write!(writer, "null")?,
Value::Bool(b) => write!(writer, "{}", b)?,
Value::Int(i) => write!(writer, "{}", i)?,
Value::Float(f) => {
if f.is_nan() {
write!(writer, ".nan")?;
} else if f.is_infinite() {
if f.is_sign_positive() {
write!(writer, ".inf")?;
} else {
write!(writer, "-.inf")?;
}
} else {
if f.fract() == 0.0 {
write!(writer, "{:.1}", f)?;
} else {
write!(writer, "{}", f)?;
}
}
}
Value::String(s) => {
self.emit_string_with_style(s, quote_style, writer)?;
}
_ => return Err(Error::emission("Non-scalar passed to emit_scalar")),
}
if let Some(comments) = comments {
if let Some(ref trailing) = comments.trailing {
self.emit_trailing_comment(trailing, writer)?;
}
}
Ok(())
}
fn emit_string<W: Write>(&self, s: &str, writer: &mut W) -> Result<()> {
self.emit_string_with_style(s, None, writer)
}
fn emit_string_with_style<W: Write>(
&self,
s: &str,
preferred_style: Option<&QuoteStyle>,
writer: &mut W,
) -> Result<()> {
match preferred_style {
Some(QuoteStyle::Single) => self.emit_single_quoted_string(s, writer),
Some(QuoteStyle::Double) => self.emit_double_quoted_string(s, writer),
Some(QuoteStyle::Plain) | None => {
if self.needs_quoting(s) {
self.emit_double_quoted_string(s, writer)
} else {
write!(writer, "{}", s)?;
Ok(())
}
}
}
}
fn needs_quoting(&self, s: &str) -> bool {
if s.is_empty() {
return true;
}
if s == "null"
|| s == "~"
|| s == "true"
|| s == "false"
|| s == "yes"
|| s == "no"
|| s == "on"
|| s == "off"
|| s.parse::<i64>().is_ok()
|| s.parse::<f64>().is_ok()
{
return true;
}
if s.chars().any(|c| c == '.') && s.chars().any(|c| c.is_ascii_digit()) {
return true;
}
if s.contains('\n')
|| s.contains('\r')
|| s.contains('\t')
|| s.contains('"')
|| s.contains('\'')
|| s.contains(':')
|| s.contains('#')
|| s.contains('-')
|| s.contains('[')
|| s.contains(']')
|| s.contains('{')
|| s.contains('}')
|| s.starts_with(' ')
|| s.ends_with(' ')
{
return true;
}
false
}
fn emit_double_quoted_string<W: Write>(&self, s: &str, writer: &mut W) -> Result<()> {
write!(writer, "\"")?;
for ch in s.chars() {
match ch {
'"' => write!(writer, "\\\"")?,
'\\' => write!(writer, "\\\\")?,
'\n' => write!(writer, "\\n")?,
'\r' => write!(writer, "\\r")?,
'\t' => write!(writer, "\\t")?,
c if c.is_control() => write!(writer, "\\u{:04x}", c as u32)?,
c => write!(writer, "{}", c)?,
}
}
write!(writer, "\"")?;
Ok(())
}
fn emit_single_quoted_string<W: Write>(&self, s: &str, writer: &mut W) -> Result<()> {
write!(writer, "'")?;
for ch in s.chars() {
match ch {
'\'' => write!(writer, "''")?, c => write!(writer, "{}", c)?,
}
}
write!(writer, "'")?;
Ok(())
}
fn emit_quoted_string<W: Write>(&self, s: &str, writer: &mut W) -> Result<()> {
self.emit_double_quoted_string(s, writer)
}
fn emit_sequence<W: Write>(&mut self, seq: &[Value], writer: &mut W) -> Result<()> {
if seq.is_empty() {
write!(writer, "[]")?;
return Ok(());
}
for (index, item) in seq.iter().enumerate() {
if index > 0 {
writeln!(writer)?;
}
self.write_indent(writer)?;
write!(writer, "- ")?;
match item {
Value::Sequence(_) | Value::Mapping(_) => {
writeln!(writer)?; self.current_indent += self.indent;
self.emit_value(item, writer)?;
self.current_indent -= self.indent;
}
_ => {
self.emit_scalar(item, writer)?;
}
}
}
Ok(())
}
fn emit_mapping_with_anchor<W: Write>(
&mut self,
map: &indexmap::IndexMap<Value, Value>,
anchor: &str,
writer: &mut W,
) -> Result<()> {
if map.is_empty() {
write!(writer, "&{} {{}}", anchor)?;
return Ok(());
}
writeln!(writer, "&{}", anchor)?;
let mut first = true;
for (key, value) in map {
if !first {
writeln!(writer)?;
}
first = false;
self.write_indent(writer)?;
let is_complex_key = matches!(key, Value::Sequence(_) | Value::Mapping(_));
if is_complex_key {
write!(writer, "? ")?;
self.emit_value(key, writer)?;
writeln!(writer)?;
self.write_indent(writer)?;
} else {
self.emit_scalar(key, writer)?;
}
write!(writer, ": ")?;
match value {
Value::Sequence(_) | Value::Mapping(_) => {
writeln!(writer)?; self.current_indent += self.indent;
self.emit_value(value, writer)?;
self.current_indent -= self.indent;
}
_ => {
self.emit_scalar(value, writer)?;
}
}
}
Ok(())
}
fn emit_mapping<W: Write>(
&mut self,
map: &indexmap::IndexMap<Value, Value>,
writer: &mut W,
) -> Result<()> {
if map.is_empty() {
write!(writer, "{{}}")?;
return Ok(());
}
let mut first = true;
for (key, value) in map {
if !first {
writeln!(writer)?;
}
first = false;
self.write_indent(writer)?;
let is_complex_key = matches!(key, Value::Sequence(_) | Value::Mapping(_));
if is_complex_key {
write!(writer, "? ")?;
match key {
Value::Mapping(map) => {
self.emit_mapping_flow_style(map, writer)?;
}
Value::Sequence(seq) => {
self.emit_sequence_flow_style(seq, writer)?;
}
_ => {
self.emit_value(key, writer)?;
}
}
writeln!(writer)?;
self.write_indent(writer)?;
} else {
self.emit_scalar(key, writer)?;
}
write!(writer, ": ")?;
match value {
Value::Sequence(_) | Value::Mapping(_) => {
writeln!(writer)?; self.current_indent += self.indent;
self.emit_value(value, writer)?;
self.current_indent -= self.indent;
}
_ => {
self.emit_scalar(value, writer)?;
}
}
}
Ok(())
}
fn emit_mapping_flow_style<W: Write>(
&self,
map: &indexmap::IndexMap<Value, Value>,
writer: &mut W,
) -> Result<()> {
write!(writer, "{{")?;
let mut first = true;
for (key, value) in map {
if !first {
write!(writer, ", ")?;
}
first = false;
match key {
Value::Mapping(nested_map) => {
self.emit_mapping_flow_style(nested_map, writer)?;
}
Value::Sequence(nested_seq) => {
self.emit_sequence_flow_style(nested_seq, writer)?;
}
_ => {
self.emit_scalar(key, writer)?;
}
}
write!(writer, ": ")?;
match value {
Value::Mapping(nested_map) => {
self.emit_mapping_flow_style(nested_map, writer)?;
}
Value::Sequence(nested_seq) => {
self.emit_sequence_flow_style(nested_seq, writer)?;
}
_ => {
self.emit_scalar(value, writer)?;
}
}
}
write!(writer, "}}")?;
Ok(())
}
fn emit_sequence_flow_style<W: Write>(&self, seq: &[Value], writer: &mut W) -> Result<()> {
write!(writer, "[")?;
let mut first = true;
for item in seq {
if !first {
write!(writer, ", ")?;
}
first = false;
match item {
Value::Mapping(nested_map) => {
self.emit_mapping_flow_style(nested_map, writer)?;
}
Value::Sequence(nested_seq) => {
self.emit_sequence_flow_style(nested_seq, writer)?;
}
_ => {
self.emit_scalar(item, writer)?;
}
}
}
write!(writer, "]")?;
Ok(())
}
fn emit_value<W: Write>(&mut self, value: &Value, writer: &mut W) -> Result<()> {
if let Some(info) = self.shared_values.get(value).cloned() {
if info.first_occurrence {
match value {
Value::Sequence(seq) => {
write!(writer, "&{} ", info.anchor_name)?;
self.emit_sequence(seq, writer)?;
}
Value::Mapping(map) => {
self.emit_mapping_with_anchor(map, &info.anchor_name, writer)?;
}
_ => self.emit_scalar(value, writer)?, }
if let Some(info_mut) = self.shared_values.get_mut(value) {
info_mut.first_occurrence = false;
}
} else {
write!(writer, "*{}", info.anchor_name)?;
}
} else {
match value {
Value::Sequence(seq) => self.emit_sequence(seq, writer),
Value::Mapping(map) => self.emit_mapping(map, writer),
_ => self.emit_scalar(value, writer),
}?;
}
Ok(())
}
fn emit_value_simple<W: Write>(&mut self, value: &Value, writer: &mut W) -> Result<()> {
match value {
Value::Sequence(seq) => self.emit_sequence(seq, writer),
Value::Mapping(map) => self.emit_mapping(map, writer),
_ => self.emit_scalar(value, writer),
}
}
fn emit_commented_value<W: Write>(
&mut self,
commented: &CommentedValue,
writer: &mut W,
) -> Result<()> {
let comments = if commented.has_comments() {
Some(&commented.comments)
} else {
None
};
let quote_style = commented.quote_style();
match &commented.value {
Value::Sequence(_) | Value::Mapping(_) => {
if let Some(comments) = comments {
self.emit_leading_comments(&comments.leading, writer)?;
}
self.emit_value(&commented.value, writer)?;
if let Some(comments) = comments {
if !comments.inner.is_empty() {
self.emit_inner_comments(&comments.inner, writer)?;
}
}
if let Some(comments) = comments {
if let Some(ref trailing) = comments.trailing {
writeln!(writer)?;
self.write_indent(writer)?;
writeln!(writer, "# {}", trailing)?;
}
}
}
_ => {
self.emit_scalar_with_comments_and_style(
&commented.value,
comments,
quote_style,
writer,
)?;
}
}
Ok(())
}
pub fn emit_commented_value_public<W: Write>(
&mut self,
commented: &CommentedValue,
writer: W,
) -> Result<()> {
let mut writer = writer;
self.emit_commented_value(commented, &mut writer)
}
}
impl Default for BasicEmitter {
fn default() -> Self {
Self::new()
}
}
impl Emitter for BasicEmitter {
fn emit<W: Write>(&mut self, value: &Value, mut writer: W) -> Result<()> {
self.current_indent = 0;
self.shared_values.clear();
self.anchor_counter = 0;
self.emit_directives(&mut writer)?;
self.analyze_shared_values(value);
if matches!(value, Value::Sequence(_)) {
writeln!(writer)?;
}
self.emit_value(value, &mut writer)?;
writeln!(writer)?; Ok(())
}
fn emit_commented<W: Write>(&mut self, value: &CommentedValue, mut writer: W) -> Result<()> {
self.current_indent = 0;
self.shared_values.clear();
self.anchor_counter = 0;
self.emit_directives(&mut writer)?;
self.analyze_shared_values(&value.value);
self.emit_commented_value(value, &mut writer)?;
writeln!(writer)?; Ok(())
}
fn emit_with_style<W: Write>(
&mut self,
value: &CommentedValue,
indent_style: &IndentStyle,
mut writer: W,
) -> Result<()> {
let original_style = self.indent_style.clone();
self.set_indent_style(indent_style.clone());
self.current_indent = 0;
self.shared_values.clear();
self.anchor_counter = 0;
self.emit_directives(&mut writer)?;
self.analyze_shared_values(&value.value);
let result = self.emit_commented_value(value, &mut writer);
if result.is_ok() {
writeln!(writer)?; }
self.set_indent_style(original_style);
result
}
fn reset(&mut self) {
self.current_indent = 0;
self.shared_values.clear();
self.anchor_counter = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn test_emit_scalar() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
emitter.emit(&Value::Int(42), &mut output).unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "42\n");
}
#[test]
fn test_emit_string() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
emitter
.emit(&Value::String("hello".to_string()), &mut output)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "hello\n");
}
#[test]
fn test_emit_quoted_string() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
emitter
.emit(&Value::String("true".to_string()), &mut output)
.unwrap();
assert_eq!(String::from_utf8(output).unwrap(), "\"true\"\n");
}
#[test]
fn test_emit_sequence() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
let seq = Value::Sequence(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
emitter.emit(&seq, &mut output).unwrap();
let result = String::from_utf8(output).unwrap();
let expected = "\n- 1\n- 2\n- 3\n";
assert_eq!(result, expected);
}
#[test]
fn test_emit_mapping() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
let mut map = IndexMap::new();
map.insert(
Value::String("key".to_string()),
Value::String("value".to_string()),
);
map.insert(Value::String("number".to_string()), Value::Int(42));
emitter.emit(&Value::Mapping(map), &mut output).unwrap();
let result = String::from_utf8(output).unwrap();
assert!(result.contains("key: value"));
assert!(result.contains("number: 42"));
}
#[test]
fn test_emit_nested_structure() {
let mut emitter = BasicEmitter::new();
let mut output = Vec::new();
let inner_seq = Value::Sequence(vec![Value::Int(1), Value::Int(2)]);
let mut outer_map = IndexMap::new();
outer_map.insert(Value::String("items".to_string()), inner_seq);
emitter
.emit(&Value::Mapping(outer_map), &mut output)
.unwrap();
let result = String::from_utf8(output).unwrap();
assert!(result.contains("items:"));
assert!(result.contains("- 1"));
assert!(result.contains("- 2"));
}
}