@cj-tech-master/excelts 7.6.0 → 8.0.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.
Files changed (79) hide show
  1. package/README.md +99 -577
  2. package/README_zh.md +101 -577
  3. package/dist/browser/index.browser.d.ts +3 -0
  4. package/dist/browser/index.browser.js +2 -0
  5. package/dist/browser/index.d.ts +3 -0
  6. package/dist/browser/index.js +2 -0
  7. package/dist/browser/modules/archive/compression/compress.browser.js +4 -4
  8. package/dist/browser/modules/archive/compression/deflate-fallback.d.ts +24 -22
  9. package/dist/browser/modules/archive/compression/deflate-fallback.js +664 -360
  10. package/dist/browser/modules/archive/compression/streaming-compress.browser.d.ts +7 -0
  11. package/dist/browser/modules/archive/compression/streaming-compress.browser.js +15 -3
  12. package/dist/browser/modules/archive/compression/streaming-compress.d.ts +5 -0
  13. package/dist/browser/modules/archive/compression/streaming-compress.js +7 -0
  14. package/dist/browser/modules/archive/zip/stream.js +27 -3
  15. package/dist/browser/modules/excel/workbook.browser.d.ts +72 -0
  16. package/dist/browser/modules/excel/workbook.browser.js +226 -0
  17. package/dist/browser/modules/excel/workbook.d.ts +32 -1
  18. package/dist/browser/modules/excel/workbook.js +47 -2
  19. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +42 -4
  20. package/dist/browser/modules/markdown/constants.d.ts +30 -0
  21. package/dist/browser/modules/markdown/constants.js +30 -0
  22. package/dist/browser/modules/markdown/errors.d.ts +21 -0
  23. package/dist/browser/modules/markdown/errors.js +23 -0
  24. package/dist/browser/modules/markdown/format/index.d.ts +54 -0
  25. package/dist/browser/modules/markdown/format/index.js +307 -0
  26. package/dist/browser/modules/markdown/index.d.ts +15 -0
  27. package/dist/browser/modules/markdown/index.js +22 -0
  28. package/dist/browser/modules/markdown/parse/index.d.ts +70 -0
  29. package/dist/browser/modules/markdown/parse/index.js +428 -0
  30. package/dist/browser/modules/markdown/types.d.ts +130 -0
  31. package/dist/browser/modules/markdown/types.js +6 -0
  32. package/dist/cjs/index.js +5 -1
  33. package/dist/cjs/modules/archive/compression/compress.browser.js +4 -4
  34. package/dist/cjs/modules/archive/compression/deflate-fallback.js +664 -360
  35. package/dist/cjs/modules/archive/compression/streaming-compress.browser.js +15 -2
  36. package/dist/cjs/modules/archive/compression/streaming-compress.js +8 -0
  37. package/dist/cjs/modules/archive/zip/stream.js +26 -2
  38. package/dist/cjs/modules/excel/workbook.browser.js +226 -0
  39. package/dist/cjs/modules/excel/workbook.js +46 -1
  40. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +42 -4
  41. package/dist/cjs/modules/markdown/constants.js +33 -0
  42. package/dist/cjs/modules/markdown/errors.js +28 -0
  43. package/dist/cjs/modules/markdown/format/index.js +310 -0
  44. package/dist/cjs/modules/markdown/index.js +30 -0
  45. package/dist/cjs/modules/markdown/parse/index.js +432 -0
  46. package/dist/cjs/modules/markdown/types.js +7 -0
  47. package/dist/esm/index.browser.js +2 -0
  48. package/dist/esm/index.js +2 -0
  49. package/dist/esm/modules/archive/compression/compress.browser.js +4 -4
  50. package/dist/esm/modules/archive/compression/deflate-fallback.js +664 -360
  51. package/dist/esm/modules/archive/compression/streaming-compress.browser.js +15 -3
  52. package/dist/esm/modules/archive/compression/streaming-compress.js +7 -0
  53. package/dist/esm/modules/archive/zip/stream.js +27 -3
  54. package/dist/esm/modules/excel/workbook.browser.js +226 -0
  55. package/dist/esm/modules/excel/workbook.js +47 -2
  56. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +42 -4
  57. package/dist/esm/modules/markdown/constants.js +30 -0
  58. package/dist/esm/modules/markdown/errors.js +23 -0
  59. package/dist/esm/modules/markdown/format/index.js +307 -0
  60. package/dist/esm/modules/markdown/index.js +22 -0
  61. package/dist/esm/modules/markdown/parse/index.js +428 -0
  62. package/dist/esm/modules/markdown/types.js +6 -0
  63. package/dist/iife/excelts.iife.js +1342 -283
  64. package/dist/iife/excelts.iife.js.map +1 -1
  65. package/dist/iife/excelts.iife.min.js +38 -34
  66. package/dist/types/index.browser.d.ts +3 -0
  67. package/dist/types/index.d.ts +3 -0
  68. package/dist/types/modules/archive/compression/deflate-fallback.d.ts +24 -22
  69. package/dist/types/modules/archive/compression/streaming-compress.browser.d.ts +7 -0
  70. package/dist/types/modules/archive/compression/streaming-compress.d.ts +5 -0
  71. package/dist/types/modules/excel/workbook.browser.d.ts +72 -0
  72. package/dist/types/modules/excel/workbook.d.ts +32 -1
  73. package/dist/types/modules/markdown/constants.d.ts +30 -0
  74. package/dist/types/modules/markdown/errors.d.ts +21 -0
  75. package/dist/types/modules/markdown/format/index.d.ts +54 -0
  76. package/dist/types/modules/markdown/index.d.ts +15 -0
  77. package/dist/types/modules/markdown/parse/index.d.ts +70 -0
  78. package/dist/types/modules/markdown/types.d.ts +130 -0
  79. package/package.json +56 -32
