use bstr::BString;
use crate::parse::{self, EventRef};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Indent {
Tabs(usize),
Spaces(usize),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Newline {
Detect,
Lf,
CrLf,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct Options {
pub root_comment_indent: Option<Indent>,
pub key_value_indent: Indent,
pub spaces_around_separator: bool,
pub newline: Newline,
pub ensure_trailing_newline: bool,
pub max_leading_blank_lines: Option<usize>,
pub max_consecutive_blank_lines: Option<usize>,
}
impl Default for Options {
fn default() -> Self {
Options {
key_value_indent: Indent::Tabs(1),
root_comment_indent: Some(Indent::Spaces(0)),
spaces_around_separator: true,
newline: Newline::Detect,
ensure_trailing_newline: true,
max_leading_blank_lines: None,
max_consecutive_blank_lines: None,
}
}
}
pub fn normalize(input: &[u8], options: Options) -> Result<BString, parse::Error> {
let parsed = parse::Events::from_bytes(input, None)?;
let events: Vec<_> = parsed.iter().collect();
Ok(normalize_events(&events, options))
}
fn detect_newline(events: &[EventRef<'_>]) -> &'static [u8] {
for event in events {
if let EventRef::Newline(n) = event {
return if n.contains(&b'\r') { b"\r\n" } else { b"\n" };
}
}
b"\n"
}
fn indentation(indent: Indent) -> Vec<u8> {
match indent {
Indent::Tabs(n) => vec![b'\t'; n],
Indent::Spaces(n) => vec![b' '; n],
}
}
fn normalize_events(
events: &[EventRef<'_>],
Options {
root_comment_indent,
key_value_indent,
spaces_around_separator,
newline,
ensure_trailing_newline,
max_leading_blank_lines,
max_consecutive_blank_lines,
}: Options,
) -> BString {
let newline: &[u8] = match newline {
Newline::Detect => detect_newline(events),
Newline::Lf => b"\n",
Newline::CrLf => b"\r\n",
};
let key_value_indent = indentation(key_value_indent);
let root_comment_indent = root_comment_indent.map(indentation);
let mut out: Vec<u8> = Vec::with_capacity(events.len() * 8);
let mut in_section = false;
let mut line_has_content = false;
let mut has_seen_content = false;
let mut consecutive_blank_lines = 0usize;
let mut events = events.iter().copied().peekable();
while let Some(event) = events.next() {
match event {
EventRef::Whitespace(_) => {
let precedes_root_comment = !in_section
&& !line_has_content
&& events
.peek()
.is_some_and(|event| matches!(event, EventRef::Comment { .. }));
if precedes_root_comment && root_comment_indent.is_none() {
event.write_to(&mut out).expect("write to Vec is infallible");
}
}
EventRef::SectionHeader { .. } => {
event.write_to(&mut out).expect("write to Vec is infallible");
in_section = true;
line_has_content = true;
consecutive_blank_lines = 0;
}
EventRef::SectionValueName(_) => {
if in_section && !line_has_content {
out.extend_from_slice(&key_value_indent);
}
event.write_to(&mut out).expect("write to Vec is infallible");
line_has_content = true;
consecutive_blank_lines = 0;
}
EventRef::KeyValueSeparator => {
if spaces_around_separator {
out.extend_from_slice(b" = ");
} else {
out.push(b'=');
}
line_has_content = true;
consecutive_blank_lines = 0;
}
EventRef::Value(_) | EventRef::ValueNotDone(_) | EventRef::ValueDone(_) => {
event.write_to(&mut out).expect("write to Vec is infallible");
line_has_content = true;
consecutive_blank_lines = 0;
}
EventRef::Comment { tag, text } => {
if line_has_content {
out.push(b' ');
} else if in_section {
out.extend_from_slice(&key_value_indent);
} else if let Some(indent) = &root_comment_indent {
out.extend_from_slice(indent);
}
out.push(tag);
let text: &[u8] = text.as_ref();
let newline_follows = events.peek().is_some_and(|event| matches!(event, EventRef::Newline(_)));
out.extend_from_slice(if newline_follows {
text.strip_suffix(b"\r").unwrap_or(text)
} else {
text
});
line_has_content = true;
consecutive_blank_lines = 0;
}
EventRef::Newline(n) => {
for _ in 0..n.iter().filter(|&&b| b == b'\n').count() {
let is_blank_line = !line_has_content;
let max_blank_lines = if has_seen_content {
max_consecutive_blank_lines
} else {
max_leading_blank_lines
};
let should_emit =
max_blank_lines.is_none_or(|max_blank| !is_blank_line || consecutive_blank_lines < max_blank);
if should_emit {
out.extend_from_slice(newline);
}
if is_blank_line {
consecutive_blank_lines = consecutive_blank_lines.saturating_add(1);
} else {
has_seen_content = true;
consecutive_blank_lines = 0;
}
line_has_content = false;
}
}
}
}
if ensure_trailing_newline && !out.is_empty() {
while out.last() == Some(&b'\n') || out.last() == Some(&b'\r') {
out.pop();
}
out.extend_from_slice(newline);
}
out.into()
}