1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Transforms run once for each defined dotfile during the deploy process.
//!
//! They can either be specified for a whole profile, in which case each dotfile
//! is transformed by them or they can be attached to a specific dotfile.
//!
//! The transformation takes place after the template resolving and takes the
//! contents in a textual representation. After processing the text a new text
//! must be returned.

use std::fmt;

use color_eyre::Result;

/// A transform takes the contents of a dotfile, processes it and returns a new
/// version of the content.
///
/// The dotfile is either the text of a resolved template or a non-template
/// dotfile.
pub trait Transform {
	/// Takes a string as input, processes it and returns a new version of it.
	///
	/// # Errors
	///
	/// If any error occurs during the processing it can be returned.
	fn transform(&self, content: String) -> Result<String>;
}

/// List of all available [`Transform`s](`crate::profile::transform::Transform`).
///
/// These can be added to a [`Profile`](`crate::profile::Profile`) or a
/// [`Dotfile`](`crate::profile::dotfile::Dotfile`) to modify the text content.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ContentTransformer {
	/// Transformer which replaces line termination characters with either unix
	/// style (`\n`) or windows style (`\r\b`).
	LineTerminator(LineTerminator),
}

impl Transform for ContentTransformer {
	fn transform(&self, content: String) -> Result<String> {
		match self {
			Self::LineTerminator(lt) => lt.transform(content),
		}
	}
}

impl fmt::Display for ContentTransformer {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
		fmt::Display::fmt(&self, f)
	}
}

/// Transformer which replaces line termination characters with either unix
/// style (`\n`) or windows style (`\r\b`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum LineTerminator {
	/// Replaces all occurrences of `\r\n` with `\n` (unix style).
	LF,

	/// Replaces all occurrences of `\n` with `\r\n` (windows style).
	CRLF,
}

impl Transform for LineTerminator {
	fn transform(&self, mut content: String) -> Result<String> {
		match self {
			Self::LF => Ok(content.replace("\r\n", "\n")),
			Self::CRLF => {
				let lf_idxs = content.match_indices('\n');
				let mut cr_idxs = content.match_indices('\r').peekable();

				// Allowed as it not needless here, the index iterator have a immutable ref
				// and are still alive when the string gets modified. To "unborrow" the
				// collect is necessary.
				#[allow(clippy::needless_collect)]
				let lf_idxs = lf_idxs
					.filter_map(|(lf_idx, _)| {
						while matches!(cr_idxs.peek(), Some((cr_idx,_)) if cr_idx + 1 < lf_idx) {
							// pop standalone `\r`
							let _ = cr_idxs.next().expect("Failed to advance peeked iterator");
						}

						if matches!(cr_idxs.peek(), Some((cr_idx, _)) if cr_idx + 1 == lf_idx) {
							// pop matched cr_idx
							let _ = cr_idxs.next().expect("Failed to advance peeked iterator");
							None
						} else {
							Some(lf_idx)
						}
					})
					.collect::<Vec<_>>();

				for (offset, lf_idx) in lf_idxs.into_iter().enumerate() {
					content.insert(lf_idx + offset, '\r');
				}

				Ok(content)
			}
		}
	}
}

impl fmt::Display for LineTerminator {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
		fmt::Debug::fmt(&self, f)
	}
}

#[cfg(test)]
mod tests {
	use pretty_assertions::assert_eq;

	use super::*;

	#[test]
	fn line_terminator_lf() -> Result<()> {
		const CONTENT: &str = "Hello\r\nWorld\nHow\nare\r\nyou today?\r\r\r\nLast line\r\\n";

		assert_eq!(
			LineTerminator::LF.transform(String::from(CONTENT))?,
			"Hello\nWorld\nHow\nare\nyou today?\r\r\nLast line\r\\n"
		);

		Ok(())
	}

	#[test]
	fn line_terminator_crlf() -> Result<()> {
		const CONTENT: &str = "Hello\r\nWorld\nHow\nare\r\nyou today?\r\r\r\nLast line\r\\n";

		assert_eq!(
			LineTerminator::CRLF.transform(String::from(CONTENT))?,
			"Hello\r\nWorld\r\nHow\r\nare\r\nyou today?\r\r\r\nLast line\r\\n"
		);

		Ok(())
	}
}