use std::borrow::Cow;
use std::ops::Range;
use saphyr_parser::{Event, Parser, ScalarStyle};
use super::{Container, Format, FormatError, Leaf, LeafVisitor, Object, Splicer, comment};
#[derive(Debug, Clone, Copy, Default)]
pub struct Yaml;
impl Format for Yaml {
fn name(&self) -> &str {
"yaml"
}
fn extensions(&self) -> &[&str] {
&["yaml", "yml"]
}
fn file_names(&self) -> &[&str] {
&[".clang-format", ".clang-tidy"]
}
fn rewrite(&self, input: &[u8], visitor: &mut dyn LeafVisitor) -> Result<Vec<u8>, FormatError> {
let text = std::str::from_utf8(input).map_err(|e| FormatError::new("yaml", e))?;
let documents = parse(text)?;
let mut splicer = Splicer::new(input);
if visitor.wants_comments() {
let mut spans = Vec::new();
for document in &documents {
collect_spans(document, &mut spans);
}
let ranges = comment::hash_outside(text, &mut spans);
comment::visit(text, &ranges, visitor, &mut splicer);
}
for document in &documents {
walk(document, None, text, visitor, &mut splicer);
}
Ok(splicer.finish())
}
}
enum Node<'a> {
Scalar {
value: Cow<'a, str>,
style: ScalarStyle,
span: Range<usize>,
},
Sequence(Vec<Node<'a>>),
Mapping(Vec<(Node<'a>, Node<'a>)>),
Alias,
}
impl Node<'_> {
fn as_str(&self) -> Option<&str> {
match self {
Node::Scalar { value, .. } => Some(value),
_ => None,
}
}
}
struct Offsets {
bytes: Vec<usize>,
len: usize,
}
impl Offsets {
fn new(text: &str) -> Self {
let bytes = if text.is_ascii() {
Vec::new()
} else {
text.char_indices().map(|(i, _)| i).collect()
};
Self {
bytes,
len: text.len(),
}
}
fn byte(&self, char_index: usize) -> usize {
if self.bytes.is_empty() {
char_index
} else {
self.bytes.get(char_index).copied().unwrap_or(self.len)
}
}
}
fn parse(text: &str) -> Result<Vec<Node<'_>>, FormatError> {
let offsets = Offsets::new(text);
let mut events = Parser::new_from_str(text).map(|event| {
event
.map(|(event, span)| {
let range = offsets.byte(span.start.index())..offsets.byte(span.end.index());
(event, range)
})
.map_err(|e| FormatError::new("yaml", e))
});
let mut documents = Vec::new();
while let Some(event) = events.next() {
match event?.0 {
Event::DocumentStart(_) => {
if let Some(node) = parse_node(&mut events)? {
documents.push(node);
}
}
Event::StreamEnd => break,
_ => {}
}
}
Ok(documents)
}
fn parse_node<'a, I>(events: &mut I) -> Result<Option<Node<'a>>, FormatError>
where
I: Iterator<Item = Result<(Event<'a>, Range<usize>), FormatError>>,
{
loop {
let Some(event) = events.next() else {
return Ok(None);
};
let (event, span) = event?;
return Ok(Some(match event {
Event::Scalar(value, style, _, _) => Node::Scalar { value, style, span },
Event::Alias(_) => Node::Alias,
Event::SequenceStart(..) => {
let mut items = Vec::new();
while let Some(item) = parse_node(events)? {
items.push(item);
}
Node::Sequence(items)
}
Event::MappingStart(..) => {
let mut pairs = Vec::new();
while let Some(key) = parse_node(events)? {
let value = parse_node(events)?
.ok_or_else(|| FormatError::new("yaml", "mapping key without a value"))?;
pairs.push((key, value));
}
Node::Mapping(pairs)
}
Event::SequenceEnd | Event::MappingEnd | Event::DocumentEnd | Event::StreamEnd => {
return Ok(None);
}
Event::Nothing | Event::StreamStart | Event::DocumentStart(_) => continue,
}));
}
}
fn collect_spans(node: &Node<'_>, out: &mut Vec<Range<usize>>) {
match node {
Node::Scalar { span, .. } => out.push(span.clone()),
Node::Sequence(items) => items.iter().for_each(|i| collect_spans(i, out)),
Node::Mapping(pairs) => {
for (k, v) in pairs {
collect_spans(k, out);
collect_spans(v, out);
}
}
Node::Alias => {}
}
}
fn walk(
node: &Node<'_>,
key: Option<&str>,
text: &str,
visitor: &mut dyn LeafVisitor,
splicer: &mut Splicer<'_>,
) {
match node {
Node::Scalar { value, style, span } => {
let quoted = matches!(style, ScalarStyle::SingleQuoted | ScalarStyle::DoubleQuoted);
let leaf = Leaf::new(value)
.with_key(key)
.with_offset(span.start + usize::from(quoted));
let Some(replacement) = visitor.leaf(&leaf) else {
return;
};
match style {
ScalarStyle::SingleQuoted | ScalarStyle::DoubleQuoted => {
let content = span.start + 1..span.end.saturating_sub(1).max(span.start + 1);
splicer.apply(span.clone(), content, value, &replacement, double_quoted);
}
ScalarStyle::Literal | ScalarStyle::Folded => {
let raw = &text[span.clone()];
let indent = indentation(text, span.start);
let folded = matches!(style, ScalarStyle::Folded);
splicer.apply(span.clone(), span.clone(), value, &replacement, |v| {
block_body(v, raw, indent, folded)
});
}
ScalarStyle::Plain => {
splicer.replace(span.clone(), double_quoted(&replacement.value));
}
}
}
Node::Sequence(items) => {
visitor.enter(key, Container::Array);
for item in items {
walk(item, None, text, visitor, splicer);
}
visitor.exit();
}
Node::Mapping(pairs) => {
let summary: Object<'_> = pairs
.iter()
.filter_map(|(k, v)| Some((k.as_str()?, v.as_str())))
.collect();
visitor.enter(key, Container::Object(&summary));
for (k, v) in pairs {
walk(v, k.as_str(), text, visitor, splicer);
}
visitor.exit();
}
Node::Alias => {}
}
}
fn double_quoted(value: &str) -> String {
serde_json::to_string(value).expect("strings always serialize")
}
fn indentation(text: &str, offset: usize) -> &str {
let line_start = text[..offset].rfind('\n').map_or(0, |i| i + 1);
let prefix = &text[line_start..offset];
&prefix[..prefix.len() - prefix.trim_start_matches([' ', '\t']).len()]
}
fn block_body(value: &str, raw: &str, indent: &str, folded: bool) -> String {
let trailing = &raw[raw.trim_end_matches(['\n', '\r', ' ', '\t']).len()..];
let body = value.trim_end_matches('\n');
let separator = if folded {
format!("\n\n{indent}")
} else {
format!("\n{indent}")
};
let mut out = body.split('\n').collect::<Vec<_>>().join(&separator);
out.push_str(trailing);
out
}