8.4 KB250 lines
Blame
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
8import type {Hash} from '../types';
9import type {Ancestor} from './render';
10
11import {LinkLine, NodeLine, PadLine, Renderer} from './render';
12
13/* eslint no-bitwise: 0 */
14/* Translated from fbcode/eden/scm/lib/renderdag/src/box_drawing.rs */
15
16const GLYPHYS = {
17 SPACE: ' ',
18 HORIZONTAL: '──',
19 PARENT: '│ ',
20 ANCESTOR: ': ',
21 MERGE_LEFT: '╯ ',
22 MERGE_RIGHT: '╰─',
23 MERGE_BOTH: '┴─',
24 FORK_LEFT: '╮ ',
25 FORK_RIGHT: '╭─',
26 FORK_BOTH: '┬─',
27 JOIN_LEFT: '┤ ',
28 JOIN_RIGHT: '├─',
29 JOIN_BOTH: '┼─',
30 TERMINATION: '~ ',
31};
32
33/** Render a graph to text (string) */
34export class TextRenderer {
35 private inner: Renderer;
36 private extraPadLine: string | undefined = undefined;
37
38 constructor(private config?: {debugLinkLineFromNode: boolean}) {
39 this.inner = new Renderer();
40 }
41
42 reserve(hash: Hash) {
43 this.inner.reserve(hash);
44 }
45
46 nextRow(hash: Hash, parents: Array<Ancestor>, message: string, glyph = 'o'): string {
47 const {debugLinkLineFromNode = false} = this.config ?? {};
48 const line = this.inner.nextRow(hash, parents);
49 const out: string[] = [];
50
51 let needExtraPadLine = false;
52 const messageLines = message.split('\n');
53 const messageIter = messageLines.values();
54 const pushWithMessageLine = (lineBuf: string[], msg?: string) => {
55 const msgLine = msg ?? messageIter.next()?.value;
56 if (msgLine != null) {
57 lineBuf.push(' ');
58 lineBuf.push(msgLine);
59 }
60 out.push(lineBuf.join('').trimEnd());
61 out.push('\n');
62 };
63
64 // Render the previous extra pad line.
65 if (this.extraPadLine != null) {
66 out.push(this.extraPadLine);
67 out.push('\n');
68 this.extraPadLine = undefined;
69 }
70
71 // Render the node line.
72 const outNodeLine: string[] = [];
73 line.nodeLine.forEach((entry, i) => {
74 if (debugLinkLineFromNode && i !== line.nodeColumn) {
75 outNodeLine.push(GLYPHYS.SPACE);
76 } else if (entry === NodeLine.Node) {
77 outNodeLine.push(glyph);
78 outNodeLine.push(' ');
79 } else if (entry === NodeLine.Parent) {
80 outNodeLine.push(GLYPHYS.PARENT);
81 } else if (entry === NodeLine.Ancestor) {
82 outNodeLine.push(GLYPHYS.ANCESTOR);
83 } else if (entry === NodeLine.Blank) {
84 outNodeLine.push(GLYPHYS.SPACE);
85 }
86 });
87 pushWithMessageLine(outNodeLine);
88
89 // Render the top pad lines.
90 const outTopPadLine: string[] = [];
91 line.postNodeLine.forEach((entry, i) => {
92 if (debugLinkLineFromNode && i !== line.nodeColumn) {
93 outTopPadLine.push(GLYPHYS.SPACE);
94 } else {
95 outTopPadLine.push(toGlyph(entry));
96 }
97 });
98 pushWithMessageLine(outTopPadLine, debugLinkLineFromNode ? '# top pad' : undefined);
99
100 // Render the link line.
101 const linkLine = debugLinkLineFromNode ? line.linkLineFromNode : line.linkLine;
102 if (linkLine != null) {
103 const outLinkLine = [];
104 for (const cur of linkLine) {
105 if (cur.intersects(LinkLine.HORIZONTAL)) {
106 if (cur.intersects(LinkLine.CHILD)) {
107 outLinkLine.push(GLYPHYS.JOIN_BOTH);
108 } else if (cur.intersects(LinkLine.ANY_FORK) && cur.intersects(LinkLine.ANY_MERGE)) {
109 outLinkLine.push(GLYPHYS.JOIN_BOTH);
110 } else if (
111 cur.intersects(LinkLine.ANY_FORK) &&
112 cur.intersects(LinkLine.VERT_PARENT) &&
113 !line.merge
114 ) {
115 outLinkLine.push(GLYPHYS.JOIN_BOTH);
116 } else if (cur.intersects(LinkLine.ANY_FORK)) {
117 outLinkLine.push(GLYPHYS.FORK_BOTH);
118 } else if (cur.intersects(LinkLine.ANY_MERGE)) {
119 outLinkLine.push(GLYPHYS.MERGE_BOTH);
120 } else {
121 outLinkLine.push(GLYPHYS.HORIZONTAL);
122 }
123 } else if (cur.intersects(LinkLine.VERT_PARENT) && !line.merge) {
124 const left = cur.intersects(LinkLine.LEFT_MERGE | LinkLine.LEFT_FORK);
125 const right = cur.intersects(LinkLine.RIGHT_MERGE | LinkLine.RIGHT_FORK);
126 if (left && right) {
127 outLinkLine.push(GLYPHYS.JOIN_BOTH);
128 } else if (left) {
129 outLinkLine.push(GLYPHYS.JOIN_LEFT);
130 } else if (right) {
131 outLinkLine.push(GLYPHYS.JOIN_RIGHT);
132 } else {
133 outLinkLine.push(GLYPHYS.PARENT);
134 }
135 } else if (
136 cur.intersects(LinkLine.VERT_PARENT | LinkLine.VERT_ANCESTOR) &&
137 !cur.intersects(LinkLine.LEFT_FORK | LinkLine.RIGHT_FORK)
138 ) {
139 const left = cur.intersects(LinkLine.LEFT_MERGE);
140 const right = cur.intersects(LinkLine.RIGHT_MERGE);
141 if (left && right) {
142 outLinkLine.push(GLYPHYS.JOIN_BOTH);
143 } else if (left) {
144 outLinkLine.push(GLYPHYS.JOIN_LEFT);
145 } else if (right) {
146 outLinkLine.push(GLYPHYS.JOIN_RIGHT);
147 } else if (cur.intersects(LinkLine.VERT_ANCESTOR)) {
148 outLinkLine.push(GLYPHYS.ANCESTOR);
149 } else {
150 outLinkLine.push(GLYPHYS.PARENT);
151 }
152 } else if (
153 cur.intersects(LinkLine.LEFT_FORK) &&
154 cur.intersects(LinkLine.LEFT_MERGE | LinkLine.CHILD)
155 ) {
156 outLinkLine.push(GLYPHYS.JOIN_LEFT);
157 } else if (
158 cur.intersects(LinkLine.RIGHT_FORK) &&
159 cur.intersects(LinkLine.RIGHT_MERGE | LinkLine.CHILD)
160 ) {
161 outLinkLine.push(GLYPHYS.JOIN_RIGHT);
162 } else if (cur.intersects(LinkLine.LEFT_MERGE) && cur.intersects(LinkLine.RIGHT_MERGE)) {
163 outLinkLine.push(GLYPHYS.MERGE_BOTH);
164 } else if (cur.intersects(LinkLine.LEFT_FORK) && cur.intersects(LinkLine.RIGHT_FORK)) {
165 outLinkLine.push(GLYPHYS.FORK_BOTH);
166 } else if (cur.intersects(LinkLine.LEFT_FORK)) {
167 outLinkLine.push(GLYPHYS.FORK_LEFT);
168 } else if (cur.intersects(LinkLine.LEFT_MERGE)) {
169 outLinkLine.push(GLYPHYS.MERGE_LEFT);
170 } else if (cur.intersects(LinkLine.RIGHT_FORK)) {
171 outLinkLine.push(GLYPHYS.FORK_RIGHT);
172 } else if (cur.intersects(LinkLine.RIGHT_MERGE)) {
173 outLinkLine.push(GLYPHYS.MERGE_RIGHT);
174 } else {
175 outLinkLine.push(GLYPHYS.SPACE);
176 }
177 }
178 pushWithMessageLine(outLinkLine, debugLinkLineFromNode ? '# link line' : undefined);
179 }
180
181 // Patch the "padLines" for debugLinkLineFromNode.
182 const {ancestryLine: padLines} = line;
183 if (debugLinkLineFromNode) {
184 padLines.forEach((padLine, i) => {
185 if (!line.parentColumns.includes(i)) {
186 padLines[i] = PadLine.Blank;
187 }
188 });
189 }
190
191 // Render the term lines.
192 // For each column, if terminated, use "-" "~". Otherwise, use the pad line.
193 const termLine = line.termLine;
194 if (termLine != null) {
195 const termStrs = [GLYPHYS.PARENT, GLYPHYS.TERMINATION];
196 termStrs.forEach(termStr => {
197 const termLineOut: string[] = [];
198 termLine.forEach((term, i) => {
199 if (term) {
200 termLineOut.push(termStr);
201 } else {
202 termLineOut.push(toGlyph(padLines.at(i)));
203 }
204 });
205 pushWithMessageLine(termLineOut, debugLinkLineFromNode ? '# term line' : undefined);
206 });
207 needExtraPadLine = true;
208 }
209
210 // Render the pad lines for long messages.
211 // basePadLine is the pad line columns, without text messages.
212 const basePadLine: string[] = [];
213 for (const entry of padLines) {
214 basePadLine.push(toGlyph(entry));
215 }
216
217 if (debugLinkLineFromNode) {
218 // For debugLinkLineFromNode, show the pad line for investigation.
219 pushWithMessageLine([...basePadLine], '# pad line');
220 out.push('-'.repeat(20) + '\n');
221 } else {
222 const messageRest = [...messageIter];
223 if (messageRest.length === 0 && basePadLine.includes(toGlyph(PadLine.Ancestor))) {
224 messageRest.push('');
225 }
226 for (const msg of messageRest) {
227 const padLine: string[] = [...basePadLine];
228 pushWithMessageLine(padLine, msg);
229 needExtraPadLine = false;
230 }
231 }
232
233 if (needExtraPadLine) {
234 this.extraPadLine = basePadLine.join('').trimEnd();
235 }
236
237 return out.join('');
238 }
239}
240
241function toGlyph(pad?: PadLine): string {
242 if (pad === PadLine.Parent) {
243 return GLYPHYS.PARENT;
244 } else if (pad === PadLine.Ancestor) {
245 return GLYPHYS.ANCESTOR;
246 } else {
247 return GLYPHYS.SPACE;
248 }
249}
250