1use atelier_sdk_diff::{Delta, DeltaKind, Diff, LineKind};
2
3#[must_use]
6pub fn render_diff(diff: &Diff) -> Vec<String> {
7 diff.deltas.iter().flat_map(render_delta).collect()
8}
9
10fn render_delta(delta: &Delta) -> Vec<String> {
15 let mut rendered = vec![printable(&format!(
16 "{} {}",
17 delta_label(delta.kind),
18 delta.address.as_str()
19 ))];
20 if let Some(summary) = &delta.summary {
21 rendered.push(format!(" {}", printable(summary)));
22 }
23 for line in &delta.lines {
24 match line.kind {
25 LineKind::Removed => rendered.push(format!("-{}", printable(&line.text))),
26 LineKind::Added => rendered.push(format!("+{}", printable(&line.text))),
27 LineKind::NoNewline => rendered.push(line.text.clone()),
30 }
31 }
32 rendered
33}
34
35#[must_use]
41pub fn printable(text: &str) -> String {
42 let mut out = String::with_capacity(text.len());
43 for c in text.chars() {
44 if c == '\\' {
47 out.push_str("\\\\");
48 } else if (c.is_control() && c != '\t') || is_bidi_control(c) {
49 out.extend(c.escape_debug());
50 } else {
51 out.push(c);
52 }
53 }
54 out
55}
56
57fn is_bidi_control(c: char) -> bool {
58 matches!(
59 c,
60 '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
61 )
62}
63
64fn delta_label(kind: DeltaKind) -> &'static str {
65 match kind {
66 DeltaKind::Added => "A",
67 DeltaKind::Removed => "D",
68 DeltaKind::Changed | DeltaKind::Moved => "M",
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use atelier_sdk_diff::{Address, Delta, DeltaKind, Fidelity};
75
76 use super::{printable, render_delta};
77
78 #[test]
79 fn delta_addresses_print_escaped_like_line_contents() {
80 let delta = Delta {
81 address: Address::new("x\n+forged.txt\u{1b}[31m"),
82 kind: DeltaKind::Changed,
83 fidelity: Fidelity::Binary,
84 before: Some("id1".to_owned()),
85 after: Some("id2".to_owned()),
86 lines: Vec::new(),
87 package: None,
88 summary: None,
89 };
90
91 assert_eq!(
92 render_delta(&delta),
93 vec!["M x\\n+forged.txt\\u{1b}[31m".to_owned()]
94 );
95 }
96
97 #[test]
98 fn literal_backslashes_escape_so_spelled_escapes_stay_distinct() {
99 assert_eq!(printable("literal \\n"), "literal \\\\n");
100 assert_eq!(printable("actual \n"), "actual \\n");
101 assert_ne!(printable("literal \\n"), printable("actual \n"));
102 }
103
104 #[test]
105 fn bidi_formatting_characters_print_escaped() {
106 assert_eq!(printable("user\u{202e}txt.exe"), "user\\u{202e}txt.exe");
107 assert_eq!(printable("a\u{2066}b\u{2069}c"), "a\\u{2066}b\\u{2069}c");
108 }
109
110 #[test]
111 fn control_characters_print_escaped_but_tabs_stay_literal() {
112 assert_eq!(
113 printable("red \u{1b}[31mnow\r\u{8}"),
114 "red \\u{1b}[31mnow\\r\\u{8}"
115 );
116 assert_eq!(printable("a\tb"), "a\tb");
117 assert_eq!(printable("plain text"), "plain text");
118 }
119}