Skip to content

Commit 304d59d

Browse files
authored
feat: support text wrapping (#6)
1 parent f2e9385 commit 304d59d

File tree

5 files changed

+212
-22
lines changed

5 files changed

+212
-22
lines changed

fixtures/inputs.js

+75
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,80 @@ module.exports = {
108108
'| Sarah | 22 | Yes |',
109109
'| Lee | 23 | Yes |'
110110
].join(os.EOL) + os.EOL
111+
},
112+
wrap: {
113+
input: [
114+
{ name: 'Benjamin', age: 21, isCool: false },
115+
{ name: 'Sarah', age: 22, isCool: true },
116+
{ name: 'Lee', age: 23, isCool: true }
117+
],
118+
options: {
119+
wrap: { width: 5 }
120+
},
121+
expected: [
122+
// headers wrap, soft wrap
123+
'| Name | Age | Is |',
124+
' cool ',
125+
'| ----- | ----- | ----- |',
126+
// hard wrap
127+
'| Benja | 21 | false |',
128+
' min ',
129+
// no wrap
130+
'| Sarah | 22 | true |',
131+
'| Lee | 23 | true |'
132+
].join(os.EOL) + os.EOL
133+
},
134+
newlines: {
135+
input: [
136+
{ name: 'Benjamin\nor Ben', age: 21, isCool: false },
137+
{ name: 'Sarah', age: 22, isCool: true },
138+
{ name: 'Lee', age: 23, isCool: true }
139+
],
140+
expected: [
141+
'| Name | Age | Is cool |',
142+
'| -------- | ----- | ------- |',
143+
'| Benjamin | 21 | false |',
144+
' or Ben ',
145+
'| Sarah | 22 | true |',
146+
'| Lee | 23 | true |'
147+
].join(os.EOL) + os.EOL
148+
},
149+
wrapAndNewlines: {
150+
input: [
151+
{ name: 'Benjamin or\nBen', age: 21, isCool: false },
152+
{ name: 'Sarah', age: 22, isCool: true },
153+
{ name: 'Lee', age: 23, isCool: true }
154+
],
155+
options: {
156+
wrap: { width: 8 }
157+
},
158+
expected: [
159+
'| Name | Age | Is cool |',
160+
'| -------- | ----- | ------- |',
161+
'| Benjamin | 21 | false |',
162+
' or ',
163+
' Ben ',
164+
'| Sarah | 22 | true |',
165+
'| Lee | 23 | true |'
166+
].join(os.EOL) + os.EOL
167+
},
168+
gutters: {
169+
input: [
170+
{ name: 'Benjamin', age: 21, isCool: false },
171+
{ name: 'Sarah', age: 22, isCool: true },
172+
{ name: 'Lee', age: 23, isCool: true }
173+
],
174+
options: {
175+
wrap: { width: 5, gutters: true }
176+
},
177+
expected: [
178+
'| Name | Age | Is |',
179+
'| | | cool |',
180+
'| ----- | ----- | ----- |',
181+
'| Benja | 21 | false |',
182+
'| min | | |',
183+
'| Sarah | 22 | true |',
184+
'| Lee | 23 | true |'
185+
].join(os.EOL) + os.EOL
111186
}
112187
}

index.js

+69-16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
const os = require('os')
44
const sentence = require('sentence-case')
5+
const split = require('split-text-to-chunks')
56

6-
const columnWidthMin = 5
7+
const width = split.width
8+
const columnsWidthMin = 5
79
const ALIGN = [ 'LEFT', 'CENTER', 'RIGHT' ]
810

911
module.exports = (input, options) => {
@@ -13,9 +15,17 @@ module.exports = (input, options) => {
1315

1416
options = Object.assign({
1517
stringify: v => typeof v === 'undefined' ? '' : String(v)
16-
}, options)
18+
}, options, {
19+
wrap: Object.assign({
20+
width: Infinity,
21+
gutters: false
22+
}, options && options.wrap)
23+
})
1724