@@ -44,3 +44,10 @@ export declare function createUnzlibStream(options?: StreamCompressOptions): Unz
44
44
  * bit-stream state across `write()` calls.
45
45
  */
46
46
  export { PureJsSyncDeflater as SyncDeflater };
47
+ /**
48
+ * Returns true when the browser supports native `CompressionStream("deflate-raw")`,
49
+ * signalling that `push()` should prefer the async path over `SyncDeflater`.
50
+ *
51
+ * Only checks for compression support — decompression is not needed for writing.
52
+ */
53
+ export declare function hasNativeAsyncDeflate(): boolean;
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { EventEmitter } from "../../../utils/event-emitter.js";
13
13
  import { deflateRawCompressed, inflateRaw, SyncDeflater as PureJsSyncDeflater } from "./deflate-fallback.js";
14
- import { hasDeflateRawWebStreams, hasGzipCompressionStream, hasGzipDecompressionStream, hasDeflateCompressionStream, hasDeflateDecompressionStream } from "./compress.base.js";
14
+ import { hasDeflateRawWebStreams, hasDeflateRawCompressionStream, hasGzipCompressionStream, hasGzipDecompressionStream, hasDeflateCompressionStream, hasDeflateDecompressionStream } from "./compress.base.js";
15
15
  import { concatUint8Arrays } from "../../../utils/binary.js";
16
16
  import { DEFAULT_COMPRESS_LEVEL } from "../shared/defaults.js";
17
17
  import { hasWorkerSupport, getDefaultWorkerPool } from "./worker-pool/index.browser.js";
