documonster 0.20.0 → 0.20.1
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/README.md +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/archive/compression/deflate-fallback.js +20 -50
- package/dist/esm/modules/archive/compression/streaming-compress.js +3 -2
- package/dist/esm/modules/archive/unzip/stream.js +1 -1
- package/dist/esm/modules/csv/stream/parser.js +5 -1
- package/dist/esm/modules/excel/stream/workbook-reader.browser.js +86 -14
- package/dist/esm/modules/excel/stream/workbook-reader.js +6 -3
- package/dist/esm/modules/excel/stream/xlsb-record-stream.js +32 -11
- package/dist/esm/modules/excel/stream/xlsb-worksheet-reader.js +32 -28
- package/dist/esm/modules/excel/utils/copy-style.js +28 -14
- package/dist/esm/modules/excel/utils/stream-buf.js +74 -2
- package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +13 -0
- package/dist/esm/modules/word/writer/streaming-writer.js +38 -6
- package/dist/esm/modules/xml/encode.js +15 -0
- package/dist/iife/documonster.archive.iife.min.js +2 -2
- package/dist/iife/documonster.csv.iife.min.js +3 -2
- package/dist/iife/documonster.draw.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +53 -53
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +11 -11
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +60 -60
- package/dist/iife/documonster.xml.iife.min.js +7 -7
- package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +26 -0
- package/dist/types/modules/excel/stream/xlsb-record-stream.d.ts +11 -0
- package/dist/types/modules/excel/utils/stream-buf.d.ts +19 -0
- package/dist/types/modules/word/writer/streaming-writer.d.ts +13 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
211
211
|
|
|
212
212
|
```html
|
|
213
213
|
<!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
|
|
214
|
-
<script src="https://unpkg.com/documonster@0.20.
|
|
214
|
+
<script src="https://unpkg.com/documonster@0.20.1/dist/iife/documonster.excel.iife.min.js"></script>
|
|
215
215
|
<script>
|
|
216
216
|
const { Workbook, Cell } = Documonster.Excel;
|
|
217
217
|
const wb = Workbook.create();
|
package/README_zh.md
CHANGED
|
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
|
|
|
198
198
|
|
|
199
199
|
```html
|
|
200
200
|
<!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
|
|
201
|
-
<script src="https://unpkg.com/documonster@0.20.
|
|
201
|
+
<script src="https://unpkg.com/documonster@0.20.1/dist/iife/documonster.excel.iife.min.js"></script>
|
|
202
202
|
<script>
|
|
203
203
|
const { Workbook, Cell } = Documonster.Excel;
|
|
204
204
|
const wb = Workbook.create();
|
|
@@ -686,75 +686,45 @@ function buildCodeLengths(freqs, maxBits) {
|
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
688
|
extractDepths(root, 0);
|
|
689
|
-
// --- Length limiting
|
|
690
|
-
//
|
|
689
|
+
// --- Length limiting (zlib's gen_bitlen overflow handling) ---
|
|
690
|
+
//
|
|
691
|
+
// A Huffman tree over 286 symbols can be deeper than DEFLATE's 15 bits. Clamping the deep leaves to `maxBits`
|
|
692
|
+
// over-subscribes the code; it is then repaired one unit at a time by turning a leaf at the deepest length below
|
|
693
|
+
// `maxBits` into a node with two children one level down, and removing one leaf at `maxBits`. That keeps the number
|
|
694
|
+
// of leaves and lowers the Kraft sum by exactly one, so the code ends exactly complete — which zlib requires: an
|
|
695
|
+
// incomplete literal/length code is "invalid literal/lengths set", and every browser's DecompressionStream is zlib.
|
|
696
|
+
// The previous repair overshot and padded with phantom codes, which took real symbols' places and left the code
|
|
697
|
+
// incomplete: only this library's lenient inflater could read it back.
|
|
691
698
|
const blCount = new Uint16Array(maxBits + 1);
|
|
692
699
|
for (let i = 0; i < n; i++) {
|
|
693
700
|
if (codeLens[i] > 0) {
|
|
694
|
-
|
|
695
|
-
blCount[maxBits]++;
|
|
696
|
-
codeLens[i] = maxBits;
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
blCount[codeLens[i]]++;
|
|
700
|
-
}
|
|
701
|
+
blCount[Math.min(codeLens[i], maxBits)]++;
|
|
701
702
|
}
|
|
702
703
|
}
|
|
703
|
-
|
|
704
|
+
const target = 1 << maxBits;
|
|
704
705
|
let kraft = 0;
|
|
705
706
|
for (let bits = 1; bits <= maxBits; bits++) {
|
|
706
707
|
kraft += blCount[bits] << (maxBits - bits);
|
|
707
708
|
}
|
|
708
|
-
const target = 1 << maxBits;
|
|
709
709
|
if (kraft === target) {
|
|
710
|
-
return codeLens;
|
|
710
|
+
return codeLens;
|
|
711
711
|
}
|
|
712
|
-
// Redistribute to satisfy Kraft's inequality.
|
|
713
|
-
// Strategy: move symbols from shorter lengths to maxBits until balanced.
|
|
714
|
-
// Each symbol moved from length `bits` to `maxBits` reduces kraft by
|
|
715
|
-
// (2^(maxBits-bits) - 1) — we remove a large weight and add a weight of 1.
|
|
716
712
|
while (kraft > target) {
|
|
717
|
-
// Find a code length < maxBits that has symbols we can push down.
|
|
718
|
-
// Start from maxBits-1 to minimize the damage per move.
|
|
719
713
|
let bits = maxBits - 1;
|
|
720
|
-
while (
|
|
714
|
+
while (blCount[bits] === 0) {
|
|
721
715
|
bits--;
|
|
722
716
|
}
|
|
723
|
-
if (bits === 0) {
|
|
724
|
-
break; // Can't redistribute further
|
|
725
|
-
}
|
|
726
|
-
// Move one symbol from length `bits` to length `maxBits`
|
|
727
717
|
blCount[bits]--;
|
|
728
|
-
blCount[
|
|
729
|
-
|
|
730
|
-
kraft
|
|
731
|
-
}
|
|
732
|
-
// If kraft < target (under-allocated), add dummy codes at maxBits.
|
|
733
|
-
// This can happen when we overshoot during redistribution.
|
|
734
|
-
while (kraft < target) {
|
|
735
|
-
blCount[maxBits]++;
|
|
736
|
-
kraft++;
|
|
737
|
-
}
|
|
738
|
-
// Reassign code lengths to symbols (preserve relative order: longer
|
|
739
|
-
// codes go to less frequent symbols, matching the Huffman property).
|
|
740
|
-
// Sort symbols by their original code length (longest first), then by
|
|
741
|
-
// frequency (rarest first) for same length.
|
|
742
|
-
const symbolsByLen = [];
|
|
743
|
-
for (let i = 0; i < n; i++) {
|
|
744
|
-
if (codeLens[i] > 0) {
|
|
745
|
-
symbolsByLen.push({ sym: i, origLen: codeLens[i], freq: freqs[i] });
|
|
746
|
-
}
|
|
718
|
+
blCount[bits + 1] += 2;
|
|
719
|
+
blCount[maxBits]--;
|
|
720
|
+
kraft--;
|
|
747
721
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
let symIdx = 0;
|
|
722
|
+
// Hand the lengths out again, longest to the rarest symbols, as the Huffman tree would have.
|
|
723
|
+
const bySymbolRarity = activeSymbols.slice().sort((a, b) => a.freq - b.freq || b.sym - a.sym);
|
|
724
|
+
let next = 0;
|
|
752
725
|
for (let bits = maxBits; bits >= 1; bits--) {
|
|
753
726
|
for (let count = blCount[bits]; count > 0; count--) {
|
|
754
|
-
|
|
755
|
-
codeLens[symbolsByLen[symIdx].sym] = bits;
|
|
756
|
-
symIdx++;
|
|
757
|
-
}
|
|
727
|
+
codeLens[bySymbolRarity[next++].sym] = bits;
|
|
758
728
|
}
|
|
759
729
|
}
|
|
760
730
|
return codeLens;
|
|
@@ -153,10 +153,11 @@ export class SyncDeflater {
|
|
|
153
153
|
_flushBatch(final) {
|
|
154
154
|
let input;
|
|
155
155
|
if (this._pending.length === 0) {
|
|
156
|
-
input =
|
|
156
|
+
input = new Uint8Array(0);
|
|
157
157
|
}
|
|
158
158
|
else if (this._pending.length === 1) {
|
|
159
|
-
|
|
159
|
+
// zlib reads a `Uint8Array` as it is; copying it into a `Buffer` first only cost a second pass over the batch.
|
|
160
|
+
input = this._pending[0];
|
|
160
161
|
}
|
|
161
162
|
else {
|
|
162
163
|
input = Buffer.concat(this._pending);
|
|
@@ -5,7 +5,7 @@ import { PullStream, runParseLoop, streamUntilValidatedDataDescriptor } from "./
|
|
|
5
5
|
* Creates an InflateRaw stream using Node.js native zlib.
|
|
6
6
|
*/
|
|
7
7
|
function createInflateRaw() {
|
|
8
|
-
return zlib.createInflateRaw();
|
|
8
|
+
return zlib.createInflateRaw({ chunkSize: 64 * 1024 });
|
|
9
9
|
}
|
|
10
10
|
const dataDescriptorSignature = DATA_DESCRIPTOR_SIGNATURE_BYTES;
|
|
11
11
|
export function createParseClass(createInflateRawFn) {
|
|
@@ -869,7 +869,11 @@ export class CsvParserStream extends Transform {
|
|
|
869
869
|
// Fast mode already split this record by its configured `lineEnding`; with a custom
|
|
870
870
|
// separator, CR and LF in `raw` are ordinary content and must not alter `line`,
|
|
871
871
|
// skipLines or toLine.
|
|
872
|
-
if (raw !== undefined &&
|
|
872
|
+
if (raw !== undefined &&
|
|
873
|
+
!this.parseConfig.fastMode &&
|
|
874
|
+
// A row holds a line break only inside a quoted field, which almost none do: a native search rules it out
|
|
875
|
+
// without walking every character of every row in script.
|
|
876
|
+
(raw.indexOf("\n") !== -1 || raw.indexOf("\r") !== -1)) {
|
|
873
877
|
let newlines = 1;
|
|
874
878
|
for (let i = 0; i < raw.length; i++) {
|
|
875
879
|
const ch = raw.charCodeAt(i);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { FileTooLargeError } from "../../archive/core/errors.js";
|
|
12
12
|
import { createParse } from "#platform/modules/archive/unzip/stream";
|
|
13
|
-
import { ExcelFileError } from "../errors.js";
|
|
13
|
+
import { ExcelFileError, ExcelStreamStateError } from "../errors.js";
|
|
14
14
|
import { HyperlinkReader } from "./hyperlink-reader.js";
|
|
15
15
|
import { WorksheetReader } from "./worksheet-reader.js";
|
|
16
16
|
import { XlsbWorksheetReader } from "./xlsb-worksheet-stream-reader.js";
|
|
@@ -97,6 +97,57 @@ function boundedPart(entry, limit) {
|
|
|
97
97
|
entry.on("error", (err) => bounded.destroy(err));
|
|
98
98
|
return entry.pipe(bounded);
|
|
99
99
|
}
|
|
100
|
+
export function handOut(source, what) {
|
|
101
|
+
let state = "unread";
|
|
102
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
103
|
+
async function* chunks() {
|
|
104
|
+
if (state === "skipped") {
|
|
105
|
+
throw new ExcelStreamStateError(`read ${what}`, "the workbook reader had already moved past it; read each part before asking for the next");
|
|
106
|
+
}
|
|
107
|
+
state = "reading";
|
|
108
|
+
try {
|
|
109
|
+
// Not `yield* source`: a caller leaving early must not close the underlying iterator, because what it has not
|
|
110
|
+
// read still has to be drained through it — see `drain`.
|
|
111
|
+
for (let next = await iterator.next(); !next.done; next = await iterator.next()) {
|
|
112
|
+
yield next.value;
|
|
113
|
+
}
|
|
114
|
+
state = "done";
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
if (state !== "done") {
|
|
118
|
+
state = "left";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
chunks: chunks(),
|
|
124
|
+
finish() {
|
|
125
|
+
if (state === "unread") {
|
|
126
|
+
state = "skipped";
|
|
127
|
+
return "all";
|
|
128
|
+
}
|
|
129
|
+
return state === "left" ? "rest" : "nothing";
|
|
130
|
+
},
|
|
131
|
+
async drain() {
|
|
132
|
+
while (!(await iterator.next()).done) {
|
|
133
|
+
// discarded
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Let the archive move past a ZIP part the caller did not read to the end. */
|
|
139
|
+
async function releasePart(entry, part, viaStream) {
|
|
140
|
+
const left = part.finish();
|
|
141
|
+
if (left === "all" && !viaStream) {
|
|
142
|
+
// Untouched: the unzipper can skip its compressed bytes without inflating them, as for any part not asked for.
|
|
143
|
+
entry.autodrain();
|
|
144
|
+
}
|
|
145
|
+
else if (left !== "nothing") {
|
|
146
|
+
// Partly read, or behind a stream that already holds some of it: read the rest through the reader's own iterator,
|
|
147
|
+
// the path a full read takes, rather than switching the entry to a skip half-way.
|
|
148
|
+
await part.drain();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
100
151
|
// ============================================================================
|
|
101
152
|
// Base Class
|
|
102
153
|
// ============================================================================
|
|
@@ -208,7 +259,9 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
208
259
|
await value.read();
|
|
209
260
|
break;
|
|
210
261
|
case "hyperlinks":
|
|
262
|
+
// Read here as a worksheet is: the archive cannot move past the part until it has been consumed.
|
|
211
263
|
this.emit(eventType, value);
|
|
264
|
+
await value.read();
|
|
212
265
|
break;
|
|
213
266
|
}
|
|
214
267
|
}
|
|
@@ -224,6 +277,11 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
224
277
|
if (eventType === "worksheet") {
|
|
225
278
|
yield value;
|
|
226
279
|
}
|
|
280
|
+
else if (eventType === "hyperlinks") {
|
|
281
|
+
// This iterator yields worksheets only, so nothing else can consume the part, and the archive cannot move
|
|
282
|
+
// past it until something does.
|
|
283
|
+
await value.read();
|
|
284
|
+
}
|
|
227
285
|
}
|
|
228
286
|
}
|
|
229
287
|
async *parse(input, options) {
|
|
@@ -680,6 +738,7 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
680
738
|
}
|
|
681
739
|
});
|
|
682
740
|
stream.pipe(zip);
|
|
741
|
+
const emitWorksheets = this.options.worksheets === "emit";
|
|
683
742
|
for await (const entry of iterateStream(zip)) {
|
|
684
743
|
let sheetNo;
|
|
685
744
|
const normalizedPath = normalizeZipPath(entry.path);
|
|
@@ -727,19 +786,25 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
727
786
|
default:
|
|
728
787
|
break;
|
|
729
788
|
}
|
|
730
|
-
|
|
789
|
+
// A part the caller has not asked for is not opened at all: it falls through to `autodrain`. One that is handed
|
|
790
|
+
// out is drained once the caller moves past it — see `handOut`.
|
|
791
|
+
sheetNo = emitWorksheets ? getXlsbWorksheetNo(normalizedPath)?.toString() : undefined;
|
|
731
792
|
if (sheetNo) {
|
|
732
793
|
// The same prerequisite test the XML branch makes, for the same reason: a cell record holds an index into
|
|
733
794
|
// the shared-string table rather than a string, so a sheet that arrives first has to be spooled and come
|
|
734
795
|
// back later. `_storeWaitingWorksheet` is shared.
|
|
735
796
|
if (this._xlsbSharedStrings !== undefined && !!this._xlsbWorkbook) {
|
|
736
|
-
|
|
797
|
+
const part = handOut(iterateStream(entry), `worksheet ${sheetNo}`);
|
|
798
|
+
yield* this._parseXlsbWorksheet(part.chunks, sheetNo);
|
|
799
|
+
await releasePart(entry, part, false);
|
|
737
800
|
continue;
|
|
738
801
|
}
|
|
739
802
|
yield { eventType: "waiting-worksheet", sheetNo, entry };
|
|
740
803
|
continue;
|
|
741
804
|
}
|
|
742
|
-
sheetNo =
|
|
805
|
+
sheetNo = emitWorksheets
|
|
806
|
+
? getWorksheetNoFromWorksheetPath(normalizedPath)?.toString()
|
|
807
|
+
: undefined;
|
|
743
808
|
if (sheetNo) {
|
|
744
809
|
// Performance: only wait for sharedStrings when they are actually needed.
|
|
745
810
|
// Also require workbook.xml to be parsed so worksheet name, id, and state
|
|
@@ -748,7 +813,9 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
748
813
|
!!this.model &&
|
|
749
814
|
(this.options.sharedStrings !== "cache" || !!this.sharedStrings);
|
|
750
815
|
if (hasPrerequisites) {
|
|
751
|
-
|
|
816
|
+
const part = handOut(iterateStream(entry), `worksheet ${sheetNo}`);
|
|
817
|
+
yield* this._parseWorksheet(part.chunks, sheetNo);
|
|
818
|
+
await releasePart(entry, part, false);
|
|
752
819
|
continue;
|
|
753
820
|
}
|
|
754
821
|
else {
|
|
@@ -757,8 +824,12 @@ export class WorkbookReaderBase extends EventEmitter {
|
|
|
757
824
|
}
|
|
758
825
|
}
|
|
759
826
|
sheetNo = getWorksheetNoFromWorksheetRelsPath(normalizedPath)?.toString();
|
|
760
|
-
if (sheetNo
|
|
761
|
-
|
|
827
|
+
if (sheetNo &&
|
|
828
|
+
(this.options.hyperlinks === "cache" || this.options.hyperlinks === "emit")) {
|
|
829
|
+
const source = boundedPart(entry, this._maxEntrySize);
|
|
830
|
+
const part = handOut(iterateStream(source), `the hyperlinks of sheet ${sheetNo}`);
|
|
831
|
+
yield* this._parseHyperlinks(part.chunks, sheetNo);
|
|
832
|
+
await releasePart(entry, part, true);
|
|
762
833
|
continue;
|
|
763
834
|
}
|
|
764
835
|
break;
|
|
@@ -805,14 +876,15 @@ class WorkbookReader extends WorkbookReaderBase {
|
|
|
805
876
|
}
|
|
806
877
|
async *_processWaitingWorksheets(waitingWorksheets) {
|
|
807
878
|
for (const ws of waitingWorksheets) {
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
})();
|
|
879
|
+
const part = handOut((async function* () {
|
|
880
|
+
yield* ws.data;
|
|
881
|
+
})(), `worksheet ${ws.sheetNo}`);
|
|
813
882
|
yield* ws.isXlsb
|
|
814
|
-
? this._parseXlsbWorksheet(
|
|
815
|
-
: this._parseWorksheet(
|
|
883
|
+
? this._parseXlsbWorksheet(part.chunks, ws.sheetNo)
|
|
884
|
+
: this._parseWorksheet(part.chunks, ws.sheetNo);
|
|
885
|
+
// Released as the Node variant's spool file is, so a sheet read out of order fails the same way on both.
|
|
886
|
+
part.finish();
|
|
887
|
+
ws.data.length = 0;
|
|
816
888
|
}
|
|
817
889
|
}
|
|
818
890
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { HyperlinkReader } from "./hyperlink-reader.js";
|
|
8
|
-
import { WorkbookReaderBase, WorkbookReaderOptionsSchema } from "./workbook-reader.browser.js";
|
|
8
|
+
import { WorkbookReaderBase, WorkbookReaderOptionsSchema, handOut } from "./workbook-reader.browser.js";
|
|
9
9
|
import { WorksheetReader } from "./worksheet-reader.js";
|
|
10
10
|
import { iterateStream } from "../utils/iterate-stream.js";
|
|
11
11
|
import { createReadStream, createWriteStream, createTempDirSync, remove } from "#platform/utils/fs";
|
|
@@ -71,12 +71,15 @@ class WorkbookReader extends WorkbookReaderBase {
|
|
|
71
71
|
for (const ws of waitingWorksheets) {
|
|
72
72
|
await ws.writePromise;
|
|
73
73
|
const fileStream = createReadStream(ws.path);
|
|
74
|
+
const part = handOut(iterateStream(fileStream), `worksheet ${ws.sheetNo}`);
|
|
74
75
|
try {
|
|
75
76
|
yield* ws.isXlsb
|
|
76
|
-
? this._parseXlsbWorksheet(
|
|
77
|
-
: this._parseWorksheet(
|
|
77
|
+
? this._parseXlsbWorksheet(part.chunks, ws.sheetNo)
|
|
78
|
+
: this._parseWorksheet(part.chunks, ws.sheetNo);
|
|
78
79
|
}
|
|
79
80
|
finally {
|
|
81
|
+
// The file goes away now, so a reader not yet read must say so if it is read later rather than wait forever.
|
|
82
|
+
part.finish();
|
|
80
83
|
fileStream.close();
|
|
81
84
|
ws.cleanup();
|
|
82
85
|
}
|
|
@@ -37,26 +37,50 @@ import { recordSpec } from "../xlsb/spec/records.js";
|
|
|
37
37
|
* what is consumed. The carry buffer grows only to the size of one record.
|
|
38
38
|
*/
|
|
39
39
|
export async function* streamBiffRecords(chunks, part) {
|
|
40
|
-
|
|
40
|
+
for await (const batch of streamBiffRecordBatches(chunks, part)) {
|
|
41
|
+
yield* batch;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* {@link streamBiffRecords}, one array per input chunk instead of one step per record.
|
|
46
|
+
*
|
|
47
|
+
* A worksheet is millions of records — usually one per cell — and an async generator step is a promise round trip,
|
|
48
|
+
* which is what made those steps the largest single cost of a streamed XLSB read. A chunk's records are framed in one
|
|
49
|
+
* synchronous pass and handed over together; the caller still drives the stream chunk by chunk.
|
|
50
|
+
*
|
|
51
|
+
* Each record's payload is a view of a buffer this function owns and never writes to again, so a record stays valid
|
|
52
|
+
* after the next chunk arrives, however the source treats its own buffers.
|
|
53
|
+
*/
|
|
54
|
+
export async function* streamBiffRecordBatches(chunks, part) {
|
|
55
|
+
let carry = EMPTY;
|
|
41
56
|
for await (const chunk of chunks) {
|
|
42
|
-
|
|
57
|
+
if (chunk.length === 0) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Copied, because a source may reuse its chunk buffer and a record handed out must stay valid; joined to the
|
|
61
|
+
// unfinished tail in the same copy when there is one.
|
|
62
|
+
const buffer = concat(carry, chunk);
|
|
63
|
+
const records = [];
|
|
43
64
|
let consumed = 0;
|
|
44
65
|
for (;;) {
|
|
45
|
-
const framed = frame(
|
|
66
|
+
const framed = frame(buffer, consumed, part);
|
|
46
67
|
if (framed === undefined) {
|
|
47
68
|
break;
|
|
48
69
|
}
|
|
49
70
|
consumed = framed.next;
|
|
50
|
-
|
|
71
|
+
records.push(framed.record);
|
|
72
|
+
}
|
|
73
|
+
// Only the unconsumed tail is kept, copied, so a sheet does not pin every buffer it was delivered in.
|
|
74
|
+
carry = consumed === buffer.length ? EMPTY : buffer.slice(consumed);
|
|
75
|
+
if (records.length > 0) {
|
|
76
|
+
yield records;
|
|
51
77
|
}
|
|
52
|
-
// Only the unconsumed tail is kept. Copying it rather than holding a view of the whole chunk is what keeps a
|
|
53
|
-
// sheet from pinning every buffer it was delivered in.
|
|
54
|
-
carry = consumed === 0 ? carry : carry.slice(consumed);
|
|
55
78
|
}
|
|
56
79
|
if (carry.length > 0) {
|
|
57
80
|
throw new XlsbParseError(part, `${carry.length} trailing byte(s) do not form a record`);
|
|
58
81
|
}
|
|
59
82
|
}
|
|
83
|
+
const EMPTY = new Uint8Array(0);
|
|
60
84
|
/**
|
|
61
85
|
* The record starting at `offset`, or `undefined` when the buffer does not hold all of it yet.
|
|
62
86
|
*
|
|
@@ -115,11 +139,8 @@ function varUInt(buffer, start, maxBytes) {
|
|
|
115
139
|
}
|
|
116
140
|
return { value, next: offset };
|
|
117
141
|
}
|
|
118
|
-
/** `left` followed by `right`,
|
|
142
|
+
/** `left` followed by `right`, in a new buffer. */
|
|
119
143
|
function concat(left, right) {
|
|
120
|
-
if (left.length === 0) {
|
|
121
|
-
return right;
|
|
122
|
-
}
|
|
123
144
|
const joined = new Uint8Array(left.length + right.length);
|
|
124
145
|
joined.set(left, 0);
|
|
125
146
|
joined.set(right, left.length);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rowCreate } from "../core/row.js";
|
|
2
2
|
import { rowSetModel } from "../core/worksheet-core.js";
|
|
3
|
-
import {
|
|
3
|
+
import { streamBiffRecordBatches } from "./xlsb-record-stream.js";
|
|
4
4
|
import { encodeCol } from "../utils/address.js";
|
|
5
5
|
import { errorTextOf } from "../xlsb/error-values.js";
|
|
6
6
|
import { BinaryReader } from "../../../utils/binary.js";
|
|
@@ -28,38 +28,42 @@ export async function* streamXlsbRows(chunks, part, context) {
|
|
|
28
28
|
cells = [];
|
|
29
29
|
return row;
|
|
30
30
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// Records arrive a chunk at a time; rows are still yielded one by one, so the caller sees the same sequence.
|
|
32
|
+
for await (const batch of streamBiffRecordBatches(chunks, part)) {
|
|
33
|
+
for (let index = 0; index < batch.length; index++) {
|
|
34
|
+
const record = batch[index];
|
|
35
|
+
switch (record.name) {
|
|
36
|
+
case "BrtBeginSheetData":
|
|
37
|
+
inSheetData = true;
|
|
38
|
+
continue;
|
|
39
|
+
case "BrtEndSheetData": {
|
|
40
|
+
// Everything after this is sheet-level: merges, conditional formats, page setup. A forward reader has already
|
|
41
|
+
// emitted its rows, so there is nothing left to attach them to — see the note at the top of this file.
|
|
42
|
+
const last = flush();
|
|
43
|
+
if (last !== undefined) {
|
|
44
|
+
yield last;
|
|
45
|
+
}
|
|
46
|
+
inSheetData = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
default:
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (!inSheetData) {
|
|
35
53
|
continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
yield last;
|
|
54
|
+
}
|
|
55
|
+
if (record.name === "BrtRowHdr") {
|
|
56
|
+
const previous = flush();
|
|
57
|
+
if (previous !== undefined) {
|
|
58
|
+
yield previous;
|
|
42
59
|
}
|
|
43
|
-
|
|
60
|
+
currentRow = new BinaryReader(record.payload, 0, part).readUint32();
|
|
44
61
|
continue;
|
|
45
62
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (!inSheetData) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (record.name === "BrtRowHdr") {
|
|
53
|
-
const previous = flush();
|
|
54
|
-
if (previous !== undefined) {
|
|
55
|
-
yield previous;
|
|
63
|
+
const cell = decodeCell(record, context, part);
|
|
64
|
+
if (cell !== undefined) {
|
|
65
|
+
cells.push(cell);
|
|
56
66
|
}
|
|
57
|
-
currentRow = new BinaryReader(record.payload, 0, part).readUint32();
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const cell = decodeCell(record, context, part);
|
|
61
|
-
if (cell !== undefined) {
|
|
62
|
-
cells.push(cell);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
const trailing = flush();
|
|
@@ -2,19 +2,33 @@
|
|
|
2
2
|
const BORDER_EDGES = ["top", "left", "bottom", "right", "diagonal"];
|
|
3
3
|
/** The facets of a style that own mutable sub-objects and therefore need copying. */
|
|
4
4
|
const COPIED_FACETS = ["font", "alignment", "protection", "border", "fill"];
|
|
5
|
+
/**
|
|
6
|
+
* Copy `obj` and each of its `nestKeys` sub-objects one level down.
|
|
7
|
+
*
|
|
8
|
+
* Written as one spread and direct assignments: this runs for every styled cell a streamed sheet yields, and the
|
|
9
|
+
* `reduce` into a second object that was then spread again built three objects per call to say what one does. The
|
|
10
|
+
* result is the same — every nested key already exists on the copy, so assigning it keeps its position.
|
|
11
|
+
*/
|
|
5
12
|
function oneDepthCopy(obj, nestKeys) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
13
|
+
const copied = { ...obj };
|
|
14
|
+
for (let i = 0; i < nestKeys.length; i++) {
|
|
15
|
+
const key = nestKeys[i];
|
|
16
|
+
if (obj[key]) {
|
|
17
|
+
copied[key] = { ...obj[key] };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return copied;
|
|
15
21
|
}
|
|
22
|
+
const COLOR_KEYS = ["color"];
|
|
23
|
+
const FILL_KEYS = ["fgColor", "bgColor", "center"];
|
|
24
|
+
/** Whether `obj` has no own enumerable key — without allocating the key array `Object.keys` would. */
|
|
16
25
|
function isEmptyObj(obj) {
|
|
17
|
-
|
|
26
|
+
for (const key in obj) {
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
18
32
|
}
|
|
19
33
|
/**
|
|
20
34
|
* Copy one style facet (`font`, `border`, `fill`, `alignment`, `protection`) deeply
|
|
@@ -44,7 +58,7 @@ function copyStyleFacet(key, value) {
|
|
|
44
58
|
const facet = value;
|
|
45
59
|
switch (key) {
|
|
46
60
|
case "font":
|
|
47
|
-
return oneDepthCopy(facet,
|
|
61
|
+
return oneDepthCopy(facet, COLOR_KEYS);
|
|
48
62
|
case "alignment":
|
|
49
63
|
case "protection":
|
|
50
64
|
return { ...facet };
|
|
@@ -61,15 +75,15 @@ function copyBorder(border) {
|
|
|
61
75
|
const copied = { ...border };
|
|
62
76
|
for (const edge of BORDER_EDGES) {
|
|
63
77
|
if (border[edge]) {
|
|
64
|
-
copied[edge] = oneDepthCopy(border[edge],
|
|
78
|
+
copied[edge] = oneDepthCopy(border[edge], COLOR_KEYS);
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
return copied;
|
|
68
82
|
}
|
|
69
83
|
function copyFill(fill) {
|
|
70
|
-
const copied = oneDepthCopy(fill,
|
|
84
|
+
const copied = oneDepthCopy(fill, FILL_KEYS);
|
|
71
85
|
if (fill.stops) {
|
|
72
|
-
copied.stops = fill.stops.map((s) => oneDepthCopy(s,
|
|
86
|
+
copied.stops = fill.stops.map((s) => oneDepthCopy(s, COLOR_KEYS));
|
|
73
87
|
}
|
|
74
88
|
return copied;
|
|
75
89
|
}
|