Skip to main content

atelier_sdk/
render.rs

1use atelier_sdk_diff::{Delta, DeltaKind, Diff, LineKind};
2
3/// The diff as printable lines: every face renders the same comparison
4/// through this one path (ADR-0006).
5#[must_use]
6pub fn render_diff(diff: &Diff) -> Vec<String> {
7    diff.deltas.iter().flat_map(render_delta).collect()
8}
9
10/// The delta's listing line; a rich delta's summary — the difference in
11/// the format's own terms — indents under it, and a text-rung line
12/// comparison follows. The two-space indent cannot collide with content
13/// lines (always signed) or the no-newline marker (starts with `\`).
14fn 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            // The synthetic marker prints bare, as git does — content
28            // lines always carry a sign, so the two can never collide.
29            LineKind::NoNewline => rendered.push(line.text.clone()),
30        }
31    }
32    rendered
33}
34
35/// The line with control characters escaped, so a diffed document cannot
36/// inject escape sequences into the terminal that reads it; tabs stay
37/// literal. Bidi formatting characters escape too — they are not
38/// `char::is_control`, but they can visually reorder or conceal diff
39/// content.
40#[must_use]
41pub fn printable(text: &str) -> String {
42    let mut out = String::with_capacity(text.len());
43    for c in text.chars() {
44        // Literal backslashes escape first, so text that merely spells an
45        // escape sequence never renders like a real control character.
46        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}