| 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 | |
| 8 | import type {Hash} from '../types'; |
| 9 | import type {Ancestor} from './render'; |
| 10 | |
| 11 | import {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 | |
| 16 | const 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) */ |
| 34 | export 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 | |
| 241 | function 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 | |