pub fn strip_bom(content: &str) -> &str {
content.strip_prefix('\u{FEFF}').unwrap_or(content)
}
pub fn unfold(content: &str) -> Vec<String> {
let content = strip_bom(content);
let normalized = content.replace("\r\n", "\n");
let mut logical: Vec<String> = Vec::new();
let mut current: Option<String> = None;
for line in normalized.split('\n') {
if let Some(rest) = line.strip_prefix(' ').or_else(|| line.strip_prefix('\t')) {
match current.as_mut() {
Some(c) => c.push_str(rest),
None => current = Some(rest.to_string()),
}
} else {
if let Some(c) = current.take() {
logical.push(c);
}
current = Some(line.to_string());
}
}
if let Some(c) = current.take() {
logical.push(c);
}
while matches!(logical.last(), Some(s) if s.is_empty()) {
logical.pop();
}
logical
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_bom_removes_leading_bom_only() {
assert_eq!(strip_bom("\u{FEFF}HELLO"), "HELLO");
assert_eq!(strip_bom("HELLO"), "HELLO");
assert_eq!(strip_bom("HEL\u{FEFF}LO"), "HEL\u{FEFF}LO");
}
#[test]
fn unfold_passes_through_single_lines() {
let input = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n";
let logical = unfold(input);
assert_eq!(
logical,
vec!["BEGIN:VCALENDAR", "VERSION:2.0", "END:VCALENDAR"]
);
}
#[test]
fn unfold_joins_space_continuation() {
let input = "A:long-value-folded\r\n here\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["A:long-value-foldedhere"]);
}
#[test]
fn unfold_joins_tab_continuation() {
let input = "A:long-value-folded\r\n\there\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["A:long-value-foldedhere"]);
}
#[test]
fn unfold_joins_multiple_continuations() {
let input = "A:part1\r\n part2\r\n part3\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["A:part1part2part3"]);
}
#[test]
fn unfold_accepts_lf_only_line_terminators() {
let input = "A:foo\nB:bar\n";
let logical = unfold(input);
assert_eq!(logical, vec!["A:foo", "B:bar"]);
}
#[test]
fn unfold_strips_leading_bom() {
let input = "\u{FEFF}BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["BEGIN:VCALENDAR", "END:VCALENDAR"]);
}
#[test]
fn unfold_preserves_utf8_in_continuation() {
let input = "SUMMARY:憲法\r\n 記念日\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["SUMMARY:憲法記念日"]);
}
#[test]
fn unfold_drops_trailing_empty_logical_line() {
let input = "A:foo\r\n";
let logical = unfold(input);
assert_eq!(logical, vec!["A:foo"]);
}
}