Permalink
Browse files

Tidy search output via columnify.

* Enable line wrapping|truncation via --long option.
* Remove updated time (but leave date) from npm search results.
* Increase space for name from 20 to 30 characters.
* Remove unnecessary brackets around 'prehistoric'.
* Make date column fit the word 'prehistoric' better.
* Tighten space between status and name.
  • Loading branch information...
1 parent f0756e5 commit d5e340cc03eb96b359128ed07f0db43773b2e2c2 @timoxley committed Mar 25, 2012
View
14 doc/cli/npm-search.md
@@ -3,7 +3,7 @@ npm-search(1) -- Search for packages
## SYNOPSIS
- npm search [search terms ...]
+ npm search [--long] [search terms ...]
npm s [search terms ...]
npm se [search terms ...]
@@ -15,6 +15,18 @@ If a term starts with `/`, then it's interpreted as a regular expression.
A trailing `/` will be ignored in this case. (Note that many regular
expression characters must be escaped or quoted in most shells.)
+## CONFIGURATION
+
+### long
+
+* Default: false
+* Type: Boolean
+
+Display full package descriptions and other long text across multiple
+lines. When disabled (default) search results are truncated to fit
+neatly on a single line. Modules with extremely long names will
+fall on multiple lines.
+
## SEE ALSO
* npm-registry(7)
View
167 lib/search.js
@@ -3,6 +3,7 @@ module.exports = exports = search
var npm = require("./npm.js")
, registry = npm.registry
+ , columnify = require('columnify')
search.usage = "npm search [some search terms ...]"
@@ -91,7 +92,8 @@ function stripData (data) {
&& (new Date(data.time.modified).toISOString()
.split("T").join(" ")
.replace(/:[0-9]{2}\.[0-9]{3}Z$/, ""))
- || "(prehistoric)"
+ .slice(0, -5) // remove time
+ || "prehistoric"
}
}
@@ -129,102 +131,77 @@ function match (words, arg) {
}
function prettify (data, args) {
- try {
- var tty = require("tty")
- , stdout = process.stdout
- , cols = !tty.isatty(stdout.fd) ? Infinity
- : process.stdout.getWindowSize()[0]
- cols = (cols == 0) ? Infinity : cols
- } catch (ex) { cols = Infinity }
-
- // name, desc, author, keywords
- var longest = []
- , spaces
- , maxLen = npm.config.get("description")
- ? [20, 60, 20, 20, 10, Infinity]
- : [20, 20, 20, 10, Infinity]
- , headings = npm.config.get("description")
- ? ["NAME", "DESCRIPTION", "AUTHOR", "DATE", "VERSION", "KEYWORDS"]
- : ["NAME", "AUTHOR", "DATE", "VERSION", "KEYWORDS"]
- , lines
- , searchsort = (npm.config.get("searchsort") || "NAME").toLowerCase()
- , sortFields = { name: 0
- , description: 1
- , author: 2
- , date: 3
- , version: 4
- , keywords: 5 }
+ var searchsort = (npm.config.get("searchsort") || "NAME").toLowerCase()
+ , sortField = searchsort.replace(/^\-+/, "")
, searchRev = searchsort.charAt(0) === "-"
- , sortField = sortFields[searchsort.replace(/^\-+/, "")]
+ , truncate = !npm.config.get("long")
+
+ if (Object.keys(data).length === 0) {
+ return "No match found for "+(args.map(JSON.stringify).join(" "))
+ }
- lines = Object.keys(data).map(function (d) {
+ var lines = Object.keys(data).map(function (d) {
+ // strip keyname
return data[d]
- }).map(function (data) {
- // turn a pkg data into a string
- // [name,who,desc,targets,keywords] tuple
- // also set longest to the longest name
- if (typeof data.keywords === "string") {
- data.keywords = data.keywords.split(/[,\s]+/)
+ }).map(function(dat) {
+ dat.author = dat.maintainers
+ delete dat.maintainers
+ dat.date = dat.time
+ delete dat.time
+ return dat
+ }).map(function(dat) {
+ // split keywords on whitespace or ,
+ if (typeof dat.keywords === "string") {
+ dat.keywords = dat.keywords.split(/[,\s]+/)
}
- if (!Array.isArray(data.keywords)) data.keywords = []
- var l = [ data.name
- , data.description || ""
- , data.maintainers.join(" ")
- , data.time
- , data.version || ""
- , (data.keywords || []).join(" ")
- ]
- l.forEach(function (s, i) {
- var len = s.length
- longest[i] = Math.min(maxLen[i] || Infinity
- ,Math.max(longest[i] || 0, len))
- if (len > longest[i]) {
- l._undent = l._undent || []
- l._undent[i] = len - longest[i]
- }
- l[i] = ('' + l[i]).replace(/\s+/g, " ")
- })
- return l
- }).sort(function (a, b) {
- // a and b are "line" objects of [name, desc, maint, time, kw]
+ if (Array.isArray(dat.keywords)) {
+ dat.keywords = dat.keywords.join(' ')
+ }
+
+ // split author on whitespace or ,
+ if (typeof dat.author === "string") {
+ dat.author = dat.author.split(/[,\s]+/)
+ }
+ if (Array.isArray(dat.author)) {
+ dat.author = dat.author.join(' ')
+ }
+ return dat
+ })
+
+ lines.sort(function(a, b) {
var aa = a[sortField].toLowerCase()
, bb = b[sortField].toLowerCase()
return aa === bb ? 0
- : aa < bb ? (searchRev ? 1 : -1)
- : (searchRev ? -1 : 1)
- }).map(function (line) {
- return line.map(function (s, i) {
- spaces = spaces || longest.map(function (n) {
- return new Array(n + 2).join(" ")
- })
- var len = s.length
- if (line._undent && line._undent[i - 1]) {
- len += line._undent[i - 1] - 1
- }
- return s + spaces[i].substr(len)
- }).join(" ").substr(0, cols).trim()
- }).map(function (line) {
- // colorize!
- args.forEach(function (arg, i) {
- line = addColorMarker(line, arg, i)
- })
- return colorize(line).trim()
+ : aa < bb ? -1 : 1
})
- if (lines.length === 0) {
- return "No match found for "+(args.map(JSON.stringify).join(" "))
- }
+ if (searchRev) lines.reverse()
- // build the heading padded to the longest in each field
- return headings.map(function (h, i) {
- var space = Math.max(2, 3 + (longest[i] || 0) - h.length)
- return h + (new Array(space).join(" "))
- }).join("").substr(0, cols).trim() + "\n" + lines.join("\n")
+ var columns = npm.config.get("description")
+ ? ["name", "description", "author", "date", "version", "keywords"]
+ : ["name", "author", "date", "version", "keywords"]
+
+ var output = columnify(lines, {
+ include: columns
+ , truncate: truncate
+ , config: {
+ name: { maxWidth: 40, truncate: false, truncateMarker: '' }
+ , description: { maxWidth: 60 }
+ , author: { maxWidth: 20 }
+ , date: { maxWidth: 11 }
+ , version: { maxWidth: 11 }
+ , keywords: { maxWidth: Infinity }
+ }
+ })
+ output = trimToMaxWidth(output)
+ output = highlightSearchTerms(output, args)
+ return output
}
var colors = [31, 33, 32, 36, 34, 35 ]
, cl = colors.length
+
function addColorMarker (str, arg, i) {
var m = i % cl + 1
, markStart = String.fromCharCode(m)
@@ -260,3 +237,29 @@ function colorize (line) {
var uncolor = npm.color ? "\033[0m" : ""
return line.split("\u0000").join(uncolor)
}
+
+function getMaxWidth() {
+ try {
+ var tty = require("tty")
+ , stdout = process.stdout
+ , cols = !tty.isatty(stdout.fd) ? Infinity
+ : process.stdout.getWindowSize()[0]
+ cols = (cols == 0) ? Infinity : cols
+ } catch (ex) { cols = Infinity }
+ return cols
+}
+
+function trimToMaxWidth(str) {
+ var maxWidth = getMaxWidth()
+ return str.split('\n').map(function(line) {
+ return line.slice(0, maxWidth)
+ }).join('\n')
+}
+
+function highlightSearchTerms(str, terms) {
+ terms.forEach(function (arg, i) {
+ str = addColorMarker(str, arg, i)
+ })
+
+ return colorize(str).trim()
+}
View
189 node_modules/columnify/Readme.md
@@ -0,0 +1,189 @@
+# columnify
+
+[![Build Status](https://travis-ci.org/timoxley/columnify.png?branch=master)](https://travis-ci.org/timoxley/columnify)
+
+Create text-based columns suitable for console output.
+Supports minimum and maximum column widths via truncation and text wrapping.
+
+Designed to [handle sensible wrapping in npm search results](https://github.com/isaacs/npm/pull/2328).
+
+`npm search` before & after integrating columnify:
+
+![npm-tidy-search](https://f.cloud.github.com/assets/43438/1848959/ae02ad04-76a1-11e3-8255-4781debffc26.gif)
+
+## Installation & Update
+
+```
+$ npm install --save columnify@latest
+```
+
+## Usage
+
+```js
+var columnify = require('columnify')
+var columns = columnify(data, options)
+console.log(columns)
+```
+
+## Examples
+
+### Simple Columns
+
+Text is aligned under column headings. Columns are automatically resized
+to fit the content of the largest cell. Each cell will be padded with
+spaces to fill the available space and ensure column contents are
+left-aligned.
+
+```js
+var columnify = require('columnify')
+
+var columns = columnify([{
+ name: 'mod1',
+ version: '0.0.1'
+}, {
+ name: 'module2',
+ version: '0.2.0'
+}])
+
+console.log(columns)
+```
+```
+NAME VERSION
+mod1 0.0.1
+module2 0.2.0
+```
+
+### Wrapping Column Cells
+
+You can define the maximum width before wrapping for individual cells in
+columns. Minimum width is also supported. Wrapping will happen at word
+boundaries. Empty cells or those which do not fill the max/min width
+will be padded with spaces.
+
+```js
+var columnify = require('columnify')
+
+var columns = columnify([{
+ name: 'mod1',
+ description: 'some description which happens to be far larger than the max',
+ version: '0.0.1',
+}, {
+ name: 'module-two',
+ description: 'another description larger than the max',
+ version: '0.2.0',
+})
+
+console.log(columns)
+```
+```
+NAME DESCRIPTION VERSION
+mod1 some description which happens 0.0.1
+ to be far larger than the max
+module-two another description larger 0.2.0
+ than the max
+```
+
+### Truncated Columns
+
+You can disable wrapping and instead truncate content at the maximum
+column width. Truncation respects word boundaries. A truncation marker,
+`…` will appear next to the last word in any truncated line.
+
+```js
+var columns = columnify(data, {
+ truncate: true,
+ config: {
+ description: {
+ maxWidth: 20
+ }
+ }
+})
+
+console.log(columns)
+```
+
+```
+NAME DESCRIPTION VERSION
+mod1 some description… 0.0.1
+module-two another description… 0.2.0
+```
+
+
+### Custom Truncation Marker
+
+You can change the truncation marker to something other than the default
+`…`.
+
+```js
+var columns = columnify(data, {
+ truncate: true,
+ truncateMarker: '>',
+ widths: {
+ description: {
+ maxWidth: 20
+ }
+ }
+})
+
+console.log(columns)
+```
+
+```
+NAME DESCRIPTION VERSION
+mod1 some description> 0.0.1
+module-two another description> 0.2.0
+```
+
+### Custom Column Splitter
+
+If your columns need some bling, you can split columns with custom
+characters.
+
+```js
+
+var columns = columnify(data, {
+ columnSplitter: ' | '
+})
+
+console.log(columns)
+```
+```
+NAME | DESCRIPTION | VERSION
+mod1 | some description which happens to be far larger than the max | 0.0.1
+module-two | another description larger than the max | 0.2.0
+```
+
+### Filtering & Ordering Columns
+
+By default, all properties are converted into columns, whether or not
+they exist on every object or not.
+
+To explicitly specify which columns to include, and in which order,
+supply an "include" array:
+
+```js
+var data = [{
+ name: 'module1',
+ description: 'some description',
+ version: '0.0.1',
+}, {
+ name: 'module2',
+ description: 'another description',
+ version: '0.2.0',
+}]
+
+var columns = columnify(data, {
+ include: ['name', 'version'] // note description not included
+})
+
+console.log(columns)
+```
+
+```
+NAME VERSION
+module1 0.0.1
+module2 0.2.0
+```
+## License
+
+MIT
View
210 node_modules/columnify/index.js
@@ -0,0 +1,210 @@
+"use strict"
+
+var utils = require('./utils')
+var padRight = utils.padRight
+var splitIntoLines = utils.splitIntoLines
+var splitLongWords = utils.splitLongWords
+
+var DEFAULTS = {
+ maxWidth: Infinity,
+ minWidth: 0,
+ columnSplitter: ' ',
+ truncate: false,
+ truncateMarker: '',
+ headingTransform: function(key) {
+ return key.toUpperCase()
+ },
+ dataTransform: function(cell, column, index) {
+ return cell
+ }
+}
+
+module.exports = function(items, options) {
+
+ options = options || {}
+
+ var columnConfigs = options.config || {}
+ delete options.config // remove config so doesn't appear on every column.
+
+ // Option defaults inheritance:
+ // options.config[columnName] => options => DEFAULTS
+ options = mixin(options, DEFAULTS)
+ options.config = options.config || Object.create(null)
+
+ options.spacing = options.spacing || '\n' // probably useless
+
+ var columnNames = options.include || [] // optional user-supplied columns to include
+
+ // if not suppled column names, automatically determine columns from data keys
+ if (!columnNames.length) {
+ items.forEach(function(item) {
+ for (var columnName in item) {
+ if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName)
+ }
+ })
+ }
+
+ // initialize column defaults (each column inherits from options.config)
+ var columns = columnNames.reduce(function(columns, columnName) {
+ var column = Object.create(options)
+ columns[columnName] = mixin(column, columnConfigs[columnName])
+ return columns
+ }, Object.create(null))
+
+ // sanitize column settings
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ column.maxWidth = Math.ceil(column.maxWidth)
+ column.minWidth = Math.ceil(column.minWidth)
+ column.truncate = !!column.truncate
+ })
+
+ // sanitize data
+ items = items.map(function(item) {
+ var result = Object.create(null)
+ columnNames.forEach(function(columnName) {
+ // null/undefined -> ''
+ result[columnName] = item[columnName] != null ? item[columnName] : ''
+ // toString everything
+ result[columnName] = '' + result[columnName]
+ // remove funky chars
+ result[columnName] = result[columnName].replace(/\s+/g, " ")
+ })
+ return result
+ })
+
+ // transform data cells
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ items = items.map(function(item, index) {
+ item[columnName] = column.dataTransform(item[columnName], column, index)
+ return item
+ })
+ })
+
+ // add headers
+ var headers = {}
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ headers[columnName] = column.headingTransform(columnName)
+ })
+ items.unshift(headers)
+
+ // get actual max-width between min & max
+ // based on length of data in columns
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ column.width = items.map(function(item) {
+ return item[columnName]
+ }).reduce(function(min, cur) {
+ return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur.length)))
+ }, 0)
+ })
+
+ // split long words so they can break onto multiple lines
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ items = items.map(function(item) {
+ item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker)
+ return item
+ })
+ })
+
+ // wrap long lines. each item is now an array of lines.
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ items = items.map(function(item, index) {
+ var cell = item[columnName]
+ item[columnName] = splitIntoLines(cell, column.width)
+
+ // if truncating required, only include first line + add truncation char
+ if (column.truncate && item[columnName].length > 1) {
+ item[columnName] = splitIntoLines(cell, column.width - column.truncateMarker.length)
+ var firstLine = item[columnName][0]
+ if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker
+ item[columnName] = item[columnName].slice(0, 1)
+ }
+ return item
+ })
+ })
+
+ // recalculate column widths from truncated output/lines
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ column.width = items.map(function(item) {
+ return item[columnName].reduce(function(min, cur) {
+ return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur.length)))
+ }, 0)
+ }).reduce(function(min, cur) {
+ return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)))
+ }, 0)
+ })
+
+ var rows = createRows(items, columns, columnNames) // merge lines into rows
+
+ // conceive output
+ return rows.reduce(function(output, row) {
+ return output.concat(row.reduce(function(rowOut, line) {
+ return rowOut.concat(line.join(options.columnSplitter))
+ }, []))
+ }, []).join(options.spacing)
+}
+
+/**
+ * Convert wrapped lines into rows with padded values.
+ *
+ * @param Array items data to process
+ * @param Array columns column width settings for wrapping
+ * @param Array columnNames column ordering
+ * @return Array items wrapped in arrays, corresponding to lines
+ */
+
+function createRows(items, columns, columnNames) {
+ return items.map(function(item) {
+ var row = []
+ var numLines = 0
+ columnNames.forEach(function(columnName) {
+ numLines = Math.max(numLines, item[columnName].length)
+ })
+ // combine matching lines of each rows
+ for (var i = 0; i < numLines; i++) {
+ row[i] = row[i] || []
+ columnNames.forEach(function(columnName) {
+ var column = columns[columnName]
+ var val = item[columnName][i] || '' // || '' ensures empty columns get padded
+ row[i].push(padRight(val, column.width))
+ })
+ }
+ return row
+ })
+}
+
+/**
+ * Generic source->target mixin.
+ * Copy properties from `source` into `target` if target doesn't have them.
+ * Destructive. Modifies `target`.
+ *
+ * @param target Object target for mixin properties.
+ * @param source Object source of mixin properties.
+ * @return Object `target` after mixin applied.
+ */
+
+function mixin(target, source) {
+ source = source || {}
+ for (var key in source) {
+ if (target.hasOwnProperty(key)) continue
+ target[key] = source[key]
+ }
+ return target
+}
+
+/**
+ * Adapted from String.prototype.endsWith polyfill.
+ */
+
+function endsWith(target, searchString, position) {
+ position = position || target.length;
+ position = position - searchString.length;
+ var lastIndex = target.lastIndexOf(searchString);
+ return lastIndex !== -1 && lastIndex === position;
+}
View
42 node_modules/columnify/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "columnify",
+ "version": "0.1.2",
+ "description": "Render data in text columns, supports in-column text-wrap.",
+ "main": "index.js",
+ "scripts": {
+ "test": "tap test"
+ },
+ "author": {
+ "name": "Tim Oxley"
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "tape": "~2.3.0",
+ "tap": "~0.4.6"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/timoxley/columnify.git"
+ },
+ "keywords": [
+ "column",
+ "text",
+ "ansi",
+ "console",
+ "terminal",
+ "wrap",
+ "table"
+ ],
+ "bugs": {
+ "url": "https://github.com/timoxley/columnify/issues"
+ },
+ "homepage": "https://github.com/timoxley/columnify",
+ "readme": "# columnify\n\n[![Build Status](https://travis-ci.org/timoxley/columnify.png?branch=master)](https://travis-ci.org/timoxley/columnify)\n\nCreate text-based columns suitable for console output. \nSupports minimum and maximum column widths via truncation and text wrapping.\n\nDesigned to [handle sensible wrapping in npm search results](https://github.com/isaacs/npm/pull/2328).\n\n`npm search` before & after integrating columnify:\n\n![npm-tidy-search](https://f.cloud.github.com/assets/43438/1848959/ae02ad04-76a1-11e3-8255-4781debffc26.gif)\n\n## Installation & Update\n\n```\n$ npm install --save columnify@latest\n```\n\n## Usage\n\n```js\nvar columnify = require('columnify')\nvar columns = columnify(data, options)\nconsole.log(columns)\n```\n\n## Examples\n\n### Simple Columns\n\nText is aligned under column headings. Columns are automatically resized\nto fit the content of the largest cell. Each cell will be padded with\nspaces to fill the available space and ensure column contents are\nleft-aligned.\n\n```js\nvar columnify = require('columnify')\n\nvar columns = columnify([{\n name: 'mod1',\n version: '0.0.1'\n}, {\n name: 'module2',\n version: '0.2.0'\n}])\n\nconsole.log(columns)\n```\n```\nNAME VERSION\nmod1 0.0.1 \nmodule2 0.2.0 \n```\n\n### Wrapping Column Cells\n\nYou can define the maximum width before wrapping for individual cells in\ncolumns. Minimum width is also supported. Wrapping will happen at word\nboundaries. Empty cells or those which do not fill the max/min width\nwill be padded with spaces.\n\n```js\nvar columnify = require('columnify')\n\nvar columns = columnify([{\n name: 'mod1',\n description: 'some description which happens to be far larger than the max',\n version: '0.0.1',\n}, {\n name: 'module-two',\n description: 'another description larger than the max',\n version: '0.2.0',\n})\n\nconsole.log(columns)\n```\n```\nNAME DESCRIPTION VERSION\nmod1 some description which happens 0.0.1\n to be far larger than the max\nmodule-two another description larger 0.2.0\n than the max\n```\n\n### Truncated Columns\n\nYou can disable wrapping and instead truncate content at the maximum\ncolumn width. Truncation respects word boundaries. A truncation marker,\n`…` will appear next to the last word in any truncated line.\n\n```js\nvar columns = columnify(data, {\n truncate: true,\n config: {\n description: {\n maxWidth: 20\n }\n }\n})\n\nconsole.log(columns)\n```\n\n```\nNAME DESCRIPTION VERSION\nmod1 some description… 0.0.1 \nmodule-two another description… 0.2.0 \n```\n\n\n### Custom Truncation Marker\n\nYou can change the truncation marker to something other than the default\n`…`.\n\n```js\nvar columns = columnify(data, {\n truncate: true,\n truncateMarker: '>',\n widths: {\n description: {\n maxWidth: 20\n }\n }\n})\n\nconsole.log(columns)\n```\n\n```\nNAME DESCRIPTION VERSION\nmod1 some description> 0.0.1 \nmodule-two another description> 0.2.0 \n```\n\n### Custom Column Splitter\n\nIf your columns need some bling, you can split columns with custom\ncharacters.\n\n```js\n\nvar columns = columnify(data, {\n columnSplitter: ' | '\n})\n\nconsole.log(columns)\n```\n```\nNAME | DESCRIPTION | VERSION\nmod1 | some description which happens to be far larger than the max | 0.0.1\nmodule-two | another description larger than the max | 0.2.0\n```\n\n### Filtering & Ordering Columns\n\nBy default, all properties are converted into columns, whether or not\nthey exist on every object or not.\n\nTo explicitly specify which columns to include, and in which order,\nsupply an \"include\" array:\n\n```js\nvar data = [{\n name: 'module1',\n description: 'some description',\n version: '0.0.1',\n}, {\n name: 'module2',\n description: 'another description',\n version: '0.2.0',\n}]\n\nvar columns = columnify(data, {\n include: ['name', 'version'] // note description not included\n})\n\nconsole.log(columns)\n```\n\n```\nNAME VERSION\nmodule1 0.0.1\nmodule2 0.2.0\n```\n## License\n\nMIT\n",
+ "readmeFilename": "Readme.md",
+ "_id": "columnify@0.1.2",
+ "dist": {
+ "shasum": "ab1a1f1e37b26ba4b87c6920fb717fe51c827042"
+ },
+ "_from": "columnify@0.1.2",
+ "_resolved": "https://registry.npmjs.org/columnify/-/columnify-0.1.2.tgz"
+}
View
76 node_modules/columnify/utils.js
@@ -0,0 +1,76 @@
+/**
+ * Pad `str` up to total length `max` with `chr`.
+ * If `str` is longer than `max`, padRight will return `str` unaltered.
+ *
+ * @param String str string to pad
+ * @param Number max total length of output string
+ * @param String chr optional. Character to pad with. default: ' '
+ * @return String padded str
+ */
+
+function padRight(str, max, chr) {
+ str = str != null ? str : ''
+ str = String(str)
+ var length = 1 + max - str.length
+ if (length <= 0) return str
+ return str + Array.apply(null, {length: length})
+ .join(chr || ' ')
+}
+
+/**
+ * Split a String `str` into lines of maxiumum length `max`.
+ * Splits on word boundaries.
+ *
+ * @param String str string to split
+ * @param Number max length of each line
+ * @return Array Array containing lines.
+ */
+
+function splitIntoLines(str, max) {
+ return str.trim().split(' ').reduce(function(lines, word) {
+ var line = lines[lines.length - 1]
+ if (line && line.join(' ').length + word.length < max) {
+ lines[lines.length - 1].push(word) // add to line
+ }
+ else lines.push([word]) // new line
+ return lines
+ }, []).map(function(l) {
+ return l.join(' ')
+ })
+}
+
+/**
+ * Add spaces and `truncationChar` between words of
+ * `str` which are longer than `max`.
+ *
+ * @param String str string to split
+ * @param Number max length of each line
+ * @param Number truncationChar character to append to split words
+ * @return String
+ */
+
+function splitLongWords(str, max, truncationChar, result) {
+ str = str.trim()
+ result = result || []
+ if (!str) return result.join(' ') || ''
+ var words = str.split(' ')
+ var word = words.shift() || str
+
+ if (word.length > max) {
+ var remainder = word.slice(max - truncationChar.length) // get remainder
+ words.unshift(remainder) // save remainder for next loop
+
+ word = word.slice(0, max - truncationChar.length) // grab truncated word
+ word += truncationChar // add trailing … or whatever
+ }
+ result.push(word)
+ return splitLongWords(words.join(' '), max, truncationChar, result)
+}
+
+/**
+ * Exports
+ */
+
+module.exports.padRight = padRight
+module.exports.splitIntoLines = splitIntoLines
+module.exports.splitLongWords = splitLongWords
View
6 package.json
@@ -79,7 +79,8 @@
"text-table": "~0.2.0",
"ansicolors": "~0.3.2",
"ansistyles": "~0.1.3",
- "path-is-inside": "~1.0.0"
+ "path-is-inside": "~1.0.0",
+ "columnify": "0.1.2"
},
"bundleDependencies": [
"semver",
@@ -129,7 +130,8 @@
"text-table",
"ansicolors",
"ansistyles",
- "path-is-inside"
+ "path-is-inside",
+ "columnify"
],
"devDependencies": {
"ronn": "~0.3.6",

0 comments on commit d5e340c

Please sign in to comment.