From ed4afb7417fec78c1548dcf422d14b7a0b452f60 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 4 Apr 2022 22:25:26 -0600 Subject: [PATCH] Hyperlink support for cells (#292) * Failing tests for hyperlink support * Add hyperlink support for cells (#73) * Add hyperlink example to basic usage docs --- basic-usage.md | 28 +++++++++++++++++++++++ examples/basic-usage-examples.js | 36 +++++++++++++++++++++++++++++ lib/print-example.js | 14 +++++++++++- src/cell.js | 39 ++++++++++++++++++++++++-------- src/utils.js | 12 ++++++++++ test/utils-test.js | 12 ++++++++++ 6 files changed, 130 insertions(+), 11 deletions(-) diff --git a/basic-usage.md b/basic-usage.md index 1aab54c..310c162 100644 --- a/basic-usage.md +++ b/basic-usage.md @@ -166,3 +166,31 @@ table.push(['Wrap', 'Text']); ``` + +##### Supports hyperlinking cell content using the href option + ┌───────────┬─────┬─────┐ + │ Text Link │ Hel │ htt │ + │ │ lo │ p:/ │ + │ │ Lin │ /ex │ + │ │ k │ amp │ + │ │ │ le. │ + │ │ │ com │ + ├───────────┴─────┴─────┤ + │ http://example.com │ + └───────────────────────┘ + + Note: Links are not displayed in documentation examples. +```javascript + const table = new Table({ + colWidths: [11, 5, 5], + style: { border: [], head: [] }, + wordWrap: true, + wrapOnWordBoundary: false, + }); + const href = 'http://example.com'; + table.push( + [{ content: 'Text Link', href }, { content: 'Hello Link', href }, { href }], + [{ href, colSpan: 3 }] + ); +``` + diff --git a/examples/basic-usage-examples.js b/examples/basic-usage-examples.js index a9dc99b..fcfb5fa 100644 --- a/examples/basic-usage-examples.js +++ b/examples/basic-usage-examples.js @@ -1,5 +1,6 @@ const Table = require('../src/table'); const colors = require('@colors/colors/safe'); +const { hyperlink } = require('../src/utils'); // prettier-ignore // Disable prettier so that examples are formatted more clearly @@ -235,6 +236,40 @@ module.exports = function (runTest) { return [makeTable, expected]; }); + + it('Supports hyperlinking cell content using the href option', () => { + function link(text) { + return hyperlink('http://example.com', text); + } + function makeTable() { + const table = new Table({ + colWidths: [11, 5, 5], + style: { border: [], head: [] }, + wordWrap: true, + wrapOnWordBoundary: false, + }); + const href = 'http://example.com'; + table.push( + [{ content: 'Text Link', href }, { content: 'Hello Link', href }, { href }], + [{ href, colSpan: 3 }] + ); + return table; + } + + let expected = [ + '┌───────────┬─────┬─────┐', + `│ ${link('Text Link')} │ ${link('Hel')} │ ${link('htt')} │`, + `│ │ ${link('lo ')} │ ${link('p:/')} │`, + `│ │ ${link('Lin')} │ ${link('/ex')} │`, + `│ │ ${link('k')} │ ${link('amp')} │`, + `│ │ │ ${link('le.')} │`, + `│ │ │ ${link('com')} │`, + '├───────────┴─────┴─────┤', + `│ ${link('http://example.com')} │`, + '└───────────────────────┘', + ]; + return [makeTable, expected]; + }); }; /* Expectation - ready to be copy/pasted and filled in. DO NOT DELETE THIS @@ -250,3 +285,4 @@ module.exports = function (runTest) { , '└──┴───┴──┴──┘' ]; */ +// Jest Snapshot v1, https://goo.gl/fbAQLP diff --git a/lib/print-example.js b/lib/print-example.js index 27f5cff..3d24de5 100644 --- a/lib/print-example.js +++ b/lib/print-example.js @@ -17,6 +17,18 @@ function logExample(fn) { ); } +function replaceLinks(str) { + const matches = str.match(/\x1B\]8;;[^\x07]+\x07[^\]]+\x1B\]8;;\x07/g); + if (matches) { + matches.forEach((match) => { + const [, text] = match.match(/\x07([^\]|\x1B]+)\x1B/); + str = str.replace(match, text); + }); + str += '\n\nNote: Links are not displayed in documentation examples.'; + } + return str; +} + function mdExample(fn, file, cb) { let buffer = []; @@ -27,7 +39,7 @@ function mdExample(fn, file, cb) { }, function logTable(table) { //md files won't render color strings properly. - table = stripColors(table); + table = replaceLinks(stripColors(table)); // indent table so is displayed preformatted text table = ' ' + table.split('\n').join('\n '); diff --git a/src/cell.js b/src/cell.js index 29abf09..8f50744 100644 --- a/src/cell.js +++ b/src/cell.js @@ -31,12 +31,19 @@ class Cell { if (['boolean', 'number', 'string'].indexOf(typeof content) !== -1) { this.content = String(content); } else if (!content) { - this.content = ''; + this.content = this.options.href || ''; } else { throw new Error('Content needs to be a primitive, got: ' + typeof content); } this.colSpan = options.colSpan || 1; this.rowSpan = options.rowSpan || 1; + if (this.options.href) { + Object.defineProperty(this, 'href', { + get() { + return this.options.href; + }, + }); + } } mergeTableOptions(tableOptions, cells) { @@ -58,24 +65,35 @@ class Cell { this.head = style.head || tableStyle.head; this.border = style.border || tableStyle.border; - let fixedWidth = tableOptions.colWidths[this.x]; - if ((tableOptions.wordWrap || tableOptions.textWrap) && fixedWidth) { - fixedWidth -= this.paddingLeft + this.paddingRight; + this.fixedWidth = tableOptions.colWidths[this.x]; + this.lines = this.computeLines(tableOptions); + + this.desiredWidth = utils.strlen(this.content) + this.paddingLeft + this.paddingRight; + this.desiredHeight = this.lines.length; + } + + computeLines(tableOptions) { + if (this.fixedWidth && (tableOptions.wordWrap || tableOptions.textWrap)) { + this.fixedWidth -= this.paddingLeft + this.paddingRight; if (this.colSpan) { let i = 1; while (i < this.colSpan) { - fixedWidth += tableOptions.colWidths[this.x + i]; + this.fixedWidth += tableOptions.colWidths[this.x + i]; i++; } } const { wrapOnWordBoundary = true } = tableOptions; - this.lines = utils.colorizeLines(utils.wordWrap(fixedWidth, this.content, wrapOnWordBoundary)); - } else { - this.lines = utils.colorizeLines(this.content.split('\n')); + return this.wrapLines(utils.wordWrap(this.fixedWidth, this.content, wrapOnWordBoundary)); } + return this.wrapLines(this.content.split('\n')); + } - this.desiredWidth = utils.strlen(this.content) + this.paddingLeft + this.paddingRight; - this.desiredHeight = this.lines.length; + wrapLines(computedLines) { + const lines = utils.colorizeLines(computedLines); + if (this.href) { + return lines.map((line) => utils.hyperlink(this.href, line)); + } + return lines; } /** @@ -382,6 +400,7 @@ let CHAR_NAMES = [ 'right-mid', 'middle', ]; + module.exports = Cell; module.exports.ColSpanCell = ColSpanCell; module.exports.RowSpanCell = RowSpanCell; diff --git a/src/utils.js b/src/utils.js index 1204a30..c922c5b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -313,6 +313,17 @@ function colorizeLines(input) { return output; } +/** + * Credit: Matheus Sampaio https://github.com/matheussampaio + */ +function hyperlink(url, text) { + const OSC = '\u001B]'; + const BEL = '\u0007'; + const SEP = ';'; + + return [OSC, '8', SEP, SEP, url || text, BEL, text, OSC, '8', SEP, SEP, BEL].join(''); +} + module.exports = { strlen: strlen, repeat: repeat, @@ -321,4 +332,5 @@ module.exports = { mergeOptions: mergeOptions, wordWrap: multiLineWordWrap, colorizeLines: colorizeLines, + hyperlink, }; diff --git a/test/utils-test.js b/test/utils-test.js index 3ef1632..e2abe7a 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -387,4 +387,16 @@ describe('utils', function () { expect(utils.colorizeLines(input)).toEqual([colors.red('漢字'), colors.red('テスト')]); }); }); + + describe('hyperlink', function () { + const url = 'http://example.com'; + const text = 'hello link'; + const expected = (u, t) => `\x1B]8;;${u}\x07${t}\x1B]8;;\x07`; + it('wraps text with link', () => { + expect(utils.hyperlink(url, text)).toEqual(expected(url, text)); + }); + it('defaults text to link', () => { + expect(utils.hyperlink(url, url)).toEqual(expected(url, url)); + }); + }); });