Skip to content

Commit fd21b05

Browse files
committed
Init
0 parents  commit fd21b05

22 files changed

+875
-0
lines changed

.editorconfig

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
indent_style = tab
5+
end_of_line = lf
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist

.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

.travis.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
language: node_js
2+
node_js:
3+
- '12'

license

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) Pedro Augusto de Paula Barbosa <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package.json

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"name": "json-excel",
3+
"version": "0.0.0",
4+
"description": "Create a pretty Excel table from JSON data with a very simple API",
5+
"license": "MIT",
6+
"repository": "papb/json-excel",
7+
"author": {
8+
"name": "Pedro Augusto de Paula Barbosa",
9+
"email": "[email protected]"
10+
},
11+
"keywords": [
12+
"excel",
13+
"json",
14+
"table",
15+
"json to excel",
16+
"json-to-excel",
17+
"convert",
18+
"xlsx",
19+
"generate",
20+
"sheet",
21+
"worksheet",
22+
"workbook",
23+
"tabular",
24+
"data",
25+
"xls"
26+
],
27+
"engines": {
28+
"node": ">=10"
29+
},
30+
"scripts": {
31+
"build": "del-cli dist && tsc",
32+
"release": "npm run build && np --no-2fa && npm pack && echo Please put the generated .tgz as a release asset on GitHub",
33+
"test": "npm run build && xo && ava",
34+
"lint": "tsc --noEmit && xo",
35+
"ava": "npm run build && ava"
36+
},
37+
"main": "dist/source",
38+
"types": "dist/source",
39+
"files": [
40+
"dist/source"
41+
],
42+
"dependencies": {
43+
"exceljs": "^4.0.1",
44+
"fs-jetpack": "^2.4.0"
45+
},
46+
"devDependencies": {
47+
"@ava/typescript": "^1.1.1",
48+
"@types/node": "^13.13.12",
49+
"ava": "^3.9.0",
50+
"del-cli": "^3.0.1",
51+
"np": "https://github.com/pixelastic/np/tarball/c3ab2e3b053c7da0ce40a572ca1616273ac080f8",
52+
"source-map-support": "^0.5.19",
53+
"tempy": "^0.5.0",
54+
"typescript": "~3.9.5",
55+
"xo": "^0.32.0"
56+
},
57+
"ava": {
58+
"verbose": true,
59+
"timeout": "60m",
60+
"require": [
61+
"source-map-support/register"
62+
],
63+
"typescript": {
64+
"rewritePaths": {
65+
"source/": "dist/source/",
66+
"test/": "dist/test/"
67+
}
68+
}
69+
},
70+
"xo": {
71+
"ignore": [
72+
"**/*.js"
73+
],
74+
"rules": {
75+
"@typescript-eslint/prefer-readonly-parameter-types": "off",
76+
"unicorn/prevent-abbreviations": "off",
77+
"unicorn/no-for-loop": "off",
78+
"ava/no-ignored-test-files": "off",
79+
"linebreak-style": [
80+
"error",
81+
"unix"
82+
],
83+
"object-curly-spacing": [
84+
"error",
85+
"always"
86+
]
87+
}
88+
}
89+
}

readme-example.png

7.89 KB
Loading

