Skip to main content

ephemeris_documents/
line_ending.rs

1//! Line ending settings, detection and conversion.
2
3use std::borrow::Cow;
4use std::fmt::{self, Display, Write};
5
6use facet::Facet;
7
8use crate::error::LineEndingError;
9
10/// The line ending setting of a document on the wire.
11#[derive(Debug, Clone, Copy, Default, Facet, PartialEq, Eq)]
12#[repr(u8)]
13pub enum LineEndingSpec {
14    /// Write LF whatever the current source uses.
15    #[default]
16    #[facet(rename = "lf")]
17    Lf,
18    /// Write CRLF whatever the current source uses.
19    #[facet(rename = "crlf")]
20    CrLf,
21    /// Keep the current source's ending, or LF for a new file.
22    #[facet(rename = "preserve")]
23    Preserve,
24}
25
26impl LineEndingSpec {
27    /// The line ending to write for `current`, the source when it is read.
28    ///
29    /// # Errors
30    /// Returns an error for a carriage return that is not part of a CRLF
31    /// pair. [`Self::Preserve`] also rejects a source that mixes CRLF and LF.
32    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/// The line ending an output document uses.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum LineEnding {
50    Lf,
51    CrLf,
52}
53
54/// Which line endings a source contains.
55struct Endings {
56    crlf: bool,
57    lf: bool,
58}
59
60impl Endings {
61    /// # Errors
62    /// Returns an error when the source has a `\r` that is not part of a
63    /// CRLF pair.
64    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    /// The source's line ending.
92    ///
93    /// # Errors
94    /// Returns an error when the source has a `\r` that is not part of a
95    /// CRLF pair, or a CRLF pair next to a bare `\n`.
96    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    /// Convert CRLF pairs in `text` to LF.
111    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    /// Render `document` with this line ending in one pass.
120    pub(crate) fn write(self, document: &impl Display) -> String {
121        let mut out = String::new();
122        // Writing to a String cannot fail.
123        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
135/// Turns each `\n` into `\r\n` as text is written.
136struct 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;