use super::line_endings::{LineEnding, LineEndingCounts, count_line_endings, serialize_lines};
const BYTE_ORDER_MARK: char = '\u{FEFF}';
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SourceDocument<'a> {
has_byte_order_mark: bool,
body: &'a str,
counts: LineEndingCounts,
}
impl<'a> SourceDocument<'a> {
#[must_use]
pub fn parse(content: &'a str) -> Self {
let (has_byte_order_mark, body) = match content.strip_prefix(BYTE_ORDER_MARK) {
Some(body) => (true, body),
None => (false, content),
};
Self {
has_byte_order_mark,
body,
counts: count_line_endings(body),
}
}
#[must_use]
pub const fn body(&self) -> &'a str { self.body }
#[must_use]
pub const fn counts(&self) -> LineEndingCounts { self.counts }
#[must_use]
pub const fn ending(&self) -> LineEnding { self.counts.ending }
#[must_use]
pub fn render(&self, lines: &[String]) -> String {
let body = serialize_lines(lines, self.ending());
if !self.has_byte_order_mark {
return body;
}
let mut rendered = String::with_capacity(body.len() + BYTE_ORDER_MARK.len_utf8());
rendered.push(BYTE_ORDER_MARK);
rendered.push_str(&body);
rendered
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::SourceDocument;
fn lines(text: &str) -> Vec<String> { text.lines().map(str::to_string).collect() }
#[test]
fn parse_splits_the_byte_order_mark_from_the_body() {
let document = SourceDocument::parse("\u{FEFF}|A|B|\n|1|2|\n");
assert_eq!(document.body(), "|A|B|\n|1|2|\n");
assert_eq!(document.counts().crlf_count, 0);
assert_eq!(document.counts().lone_lf_count, 2);
}
#[rstest]
#[case::empty("", "")]
#[case::no_mark("alpha\n", "alpha\n")]
#[case::mark_only("\u{FEFF}", "")]
#[case::mark_and_body("\u{FEFF}alpha\n", "alpha\n")]
fn parse_removes_only_the_leading_mark(#[case] content: &str, #[case] expected: &str) {
assert_eq!(SourceDocument::parse(content).body(), expected);
}
#[test]
fn parse_counts_the_body_not_the_mark() {
let document = SourceDocument::parse("\u{FEFF}alpha\r\nbeta\r\n");
assert_eq!(document.counts().crlf_count, 2);
assert_eq!(document.counts().lone_lf_count, 0);
assert_eq!(document.ending().as_str(), "\r\n");
}
#[rstest]
#[case::empty(vec![], "")]
#[case::single(lines("alpha\n"), "alpha\n")]
#[case::pair(lines("alpha\nbeta\n"), "alpha\nbeta\n")]
fn render_terminates_every_line(#[case] lines: Vec<String>, #[case] expected: &str) {
assert_eq!(SourceDocument::parse("").render(&lines), expected);
}
#[test]
fn render_uses_the_document_ending() {
let document = SourceDocument::parse("a\r\nb\r\n");
assert_eq!(
document.render(&["x".to_string(), "y".to_string()]),
"x\r\ny\r\n"
);
}
#[test]
fn render_restores_the_byte_order_mark() {
let document = SourceDocument::parse("\u{FEFF}alpha\n");
assert_eq!(document.render(&["x".to_string()]), "\u{FEFF}x\n");
}
#[test]
fn render_keeps_a_mark_that_formats_to_no_lines() {
let document = SourceDocument::parse("\u{FEFF}alpha\n");
assert_eq!(document.render(&[]), "\u{FEFF}");
}
#[rstest]
#[case::byte_order_mark_and_carriage_returns("\u{FEFF}|A|B|\r\n|1|2|\r\n")]
#[case::line_feeds("alpha\nbeta\n")]
#[case::empty("")]
#[case::lone_carriage_return("alpha\rbeta\n")]
fn parse_then_render_round_trips(#[case] content: &str) {
let document = SourceDocument::parse(content);
assert_eq!(
document.render(&lines(document.body())),
content,
"a document whose endings are already uniform must be byte-identical"
);
}
#[rstest]
#[case::tie_prefers_line_feeds("alpha\r\nbeta\n", "alpha\nbeta\n")]
#[case::missing_trailing_newline("alpha\nbeta", "alpha\nbeta\n")]
fn parse_then_render_normalizes(#[case] content: &str, #[case] expected: &str) {
let document = SourceDocument::parse(content);
assert_eq!(document.render(&lines(document.body())), expected);
}
}