cli-box
Version:
A library to generate ASCII boxes via NodeJS
388 lines (328 loc) • 14.4 kB
JavaScript
;
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;
};