Skip to content

Commit c0785d8

Browse files
chore: v9.0.0 release
1 parent 060b73e commit c0785d8

File tree

3 files changed

+334
-0
lines changed

3 files changed

+334
-0
lines changed

build/lib/index.d.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export interface UIOptions {
2+
width: number;
3+
wrap?: boolean;
4+
rows?: string[];
5+
}
6+
interface Column {
7+
text: string;
8+
width?: number;
9+
align?: 'right' | 'left' | 'center';
10+
padding: number[];
11+
border?: boolean;
12+
}
13+
interface ColumnArray extends Array<Column> {
14+
span: boolean;
15+
}
16+
interface Line {
17+
hidden?: boolean;
18+
text: string;
19+
span?: boolean;
20+
}
21+
interface Mixin {
22+
stringWidth: Function;
23+
stripAnsi: Function;
24+
wrap: Function;
25+
}
26+
export declare class UI {
27+
width: number;
28+
wrap: boolean;
29+
rows: ColumnArray[];
30+
constructor(opts: UIOptions);
31+
span(...args: ColumnArray): void;
32+
resetOutput(): void;
33+
div(...args: (Column | string)[]): ColumnArray;
34+
private shouldApplyLayoutDSL;
35+
private applyLayoutDSL;
36+
private colFromString;
37+
private measurePadding;
38+
toString(): string;
39+
rowToString(row: ColumnArray, lines: Line[]): Line[];
40+
private renderInline;
41+
private rasterize;
42+
private negatePadding;
43+
private columnWidths;
44+
}
45+
export declare function cliui(opts: Partial<UIOptions>, _mixin: Mixin): UI;
46+
export {};

