renderdag/
ascii.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::marker::PhantomData;
9
10use itertools::Itertools;
11
12use super::output::OutputRendererOptions;
13use super::render::Ancestor;
14use super::render::GraphRow;
15use super::render::LinkLine;
16use super::render::NodeLine;
17use super::render::PadLine;
18use super::render::Renderer;
19
20pub struct AsciiRenderer<N, R>
21where
22    R: Renderer<N, Output = GraphRow<N>> + Sized,
23{
24    inner: R,
25    options: OutputRendererOptions,
26    extra_pad_line: Option<String>,
27    _phantom: PhantomData<N>,
28}
29
30impl<N, R> AsciiRenderer<N, R>
31where
32    R: Renderer<N, Output = GraphRow<N>> + Sized,
33{
34    pub(crate) fn new(inner: R, options: OutputRendererOptions) -> Self {
35        AsciiRenderer {
36            inner,
37            options,
38            extra_pad_line: None,
39            _phantom: PhantomData,
40        }
41    }
42}
43
44impl<N, R> Renderer<N> for AsciiRenderer<N, R>
45where
46    N: Clone + Eq,
47    R: Renderer<N, Output = GraphRow<N>> + Sized,
48{
49    type Output = String;
50
51    fn width(&self, node: Option<&N>, parents: Option<&Vec<Ancestor<N>>>) -> u64 {
52        self.inner
53            .width(node, parents)
54            .saturating_mul(2)
55            .saturating_add(1)
56    }
57
58    fn reserve(&mut self, node: N) {
59        self.inner.reserve(node);
60    }
61
62    fn next_row(
63        &mut self,
64        node: N,
65        parents: Vec<Ancestor<N>>,
66        glyph: String,
67        message: String,
68    ) -> String {
69        let line = self.inner.next_row(node, parents, glyph, message);
70        let mut out = String::new();
71        let mut message_lines = line
72            .message
73            .lines()
74            .pad_using(self.options.min_row_height, |_| "");
75        let mut need_extra_pad_line = false;
76
77        // Render the previous extra pad line
78        if let Some(extra_pad_line) = self.extra_pad_line.take() {
79            out.push_str(extra_pad_line.trim_end());
80            out.push('\n');
81        }
82
83        // Render the nodeline
84        let mut node_line = String::new();
85        for entry in line.node_line.iter() {
86            match entry {
87                NodeLine::Node => {
88                    node_line.push_str(&line.glyph);
89                    node_line.push(' ');
90                }
91                NodeLine::Parent => node_line.push_str("| "),
92                NodeLine::Ancestor => node_line.push_str(". "),
93                NodeLine::Blank => node_line.push_str("  "),
94            }
95        }
96        if let Some(msg) = message_lines.next() {
97            node_line.push(' ');
98            node_line.push_str(msg);
99        }
100        out.push_str(node_line.trim_end());
101        out.push('\n');
102
103        // Render the link line
104        if let Some(link_row) = line.link_line {
105            let mut link_line = String::new();
106            let any_horizontal = link_row
107                .iter()
108                .any(|cur| cur.intersects(LinkLine::HORIZONTAL));
109            for (cur, next) in link_row
110                .iter()
111                .chain(Some(LinkLine::empty()).iter())
112                .tuple_windows()
113            {
114                // Draw the parent/ancestor line.
115                if cur.intersects(LinkLine::HORIZONTAL) {
116                    if cur.intersects(LinkLine::CHILD | LinkLine::ANY_FORK_OR_MERGE) {
117                        link_line.push('+');
118                    } else {
119                        link_line.push('-');
120                    }
121                } else if cur.intersects(LinkLine::VERTICAL) {
122                    if cur.intersects(LinkLine::ANY_FORK_OR_MERGE) && any_horizontal {
123                        link_line.push('+');
124                    } else if cur.intersects(LinkLine::VERT_PARENT) {
125                        link_line.push('|');
126                    } else {
127                        link_line.push('.');
128                    }
129                } else if cur.intersects(LinkLine::ANY_MERGE) && any_horizontal {
130                    link_line.push('\'');
131                } else if cur.intersects(LinkLine::ANY_FORK) && any_horizontal {
132                    link_line.push('.');
133                } else {
134                    link_line.push(' ');
135                }
136
137                // Draw the connecting line.
138                if cur.intersects(LinkLine::HORIZONTAL) {
139                    link_line.push('-');
140                } else if cur.intersects(LinkLine::RIGHT_MERGE) {
141                    if next.intersects(LinkLine::LEFT_FORK) && !any_horizontal {
142                        link_line.push('\\');
143                    } else {
144                        link_line.push('-');
145                    }
146                } else if cur.intersects(LinkLine::RIGHT_FORK) {
147                    if next.intersects(LinkLine::LEFT_MERGE) && !any_horizontal {
148                        link_line.push('/');
149                    } else {
150                        link_line.push('-');
151                    }
152                } else {
153                    link_line.push(' ');
154                }
155            }
156            if let Some(msg) = message_lines.next() {
157                link_line.push(' ');
158                link_line.push_str(msg);
159            }
160            out.push_str(link_line.trim_end());
161            out.push('\n');
162        }
163
164        // Render the term line
165        if let Some(term_row) = line.term_line {
166            let term_strs = ["| ", "~ "];
167            for term_str in term_strs.iter() {
168                let mut term_line = String::new();
169                for (i, term) in term_row.iter().enumerate() {
170                    if *term {
171                        term_line.push_str(term_str);
172                    } else {
173                        term_line.push_str(match line.pad_lines[i] {
174                            PadLine::Parent => "| ",
175                            PadLine::Ancestor => ". ",
176                            PadLine::Blank => "  ",
177                        });
178                    }
179                }
180                if let Some(msg) = message_lines.next() {
181                    term_line.push(' ');
182                    term_line.push_str(msg);
183                }
184                out.push_str(term_line.trim_end());
185                out.push('\n');
186            }
187            need_extra_pad_line = true;
188        }
189
190        let mut base_pad_line = String::new();
191        for entry in line.pad_lines.iter() {
192            base_pad_line.push_str(match entry {
193                PadLine::Parent => "| ",
194                PadLine::Ancestor => ". ",
195                PadLine::Blank => "  ",
196            });
197        }
198
199        // Render any pad lines
200        for msg in message_lines {
201            let mut pad_line = base_pad_line.clone();
202            pad_line.push(' ');
203            pad_line.push_str(msg);
204            out.push_str(pad_line.trim_end());
205            out.push('\n');
206            need_extra_pad_line = false;
207        }
208
209        if need_extra_pad_line {
210            self.extra_pad_line = Some(base_pad_line);
211        }
212
213        out
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::super::test_fixtures;
220    use super::super::test_fixtures::TestFixture;
221    use super::super::test_utils::render_string;
222    use crate::GraphRowRenderer;
223
224    fn render(fixture: &TestFixture) -> String {
225        let mut renderer = GraphRowRenderer::new().output().build_ascii();
226        render_string(fixture, &mut renderer)
227    }
228
229    #[test]
230    fn basic() {
231        assert_eq!(
232            render(&test_fixtures::BASIC),
233            r#"
234            o  C
235            |
236            o  B
237            |
238            o  A"#
239        );
240    }
241
242    #[test]
243    fn branches_and_merges() {
244        assert_eq!(
245            render(&test_fixtures::BRANCHES_AND_MERGES),
246            r#"
247            o  W
248            |
249            o    V
250            |\
251            | o    U
252            | |\
253            | | o  T
254            | | |
255            | o |  S
256            |   |
257            o   |  R
258            |   |
259            o   |  Q
260            |\  |
261            | o |    P
262            | +---.
263            | | | o  O
264            | | | |
265            | | | o    N
266            | | | |\
267            | o | | |  M
268            | | | | |
269            | o | | |  L
270            | | | | |
271            o | | | |  K
272            +-------'
273            o | | |  J
274            | | | |
275            o | | |  I
276            |/  | |
277            o   | |  H
278            |   | |
279            o   | |  G
280            +-----+
281            |   | o  F
282            |   |/
283            |   o  E
284            |   |
285            o   |  D
286            |   |
287            o   |  C
288            +---'
289            o  B
290            |
291            o  A"#
292        );
293    }
294
295    #[test]
296    fn octopus_branch_and_merge() {
297        assert_eq!(
298            render(&test_fixtures::OCTOPUS_BRANCH_AND_MERGE),
299            r#"
300            o      J
301            +-+-.
302            | | o  I
303            | | |
304            | o |      H
305            +-+-+-+-.
306            | | | | o  G
307            | | | | |
308            | | | o |  E
309            | | | |/
310            | | o |  D
311            | | |\|
312            | o | |  C
313            | +---'
314            o | |  F
315            |/  |
316            o   |  B
317            +---'
318            o  A"#
319        );
320    }
321
322    #[test]
323    fn reserved_column() {
324        assert_eq!(
325            render(&test_fixtures::RESERVED_COLUMN),
326            r#"
327              o  Z
328              |
329              o  Y
330              |
331              o  X
332             /
333            | o  W
334            |/
335            o  G
336            |
337            o    F
338            |\
339            | o  E
340            | |
341            | o  D
342            |
343            o  C
344            |
345            o  B
346            |
347            o  A"#
348        );
349    }
350
351    #[test]
352    fn ancestors() {
353        assert_eq!(
354            render(&test_fixtures::ANCESTORS),
355            r#"
356              o  Z
357              |
358              o  Y
359             /
360            o  F
361            .
362            . o  X
363            ./
364            | o  W
365            |/
366            o  E
367            .
368            o    D
369            |\
370            | o  C
371            | .
372            o .  B
373            |/
374            o  A"#
375        );
376    }
377
378    #[test]
379    fn split_parents() {
380        assert_eq!(
381            render(&test_fixtures::SPLIT_PARENTS),
382            r#"
383                  o  E
384            .-+-+-+
385            . o | .  D
386            ./ \| .
387            |   o .  C
388            |   |/
389            o   |  B
390            +---'
391            o  A"#
392        );
393    }
394
395    #[test]
396    fn terminations() {
397        assert_eq!(
398            render(&test_fixtures::TERMINATIONS),
399            r#"
400              o  K
401              |
402              | o  J
403              |/
404              o    I
405             /|\
406            | | |
407            | ~ |
408            |   |
409            o   |  E
410            |   |
411            |   o  H
412            +---'
413            o  D
414            |
415            ~
416            
417            o  C
418            |
419            o  B
420            |
421            ~"#
422        );
423    }
424
425    #[test]
426    fn long_messages() {
427        assert_eq!(
428            render(&test_fixtures::LONG_MESSAGES),
429            r#"
430            o      F
431            +-+-.  very long message 1
432            | | |  very long message 2
433            | | ~  very long message 3
434            | |
435            | |    very long message 4
436            | |    very long message 5
437            | |    very long message 6
438            | |
439            | o  E
440            | |
441            | o  D
442            | |
443            o |  C
444            |/   long message 1
445            |    long message 2
446            |    long message 3
447            |
448            o  B
449            |
450            o  A
451            |  long message 1
452            ~  long message 2
453               long message 3"#
454        );
455    }
456}