readme.md

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# json-excel [![Build Status](https://travis-ci.com/papb/json-excel.svg?branch=master)](https://travis-ci.com/papb/json-excel)
2+
3+
> Create a pretty Excel table from JSON data with a very simple API
4+
5+
6+
## Highlights
7+
8+
* Pretty output
9+
* Intelligently auto-fits cell sizes by default
10+
* Checks for [Excel limitations](https://support.microsoft.com/en-ie/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3) automatically (such as maximum cell length) and throws helpful errors if any limit is exceeded
11+
* Get the *Format as Table* Excel styling, with filterable headers, by simply enabling an option
12+
* Written in TypeScript (you get autocomplete suggestions in your IDE!)
13+
14+
15+
## Install
16+
17+
```
18+
$ npm install json-excel
19+
```
20+
21+
22+
## Usage
23+
24+
```js
25+
const jsonToExcel = require('json-excel');
26+
27+
(async () => {
28+
await jsonToExcel([
29+
{
30+
sheetName: 'Hello World',
31+
data: [
32+
['Foo', 'Bar', 'Baz'],
33+
['A large string here but with\none line break', 'Hi', 'Test'],
34+
[
35+
'\'starting single quote\nis rendered normally',
36+
'Lots\nof\nline\nbreaks',
37+
'Auto-fits cells with a little extra margin'
38+
],
39+
['Nice!', '', 'Quick and to the point!']
40+
],
41+
formatAsTable: true
42+
}
43+
], 'example.xlsx', { overwrite: true });
44+
})();
45+
```
46+
47+
Output is an excel file called `example.xlsx` with a single sheet (called `Hello World`) and the following content:
48+
49+
![](readme-example.png)
50+
51+
52+
## API
53+
54+
<!-- Ensure this part is consistent with ./types.ts and ./defaults.ts -->
55+
56+
### jsonToExcel(jsonSheets, destinationPath, options?)
57+
58+
Async function that creates a xlsx file with the provided data.
59+
60+
#### jsonSheets
61+
62+
Type: `object[]`
63+
64+
An array of objects, each representing one sheet, with:
65+
66+
* `sheetName` (`string`, required): The name of the Worksheet (shown in the sheet tab in the bottom in Excel).
67+
* `data` (`string[][]`, required): The data to be populated in the Worksheet.
68+
* `formatAsTable` (`boolean`, optional, default `false`): Whether or not to enable the *"Format as Table"* styling, like in the above example. This will enable striped rows and filter arrows on all headers.
69+
* `tableTheme` (`string`, optional, default `'TableStyleMedium9'`): Which theme to use when formatting as table. This option is ignored if `formatAsTable` is `false`. The default value corresponds to the one from the screenshot above (medium blue). The list of supported themes is shown right in your IDE via autocomplete suggestions to this option. The autocomplete works even if you are not using TypeScript!
70+
* `autoTrimWhitespace` (`boolean`, optional, default `true`): Whether or not to automatically remove leading and trailing whitespace from each cell. Having this enabled is great to make the cell content alignment be consistent with what is visible.
71+
* `autoFitCellSizes` (`boolean`, optional, default `true`): Whether or not to automatically calculate best widths for every column and best heights for every row.
72+
* `autoFitCellSizesOptions` (`object`, optional): Extra options for configuring the behavior of the auto-fitting of cell sizes:
73+
* `minHeight` (`number`, optional, default `15`): The minimum height (in *"excel points"*) for every row.
74+
* `maxHeight` (`number`, optional, default `408`): The maximum height (in *"excel points"*) for every row. Cannot be greater than 408 (this is an Excel limitation).
75+
* `minWidth` (`number`, optional, default `6`): The minimum width (in *"excel points"*) for every column.
76+
* `maxWidth` (`number`, optional, default `170`): The maximum width (in *"excel points"*) for every column. Cannot be greater than 254 (this is an Excel limitation).
77+
* `horizontalPadding` (`number`, optional, default `3`): Extra horizontal padding (in *"excel points"*) for every column. This amount will be added to the auto-calculated minimal width in which the contents fit.
78+
* `verticalPadding` (`number`, optional, default `2`): Extra vertical padding (in *"excel points"*) for every cell. This amount will be added to the auto-calculated minimal height in which the contents fit.
79+
80+
#### destinationPath
81+
82+
Type: `string`
83+
84+
The path (absolute, or relative to `process.cwd()`) in which the new xlsx file should be created. In windows, both `/` and `\` are accepted as path separators.
85+
86+
#### options
87+
88+
Type: `object`
89+
90+
##### overwrite
91+
92+
Type: `boolean`
93+
Default: `false`
94+
95+
Whether or not to overwrite the destination file if it already exists.
96+
97+
98+
## Tip: usage with `object[]` instead of `string[][]`
99+
100+
If, instead of directly tabular data, you have a list of objects such as...
101+
102+
```js
103+
const data = [
104+
{ name: 'Grape', size: 'small' },
105+
{ name: 'Watermelon', size: 'big' },
106+
{ name: 'Apple', size: 'medium' }
107+
];
108+
```
109+
110+
...you can use `jsonToExcel` by simply converting that to a `string[][]` first, with a simple loop. Example:
111+
112+
```js
113+
const headers = ['Name', 'Size'];
114+
const dataAs2DArray = data.map(fruit => [fruit.name, fruit.size]);
115+
116+
jsonToExcel([{
117+
sheetName: 'Fruits',
118+
data: [
119+
headers,
120+
...dataAs2DArray
121+
],
122+
formatAsTable: true
123+
}], 'fruits.xlsx');
124+
```
125+
126+
127+
## License
128+
129+
MIT © [Pedro Augusto de Paula Barbosa](https://github.com/papb)
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function assertDimensionsAcceptable(rows: number, columns: number): void {
2+
// https://support.microsoft.com/en-ie/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
3+
4+
const MAX_ROWS = 1048576;
5+
const MAX_COLUMNS = 16384;
6+
7+
if (rows > MAX_ROWS) {
8+
throw new TypeError(`Expected at most ${MAX_ROWS} rows, got ${rows}`);
9+
}
10+
11+
if (columns > MAX_COLUMNS) {
12+
throw new TypeError(`Expected at most ${MAX_COLUMNS} columns, got ${columns}`);
13+
}
14+
}

source/assert-valid-cell-content.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function assertValidCellContent(string: string): void {
2+
// https://support.microsoft.com/en-ie/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
3+
4+
const MAX_CHARS = 32767;
5+
const MAX_LINEFEEDS = 253;
6+
7+
if (string.length > MAX_CHARS) {
8+
throw new TypeError(`Expected at most ${MAX_CHARS} characters, got ${string.length}`);
9+
}
10+
11+
const amountOfLinefeeds = string.split('\n').length - 1;
12+
13+
if (amountOfLinefeeds > MAX_LINEFEEDS) {
14+
throw new TypeError(`Expected at most ${MAX_LINEFEEDS} linefeeds (\`\\n\`), got ${amountOfLinefeeds}`);
15+
}
16+
}

