@yuanliwei/exceljs 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +3024 -0
- package/README_zh.md +2878 -0
- package/excel.js +13 -0
- package/index.d.ts +2040 -0
- package/index.ts +2 -0
- package/lib/csv/csv.js +191 -0
- package/lib/csv/line-buffer.js +74 -0
- package/lib/csv/stream-converter.js +135 -0
- package/lib/doc/anchor.js +91 -0
- package/lib/doc/cell.js +1124 -0
- package/lib/doc/column.js +320 -0
- package/lib/doc/data/theme1.json +234 -0
- package/lib/doc/data-validations.js +19 -0
- package/lib/doc/defined-names.js +196 -0
- package/lib/doc/enums.js +48 -0
- package/lib/doc/image.js +59 -0
- package/lib/doc/modelcontainer.js +18 -0
- package/lib/doc/note.js +65 -0
- package/lib/doc/pivot-table.js +132 -0
- package/lib/doc/range.js +257 -0
- package/lib/doc/row.js +415 -0
- package/lib/doc/table.js +465 -0
- package/lib/doc/workbook.js +224 -0
- package/lib/doc/worksheet.js +949 -0
- package/lib/exceljs.bare.js +13 -0
- package/lib/exceljs.browser.js +36 -0
- package/lib/exceljs.nodejs.js +14 -0
- package/lib/stream/xlsx/hyperlink-reader.js +83 -0
- package/lib/stream/xlsx/sheet-comments-writer.js +121 -0
- package/lib/stream/xlsx/sheet-rels-writer.js +119 -0
- package/lib/stream/xlsx/workbook-reader.js +337 -0
- package/lib/stream/xlsx/workbook-writer.js +347 -0
- package/lib/stream/xlsx/worksheet-reader.js +374 -0
- package/lib/stream/xlsx/worksheet-writer.js +717 -0
- package/lib/utils/auto-drain.js +15 -0
- package/lib/utils/browser-buffer-decode.js +14 -0
- package/lib/utils/browser-buffer-encode.js +15 -0
- package/lib/utils/cell-matrix.js +165 -0
- package/lib/utils/col-cache.js +287 -0
- package/lib/utils/copy-style.js +43 -0
- package/lib/utils/encryptor.js +55 -0
- package/lib/utils/iterate-stream.js +48 -0
- package/lib/utils/parse-sax.js +30 -0
- package/lib/utils/shared-formula.js +44 -0
- package/lib/utils/shared-strings.js +35 -0
- package/lib/utils/stream-base64.js +72 -0
- package/lib/utils/stream-buf.js +364 -0
- package/lib/utils/string-buf.js +82 -0
- package/lib/utils/string-builder.js +35 -0
- package/lib/utils/stuttered-pipe.js +67 -0
- package/lib/utils/typed-stack.js +24 -0
- package/lib/utils/under-dash.js +184 -0
- package/lib/utils/utils.js +205 -0
- package/lib/utils/xml-stream.js +169 -0
- package/lib/utils/zip-stream.js +87 -0
- package/lib/xlsx/.rels +11 -0
- package/lib/xlsx/calcChain.xml +6 -0
- package/lib/xlsx/core.xml +7 -0
- package/lib/xlsx/defaultnumformats.js +153 -0
- package/lib/xlsx/rel-type.js +20 -0
- package/lib/xlsx/styles.xml +41 -0
- package/lib/xlsx/workbook.xml +16 -0
- package/lib/xlsx/xform/base-xform.js +145 -0
- package/lib/xlsx/xform/book/defined-name-xform.js +91 -0
- package/lib/xlsx/xform/book/sheet-xform.js +34 -0
- package/lib/xlsx/xform/book/workbook-calc-properties-xform.js +26 -0
- package/lib/xlsx/xform/book/workbook-pivot-cache-xform.js +29 -0
- package/lib/xlsx/xform/book/workbook-properties-xform.js +29 -0
- package/lib/xlsx/xform/book/workbook-view-xform.js +53 -0
- package/lib/xlsx/xform/book/workbook-xform.js +259 -0
- package/lib/xlsx/xform/comment/comment-xform.js +105 -0
- package/lib/xlsx/xform/comment/comments-xform.js +82 -0
- package/lib/xlsx/xform/comment/style/vml-position-xform.js +39 -0
- package/lib/xlsx/xform/comment/style/vml-protection-xform.js +36 -0
- package/lib/xlsx/xform/comment/vml-anchor-xform.js +60 -0
- package/lib/xlsx/xform/comment/vml-client-data-xform.js +95 -0
- package/lib/xlsx/xform/comment/vml-notes-xform.js +107 -0
- package/lib/xlsx/xform/comment/vml-shape-xform.js +95 -0
- package/lib/xlsx/xform/comment/vml-textbox-xform.js +64 -0
- package/lib/xlsx/xform/composite-xform.js +56 -0
- package/lib/xlsx/xform/core/app-heading-pairs-xform.js +32 -0
- package/lib/xlsx/xform/core/app-titles-of-parts-xform.js +28 -0
- package/lib/xlsx/xform/core/app-xform.js +100 -0
- package/lib/xlsx/xform/core/content-types-xform.js +135 -0
- package/lib/xlsx/xform/core/core-xform.js +136 -0
- package/lib/xlsx/xform/core/relationship-xform.js +25 -0
- package/lib/xlsx/xform/core/relationships-xform.js +73 -0
- package/lib/xlsx/xform/drawing/base-cell-anchor-xform.js +48 -0
- package/lib/xlsx/xform/drawing/blip-fill-xform.js +71 -0
- package/lib/xlsx/xform/drawing/blip-xform.js +42 -0
- package/lib/xlsx/xform/drawing/c-nv-pic-pr-xform.js +38 -0
- package/lib/xlsx/xform/drawing/c-nv-pr-xform.js +68 -0
- package/lib/xlsx/xform/drawing/cell-position-xform.js +77 -0
- package/lib/xlsx/xform/drawing/drawing-xform.js +109 -0
- package/lib/xlsx/xform/drawing/ext-lst-xform.js +43 -0
- package/lib/xlsx/xform/drawing/ext-xform.js +44 -0
- package/lib/xlsx/xform/drawing/hlink-click-xform.js +41 -0
- package/lib/xlsx/xform/drawing/nv-pic-pr-xform.js +65 -0
- package/lib/xlsx/xform/drawing/one-cell-anchor-xform.js +63 -0
- package/lib/xlsx/xform/drawing/pic-xform.js +77 -0
- package/lib/xlsx/xform/drawing/sp-pr.js +17 -0
- package/lib/xlsx/xform/drawing/two-cell-anchor-xform.js +62 -0
- package/lib/xlsx/xform/list-xform.js +95 -0
- package/lib/xlsx/xform/pivot-table/cache-field.js +43 -0
- package/lib/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +77 -0
- package/lib/xlsx/xform/pivot-table/pivot-cache-records-xform.js +103 -0
- package/lib/xlsx/xform/pivot-table/pivot-table-xform.js +189 -0
- package/lib/xlsx/xform/sheet/auto-filter-xform.js +38 -0
- package/lib/xlsx/xform/sheet/cell-xform.js +498 -0
- package/lib/xlsx/xform/sheet/cf/cf-rule-xform.js +301 -0
- package/lib/xlsx/xform/sheet/cf/cfvo-xform.js +27 -0
- package/lib/xlsx/xform/sheet/cf/color-scale-xform.js +45 -0
- package/lib/xlsx/xform/sheet/cf/conditional-formatting-xform.js +48 -0
- package/lib/xlsx/xform/sheet/cf/conditional-formattings-xform.js +92 -0
- package/lib/xlsx/xform/sheet/cf/databar-xform.js +49 -0
- package/lib/xlsx/xform/sheet/cf/ext-lst-ref-xform.js +87 -0
- package/lib/xlsx/xform/sheet/cf/formula-xform.js +25 -0
- package/lib/xlsx/xform/sheet/cf/icon-set-xform.js +47 -0
- package/lib/xlsx/xform/sheet/cf-ext/cf-icon-ext-xform.js +27 -0
- package/lib/xlsx/xform/sheet/cf-ext/cf-rule-ext-xform.js +98 -0
- package/lib/xlsx/xform/sheet/cf-ext/cfvo-ext-xform.js +43 -0
- package/lib/xlsx/xform/sheet/cf-ext/conditional-formatting-ext-xform.js +62 -0
- package/lib/xlsx/xform/sheet/cf-ext/conditional-formattings-ext-xform.js +50 -0
- package/lib/xlsx/xform/sheet/cf-ext/databar-ext-xform.js +98 -0
- package/lib/xlsx/xform/sheet/cf-ext/f-ext-xform.js +25 -0
- package/lib/xlsx/xform/sheet/cf-ext/icon-set-ext-xform.js +73 -0
- package/lib/xlsx/xform/sheet/cf-ext/sqref-ext-xform.js +25 -0
- package/lib/xlsx/xform/sheet/col-xform.js +86 -0
- package/lib/xlsx/xform/sheet/data-validations-xform.js +257 -0
- package/lib/xlsx/xform/sheet/dimension-xform.js +29 -0
- package/lib/xlsx/xform/sheet/drawing-xform.js +33 -0
- package/lib/xlsx/xform/sheet/ext-lst-xform.js +86 -0
- package/lib/xlsx/xform/sheet/header-footer-xform.js +146 -0
- package/lib/xlsx/xform/sheet/hyperlink-xform.js +54 -0
- package/lib/xlsx/xform/sheet/merge-cell-xform.js +27 -0
- package/lib/xlsx/xform/sheet/merges.js +56 -0
- package/lib/xlsx/xform/sheet/outline-properties-xform.js +43 -0
- package/lib/xlsx/xform/sheet/page-breaks-xform.js +27 -0
- package/lib/xlsx/xform/sheet/page-margins-xform.js +49 -0
- package/lib/xlsx/xform/sheet/page-setup-properties-xform.js +35 -0
- package/lib/xlsx/xform/sheet/page-setup-xform.js +103 -0
- package/lib/xlsx/xform/sheet/picture-xform.js +33 -0
- package/lib/xlsx/xform/sheet/print-options-xform.js +49 -0
- package/lib/xlsx/xform/sheet/row-breaks-xform.js +39 -0
- package/lib/xlsx/xform/sheet/row-xform.js +142 -0
- package/lib/xlsx/xform/sheet/sheet-format-properties-xform.js +55 -0
- package/lib/xlsx/xform/sheet/sheet-properties-xform.js +90 -0
- package/lib/xlsx/xform/sheet/sheet-protection-xform.js +89 -0
- package/lib/xlsx/xform/sheet/sheet-view-xform.js +202 -0
- package/lib/xlsx/xform/sheet/table-part-xform.js +33 -0
- package/lib/xlsx/xform/sheet/worksheet-xform.js +548 -0
- package/lib/xlsx/xform/simple/boolean-xform.js +31 -0
- package/lib/xlsx/xform/simple/date-xform.js +66 -0
- package/lib/xlsx/xform/simple/float-xform.js +51 -0
- package/lib/xlsx/xform/simple/integer-xform.js +57 -0
- package/lib/xlsx/xform/simple/string-xform.js +51 -0
- package/lib/xlsx/xform/static-xform.js +64 -0
- package/lib/xlsx/xform/strings/phonetic-text-xform.js +98 -0
- package/lib/xlsx/xform/strings/rich-text-xform.js +101 -0
- package/lib/xlsx/xform/strings/shared-string-xform.js +102 -0
- package/lib/xlsx/xform/strings/shared-strings-xform.js +127 -0
- package/lib/xlsx/xform/strings/text-xform.js +44 -0
- package/lib/xlsx/xform/style/alignment-xform.js +172 -0
- package/lib/xlsx/xform/style/border-xform.js +207 -0
- package/lib/xlsx/xform/style/color-xform.js +63 -0
- package/lib/xlsx/xform/style/dxf-xform.js +111 -0
- package/lib/xlsx/xform/style/fill-xform.js +364 -0
- package/lib/xlsx/xform/style/font-xform.js +102 -0
- package/lib/xlsx/xform/style/numfmt-xform.js +63 -0
- package/lib/xlsx/xform/style/protection-xform.js +60 -0
- package/lib/xlsx/xform/style/style-xform.js +125 -0
- package/lib/xlsx/xform/style/styles-xform.js +527 -0
- package/lib/xlsx/xform/style/underline-xform.js +47 -0
- package/lib/xlsx/xform/table/auto-filter-xform.js +81 -0
- package/lib/xlsx/xform/table/custom-filter-xform.js +33 -0
- package/lib/xlsx/xform/table/filter-column-xform.js +96 -0
- package/lib/xlsx/xform/table/filter-xform.js +31 -0
- package/lib/xlsx/xform/table/table-column-xform.js +44 -0
- package/lib/xlsx/xform/table/table-style-info-xform.js +41 -0
- package/lib/xlsx/xform/table/table-xform.js +131 -0
- package/lib/xlsx/xlsx.js +774 -0
- package/lib/xlsx/xml/theme1.js +3 -0
- package/lib/xlsx/xml/theme1.xml +318 -0
- package/package.json +149 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const {EventEmitter} = require('events');
|
|
3
|
+
const {PassThrough, Readable} = require('readable-stream');
|
|
4
|
+
const nodeStream = require('stream');
|
|
5
|
+
const unzip = require('unzipper');
|
|
6
|
+
const tmp = require('tmp');
|
|
7
|
+
const iterateStream = require('../../utils/iterate-stream');
|
|
8
|
+
const parseSax = require('../../utils/parse-sax');
|
|
9
|
+
|
|
10
|
+
const StyleManager = require('../../xlsx/xform/style/styles-xform');
|
|
11
|
+
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
|
|
12
|
+
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
|
|
13
|
+
|
|
14
|
+
const WorksheetReader = require('./worksheet-reader');
|
|
15
|
+
const HyperlinkReader = require('./hyperlink-reader');
|
|
16
|
+
|
|
17
|
+
tmp.setGracefulCleanup();
|
|
18
|
+
|
|
19
|
+
class WorkbookReader extends EventEmitter {
|
|
20
|
+
constructor(input, options = {}) {
|
|
21
|
+
super();
|
|
22
|
+
|
|
23
|
+
this.input = input;
|
|
24
|
+
|
|
25
|
+
this.options = {
|
|
26
|
+
worksheets: 'emit',
|
|
27
|
+
sharedStrings: 'cache',
|
|
28
|
+
hyperlinks: 'ignore',
|
|
29
|
+
styles: 'ignore',
|
|
30
|
+
entries: 'ignore',
|
|
31
|
+
...options,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
this.styles = new StyleManager();
|
|
35
|
+
this.styles.init();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_getStream(input) {
|
|
39
|
+
if (input instanceof nodeStream.Readable || input instanceof Readable) {
|
|
40
|
+
return input;
|
|
41
|
+
}
|
|
42
|
+
if (typeof input === 'string') {
|
|
43
|
+
return fs.createReadStream(input);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Could not recognise input: ${input}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async read(input, options) {
|
|
49
|
+
try {
|
|
50
|
+
for await (const {eventType, value} of this.parse(input, options)) {
|
|
51
|
+
switch (eventType) {
|
|
52
|
+
case 'shared-strings':
|
|
53
|
+
this.emit(eventType, value);
|
|
54
|
+
break;
|
|
55
|
+
case 'worksheet':
|
|
56
|
+
this.emit(eventType, value);
|
|
57
|
+
await value.read();
|
|
58
|
+
break;
|
|
59
|
+
case 'hyperlinks':
|
|
60
|
+
this.emit(eventType, value);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.emit('end');
|
|
65
|
+
this.emit('finished');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.emit('error', error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async *[Symbol.asyncIterator]() {
|
|
72
|
+
for await (const {eventType, value} of this.parse()) {
|
|
73
|
+
if (eventType === 'worksheet') {
|
|
74
|
+
yield value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async *parse(input, options) {
|
|
80
|
+
if (options) this.options = options;
|
|
81
|
+
const stream = (this.stream = this._getStream(input || this.input));
|
|
82
|
+
const zip = unzip.Parse({forceStream: true});
|
|
83
|
+
stream.pipe(zip);
|
|
84
|
+
|
|
85
|
+
// worksheets, deferred for parsing after shared strings reading
|
|
86
|
+
const waitingWorkSheets = [];
|
|
87
|
+
|
|
88
|
+
for await (const entry of iterateStream(zip)) {
|
|
89
|
+
let match;
|
|
90
|
+
let sheetNo;
|
|
91
|
+
switch (entry.path) {
|
|
92
|
+
case '_rels/.rels':
|
|
93
|
+
break;
|
|
94
|
+
case 'xl/_rels/workbook.xml.rels':
|
|
95
|
+
await this._parseRels(entry);
|
|
96
|
+
break;
|
|
97
|
+
case 'xl/workbook.xml':
|
|
98
|
+
await this._parseWorkbook(entry);
|
|
99
|
+
break;
|
|
100
|
+
case 'xl/sharedStrings.xml':
|
|
101
|
+
yield* this._parseSharedStrings(entry);
|
|
102
|
+
break;
|
|
103
|
+
case 'xl/styles.xml':
|
|
104
|
+
await this._parseStyles(entry);
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
|
|
108
|
+
match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
|
|
109
|
+
sheetNo = match[1];
|
|
110
|
+
if (this.sharedStrings && this.workbookRels) {
|
|
111
|
+
yield* this._parseWorksheet(iterateStream(entry), sheetNo);
|
|
112
|
+
} else {
|
|
113
|
+
// create temp file for each worksheet
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
tmp.file((err, path, fd, tempFileCleanupCallback) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
return reject(err);
|
|
118
|
+
}
|
|
119
|
+
waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback});
|
|
120
|
+
|
|
121
|
+
const tempStream = fs.createWriteStream(path);
|
|
122
|
+
tempStream.on('error', reject);
|
|
123
|
+
entry.pipe(tempStream);
|
|
124
|
+
return tempStream.on('finish', () => {
|
|
125
|
+
return resolve();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else if (entry.path.match(/xl\/worksheets\/_rels\/sheet\d+[.]xml.rels/)) {
|
|
131
|
+
match = entry.path.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
|
|
132
|
+
sheetNo = match[1];
|
|
133
|
+
yield* this._parseHyperlinks(iterateStream(entry), sheetNo);
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
entry.autodrain();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const {sheetNo, path, tempFileCleanupCallback} of waitingWorkSheets) {
|
|
141
|
+
let fileStream = fs.createReadStream(path);
|
|
142
|
+
// TODO: Remove once node v8 is deprecated
|
|
143
|
+
// Detect and upgrade old fileStreams
|
|
144
|
+
if (!fileStream[Symbol.asyncIterator]) {
|
|
145
|
+
fileStream = fileStream.pipe(new PassThrough());
|
|
146
|
+
}
|
|
147
|
+
yield* this._parseWorksheet(fileStream, sheetNo);
|
|
148
|
+
tempFileCleanupCallback();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_emitEntry(payload) {
|
|
153
|
+
if (this.options.entries === 'emit') {
|
|
154
|
+
this.emit('entry', payload);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async _parseRels(entry) {
|
|
159
|
+
const xform = new RelationshipsXform();
|
|
160
|
+
this.workbookRels = await xform.parseStream(iterateStream(entry));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _parseWorkbook(entry) {
|
|
164
|
+
this._emitEntry({type: 'workbook'});
|
|
165
|
+
|
|
166
|
+
const workbook = new WorkbookXform();
|
|
167
|
+
await workbook.parseStream(iterateStream(entry));
|
|
168
|
+
|
|
169
|
+
this.properties = workbook.map.workbookPr;
|
|
170
|
+
this.model = workbook.model;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async *_parseSharedStrings(entry) {
|
|
174
|
+
this._emitEntry({type: 'shared-strings'});
|
|
175
|
+
switch (this.options.sharedStrings) {
|
|
176
|
+
case 'cache':
|
|
177
|
+
this.sharedStrings = [];
|
|
178
|
+
break;
|
|
179
|
+
case 'emit':
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let text = null;
|
|
186
|
+
let richText = [];
|
|
187
|
+
let index = 0;
|
|
188
|
+
let font = null;
|
|
189
|
+
for await (const events of parseSax(iterateStream(entry))) {
|
|
190
|
+
for (const {eventType, value} of events) {
|
|
191
|
+
if (eventType === 'opentag') {
|
|
192
|
+
const node = value;
|
|
193
|
+
switch (node.name) {
|
|
194
|
+
case 'b':
|
|
195
|
+
font = font || {};
|
|
196
|
+
font.bold = true;
|
|
197
|
+
break;
|
|
198
|
+
case 'charset':
|
|
199
|
+
font = font || {};
|
|
200
|
+
font.charset = parseInt(node.attributes.charset, 10);
|
|
201
|
+
break;
|
|
202
|
+
case 'color':
|
|
203
|
+
font = font || {};
|
|
204
|
+
font.color = {};
|
|
205
|
+
if (node.attributes.rgb) {
|
|
206
|
+
font.color.argb = node.attributes.argb;
|
|
207
|
+
}
|
|
208
|
+
if (node.attributes.val) {
|
|
209
|
+
font.color.argb = node.attributes.val;
|
|
210
|
+
}
|
|
211
|
+
if (node.attributes.theme) {
|
|
212
|
+
font.color.theme = node.attributes.theme;
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case 'family':
|
|
216
|
+
font = font || {};
|
|
217
|
+
font.family = parseInt(node.attributes.val, 10);
|
|
218
|
+
break;
|
|
219
|
+
case 'i':
|
|
220
|
+
font = font || {};
|
|
221
|
+
font.italic = true;
|
|
222
|
+
break;
|
|
223
|
+
case 'outline':
|
|
224
|
+
font = font || {};
|
|
225
|
+
font.outline = true;
|
|
226
|
+
break;
|
|
227
|
+
case 'rFont':
|
|
228
|
+
font = font || {};
|
|
229
|
+
font.name = node.value;
|
|
230
|
+
break;
|
|
231
|
+
case 'si':
|
|
232
|
+
font = null;
|
|
233
|
+
richText = [];
|
|
234
|
+
text = null;
|
|
235
|
+
break;
|
|
236
|
+
case 'sz':
|
|
237
|
+
font = font || {};
|
|
238
|
+
font.size = parseInt(node.attributes.val, 10);
|
|
239
|
+
break;
|
|
240
|
+
case 'strike':
|
|
241
|
+
break;
|
|
242
|
+
case 't':
|
|
243
|
+
text = null;
|
|
244
|
+
break;
|
|
245
|
+
case 'u':
|
|
246
|
+
font = font || {};
|
|
247
|
+
font.underline = true;
|
|
248
|
+
break;
|
|
249
|
+
case 'vertAlign':
|
|
250
|
+
font = font || {};
|
|
251
|
+
font.vertAlign = node.attributes.val;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
} else if (eventType === 'text') {
|
|
255
|
+
text = text ? text + value : value;
|
|
256
|
+
} else if (eventType === 'closetag') {
|
|
257
|
+
const node = value;
|
|
258
|
+
switch (node.name) {
|
|
259
|
+
case 'r':
|
|
260
|
+
richText.push({
|
|
261
|
+
font,
|
|
262
|
+
text,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
font = null;
|
|
266
|
+
text = null;
|
|
267
|
+
break;
|
|
268
|
+
case 'si':
|
|
269
|
+
if (this.options.sharedStrings === 'cache') {
|
|
270
|
+
this.sharedStrings.push(richText.length ? {richText} : text);
|
|
271
|
+
} else if (this.options.sharedStrings === 'emit') {
|
|
272
|
+
yield {index: index++, text: richText.length ? {richText} : text};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
richText = [];
|
|
276
|
+
font = null;
|
|
277
|
+
text = null;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async _parseStyles(entry) {
|
|
286
|
+
this._emitEntry({type: 'styles'});
|
|
287
|
+
if (this.options.styles === 'cache') {
|
|
288
|
+
this.styles = new StyleManager();
|
|
289
|
+
await this.styles.parseStream(iterateStream(entry));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
*_parseWorksheet(iterator, sheetNo) {
|
|
294
|
+
this._emitEntry({type: 'worksheet', id: sheetNo});
|
|
295
|
+
const worksheetReader = new WorksheetReader({
|
|
296
|
+
workbook: this,
|
|
297
|
+
id: sheetNo,
|
|
298
|
+
iterator,
|
|
299
|
+
options: this.options,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const matchingRel = (this.workbookRels || []).find(rel => rel.Target === `worksheets/sheet${sheetNo}.xml`);
|
|
303
|
+
const matchingSheet = matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id);
|
|
304
|
+
if (matchingSheet) {
|
|
305
|
+
worksheetReader.id = matchingSheet.id;
|
|
306
|
+
worksheetReader.name = matchingSheet.name;
|
|
307
|
+
worksheetReader.state = matchingSheet.state;
|
|
308
|
+
}
|
|
309
|
+
if (this.options.worksheets === 'emit') {
|
|
310
|
+
yield {eventType: 'worksheet', value: worksheetReader};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
*_parseHyperlinks(iterator, sheetNo) {
|
|
315
|
+
this._emitEntry({type: 'hyperlinks', id: sheetNo});
|
|
316
|
+
const hyperlinksReader = new HyperlinkReader({
|
|
317
|
+
workbook: this,
|
|
318
|
+
id: sheetNo,
|
|
319
|
+
iterator,
|
|
320
|
+
options: this.options,
|
|
321
|
+
});
|
|
322
|
+
if (this.options.hyperlinks === 'emit') {
|
|
323
|
+
yield {eventType: 'hyperlinks', value: hyperlinksReader};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// for reference - these are the valid values for options
|
|
329
|
+
WorkbookReader.Options = {
|
|
330
|
+
worksheets: ['emit', 'ignore'],
|
|
331
|
+
sharedStrings: ['cache', 'emit', 'ignore'],
|
|
332
|
+
hyperlinks: ['cache', 'emit', 'ignore'],
|
|
333
|
+
styles: ['cache', 'ignore'],
|
|
334
|
+
entries: ['emit', 'ignore'],
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
module.exports = WorkbookReader;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const Archiver = require('archiver');
|
|
3
|
+
|
|
4
|
+
const StreamBuf = require('../../utils/stream-buf');
|
|
5
|
+
|
|
6
|
+
const RelType = require('../../xlsx/rel-type');
|
|
7
|
+
const StylesXform = require('../../xlsx/xform/style/styles-xform');
|
|
8
|
+
const SharedStrings = require('../../utils/shared-strings');
|
|
9
|
+
const DefinedNames = require('../../doc/defined-names');
|
|
10
|
+
|
|
11
|
+
const CoreXform = require('../../xlsx/xform/core/core-xform');
|
|
12
|
+
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
|
|
13
|
+
const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
|
|
14
|
+
const AppXform = require('../../xlsx/xform/core/app-xform');
|
|
15
|
+
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
|
|
16
|
+
const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
|
|
17
|
+
|
|
18
|
+
const WorksheetWriter = require('./worksheet-writer');
|
|
19
|
+
|
|
20
|
+
const theme1Xml = require('../../xlsx/xml/theme1.js');
|
|
21
|
+
|
|
22
|
+
class WorkbookWriter {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
options = options || {};
|
|
25
|
+
|
|
26
|
+
this.created = options.created || new Date();
|
|
27
|
+
this.modified = options.modified || this.created;
|
|
28
|
+
this.creator = options.creator || 'ExcelJS';
|
|
29
|
+
this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
|
|
30
|
+
this.lastPrinted = options.lastPrinted;
|
|
31
|
+
|
|
32
|
+
// using shared strings creates a smaller xlsx file but may use more memory
|
|
33
|
+
this.useSharedStrings = options.useSharedStrings || false;
|
|
34
|
+
this.sharedStrings = new SharedStrings();
|
|
35
|
+
|
|
36
|
+
// style manager
|
|
37
|
+
this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
|
|
38
|
+
|
|
39
|
+
// defined names
|
|
40
|
+
this._definedNames = new DefinedNames();
|
|
41
|
+
|
|
42
|
+
this._worksheets = [];
|
|
43
|
+
this.views = [];
|
|
44
|
+
|
|
45
|
+
this.zipOptions = options.zip;
|
|
46
|
+
|
|
47
|
+
this.media = [];
|
|
48
|
+
this.commentRefs = [];
|
|
49
|
+
|
|
50
|
+
this.zip = Archiver('zip', this.zipOptions);
|
|
51
|
+
if (options.stream) {
|
|
52
|
+
this.stream = options.stream;
|
|
53
|
+
} else if (options.filename) {
|
|
54
|
+
this.stream = fs.createWriteStream(options.filename);
|
|
55
|
+
} else {
|
|
56
|
+
this.stream = new StreamBuf();
|
|
57
|
+
}
|
|
58
|
+
this.zip.pipe(this.stream);
|
|
59
|
+
|
|
60
|
+
// these bits can be added right now
|
|
61
|
+
this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get definedNames() {
|
|
65
|
+
return this._definedNames;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_openStream(path) {
|
|
69
|
+
const stream = new StreamBuf({bufSize: 65536, batch: true});
|
|
70
|
+
this.zip.append(stream, {name: path});
|
|
71
|
+
stream.on('finish', () => {
|
|
72
|
+
stream.emit('zipped');
|
|
73
|
+
});
|
|
74
|
+
return stream;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_commitWorksheets() {
|
|
78
|
+
const commitWorksheet = function(worksheet) {
|
|
79
|
+
if (!worksheet.committed) {
|
|
80
|
+
return new Promise(resolve => {
|
|
81
|
+
worksheet.stream.on('zipped', () => {
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
worksheet.commit();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
};
|
|
89
|
+
// if there are any uncommitted worksheets, commit them now and wait
|
|
90
|
+
const promises = this._worksheets.map(commitWorksheet);
|
|
91
|
+
if (promises.length) {
|
|
92
|
+
return Promise.all(promises);
|
|
93
|
+
}
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async commit() {
|
|
98
|
+
// commit all worksheets, then add suplimentary files
|
|
99
|
+
await this.promise;
|
|
100
|
+
await this.addMedia();
|
|
101
|
+
await this._commitWorksheets();
|
|
102
|
+
await Promise.all([
|
|
103
|
+
this.addContentTypes(),
|
|
104
|
+
this.addApp(),
|
|
105
|
+
this.addCore(),
|
|
106
|
+
this.addSharedStrings(),
|
|
107
|
+
this.addStyles(),
|
|
108
|
+
this.addWorkbookRels(),
|
|
109
|
+
]);
|
|
110
|
+
await this.addWorkbook();
|
|
111
|
+
return this._finalize();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get nextId() {
|
|
115
|
+
// find the next unique spot to add worksheet
|
|
116
|
+
let i;
|
|
117
|
+
for (i = 1; i < this._worksheets.length; i++) {
|
|
118
|
+
if (!this._worksheets[i]) {
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return this._worksheets.length || 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
addImage(image) {
|
|
126
|
+
const id = this.media.length;
|
|
127
|
+
const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
|
|
128
|
+
this.media.push(medium);
|
|
129
|
+
return id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getImage(id) {
|
|
133
|
+
return this.media[id];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
addWorksheet(name, options) {
|
|
137
|
+
// it's possible to add a worksheet with different than default
|
|
138
|
+
// shared string handling
|
|
139
|
+
// in fact, it's even possible to switch it mid-sheet
|
|
140
|
+
options = options || {};
|
|
141
|
+
const useSharedStrings =
|
|
142
|
+
options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
|
|
143
|
+
|
|
144
|
+
if (options.tabColor) {
|
|
145
|
+
// eslint-disable-next-line no-console
|
|
146
|
+
console.trace('tabColor option has moved to { properties: tabColor: {...} }');
|
|
147
|
+
options.properties = Object.assign(
|
|
148
|
+
{
|
|
149
|
+
tabColor: options.tabColor,
|
|
150
|
+
},
|
|
151
|
+
options.properties
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const id = this.nextId;
|
|
156
|
+
name = name || `sheet${id}`;
|
|
157
|
+
|
|
158
|
+
const worksheet = new WorksheetWriter({
|
|
159
|
+
id,
|
|
160
|
+
name,
|
|
161
|
+
workbook: this,
|
|
162
|
+
useSharedStrings,
|
|
163
|
+
properties: options.properties,
|
|
164
|
+
state: options.state,
|
|
165
|
+
pageSetup: options.pageSetup,
|
|
166
|
+
views: options.views,
|
|
167
|
+
autoFilter: options.autoFilter,
|
|
168
|
+
headerFooter: options.headerFooter,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this._worksheets[id] = worksheet;
|
|
172
|
+
return worksheet;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getWorksheet(id) {
|
|
176
|
+
if (id === undefined) {
|
|
177
|
+
return this._worksheets.find(() => true);
|
|
178
|
+
}
|
|
179
|
+
if (typeof id === 'number') {
|
|
180
|
+
return this._worksheets[id];
|
|
181
|
+
}
|
|
182
|
+
if (typeof id === 'string') {
|
|
183
|
+
return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
addStyles() {
|
|
189
|
+
return new Promise(resolve => {
|
|
190
|
+
this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
addThemes() {
|
|
196
|
+
return new Promise(resolve => {
|
|
197
|
+
this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
|
|
198
|
+
resolve();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
addOfficeRels() {
|
|
203
|
+
return new Promise(resolve => {
|
|
204
|
+
const xform = new RelationshipsXform();
|
|
205
|
+
const xml = xform.toXml([
|
|
206
|
+
{Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
|
|
207
|
+
{Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
|
|
208
|
+
{Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
|
|
209
|
+
]);
|
|
210
|
+
this.zip.append(xml, {name: '/_rels/.rels'});
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
addContentTypes() {
|
|
216
|
+
return new Promise(resolve => {
|
|
217
|
+
const model = {
|
|
218
|
+
worksheets: this._worksheets.filter(Boolean),
|
|
219
|
+
sharedStrings: this.sharedStrings,
|
|
220
|
+
commentRefs: this.commentRefs,
|
|
221
|
+
media: this.media,
|
|
222
|
+
};
|
|
223
|
+
const xform = new ContentTypesXform();
|
|
224
|
+
const xml = xform.toXml(model);
|
|
225
|
+
this.zip.append(xml, {name: '[Content_Types].xml'});
|
|
226
|
+
resolve();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
addMedia() {
|
|
231
|
+
return Promise.all(
|
|
232
|
+
this.media.map(medium => {
|
|
233
|
+
if (medium.type === 'image') {
|
|
234
|
+
const filename = `xl/media/${medium.name}`;
|
|
235
|
+
if (medium.filename) {
|
|
236
|
+
return this.zip.file(medium.filename, {name: filename});
|
|
237
|
+
}
|
|
238
|
+
if (medium.buffer) {
|
|
239
|
+
return this.zip.append(medium.buffer, {name: filename});
|
|
240
|
+
}
|
|
241
|
+
if (medium.base64) {
|
|
242
|
+
const dataimg64 = medium.base64;
|
|
243
|
+
const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
|
|
244
|
+
return this.zip.append(content, {name: filename, base64: true});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
throw new Error('Unsupported media');
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
addApp() {
|
|
253
|
+
return new Promise(resolve => {
|
|
254
|
+
const model = {
|
|
255
|
+
worksheets: this._worksheets.filter(Boolean),
|
|
256
|
+
};
|
|
257
|
+
const xform = new AppXform();
|
|
258
|
+
const xml = xform.toXml(model);
|
|
259
|
+
this.zip.append(xml, {name: 'docProps/app.xml'});
|
|
260
|
+
resolve();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
addCore() {
|
|
265
|
+
return new Promise(resolve => {
|
|
266
|
+
const coreXform = new CoreXform();
|
|
267
|
+
const xml = coreXform.toXml(this);
|
|
268
|
+
this.zip.append(xml, {name: 'docProps/core.xml'});
|
|
269
|
+
resolve();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
addSharedStrings() {
|
|
274
|
+
if (this.sharedStrings.count) {
|
|
275
|
+
return new Promise(resolve => {
|
|
276
|
+
const sharedStringsXform = new SharedStringsXform();
|
|
277
|
+
const xml = sharedStringsXform.toXml(this.sharedStrings);
|
|
278
|
+
this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
|
|
279
|
+
resolve();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return Promise.resolve();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
addWorkbookRels() {
|
|
286
|
+
let count = 1;
|
|
287
|
+
const relationships = [
|
|
288
|
+
{Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
|
|
289
|
+
{Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
|
|
290
|
+
];
|
|
291
|
+
if (this.sharedStrings.count) {
|
|
292
|
+
relationships.push({
|
|
293
|
+
Id: `rId${count++}`,
|
|
294
|
+
Type: RelType.SharedStrings,
|
|
295
|
+
Target: 'sharedStrings.xml',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
this._worksheets.forEach(worksheet => {
|
|
299
|
+
if (worksheet) {
|
|
300
|
+
worksheet.rId = `rId${count++}`;
|
|
301
|
+
relationships.push({
|
|
302
|
+
Id: worksheet.rId,
|
|
303
|
+
Type: RelType.Worksheet,
|
|
304
|
+
Target: `worksheets/sheet${worksheet.id}.xml`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
return new Promise(resolve => {
|
|
309
|
+
const xform = new RelationshipsXform();
|
|
310
|
+
const xml = xform.toXml(relationships);
|
|
311
|
+
this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
|
|
312
|
+
resolve();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
addWorkbook() {
|
|
317
|
+
const {zip} = this;
|
|
318
|
+
const model = {
|
|
319
|
+
worksheets: this._worksheets.filter(Boolean),
|
|
320
|
+
definedNames: this._definedNames.model,
|
|
321
|
+
views: this.views,
|
|
322
|
+
properties: {},
|
|
323
|
+
calcProperties: {},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return new Promise(resolve => {
|
|
327
|
+
const xform = new WorkbookXform();
|
|
328
|
+
xform.prepare(model);
|
|
329
|
+
zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_finalize() {
|
|
335
|
+
return new Promise((resolve, reject) => {
|
|
336
|
+
this.stream.on('error', reject);
|
|
337
|
+
this.stream.on('finish', () => {
|
|
338
|
+
resolve(this);
|
|
339
|
+
});
|
|
340
|
+
this.zip.on('error', reject);
|
|
341
|
+
|
|
342
|
+
this.zip.finalize();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = WorkbookWriter;
|