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