source/assert-valid-sheet-names.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
function assertValidSheetName(name: string): void {
2+
if (name === '') {
3+
throw new TypeError('Sheet name cannot be empty.');
4+
}
5+
6+
if (name.length > 31) {
7+
throw new TypeError('Sheet name cannot exceed 31 characters.');
8+
}
9+
10+
if (/[':\\/?*[\]]/.test(name)) {
11+
throw new TypeError('Sheet name cannot include any of the following characters: []\':\\/?*');
12+
}
13+
}
14+
15+
export function assertValidSheetNames(names: string[]): void {
16+
for (const name of names) {
17+
assertValidSheetName(name);
18+
}
19+
20+
if (names.length !== [...new Set(names.map(name => name.toLowerCase()))].length) {
21+
throw new TypeError('Two sheets cannot have the same name (case-insensitive).');
22+
}
23+
}

source/auto-sizes.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { getMax, getSum, clamp } from './numeric-helpers';
2+
import { getStringVisualWidth, predictAmountOfLineWraps } from './visual-string-width';
3+
import type { ExpandedAutoFitCellSizesOptions } from './types';
4+
5+
export function getStringHeight(string: string, maxVisualWidth?: number): number {
6+
const lines = string.split(/\r?\n/);
7+
8+
if (!maxVisualWidth) {
9+
return lines.length;
10+
}
11+
12+
return lines.length + getSum(
13+
lines.map(line => predictAmountOfLineWraps(line, maxVisualWidth))
14+
);
15+
}
16+
17+
function getMinimalFittingColumnWidth(cellsInColumn: string[]): number {
18+
return getMax(
19+
cellsInColumn.map(cell => getStringVisualWidth(cell))
20+
);
21+
}
22+
23+
function getMinimalFittingRowHeight(cellsInRow: string[], realColumnWidths: number[]): number {
24+
// Since the actual applied column width may be different from the minimal fitting
25+
// column width, in a few edge cases the minimal fitting height might be actually
26+
// greater than 1 + the number of explicit linebreaks in the string, due to auto-applied
27+
// line wraps in long lines.
28+
29+
const maxAmountOfLines = getMax(
30+
cellsInRow.map((cell, index) => getStringHeight(cell, realColumnWidths[index]))
31+
);
32+
33+
return 15 * maxAmountOfLines;
34+
}
35+
36+
export type AutoFitSizesResult = {
37+
columnWidths: number[];
38+
rowHeights: number[];
39+
};
40+
41+
export function getAutoFitCellSizes(
42+
data: string[][],
43+
considerExtraRoomForHeaderFilterArrow: boolean,
44+
options: ExpandedAutoFitCellSizesOptions
45+
): AutoFitSizesResult {
46+
const result: AutoFitSizesResult = {
47+
columnWidths: [],
48+
rowHeights: []
49+
};
50+
51+
const rowCount = data.length;
52+
const columnCount = data[0].length;
53+
54+
for (let ci = 0; ci < columnCount; ci++) {
55+
let width = getMinimalFittingColumnWidth(data.map(row => row[ci]));
56+
57+
if (considerExtraRoomForHeaderFilterArrow) {
58+
width = Math.max(width, 4 + getStringVisualWidth(data[0][ci]));
59+
}
60+
61+
width += options.horizontalPadding;
62+
width = clamp(width, options.minWidth, options.maxWidth);
63+
64+
result.columnWidths.push(width);
65+
}
66+
67+
for (let ri = 0; ri < rowCount; ri++) {
68+
let height = getMinimalFittingRowHeight(data[ri], result.columnWidths);
69+
height += options.verticalPadding;
70+
height = clamp(height, options.minHeight, options.maxHeight);
71+
72+
result.rowHeights.push(height);
73+
}
74+
75+
return result;
76+
}

0 commit comments

Comments
 (0)