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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/dist/esm/modules/archive/compression/deflate-fallback.js +20 -50
  4. package/dist/esm/modules/archive/compression/streaming-compress.js +3 -2
  5. package/dist/esm/modules/archive/unzip/stream.js +1 -1
  6. package/dist/esm/modules/csv/stream/parser.js +5 -1
  7. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +86 -14
  8. package/dist/esm/modules/excel/stream/workbook-reader.js +6 -3
  9. package/dist/esm/modules/excel/stream/xlsb-record-stream.js +32 -11
  10. package/dist/esm/modules/excel/stream/xlsb-worksheet-reader.js +32 -28
  11. package/dist/esm/modules/excel/utils/copy-style.js +28 -14
  12. package/dist/esm/modules/excel/utils/stream-buf.js +74 -2
  13. package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +13 -0
  14. package/dist/esm/modules/word/writer/streaming-writer.js +38 -6
  15. package/dist/esm/modules/xml/encode.js +15 -0
  16. package/dist/iife/documonster.archive.iife.min.js +2 -2
  17. package/dist/iife/documonster.csv.iife.min.js +3 -2
  18. package/dist/iife/documonster.draw.iife.min.js +1 -1
  19. package/dist/iife/documonster.excel.iife.min.js +53 -53
  20. package/dist/iife/documonster.formula.iife.min.js +1 -1
  21. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  22. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  23. package/dist/iife/documonster.pdf.iife.min.js +11 -11
  24. package/dist/iife/documonster.stream.iife.min.js +1 -1
  25. package/dist/iife/documonster.word.iife.min.js +60 -60
  26. package/dist/iife/documonster.xml.iife.min.js +7 -7
  27. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +26 -0
  28. package/dist/types/modules/excel/stream/xlsb-record-stream.d.ts +11 -0
  29. package/dist/types/modules/excel/utils/stream-buf.d.ts +19 -0
  30. package/dist/types/modules/word/writer/streaming-writer.d.ts +13 -0
  31. 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.0/dist/iife/documonster.excel.iife.min.js"></script>
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.0/dist/iife/documonster.excel.iife.min.js"></script>
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 using the zlib bl_count redistribution algorithm ---
690
- // Count code lengths at each bit depth
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
- if (codeLens[i] > maxBits) {
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
- // Check Kraft inequality: sum of 2^(maxBits - len) must equal 2^maxBits
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; // Already valid
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 (bits > 0 && blCount[bits] === 0) {
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[maxBits]++;
729
- // Kraft change: removed 2^(maxBits-bits), added 2^0 = 1
730
- kraft -= (1 << (maxBits - bits)) - 1;
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
- symbolsByLen.sort((a, b) => b.origLen - a.origLen || a.freq - b.freq);
749
- // Assign new lengths from the bl_count distribution
750
- codeLens.fill(0);
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
- if (symIdx < symbolsByLen.length) {
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 = Buffer.alloc(0);
156
+ input = new Uint8Array(0);
157
157
  }
158
158
  else if (this._pending.length === 1) {
159
- input = Buffer.from(this._pending[0]);
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 && !this.parseConfig.fastMode) {
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
- sheetNo = getXlsbWorksheetNo(normalizedPath)?.toString();
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
- yield* this._parseXlsbWorksheet(iterateStream(entry), sheetNo);
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 = getWorksheetNoFromWorksheetPath(normalizedPath)?.toString();
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
- yield* this._parseWorksheet(iterateStream(entry), sheetNo);
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
- yield* this._parseHyperlinks(iterateStream(boundedPart(entry, this._maxEntrySize)), sheetNo);
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 iterator = (async function* () {
809
- for (const chunk of ws.data) {
810
- yield chunk;
811
- }
812
- })();
879
+ const part = handOut((async function* () {
880
+ yield* ws.data;
881
+ })(), `worksheet ${ws.sheetNo}`);
813
882
  yield* ws.isXlsb
814
- ? this._parseXlsbWorksheet(iterator, ws.sheetNo)
815
- : this._parseWorksheet(iterator, ws.sheetNo);
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(iterateStream(fileStream), ws.sheetNo)
77
- : this._parseWorksheet(iterateStream(fileStream), ws.sheetNo);
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
- let carry = new Uint8Array(0);
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
- carry = concat(carry, Uint8Array.from(chunk));
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(carry, consumed, part);
66
+ const framed = frame(buffer, consumed, part);
46
67
  if (framed === undefined) {
47
68
  break;
48
69
  }
49
70
  consumed = framed.next;
50
- yield framed.record;
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`, avoiding a copy when `left` is empty — the common case after a clean chunk. */
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 { streamBiffRecords } from "./xlsb-record-stream.js";
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
- for await (const record of streamBiffRecords(chunks, part)) {
32
- switch (record.name) {
33
- case "BrtBeginSheetData":
34
- inSheetData = true;
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
- case "BrtEndSheetData": {
37
- // Everything after this is sheet-level: merges, conditional formats, page setup. A forward reader has already
38
- // emitted its rows, so there is nothing left to attach them to — see the note at the top of this file.
39
- const last = flush();
40
- if (last !== undefined) {
41
- yield last;
54
+ }
55
+ if (record.name === "BrtRowHdr") {
56
+ const previous = flush();
57
+ if (previous !== undefined) {
58
+ yield previous;
42
59
  }
43
- inSheetData = false;
60
+ currentRow = new BinaryReader(record.payload, 0, part).readUint32();
44
61
  continue;
45
62
  }
46
- default:
47
- break;
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
- return {
7
- ...obj,
8
- ...nestKeys.reduce((memo, key) => {
9
- if (obj[key]) {
10
- memo[key] = { ...obj[key] };
11
- }
12
- return memo;
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
- return Object.keys(obj).length === 0;
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, ["color"]);
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], ["color"]);
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, ["fgColor", "bgColor", "center"]);
84
+ const copied = oneDepthCopy(fill, FILL_KEYS);
71
85
  if (fill.stops) {
72
- copied.stops = fill.stops.map((s) => oneDepthCopy(s, ["color"]));
86
+ copied.stops = fill.stops.map((s) => oneDepthCopy(s, COLOR_KEYS));
73
87
  }
74
88
  return copied;
75
89
  }