diff --git a/README.md b/README.md index 58543fa..ae7f5a5 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ Ascii Table 3 [![Build stats](https://travis-ci.com/AllMightySauron/ascii-table3.png)](https://travis-ci.com/AllMightySauron/ascii-table3) [![npm version](https://badge.fury.io/js/ascii-table3.png)](https://badge.fury.io/js/ascii-table3) -`ascii-table3` is a pure ascii table renderer and beautifier, heavily inspired by the ascii-table package created by Beau Sorensen (http://github.com/sorensen). The original package lacked support for multiple table styles and that is what motivated me to create this new one. +`ascii-table3` is a pure ascii table renderer and beautifier, heavily inspired by the `ascii-table` package created by Beau Sorensen. The original package lacked support for multiple table styles and that is what motivated me to create this new one. Currently with **over a dozen** predefined table styles, the collection style keeps growing. I am pretty sure there is a style for everyone. If not, you can even design your own custom stye and add it to the library! +Please direct any issues, suggestions or feature requests to the [ascii-table3 github] (https://github.com/AllMightySauron/ascii-table3) page. + Existing code for the original `ascii-table` package should run fine with very few changes (see examples below). # Usage @@ -146,11 +148,24 @@ var table = AsciiTable3.AsciiTable3('Data'); Returns wether a `val` is numeric or not, irrespective of its type. +* `val` - value to check + ```javascript AsciiTable3.isNumeric('test') // false AsciiTable3.isNumeric(10) // true AsciiTable3.isNumeric(3.14) // true ``` +### AsciiTable3.isWhiteSpace(str) + +Return whether this character is whitespace (used internally for word wrapping purposes). + +* `str` - character to test + +```javascript +AsciiTable3.isWhiteSpace(' '') // true +AsciiTable3.isWhiteSpace('\t') // true +AsciiTable3.isWhiteSpace('*') // false +``` ### AsciiTable3.align(direction, val, len, [pad]) @@ -218,6 +233,30 @@ Example: AsciiTable3.align(AsciiTable3.LEFT, 'hey', 7) // 'hey ' ``` +### AsciiTable3.wordWrap(str, maxWidth) + +Wraps a string into multiple lines of a limited width. + +* `str` - string to wrap +* `maxWidth` - maximum width for the wrapped string + +```javascript +AsciiTable3.wordWrap('dummy', 5) // dummy + +AsciiTable3.wordWrap('this is a test', 5) +// this +// is a +// test + +AsciiTable3.wordWrap('this is a test', 3) +// thi +// s +// is +// a +// tes +// t +``` + ### AsciiTable3.truncateString(str, len) Truncates a string up to a maximum number of characters (if needed). @@ -957,6 +996,72 @@ table.setAlignRight(2); table.getAlign(2) // AsciiTable3.RIGHT ``` +#### instance.setWrapped(idx, [wrap]) + +Sets the wrapping property for a specific column (wrapped content will generate more than one data row if needed). + +* `idx` - column to wrap (starts at 1). +* `wrap` - wrap boolean setting (default is true). + +```javascript +var table = + new AsciiTable3.AsciiTable3('Sample table') + .setHeading('Name', 'Age', 'Eye color') + .setAlign(3, AsciiTable3.CENTER) + .addRowMatrix([ + ['James Bond', 41, 'blue'], + ['Harry Potter', 18, 'brown'], + ['Scooby Doo', 23, 'brown'], + ['Mickey Mouse', 120, 'black'] + ]); + +// first column width is 8 characters and wrapped +table.setWidth(1, 8).setWrapped(1); + +console.log(table.toString()); +``` +```asciidoc +.------------------------------. +| Sample table | +:--------.--------.------------: +| Name | Age | Eye color | +:--------+--------+------------: +| James | 41 | blue | +| Bond | | | +| Harry | 18 | brown | +| Potter | | | +| Scooby | 23 | brown | +| Doo | | | +| Mickey | 120 | black | +| Mouse | | | +'--------'--------'------------' +``` + +#### instance.isWrapped(idx) + +Gets the wrapping setting for a given column (true or false). + +* `idx` - column to check (starts at 1) + +```javascript +var table = + new AsciiTable3.AsciiTable3('Sample table') + .setHeading('Name', 'Age', 'Eye color') + .setAlign(3, AsciiTable3.CENTER) + .addRowMatrix([ + ['James Bond', 41, 'blue'], + ['Harry Potter', 18, 'brown'], + ['Scooby Doo', 23, 'brown'], + ['Mickey Mouse', 120, 'black'] + ]); + +// first column width is 8 characters and wrapped +table.setWidth(1, 8).setWrapped(1); + +table.isWrapped(1) // true +table.isWrapped(2) // false +``` + #### instance.setCellMargin(margin) Sets internal margin for cell data (table default is 1). diff --git a/ascii-table3.js b/ascii-table3.js index 23e93dd..d6ec518 100644 --- a/ascii-table3.js +++ b/ascii-table3.js @@ -72,6 +72,18 @@ class AsciiTable3 { return !isNaN(parseFloat(value)) && isFinite(value); } + /** + * Returns on whether the character is white space. + * @static + * @param {string} x The character to test. + * @returns {boolean} Whether we have found a white char. + */ + static isWhiteSpace(x) { + var white = new RegExp(/^\s$/); + + return white.test(x.charAt(0)); + } + /** * Generic string alignment. * @static @@ -144,6 +156,43 @@ class AsciiTable3 { } } + /** + * Wraps a string into multiple lines of a limited width. + * @param {string} str The string to wrap. + * @param {num} maxWidth The maximum width for the wrapped string. + * @returns {string} The wrapped string. + */ + static wordWrap(str, maxWidth) { + const NEW_LINE = "\n"; + + // make sure we have a string as parameter + str = '' + str; + + var found = false; + var res = ''; + + while (str.length > maxWidth) { + found = false; + // Inserts new line at first whitespace of the line + for (var i = maxWidth - 1; i >= 0; i--) { + if (AsciiTable3.isWhiteSpace(str.charAt(i))) { + res += str.substring(0, i).trimStart() + NEW_LINE; + str = str.slice(i + 1); + found = true; + break; + } + } + + // Inserts new line at maxWidth position, the word is too long to wrap + if (!found) { + res += str.substring(0, maxWidth).trimStart() + NEW_LINE; + str = str.slice(maxWidth); + } + } + + return res + str.trimStart(); + } + /** * Truncates a string up to a maximum number of characters (if needed). * @static @@ -617,6 +666,43 @@ class AsciiTable3 { return this.setAlign(idx, AlignmentEnum.CENTER); } + /** + * Sets the wrapping property for a specific column (wrapped content will generate more than one data row if needed). + * @param {number} idx Column index to align (starts at 1). + * @param {boolean} wrap Whether to wrap the content (default is true). + * @returns {AsciiTable3} The AsciiTable3 object instance. + */ + setWrapped(idx, wrap = true) { + if (this.wrapping) { + // resize if needed + AsciiTable3.arrayResize(this.wrapping, idx); + } else { + // create array + this.wrapping = AsciiTable3.arrayFill(idx); + } + + // arrays are 0-based + this.wrapping[idx - 1] = wrap; + + return this; + } + + /** + * Gets the wrapping setting for a given column. + * @param {number} idx Column index to get wrapping (starts at 1). + */ + isWrapped(idx) { + // wrapping defaults to false + var result = false; + + if (this.wrapping && idx <= this.wrapping.length) { + // arrays are 0-based + result = this.wrapping[idx - 1]; + } + + return result; + } + /** * Return the JSON representation of the table, this also allows us to call JSON.stringify on the instance. * @returns {string} The table JSON representation. @@ -747,19 +833,59 @@ class AsciiTable3 { } /** - * Get string with the rendering of a heading row. + * Get array of wrapped row data from a "normal" row. + * @private + * @param {*[]} row Row of data. + * @returns Array of data rows after word wrapping. + */ + getWrappedRows(row) { + // setup a new wrapped row + const wrappedRow = AsciiTable3.arrayFill(row.length); + + var maxRows = 1; + + // loop over columns and wrap + for (var col = 0; col < row.length; col++) { + const cell = row[col]; + + if (this.getWidth(col + 1) && this.isWrapped(col + 1)) { + wrappedRow[col] = AsciiTable3.wordWrap(cell, this.getWidth(col + 1) - this.getCellMargin() * 2).split("\n"); + + if (wrappedRow[col].length > maxRows) maxRows = wrappedRow[col].length; + } else { + wrappedRow[col] = [ cell ]; + } + } + + // create resulting array with (potentially) multiple rows + const result = AsciiTable3.arrayFill(maxRows); + for (var i = 0; i < maxRows; i++) { + result[i] = AsciiTable3.arrayFill(row.length, ''); + } + + // fill in values + for (var nCol = 0; nCol < row.length; nCol++) { + for (var nRow = 0; nRow < wrappedRow[nCol].length; nRow++) { + result[nRow][nCol] = wrappedRow[nCol][nRow]; + } + } + + return result; + } + + /** + * Get string with the rendering of a heading row (truncating if needed). * @private * @param {Style} posStyle The heading row style. * @param {number[]} colsWidth Array with the desired width for each heading column. + * @param {string} row The heading row to generate. * @returns {string} String representation of table heading row line. */ - getHeadingRow(posStyle, colsWidth) { - const heading = this.getHeading(); - + getHeadingRowTruncated(posStyle, colsWidth, row) { var result = posStyle.left; - for (var col = 0; col < heading.length; col++) { - const cell = '' + heading[col]; + for (var col = 0; col < row.length; col++) { + const cell = '' + row[col]; // align contents disregarding margins const cellAligned = AsciiTable3.align(this.getHeadingAlign(), cell, colsWidth[col] - this.getCellMargin() * 2); @@ -768,7 +894,7 @@ class AsciiTable3 { AsciiTable3.truncateString(cellAligned, colsWidth[col] - this.getCellMargin() * 2) + ''.padStart(this.getCellMargin()); - if (col < heading.length - 1) result += posStyle.colSeparator; + if (col < row.length - 1) result += posStyle.colSeparator; } result += posStyle.right + '\n'; @@ -776,14 +902,34 @@ class AsciiTable3 { } /** - * Get string with the rendering of a data row. + * Get string with the rendering of a heading row. + * @private + * @param {Style} posStyle The heading row style. + * @param {number[]} colsWidth Array with the desired width for each heading column. + * @returns {string} String representation of table heading row line. + */ + getHeadingRow(posStyle, colsWidth) { + var result = ''; + + // wrap heading if needed + const rows = this.getWrappedRows(this.getHeading()); + + rows.forEach(aRow => { + result += this.getHeadingRowTruncated(posStyle, colsWidth, aRow); + }); + + return result; + } + + /** + * Get string with the rendering of a data row (truncating if needed). * @private * @param {Style} posStyle The data row style. * @param {number[]} colsWidth Array with the desired width for each data column. * @param {*[]} row Array with cell values for this row. * @returns {string} String representation of table data row line. */ - getDataRow(posStyle, colsWidth, row) { + getDataRowTruncated(posStyle, colsWidth, row) { var result = posStyle.left; // loop over data columns in row @@ -804,6 +950,27 @@ class AsciiTable3 { return result; } + /** + * Get string with the rendering of a data row (please not that it may result in several rows, depending on wrap settings). + * @private + * @param {Style} posStyle The data row style. + * @param {number[]} colsWidth Array with the desired width for each data column. + * @param {*[]} row Array with cell values for this row. + * @returns {string} String representation of table data row line. + */ + getDataRow(posStyle, colsWidth, row) { + var result = ''; + + // wrap data row if needed + const rows = this.getWrappedRows(row); + + rows.forEach(aRow => { + result += this.getDataRowTruncated(posStyle, colsWidth, aRow); + }); + + return result; + } + /** * Render the instance as a string for output. * @returns {string} String rendiring of this instance table. diff --git a/package.json b/package.json index c928540..7f03b94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ascii-table3", - "version": "0.3.0", + "version": "0.4.0", "author": "João Simões (https://github.com/AllMightySauron)", "description": "Javascript ASCII renderer for beautiful console-based tables", "repository": { diff --git a/samples.js b/samples.js index b1dfa29..39bb7c5 100644 --- a/samples.js +++ b/samples.js @@ -77,11 +77,20 @@ table.addStyle(roundedStyle); table.setStyle("rounded"); console.log(`rounded style:\n${table.toString()}`); -// example 6. no title or heading -console.log ('>>>>>> example 6: no title or heading'); +// example 6. row data wrapping +console.log ('>>>>>> example 6: row data wrapping'); +table.clearRows(); +table.addRow('James Bond', 41, 'blue').addRow('Harry Potter', 18, 'brown').addRow('Scooby Doo', 23, 'brown').addRow('Mickey Mouse', 120, 'black'); +table.setWidth(1, 8).setWrapped(1); + +console.log(`data wrapping:\n${table.toString()}`); + +// example 7. no title or heading +console.log ('>>>>>> example 7: no title or heading'); table.setTitle(); table.setHeading(); table.setStyle('ramac'); console.log(`no title/heading:\n${table.toString()}`); + diff --git a/test/ascii-table3.test.js b/test/ascii-table3.test.js index b485e0c..b48e1e4 100644 --- a/test/ascii-table3.test.js +++ b/test/ascii-table3.test.js @@ -43,6 +43,21 @@ describe('String methods', () => { it('Truncate string (small)', () => { assert.strictEqual(AsciiTable3.AsciiTable3.truncateString('Dummy 1', 2), '..'); }); + + // word wrapping + it('Test for white space', () => { + assert.strictEqual(AsciiTable3.AsciiTable3.isWhiteSpace(' '), true); + assert.strictEqual(AsciiTable3.AsciiTable3.isWhiteSpace('\t'), true); + assert.strictEqual(AsciiTable3.AsciiTable3.isWhiteSpace('\n'), true); + + assert.strictEqual(AsciiTable3.AsciiTable3.isWhiteSpace('a'), false); + }); + it('Word wrapping', () => { + assert.strictEqual(AsciiTable3.AsciiTable3.wordWrap('dummy', 5), 'dummy'); + assert.strictEqual(AsciiTable3.AsciiTable3.wordWrap('this is a test', 5), 'this\nis a\ntest'); + assert.strictEqual(AsciiTable3.AsciiTable3.wordWrap('this is a test', 3), 'thi\ns\nis\na\ntes\nt'); + assert.strictEqual(AsciiTable3.AsciiTable3.wordWrap('rate (%)', 4), 'rate\n(%)'); + }); }); @@ -215,6 +230,17 @@ describe('Styling', () => { assert.strictEqual(asciiTable.getAlign(3), AsciiTable3.AUTO); }); + it('setWrapped/isWrapped', () => { + // default is false + assert.strictEqual(asciiTable.isWrapped(1), false); + + asciiTable.setWrapped(2); + assert.strictEqual(asciiTable.isWrapped(2), true); + + asciiTable.setWrapped(2, false); + assert.strictEqual(asciiTable.isWrapped(2), false); + }); + it('addStyle', () => { const roundedStyle = { name: "rounded", @@ -382,6 +408,68 @@ describe('Rendering', () => { ); }); + it ('toString (wrapping)', () => { + asciiTable + .setWidths().setWidth(1, 7) + .setWrapped(1).setAlign(1, AsciiTable3.LEFT) + .setStyle('ramac').setCellMargin(1); + + assert.strictEqual(asciiTable.toString(), + '+--------------------------+\n' + + '| Dummy title |\n' + + '+-------+-------+----------+\n' + + '| Title | Count | Rate (%) |\n' + + '+-------+-------+----------+\n' + + '| Dummy | 10 | 2.3 |\n' + + '| 1 | | |\n' + + '| Dummy | 5 | 3.1 |\n' + + '| 2 | | |\n' + + '| Dummy | 100 | 3.14 |\n' + + '| 3 | | |\n' + + '| Dummy | 0 | 1 |\n' + + '| 4 | | |\n' + + '+-------+-------+----------+\n' + ); + + asciiTable.setWidth(3, 6).setWrapped(3); + assert.strictEqual(asciiTable.toString(), + '+----------------------+\n' + + '| Dummy title |\n' + + '+-------+-------+------+\n' + + '| Title | Count | Rate |\n' + + '| | | (%) |\n' + + '+-------+-------+------+\n' + + '| Dummy | 10 | 2.3 |\n' + + '| 1 | | |\n' + + '| Dummy | 5 | 3.1 |\n' + + '| 2 | | |\n' + + '| Dummy | 100 | 3.14 |\n' + + '| 3 | | |\n' + + '| Dummy | 0 | 1 |\n' + + '| 4 | | |\n' + + '+-------+-------+------+\n' + ); + + asciiTable.setAlign(1, AsciiTable3.CENTER); + assert.strictEqual(asciiTable.toString(), + '+----------------------+\n' + + '| Dummy title |\n' + + '+-------+-------+------+\n' + + '| Title | Count | Rate |\n' + + '| | | (%) |\n' + + '+-------+-------+------+\n' + + '| Dummy | 10 | 2.3 |\n' + + '| 1 | | |\n' + + '| Dummy | 5 | 3.1 |\n' + + '| 2 | | |\n' + + '| Dummy | 100 | 3.14 |\n' + + '| 3 | | |\n' + + '| Dummy | 0 | 1 |\n' + + '| 4 | | |\n' + + '+-------+-------+------+\n' + ); + }); + it ('toString (no heading)', () => { const aTable = new AsciiTable3.AsciiTable3('Dummy title') .setAlign(1, AsciiTable3.LEFT)