Skip to content

Commit c85c581

Browse files
mbostockvisnup
andauthored
Mbostock/xlsx tweaks (#254)
* Update xlsx.js * Update xlsx.js * Use Object.create(null) * Prefer public ExcelJS APIs (#255) * Use Object.create(null) * Prefer public ExcelJS APIs * Use latest tap API * Coerce header row values to strings before fallback check * Update src/xlsx.js Co-authored-by: Mike Bostock <[email protected]> * Public API Co-authored-by: Visnu Pitiyanuvath <[email protected]>
1 parent 3dee66d commit c85c581

File tree

4 files changed

+539
-66
lines changed

4 files changed

+539
-66
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"devDependencies": {
3434
"eslint": "^7.18.0",
35+
"exceljs": "^4.3.0",
3536
"husky": "^4.3.8",
3637
"node-fetch": "^2.6.1",
3738
"rollup": "^2.37.1",

src/xlsx.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,25 @@ export class Workbook {
2121
}
2222
}
2323

24-
function extract(sheet, {range, headers = false} = {}) {
24+
function extract(sheet, {range, headers} = {}) {
2525
let [[c0, r0], [c1, r1]] = parseRange(range, sheet);
26-
const headerRow = headers && sheet._rows[r0++];
26+
const headerRow = headers ? sheet._rows[r0++] : null;
2727
let names = new Set(["#"]);
2828
for (let n = c0; n <= c1; n++) {
29-
let name = (headerRow ? valueOf(headerRow._cells[n]) : null) || toColumn(n);
29+
const value = headerRow ? valueOf(headerRow.findCell(n + 1)) : null;
30+
let name = (value && value + "") || toColumn(n);
3031
while (names.has(name)) name += "_";
3132
names.add(name);
3233
}
3334
names = new Array(c0).concat(Array.from(names));
3435

3536
const output = new Array(r1 - r0 + 1);
3637
for (let r = r0; r <= r1; r++) {
37-
const row = (output[r - r0] = Object.defineProperty({}, "#", {
38-
value: r + 1,
39-
}));
40-
const _row = sheet._rows[r];
41-
if (_row && _row.hasValues)
38+
const row = (output[r - r0] = Object.create(null, {"#": {value: r + 1}}));
39+
const _row = sheet.getRow(r + 1);
40+
if (_row.hasValues)
4241
for (let c = c0; c <= c1; c++) {
43-
const value = valueOf(_row._cells[c]);
42+
const value = valueOf(_row.findCell(c + 1));
4443
if (value != null) row[names[c + 1]] = value;
4544
}
4645
}
@@ -52,14 +51,16 @@ function extract(sheet, {range, headers = false} = {}) {
5251
function valueOf(cell) {
5352
if (!cell) return;
5453
const {value} = cell;
55-
if (value && value instanceof Date) return value;
56-
if (value && typeof value === "object") {
57-
if (value.formula || value.sharedFormula)
54+
if (value && typeof value === "object" && !(value instanceof Date)) {
55+
if (value.formula || value.sharedFormula) {
5856
return value.result && value.result.error ? NaN : value.result;
59-
if (value.richText) return value.richText.map((d) => d.text).join("");
57+
}
58+
if (value.richText) {
59+
return richText(value);
60+
}
6061
if (value.text) {
6162
let {text} = value;
62-
if (text.richText) text = text.richText.map((d) => d.text).join("");
63+
if (text.richText) text = richText(text);
6364
return value.hyperlink && value.hyperlink !== text
6465
? `${value.hyperlink} ${text}`
6566
: text;
@@ -69,6 +70,10 @@ function valueOf(cell) {
6970
return value;
7071
}
7172

73+
function richText(value) {
74+
return value.richText.map((d) => d.text).join("");
75+
}
76+
7277
function parseRange(specifier = ":", {columnCount, rowCount}) {
7378
specifier += "";
7479
if (!specifier.match(/^[A-Z]*\d*:[A-Z]*\d*$/))

test/xlsx-test.js

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,31 @@
11
import {test} from "tap";
22
import {Workbook} from "../src/xlsx.js";
3+
import ExcelJS from "exceljs";
34

4-
function mockWorkbook(contents, overrides = {}) {
5-
return {
6-
worksheets: Object.keys(contents).map((name) => ({name})),
7-
getWorksheet(name) {
8-
const _rows = contents[name];
9-
return Object.assign(
10-
{
11-
_rows: _rows.map((row) => ({
12-
_cells: row.map((cell) => ({value: cell})),
13-
hasValues: !!row.length,
14-
})),
15-
rowCount: _rows.length,
16-
columnCount: Math.max(..._rows.map((r) => r.length)),
17-
},
18-
overrides
19-
);
20-
},
21-
};
5+
function exceljs(contents) {
6+
const workbook = new ExcelJS.Workbook();
7+
for (const [sheet, rows] of Object.entries(contents)) {
8+
const ws = workbook.addWorksheet(sheet);
9+
for (const row of rows) ws.addRow(row);
10+
}
11+
return workbook;
2212
}
2313

2414
test("FileAttachment.xlsx reads sheet names", (t) => {
25-
const workbook = new Workbook(mockWorkbook({Sheet1: []}));
15+
const workbook = new Workbook(exceljs({Sheet1: []}));
2616
t.same(workbook.sheetNames, ["Sheet1"]);
2717
t.end();
2818
});
2919

3020
test("FileAttachment.xlsx sheet(name) throws on unknown sheet name", (t) => {
31-
const workbook = new Workbook(mockWorkbook({Sheet1: []}));
21+
const workbook = new Workbook(exceljs({Sheet1: []}));
3222
t.throws(() => workbook.sheet("bad"));
3323
t.end();
3424
});
3525

3626
test("FileAttachment.xlsx reads sheets", (t) => {
3727
const workbook = new Workbook(
38-
mockWorkbook({
28+
exceljs({
3929
Sheet1: [
4030
["one", "two", "three"],
4131
[1, 2, 3],
@@ -50,13 +40,15 @@ test("FileAttachment.xlsx reads sheets", (t) => {
5040
{A: "one", B: "two", C: "three"},
5141
{A: 1, B: 2, C: 3},
5242
]);
43+
t.equal(workbook.sheet(0)[0]["#"], 1);
44+
t.equal(workbook.sheet(0)[1]["#"], 2);
5345
t.end();
5446
});
5547

5648
test("FileAttachment.xlsx reads sheets with different types", (t) => {
5749
t.same(
5850
new Workbook(
59-
mockWorkbook({
51+
exceljs({
6052
Sheet1: [
6153
[],
6254
[null, undefined],
@@ -79,7 +71,7 @@ test("FileAttachment.xlsx reads sheets with different types", (t) => {
7971
);
8072
t.same(
8173
new Workbook(
82-
mockWorkbook({
74+
exceljs({
8375
Sheet1: [
8476
[
8577
{richText: [{text: "two"}, {text: "three"}]}, // A
@@ -112,7 +104,7 @@ test("FileAttachment.xlsx reads sheets with different types", (t) => {
112104
);
113105
t.same(
114106
new Workbook(
115-
mockWorkbook({
107+
exceljs({
116108
Sheet1: [
117109
[
118110
{formula: "=B2*5", result: 10},
@@ -131,7 +123,7 @@ test("FileAttachment.xlsx reads sheets with different types", (t) => {
131123

132124
test("FileAttachment.xlsx reads sheets with headers", (t) => {
133125
const workbook = new Workbook(
134-
mockWorkbook({
126+
exceljs({
135127
Sheet1: [
136128
[null, "one", "one", "two", "A", "0"],
137129
[1, null, 3, 4, 5, "zero"],
@@ -156,9 +148,10 @@ test("FileAttachment.xlsx reads sheets with headers", (t) => {
156148
});
157149

158150
test("FileAttachment.xlsx throws on invalid ranges", (t) => {
159-
const workbook = new Workbook(mockWorkbook({Sheet1: []}));
151+
const workbook = new Workbook(exceljs({Sheet1: []}));
160152
const malformed = new Error("Malformed range specifier");
161153

154+
t.throws(() => t.same(workbook.sheet(0, {range: 0})), malformed);
162155
t.throws(() => t.same(workbook.sheet(0, {range: ""})), malformed);
163156
t.throws(() => t.same(workbook.sheet(0, {range: "-:"})), malformed);
164157
t.throws(() => t.same(workbook.sheet(0, {range: " :"})), malformed);
@@ -174,7 +167,7 @@ test("FileAttachment.xlsx throws on invalid ranges", (t) => {
174167

175168
test("FileAttachment.xlsx reads sheet ranges", (t) => {
176169
const workbook = new Workbook(
177-
mockWorkbook({
170+
exceljs({
178171
Sheet1: [
179172
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
180173
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
@@ -248,31 +241,22 @@ test("FileAttachment.xlsx reads sheet ranges", (t) => {
248241
t.end();
249242
});
250243

251-
test("FileAttachment.xlsx throws on unknown range specifier", (t) => {
252-
const workbook = new Workbook(mockWorkbook({Sheet1: []}));
253-
t.throws(() => workbook.sheet(0, {range: 0}));
254-
t.end();
255-
});
256-
257244
test("FileAttachment.xlsx derives column names such as A AA AAA…", (t) => {
258-
const l0 = 26 * 26 * 26 + 26 * 26 + 26;
245+
const l0 = 26 * 26 * 23;
259246
const workbook = new Workbook(
260-
mockWorkbook({
247+
exceljs({
261248
Sheet1: [Array.from({length: l0}).fill(1)],
262249
})
263250
);
264251
t.same(
265-
workbook.sheet(0, {headers: false}).columns.filter((d) => d.match(/^A*$/)),
252+
workbook.sheet(0).columns.filter((d) => d.match(/^A+$/)),
266253
["A", "AA", "AAA"]
267254
);
268-
const workbook1 = new Workbook(
269-
mockWorkbook({
270-
Sheet1: [Array.from({length: l0 + 1}).fill(1)],
271-
})
272-
);
273-
t.same(
274-
workbook1.sheet(0, {headers: false}).columns.filter((d) => d.match(/^A*$/)),
275-
["A", "AA", "AAA", "AAAA"]
276-
);
255+
t.end();
256+
});
257+
258+
test("FileAttachment.sheet headers protects __proto__ of row objects", (t) => {
259+
const workbook = new Workbook(exceljs({Sheet1: [["__proto__"], [{a: 1}]]}));
260+
t.not(workbook.sheet(0, {headers: true})[0].a, 1);
277261
t.end();
278262
});

0 commit comments

Comments
 (0)