@@ -253,10 +253,13 @@ function createStreamCodec(type, options) {
253
253
  if (options.useWorker && hasWorkerSupport()) {
254
254
  return createWorkerStreamCodec(type, options.workerPool, level, options.allowTransfer);
255
255
  }
256
- if (hasDeflateRawWebStreams()) {
256
+ // Use native CompressionStream/DecompressionStream when the required
257
+ // direction is available. Compression only needs CompressionStream;
258
+ // decompression only needs DecompressionStream.
259
+ if (type === "deflate" ? hasDeflateRawCompressionStream() : hasDeflateRawWebStreams()) {
257
260
  return createNativeWebStreamCodec("deflate-raw", type === "deflate");
258
261
  }
259
- return new BufferedCodec(type === "deflate" ? deflateRawCompressed : inflateRaw);
262
+ return new BufferedCodec(type === "deflate" ? data => deflateRawCompressed(data, level) : inflateRaw);
260
263
  }
261
264
  // =============================================================================
262
265
  // Public API
@@ -317,3 +320,12 @@ export function createUnzlibStream(options = {}) {
317
320
  * bit-stream state across `write()` calls.
318
321
  */
319
322
  export { PureJsSyncDeflater as SyncDeflater };
323
+ /**
324
+ * Returns true when the browser supports native `CompressionStream("deflate-raw")`,
325
+ * signalling that `push()` should prefer the async path over `SyncDeflater`.
326
+ *
327
+ * Only checks for compression support — decompression is not needed for writing.
328
+ */
329
+ export function hasNativeAsyncDeflate() {
330
+ return hasDeflateRawCompressionStream();
331
+ }
@@ -75,3 +75,8 @@ export declare class SyncDeflater implements SyncDeflaterLike {
75
75
  finish(): Uint8Array;
76
76
  private _flushBatch;
77
77
  }
78
+ /**
79
+ * On Node.js, `zlib.deflateRawSync` is native and fast — no need to detour
80
+ * through the async streaming path. Always returns false.
81
+ */
82
+ export declare function hasNativeAsyncDeflate(): boolean;
@@ -166,3 +166,10 @@ export class SyncDeflater {
166
166
  return new Uint8Array(result);
167
167
  }
168
168
  }
169
+ /**
170
+ * On Node.js, `zlib.deflateRawSync` is native and fast — no need to detour
171
+ * through the async streaming path. Always returns false.
172
+ */
173
+ export function hasNativeAsyncDeflate() {
174
+ return false;
175
+ }
@@ -6,7 +6,7 @@
6
6
  * - In browser builds the bundler aliases those imports to their browser variants.
7
7
  */
8
8
  import { crc32Update, crc32Finalize, ensureZlibSync } from "../compression/crc32.browser.js";
9
- import { createDeflateStream, SyncDeflater } from "../compression/streaming-compress.browser.js";
9
+ import { createDeflateStream, SyncDeflater, hasNativeAsyncDeflate } from "../compression/streaming-compress.browser.js";
10
10
  import { zipCryptoInitKeys, zipCryptoCreateHeader, zipCryptoEncryptByte, aesEncrypt, buildAesExtraField, randomBytes, isAesEncryption, getAesKeyStrength } from "../crypto/index.js";
11
11
  import { DEFAULT_ZIP_LEVEL, DEFAULT_ZIP_TIMESTAMPS } from "../shared/defaults.js";
12
12
  import { EMPTY_UINT8ARRAY } from "../shared/bytes.js";
@@ -571,6 +571,12 @@ export class ZipDeflateFile {
571
571
  this._tapCallback(promise, callback);
572
572
  return promise;
573
573
  }
574
+ // If a previous async operation already failed, don't do more work.
575
+ if (this._completeError) {
576
+ const promise = Promise.reject(this._completeError);
577
+ this._tapCallback(promise, callback);
578
+ return promise;
579
+ }
574
580
  if (this._deflateWanted === null) {
575
581
  this._accumulateSampleLen(data);
576
582
  if (!this._shouldDecide(final)) {
@@ -613,7 +619,16 @@ export class ZipDeflateFile {
613
619
  * memory growth when callers push data in a tight synchronous loop.
614
620
  */
615
621
  push(data, final = false, callback) {
616
- if (!this._deflate && this._encryptionMethod === "none") {
622
+ // Use the synchronous path only when:
623
+ // 1. No async deflate stream is already active
624
+ // 2. No encryption (which requires async crypto)
625
+ // 3. No native async deflate available (browser CompressionStream)
626
+ //
627
+ // When a native async CompressionStream("deflate-raw") is available
628
+ // (modern browsers), prefer the async path — it produces better
629
+ // compression (Dynamic Huffman via the browser's native engine) and
630
+ // doesn't block the main thread.
631
+ if (!this._deflate && this._encryptionMethod === "none" && !hasNativeAsyncDeflate()) {
617
632
  try {
618
633
  this._pushSyncPath(data, final);
619
634
  callback?.();
@@ -624,7 +639,10 @@ export class ZipDeflateFile {
624
639
  }
625
640
  return Promise.resolve();
626
641
  }
627
- const promise = (this._pushChain = this._pushChain.then(() => this._pushUnchained(data, final, callback)));
642
+ // Chain the async push so calls are serialized. Use a recovery wrapper
643
+ // so that a single failed push does not break the chain for subsequent
644
+ // pushes — errors are surfaced via onerror/rejectComplete instead.
645
+ const promise = (this._pushChain = this._pushChain.then(() => this._pushUnchained(data, final, callback), () => this._pushUnchained(data, final, callback)));
628
646
  // Prevent unhandled rejection when callers intentionally ignore the Promise.
629
647
  promise.catch(() => { });
630
648
  return promise;
@@ -695,6 +713,12 @@ export class ZipDeflateFile {
695
713
  this._syncDeflater = null;
696
714
  }
697
715
  this._emitDataDescriptor();
716
+ // _emitDataDescriptor may call _rejectComplete instead of throwing
717
+ // (e.g. ZIP64 required but zip64=false). In the sync path, surface
718
+ // this as a throw so push() can reject properly.
719
+ if (this._completeError) {
720
+ throw this._completeError;
721
+ }
698
722
  }
699
723
  /**
700
724
  * Emit local file header with Data Descriptor flag
@@ -16,6 +16,7 @@ import { WorkbookWriter, type WorkbookWriterOptions } from "./stream/workbook-wr
16
16
  import { WorkbookReader, type WorkbookReaderOptions } from "./stream/workbook-reader.js";
17
17
  import { type DateFormat } from "../../utils/datetime.js";
18
18
  import type { CsvParseOptions, CsvFormatOptions } from "../csv/types.js";
19
+ import type { MarkdownOptions } from "../markdown/types.js";
19
20
  import type { Readable } from "../stream/index.js";
20
21
  import type { IReadable, IWritable } from "../stream/types.js";
21
22
  import type { PivotTable } from "./pivot-table.js";
@@ -253,6 +254,77 @@ declare class Workbook {
253
254
  private _readCsvFile;
254
255
  private _readCsvBlob;
255
256
  private _writeCsvString;
257
+ /**
258
+ * Populate a worksheet from a parsed Markdown table result.
259
+ * Shared by readMarkdown and readMarkdownAll.
260
+ */
261
+ private _populateMarkdownWorksheet;
262
+ /**
263
+ * Read a Markdown table and add as worksheet.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * // From a Markdown string
268
+ * workbook.readMarkdown("| Name | Age |\n| --- | --- |\n| Alice | 30 |");
269
+ *
270
+ * // With options
271
+ * workbook.readMarkdown(markdownString, { sheetName: "Data", map: (v, col) => Number(v) || v });
272
+ * ```
273
+ */
274
+ readMarkdown(input: string, options?: MarkdownOptions): Worksheet;
275
+ /**
276
+ * Read all Markdown tables from a document, each becoming a separate worksheet.
277
+ *
278
+ * @param input - Markdown string containing one or more tables
279
+ * @param options - Parse options (sheetName is used as prefix: "sheetName", "sheetName_2", ...)
280
+ * @returns Array of created worksheets (empty if no tables found)
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * // Parse a document with multiple tables
285
+ * const sheets = workbook.readMarkdownAll(markdownDoc);
286
+ * console.log(`Created ${sheets.length} worksheets`);
287
+ *
288
+ * // With a naming prefix
289
+ * const sheets = workbook.readMarkdownAll(markdownDoc, { sheetName: "Table" });
290
+ * // Creates "Table", "Table_2", "Table_3", ...
291
+ * ```
292
+ */
293
+ readMarkdownAll(input: string, options?: MarkdownOptions): Worksheet[];
294
+ /**
295
+ * Write worksheet as a Markdown table string.
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * // Write first worksheet
300
+ * const markdownText = workbook.writeMarkdown();
301
+ *
302
+ * // Write specific worksheet with options
303
+ * const markdownText = workbook.writeMarkdown({ sheetName: "Data", padding: true });
304
+ * ```
305
+ */
306
+ writeMarkdown(options?: MarkdownOptions): string;
307
+ /**
308
+ * Write worksheet to Markdown buffer (Uint8Array).
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * const buffer = workbook.writeMarkdownBuffer();
313
+ * ```
314
+ */
315
+ writeMarkdownBuffer(options?: MarkdownOptions): Uint8Array;
316
+ /**
317
+ * Read Markdown from file (Node.js only - throws in browser)
318
+ */
319
+ readMarkdownFile(_filename: string, _options?: MarkdownOptions): Promise<Worksheet>;
320
+ /**
321
+ * Read all Markdown tables from file (Node.js only - throws in browser)
322
+ */
323
+ readMarkdownAllFile(_filename: string, _options?: MarkdownOptions): Promise<Worksheet[]>;
324
+ /**
325
+ * Write Markdown to file (Node.js only - throws in browser)
326
+ */
327
+ writeMarkdownFile(_filename: string, _options?: MarkdownOptions): Promise<void>;
256
328
  /**
257
329
  * Create a streaming workbook writer for large files.
258
330
  * This is more memory-efficient than using Workbook for large datasets.
@@ -19,6 +19,8 @@ import { parseCsv } from "../csv/parse/index.js";
19
19
  import { formatCsv } from "../csv/format/index.js";
20
20
  import { CsvParserStream, CsvFormatterStream } from "../csv/stream/index.js";
21
21
  import { parseNumberFromCsv } from "../csv/utils/number.js";
22
+ import { parseMarkdown, parseMarkdownAll } from "../markdown/parse/index.js";
23
+ import { formatMarkdown } from "../markdown/format/index.js";
22
24
  import { ExcelDownloadError, ExcelNotSupportedError } from "./errors.js";
23
25
  import { pipeline } from "../stream/index.browser.js";
24
26
  import { readableStreamToAsyncIterable } from "../stream/utils.base.js";
@@ -113,6 +115,57 @@ function createDefaultWriteMapper(dateFormat, dateUTC) {
113
115
  };
114
116
  }
115
117
  // =============================================================================
118
+ // Markdown Value Mapper (Internal)
119
+ // =============================================================================
120
+ /**
121
+ * Create a stringify function for Markdown output.
122
+ * Handles hyperlinks, formulas, rich text, dates, errors, and objects.
123
+ */
124
+ function createMarkdownStringify(dateFormat, dateUTC) {
125
+ const formatter = dateFormat
126
+ ? DateFormatter.create(dateFormat, { utc: dateUTC })
127
+ : DateFormatter.iso(dateUTC);
128
+ return function stringify(value) {
129
+ if (value === null || value === undefined) {
130
+ return "";
131
+ }
132
+ if (typeof value === "string") {
133
+ return value;
134
+ }
135
+ if (typeof value === "number" || typeof value === "bigint") {
136
+ return String(value);
137
+ }
138
+ if (typeof value === "boolean") {
139
+ return value ? "true" : "false";
140
+ }
141
+ if (value instanceof Date) {
142
+ return formatter.format(value);
143
+ }
144
+ if (typeof value === "object") {
145
+ const v = value;
146
+ if (v.text || v.hyperlink) {
147
+ return v.hyperlink || v.text || "";
148
+ }
149
+ if (v.formula || v.result) {
150
+ return v.result != null ? String(v.result) : "";
151
+ }
152
+ if (v.richText && Array.isArray(v.richText)) {
153
+ return v.richText.map((r) => r.text).join("");
154
+ }
155
+ if (v.error) {
156
+ return v.error;
157
+ }
158
+ try {
159
+ return JSON.stringify(value);
160
+ }
161
+ catch {
162
+ return "[object Object]";
163
+ }
164
+ }
165
+ return String(value);
166
+ };
167
+ }
168
+ // =============================================================================
116
169
  // CSV Input Type Detection (Internal)
117
170
  // =============================================================================
118
171
  function isUrl(input) {
@@ -560,6 +613,179 @@ class Workbook {
560
613
  formatter.end();
561
614
  await pipelinePromise;
562
615
  }
616
+ /**
617
+ * Populate a worksheet from a parsed Markdown table result.
618
+ * Shared by readMarkdown and readMarkdownAll.
619
+ */
620
+ _populateMarkdownWorksheet(worksheet, result, map) {
621
+ worksheet.addRow(result.headers);
622
+ worksheet._markdownAlignments = result.alignments;
623
+ for (const row of result.rows) {
624
+ if (map) {
625
+ worksheet.addRow(row.map((v, i) => map(v, i)));
626
+ }
627
+ else {
628
+ worksheet.addRow(row);
629
+ }
630
+ }
631
+ }
632
+ /**
633
+ * Read a Markdown table and add as worksheet.
634
+ *
635
+ * @example
636
+ * ```ts
637
+ * // From a Markdown string
638
+ * workbook.readMarkdown("| Name | Age |\n| --- | --- |\n| Alice | 30 |");
639
+ *
640
+ * // With options
641
+ * workbook.readMarkdown(markdownString, { sheetName: "Data", map: (v, col) => Number(v) || v });
642
+ * ```
643
+ */
644
+ readMarkdown(input, options) {
645
+ const parseResult = parseMarkdown(input, {
646
+ trim: options?.trim,
647
+ unescape: options?.unescape,
648
+ skipEmptyRows: options?.skipEmptyRows,
649
+ maxRows: options?.maxRows,
650
+ convertBr: options?.convertBr
651
+ });
652
+ const worksheet = this.addWorksheet(options?.sheetName);
653
+ this._populateMarkdownWorksheet(worksheet, parseResult, options?.map);
654
+ return worksheet;
655
+ }
656
+ /**
657
+ * Read all Markdown tables from a document, each becoming a separate worksheet.
658
+ *
659
+ * @param input - Markdown string containing one or more tables
660
+ * @param options - Parse options (sheetName is used as prefix: "sheetName", "sheetName_2", ...)
661
+ * @returns Array of created worksheets (empty if no tables found)
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * // Parse a document with multiple tables
666
+ * const sheets = workbook.readMarkdownAll(markdownDoc);
667
+ * console.log(`Created ${sheets.length} worksheets`);
668
+ *
669
+ * // With a naming prefix
670
+ * const sheets = workbook.readMarkdownAll(markdownDoc, { sheetName: "Table" });
671
+ * // Creates "Table", "Table_2", "Table_3", ...
672
+ * ```
673
+ */
674
+ readMarkdownAll(input, options) {
675
+ const parseResults = parseMarkdownAll(input, {
676
+ trim: options?.trim,
677
+ unescape: options?.unescape,
678
+ skipEmptyRows: options?.skipEmptyRows,
679
+ maxRows: options?.maxRows,
680
+ convertBr: options?.convertBr
681
+ });
682
+ const baseName = options?.sheetName;
683
+ const map = options?.map;
684
+ const worksheets = [];
685
+ for (let t = 0; t < parseResults.length; t++) {
686
+ const name = baseName ? (t === 0 ? baseName : `${baseName}_${t + 1}`) : undefined;
687
+ const worksheet = this.addWorksheet(name);
688
+ this._populateMarkdownWorksheet(worksheet, parseResults[t], map);
689
+ worksheets.push(worksheet);
690
+ }
691
+ return worksheets;
692
+ }
693
+ /**
694
+ * Write worksheet as a Markdown table string.
695
+ *
696
+ * @example
697
+ * ```ts
698
+ * // Write first worksheet
699
+ * const markdownText = workbook.writeMarkdown();
700
+ *
701
+ * // Write specific worksheet with options
702
+ * const markdownText = workbook.writeMarkdown({ sheetName: "Data", padding: true });
703
+ * ```
704
+ */
705
+ writeMarkdown(options) {
706
+ const worksheet = this.getWorksheet(options?.sheetName || options?.sheetId);
707
+ if (!worksheet) {
708
+ return "";
709
+ }
710
+ const dateFormat = options?.dateFormat;
711
+ const dateUTC = options?.dateUTC;
712
+ const includeEmptyRows = options?.includeEmptyRows !== false;
713
+ // Build stringify function
714
+ const stringify = options?.stringify ?? createMarkdownStringify(dateFormat, dateUTC);
715
+ // Collect all rows from worksheet
716
+ const allRows = [];
717
+ let lastRow = 1;
718
+ worksheet.eachRow((row, rowNumber) => {
719
+ if (includeEmptyRows) {
720
+ while (lastRow++ < rowNumber - 1) {
721
+ allRows.push([]);
722
+ }
723
+ }
724
+ // row.values is a 1-indexed sparse array — use Array.from to fill holes
725
+ // with undefined, then slice(1) to remove the leading 1-indexed slot
726
+ const values = Array.from(row.values).slice(1);
727
+ allRows.push(values);
728
+ lastRow = rowNumber;
729
+ });
730
+ if (allRows.length === 0) {
731
+ return "";
732
+ }
733
+ // First row is the header
734
+ const headerRow = allRows[0];
735
+ const headers = headerRow.map(v => stringify(v));
736
+ const dataRows = allRows.slice(1);
737
+ // Check for stored alignments from a previous readMarkdown
738
+ const storedAlignments = worksheet
739
+ ._markdownAlignments;
740
+ // Build column configs
741
+ const columns = options?.columns;
742
+ let resolvedColumns;
743
+ if (!columns && storedAlignments) {
744
+ // Use stored alignments from parsed Markdown
745
+ resolvedColumns = headers.map((h, i) => ({
746
+ header: h,
747
+ alignment: i < storedAlignments.length ? storedAlignments[i] : undefined
748
+ }));
749
+ }
750
+ return formatMarkdown(headers, dataRows, {
751
+ columns: resolvedColumns ?? columns,
752
+ alignment: options?.alignment,
753
+ padding: options?.padding,
754
+ trailingNewline: options?.trailingNewline,
755
+ escapeContent: options?.escapeContent,
756
+ stringify
757
+ });
758
+ }
759
+ /**
760
+ * Write worksheet to Markdown buffer (Uint8Array).
761
+ *
762
+ * @example
763
+ * ```ts
764
+ * const buffer = workbook.writeMarkdownBuffer();
765
+ * ```
766
+ */
767
+ writeMarkdownBuffer(options) {
768
+ const markdownString = this.writeMarkdown(options);
769
+ return new TextEncoder().encode(markdownString);
770
+ }
771
+ /**
772
+ * Read Markdown from file (Node.js only - throws in browser)
773
+ */
774
+ async readMarkdownFile(_filename, _options) {
775
+ throw new ExcelNotSupportedError("readMarkdownFile()", "not available in browser. Use readMarkdown(string) instead.");
776
+ }
777
+ /**
778
+ * Read all Markdown tables from file (Node.js only - throws in browser)
779
+ */
780
+ async readMarkdownAllFile(_filename, _options) {
781
+ throw new ExcelNotSupportedError("readMarkdownAllFile()", "not available in browser. Use readMarkdownAll(string) instead.");
782
+ }
783
+ /**
784
+ * Write Markdown to file (Node.js only - throws in browser)
785
+ */
786
+ async writeMarkdownFile(_filename, _options) {
787
+ throw new ExcelNotSupportedError("writeMarkdownFile()", "not available in browser. Use writeMarkdown() and trigger a download instead.");
788
+ }
563
789
  // ===========================================================================
564
790
  // Static Factory Methods for Streaming
565
791
  // ===========================================================================
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Workbook - Node.js Version
3
3
  *
4
- * Extends browser Workbook with Node.js file system support for CSV operations.
4
+ * Extends browser Workbook with Node.js file system support for CSV and Markdown operations.
5
5
  */
6
6
  import { Workbook as WorkbookBrowser, type CsvOptions } from "./workbook.browser.js";
7
7
  import type { Worksheet } from "./worksheet.js";
8
+ import type { MarkdownOptions } from "../markdown/types.js";
8
9
  declare class Workbook extends WorkbookBrowser {
9
10
  /**
10
11
  * Read CSV from file (Node.js only)
@@ -27,6 +28,36 @@ declare class Workbook extends WorkbookBrowser {
27
28
  * ```
28
29
  */
29
30
  writeCsvFile(filename: string, options?: CsvOptions): Promise<void>;
31
+ /**
32
+ * Read Markdown table from file (Node.js only)
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * await workbook.readMarkdownFile("table.md");
37
+ * await workbook.readMarkdownFile("table.md", { sheetName: "Data" });
38
+ * ```
39
+ */
40
+ readMarkdownFile(filename: string, options?: MarkdownOptions): Promise<Worksheet>;
41
+ /**
42
+ * Read all Markdown tables from file, each as a separate worksheet (Node.js only)
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * await workbook.readMarkdownAllFile("doc.md");
47
+ * await workbook.readMarkdownAllFile("doc.md", { sheetName: "Table" });
48
+ * ```
49
+ */
50
+ readMarkdownAllFile(filename: string, options?: MarkdownOptions): Promise<Worksheet[]>;
51
+ /**
52
+ * Write Markdown table to file (Node.js only)
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * await workbook.writeMarkdownFile("output.md");
57
+ * await workbook.writeMarkdownFile("output.md", { sheetName: "Data", padding: true });
58
+ * ```
59
+ */
60
+ writeMarkdownFile(filename: string, options?: MarkdownOptions): Promise<void>;
30
61
  }
31
62
  export { Workbook };
32
63
  export type { CsvOptions, CsvInput, WorkbookModel, WorkbookMedia } from "./workbook.browser.js";
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Workbook - Node.js Version
3
3
  *
4
- * Extends browser Workbook with Node.js file system support for CSV operations.
4
+ * Extends browser Workbook with Node.js file system support for CSV and Markdown operations.
5
5
  */
6
- import { fileExists, createReadStream, createWriteStream } from "../../utils/fs.browser.js";
6
+ import { fileExists, createReadStream, createWriteStream, readFileText, writeFileText } from "../../utils/fs.browser.js";
7
7
  import { Workbook as WorkbookBrowser } from "./workbook.browser.js";
8
8
  import { ExcelFileError } from "./errors.js";
9
9
  // =============================================================================
@@ -57,5 +57,50 @@ class Workbook extends WorkbookBrowser {
57
57
  }
58
58
  return this._writeCsvStream(writeStream, options);
59
59
  }
60
+ /**
61
+ * Read Markdown table from file (Node.js only)
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * await workbook.readMarkdownFile("table.md");
66
+ * await workbook.readMarkdownFile("table.md", { sheetName: "Data" });
67
+ * ```
68
+ */
69
+ async readMarkdownFile(filename, options) {
70
+ if (!(await fileExists(filename))) {
71
+ throw new ExcelFileError(filename, "read", "file not found");
72
+ }
73
+ const content = await readFileText(filename);
74
+ return this.readMarkdown(content, options);
75
+ }
76
+ /**
77
+ * Read all Markdown tables from file, each as a separate worksheet (Node.js only)
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * await workbook.readMarkdownAllFile("doc.md");
82
+ * await workbook.readMarkdownAllFile("doc.md", { sheetName: "Table" });
83
+ * ```
84
+ */
85
+ async readMarkdownAllFile(filename, options) {
86
+ if (!(await fileExists(filename))) {
87
+ throw new ExcelFileError(filename, "read", "file not found");
88
+ }
89
+ const content = await readFileText(filename);
90
+ return this.readMarkdownAll(content, options);
91
+ }
92
+ /**
93
+ * Write Markdown table to file (Node.js only)
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * await workbook.writeMarkdownFile("output.md");
98
+ * await workbook.writeMarkdownFile("output.md", { sheetName: "Data", padding: true });
99
+ * ```
100
+ */
101
+ async writeMarkdownFile(filename, options) {
102
+ const markdownString = this.writeMarkdown(options);
103
+ await writeFileText(filename, markdownString);
104
+ }
60
105
  }
61
106
  export { Workbook };
@@ -46,6 +46,13 @@ class StreamingZipWriterAdapter {
46
46
  // Backpressure tracking
47
47
  this._needsDrain = false;
48
48
  this._drainResolvers = [];
49
+ // Count of in-flight async write() calls whose backpressure result is unknown.
50
+ // waitForDrain() must wait for this to reach 0 before checking _needsDrain.
51
+ this._pendingWrites = 0;
52
+ this._pendingWriteResolvers = [];
53
+ // Buffer errors that occur before _finalize registers its error listener,
54
+ // so async compression errors during writeToZip() are never silently lost.
55
+ this._earlyError = null;
49
56
  this.level = options?.level ?? 6;
50
57
  this.modTime = options?.modTime;
51
58
  this.timestamps = options?.timestamps;
@@ -69,6 +76,15 @@ class StreamingZipWriterAdapter {
69
76
  });
70
77
  }
71
78
  _emit(event, ...args) {
79
+ // Buffer error events that fire before any listener is registered,
80
+ // so _finalize() can surface them even if it registers late.
81
+ if (event === "error") {
82
+ const callbacks = this.events.get(event);
83
+ if (!callbacks || callbacks.size === 0) {
84
+ this._earlyError = args[0] instanceof Error ? args[0] : new Error(String(args[0]));
85
+ return;
86
+ }
87
+ }
72
88
  const callbacks = this.events.get(event);
73
89
  if (!callbacks) {
74
90
  return;
@@ -83,12 +99,21 @@ class StreamingZipWriterAdapter {
83
99
  */
84
100
  _checkBackpressure(ok) {
85
101
  if (ok instanceof Promise) {
102
+ this._pendingWrites++;
86
103
  ok.then(result => {
87
104
  if (result === false) {
88
105
  this._needsDrain = true;
89
106
  }
90
107
  }, () => { } // write errors surface via the stream's error event
91
- );
108
+ ).finally(() => {
109
+ this._pendingWrites--;
110
+ if (this._pendingWrites === 0) {
111
+ const resolvers = this._pendingWriteResolvers.splice(0);
112
+ for (const resolve of resolvers) {
113
+ resolve();
114
+ }
115
+ }
116
+ });
92
117
  return;
93
118
  }
94
119
  if (ok === false) {
@@ -99,6 +124,12 @@ class StreamingZipWriterAdapter {
99
124
  const callbacks = this.events.get(event) || new Set();
100
125
  callbacks.add(callback);
101
126
  this.events.set(event, callbacks);
127
+ // If an error was buffered before any listener was registered, deliver it now.
128
+ if (event === "error" && this._earlyError) {
129
+ const err = this._earlyError;
130
+ this._earlyError = null;
131
+ callback(err);
132
+ }
102
133
  return this;
103
134
  }
104
135
  once(event, callback) {
@@ -134,11 +165,18 @@ class StreamingZipWriterAdapter {
134
165
  }
135
166
  /**
136
167
  * Wait for the downstream writable to drain if it signaled backpressure.
137
- * Resolves immediately if no backpressure is active.
168
+ * If any write() calls are still in-flight (returned a Promise that hasn't
169
+ * settled), waits for all of them first so the backpressure signal isn't missed.
138
170
  */
139
- waitForDrain() {
171
+ async waitForDrain() {
172
+ // Wait for all in-flight async writes to settle so _needsDrain is up to date.
173
+ if (this._pendingWrites > 0) {
174
+ await new Promise(resolve => {
175
+ this._pendingWriteResolvers.push(resolve);
176
+ });
177
+ }
140
178
  if (!this._needsDrain || !this.pipedStream) {
141
- return Promise.resolve();
179
+ return;
142
180
  }
143
181
  return new Promise(resolve => {
144
182
  this._drainResolvers.push(resolve);