Skip to content

Commit ed4afb7

Browse files
authored
Hyperlink support for cells (#292)
* Failing tests for hyperlink support * Add hyperlink support for cells (#73) * Add hyperlink example to basic usage docs
1 parent 40e86bc commit ed4afb7

File tree

6 files changed

+130
-11
lines changed

6 files changed

+130
-11
lines changed

basic-usage.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,31 @@
166166
table.push(['Wrap', 'Text']);
167167
```
168168

169+
170+
##### Supports hyperlinking cell content using the href option
171+
┌───────────┬─────┬─────┐
172+
│ Text Link │ Hel │ htt │
173+
│ │ lo │ p:/ │
174+
│ │ Lin │ /ex │
175+
│ │ k │ amp │
176+
│ │ │ le. │
177+
│ │ │ com │
178+
├───────────┴─────┴─────┤
179+
│ http://example.com │
180+
└───────────────────────┘
181+
182+
Note: Links are not displayed in documentation examples.
183+
```javascript
184+
const table = new Table({
185+
colWidths: [11, 5, 5],
186+
style: { border: [], head: [] },
187+
wordWrap: true,
188+
wrapOnWordBoundary: false,
189+
});
190+
const href = 'http://example.com';
191+
table.push(
192+
[{ content: 'Text Link', href }, { content: 'Hello Link', href }, { href }],
193+
[{ href, colSpan: 3 }]
194+
);
195+
```
196+

examples/basic-usage-examples.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const Table = require('../src/table');
22
const colors = require('@colors/colors/safe');
3+
const { hyperlink } = require('../src/utils');
34

45
// prettier-ignore
56
// Disable prettier so that examples are formatted more clearly
@@ -235,6 +236,40 @@ module.exports = function (runTest) {
235236

236237
return [makeTable, expected];
237238
});
239+
240+
it('Supports hyperlinking cell content using the href option', () => {
241+
function link(text) {
242+
return hyperlink('http://example.com', text);
243+
}
244+
function makeTable() {
245+
const table = new Table({
246+
colWidths: [11, 5, 5],
247+
style: { border: [], head: [] },
248+
wordWrap: true,
249+
wrapOnWordBoundary: false,
250+
});
251+
const href = 'http://example.com';
252+
table.push(
253+
[{ content: 'Text Link', href }, { content: 'Hello Link', href }, { href }],
254+
[{ href, colSpan: 3 }]
255+
);
256+
return table;
257+
}
258+
259+
let expected = [
260+
'┌───────────┬─────┬─────┐',
261+
`│ ${link('Text Link')}${link('Hel')}${link('htt')} │`,
262+
`│ │ ${link('lo ')}${link('p:/')} │`,
263+
`│ │ ${link('Lin')}${link('/ex')} │`,
264+
`│ │ ${link('k')}${link('amp')} │`,
265+
`│ │ │ ${link('le.')} │`,
266+
`│ │ │ ${link('com')} │`,
267+
'├───────────┴─────┴─────┤',
268+
`│ ${link('http://example.com')} │`,
269+
'└───────────────────────┘',
270+
];
271+
return [makeTable, expected];
272+
});
238273
};
239274

240275
/* Expectation - ready to be copy/pasted and filled in. DO NOT DELETE THIS
@@ -250,3 +285,4 @@ module.exports = function (runTest) {
250285
, '└──┴───┴──┴──┘'
251286
];
252287
*/
288+
// Jest Snapshot v1, https://goo.gl/fbAQLP

lib/print-example.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ function logExample(fn) {
1717
);
1818
}
1919

20+
function replaceLinks(str) {
21+
const matches = str.match(/\x1B\]8;;[^\x07]+\x07[^\]]+\x1B\]8;;\x07/g);
22+
if (matches) {
23+
matches.forEach((match) => {
24+
const [, text] = match.match(/\x07([^\]|\x1B]+)\x1B/);
25+
str = str.replace(match, text);
26+
});
27+
str += '\n\nNote: Links are not displayed in documentation examples.';
28+
}
29+
return str;
30+
}
31+
2032
function mdExample(fn, file, cb) {
2133
let buffer = [];
2234

@@ -27,7 +39,7 @@ function mdExample(fn, file, cb) {
2739
},
2840
function logTable(table) {
2941
//md files won't render color strings properly.
30-
table = stripColors(table);
42+
table = replaceLinks(stripColors(table));
3143

3244
// indent table so is displayed preformatted text
3345
table = ' ' + table.split('\n').join('\n ');

src/cell.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@ class Cell {
3131
if (['boolean', 'number', 'string'].indexOf(typeof content) !== -1) {
3232
this.content = String(content);
3333
} else if (!content) {
34-
this.content = '';
34+
this.content = this.options.href || '';
3535
} else {
3636
throw new Error('Content needs to be a primitive, got: ' + typeof content);
3737
}
3838
this.colSpan = options.colSpan || 1;
3939
this.rowSpan = options.rowSpan || 1;
40+
if (this.options.href) {
41+
Object.defineProperty(this, 'href', {
42+
get() {
43+
return this.options.href;
44+
},
45+
});
46+
}
4047
}
4148

4249
mergeTableOptions(tableOptions, cells) {
@@ -58,24 +65,35 @@ class Cell {
5865
this.head = style.head || tableStyle.head;
5966
this.border = style.border || tableStyle.border;
6067

61-
let fixedWidth = tableOptions.colWidths[this.x];
62-
if ((tableOptions.wordWrap || tableOptions.textWrap) && fixedWidth) {
63-
fixedWidth -= this.paddingLeft + this.paddingRight;
68+
this.fixedWidth = tableOptions.colWidths[this.x];
69+
this.lines = this.computeLines(tableOptions);
70+
71+
this.desiredWidth = utils.strlen(this.content) + this.paddingLeft + this.paddingRight;
72+
this.desiredHeight = this.lines.length;
73+
}
74+
75+
computeLines(tableOptions) {
76+
if (this.fixedWidth && (tableOptions.wordWrap || tableOptions.textWrap)) {
77+
this.fixedWidth -= this.paddingLeft + this.paddingRight;
6478
if (this.colSpan) {
6579
let i = 1;
6680
while (i < this.colSpan) {
67-
fixedWidth += tableOptions.colWidths[this.x + i];
81+
this.fixedWidth += tableOptions.colWidths[this.x + i];
6882
i++;
6983
}
7084
}
7185
const { wrapOnWordBoundary = true } = tableOptions;
72-
this.lines = utils.colorizeLines(utils.wordWrap(fixedWidth, this.content, wrapOnWordBoundary));
73-
} else {
74-
this.lines = utils.colorizeLines(this.content.split('\n'));
86+
return this.wrapLines(utils.wordWrap(this.fixedWidth, this.content, wrapOnWordBoundary));
7587
}
88+
return this.wrapLines(this.content.split('\n'));
89+
}
7690

77-
this.desiredWidth = utils.strlen(this.content) + this.paddingLeft + this.paddingRight;
78-
this.desiredHeight = this.lines.length;
91+
wrapLines(computedLines) {
92+
const lines = utils.colorizeLines(computedLines);
93+
if (this.href) {
94+
return lines.map((line) => utils.hyperlink(this.href, line));
95+
}
96+
return lines;
7997
}
8098

8199
/**
@@ -382,6 +400,7 @@ let CHAR_NAMES = [
382400
'right-mid',
383401
'middle',
384402
];
403+
385404
module.exports = Cell;
386405
module.exports.ColSpanCell = ColSpanCell;
387406
module.exports.RowSpanCell = RowSpanCell;

src/utils.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,17 @@ function colorizeLines(input) {
313313
return output;
314314
}
315315

316+
/**
317+
* Credit: Matheus Sampaio https://github.com/matheussampaio
318+
*/
319+
function hyperlink(url, text) {
320+
const OSC = '\u001B]';
321+
const BEL = '\u0007';
322+
const SEP = ';';
323+
324+
return [OSC, '8', SEP, SEP, url || text, BEL, text, OSC, '8', SEP, SEP, BEL].join('');
325+
}
326+
316327
module.exports = {
317328
strlen: strlen,
318329
repeat: repeat,
@@ -321,4 +332,5 @@ module.exports = {
321332
mergeOptions: mergeOptions,
322333
wordWrap: multiLineWordWrap,
323334
colorizeLines: colorizeLines,
335+
hyperlink,
324336
};

test/utils-test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,16 @@ describe('utils', function () {
387387
expect(utils.colorizeLines(input)).toEqual([colors.red('漢字'), colors.red('テスト')]);
388388
});
389389
});
390+
391+
describe('hyperlink', function () {
392+
const url = 'http://example.com';
393+
const text = 'hello link';
394+
const expected = (u, t) => `\x1B]8;;${u}\x07${t}\x1B]8;;\x07`;
395+
it('wraps text with link', () => {
396+
expect(utils.hyperlink(url, text)).toEqual(expected(url, text));
397+
});
398+
it('defaults text to link', () => {
399+
expect(utils.hyperlink(url, url)).toEqual(expected(url, url));
400+
});
401+
});
390402
});

0 commit comments

Comments
 (0)