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