ephemeris_documents/
line_ending.rs1use std::borrow::Cow;
4use std::fmt::{self, Display, Write};
5
6use facet::Facet;
7
8use crate::error::LineEndingError;
9
10#[derive(Debug, Clone, Copy, Default, Facet, PartialEq, Eq)]
12#[repr(u8)]
13pub enum LineEndingSpec {
14 #[default]
16 #[facet(rename = "lf")]
17 Lf,
18 #[facet(rename = "crlf")]
20 CrLf,
21 #[facet(rename = "preserve")]
23 Preserve,
24}
25
26impl LineEndingSpec {
27 pub(crate) fn resolve(self, current: Option<&str>) -> Result<LineEnding, LineEndingError> {
33 let Some(source) = current else {
34 return Ok(match self {
35 Self::CrLf => LineEnding::CrLf,
36 Self::Lf | Self::Preserve => LineEnding::Lf,
37 });
38 };
39 match self {
40 Self::Lf => Endings::scan(source).map(|_| LineEnding::Lf),
41 Self::CrLf => Endings::scan(source).map(|_| LineEnding::CrLf),
42 Self::Preserve => LineEnding::detect(source),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum LineEnding {
50 Lf,
51 CrLf,
52}
53
54struct Endings {
56 crlf: bool,
57 lf: bool,
58}
59
60impl Endings {
61 fn scan(source: &str) -> Result<Self, LineEndingError> {
65 let mut endings = Self {
66 crlf: false,
67 lf: false,
68 };
69 let mut previous = 0;
70 for &byte in source.as_bytes() {
71 if previous == b'\r' && byte != b'\n' {
72 return Err(LineEndingError::BareCarriageReturn);
73 }
74 if byte == b'\n' {
75 if previous == b'\r' {
76 endings.crlf = true;
77 } else {
78 endings.lf = true;
79 }
80 }
81 previous = byte;
82 }
83 if previous == b'\r' {
84 return Err(LineEndingError::BareCarriageReturn);
85 }
86 Ok(endings)
87 }
88}
89
90impl LineEnding {
91 fn detect(source: &str) -> Result<Self, LineEndingError> {
97 match Endings::scan(source)? {
98 Endings {
99 crlf: true,
100 lf: true,
101 } => Err(LineEndingError::Mixed),
102 Endings {
103 crlf: true,
104 lf: false,
105 } => Ok(Self::CrLf),
106 Endings { crlf: false, lf: _ } => Ok(Self::Lf),
107 }
108 }
109
110 pub(crate) fn normalise(text: &str) -> Cow<'_, str> {
112 if text.contains('\r') {
113 Cow::Owned(text.replace("\r\n", "\n"))
114 } else {
115 Cow::Borrowed(text)
116 }
117 }
118
119 pub(crate) fn write(self, document: &impl Display) -> String {
121 let mut out = String::new();
122 match self {
124 Self::Lf => {
125 let _infallible = write!(out, "{document}");
126 }
127 Self::CrLf => {
128 let _infallible = write!(CrLfWriter(&mut out), "{document}");
129 }
130 }
131 out
132 }
133}
134
135struct CrLfWriter<'a>(&'a mut String);
137
138impl Write for CrLfWriter<'_> {
139 fn write_str(&mut self, s: &str) -> fmt::Result {
140 let mut lines = s.split('\n');
141 if let Some(first) = lines.next() {
142 self.0.push_str(first);
143 }
144 for line in lines {
145 self.0.push_str("\r\n");
146 self.0.push_str(line);
147 }
148 Ok(())
149 }
150}
151
152#[cfg(test)]
153mod tests;