UNPKG

cli-box

Version:

A library to generate ASCII boxes via NodeJS

388 lines (328 loc) 14.4 kB
"use strict"; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var isWin = require("is-win"), isUndefined = require("is-undefined"), AnsiParser = require("ansi-parser"), ul = require("ul"), deffy = require("deffy"); /** * CliBox * Creates a new ASCII box. * * @name CliBox * @function * @param {Object|String} options A string representing the size: `WIDTHxHEIGHT` * (e.g. `10x20`) or an object: * * - `width` or `w` (Number): The box width. * - `height` or `h` (Number): The box height. * - `fullscreen` (Boolean): If `true`, the box will have full size * (default: `false`). * - `stringify` (Boolean): If `false` the box will not be stringified (the * `CliBox` object will be returned instead). * - `marks` (Object): An object containing mark characters. Default: * - `nw`: `"┌"` * - `n`: `"─"` * - `ne`: `"┐"` * - `e`: `"│"` * - `se`: `"┘"` * - `s`: `"─"` * - `sw`: `"└"` * - `w`: `"|"` * - `b`: `" "` * * @param {Object|String} text A string to be displayed or an object: * * - `text` (String): The text to be displayed. * - `stretch` (Boolean): Stretch box to fix text (default: `false`). * - `autoEOL` (Boolean): Break lines automatically (default: `false`). * - `hAlign` (String): Horizontal alignement (default: `"middle"`). It can * take one of the values: `"left"`, `"middle"`, `"right"`. * - `vAlign` (String): Vertical alignement (default: `"center"`). It can take * one of the values: `"top"`, `"center"`, `"bottom"`. * * @return {Object|Stringify} The `CliBox` object (if `options.stringify` is `false`) * or the stringified box. */ var CliBox = function () { function CliBox(options, text) { _classCallCheck(this, CliBox); var w = options.width || options.w, h = options.height || options.h, fullscreen = options.fullscreen || false, defaults = CliBox.defaults, lines = [], line = "", splits = null, noAnsiText = "", textOffsetY = void 0, i = void 0; if (fullscreen) { // 3 = 1 (top border) + 1 (bottom border) + 1 (bottom padding) h = process.stdout.rows - 3; w = process.stdout.columns; // Compensate for Windows bug, see node-cli-update/issue #4 if (isWin()) { w -= 1; } } else { // Handle "x" in options parameter if (typeof options === "string" && options.split("x").length === 2) { splits = options.split("x"); w = parseInt(splits[0]); h = parseInt(splits[1]); options = { marks: {} }; } } // Handle text parameter if (text) { noAnsiText = AnsiParser.removeAnsi(isUndefined(text.text) ? text : text.text); var alignTextVertically = function alignTextVertically(splits, mode) { if (splits.length > h && !mode) mode = "top"; mode = ["top", "bottom"].indexOf(mode) > -1 ? mode : "middle"; if (mode == "middle") { return Math.floor(h / 2 - splits.length / 2); } else if (mode == "top") { return 0; } else if (mode == "bottom") { return h - splits.length; } }; var alignLineHorizontally = function alignLineHorizontally(line, mode) { mode = ["left", "right"].indexOf(mode) > -1 ? mode : "center"; if (mode == "center") { line.offset.x = parseInt((w - 2) / 2 - line.text.length / 2); } else if (mode == "left") { line.offset.x = 0; } else if (mode == "right") { line.offset.x = w - 2 - line.text.length; } if (line.offset.x < 0) line.offset.x = 0; // Handle overflowing text if (AnsiParser.removeAnsi(line.text).length > w - 2) { line.text = line.text.substr(0, w - 5) + "..."; } return line; }; var escapeLine = function escapeLine(line) { var length = line.text.length, results = [], lineText = line.text, index; while ((index = lineText.indexOf("\x1B")) > -1) { results.push({ index: index, code: lineText.substr(index, lineText.indexOf("m", index) - index + 1) }); lineText = lineText.replace(/\u001b\[.*?m/, ""); line.text = lineText; } line.escapeCodes = results; return; }; // Divide text into lines and calculate position if (typeof text === "string") { splits = text.split("\n").map(function (val) { return val.trim(); }); textOffsetY = alignTextVertically(splits, options.vAlign); for (i = 0; i < splits.length; ++i) { line = { text: splits[i], offset: { y: textOffsetY + i } }; escapeLine(line); line = alignLineHorizontally(line, options.hAlign); lines.push(line); } } else if ((typeof text === "undefined" ? "undefined" : _typeof(text)) === "object") { var stretch = text.stretch || false, autoEOL = text.autoEOL || false, hAlign = text.hAlign || undefined, vAlign = text.vAlign || undefined; splits = text.text.split("\n").map(function (val) { return val.trim(); }); // Stretch box to fit text (or console) if (stretch) { var longest = AnsiParser.removeAnsi(splits.reduce(function (prev, curr) { return AnsiParser.removeAnsi(prev).length > AnsiParser.removeAnsi(curr).length ? prev : curr; })).length; if (longest > w - 2) { if (longest - 2 > process.stdout.columns) { w = process.stdout.columns; } else { w = longest + 2; } } h = splits.length > h ? splits.length : h; } // Break lines automatically if (autoEOL) { for (i = 0; i < splits.length; ++i) { var escaped = AnsiParser.removeAnsi(splits[i]); // If too long to fit if (escaped.length > w - 2) { // Find a place to break line var actualPlace = 0, outsideCode = true, escapedIndex = 0, ii; // Find possible places for line breaks in pure text ii = escaped.lastIndexOf(" ", w - 2); ii = ii == -1 ? escaped.indexOf(" ", w - 2) : ii; // Find actual index of line break while (escapedIndex != ii && actualPlace < splits[i].length) { // Omit colour codes if (splits[i][actualPlace] == "\x1B") { while (splits[i][actualPlace] != "m") { actualPlace++; } } if (splits[i][actualPlace] == escaped[escapedIndex] && outsideCode) { escapedIndex++; } actualPlace++; } // Divide line if (ii > 0 && ii < splits[i].length) { var div1 = splits[i].substr(0, actualPlace), div2 = splits[i].slice(actualPlace).trim(); // Trim whitespace after escape code if (div2[0] == "\x1B") { div2 = div2.substr(0, div2.indexOf(" ")) + div2.slice(div2.indexOf(" ") + 1); } splits.splice(i, 1, div1, div2); } } } } // Recalculate line number if necessary if (stretch) h = splits.length > h ? splits.length : h; // Get vertical text offset textOffsetY = alignTextVertically(splits, vAlign); // Push lines for (i = 0; i < splits.length; ++i) { line = { text: splits[i], offset: { y: textOffsetY + i } }; escapeLine(line); line = alignLineHorizontally(line, hAlign); lines.push(line); } } } var settings = { width: w, height: h, marks: ul.merge(options.marks, defaults.marks), lines: lines }; this.settings = settings; this.options = { width: settings.width, height: settings.height, marks: settings.marks, fullscreen: fullscreen, stringify: deffy(options.stringify, options.stringify) }; } /** * stringify * Returns the stringified box. * * @name stringify * @function * @return {String} Stringified box string. */ _createClass(CliBox, [{ key: "stringify", value: function stringify() { var box = ""; // Top box += this.settings.marks.nw; for (var i = 0; i < this.settings.width - 2; ++i) { box += this.settings.marks.n; } // Right box += this.settings.marks.ne; // The other lines var nextLine = this.settings.lines.length ? this.settings.lines.shift() : undefined, lastCode = ""; for (var _i = 0; _i < this.settings.height; ++_i) { // Get next line to display if one exists while (nextLine && _i > nextLine.offset.y && this.settings.lines.length) { nextLine = this.settings.lines.shift(); } box += "\n" + this.settings.marks.w + lastCode; for (var ii = 0; ii < this.settings.width - 2; ++ii) { // there is something to display if (nextLine // it's the correct line && _i == nextLine.offset.y // it's after the x offset && ii >= nextLine.offset.x // the text hasn't ended yet && ii < nextLine.offset.x + nextLine.text.length) { // Display escape codes while (nextLine.escapeCodes.length && ii - nextLine.offset.x == nextLine.escapeCodes[0].index) { lastCode = nextLine.escapeCodes.shift().code; box += lastCode; } box += nextLine.text[ii - nextLine.offset.x]; } else { box += this.settings.marks.b; } } // Display remaining codes while (nextLine && nextLine.escapeCodes.length && _i == nextLine.offset.y) { lastCode = nextLine.escapeCodes.shift().code; box += lastCode; } box += "\x1B[0m" + this.settings.marks.e; } // Bottom box += "\n" + this.settings.marks.sw; for (var _i2 = 0; _i2 < this.settings.width - 2; ++_i2) { box += this.settings.marks.s; } box += this.settings.marks.se; if (this.options.noAnsi) { box = AnsiParser.removeAnsi(box); } return box; } }]); return CliBox; }(); // Default settings CliBox.defaults = { marks: { nw: "┌", n: "─", ne: "┐", e: "│", se: "┘", s: "─", sw: "└", w: "|", b: " " } }; module.exports = function Box(options, text) { var box = new CliBox(options, text); options = box.options; if (options.stringify !== false) { return box.stringify(); } return box; };