use super::plan::{FieldBody, FieldPlan};
use super::rcode;
use super::wrap;
use crate::formatter::style::FormatStyle;
pub(super) fn render(field: &FieldPlan, style: FormatStyle, indent: &str) -> Vec<String> {
let name = field.name.as_str();
match &field.body {
FieldBody::Wrapped(value) => wrap::fill(name, value, style.line_width, indent),
FieldBody::CommaList(entries) => comma_list(name, entries, indent),
FieldBody::OrderedList(tokens) => ordered_list(name, tokens, indent),
FieldBody::RCode(source) => rcode::render(name, source, style, indent)
.unwrap_or_else(|| opaque(name, &split_folded(source), indent)),
FieldBody::Opaque(lines) => opaque(name, lines, indent),
FieldBody::Verbatim(raw) => verbatim(name, raw),
}
}
fn comma_list(name: &str, entries: &[String], indent: &str) -> Vec<String> {
if entries.is_empty() {
return vec![format!("{name}:")];
}
let mut lines = Vec::with_capacity(entries.len() + 1);
lines.push(format!("{name}:"));
for (index, entry) in entries.iter().enumerate() {
let comma = if index + 1 < entries.len() { "," } else { "" };
lines.push(format!("{indent}{entry}{comma}"));
}
lines
}
fn ordered_list(name: &str, tokens: &[String], indent: &str) -> Vec<String> {
if tokens.is_empty() {
return vec![format!("{name}:")];
}
let mut lines = Vec::with_capacity(tokens.len() + 1);
lines.push(format!("{name}:"));
lines.extend(tokens.iter().map(|token| format!("{indent}'{token}'")));
lines
}
fn opaque(name: &str, lines: &[String], indent: &str) -> Vec<String> {
let mut iter = lines.iter();
let head = match iter.next() {
Some(first) if !first.is_empty() => format!("{name}: {first}"),
_ => format!("{name}:"),
};
let mut out = vec![head];
out.extend(iter.map(|line| format!("{indent}{line}")));
out
}
fn verbatim(name: &str, raw: &str) -> Vec<String> {
let joined = format!("{name}:{raw}");
let mut lines: Vec<String> = joined
.split('\n')
.map(|line| line.strip_suffix('\r').unwrap_or(line).to_string())
.collect();
if lines.last().is_some_and(String::is_empty) {
lines.pop();
}
lines
}
fn split_folded(source: &str) -> Vec<String> {
source.split('\n').map(str::to_string).collect()
}
#[cfg(test)]
mod tests {
use super::*;
const INDENT: &str = " ";
fn field(name: &str, body: FieldBody) -> FieldPlan {
FieldPlan {
name: name.to_string(),
leading_comments: Vec::new(),
body,
}
}
fn rendered(name: &str, body: FieldBody) -> Vec<String> {
render(&field(name, body), FormatStyle::default(), INDENT)
}
#[test]
fn a_single_entry_comma_list_still_breaks() {
assert_eq!(
rendered("Imports", FieldBody::CommaList(vec!["cli".to_string()])),
vec!["Imports:", " cli"]
);
}
#[test]
fn an_empty_comma_list_keeps_the_field() {
assert_eq!(
rendered("Suggests", FieldBody::CommaList(Vec::new())),
vec!["Suggests:"]
);
}
#[test]
fn ordered_lists_are_quoted_in_place() {
assert_eq!(
rendered(
"Collate",
FieldBody::OrderedList(vec!["b.R".to_string(), "a.R".to_string()])
),
vec!["Collate:", " 'b.R'", " 'a.R'"]
);
}
#[test]
fn an_opaque_field_with_an_empty_own_line_keeps_it_empty() {
assert_eq!(
rendered(
"Config/x",
FieldBody::Opaque(vec![String::new(), "value".to_string()])
),
vec!["Config/x:", " value"]
);
}
#[test]
fn verbatim_replays_the_source_bytes() {
assert_eq!(
rendered(
"Collate",
FieldBody::Verbatim("\n 'a.R'\n# why\n 'b.R'\n".to_string())
),
vec!["Collate:", " 'a.R'", "# why", " 'b.R'"]
);
}
#[test]
fn unparseable_r_falls_back_to_the_opaque_shape() {
assert_eq!(
rendered("Authors@R", FieldBody::RCode("person(\"Jo\",".to_string())),
vec!["Authors@R: person(\"Jo\","]
);
}
}