1825
const stringify = options.stringify
26+
const columnsMaxWidth = options.wrap.width
27+
const gutters = options.wrap.gutters
28+
1929
const keys = Object.keys(input[0])
2030

2131
const titles = keys.map((key, i) => {
@@ -34,9 +44,9 @@ module.exports = (input, options) => {
3444

3545
const widths = input.reduce(
3646
(sizes, item) => keys.map(
37-
(key, i) => Math.max(columnWidthMin, stringify(item[key]).length, sizes[i])
47+
(key, i) => Math.max(width(stringify(item[key]), columnsMaxWidth), sizes[i])
3848
),
39-
titles.map(t => t.length)
49+
titles.map(t => Math.max(columnsWidthMin, width(t, columnsMaxWidth)))
4050
)
4151

4252
const alignments = keys.map((key, i) => {
@@ -58,29 +68,76 @@ module.exports = (input, options) => {
5868
let table = ''
5969

6070
// header line
61-
table += row(titles.map(
62-
(title, i) => pad(alignments[i], widths[i], title)
63-
))
71+
table += row(alignments, widths, titles, gutters)
6472

6573
// header separator
66-
table += row(alignments.map(
74+
table += line(alignments.map(
6775
(align, i) => (
6876
(align === 'LEFT' || align === 'CENTER' ? ':' : '-') +
6977
repeat('-', widths[i] - 2) +
7078
(align === 'RIGHT' || align === 'CENTER' ? ':' : '-')
7179
)
72-
))
80+
), true)
7381

7482
// table body
7583
table += input.map(
76-
item => row(keys.map(
77-
(key, i) => pad(alignments[i], widths[i], stringify(item[key]))
78-
))
84+
(item, i) => row(alignments, widths, keys.map(
85+
key => stringify(item[key])
86+
), gutters)
7987
).join('')
8088

8189
return table
8290
}
8391

92+
function row (alignments, widths, columns, gutters) {
93+
const width = columns.length
94+
const values = new Array(width)
95+
const first = new Array(width)
96+
let height = 1
97+
98+
for (let h = 0; h < width; h++) {
99+
const cells = values[h] = split(columns[h], widths[h])
100+
if (cells.length > height) height = cells.length
101+
first[h] = pad(alignments[h], widths[h], cells[0])
102+
}
103+
104+
if (height === 1) return line(first, true)
105+
106+
const lines = new Array(height)
107+
lines[0] = line(first, true)
108+
109+
for (let v = 1; v < height; v++) {
110+
lines[v] = new Array(width)
111+
}
112+
113+
for (let h = 0; h < width; h++) {
114+
const cells = values[h]
115+
let v = 1
116+
117+
for (;v < cells.length; v++) {
118+
lines[v][h] = pad(alignments[h], widths[h], cells[v])
119+
}
120+
121+
for (;v < height; v++) {
122+
lines[v][h] = repeat(' ', widths[h])
123+
}
124+
}
125+
126+
for (let h = 1; h < height; h++) {
127+
lines[h] = line(lines[h], gutters)
128+
}
129+
130+
return lines.join('')
131+
}
132+
133+
function line (columns, gutters) {
134+
return (
135+
(gutters ? '| ' : ' ') +
136+
columns.join((gutters ? ' | ' : ' ')) +
137+
(gutters ? ' |' : ' ') + os.EOL
138+
)
139+
}
140+
84141
function pad (alignment, width, what) {
85142
if (!alignment || alignment === 'LEFT') {
86143
return padEnd(what, width)
@@ -97,10 +154,6 @@ function pad (alignment, width, what) {
97154
return repeat(' ', sides) + what + repeat(' ', sides + remainder)
98155
}
99156

100-
function row (cells) {
101-
return '| ' + cells.join(' | ') + ' |' + os.EOL
102-
}
103-
104157
function repeat (what, times) {
105158
return new Array(times).fill(what).join('')
106159
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"standard": "^10.0.2"
3939
},
4040
"dependencies": {
41-
"sentence-case": "^2.1.1"
41+
"sentence-case": "^2.1.1",
42+
"split-text-to-chunks": "^1.0.0"
4243
}
4344
}

readme.md

+46-5
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,59 @@ tablemark(input, [options = {}])
7979
- `{Array<Object>} input`: the data to table-ify
8080
- `{Object} [options = {}]`
8181

82-
| key | type | default | description |
83-
| :-----------: | :----------: | :-----: | ---------------------------------------- |
84-
| `columns` | `<Array>` | - | Array of column descriptors. |
85-
| `caseHeaders` | `<Boolean>` | `true` | Sentence case headers derived from keys. |
86-
| `stringify` | `<Function>` | - | Provide a custom "toString" function. |
82+
| key | type | default | description |
83+
| :------------: | :----------: | :--------: | -------------------------------------------- |
84+
| `columns` | `<Array>` | - | Array of column descriptors. |
85+
| `caseHeaders` | `<Boolean>` | `true` | Sentence case headers derived from keys. |
86+
| `stringify` | `<Function>` | - | Provide a custom "toString" function. |
87+
| `wrap.width` | `<Number>` | `Infinity` | Wrap texts at this length. |
88+
| `wrap.gutters` | `<Boolean>` | `false` | Add sides (`| <content> |`) to wrapped rows. |
8789

8890
The `columns` array can either contain objects, in which case their
8991
`name` and `align` properties will be used to alter the display of
9092
the column in the table, or any other type which will be coerced
9193
to a string if necessary and used as a replacement for the column
9294
name.
9395

96+
## text wrapping
97+
98+
To output valid [GitHub Flavored Markdown](https://github.github.com/gfm/) a
99+
cell must not contain newlines. Consider replacing those with `<br />` (e.g.
100+
using the `stringify` option).
101+
102+
Set the `wrap.width` option to wrap any content at that length onto a new
103+
adjacent line:
104+
105+
```js
106+
tablemark([
107+
{ star: false, name: 'Benjamin' },
108+
{ star: true, name: 'Jet Li' }
109+
], { wrap: { width: 5 } })
110+
111+
// | Star | Name |
112+
// | ----- | ----- |
113+
// | false | Benja |
114+
// min
115+
// | true | Jet |
116+
// Li
117+
```
118+
119+
Enable `wrap.gutters` to add pipes on all lines:
120+
121+
```js
122+
tablemark([
123+
{ star: false, name: 'Benjamin' },
124+
{ star: true, name: 'Jet Li' }
125+
], { wrap: { width: 5, gutters: true } })
126+
127+
// | Star | Name |
128+
// | ----- | ----- |
129+
// | false | Benja |
130+
// | | min |
131+
// | true | Jet |
132+
// | | Li |
133+
```
134+
94135
## see also
95136

96137
- [`tablemark-cli`](https://github.com/citycide/tablemark-cli): use this module from the command line

test.js

+20
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,23 @@ test('can use custom stringify function', t => {
2727
const result = fn(cases.coerce.input, cases.coerce.options)
2828
t.is(result, cases.coerce.expected)
2929
})
30+
31+
test('text wrapping', t => {
32+
const result = fn(cases.wrap.input, cases.wrap.options)
33+
t.is(result, cases.wrap.expected)
34+
})
35+
36+
test('newlines', t => {
37+
const result = fn(cases.newlines.input, cases.newlines.options)
38+
t.is(result, cases.newlines.expected)
39+
})
40+
41+
test('text wrapping and newlines combined', t => {
42+
const result = fn(cases.wrapAndNewlines.input, cases.wrapAndNewlines.options)
43+
t.is(result, cases.wrapAndNewlines.expected)
44+
})
45+
46+
test('gutters', t => {
47+
const result = fn(cases.gutters.input, cases.gutters.options)
48+
t.is(result, cases.gutters.expected)
49+
})

0 commit comments

Comments
 (0)