├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── Readme.md ├── bench ├── index.js └── large.json ├── index.js ├── package-lock.json ├── package.json ├── test ├── 1-column-simple-expected.txt ├── 1-column-simple.js ├── 2-column-simple-expected.txt ├── 2-column-simple.js ├── align-center-expected.txt ├── align-center.js ├── align-right-expected.txt ├── align-right.js ├── ansi-expected.txt ├── ansi.js ├── auto-columns-expected.txt ├── auto-columns.js ├── column-splitter-character-expected.txt ├── column-splitter-character.js ├── data-transform-expected.txt ├── data-transform.js ├── define-columns.js ├── existing-linebreaks-expected.txt ├── existing-linebreaks.js ├── headings.js ├── hide-individual-header-expected.txt ├── hide-individual-header.js ├── padCenter.js ├── padRight.js ├── remove-existing-linebreaks-expected.txt ├── show-headers-expected.txt ├── show-headers.js ├── splitintolines.js ├── splitlongwords.js ├── truncate-column-expected.txt ├── truncate-column.js ├── truncate-expected.txt ├── truncate-multichar-expected.txt ├── truncate-string-maxlinewidth-expected.txt ├── truncate-string-maxlinewidth.js ├── truncate-with-padding-expected.txt ├── truncate-with-padding.js ├── truncate.js ├── truncatestring.js ├── truncation-character.js ├── wrap-expected.txt ├── wrap-with-padding-expected.txt ├── wrap-with-padding.js └── wrap.js ├── utils.js └── width.js /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Columnify Unit Tests 2 | 'on': 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | Build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 16.x 16 | - 14.x 17 | - 12.x 18 | - 10.x 19 | steps: 20 | - name: 'Set up Node.js ${{ matrix.node-version }}' 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: '${{ matrix.node-version }}' 24 | cache: 'npm' 25 | - uses: actions/checkout@v2 26 | - name: Install dependencies 27 | run: npm ci 28 | - name: Run Tests 29 | run: npm test 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | columnify.js 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npmignore 3 | .travis.yml 4 | test 5 | bench 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Tim Oxley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: columnify.js 3 | 4 | prepublish: all 5 | 6 | columnify.js: index.js package.json 7 | babel index.js > columnify.js 8 | 9 | .PHONY: all prepublish 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # columnify 2 | 3 | [![Columnify Unit Tests](https://github.com/timoxley/columnify/actions/workflows/test.yml/badge.svg)](https://github.com/timoxley/columnify/actions/workflows/test.yml) 4 | [![NPM Version](https://img.shields.io/npm/v/columnify.svg?style=flat)](https://npmjs.org/package/columnify) 5 | [![License](http://img.shields.io/npm/l/columnify.svg?style=flat)](LICENSE) 6 | 7 | Create text-based columns suitable for console output from objects or 8 | arrays of objects. 9 | 10 | Columns are automatically resized to fit the content of the largest 11 | cell. Each cell will be padded with spaces to fill the available space 12 | and ensure column contents are left-aligned. 13 | 14 | Designed to [handle sensible wrapping in npm search results](https://github.com/isaacs/npm/pull/2328). 15 | 16 | `npm search` before & after integrating columnify: 17 | 18 | ![npm-tidy-search](https://f.cloud.github.com/assets/43438/1848959/ae02ad04-76a1-11e3-8255-4781debffc26.gif) 19 | 20 | ## Installation 21 | 22 | ``` 23 | $ npm install columnify 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```javascript 29 | var columnify = require('columnify') 30 | var columns = columnify(data, options) 31 | console.log(columns) 32 | ``` 33 | 34 | ## Examples 35 | 36 | ### Columnify Objects 37 | 38 | Objects are converted to a list of key/value pairs: 39 | 40 | ```javascript 41 | var data = { 42 | "commander@0.6.1": 1, 43 | "minimatch@0.2.14": 3, 44 | "mkdirp@0.3.5": 2, 45 | "sigmund@1.0.0": 3 46 | } 47 | 48 | console.log(columnify(data)) 49 | ``` 50 | #### Output: 51 | ``` 52 | KEY VALUE 53 | commander@0.6.1 1 54 | minimatch@0.2.14 3 55 | mkdirp@0.3.5 2 56 | sigmund@1.0.0 3 57 | ``` 58 | 59 | ### Custom Column Names 60 | 61 | ```javascript 62 | var data = { 63 | "commander@0.6.1": 1, 64 | "minimatch@0.2.14": 3, 65 | "mkdirp@0.3.5": 2, 66 | "sigmund@1.0.0": 3 67 | } 68 | 69 | console.log(columnify(data, {columns: ['MODULE', 'COUNT']})) 70 | ``` 71 | #### Output: 72 | ``` 73 | MODULE COUNT 74 | commander@0.6.1 1 75 | minimatch@0.2.14 3 76 | mkdirp@0.3.5 2 77 | sigmund@1.0.0 3 78 | ``` 79 | 80 | ### Columnify Arrays of Objects 81 | 82 | Column headings are extracted from the keys in supplied objects. 83 | 84 | ```javascript 85 | var columnify = require('columnify') 86 | 87 | var columns = columnify([{ 88 | name: 'mod1', 89 | version: '0.0.1' 90 | }, { 91 | name: 'module2', 92 | version: '0.2.0' 93 | }]) 94 | 95 | console.log(columns) 96 | ``` 97 | #### Output: 98 | ``` 99 | NAME VERSION 100 | mod1 0.0.1 101 | module2 0.2.0 102 | ``` 103 | 104 | ### Filtering & Ordering Columns 105 | 106 | By default, all properties are converted into columns, whether or not 107 | they exist on every object or not. 108 | 109 | To explicitly specify which columns to include, and in which order, 110 | supply a "columns" or "include" array ("include" is just an alias). 111 | 112 | ```javascript 113 | var data = [{ 114 | name: 'module1', 115 | description: 'some description', 116 | version: '0.0.1', 117 | }, { 118 | name: 'module2', 119 | description: 'another description', 120 | version: '0.2.0', 121 | }] 122 | 123 | var columns = columnify(data, { 124 | columns: ['name', 'version'] 125 | }) 126 | 127 | console.log(columns) 128 | ``` 129 | 130 | #### Output: 131 | ``` 132 | NAME VERSION 133 | module1 0.0.1 134 | module2 0.2.0 135 | ``` 136 | 137 | ## Global and Per Column Options 138 | You can set a number of options at a global level (ie. for all columns) or on a per column basis. 139 | 140 | Set options on a per column basis by using the `config` option to specify individual columns: 141 | 142 | ```javascript 143 | var columns = columnify(data, { 144 | optionName: optionValue, 145 | config: { 146 | columnName: {optionName: optionValue}, 147 | columnName: {optionName: optionValue}, 148 | } 149 | }) 150 | ``` 151 | 152 | ### Maximum and Minimum Column Widths 153 | As with all options, you can define the `maxWidth` and `minWidth` globally, or for specified columns. By default, wrapping will happen at word boundaries. Empty cells or those which do not fill the `minWidth` will be padded with spaces. 154 | 155 | ```javascript 156 | var columns = columnify([{ 157 | name: 'mod1', 158 | description: 'some description which happens to be far larger than the max', 159 | version: '0.0.1', 160 | }, { 161 | name: 'module-two', 162 | description: 'another description larger than the max', 163 | version: '0.2.0', 164 | }], { 165 | minWidth: 20, 166 | config: { 167 | description: {maxWidth: 30} 168 | } 169 | }) 170 | 171 | console.log(columns) 172 | ``` 173 | 174 | #### Output: 175 | ``` 176 | NAME DESCRIPTION VERSION 177 | mod1 some description which happens 0.0.1 178 | to be far larger than the max 179 | module-two another description larger 0.2.0 180 | than the max 181 | ``` 182 | 183 | #### Maximum Line Width 184 | 185 | You can set a hard maximum line width using the `maxLineWidth` option. 186 | Beyond this value data is unceremoniously truncated with no truncation 187 | marker. 188 | 189 | This can either be a number or 'auto' to set the value to the width of 190 | stdout. 191 | 192 | Setting this value to 'auto' prevent TTY-imposed line-wrapping when 193 | lines exceed the screen width. 194 | 195 | #### Truncating Column Cells Instead of Wrapping 196 | 197 | You can disable wrapping and instead truncate content at the maximum 198 | column width by using the `truncate` option. Truncation respects word boundaries. A truncation marker, `…`, will appear next to the last word in any truncated line. 199 | 200 | ```javascript 201 | var columns = columnify(data, { 202 | truncate: true, 203 | config: { 204 | description: { 205 | maxWidth: 20 206 | } 207 | } 208 | }) 209 | 210 | console.log(columns) 211 | ``` 212 | #### Output: 213 | ``` 214 | NAME DESCRIPTION VERSION 215 | mod1 some description… 0.0.1 216 | module-two another description… 0.2.0 217 | ``` 218 | 219 | 220 | ### Align Right/Center 221 | You can set the alignment of the column data by using the `align` option. 222 | 223 | ```js 224 | var data = { 225 | "mocha@1.18.2": 1, 226 | "commander@2.0.0": 1, 227 | "debug@0.8.1": 1 228 | } 229 | 230 | columnify(data, {config: {value: {align: 'right'}}}) 231 | ``` 232 | 233 | #### Output: 234 | ``` 235 | KEY VALUE 236 | mocha@1.18.2 1 237 | commander@2.0.0 1 238 | debug@0.8.1 1 239 | ``` 240 | 241 | `align: 'center'` works in a similar way. 242 | 243 | 244 | ### Padding Character 245 | 246 | Set a character to fill whitespace within columns with the `paddingChr` option. 247 | 248 | ```js 249 | var data = { 250 | "shortKey": "veryVeryVeryVeryVeryLongVal", 251 | "veryVeryVeryVeryVeryLongKey": "shortVal" 252 | } 253 | 254 | columnify(data, { paddingChr: '.'}) 255 | ``` 256 | 257 | #### Output: 258 | ``` 259 | KEY........................ VALUE...................... 260 | shortKey................... veryVeryVeryVeryVeryLongVal 261 | veryVeryVeryVeryVeryLongKey shortVal................... 262 | ``` 263 | 264 | ### Preserve Existing Newlines 265 | 266 | By default, `columnify` sanitises text by replacing any occurance of 1 or more whitespace characters with a single space. 267 | 268 | `columnify` can be configured to respect existing new line characters using the `preserveNewLines` option. Note this will still collapse all other whitespace. 269 | 270 | ```javascript 271 | var data = [{ 272 | name: "glob@3.2.9", 273 | paths: [ 274 | "node_modules/tap/node_modules/glob", 275 | "node_modules/tape/node_modules/glob" 276 | ].join('\n') 277 | }, { 278 | name: "nopt@2.2.1", 279 | paths: [ 280 | "node_modules/tap/node_modules/nopt" 281 | ] 282 | }, { 283 | name: "runforcover@0.0.2", 284 | paths: "node_modules/tap/node_modules/runforcover" 285 | }] 286 | 287 | console.log(columnify(data, {preserveNewLines: true})) 288 | ``` 289 | #### Output: 290 | ``` 291 | NAME PATHS 292 | glob@3.2.9 node_modules/tap/node_modules/glob 293 | node_modules/tape/node_modules/glob 294 | nopt@2.2.1 node_modules/tap/node_modules/nopt 295 | runforcover@0.0.2 node_modules/tap/node_modules/runforcover 296 | ``` 297 | 298 | Compare this with output without `preserveNewLines`: 299 | 300 | ```javascript 301 | console.log(columnify(data, {preserveNewLines: false})) 302 | // or just 303 | console.log(columnify(data)) 304 | ``` 305 | 306 | ``` 307 | NAME PATHS 308 | glob@3.2.9 node_modules/tap/node_modules/glob node_modules/tape/node_modules/glob 309 | nopt@2.2.1 node_modules/tap/node_modules/nopt 310 | runforcover@0.0.2 node_modules/tap/node_modules/runforcover 311 | ``` 312 | 313 | ### Custom Truncation Marker 314 | 315 | You can change the truncation marker to something other than the default 316 | `…` by using the `truncateMarker` option. 317 | 318 | ```javascript 319 | var columns = columnify(data, { 320 | truncate: true, 321 | truncateMarker: '>', 322 | widths: { 323 | description: { 324 | maxWidth: 20 325 | } 326 | } 327 | }) 328 | 329 | console.log(columns) 330 | ``` 331 | #### Output: 332 | ``` 333 | NAME DESCRIPTION VERSION 334 | mod1 some description> 0.0.1 335 | module-two another description> 0.2.0 336 | ``` 337 | 338 | ### Custom Column Splitter 339 | 340 | If your columns need some bling, you can split columns with custom 341 | characters by using the `columnSplitter` option. 342 | 343 | ```javascript 344 | var columns = columnify(data, { 345 | columnSplitter: ' | ' 346 | }) 347 | 348 | console.log(columns) 349 | ``` 350 | #### Output: 351 | ``` 352 | NAME | DESCRIPTION | VERSION 353 | mod1 | some description which happens to be far larger than the max | 0.0.1 354 | module-two | another description larger than the max | 0.2.0 355 | ``` 356 | 357 | ### Control Header Display 358 | 359 | Control whether column headers are displayed by using the `showHeaders` option. 360 | 361 | ```javascript 362 | var columns = columnify(data, { 363 | showHeaders: false 364 | }) 365 | ``` 366 | 367 | This also works well for hiding a single column header, like an `id` column: 368 | ```javascript 369 | var columns = columnify(data, { 370 | config: { 371 | id: { showHeaders: false } 372 | } 373 | }) 374 | ``` 375 | 376 | ### Transforming Column Data and Headers 377 | If you need to modify the presentation of column content or heading content there are two useful options for doing that: `dataTransform` and `headingTransform`. Both of these take a function and need to return a valid string. 378 | 379 | ```javascript 380 | var columns = columnify([{ 381 | name: 'mod1', 382 | description: 'SOME DESCRIPTION TEXT.' 383 | }, { 384 | name: 'module-two', 385 | description: 'SOME SLIGHTLY LONGER DESCRIPTION TEXT.' 386 | }], { 387 | dataTransform: function(data) { 388 | return data.toLowerCase() 389 | }, 390 | headingTransform: function(heading) { 391 | return heading.toLowerCase() 392 | }, 393 | config: { 394 | name: { 395 | headingTransform: function(heading) { 396 | heading = "module " + heading 397 | return "*" + heading.toUpperCase() + "*" 398 | } 399 | } 400 | } 401 | }) 402 | ``` 403 | #### Output: 404 | ``` 405 | *MODULE NAME* description 406 | mod1 some description text. 407 | module-two some slightly longer description text. 408 | ``` 409 | 410 | 411 | ## Multibyte Character Support 412 | 413 | `columnify` uses [mycoboco/wcwidth.js](https://github.com/mycoboco/wcwidth.js) to calculate length of multibyte characters: 414 | 415 | ```javascript 416 | var data = [{ 417 | name: 'module-one', 418 | description: 'some description', 419 | version: '0.0.1', 420 | }, { 421 | name: '这是一个很长的名字的模块', 422 | description: '这真的是一个描述的内容这个描述很长', 423 | version: "0.3.3" 424 | }] 425 | 426 | console.log(columnify(data)) 427 | ``` 428 | 429 | #### Without multibyte handling: 430 | 431 | i.e. before columnify added this feature 432 | 433 | ``` 434 | NAME DESCRIPTION VERSION 435 | module-one some description 0.0.1 436 | 这是一个很长的名字的模块 这真的是一个描述的内容这个描述很长 0.3.3 437 | ``` 438 | 439 | #### With multibyte handling: 440 | 441 | ``` 442 | NAME DESCRIPTION VERSION 443 | module-one some description 0.0.1 444 | 这是一个很长的名字的模块 这真的是一个描述的内容这个描述很长 0.3.3 445 | ``` 446 | 447 | ## Contributions 448 | 449 | ``` 450 | project : columnify 451 | repo age : 8 years 452 | active : 47 days 453 | commits : 180 454 | files : 57 455 | authors : 456 | 123 Tim Oxley 68.3% 457 | 11 Nicholas Hoffman 6.1% 458 | 8 Tim 4.4% 459 | 7 Arjun Mehta 3.9% 460 | 6 Dany 3.3% 461 | 5 Tim Kevin Oxley 2.8% 462 | 5 Wei Gao 2.8% 463 | 4 Matias Singers 2.2% 464 | 3 Michael Kriese 1.7% 465 | 2 sreekanth370 1.1% 466 | 2 Dany Shaanan 1.1% 467 | 1 Tim Malone 0.6% 468 | 1 Seth Miller 0.6% 469 | 1 andyfusniak 0.6% 470 | 1 Isaac Z. Schlueter 0.6% 471 | ``` 472 | 473 | ## License 474 | 475 | MIT 476 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = require('./large.json') 7 | var data2 = fs.readFileSync(__dirname + '/large.json', 'utf8') 8 | 9 | test('handling large data', function(t) { 10 | t.plan(3) 11 | 12 | var maxStringLength = data2.length / 360 13 | console.time('large data as single cell') 14 | t.ok(columnify({key: 'root', description: data2.slice(0, maxStringLength)}, { 15 | config: { 16 | description: { 17 | maxWidth: 30, 18 | minWidth: 10 19 | } 20 | } 21 | })) 22 | console.timeEnd('large data as single cell') 23 | 24 | // have to reduce dataset, otherwise bench 25 | // blows memory limit 26 | data = data.slice(0, data.length / 20) 27 | console.time('large data 1') 28 | t.ok(columnify(data, { 29 | config: { 30 | description: { 31 | maxWidth: 30, 32 | minWidth: 10 33 | } 34 | } 35 | })) 36 | console.timeEnd('large data 1') 37 | console.time('large data 2') 38 | t.ok(columnify(data, { 39 | config: { 40 | description: { 41 | maxWidth: 30, 42 | minWidth: 10 43 | } 44 | } 45 | })) 46 | console.timeEnd('large data 2') 47 | console.time('large data 3') 48 | t.ok(columnify(data, { 49 | config: { 50 | description: { 51 | maxWidth: 30, 52 | minWidth: 10 53 | } 54 | } 55 | })) 56 | console.timeEnd('large data 3') 57 | }) 58 | 59 | 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const wcwidth = require('./width') 4 | const { 5 | padRight, 6 | padCenter, 7 | padLeft, 8 | splitIntoLines, 9 | splitLongWords, 10 | truncateString 11 | } = require('./utils') 12 | 13 | const DEFAULT_HEADING_TRANSFORM = key => key.toUpperCase() 14 | 15 | const DEFAULT_DATA_TRANSFORM = (cell, column, index) => cell 16 | 17 | const DEFAULTS = Object.freeze({ 18 | maxWidth: Infinity, 19 | minWidth: 0, 20 | columnSplitter: ' ', 21 | truncate: false, 22 | truncateMarker: '…', 23 | preserveNewLines: false, 24 | paddingChr: ' ', 25 | showHeaders: true, 26 | headingTransform: DEFAULT_HEADING_TRANSFORM, 27 | dataTransform: DEFAULT_DATA_TRANSFORM 28 | }) 29 | 30 | module.exports = function(items, options = {}) { 31 | 32 | let columnConfigs = options.config || {} 33 | delete options.config // remove config so doesn't appear on every column. 34 | 35 | let maxLineWidth = options.maxLineWidth || Infinity 36 | if (maxLineWidth === 'auto') maxLineWidth = process.stdout.columns || Infinity 37 | delete options.maxLineWidth // this is a line control option, don't pass it to column 38 | 39 | // Option defaults inheritance: 40 | // options.config[columnName] => options => DEFAULTS 41 | options = mixin({}, DEFAULTS, options) 42 | 43 | options.config = options.config || Object.create(null) 44 | 45 | options.spacing = options.spacing || '\n' // probably useless 46 | options.preserveNewLines = !!options.preserveNewLines 47 | options.showHeaders = !!options.showHeaders; 48 | options.columns = options.columns || options.include // alias include/columns, prefer columns if supplied 49 | let columnNames = options.columns || [] // optional user-supplied columns to include 50 | 51 | items = toArray(items, columnNames) 52 | 53 | // if not suppled column names, automatically determine columns from data keys 54 | if (!columnNames.length) { 55 | items.forEach(function(item) { 56 | for (let columnName in item) { 57 | if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName) 58 | } 59 | }) 60 | } 61 | 62 | // initialize column defaults (each column inherits from options.config) 63 | let columns = columnNames.reduce((columns, columnName) => { 64 | let column = Object.create(options) 65 | columns[columnName] = mixin(column, columnConfigs[columnName]) 66 | return columns 67 | }, Object.create(null)) 68 | 69 | // sanitize column settings 70 | columnNames.forEach(columnName => { 71 | let column = columns[columnName] 72 | column.name = columnName 73 | column.maxWidth = Math.ceil(column.maxWidth) 74 | column.minWidth = Math.ceil(column.minWidth) 75 | column.truncate = !!column.truncate 76 | column.align = column.align || 'left' 77 | }) 78 | 79 | // sanitize data 80 | items = items.map(item => { 81 | let result = Object.create(null) 82 | columnNames.forEach(columnName => { 83 | // null/undefined -> '' 84 | result[columnName] = item[columnName] != null ? item[columnName] : '' 85 | // toString everything 86 | result[columnName] = '' + result[columnName] 87 | if (columns[columnName].preserveNewLines) { 88 | // merge non-newline whitespace chars 89 | result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ') 90 | } else { 91 | // merge all whitespace chars 92 | result[columnName] = result[columnName].replace(/\s/gmi, ' ') 93 | } 94 | }) 95 | return result 96 | }) 97 | 98 | // transform data cells 99 | columnNames.forEach(columnName => { 100 | let column = columns[columnName] 101 | items = items.map((item, index) => { 102 | let col = Object.create(column) 103 | item[columnName] = column.dataTransform(item[columnName], col, index) 104 | 105 | let changedKeys = Object.keys(col) 106 | // disable default heading transform if we wrote to column.name 107 | if (changedKeys.indexOf('name') !== -1) { 108 | if (column.headingTransform !== DEFAULT_HEADING_TRANSFORM) return 109 | column.headingTransform = heading => heading 110 | } 111 | changedKeys.forEach(key => column[key] = col[key]) 112 | return item 113 | }) 114 | }) 115 | 116 | // add headers 117 | let headers = {} 118 | if(options.showHeaders) { 119 | columnNames.forEach(columnName => { 120 | let column = columns[columnName] 121 | 122 | if(!column.showHeaders){ 123 | headers[columnName] = ''; 124 | return; 125 | } 126 | 127 | headers[columnName] = column.headingTransform(column.name) 128 | }) 129 | items.unshift(headers) 130 | } 131 | // get actual max-width between min & max 132 | // based on length of data in columns 133 | columnNames.forEach(columnName => { 134 | let column = columns[columnName] 135 | column.width = items 136 | .map(item => item[columnName]) 137 | .reduce((min, cur) => { 138 | // if already at maxWidth don't bother testing 139 | if (min >= column.maxWidth) return min 140 | return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur)))) 141 | }, 0) 142 | }) 143 | 144 | // split long words so they can break onto multiple lines 145 | columnNames.forEach(columnName => { 146 | let column = columns[columnName] 147 | items = items.map(item => { 148 | item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker) 149 | return item 150 | }) 151 | }) 152 | 153 | // wrap long lines. each item is now an array of lines. 154 | columnNames.forEach(columnName => { 155 | let column = columns[columnName] 156 | items = items.map((item, index) => { 157 | let cell = item[columnName] 158 | item[columnName] = splitIntoLines(cell, column.width) 159 | 160 | // if truncating required, only include first line + add truncation char 161 | if (column.truncate && item[columnName].length > 1) { 162 | item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker)) 163 | let firstLine = item[columnName][0] 164 | if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker 165 | item[columnName] = item[columnName].slice(0, 1) 166 | } 167 | return item 168 | }) 169 | }) 170 | 171 | // recalculate column widths from truncated output/lines 172 | columnNames.forEach(columnName => { 173 | let column = columns[columnName] 174 | column.width = items.map(item => { 175 | return item[columnName].reduce((min, cur) => { 176 | if (min >= column.maxWidth) return min 177 | return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur)))) 178 | }, 0) 179 | }).reduce((min, cur) => { 180 | if (min >= column.maxWidth) return min 181 | return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur))) 182 | }, 0) 183 | }) 184 | 185 | 186 | let rows = createRows(items, columns, columnNames, options.paddingChr) // merge lines into rows 187 | // conceive output 188 | return rows.reduce((output, row) => { 189 | return output.concat(row.reduce((rowOut, line) => { 190 | return rowOut.concat(line.join(options.columnSplitter)) 191 | }, [])) 192 | }, []) 193 | .map(line => truncateString(line, maxLineWidth)) 194 | .join(options.spacing) 195 | } 196 | 197 | /** 198 | * Convert wrapped lines into rows with padded values. 199 | * 200 | * @param Array items data to process 201 | * @param Array columns column width settings for wrapping 202 | * @param Array columnNames column ordering 203 | * @return Array items wrapped in arrays, corresponding to lines 204 | */ 205 | 206 | function createRows(items, columns, columnNames, paddingChr) { 207 | return items.map(item => { 208 | let row = [] 209 | let numLines = 0 210 | columnNames.forEach(columnName => { 211 | numLines = Math.max(numLines, item[columnName].length) 212 | }) 213 | // combine matching lines of each rows 214 | for (let i = 0; i < numLines; i++) { 215 | row[i] = row[i] || [] 216 | columnNames.forEach(columnName => { 217 | let column = columns[columnName] 218 | let val = item[columnName][i] || '' // || '' ensures empty columns get padded 219 | if (column.align === 'right') row[i].push(padLeft(val, column.width, paddingChr)) 220 | else if (column.align === 'center' || column.align === 'centre') row[i].push(padCenter(val, column.width, paddingChr)) 221 | else row[i].push(padRight(val, column.width, paddingChr)) 222 | }) 223 | } 224 | return row 225 | }) 226 | } 227 | 228 | /** 229 | * Object.assign 230 | * 231 | * @return Object Object with properties mixed in. 232 | */ 233 | 234 | function mixin(...args) { 235 | if (Object.assign) return Object.assign(...args) 236 | return ObjectAssign(...args) 237 | } 238 | 239 | function ObjectAssign(target, firstSource) { 240 | "use strict"; 241 | if (target === undefined || target === null) 242 | throw new TypeError("Cannot convert first argument to object"); 243 | 244 | var to = Object(target); 245 | 246 | var hasPendingException = false; 247 | var pendingException; 248 | 249 | for (var i = 1; i < arguments.length; i++) { 250 | var nextSource = arguments[i]; 251 | if (nextSource === undefined || nextSource === null) 252 | continue; 253 | 254 | var keysArray = Object.keys(Object(nextSource)); 255 | for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { 256 | var nextKey = keysArray[nextIndex]; 257 | try { 258 | var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); 259 | if (desc !== undefined && desc.enumerable) 260 | to[nextKey] = nextSource[nextKey]; 261 | } catch (e) { 262 | if (!hasPendingException) { 263 | hasPendingException = true; 264 | pendingException = e; 265 | } 266 | } 267 | } 268 | 269 | if (hasPendingException) 270 | throw pendingException; 271 | } 272 | return to; 273 | } 274 | 275 | /** 276 | * Adapted from String.prototype.endsWith polyfill. 277 | */ 278 | 279 | function endsWith(target, searchString, position) { 280 | position = position || target.length; 281 | position = position - searchString.length; 282 | let lastIndex = target.lastIndexOf(searchString); 283 | return lastIndex !== -1 && lastIndex === position; 284 | } 285 | 286 | 287 | function toArray(items, columnNames) { 288 | if (Array.isArray(items)) return items 289 | let rows = [] 290 | for (let key in items) { 291 | let item = {} 292 | item[columnNames[0] || 'key'] = key 293 | item[columnNames[1] || 'value'] = items[key] 294 | rows.push(item) 295 | } 296 | return rows 297 | } 298 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "columnify", 3 | "version": "1.6.0", 4 | "description": "Render data in text columns. Supports in-column text-wrap.", 5 | "main": "columnify.js", 6 | "scripts": { 7 | "pretest": "npm prune", 8 | "test": "make prepublish && tape test/*.js | tap-spec", 9 | "bench": "npm test && node bench", 10 | "prepublish": "make prepublish" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "es2015" 15 | ] 16 | }, 17 | "author": "Tim Oxley", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-cli": "^6.26.0", 21 | "babel-preset-es2015": "^6.3.13", 22 | "chalk": "^1.1.1", 23 | "tap-spec": "^5.0.0", 24 | "tape": "^4.4.0" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/timoxley/columnify.git" 29 | }, 30 | "keywords": [ 31 | "column", 32 | "text", 33 | "ansi", 34 | "console", 35 | "terminal", 36 | "wrap", 37 | "table" 38 | ], 39 | "bugs": { 40 | "url": "https://github.com/timoxley/columnify/issues" 41 | }, 42 | "homepage": "https://github.com/timoxley/columnify", 43 | "engines": { 44 | "node": ">=8.0.0" 45 | }, 46 | "dependencies": { 47 | "strip-ansi": "^6.0.1", 48 | "wcwidth": "^1.0.0" 49 | }, 50 | "directories": { 51 | "test": "test" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/1-column-simple-expected.txt: -------------------------------------------------------------------------------- 1 | NAME 2 | module1 3 | module2 4 | -------------------------------------------------------------------------------- /test/1-column-simple.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'module1' 8 | }, { 9 | name: 'module2' 10 | }] 11 | 12 | test('1 column', function(t) { 13 | t.plan(1) 14 | var expected = fs.readFileSync(__dirname + '/1-column-simple-expected.txt', 'utf8') 15 | t.equal(columnify(data).trim(), expected.trim()) 16 | }) 17 | -------------------------------------------------------------------------------- /test/2-column-simple-expected.txt: -------------------------------------------------------------------------------- 1 | NAME VERSION 2 | module1 0.0.1 3 | module2 0.2.0 4 | -------------------------------------------------------------------------------- /test/2-column-simple.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'module1', 8 | version: '0.0.1' 9 | }, { 10 | name: 'module2', 11 | version: '0.2.0' 12 | }] 13 | 14 | test('2 column', function(t) { 15 | t.plan(1) 16 | var expected = fs.readFileSync(__dirname + '/2-column-simple-expected.txt', 'utf8') 17 | t.equal(columnify(data).trim(), expected.trim()) 18 | }) 19 | -------------------------------------------------------------------------------- /test/align-center-expected.txt: -------------------------------------------------------------------------------- 1 | KEY............ VALUE 2 | mocha@1.18.2... ..1.. 3 | commander@2.0.0 ..1.. 4 | debug@0.8.1.... ..1.. 5 | -------------------------------------------------------------------------------- /test/align-center.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = { 7 | "mocha@1.18.2": 1, 8 | "commander@2.0.0": 1, 9 | "debug@0.8.1": 1 10 | } 11 | 12 | test('columns can be aligned to the center', function(t) { 13 | t.plan(1) 14 | var expected = fs.readFileSync(__dirname + '/align-center-expected.txt', 'utf8') 15 | var actual = columnify(data, { paddingChr: '.', config: {value: {align: 'center'}}}) 16 | t.equal(actual.trim(), expected.trim()) 17 | }) 18 | 19 | test('columns can be aligned to the centre using the correct spelling of centre', function(t) { 20 | t.plan(1) 21 | var expected = fs.readFileSync(__dirname + '/align-center-expected.txt', 'utf8') 22 | var actual = columnify(data, { paddingChr: '.', config: {value: {align: 'centre'}}}) 23 | t.equal(actual.trim(), expected.trim()) 24 | }) 25 | -------------------------------------------------------------------------------- /test/align-right-expected.txt: -------------------------------------------------------------------------------- 1 | KEY VALUE 2 | mocha@1.18.2 1 3 | commander@2.0.0 1 4 | debug@0.8.1 1 5 | diff@1.0.7 1 6 | glob@3.2.3 1 7 | graceful-fs@2.0.3 1 8 | growl@1.7.0 1 9 | inherits@2.0.1 4 10 | jade@0.26.3 1 11 | commander@0.6.1 1 12 | lru-cache@2.5.0 3 13 | minimatch@0.2.14 3 14 | mkdirp@0.3.5 2 15 | sigmund@1.0.0 3 16 | object-inspect@0.4.0 1 17 | resumer@0.0.0 1 18 | through@2.3.4 1 19 | -------------------------------------------------------------------------------- /test/align-right.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = { 7 | "mocha@1.18.2": 1, 8 | "commander@2.0.0": 1, 9 | "debug@0.8.1": 1, 10 | "diff@1.0.7": 1, 11 | "glob@3.2.3": 1, 12 | "graceful-fs@2.0.3": 1, 13 | "growl@1.7.0": 1, 14 | "inherits@2.0.1": 4, 15 | "jade@0.26.3": 1, 16 | "commander@0.6.1": 1, 17 | "lru-cache@2.5.0": 3, 18 | "minimatch@0.2.14": 3, 19 | "mkdirp@0.3.5": 2, 20 | "sigmund@1.0.0": 3, 21 | "object-inspect@0.4.0": 1, 22 | "resumer@0.0.0": 1, 23 | "through@2.3.4": 1 24 | } 25 | 26 | test('columns can be aligned right', function(t) { 27 | t.plan(1) 28 | var expected = fs.readFileSync(__dirname + '/align-right-expected.txt', 'utf8') 29 | var actual = columnify(data, {config: {value: {align: 'right'}}}) 30 | t.equal(actual.trim(), expected.trim()) 31 | }) 32 | -------------------------------------------------------------------------------- /test/ansi-expected.txt: -------------------------------------------------------------------------------- 1 | KEY VALUE 2 | mocha@1.18.2 3 3 | commander@2.0.0 1 4 | debug@0.8.1 6 -------------------------------------------------------------------------------- /test/ansi.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | var chalk = require('chalk') 4 | // required when running inside faucet etc 5 | // as chalk will detect output is not a tty 6 | // and disable color for you automatically 7 | chalk.enabled = true 8 | 9 | var columnify = require('../') 10 | 11 | var data = { 12 | "mocha@1.18.2": chalk.yellow('3'), 13 | "commander@2.0.0": chalk.green('1'), 14 | "debug@0.8.1": chalk.red('6') 15 | } 16 | 17 | test('width calculated correctly even if ansi colors used.', function(t) { 18 | t.plan(1) 19 | var expected = fs.readFileSync(__dirname + '/ansi-expected.txt', 'utf8') 20 | var actual = columnify(data) 21 | t.equal(actual.trim(), expected.trim()) 22 | }) 23 | -------------------------------------------------------------------------------- /test/auto-columns-expected.txt: -------------------------------------------------------------------------------- 1 | KEY VALUE 2 | mocha@1.18.2 1 3 | commander@2.0.0 1 4 | debug@0.8.1 1 5 | diff@1.0.7 1 6 | glob@3.2.3 1 7 | graceful-fs@2.0.3 1 8 | growl@1.7.0 1 9 | inherits@2.0.1 4 10 | jade@0.26.3 1 11 | commander@0.6.1 1 12 | lru-cache@2.5.0 3 13 | minimatch@0.2.14 3 14 | mkdirp@0.3.5 2 15 | sigmund@1.0.0 3 16 | object-inspect@0.4.0 1 17 | resumer@0.0.0 1 18 | through@2.3.4 1 -------------------------------------------------------------------------------- /test/auto-columns.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = { 7 | "mocha@1.18.2": 1, 8 | "commander@2.0.0": 1, 9 | "debug@0.8.1": 1, 10 | "diff@1.0.7": 1, 11 | "glob@3.2.3": 1, 12 | "graceful-fs@2.0.3": 1, 13 | "growl@1.7.0": 1, 14 | "inherits@2.0.1": 4, 15 | "jade@0.26.3": 1, 16 | "commander@0.6.1": 1, 17 | "lru-cache@2.5.0": 3, 18 | "minimatch@0.2.14": 3, 19 | "mkdirp@0.3.5": 2, 20 | "sigmund@1.0.0": 3, 21 | "object-inspect@0.4.0": 1, 22 | "resumer@0.0.0": 1, 23 | "through@2.3.4": 1 24 | } 25 | 26 | test('objects are automatically converted into k/v array', function(t) { 27 | t.plan(1) 28 | var expected = fs.readFileSync(__dirname + '/auto-columns-expected.txt', 'utf8') 29 | t.equal(columnify(data).trim(), expected.trim()) 30 | }) 31 | 32 | test('column names can be provided', function(t) { 33 | t.plan(1) 34 | var expected = fs.readFileSync(__dirname + '/auto-columns-expected.txt', 'utf8') 35 | expected = expected.replace('VALUE', 'COUNT', 'gmi') 36 | // RE 'count': picked a word with same length (as 'value') so didn't need a 37 | // new fixture (damn whitespace) 38 | t.equal(columnify(data, {columns: ['key', 'count']}).trim(), expected.trim()) 39 | }) 40 | -------------------------------------------------------------------------------- /test/column-splitter-character-expected.txt: -------------------------------------------------------------------------------- 1 | NAME | DESCRIPTION | VERSION 2 | mod1 | some description which happens to be far larger than the max | 0.0.1 3 | module-two | another description larger than the max | 0.2.0 4 | -------------------------------------------------------------------------------- /test/column-splitter-character.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description which happens to be far larger than the max', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }] 15 | 16 | test('column splitter character', function(t) { 17 | t.plan(1) 18 | var expected = fs.readFileSync(__dirname + '/column-splitter-character-expected.txt', 'utf8') 19 | t.equal(columnify(data, { 20 | columnSplitter: ' | ' 21 | }).trim(), expected.trim()) 22 | }) 23 | -------------------------------------------------------------------------------- /test/data-transform-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VERSION 2 | MODULE1 SOME DESCRIPTION 0.0.1 3 | MODULE2 ANOTHER DESCRIPTION 0.2.0 4 | MODULE3 这是一段描述 0.3.2 -------------------------------------------------------------------------------- /test/data-transform.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'module1', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module2', 12 | description: 'another description', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module3', 16 | description: '这是一段描述', 17 | version: '0.3.2' 18 | }] 19 | 20 | 21 | test('column data can be transformed', function(t) { 22 | t.plan(1) 23 | var expected = fs.readFileSync(__dirname + '/data-transform-expected.txt', 'utf8') 24 | t.equal(columnify(data, { 25 | dataTransform: function(data, column) { 26 | return data.toUpperCase() 27 | } 28 | }).trim(), expected.trim()) 29 | }) 30 | 31 | test('dataTransform gets columns', function(t) { 32 | var COLUMNS = Object.keys(data[0]).length 33 | var ASSERTIONS = 6 34 | t.plan(data.length * COLUMNS * ASSERTIONS) 35 | columnify(data, { 36 | dataTransform: function(data, column) { 37 | t.ok(column, 'has column') 38 | t.ok(column.name, 'column has name') 39 | t.ok(column.align, 'column has align') 40 | t.ok(column.maxWidth, 'column has maxWidth') 41 | t.ok('minWidth' in column, 'column has minWidth') 42 | t.ok('truncate' in column, 'column has truncate') 43 | return data 44 | } 45 | }) 46 | }) 47 | 48 | test('column headings can be transformed', function(t) { 49 | t.plan(1) 50 | var expected = fs 51 | .readFileSync(__dirname + '/data-transform-expected.txt', 'utf8') 52 | .replace(/VERSION/gmi, 'Version') 53 | 54 | t.equal(columnify(data, { 55 | dataTransform: function(data, column) { 56 | if (column.name === 'version') column.name = 'Version' 57 | return data.toUpperCase() 58 | } 59 | }).trim(), expected.trim()) 60 | }) 61 | 62 | test('column data can be transformed on a per-column basis', function(t) { 63 | var result = columnify(data, { 64 | config: { 65 | name: { 66 | dataTransform: function(data, column) { // only uppercase name 67 | t.equal(column.name, "name") 68 | return data.toUpperCase() 69 | } 70 | } 71 | } 72 | }) 73 | t.ok(result.indexOf('MODULE1') !== -1, 'Module1 name was transformed') 74 | t.ok(result.indexOf('MODULE2') !== -1, 'Module2 name was transformed') 75 | t.ok(result.indexOf('MODULE3') !== -1, 'Module3 name was transformed') 76 | t.ok(result.indexOf('another description') !== -1, 'Description was not transformed') 77 | t.end() 78 | }) 79 | 80 | -------------------------------------------------------------------------------- /test/define-columns.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'module1', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module2', 12 | description: 'another description', 13 | version: '0.2.0', 14 | }] 15 | 16 | test('removes description column', function(t) { 17 | t.plan(2) 18 | var result = columnify(data, { 19 | columns: ['name', 'version'], 20 | }) 21 | 22 | var columnHeaders = result.split('\n')[0] 23 | t.deepEqual(columnHeaders.split(/\s+/), [ 24 | "NAME", 25 | "VERSION" 26 | ]) 27 | t.ok(!(/description/gi.test(result))) 28 | }) 29 | 30 | test('include == columns', function(t) { 31 | t.plan(2) 32 | var result = columnify(data, { 33 | include: ['name', 'version'], 34 | }) 35 | 36 | var columnHeaders = result.split('\n')[0] 37 | t.deepEqual(columnHeaders.split(/\s+/), [ 38 | "NAME", 39 | "VERSION" 40 | ]) 41 | t.ok(!(/description/gi.test(result))) 42 | }) 43 | 44 | test('columns preferred over include if both supplied', function(t) { 45 | t.plan(2) 46 | var result = columnify(data, { 47 | columns: ['name', 'version'], 48 | include: ['description'], 49 | }) 50 | 51 | var columnHeaders = result.split('\n')[0] 52 | t.deepEqual(columnHeaders.split(/\s+/), [ 53 | "NAME", 54 | "VERSION" 55 | ]) 56 | t.ok(!(/description/gi.test(result))) 57 | }) 58 | -------------------------------------------------------------------------------- /test/existing-linebreaks-expected.txt: -------------------------------------------------------------------------------- 1 | NAME PATHS 2 | glob@3.2.9 node_modules/tap/node_modules/glob 3 | node_modules/tape/node_modules/glob 4 | nopt@2.2.1 node_modules/tap/node_modules/nopt 5 | runforcover@0.0.2 node_modules/tap/node_modules/runforcover 6 | -------------------------------------------------------------------------------- /test/existing-linebreaks.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: "glob@3.2.9", 8 | paths: [ 9 | "node_modules/tap/node_modules/glob", 10 | "node_modules/tape/node_modules/glob" 11 | ].join('\n') 12 | }, { 13 | name: "nopt@2.2.1", 14 | paths: [ 15 | "node_modules/tap/node_modules/nopt" 16 | ] 17 | }, { 18 | name: "runforcover@0.0.2", 19 | paths: "node_modules/tap/node_modules/runforcover" 20 | }] 21 | 22 | test('leaving existing linebreaks', function(t) { 23 | t.plan(1) 24 | var expected = fs.readFileSync(__dirname + '/existing-linebreaks-expected.txt', 'utf8') 25 | t.equal(columnify(data, {preserveNewLines: true}).trim(), expected.trim()) 26 | t.end() 27 | }) 28 | 29 | test('removing existing linebreaks', function(t) { 30 | t.plan(1) 31 | var expected = fs.readFileSync(__dirname + '/remove-existing-linebreaks-expected.txt', 'utf8') 32 | t.equal(columnify(data).trim(), expected.trim()) 33 | t.end() 34 | }) 35 | -------------------------------------------------------------------------------- /test/headings.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | var columnify = require('../') 4 | 5 | var data = [{ 6 | name: 'module1', 7 | description: 'some description', 8 | version: '0.0.1', 9 | }, { 10 | name: 'module2', 11 | description: 'another description', 12 | version: '0.2.0', 13 | }] 14 | 15 | 16 | test('column headings are uppercased by default', function(t) { 17 | t.plan(3) 18 | var result = columnify(data) 19 | var headings = result.split('\n')[0]// first line 20 | t.ok(headings.indexOf('NAME') !== -1, 'NAME exists') 21 | t.ok(headings.indexOf('DESCRIPTION') !== -1, 'DESCRIPTION exists') 22 | t.ok(headings.indexOf('VERSION') !== -1, 'VERSION exists') 23 | }) 24 | 25 | test('headings can be transformed by a function', function(t) { 26 | t.plan(3) 27 | var result = columnify(data, { 28 | headingTransform: function(name) { 29 | return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase() 30 | } 31 | }) 32 | var headings = result.split('\n')[0] // first line 33 | t.ok(headings.indexOf('Name') !== -1, 'Name exists') 34 | t.ok(headings.indexOf('Description') !== -1, 'Description exists') 35 | t.ok(headings.indexOf('Version') !== -1, 'Version exists') 36 | }) 37 | 38 | test('headings can be transformed on a per-column basis', function(t) { 39 | t.plan(3) 40 | var result = columnify(data, { 41 | config: { 42 | // leave default uppercase heading 43 | name: { 44 | headingTransform: function(name) { // only title case name 45 | return name.slice(0, 1).toUpperCase() + name.slice(1).toLowerCase() 46 | } 47 | } 48 | } 49 | }) 50 | var headings = result.split('\n')[0] // first line 51 | t.ok(headings.indexOf('Name') !== -1, 'Name exists') 52 | t.ok(headings.indexOf('DESCRIPTION') !== -1, 'Description exists') 53 | t.ok(headings.indexOf('VERSION') !== -1, 'Version exists') 54 | }) 55 | -------------------------------------------------------------------------------- /test/hide-individual-header-expected.txt: -------------------------------------------------------------------------------- 1 | NAME VERSION 2 | 0 module1 0.0.1 3 | 1 module2 0.2.0 -------------------------------------------------------------------------------- /test/hide-individual-header.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | id: 0, 8 | name: 'module1', 9 | version: '0.0.1' 10 | }, { 11 | id: 1, 12 | name: 'module2', 13 | version: '0.2.0' 14 | }] 15 | 16 | test('hide id column', function(t) { 17 | t.plan(1) 18 | var expected = fs.readFileSync(__dirname + '/hide-individual-header-expected.txt', 'utf8') 19 | t.equal(columnify(data, {config: {id: {showHeaders: false}} }), expected) 20 | }) 21 | -------------------------------------------------------------------------------- /test/padCenter.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | var padCenter = require('../utils').padCenter 4 | 5 | test('pad string with spaces up to len (sides equal)', function(t) { 6 | t.plan(1) 7 | t.equal(padCenter('word', 10), ' word ') 8 | }) 9 | 10 | test('pad string with spaces up to len (sides not equal)', function(t) { 11 | t.plan(1) 12 | t.equal(padCenter('words', 10), ' words ') 13 | }) 14 | 15 | test('pad string with paddingChr of length >1, up to len', function(t) { 16 | t.plan(1) 17 | t.equal(padCenter('word', 10, ' .'), ' . word . ') 18 | }) 19 | -------------------------------------------------------------------------------- /test/padRight.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var padRight = require('../utils').padRight 5 | 6 | test('pad string with spaces up to len', function(t) { 7 | t.plan(1) 8 | t.equal(padRight('word', 10), 'word ') 9 | }) 10 | 11 | test('pad empty string with spaces up to len', function(t) { 12 | t.plan(1) 13 | t.equal(padRight('', 10), ' ') 14 | }) 15 | 16 | test('leaves long strings along', function(t) { 17 | t.plan(1) 18 | t.equal(padRight('012345678910', 10), '012345678910') 19 | }) 20 | 21 | test('custom padding', function(t) { 22 | t.plan(1) 23 | t.equal(padRight('', 10, '.'), '..........') 24 | }) 25 | 26 | test('handling funky data with spaces up to len', function(t) { 27 | t.plan(5) 28 | t.equal(padRight(null, 10), ' ') 29 | t.equal(padRight(false, 10), 'false ') 30 | t.equal(padRight(0, 10), '0 ') 31 | t.equal(padRight(10, 10), '10 ') 32 | t.equal(padRight([], 10), ' ') 33 | }) 34 | 35 | test('pad string with paddingChr up to len', function(t) { 36 | t.plan(1) 37 | t.equal(padRight('word', 10, '.'), 'word......') 38 | }) 39 | 40 | test('pad string with paddingChr of length >1, up to len', function(t) { 41 | t.plan(1) 42 | t.equal(padRight('words', 10, ' .'), 'words . . ') 43 | }) 44 | -------------------------------------------------------------------------------- /test/remove-existing-linebreaks-expected.txt: -------------------------------------------------------------------------------- 1 | NAME PATHS 2 | glob@3.2.9 node_modules/tap/node_modules/glob node_modules/tape/node_modules/glob 3 | nopt@2.2.1 node_modules/tap/node_modules/nopt 4 | runforcover@0.0.2 node_modules/tap/node_modules/runforcover -------------------------------------------------------------------------------- /test/show-headers-expected.txt: -------------------------------------------------------------------------------- 1 | module1 0.0.1 2 | module2 0.2.0 3 | -------------------------------------------------------------------------------- /test/show-headers.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'module1', 8 | version: '0.0.1' 9 | }, { 10 | name: 'module2', 11 | version: '0.2.0' 12 | }] 13 | 14 | test('show column', function(t) { 15 | t.plan(1) 16 | var expected = fs.readFileSync(__dirname + '/show-headers-expected.txt', 'utf8') 17 | t.equal(columnify(data, {showHeaders:false}).trim(), expected.trim()) 18 | }) 19 | -------------------------------------------------------------------------------- /test/splitintolines.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var splitIntoLines = require('../utils').splitIntoLines 5 | 6 | test('lines under max are ok', function(t) { 7 | t.plan(1) 8 | t.deepEqual(splitIntoLines('dinosaur cabbages', 32), ['dinosaur cabbages']) 9 | }) 10 | 11 | test('lines at max are ok', function(t) { 12 | t.plan(1) 13 | t.deepEqual(splitIntoLines('dinosaur cabbages', 17), ['dinosaur cabbages']) 14 | }) 15 | 16 | test('lines at max with multiple spaces are ok', function(t) { 17 | t.plan(1) 18 | t.deepEqual(splitIntoLines('dinosaur cabbages mechanic', 26), ['dinosaur cabbages mechanic']) 19 | }) 20 | 21 | test('lines over max will be split', function(t) { 22 | t.plan(1) 23 | t.deepEqual(splitIntoLines('dinosaur cabbages', 16), ['dinosaur', 'cabbages']) 24 | }) 25 | 26 | test('splits lines under max onto multiple lines', function(t) { 27 | t.plan(1) 28 | t.deepEqual(splitIntoLines('dinosaur cabbages', 7), ['dinosaur', 'cabbages']) 29 | }) 30 | 31 | test('can put multiple words per line', function(t) { 32 | t.plan(1) 33 | t.deepEqual(splitIntoLines('dog cat cow bat mat', 7), [ 34 | 'dog cat', 35 | 'cow bat', 36 | 'mat' 37 | ]) 38 | }) 39 | 40 | test('single existing newline is preserved', function(t) { 41 | t.plan(1) 42 | t.deepEqual(splitIntoLines('dog\n cat cow bat mat', 7), [ 43 | 'dog', 44 | 'cat cow', 45 | 'bat mat' 46 | ]) 47 | }) 48 | 49 | test('multiple existing newlines are preserved', function(t) { 50 | t.plan(1) 51 | t.deepEqual(splitIntoLines('dog\n\n cat\n cow \nbat mat', 7), [ 52 | 'dog', 53 | '', 54 | 'cat', 55 | 'cow', 56 | 'bat mat' 57 | ]) 58 | }) 59 | -------------------------------------------------------------------------------- /test/splitlongwords.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var splitLongWords = require('../utils').splitLongWords 5 | 6 | test('split long word with …', function(t) { 7 | t.plan(1) 8 | // note is 5 letters to take … into account 9 | t.equal(splitLongWords('dinosaur', 5, '…'), 'dino… saur') 10 | }) 11 | 12 | 13 | test('split long words with …', function(t) { 14 | t.plan(1) 15 | t.equal(splitLongWords('dinosaur cabbages', 5, '…'), 'dino… saur cabb… ages') 16 | }) 17 | 18 | test('ignores short words', function(t) { 19 | t.plan(1) 20 | t.equal(splitLongWords('cow car mouse', 5, '…'), 'cow car mouse') 21 | }) 22 | 23 | test('can split long words multiple times', function(t) { 24 | t.plan(1) 25 | t.equal(splitLongWords('dodecahedrons', 3, '…'), 'do… de… ca… he… dr… ons') 26 | }) 27 | 28 | test('ignores/strips leading whitespace', function(t) { 29 | t.plan(1) 30 | t.equal(splitLongWords(' dodecahedrons', 3, '…'), 'do… de… ca… he… dr… ons') 31 | }) 32 | 33 | test('multibytes characters', function(t) { 34 | t.plan(1) 35 | t.equal(splitLongWords('cow 开汽车 mouse 안녕하세요', 3, '…'), 'cow 开… 汽… 车 mo… use 안… 녕… 하… 세… 요') 36 | }) 37 | -------------------------------------------------------------------------------- /test/truncate-column-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VERSION 2 | alongname some description 0.0.1 3 | shouldbes 4 | plitoverm 5 | ultipleli 6 | nes 7 | module-tw another description… 0.2.0 8 | o 9 | module-th thisisaverylongword… 0.2.0 10 | ree 11 | 这是一个 这真的是一个描述的… 0.3.3 12 | 很长的名 13 | 字的模块 -------------------------------------------------------------------------------- /test/truncate-column.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'alongnameshouldbesplitovermultiplelines', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module-three', 16 | description: 'thisisaverylongwordandshouldbetruncated', 17 | version: '0.2.0', 18 | }, { 19 | name: '这是一个很长的名字的模块', 20 | description: '这真的是一个描述的内容这个描述很长', 21 | version: "0.3.3" 22 | }] 23 | 24 | test('specific columns can be truncated, while others not', function(t) { 25 | t.plan(1) 26 | var expected = fs.readFileSync(__dirname + '/truncate-column-expected.txt', 'utf8') 27 | 28 | t.equal(columnify(data, { 29 | config: { 30 | name: { 31 | truncate: false, 32 | maxWidth: 9, 33 | truncateMarker: '' 34 | }, 35 | description: { 36 | truncate: true, 37 | maxWidth: 20 38 | } 39 | } 40 | }).trim(), expected.trim()) 41 | }) 42 | 43 | test('string proto does not get polluted by wcwidth', function(t) { 44 | t.equal(String.prototype.wcwidth, undefined) 45 | t.end() 46 | }) 47 | -------------------------------------------------------------------------------- /test/truncate-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VERSION 2 | mod1 some description 0.0.1 3 | module-two another description… 0.2.0 4 | module-three thisisaverylongword… 0.2.0 5 | -------------------------------------------------------------------------------- /test/truncate-multichar-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VERSION 2 | mod1 some description 0.0.1 3 | module-two another... 0.2.0 4 | module-three thisisaverylongwo... 0.2.0 5 | -------------------------------------------------------------------------------- /test/truncate-string-maxlinewidth-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VER 2 | alongname some description 0.0 3 | shouldbes 4 | plitoverm 5 | ultipleli 6 | nes 7 | module-tw another description… 0.2 8 | o 9 | module-th thisisaverylongword… 0.2 10 | ree 11 | 这是一个 这真的是一个描述的… 0.3 12 | 很长的名 13 | 字的模块 -------------------------------------------------------------------------------- /test/truncate-string-maxlinewidth.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'alongnameshouldbesplitovermultiplelines', 8 | description: 'some description', 9 | version: '0.0.1' 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module-three', 16 | description: 'thisisaverylongwordandshouldbetruncated', 17 | version: '0.2.0', 18 | }, { 19 | name: '这是一个很长的名字的模块', 20 | description: '这真的是一个描述的内容这个描述很长', 21 | version: "0.3.3" 22 | }] 23 | 24 | test('specific columns can be truncated, while others not', function(t) { 25 | var expected = fs.readFileSync(__dirname + '/truncate-string-maxlinewidth-expected.txt', 'utf8') 26 | 27 | t.equal(columnify(data, { 28 | maxLineWidth: 34, 29 | config: { 30 | name: { 31 | truncate: false, 32 | maxWidth: 9, 33 | truncateMarker: '' 34 | }, 35 | description: { 36 | truncate: true, 37 | maxWidth: 20 38 | } 39 | } 40 | }).trim(), expected.trim()) 41 | t.end() 42 | }) 43 | 44 | test('when no maxLineWidth, nothing is changed', function(t) { 45 | t.equal(columnify(data, { 46 | config: { 47 | name: { 48 | truncate: false, 49 | maxWidth: 9, 50 | truncateMarker: '' 51 | }, 52 | description: { 53 | truncate: true, 54 | maxWidth: 20 55 | } 56 | } 57 | }).trim(), fs.readFileSync(__dirname + '/truncate-column-expected.txt', 'utf8').trim()) 58 | 59 | t.end() 60 | }) 61 | 62 | test('maxLineWidth: "auto" with column max widths', function(t) { 63 | t.equal(columnify(data, { 64 | maxLineWidth: 'auto', 65 | config: { 66 | name: { 67 | truncate: false, 68 | maxWidth: 9, 69 | truncateMarker: '' 70 | }, 71 | description: { 72 | truncate: true, 73 | maxWidth: 20 74 | } 75 | } 76 | }).trim(), fs.readFileSync(__dirname + '/truncate-column-expected.txt', 'utf8').trim()) 77 | t.end() 78 | }) 79 | -------------------------------------------------------------------------------- /test/truncate-with-padding-expected.txt: -------------------------------------------------------------------------------- 1 | NAME........ DESCRIPTION......... VERSION 2 | mod1........ some description.... 0.0.1.. 3 | module-two.. another description… 0.2.0.. 4 | module-three thisisaverylongword… 0.2.0.. 5 | -------------------------------------------------------------------------------- /test/truncate-with-padding.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module-three', 16 | description: 'thisisaverylongwordandshouldbetruncated', 17 | version: '0.2.0', 18 | }] 19 | 20 | test('columns are limited when truncation enabled', function(t) { 21 | t.plan(1) 22 | var expected = fs.readFileSync(__dirname + '/truncate-with-padding-expected.txt', 'utf8') 23 | t.equal(columnify(data, { 24 | truncate: true, 25 | paddingChr: '.', 26 | config: { 27 | description: { 28 | maxWidth: 20 29 | } 30 | } 31 | }).trim(), expected.trim()) 32 | }) 33 | -------------------------------------------------------------------------------- /test/truncate.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module-three', 16 | description: 'thisisaverylongwordandshouldbetruncated', 17 | version: '0.2.0', 18 | }] 19 | 20 | test('columns are limited when truncation enabled', function(t) { 21 | t.plan(1) 22 | var expected = fs.readFileSync(__dirname + '/truncate-expected.txt', 'utf8') 23 | t.equal(columnify(data, { 24 | truncate: true, 25 | config: { 26 | description: { 27 | maxWidth: 20 28 | } 29 | } 30 | }).trim(), expected.trim()) 31 | }) 32 | 33 | -------------------------------------------------------------------------------- /test/truncatestring.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var truncateString = require('../utils').truncateString 5 | 6 | 7 | test('truncate string which is longer than max', function(t) { 8 | t.plan(1) 9 | t.equal(truncateString('This is a very long sentencies', 20), 'This is a very long ') 10 | }) 11 | 12 | test('truncate string which is shorter than max', function(t) { 13 | t.plan(1) 14 | t.equal(truncateString('short', 10), 'short') 15 | }) 16 | 17 | test('truncate string with multibytes characters', function(t) { 18 | t.plan(1) 19 | t.equal(truncateString('这是一句话 That is a word', 15), '这是一句话 That') 20 | }) 21 | 22 | test('return string when maxLineWidth is Infinity', function(t) { 23 | t.plan(1) 24 | t.equal(truncateString('这是一句话 That is a word', Infinity), '这是一句话 That is a word'); 25 | }) 26 | 27 | test('truncate funky data', function(t) { 28 | t.plan(5) 29 | t.equal(truncateString(null, 2), '') 30 | t.equal(truncateString(false, 4), 'fals') 31 | t.equal(truncateString(100005, 5), '10000') 32 | t.equal(truncateString(10, 10), '10') 33 | t.equal(truncateString([], 5), '') 34 | }) 35 | -------------------------------------------------------------------------------- /test/truncation-character.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'module-three', 16 | description: 'thisisaverylongwordandshouldbetruncated', 17 | version: '0.2.0', 18 | }] 19 | 20 | test('truncation character is configurable', function(t) { 21 | t.plan(1) 22 | var expected = fs.readFileSync(__dirname + '/truncate-expected.txt', 'utf8').replace(/…/g, '>') 23 | t.equal(columnify(data, { 24 | truncateMarker: '>', 25 | truncate: true, 26 | config: { 27 | description: { 28 | maxWidth: 20 29 | } 30 | } 31 | }).trim(), expected.trim()) 32 | }) 33 | 34 | test('truncation character can be multichar', function(t) { 35 | t.plan(1) 36 | var expected = fs.readFileSync(__dirname + '/truncate-multichar-expected.txt', 'utf8') 37 | 38 | t.equal(columnify(data, { 39 | truncateMarker: '...', 40 | truncate: true, 41 | config: { 42 | description: { 43 | maxWidth: 20 44 | } 45 | } 46 | }).trim(), expected.trim()) 47 | }) 48 | -------------------------------------------------------------------------------- /test/wrap-expected.txt: -------------------------------------------------------------------------------- 1 | NAME DESCRIPTION VERSION 2 | mod1 some description which happens 0.0.1 3 | to be far larger than the max 4 | module-two another description larger 0.2.0 5 | than the max 6 | mod3 thisisaverylongwordandshouldb… 0.3.0 7 | ewrapped 8 | module-four-four-four-four 0.0.4 9 | -------------------------------------------------------------------------------- /test/wrap-with-padding-expected.txt: -------------------------------------------------------------------------------- 1 | NAME...................... DESCRIPTION................... VERSION 2 | mod1...................... some description which happens 0.0.1.. 3 | .......................... to be far larger than the max. ....... 4 | module-two................ another description larger.... 0.2.0.. 5 | .......................... than the max.................. ....... 6 | mod3...................... thisisaverylongwordandshouldb… 0.3.0.. 7 | .......................... ewrapped...................... ....... 8 | module-four-four-four-four .............................. 0.0.4.. 9 | -------------------------------------------------------------------------------- /test/wrap-with-padding.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description which happens to be far larger than the max', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'mod3', 16 | description: 'thisisaverylongwordandshouldbewrapped', 17 | version: '0.3.0', 18 | }, { 19 | name: 'module-four-four-four-four', 20 | description: '', 21 | version: '0.0.4', 22 | }] 23 | 24 | test('wrapping wide columns', function(t) { 25 | t.plan(1) 26 | var expected = fs.readFileSync(__dirname + '/wrap-with-padding-expected.txt', 'utf8') 27 | t.equal(columnify(data, { 28 | paddingChr: '.', 29 | config: { 30 | description: { 31 | maxWidth: 30, 32 | minWidth: 10 33 | } 34 | } 35 | }).trim(), expected.trim()) 36 | }) 37 | -------------------------------------------------------------------------------- /test/wrap.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | 4 | var columnify = require('../') 5 | 6 | var data = [{ 7 | name: 'mod1', 8 | description: 'some description which happens to be far larger than the max', 9 | version: '0.0.1', 10 | }, { 11 | name: 'module-two', 12 | description: 'another description larger than the max', 13 | version: '0.2.0', 14 | }, { 15 | name: 'mod3', 16 | description: 'thisisaverylongwordandshouldbewrapped', 17 | version: '0.3.0', 18 | }, { 19 | name: 'module-four-four-four-four', 20 | description: '', 21 | version: '0.0.4', 22 | }] 23 | 24 | test('wrapping wide columns', function(t) { 25 | t.plan(1) 26 | var expected = fs.readFileSync(__dirname + '/wrap-expected.txt', 'utf8') 27 | t.equal(columnify(data, { 28 | config: { 29 | description: { 30 | maxWidth: 30, 31 | minWidth: 10 32 | } 33 | } 34 | }).trim(), expected.trim()) 35 | }) 36 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var wcwidth = require('./width') 4 | 5 | /** 6 | * repeat string `str` up to total length of `len` 7 | * 8 | * @param String str string to repeat 9 | * @param Number len total length of output string 10 | */ 11 | 12 | function repeatString(str, len) { 13 | return Array.apply(null, {length: len + 1}).join(str).slice(0, len) 14 | } 15 | 16 | /** 17 | * Pad `str` up to total length `max` with `chr`. 18 | * If `str` is longer than `max`, padRight will return `str` unaltered. 19 | * 20 | * @param String str string to pad 21 | * @param Number max total length of output string 22 | * @param String chr optional. Character to pad with. default: ' ' 23 | * @return String padded str 24 | */ 25 | 26 | function padRight(str, max, chr) { 27 | str = str != null ? str : '' 28 | str = String(str) 29 | var length = max - wcwidth(str) 30 | if (length <= 0) return str 31 | return str + repeatString(chr || ' ', length) 32 | } 33 | 34 | /** 35 | * Pad `str` up to total length `max` with `chr`. 36 | * If `str` is longer than `max`, padCenter will return `str` unaltered. 37 | * 38 | * @param String str string to pad 39 | * @param Number max total length of output string 40 | * @param String chr optional. Character to pad with. default: ' ' 41 | * @return String padded str 42 | */ 43 | 44 | function padCenter(str, max, chr) { 45 | str = str != null ? str : '' 46 | str = String(str) 47 | var length = max - wcwidth(str) 48 | if (length <= 0) return str 49 | var lengthLeft = Math.floor(length/2) 50 | var lengthRight = length - lengthLeft 51 | return repeatString(chr || ' ', lengthLeft) + str + repeatString(chr || ' ', lengthRight) 52 | } 53 | 54 | /** 55 | * Pad `str` up to total length `max` with `chr`, on the left. 56 | * If `str` is longer than `max`, padRight will return `str` unaltered. 57 | * 58 | * @param String str string to pad 59 | * @param Number max total length of output string 60 | * @param String chr optional. Character to pad with. default: ' ' 61 | * @return String padded str 62 | */ 63 | 64 | function padLeft(str, max, chr) { 65 | str = str != null ? str : '' 66 | str = String(str) 67 | var length = max - wcwidth(str) 68 | if (length <= 0) return str 69 | return repeatString(chr || ' ', length) + str 70 | } 71 | 72 | /** 73 | * Split a String `str` into lines of maxiumum length `max`. 74 | * Splits on word boundaries. Preserves existing new lines. 75 | * 76 | * @param String str string to split 77 | * @param Number max length of each line 78 | * @return Array Array containing lines. 79 | */ 80 | 81 | function splitIntoLines(str, max) { 82 | function _splitIntoLines(str, max) { 83 | return str.trim().split(' ').reduce(function(lines, word) { 84 | var line = lines[lines.length - 1] 85 | if (line && wcwidth(line.join(' ')) + wcwidth(word) < max) { 86 | lines[lines.length - 1].push(word) // add to line 87 | } 88 | else lines.push([word]) // new line 89 | return lines 90 | }, []).map(function(l) { 91 | return l.join(' ') 92 | }) 93 | } 94 | return str.split('\n').map(function(str) { 95 | return _splitIntoLines(str, max) 96 | }).reduce(function(lines, line) { 97 | return lines.concat(line) 98 | }, []) 99 | } 100 | 101 | /** 102 | * Add spaces and `truncationChar` between words of 103 | * `str` which are longer than `max`. 104 | * 105 | * @param String str string to split 106 | * @param Number max length of each line 107 | * @param Number truncationChar character to append to split words 108 | * @return String 109 | */ 110 | 111 | function splitLongWords(str, max, truncationChar) { 112 | str = str.trim() 113 | var result = [] 114 | var words = str.split(' ') 115 | var remainder = '' 116 | 117 | var truncationWidth = wcwidth(truncationChar) 118 | 119 | while (remainder || words.length) { 120 | if (remainder) { 121 | var word = remainder 122 | remainder = '' 123 | } else { 124 | var word = words.shift() 125 | } 126 | 127 | if (wcwidth(word) > max) { 128 | // slice is based on length no wcwidth 129 | var i = 0 130 | var wwidth = 0 131 | var limit = max - truncationWidth 132 | while (i < word.length) { 133 | var w = wcwidth(word.charAt(i)) 134 | if (w + wwidth > limit) { 135 | break 136 | } 137 | wwidth += w 138 | ++i 139 | } 140 | 141 | remainder = word.slice(i) // get remainder 142 | // save remainder for next loop 143 | 144 | word = word.slice(0, i) // grab truncated word 145 | word += truncationChar // add trailing … or whatever 146 | } 147 | result.push(word) 148 | } 149 | 150 | return result.join(' ') 151 | } 152 | 153 | 154 | /** 155 | * Truncate `str` into total width `max` 156 | * If `str` is shorter than `max`, will return `str` unaltered. 157 | * 158 | * @param String str string to truncated 159 | * @param Number max total wcwidth of output string 160 | * @return String truncated str 161 | */ 162 | 163 | function truncateString(str, max) { 164 | 165 | str = str != null ? str : '' 166 | str = String(str) 167 | 168 | if(max == Infinity) return str 169 | 170 | var i = 0 171 | var wwidth = 0 172 | while (i < str.length) { 173 | var w = wcwidth(str.charAt(i)) 174 | if(w + wwidth > max) 175 | break 176 | wwidth += w 177 | ++i 178 | } 179 | return str.slice(0, i) 180 | } 181 | 182 | 183 | 184 | /** 185 | * Exports 186 | */ 187 | 188 | module.exports.padRight = padRight 189 | module.exports.padCenter = padCenter 190 | module.exports.padLeft = padLeft 191 | module.exports.splitIntoLines = splitIntoLines 192 | module.exports.splitLongWords = splitLongWords 193 | module.exports.truncateString = truncateString 194 | -------------------------------------------------------------------------------- /width.js: -------------------------------------------------------------------------------- 1 | var stripAnsi = require('strip-ansi') 2 | var wcwidth = require('wcwidth') 3 | 4 | module.exports = function(str) { 5 | return wcwidth(stripAnsi(str)) 6 | } 7 | --------------------------------------------------------------------------------