build/lib/index.js

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
'use strict';
2+
const align = {
3+
right: alignRight,
4+
center: alignCenter
5+
};
6+
const top = 0;
7+
const right = 1;
8+
const bottom = 2;
9+
const left = 3;
10+
export class UI {
11+
constructor(opts) {
12+
var _a;
13+
this.width = opts.width;
14+
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
15+
this.rows = [];
16+
}
17+
span(...args) {
18+
const cols = this.div(...args);
19+
cols.span = true;
20+
}
21+
resetOutput() {
22+
this.rows = [];
23+
}
24+
div(...args) {
25+
if (args.length === 0) {
26+
this.div('');
27+
}
28+
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
29+
return this.applyLayoutDSL(args[0]);
30+
}
31+
const cols = args.map(arg => {
32+
if (typeof arg === 'string') {
33+
return this.colFromString(arg);
34+
}
35+
return arg;
36+
});
37+
this.rows.push(cols);
38+
return cols;
39+
}
40+
shouldApplyLayoutDSL(...args) {
41+
return args.length === 1 && typeof args[0] === 'string' &&
42+
/[\t\n]/.test(args[0]);
43+
}
44+
applyLayoutDSL(str) {
45+
const rows = str.split('\n').map(row => row.split('\t'));
46+
let leftColumnWidth = 0;
47+
// simple heuristic for layout, make sure the
48+
// second column lines up along the left-hand.
49+
// don't allow the first column to take up more
50+
// than 50% of the screen.
51+
rows.forEach(columns => {
52+
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
53+
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
54+
}
55+
});
56+
// generate a table:
57+
// replacing ' ' with padding calculations.
58+
// using the algorithmically generated width.
59+
rows.forEach(columns => {
60+
this.div(...columns.map((r, i) => {
61+
return {
62+
text: r.trim(),
63+
padding: this.measurePadding(r),
64+
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
65+
};
66+
}));
67+
});
68+
return this.rows[this.rows.length - 1];
69+
}
70+
colFromString(text) {
71+
return {
72+
text,
73+
padding: this.measurePadding(text)
74+
};
75+
}
76+
measurePadding(str) {
77+
// measure padding without ansi escape codes
78+
const noAnsi = mixin.stripAnsi(str);
79+
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
80+
}
81+
toString() {
82+
const lines = [];
83+
this.rows.forEach(row => {
84+
this.rowToString(row, lines);
85+
});
86+
// don't display any lines with the
87+
// hidden flag set.
88+
return lines
89+
.filter(line => !line.hidden)
90+
.map(line => line.text)
91+
.join('\n');
92+
}
93+
rowToString(row, lines) {
94+
this.rasterize(row).forEach((rrow, r) => {
95+
let str = '';
96+
rrow.forEach((col, c) => {
97+
const { width } = row[c]; // the width with padding.
98+
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
99+
let ts = col; // temporary string used during alignment/padding.
100+
if (wrapWidth > mixin.stringWidth(col)) {
101+
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
102+
}
103+
// align the string within its column.
104+
if (row[c].align && row[c].align !== 'left' && this.wrap) {
105+
const fn = align[row[c].align];
106+
ts = fn(ts, wrapWidth);
107+
if (mixin.stringWidth(ts) < wrapWidth) {
108+
ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1);
109+
}
110+
}
111+
// apply border and padding to string.
112+
const padding = row[c].padding || [0, 0, 0, 0];
113+
if (padding[left]) {
114+
str += ' '.repeat(padding[left]);
115+
}
116+
str += addBorder(row[c], ts, '| ');
117+
str += ts;
118+
str += addBorder(row[c], ts, ' |');
119+
if (padding[right]) {
120+
str += ' '.repeat(padding[right]);
121+
}
122+
// if prior row is span, try to render the
123+
// current row on the prior line.
124+
if (r === 0 && lines.length > 0) {
125+
str = this.renderInline(str, lines[lines.length - 1]);
126+
}
127+
});
128+
// remove trailing whitespace.
129+
lines.push({
130+
text: str.replace(/ +$/, ''),
131+
span: row.span
132+
});
133+
});
134+
return lines;
135+
}
136+
// if the full 'source' can render in
137+
// the target line, do so.
138+
renderInline(source, previousLine) {
139+
const match = source.match(/^ */);
140+
const leadingWhitespace = match ? match[0].length : 0;
141+
const target = previousLine.text;
142+
const targetTextWidth = mixin.stringWidth(target.trimRight());
143+
if (!previousLine.span) {
144+
return source;
145+
}
146+
// if we're not applying wrapping logic,
147+
// just always append to the span.
148+
if (!this.wrap) {
149+
previousLine.hidden = true;
150+
return target + source;
151+
}
152+
if (leadingWhitespace < targetTextWidth) {
153+
return source;
154+
}
155+
previousLine.hidden = true;
156+
return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft();
157+
}
158+
rasterize(row) {
159+
const rrows = [];
160+
const widths = this.columnWidths(row);
161+
let wrapped;
162+
// word wrap all columns, and create
163+
// a data-structure that is easy to rasterize.
164+
row.forEach((col, c) => {
165+
// leave room for left and right padding.
166+
col.width = widths[c];
167+
if (this.wrap) {
168+
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
169+
}
170+
else {
171+
wrapped = col.text.split('\n');
172+
}
173+
if (col.border) {
174+
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
175+
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
176+
}
177+
// add top and bottom padding.
178+
if (col.padding) {
179+
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
180+
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
181+
}
182+
wrapped.forEach((str, r) => {
183+
if (!rrows[r]) {
184+
rrows.push([]);
185+
}
186+
const rrow = rrows[r];
187+
for (let i = 0; i < c; i++) {
188+
if (rrow[i] === undefined) {
189+
rrow.push('');
190+
}
191+
}
192+
rrow.push(str);
193+
});
194+
});
195+
return rrows;
196+
}
197+
negatePadding(col) {
198+
let wrapWidth = col.width || 0;
199+
if (col.padding) {
200+
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
201+
}
202+
if (col.border) {
203+
wrapWidth -= 4;
204+
}
205+
return wrapWidth;
206+
}
207+
columnWidths(row) {
208+
if (!this.wrap) {
209+
return row.map(col => {
210+
return col.width || mixin.stringWidth(col.text);
211+
});
212+
}
213+
let unset = row.length;
214+
let remainingWidth = this.width;
215+
// column widths can be set in config.
216+
const widths = row.map(col => {
217+
if (col.width) {
218+
unset--;
219+
remainingWidth -= col.width;
220+
return col.width;
221+
}
222+
return undefined;
223+
});
224+
// any unset widths should be calculated.
225+
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
226+
return widths.map((w, i) => {
227+
if (w === undefined) {
228+
return Math.max(unsetWidth, _minWidth(row[i]));
229+
}
230+
return w;
231+
});
232+
}
233+
}
234+
function addBorder(col, ts, style) {
235+
if (col.border) {
236+
if (/[.']-+[.']/.test(ts)) {
237+
return '';
238+
}
239+
if (ts.trim().length !== 0) {
240+
return style;
241+
}
242+
return ' ';
243+
}
244+
return '';
245+
}
246+
// calculates the minimum width of
247+
// a column, based on padding preferences.
248+
function _minWidth(col) {
249+
const padding = col.padding || [];
250+
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
251+
if (col.border) {
252+
return minWidth + 4;
253+
}
254+
return minWidth;
255+
}
256+
function getWindowWidth() {
257+
/* c8 ignore next 5: depends on terminal */
258+
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
259+
return process.stdout.columns;
260+
}
261+
return 80;
262+
}
263+
function alignRight(str, width) {
264+
str = str.trim();
265+
const strWidth = mixin.stringWidth(str);
266+
if (strWidth < width) {
267+
return ' '.repeat(width - strWidth) + str;
268+
}
269+
return str;
270+
}
271+
function alignCenter(str, width) {
272+
str = str.trim();
273+
const strWidth = mixin.stringWidth(str);
274+
/* c8 ignore next 3 */
275+
if (strWidth >= width) {
276+
return str;
277+
}
278+
return ' '.repeat((width - strWidth) >> 1) + str;
279+
}
280+
let mixin;
281+
export function cliui(opts, _mixin) {
282+
mixin = _mixin;
283+
return new UI({
284+
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
285+
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
286+
});
287+
}

0 commit comments

Comments
 (0)