basant307/AI_Governance_Project
048
1import process from 'node:process';2import stringWidth from 'string-width';3import chalk from 'chalk';4import widestLine from 'widest-line';5import cliBoxes from 'cli-boxes';6import camelCase from 'camelcase';7import ansiAlign from 'ansi-align';8import wrapAnsi from 'wrap-ansi';9 10const NEWLINE = '\n';11const PAD = ' ';12const NONE = 'none';13 14const terminalColumns = () => {15 const {env, stdout, stderr} = process;16 17 if (stdout?.columns) {18 return stdout.columns;19 }20 21 if (stderr?.columns) {22 return stderr.columns;23 }24 25 if (env.COLUMNS) {26 return Number.parseInt(env.COLUMNS, 10);27 }28 29 return 80;30};31 32const getObject = detail => typeof detail === 'number' ? {33 top: detail,34 right: detail * 3,35 bottom: detail,36 left: detail * 3,37} : {38 top: 0,39 right: 0,40 bottom: 0,41 left: 0,42 ...detail,43};44 45const getBorderWidth = borderStyle => borderStyle === NONE ? 0 : 2;46 47const getBorderChars = borderStyle => {48 const sides = [49 'topLeft',50 'topRight',51 'bottomRight',52 'bottomLeft',53 'left',54 'right',55 'top',56 'bottom',57 ];58 59 let characters;60 61 // Create empty border style62 if (borderStyle === NONE) {63 borderStyle = {};64 for (const side of sides) {65 borderStyle[side] = '';66 }67 }68 69 if (typeof borderStyle === 'string') {70 characters = cliBoxes[borderStyle];71 72 if (!characters) {73 throw new TypeError(`Invalid border style: ${borderStyle}`);74 }75 } else {76 // Ensure retro-compatibility77 if (typeof borderStyle?.vertical === 'string') {78 borderStyle.left = borderStyle.vertical;79 borderStyle.right = borderStyle.vertical;80 }81 82 // Ensure retro-compatibility83 if (typeof borderStyle?.horizontal === 'string') {84 borderStyle.top = borderStyle.horizontal;85 borderStyle.bottom = borderStyle.horizontal;86 }87 88 for (const side of sides) {89 if (borderStyle[side] === null || typeof borderStyle[side] !== 'string') {90 throw new TypeError(`Invalid border style: ${side}`);91 }92 }93 94 characters = borderStyle;95 }96 97 return characters;98};99 100const makeTitle = (text, horizontal, alignment) => {101 let title = '';102 103 const textWidth = stringWidth(text);104 105 switch (alignment) {106 case 'left': {107 title = text + horizontal.slice(textWidth);108 break;109 }110 111 case 'right': {112 title = horizontal.slice(textWidth) + text;113 break;114 }115 116 default: {117 horizontal = horizontal.slice(textWidth);118 119 if (horizontal.length % 2 === 1) { // This is needed in case the length is odd120 horizontal = horizontal.slice(Math.floor(horizontal.length / 2));121 title = horizontal.slice(1) + text + horizontal; // We reduce the left part of one character to avoid the bar to go beyond its limit122 } else {123 horizontal = horizontal.slice(horizontal.length / 2);124 title = horizontal + text + horizontal;125 }126 127 break;128 }129 }130 131 return title;132};133 134const makeContentText = (text, {padding, width, textAlignment, height}) => {135 text = ansiAlign(text, {align: textAlignment});136 let lines = text.split(NEWLINE);137 const textWidth = widestLine(text);138 139 const max = width - padding.left - padding.right;140 141 if (textWidth > max) {142 const newLines = [];143 for (const line of lines) {144 const createdLines = wrapAnsi(line, max, {hard: true});145 const alignedLines = ansiAlign(createdLines, {align: textAlignment});146 const alignedLinesArray = alignedLines.split('\n');147 const longestLength = Math.max(...alignedLinesArray.map(s => stringWidth(s)));148 149 for (const alignedLine of alignedLinesArray) {150 let paddedLine;151 switch (textAlignment) {152 case 'center': {153 paddedLine = PAD.repeat((max - longestLength) / 2) + alignedLine;154 break;155 }156 157 case 'right': {158 paddedLine = PAD.repeat(max - longestLength) + alignedLine;159 break;160 }161 162 default: {163 paddedLine = alignedLine;164 break;165 }166 }167 168 newLines.push(paddedLine);169 }170 }171 172 lines = newLines;173 }174 175 if (textAlignment === 'center' && textWidth < max) {176 lines = lines.map(line => PAD.repeat((max - textWidth) / 2) + line);177 } else if (textAlignment === 'right' && textWidth < max) {178 lines = lines.map(line => PAD.repeat(max - textWidth) + line);179 }180 181 const paddingLeft = PAD.repeat(padding.left);182 const paddingRight = PAD.repeat(padding.right);183 184 lines = lines.map(line => paddingLeft + line + paddingRight);185 186 lines = lines.map(line => {187 if (width - stringWidth(line) > 0) {188 switch (textAlignment) {189 case 'center': {190 return line + PAD.repeat(width - stringWidth(line));191 }192 193 case 'right': {194 return line + PAD.repeat(width - stringWidth(line));195 }196 197 default: {198 return line + PAD.repeat(width - stringWidth(line));199 }200 }201 }202 203 return line;204 });205 206 if (padding.top > 0) {207 lines = [...Array.from({length: padding.top}).fill(PAD.repeat(width)), ...lines];208 }209 210 if (padding.bottom > 0) {211 lines = [...lines, ...Array.from({length: padding.bottom}).fill(PAD.repeat(width))];212 }213 214 if (height && lines.length > height) {215 lines = lines.slice(0, height);216 } else if (height && lines.length < height) {217 lines = [...lines, ...Array.from({length: height - lines.length}).fill(PAD.repeat(width))];218 }219 220 return lines.join(NEWLINE);221};222 223const boxContent = (content, contentWidth, options) => {224 const colorizeBorder = border => {225 const newBorder = options.borderColor ? getColorFn(options.borderColor)(border) : border;226 return options.dimBorder ? chalk.dim(newBorder) : newBorder;227 };228 229 const colorizeContent = content => options.backgroundColor ? getBGColorFn(options.backgroundColor)(content) : content;230 231 const chars = getBorderChars(options.borderStyle);232 const columns = terminalColumns();233 let marginLeft = PAD.repeat(options.margin.left);234 235 if (options.float === 'center') {236 const marginWidth = Math.max((columns - contentWidth - getBorderWidth(options.borderStyle)) / 2, 0);237 marginLeft = PAD.repeat(marginWidth);238 } else if (options.float === 'right') {239 const marginWidth = Math.max(columns - contentWidth - options.margin.right - getBorderWidth(options.borderStyle), 0);240 marginLeft = PAD.repeat(marginWidth);241 }242 243 let result = '';244 245 if (options.margin.top) {246 result += NEWLINE.repeat(options.margin.top);247 }248 249 if (options.borderStyle !== NONE || options.title) {250 result += colorizeBorder(marginLeft + chars.topLeft + (options.title ? makeTitle(options.title, chars.top.repeat(contentWidth), options.titleAlignment) : chars.top.repeat(contentWidth)) + chars.topRight) + NEWLINE;251 }252 253 const lines = content.split(NEWLINE);254 255 result += lines.map(line => marginLeft + colorizeBorder(chars.left) + colorizeContent(line) + colorizeBorder(chars.right)).join(NEWLINE);256 257 if (options.borderStyle !== NONE) {258 result += NEWLINE + colorizeBorder(marginLeft + chars.bottomLeft + chars.bottom.repeat(contentWidth) + chars.bottomRight);259 }260 261 if (options.margin.bottom) {262 result += NEWLINE.repeat(options.margin.bottom);263 }264 265 return result;266};267 268const sanitizeOptions = options => {269 // If fullscreen is enabled, max-out unspecified width/height270 if (options.fullscreen && process?.stdout) {271 let newDimensions = [process.stdout.columns, process.stdout.rows];272 273 if (typeof options.fullscreen === 'function') {274 newDimensions = options.fullscreen(...newDimensions);275 }276 277 if (!options.width) {278 options.width = newDimensions[0];279 }280 281 if (!options.height) {282 options.height = newDimensions[1];283 }284 }285 286 // If width is provided, make sure it's not below 1287 if (options.width) {288 options.width = Math.max(1, options.width - getBorderWidth(options.borderStyle));289 }290 291 // If height is provided, make sure it's not below 1292 if (options.height) {293 options.height = Math.max(1, options.height - getBorderWidth(options.borderStyle));294 }295 296 return options;297};298 299const formatTitle = (title, borderStyle) => borderStyle === NONE ? title : ` ${title} `;300 301const determineDimensions = (text, options) => {302 options = sanitizeOptions(options);303 const widthOverride = options.width !== undefined;304 const columns = terminalColumns();305 const borderWidth = getBorderWidth(options.borderStyle);306 const maxWidth = columns - options.margin.left - options.margin.right - borderWidth;307 308 const widest = widestLine(wrapAnsi(text, columns - borderWidth, {hard: true, trim: false})) + options.padding.left + options.padding.right;309 310 // If title and width are provided, title adheres to fixed width311 if (options.title && widthOverride) {312 options.title = options.title.slice(0, Math.max(0, options.width - 2));313 if (options.title) {314 options.title = formatTitle(options.title, options.borderStyle);315 }316 } else if (options.title) {317 options.title = options.title.slice(0, Math.max(0, maxWidth - 2));318 319 // Recheck if title isn't empty now320 if (options.title) {321 options.title = formatTitle(options.title, options.borderStyle);322 // If the title is larger than content, box adheres to title width323 if (stringWidth(options.title) > widest) {324 options.width = stringWidth(options.title);325 }326 }327 }328 329 // If fixed width is provided, use it or content width as reference330 options.width = options.width ? options.width : widest;331 332 if (!widthOverride) {333 if ((options.margin.left && options.margin.right) && options.width > maxWidth) {334 // Let's assume we have margins: left = 3, right = 5, in total = 8335 const spaceForMargins = columns - options.width - borderWidth;336 // Let's assume we have space = 4337 const multiplier = spaceForMargins / (options.margin.left + options.margin.right);338 // Here: multiplier = 4/8 = 0.5339 options.margin.left = Math.max(0, Math.floor(options.margin.left * multiplier));340 options.margin.right = Math.max(0, Math.floor(options.margin.right * multiplier));341 // Left: 3 * 0.5 = 1.5 -> 1342 // Right: 6 * 0.5 = 3343 }344 345 // Re-cap width considering the margins after shrinking346 options.width = Math.min(options.width, columns - borderWidth - options.margin.left - options.margin.right);347 }348 349 // Prevent padding overflow350 if (options.width - (options.padding.left + options.padding.right) <= 0) {351 options.padding.left = 0;352 options.padding.right = 0;353 }354 355 if (options.height && options.height - (options.padding.top + options.padding.bottom) <= 0) {356 options.padding.top = 0;357 options.padding.bottom = 0;358 }359 360 return options;361};362 363const isHex = color => color.match(/^#(?:[0-f]{3}){1,2}$/i);364const isColorValid = color => typeof color === 'string' && (chalk[color] ?? isHex(color));365const getColorFn = color => isHex(color) ? chalk.hex(color) : chalk[color];366const getBGColorFn = color => isHex(color) ? chalk.bgHex(color) : chalk[camelCase(['bg', color])];367 368export default function boxen(text, options) {369 options = {370 padding: 0,371 borderStyle: 'single',372 dimBorder: false,373 textAlignment: 'left',374 float: 'left',375 titleAlignment: 'left',376 ...options,377 };378 379 // This option is deprecated380 if (options.align) {381 options.textAlignment = options.align;382 }383 384 if (options.borderColor && !isColorValid(options.borderColor)) {385 throw new Error(`${options.borderColor} is not a valid borderColor`);386 }387 388 if (options.backgroundColor && !isColorValid(options.backgroundColor)) {389 throw new Error(`${options.backgroundColor} is not a valid backgroundColor`);390 }391 392 options.padding = getObject(options.padding);393 options.margin = getObject(options.margin);394 395 options = determineDimensions(text, options);396 397 text = makeContentText(text, options);398 399 return boxContent(text, options.width, options);400}401 402export {default as _borderStyles} from 'cli-boxes';403 