From 3a887ae3b13f3579dd3d54bc9bb2a16ea18f761a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Thu, 14 Aug 2025 23:59:16 -0400 Subject: [PATCH 01/16] feat: export streaming --- packages/demo/src/app-routing.ts | 2 + packages/demo/src/examples/example15.html | 17 ++ packages/demo/src/examples/example15.ts | 51 ++++++ .../dist/index.d.ts | 22 +++ .../src/Excel/Workbook.ts | 10 ++ .../src/Excel/Worksheet.ts | 166 ++++++++++++++++++ packages/excel-builder-vanilla/src/factory.ts | 2 +- packages/excel-builder-vanilla/src/index.ts | 1 + .../excel-builder-vanilla/src/streaming.ts | 75 ++++++++ 9 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 packages/demo/src/examples/example15.html create mode 100644 packages/demo/src/examples/example15.ts create mode 100644 packages/excel-builder-vanilla/src/streaming.ts diff --git a/packages/demo/src/app-routing.ts b/packages/demo/src/app-routing.ts index ecdef79..775db44 100644 --- a/packages/demo/src/app-routing.ts +++ b/packages/demo/src/app-routing.ts @@ -12,6 +12,7 @@ import Example11 from './examples/example11.js'; import Example12 from './examples/example12.js'; import Example13 from './examples/example13.js'; import Example14 from './examples/example14.js'; +import Example15 from './examples/example15.js'; import GettingStarted from './getting-started.js'; export const navbarRouting = [ @@ -42,6 +43,7 @@ export const exampleRouting = [ { name: 'example12', view: '/src/examples/example12.html', viewModel: Example12, title: '12- Worksheet Headers/Footers' }, { name: 'example13', view: '/src/examples/example13.html', viewModel: Example13, title: '13- Pictures with 2 anchors' }, { name: 'example14', view: '/src/examples/example14.html', viewModel: Example14, title: '14- Pictures with different anchors' }, + { name: 'example15', view: '/src/examples/example15.html', viewModel: Example15, title: '15- Streaming Excel Export' }, ], }, ]; diff --git a/packages/demo/src/examples/example15.html b/packages/demo/src/examples/example15.html new file mode 100644 index 0000000..74ad083 --- /dev/null +++ b/packages/demo/src/examples/example15.html @@ -0,0 +1,17 @@ +
+
+
+

Example 15: Streaming Excel Export (100,000 rows)

+
+ This example demonstrates streaming export for large datasets using createExcelFileStream. Progress is shown below. +
+
+
+
+ +
+
+
diff --git a/packages/demo/src/examples/example15.ts b/packages/demo/src/examples/example15.ts new file mode 100644 index 0000000..577e258 --- /dev/null +++ b/packages/demo/src/examples/example15.ts @@ -0,0 +1,51 @@ +import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla'; + +export default class Example { + exportBtnElm!: HTMLButtonElement; + progressElm!: HTMLDivElement; + + mount() { + this.exportBtnElm = document.querySelector('#export') as HTMLButtonElement; + this.progressElm = document.querySelector('#progress') as HTMLDivElement; + this.exportBtnElm.addEventListener('click', this.startProcess.bind(this)); + } + + unmount() { + this.exportBtnElm.removeEventListener('click', this.startProcess.bind(this)); + } + + async startProcess() { + const ROWS = 100_000; + const originalData = [['Artist', 'Album', 'Price']]; + for (let i = 0; i < ROWS; i++) { + originalData.push([`Artist ${i}`, `Album ${i}`, Math.round(Math.random() * 10000) / 100]); + } + + const artistWorkbook = createWorkbook(); + const albumList = artistWorkbook.createWorksheet({ name: 'Artists' }); + albumList.setData(originalData); + artistWorkbook.addWorksheet(albumList); + + // Streaming export + const stream = createExcelFileStream(artistWorkbook, { chunkSize: 1000 }); + const chunks: Uint8Array[] = []; + let processed = 0; + + for await (const chunk of stream) { + chunks.push(chunk); + processed += 1000; + console.log(`Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`); + this.progressElm.textContent = `Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`; + } + + // Combine chunks and trigger download + const blob = new Blob(chunks, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'LargeArtistWB.xlsx'; + a.click(); + URL.revokeObjectURL(url); + this.progressElm.textContent = 'Export complete!'; + } +} diff --git a/packages/excel-builder-vanilla-types/dist/index.d.ts b/packages/excel-builder-vanilla-types/dist/index.d.ts index 618ecae..8059db3 100644 --- a/packages/excel-builder-vanilla-types/dist/index.d.ts +++ b/packages/excel-builder-vanilla-types/dist/index.d.ts @@ -842,6 +842,10 @@ export declare class Worksheet { * @returns {undefined} */ exportPageSettings(doc: XMLDOM, worksheet: XMLNode): void; + /** + * Serialize a chunk of rows to XML. + */ + serializeRows(rows: any[]): string; /** * http://www.schemacentral.com/sc/ooxml/t-ssml_ST_Orientation.html * @@ -968,6 +972,8 @@ export declare class Workbook { generateFiles(): Promise<{ [path: string]: string; }>; + serializeHeader(): string; + serializeFooter(): string; } export declare class Picture extends Drawing { id: string; @@ -1043,6 +1049,22 @@ export declare function downloadExcelFile(workbook: Workbook, filename: string, mimeType?: string; zipOptions?: ZipOptions; }): Promise; +/** + * Async generator that yields zipped Excel file chunks. + * @param workbook Workbook instance + * @param options {chunkSize} Number of rows per chunk + */ +export declare function createExcelFileStream(workbook: Workbook, options?: { + chunkSize?: number; +}): AsyncGenerator | { + type: string; + name: string; + xml: string; +} | { + type: string; + xml: string; + name?: undefined; +}, void, unknown>; /** * Converts the characters "&", "<", ">", '"', and "'" in `string` to their * corresponding HTML entities. diff --git a/packages/excel-builder-vanilla/src/Excel/Workbook.ts b/packages/excel-builder-vanilla/src/Excel/Workbook.ts index 9caab74..22bf938 100644 --- a/packages/excel-builder-vanilla/src/Excel/Workbook.ts +++ b/packages/excel-builder-vanilla/src/Excel/Workbook.ts @@ -356,4 +356,14 @@ export class Workbook { return resolve(files); }); } + + serializeHeader(): string { + // Return workbook XML header + return ''; + } + + serializeFooter(): string { + // Return workbook XML footer + return ''; + } } diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index 5879e62..f008972 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -644,4 +644,170 @@ export class Worksheet { setColumnFormats(columnFormats: ExcelColumnFormat[]) { this.columnFormats = columnFormats; } + + /** + * Returns worksheet XML header (everything before ) + */ + getWorksheetXmlHeader(): string { + // const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'worksheet'); + // const worksheet = doc.documentElement; + // worksheet.setAttribute('xmlns:r', Util.schemas.relationships); + // worksheet.setAttribute('xmlns:mc', Util.schemas.markupCompat); + + // let maxX = 0; + // const data = this.data; + // const columns = this.columns || []; + // for (let row = 0, l = data.length; row < l; row++) { + // const cellCount = data[row].length; + // maxX = cellCount > maxX ? cellCount : maxX; + // } + + // if (maxX !== 0) { + // worksheet.appendChild( + // Util.createElement(doc, 'dimension', [ + // ['ref', `${Util.positionToLetterRef(1, 1)}:${Util.positionToLetterRef(maxX, String(data.length))}`], + // ]), + // ); + // } else { + // worksheet.appendChild(Util.createElement(doc, 'dimension', [['ref', Util.positionToLetterRef(1, 1)]])); + // } + + // worksheet.appendChild(this.sheetView.exportXML(doc)); + + // if (this.columns.length) { + // worksheet.appendChild(this.exportColumns(doc)); + // } + + // // Add start tag + // const xml = doc.toString(); + // return xml.substring(0, xml.indexOf('') + ''.length); + return ` + + `; + } + + /** + * Returns worksheet XML footer (everything after ) + */ + getWorksheetXmlFooter(): string { + // const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'worksheet'); + // const worksheet = doc.documentElement; + + // // Add all elements after + // if (this.sheetProtection) { + // worksheet.appendChild(this.sheetProtection.exportXML(doc)); + // } + + // if (this.hyperlinks.length > 0) { + // const hyperlinksEl = doc.createElement('hyperlinks'); + // const hyperlinks = this.hyperlinks; + // for (let i = 0, l = hyperlinks.length; i < l; i++) { + // const hyperlinkEl = doc.createElement('hyperlink'); + // const hyperlink: any = hyperlinks[i]; + // hyperlinkEl.setAttribute('ref', String(hyperlink.cell)); + // hyperlink.id = Util.uniqueId('hyperlink'); + // this.relations.addRelation( + // { + // id: hyperlink.id, + // target: hyperlink.location, + // targetMode: hyperlink.targetMode || 'External', + // }, + // 'hyperlink', + // ); + // hyperlinkEl.setAttribute('r:id', this.relations.getRelationshipId(hyperlink)); + // hyperlinksEl.appendChild(hyperlinkEl); + // } + // worksheet.appendChild(hyperlinksEl); + // } + + // if (this.mergedCells.length > 0) { + // const mergeCells = doc.createElement('mergeCells'); + // for (let i = 0, l = this.mergedCells.length; i < l; i++) { + // const mergeCell = doc.createElement('mergeCell'); + // mergeCell.setAttribute('ref', `${this.mergedCells[i][0]}:${this.mergedCells[i][1]}`); + // mergeCells.appendChild(mergeCell); + // } + // worksheet.appendChild(mergeCells); + // } + + // this.exportPageSettings(doc, worksheet); + + // if (this._headers.length > 0 || this._footers.length > 0) { + // const headerFooter = doc.createElement('headerFooter'); + // if (this._headers.length > 0) { + // headerFooter.appendChild(this.exportHeader(doc)); + // } + // if (this._footers.length > 0) { + // headerFooter.appendChild(this.exportFooter(doc)); + // } + // worksheet.appendChild(headerFooter); + // } + + // for (let i = 0, l = this._drawings.length; i < l; i++) { + // const drawing = doc.createElement('drawing'); + // drawing.setAttribute('r:id', this.relations.getRelationshipId(this._drawings[i])); + // worksheet.appendChild(drawing); + // } + + // if (this._tables.length > 0) { + // const tables = doc.createElement('tableParts'); + // tables.setAttribute('count', this._tables.length); + // for (let i = 0, l = this._tables.length; i < l; i++) { + // const table = doc.createElement('tablePart'); + // table.setAttribute('r:id', this.relations.getRelationshipId(this._tables[i])); + // tables.appendChild(table); + // } + // worksheet.appendChild(tables); + // } + + // // Get everything after + // const xml = doc.toString(); + // return xml.substring(xml.indexOf('') + ''.length); + return ''; + } + + /** + * Serialize a chunk of rows to XML (same logic as in toXML) + */ + serializeRows(rows: (number | string | boolean | Date | null | ExcelColumnMetadata)[][], startRow = 0): string { + const columns = this.columns || []; + let xml = ''; + for (let row = 0, l = rows.length; row < l; row++) { + const dataRow = rows[row]; + const cellCount = dataRow.length; + let rowXml = ``; + for (let c = 0; c < cellCount; c++) { + let cellValue = dataRow[c]; + let cellType: any = typeof cellValue; + // Always treat first row as text + if (startRow + row === 0) { + cellType = 'text'; + } + let cellXml = ''; + const rAttr = ` r="${String.fromCharCode(65 + c)}${startRow + row + 1}"`; + switch (cellType) { + case 'number': + cellXml = `${cellValue}`; + break; + case 'text': + default: { + let id: number | undefined; + if (typeof this.sharedStrings?.strings[cellValue as string] !== 'undefined') { + id = this.sharedStrings.strings[cellValue as string]; + } else { + id = this.sharedStrings?.addString(cellValue as string); + } + cellXml = `${id}`; + break; + } + } + rowXml += cellXml; + } + rowXml += ''; + xml += rowXml; + } + return xml; + } } diff --git a/packages/excel-builder-vanilla/src/factory.ts b/packages/excel-builder-vanilla/src/factory.ts index 511aa5b..6a70638 100644 --- a/packages/excel-builder-vanilla/src/factory.ts +++ b/packages/excel-builder-vanilla/src/factory.ts @@ -1,4 +1,4 @@ -import { type ZipOptions, strToU8, zip } from 'fflate'; +import { strToU8, type ZipOptions, zip } from 'fflate'; import { Workbook } from './Excel/Workbook.js'; diff --git a/packages/excel-builder-vanilla/src/index.ts b/packages/excel-builder-vanilla/src/index.ts index e2852fc..b96089d 100644 --- a/packages/excel-builder-vanilla/src/index.ts +++ b/packages/excel-builder-vanilla/src/index.ts @@ -18,6 +18,7 @@ export { Workbook } from './Excel/Workbook.js'; export { Worksheet } from './Excel/Worksheet.js'; export { XMLDOM, XMLNode } from './Excel/XMLDOM.js'; export { createExcelFile, createWorkbook, downloadExcelFile } from './factory.js'; +export { createExcelFileStream } from './streaming.js'; export * from './interfaces.js'; export { htmlEscape } from './utilities/escape.js'; export { isObject, isPlainObject, isString } from './utilities/isTypeOf.js'; diff --git a/packages/excel-builder-vanilla/src/streaming.ts b/packages/excel-builder-vanilla/src/streaming.ts new file mode 100644 index 0000000..1ea69f6 --- /dev/null +++ b/packages/excel-builder-vanilla/src/streaming.ts @@ -0,0 +1,75 @@ +import { strToU8, zip } from 'fflate'; + +import type { Workbook } from './Excel/Workbook.js'; +import { base64ToUint8Array } from './factory.js'; + +export interface ExcelFileStreamOptions { + chunkSize?: number; + outputType?: 'Uint8Array' | 'Blob'; + fileFormat?: 'xlsx' | 'xls'; + mimeType?: string; +} + +/** + * Async generator that yields zipped Excel file chunks. + * @param workbook Workbook instance + * @param options {chunkSize} Number of rows per chunk + */ +export async function* createExcelFileStream(workbook: Workbook, options?: ExcelFileStreamOptions) { + const chunkSize = options?.chunkSize ?? 1000; + const files = await workbook.generateFiles(); + + // Replace worksheet XML with streamed version + for (let i = 0; i < workbook.worksheets.length; i++) { + const worksheet = workbook.worksheets[i]; + let worksheetXml = ''; + worksheetXml += worksheet.getWorksheetXmlHeader(); + let rowIndex = 0; + const totalRows = worksheet.data.length; + while (rowIndex < totalRows) { + const rowsChunk = worksheet.data.slice(rowIndex, rowIndex + chunkSize); + worksheetXml += worksheet.serializeRows(rowsChunk, rowIndex); + rowIndex += chunkSize; + await new Promise(r => setTimeout(r, 0)); + } + worksheetXml += ''; + worksheetXml += worksheet.getWorksheetXmlFooter(); + worksheetXml += ''; + + // Use the same path as generateFiles + const wsPath = `/xl/worksheets/sheet${i + 1}.xml`; + files[wsPath] = worksheetXml; + } + + // Convert files to Uint8Array + const zipObj: { [name: string]: Uint8Array } = {}; + for (const [path, content] of Object.entries(files)) { + const outPath = path.startsWith('/') ? path.substr(1) : path; + if (path.indexOf('.xml') !== -1 || path.indexOf('.rel') !== -1) { + zipObj[outPath] = strToU8(content); + } else { + zipObj[outPath] = base64ToUint8Array(content); + } + } + + // Zip and yield + const zipped: Uint8Array = await new Promise((resolve, reject) => { + zip(zipObj, (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); + + const outputType = options?.outputType ?? 'Uint8Array'; + if (outputType === 'Uint8Array') { + yield zipped; + } else { + const format = options?.fileFormat ?? 'xlsx'; + let mimeType = options?.mimeType; + if (mimeType === undefined) { + mimeType = format === 'xls' ? 'application/vnd.ms-excel' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + const arrayBuffer = zipped.buffer.slice(zipped.byteOffset, zipped.byteOffset + zipped.byteLength); + yield new Blob([arrayBuffer as BlobPart], { type: mimeType }); + } +} From 30274748cc9feaacb9f99aa35d3c1cf06b106a2b Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 13:13:52 -0400 Subject: [PATCH 02/16] chore: get a working streaming to excel but without header/footer --- biome.json | 3 +- package.json | 2 +- packages/demo/src/examples/example15.ts | 32 +- .../dist/index.d.ts | 1122 ----------------- .../excel-builder-vanilla-types/dist/index.js | 1 - .../src/Excel/Workbook.ts | 5 +- .../src/Excel/Worksheet.ts | 108 +- .../excel-builder-vanilla/src/streaming.ts | 121 +- 8 files changed, 119 insertions(+), 1275 deletions(-) delete mode 100644 packages/excel-builder-vanilla-types/dist/index.d.ts delete mode 100644 packages/excel-builder-vanilla-types/dist/index.js diff --git a/biome.json b/biome.json index 479bfa8..c8c8d01 100644 --- a/biome.json +++ b/biome.json @@ -51,7 +51,8 @@ "recommended": true, "complexity": { "noForEach": "off", - "noStaticOnlyClass": "off" + "noStaticOnlyClass": "off", + "noUselessSwitchCase": "off" }, "performance": { "noBarrelFile": "off", diff --git a/package.json b/package.json index 90fd282..02943fa 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "url": "https://ko-fi.com/ghiscoding" }, "scripts": { - "clean": "remove --glob **/dist **/tsconfig.tsbuildinfo", + "clean": "remove --glob \"**/dist **/tsconfig.tsbuildinfo\"", "prebuild": "pnpm run clean && pnpm run biome:lint:write && pnpm run biome:format:write", "build": "pnpm -r --stream build", "build:demo": "pnpm -r --stream --filter \"./packages/demo/**\" build", diff --git a/packages/demo/src/examples/example15.ts b/packages/demo/src/examples/example15.ts index 577e258..26d4a61 100644 --- a/packages/demo/src/examples/example15.ts +++ b/packages/demo/src/examples/example15.ts @@ -1,4 +1,4 @@ -import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla'; +import { createExcelFileStream, createWorkbook } from 'excel-builder-vanilla'; export default class Example { exportBtnElm!: HTMLButtonElement; @@ -16,7 +16,7 @@ export default class Example { async startProcess() { const ROWS = 100_000; - const originalData = [['Artist', 'Album', 'Price']]; + const originalData: (number | string | boolean | Date | null)[][] = [['Artist', 'Album', 'Price']]; for (let i = 0; i < ROWS; i++) { originalData.push([`Artist ${i}`, `Album ${i}`, Math.round(Math.random() * 10000) / 100]); } @@ -31,15 +31,31 @@ export default class Example { const chunks: Uint8Array[] = []; let processed = 0; - for await (const chunk of stream) { - chunks.push(chunk); - processed += 1000; - console.log(`Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`); - this.progressElm.textContent = `Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`; + if (typeof window !== 'undefined' && stream && typeof stream.getReader === 'function') { + // Browser: ReadableStream + const reader = stream.getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + processed += value.length; + const rowsExported = Math.floor(processed / (chunks.length > 0 ? chunks[0].length : 1)) * 1000; + this.progressElm.textContent = `Exported ~${rowsExported} / ${ROWS} rows...`; + } + } else { + // Node/fallback: async generator + for await (const chunk of stream) { + chunks.push(chunk); + processed += 1000; + this.progressElm.textContent = `Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`; + } } // Combine chunks and trigger download - const blob = new Blob(chunks, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const blob = new Blob( + chunks.map(chunk => chunk.slice()), + { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + ); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; diff --git a/packages/excel-builder-vanilla-types/dist/index.d.ts b/packages/excel-builder-vanilla-types/dist/index.d.ts deleted file mode 100644 index 8059db3..0000000 --- a/packages/excel-builder-vanilla-types/dist/index.d.ts +++ /dev/null @@ -1,1122 +0,0 @@ -// Generated by dts-bundle-generator v9.5.1 - -import { ZipOptions } from 'fflate'; - -export type XMLNodeOption = { - attributes?: { - [key: string]: any; - }; - children?: XMLNode[]; - nodeName: string; - nodeValue?: string; - type?: string; -}; -export declare class XMLDOM { - documentElement: XMLNode; - constructor(ns: string | null, rootNodeName: string); - createElement(name: string): XMLNode; - createTextNode(text: string): TextNode; - toString(): string; - static Node: { - Create: (config: any) => XMLNode | TextNode | null; - }; -} -declare class TextNode { - nodeValue: any; - constructor(text: string); - toJSON(): { - nodeValue: any; - type: string; - }; - toString(): string; -} -export declare class XMLNode { - nodeName: string; - children: XMLNode[]; - nodeValue: string; - attributes: { - [key: string]: any; - }; - firstChild?: XMLNode; - constructor(config: XMLNodeOption); - toString(): string; - toJSON(): { - nodeName: string; - children: any[]; - nodeValue: string; - attributes: { - [key: string]: any; - }; - type: string; - }; - setAttribute(name: string, val: any): void; - appendChild(child: any): void; - cloneNode(_deep?: boolean): XMLNode; -} -/** - * - * @param {Object} config - * @param {Number} config.x The cell column number that the top left of the picture will start in - * @param {Number} config.y The cell row number that the top left of the picture will start in - * @param {Number} config.width Width in EMU's - * @param {Number} config.height Height in EMU's - * @constructor - */ -export declare class OneCellAnchor { - x: number | null; - y: number | null; - xOff: boolean | null; - yOff: boolean | null; - width: number | null; - height: number | null; - constructor(config: AnchorOption); - setPos(x: number, y: number, xOff?: boolean, yOff?: boolean): void; - setDimensions(width: number, height: number): void; - toXML(xmlDoc: XMLDOM, content: any): XMLNode; -} -export declare class TwoCellAnchor { - from: any; - to: any; - constructor(config: DualAnchorOption); - setFrom(x: number, y: number, xOff?: boolean, yOff?: boolean): void; - setTo(x: number, y: number, xOff?: boolean, yOff?: boolean): void; - toXML(xmlDoc: XMLDOM, content: any): XMLNode; -} -export interface AnchorOption { - /** X offset in EMU's */ - x: number; - /** Y offset in EMU's */ - y: number; - xOff?: boolean; - yOff?: boolean; - /** Width in EMU's */ - height: number; - /** Height in EMU's */ - width: number; - drawing?: Drawing; -} -export interface DualAnchorOption { - to: AnchorOption; - from: AnchorOption; - drawing?: Drawing; -} -/** - * This is mostly a global spot where all of the relationship managers can get and set - * path information from/to. - * @module Excel/Drawing - */ -export declare class Drawing { - anchor: AbsoluteAnchor | OneCellAnchor | TwoCellAnchor; - id: string; - /** - * - * @param {String} type Can be 'absoluteAnchor', 'oneCellAnchor', or 'twoCellAnchor'. - * @param {Object} config Shorthand - pass the created anchor coords that can normally be used to construct it. - * @returns {Anchor} - */ - createAnchor(type: "absoluteAnchor" | "oneCellAnchor" | "twoCellAnchor", config: any): AbsoluteAnchor | OneCellAnchor | TwoCellAnchor; -} -/** - * - * @param {Object} config - * @param {Number} config.x X offset in EMU's - * @param {Number} config.y Y offset in EMU's - * @param {Number} config.width Width in EMU's - * @param {Number} config.height Height in EMU's - * @constructor - */ -export declare class AbsoluteAnchor { - x: number | null; - y: number | null; - width: number | null; - height: number | null; - constructor(config: AnchorOption); - /** - * Sets the X and Y offsets. - * - * @param {Number} x - * @param {Number} y - * @returns {undefined} - */ - setPos(x: number, y: number): void; - /** - * Sets the width and height of the image. - * - * @param {Number} width - * @param {Number} height - * @returns {undefined} - */ - setDimensions(width: number, height: number): void; - toXML(xmlDoc: XMLDOM, content: any): XMLNode; -} -export declare class Chart { -} -/** - * @module Excel/Util - */ -export declare class Util { - static _idSpaces: { - [space: string]: number; - }; - /** - * Returns a number based on a namespace. So, running with 'Picture' will return 1. Run again, you will get 2. Run with 'Foo', you'll get 1. - * @param {String} space - * @returns {Number} - */ - static uniqueId(space: string): number; - /** - * Attempts to create an XML document. After some investigation, using the 'fake' document - * is significantly faster than creating an actual XML document, so we're going to go with - * that. Besides, it just makes it easier to port to node. - * - * Takes a namespace to start the xml file in, as well as the root element - * of the xml file. - * - * @param {type} ns - * @param {type} base - * @returns {@new;XMLDOM} - */ - static createXmlDoc(ns: string, base: string): XMLDOM; - /** - * Creates an xml node (element). Used to simplify some calls, as IE is - * very particular about namespaces and such. - * - * @param {XMLDOM} doc An xml document (actual DOM or fake DOM, not a string) - * @param {type} name The name of the element - * @param {type} attributes - * @returns {XML Node} - */ - static createElement(doc: XMLDOM, name: string, attributes?: any): XMLNode; - /** - * This is sort of slow, but it's a huge convenience method for the code. It probably shouldn't be used - * in high repetition areas. - * - * @param {XMLDoc} doc - * @param {Object} attrs - */ - static setAttributesOnDoc(doc: XMLNode, attrs: { - [key: string]: any; - }): void; - static LETTER_REFS: any; - static positionToLetterRef(x: number, y: number | string): any; - static schemas: { - worksheet: string; - sharedStrings: string; - stylesheet: string; - relationships: string; - relationshipPackage: string; - contentTypes: string; - spreadsheetml: string; - markupCompat: string; - x14ac: string; - officeDocument: string; - package: string; - table: string; - spreadsheetDrawing: string; - drawing: string; - drawingRelationship: string; - image: string; - chart: string; - hyperlink: string; - }; -} -export type Relation = { - [id: string]: { - id: string; - schema: string; - object: any; - data?: { - id: number; - schema: string; - object: any; - }; - }; -}; -/** - * @module Excel/RelationshipManager - */ -export declare class RelationshipManager { - relations: Relation; - lastId: number; - constructor(); - importData(data: { - relations: Relation; - lastId: number; - }): void; - exportData(): { - relations: Relation; - lastId: number; - }; - addRelation(object: { - id: string; - }, type: keyof typeof Util.schemas): string; - getRelationshipId(object: { - id: string; - }): string | null; - toXML(): XMLDOM; -} -/** - * @module Excel/Drawings - */ -export declare class Drawings { - drawings: (Drawing | Picture)[]; - relations: RelationshipManager; - id: string; - /** - * Adds a drawing (more likely a subclass of a Drawing) to the 'Drawings' for a particular worksheet. - * - * @param {Drawing} drawing - * @returns {undefined} - */ - addDrawing(drawing: Drawing): void; - getCount(): number; - toXML(): XMLDOM; -} -/** - * @module Excel/SharedStrings - */ -export declare class SharedStrings { - strings: { - [key: string]: number; - }; - stringArray: string[]; - id: string; - /** - * Adds a string to the shared string file, and returns the ID of the - * string which can be used to reference it in worksheets. - * - * @param str {String} - * @return int - */ - addString(str: string): number; - exportData(): { - [key: string]: number; - }; - toXML(): XMLDOM; -} -/** - * Excel Color in ARGB format, for color aren't transparent just use "FF" as prefix. - * For example if the color you want to add is a blue with HTML color "#0000FF", then the excel color we need to add is "FF0000FF" - * Online tool: https://www.myfixguide.com/color-converter/ - */ -export type ExcelColorStyle = string | { - theme: number; -}; -export interface ExcelAlignmentStyle { - horizontal?: "center" | "fill" | "general" | "justify" | "left" | "right"; - justifyLastLine?: boolean; - readingOrder?: string; - relativeIndent?: boolean; - shrinkToFit?: boolean; - textRotation?: string | number; - vertical?: "bottom" | "distributed" | "center" | "justify" | "top"; - wrapText?: boolean; -} -export type ExcelBorderLineStyle = "continuous" | "dash" | "dashDot" | "dashDotDot" | "dotted" | "double" | "lineStyleNone" | "medium" | "slantDashDot" | "thin" | "thick"; -export interface ExcelBorderStyle { - bottom?: { - color?: ExcelColorStyle; - style?: ExcelBorderLineStyle; - }; - top?: { - color?: ExcelColorStyle; - style?: ExcelBorderLineStyle; - }; - left?: { - color?: ExcelColorStyle; - style?: ExcelBorderLineStyle; - }; - right?: { - color?: ExcelColorStyle; - style?: ExcelBorderLineStyle; - }; - diagonal?: any; - outline?: boolean; - diagonalUp?: boolean; - diagonalDown?: boolean; -} -export interface ExcelColumn { - bestFit?: boolean; - customWidth?: number; - hidden?: boolean; - min?: number; - max?: number; - width?: number; -} -export type ExcelColumnFormat = "bestFit" | "collapsed" | "customWidth" | "hidden" | "max" | "min" | "outlineLevel" | "phonetic" | "style" | "width"; -export interface ExcelTableColumn { - name: string; - dataCellStyle?: any; - dataDxfId?: number; - headerRowCellStyle?: ExcelStyleInstruction; - headerRowDxfId?: number; - totalsRowCellStyle?: ExcelStyleInstruction; - totalsRowDxfId?: number; - totalsRowFunction?: any; - totalsRowLabel?: string; - columnFormula?: string; - columnFormulaIsArrayType?: boolean; - totalFormula?: string; - totalFormulaIsArrayType?: boolean; -} -export interface ExcelFillStyle { - type?: "gradient" | "pattern"; - patternType?: string; - degree?: number; - fgColor?: ExcelColorStyle; - start?: ExcelColorStyle; - end?: { - pureAt?: number; - color?: ExcelColorStyle; - }; -} -export interface ExcelFontStyle { - bold?: boolean; - color?: ExcelColorStyle; - fontName?: string; - italic?: boolean; - outline?: boolean; - size?: number; - shadow?: boolean; - strike?: boolean; - subscript?: boolean; - superscript?: boolean; - underline?: boolean | "single" | "double" | "singleAccounting" | "doubleAccounting"; -} -export interface ExcelMetadata { - type?: string; - style?: number; -} -export interface ExcelColumnMetadata { - value: any; - metadata?: ExcelMetadata; -} -export interface ExcelMargin { - top: number; - bottom: number; - left: number; - right: number; - header: number; - footer: number; -} -export interface ExcelSortState { - caseSensitive?: boolean; - dataRange?: any; - columnSort?: boolean; - sortDirection?: "ascending" | "descending"; - sortRange?: any; -} -/** Excel custom formatting that will be applied to a column */ -export interface ExcelStyleInstruction { - id?: number; - alignment?: ExcelAlignmentStyle; - border?: ExcelBorderStyle; - borderId?: number; - fill?: ExcelFillStyle; - fillId?: number; - font?: ExcelFontStyle; - fontId?: number; - format?: string; - height?: number; - numFmt?: string; - numFmtId?: number; - width?: number; - xfId?: number; - protection?: { - locked?: boolean; - hidden?: boolean; - }; - /** style id */ - style?: number; -} -/** - * @module Excel/StyleSheet - */ -declare class StyleSheet$1 { - id: string; - cellStyles: { - name: string; - xfId: string; - builtinId: string; - }[]; - defaultTableStyle: boolean; - differentialStyles: any[]; - masterCellFormats: any[]; - masterCellStyles: any[]; - fonts: ExcelFontStyle[]; - numberFormatters: any[]; - fills: any[]; - borders: any[]; - tableStyles: any[]; - createSimpleFormatter(type: string): { - [id: string]: number; - }; - createFill(fillInstructions: any): any; - createNumberFormatter(formatInstructions: any): { - id: number; - formatCode: any; - }; - /** - * alignment: { - * horizontal: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_HorizontalAlignment.html - * vertical: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_VerticalAlignment.html - * @param {Object} styleInstructions - */ - createFormat(styleInstructions: ExcelStyleInstruction): any; - createDifferentialStyle(styleInstructions: ExcelStyleInstruction): ExcelStyleInstruction; - /** - * Should be an object containing keys that match with one of the keys from this list: - * http://www.schemacentral.com/sc/ooxml/t-ssml_ST_TableStyleType.html - * - * The value should be a reference to a differential format (dxf) - * @param {Object} instructions - */ - createTableStyle(instructions: any): void; - /** - * All params optional - * Expects: { - * top: {}, - * left: {}, - * right: {}, - * bottom: {}, - * diagonal: {}, - * outline: boolean, - * diagonalUp: boolean, - * diagonalDown: boolean - * } - * Each border should follow: - * { - * style: styleString, http://www.schemacentral.com/sc/ooxml/t-ssml_ST_BorderStyle.html - * color: ARBG color (requires the A, so for example FF006666) - * } - * @param {Object} border - */ - createBorderFormatter(border: any): any; - /** - * Supported font styles: - * bold - * italic - * underline (single, double, singleAccounting, doubleAccounting) - * size - * color - * fontName - * strike (strikethrough) - * outline (does this actually do anything?) - * shadow (does this actually do anything?) - * superscript - * subscript - * - * Color is a future goal - at the moment it's looking a bit complicated - * @param {Object} instructions - */ - createFontStyle(instructions: ExcelFontStyle): any; - exportBorders(doc: XMLDOM): XMLNode; - exportBorder(doc: XMLDOM, data: any): XMLNode; - exportColor(doc: XMLDOM, color: any): XMLNode; - exportMasterCellFormats(doc: XMLDOM): XMLNode; - exportMasterCellStyles(doc: XMLDOM): XMLNode; - exportCellFormatElement(doc: XMLDOM, styleInstructions: ExcelStyleInstruction): XMLNode; - exportAlignment(doc: XMLDOM, alignmentData: any): XMLNode; - exportFonts(doc: XMLDOM): XMLNode; - exportFont(doc: XMLDOM, fd: any): XMLNode; - exportFills(doc: XMLDOM): XMLNode; - exportFill(doc: XMLDOM, fd: any): XMLNode; - exportGradientFill(doc: XMLDOM, data: any): XMLNode; - /** - * Pattern types: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_PatternType.html - * @param {XMLDoc} doc - * @param {Object} data - */ - exportPatternFill(doc: XMLDOM, data: any): XMLNode; - exportNumberFormatters(doc: XMLDOM): XMLNode; - exportNumberFormatter(doc: XMLDOM, fd: any): XMLNode; - exportCellStyles(doc: XMLDOM): XMLNode; - exportDifferentialStyles(doc: XMLDOM): XMLNode; - exportDFX(doc: XMLDOM, style: any): XMLNode; - exportTableStyles(doc: XMLDOM): XMLNode; - exportTableStyle(doc: XMLDOM, style: { - name: string; - wholeTable?: number; - headerRow?: number; - }): XMLNode; - exportProtection(doc: XMLDOM, protectionData: any): XMLNode; - toXML(): XMLDOM; -} -/** - * @module Excel/Table - */ -export declare class Table { - name: string; - id: string; - tableId: string; - displayName: string; - dataCellStyle: any; - dataDfxId: number | null; - headerRowBorderDxfId: number | null; - headerRowCellStyle: any; - headerRowCount: number; - headerRowDxfId: number | null; - insertRow: boolean; - insertRowShift: boolean; - ref: any; - tableBorderDxfId: number | null; - totalsRowBorderDxfId: number | null; - totalsRowCellStyle: any; - totalsRowCount: number; - totalsRowDxfId: number | null; - tableColumns: any; - autoFilter: any; - sortState: any; - styleInfo: any; - constructor(config?: any); - initialize(config: any): void; - setReferenceRange(start: number[], end: number[]): void; - setTableColumns(columns: Array): void; - /** - * Expects an object with the following optional properties: - * name (required) - * dataCellStyle - * dataDxfId - * headerRowCellStyle - * headerRowDxfId - * totalsRowCellStyle - * totalsRowDxfId - * totalsRowFunction - * totalsRowLabel - * columnFormula - * columnFormulaIsArrayType (boolean) - * totalFormula - * totalFormulaIsArrayType (boolean) - */ - addTableColumn(column: ExcelTableColumn | string): void; - /** - * Expects an object with the following properties: - * caseSensitive (boolean) - * dataRange - * columnSort (assumes true) - * sortDirection - * sortRange (defaults to dataRange) - */ - setSortState(state: ExcelSortState): void; - toXML(): XMLDOM; - exportTableColumns(doc: XMLDOM): XMLNode; - exportAutoFilter(doc: XMLDOM): XMLNode; - exportTableStyleInfo(doc: XMLDOM): XMLNode; - addAutoFilter(startRef: any, endRef: any): void; -} -export declare class Pane { - state: null | "split" | "frozen" | "frozenSplit"; - xSplit: number | null; - ySplit: number | null; - activePane: string; - topLeftCell: number | string | null; - _freezePane: { - xSplit: number; - ySplit: number; - cell: string; - }; - freezePane(column: number, row: number, cell: string): void; - exportXML(doc: XMLDOM): XMLNode; -} -export interface SheetViewOption { - pane?: Pane; -} -/** - * @module Excel/SheetView - * https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.sheetview%28v=office.14%29.aspx - * - */ -export declare class SheetView { - pane: Pane; - showZeros: boolean | null; - defaultGridColor: string | null; - colorId: number | null; - rightToLeft: boolean | null; - showFormulas: boolean | null; - showGridLines: boolean | null; - showOutlineSymbols: boolean | null; - showRowColHeaders: boolean | null; - showRuler: boolean | null; - showWhiteSpace: boolean | null; - tabSelected: boolean | null; - topLeftCell: boolean | null; - viewType: null; - windowProtection: boolean | null; - zoomScale: boolean | null; - zoomScaleNormal: any; - zoomScalePageLayoutView: any; - zoomScaleSheetLayoutView: any; - constructor(config?: SheetViewOption); - /** - * Added froze pane - * @param column - column number: 0, 1, 2 ... - * @param row - row number: 0, 1, 2 ... - * @param cell - 'A1' - * @deprecated - */ - freezePane(column: number, row: number, cell: string): void; - exportXML(doc: XMLDOM): XMLNode; -} -export interface CharType { - font?: string; - bold?: boolean; - fontSize?: number; - text?: string; - underline?: boolean; -} -export interface WorksheetOption { - name?: string; - sheetView?: SheetView; -} -/** - * This module represents an excel worksheet in its basic form - no tables, charts, etc. Its purpose is - * to hold data, the data's link to how it should be styled, and any links to other outside resources. - * - * @module Excel/Worksheet - */ -export declare class Worksheet { - name: string; - id: string; - _timezoneOffset: number; - relations: any; - columnFormats: ExcelColumnFormat[]; - data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][]; - mergedCells: string[][]; - columns: ExcelColumn[]; - sheetProtection: any; - _headers: [ - left?: any, - center?: any, - right?: any - ]; - _footers: [ - left?: any, - center?: any, - right?: any - ]; - _tables: Table[]; - _drawings: Array; - _orientation?: string; - _margin?: ExcelMargin; - _rowInstructions: any; - _freezePane: { - xSplit?: number; - ySplit?: number; - cell?: string; - }; - sharedStrings: SharedStrings | null; - hyperlinks: never[]; - sheetView: SheetView; - showZeros: any; - constructor(config: WorksheetOption); - initialize(config: any): void; - /** - * Returns an object that can be consumed by a Worksheet/Export/Worker - * @returns {Object} - */ - exportData(): { - relations: any; - columnFormats: ExcelColumnFormat[]; - data: (string | number | boolean | Date | ExcelColumnMetadata | null)[][]; - columns: ExcelColumn[]; - mergedCells: string[][]; - _headers: [ - left?: any, - center?: any, - right?: any - ]; - _footers: [ - left?: any, - center?: any, - right?: any - ]; - _tables: Table[]; - _rowInstructions: any; - _freezePane: { - xSplit?: number; - ySplit?: number; - cell?: string; - }; - name: string; - id: string; - }; - /** - * Imports data - to be used while inside of a WorksheetExportWorker. - * @param {Object} data - */ - importData(data: any): void; - setSharedStringCollection(stringCollection: SharedStrings): void; - addTable(table: Table): void; - addDrawings(drawings: Drawings): void; - setRowInstructions(rowIndex: number, instructions: ExcelStyleInstruction): void; - /** - * Expects an array length of three. - * - * @see Excel/Worksheet compilePageDetailPiece - * @see Adding headers and footers to a worksheet - * - * @param {Array} headers [left, center, right] - */ - setHeader(headers: [ - left: any, - center: any, - right: any - ]): void; - /** - * Expects an array length of three. - * - * @see Excel/Worksheet compilePageDetailPiece - * @see Adding headers and footers to a worksheet - * - * @param {Array} footers [left, center, right] - */ - setFooter(footers: [ - left: any, - center: any, - right: any - ]): void; - /** - * Turns page header/footer details into the proper format for Excel. - * @param {type} data - * @returns {String} - */ - compilePageDetailPackage(data: any): string; - /** - * Turns instructions on page header/footer details into something - * usable by Excel. - * - * @param {type} data - * @returns {String|@exp;_@call;reduce} - */ - compilePageDetailPiece(data: string | CharType | any[]): any; - /** - * Creates the header node. - * - * @todo implement the ability to do even/odd headers - * @param {XML Doc} doc - * @returns {XML Node} - */ - exportHeader(doc: XMLDOM): XMLNode; - /** - * Creates the footer node. - * - * @todo implement the ability to do even/odd footers - * @param {XML Doc} doc - * @returns {XML Node} - */ - exportFooter(doc: XMLDOM): XMLNode; - /** - * This creates some nodes ahead of time, which cuts down on generation time due to - * most cell definitions being essentially the same, but having multiple nodes that need - * to be created. Cloning takes less time than creation. - * - * @private - * @param {XML Doc} doc - * @returns {_L8.Anonym$0._buildCache.Anonym$2} - */ - _buildCache(doc: XMLDOM): { - number: XMLNode; - date: XMLNode; - string: XMLNode; - formula: XMLNode; - }; - /** - * Runs through the XML document and grabs all of the strings that will - * be sent to the 'shared strings' document. - * - * @returns {Array} - */ - collectSharedStrings(): string[]; - toXML(): XMLDOM; - /** - * - * @param {XML Doc} doc - * @returns {XML Node} - */ - exportColumns(doc: XMLDOM): XMLNode; - /** - * Sets the page settings on a worksheet node. - * - * @param {XML Doc} doc - * @param {XML Node} worksheet - * @returns {undefined} - */ - exportPageSettings(doc: XMLDOM, worksheet: XMLNode): void; - /** - * Serialize a chunk of rows to XML. - */ - serializeRows(rows: any[]): string; - /** - * http://www.schemacentral.com/sc/ooxml/t-ssml_ST_Orientation.html - * - * Can be one of 'portrait' or 'landscape'. - * - * @param {'default' | 'portrait' | 'landscape'} orientation - * @returns {undefined} - */ - setPageOrientation(orientation: "default" | "portrait" | "landscape"): void; - /** - * Set page details in inches. - * use this structure: - * { - * top: 0.7 - * , bottom: 0.7 - * , left: 0.7 - * , right: 0.7 - * , header: 0.3 - * , footer: 0.3 - * } - * - * @returns {undefined} - */ - setPageMargin(input: ExcelMargin): void; - /** - * Expects an array of column definitions. Each column definition needs to have a width assigned to it. - * - * @param {Array} columns - */ - setColumns(columns: ExcelColumn[]): void; - /** - * Expects an array of data to be translated into cells. - * - * @param {Array} data Two dimensional array - [ [A1, A2], [B1, B2] ] - * @see Adding data to a worksheet - */ - setData(data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][]): void; - /** - * Merge cells in given range - * - * @param cell1 - A1, A2... - * @param cell2 - A2, A3... - */ - mergeCells(cell1: string, cell2: string): void; - /** - * Added frozen pane - * @param column - column number: 0, 1, 2 ... - * @param row - row number: 0, 1, 2 ... - * @param cell - 'A1' - * @deprecated - */ - freezePane(column: number, row: number, cell: string): void; - /** - * Expects an array containing an object full of column format definitions. - * http://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx - * bestFit - * collapsed - * customWidth - * hidden - * max - * min - * outlineLevel - * phonetic - * style - * width - * @param {Array} columnFormats - */ - setColumnFormats(columnFormats: ExcelColumnFormat[]): void; -} -export interface MediaMeta { - id: string; - data: string; - fileName: string; - contentType: string | null; - extension: string; - rId?: string; -} -/** - * @module Excel/Workbook - */ -export declare class Workbook { - id: string; - styleSheet: StyleSheet$1; - sharedStrings: SharedStrings; - relations: RelationshipManager; - worksheets: Worksheet[]; - tables: Table[]; - drawings: Drawings[]; - media: { - [filename: string]: MediaMeta; - }; - printTitles: any; - constructor(); - initialize(): void; - createWorksheet(config?: any): Worksheet; - getStyleSheet(): StyleSheet$1; - addTable(table: Table): void; - addDrawings(drawings: Drawings): void; - /** - * Set number of rows to repeat for this sheet. - * - * @param {String} sheet name - * @param {int} number of rows to repeat from the top - * @returns {undefined} - */ - setPrintTitleTop(inSheet: string, inRowCount: number): void; - /** - * Set number of rows to repeat for this sheet. - * - * @param {String} sheet name - * @param {int} number of columns to repeat from the left - * @returns {undefined} - */ - setPrintTitleLeft(inSheet: string, inRowCount: number): void; - addMedia(_type: string, fileName: string, fileData: any, contentType?: string | null): MediaMeta; - addWorksheet(worksheet: Worksheet): void; - createContentTypes(): XMLDOM; - toXML(): XMLDOM; - createWorkbookRelationship(): XMLDOM; - _generateCorePaths(files: any): void; - _prepareFilesForPackaging(files: { - [path: string]: XMLDOM | string; - }): void; - generateFiles(): Promise<{ - [path: string]: string; - }>; - serializeHeader(): string; - serializeFooter(): string; -} -export declare class Picture extends Drawing { - id: string; - pictureId: number; - fill: any; - mediaData: MediaMeta | null; - description: string; - constructor(); - setMedia(mediaRef: MediaMeta): void; - setDescription(description: string): void; - setFillType(type: string): void; - setFillConfig(config: any): void; - getMediaType(): keyof typeof Util.schemas; - getMediaData(): MediaMeta; - setRelationshipId(rId: string): void; - toXML(xmlDoc: XMLDOM): XMLNode; -} -/** - * This is mostly a global spot where all of the relationship managers can get and set - * path information from/to. - * @module Excel/Paths - */ -export declare const Paths: { - [path: string]: string; -}; -/** - * Converts pixel sizes to 'EMU's, which is what Open XML uses. - * - * @todo clean this up. Code borrowed from http://polymathprogrammer.com/2009/10/22/english-metric-units-and-open-xml/, - * but not sure that it's going to be as accurate as it needs to be. - * - * @param int pixels - * @returns int - */ -export declare class Positioning { - static pixelsToEMUs(pixels: number): number; -} -export type InferOutputByType = T extends "Blob" ? Blob : T extends "Uint8Array" ? Uint8Array : any; -/** - * Creates a new workbook. - */ -export declare function createWorkbook(): Workbook; -/** - * Turns a workbook into a downloadable file, you can between a 'Blob' or 'Uint8Array', - * and if nothing is provided then 'Blob' will be the default - * @param {Excel/Workbook} workbook - The workbook that is being converted - * @param {'Uint8Array' | 'Blob'} [outputType='Blob'] - defaults to 'Blob' - * @param {Object} [options] - * - `fileFormat` defaults to "xlsx" - * - `mimeType`: a mime type can be provided by the user or auto-detect the mime when undefined (by file extension .xls/.xlsx) - * (user can pass an empty string to completely cancel the mime type altogether) - * - `zipOptions` to specify any `fflate` options to modify how the zip is created. - * @returns {Promise} - */ -export declare function createExcelFile(workbook: Workbook, outputType?: T, options?: { - fileFormat?: "xls" | "xlsx"; - mimeType?: string; - zipOptions?: ZipOptions; -}): Promise>; -/** - * Download Excel file, currently only supports a "browser" as `downloadType` - * but it could be expended in the future to also other type of platform like NodeJS for example. - * @param {Workbook} workbook - * @param {String} filename - filename (must also include file extension, xls/xlsx) - * @param {Object} [options] - * - `downloadType`: download type (browser/node), currently only a "browser" download as a Blob - * - `mimeType`: a mime type can be provided by the user or auto-detect the mime when undefined (by file extension .xls/.xlsx) - * (user can pass an empty string to completely cancel the mime type altogether) - * - `zipOptions` to specify any `fflate` options to modify how the zip is created. - */ -export declare function downloadExcelFile(workbook: Workbook, filename: string, options?: { - downloadType?: "browser" | "node"; - mimeType?: string; - zipOptions?: ZipOptions; -}): Promise; -/** - * Async generator that yields zipped Excel file chunks. - * @param workbook Workbook instance - * @param options {chunkSize} Number of rows per chunk - */ -export declare function createExcelFileStream(workbook: Workbook, options?: { - chunkSize?: number; -}): AsyncGenerator | { - type: string; - name: string; - xml: string; -} | { - type: string; - xml: string; - name?: undefined; -}, void, unknown>; -/** - * Converts the characters "&", "<", ">", '"', and "'" in `string` to their - * corresponding HTML entities. - * - * **Note:** No other characters are escaped. To escape additional - * characters use a third-party library like [_he_](https://mths.be/he). - * - * Though the ">" character is escaped for symmetry, characters like - * ">" and "/" don't need escaping in HTML and have no special meaning - * unless they're part of a tag or unquoted attribute value. See - * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) - * (under "semi-related fun fact") for more details. - * - * When working with HTML you should always - * [quote attribute values](http://wonko.com/post/html-escaping) to reduce - * XSS vectors. - * - * @since 0.1.0 - * @category String - * @param {string} [str=''] The string to escape. - * @returns {string} Returns the escaped string. - * @see escapeRegExp, unescape - * @example - * - * escape('fred, barney, & pebbles') - * // => 'fred, barney, & pebbles' - */ -export declare const htmlEscape: (str: string) => string; -export declare function isObject(value: unknown): value is object; -export declare function isPlainObject(value: unknown): boolean; -export declare function isString(value: any): value is string; -export declare function pick(object: any, keys: string[]): any; -/** - * Generates a unique ID. If `prefix` is given, the ID is appended to it. - * - * @since 0.1.0 - * @category Util - * @param {string} [prefix=''] The value to prefix the ID with. - * @returns {string} Returns the unique ID. - * @see random - * @example - * - * uniqueId('contact_') - * // => 'contact_104' - * - * uniqueId() - * // => '105' - */ -export declare function uniqueId(prefix?: string): string; - -export { - StyleSheet$1 as StyleSheet, -}; - -export {}; diff --git a/packages/excel-builder-vanilla-types/dist/index.js b/packages/excel-builder-vanilla-types/dist/index.js deleted file mode 100644 index a726efc..0000000 --- a/packages/excel-builder-vanilla-types/dist/index.js +++ /dev/null @@ -1 +0,0 @@ -'use strict'; \ No newline at end of file diff --git a/packages/excel-builder-vanilla/src/Excel/Workbook.ts b/packages/excel-builder-vanilla/src/Excel/Workbook.ts index 22bf938..ad8b47e 100644 --- a/packages/excel-builder-vanilla/src/Excel/Workbook.ts +++ b/packages/excel-builder-vanilla/src/Excel/Workbook.ts @@ -346,7 +346,10 @@ export class Workbook { this._generateCorePaths(files); for (let i = 0, l = this.worksheets.length; i < l; i++) { - files[`/xl/worksheets/sheet${i + 1}.xml`] = this.worksheets[i].toXML(); + const xml = this.worksheets[i].toXML(); + // Log non-streaming worksheet XML for comparison + console.log(`--- Non-streaming Worksheet XML for sheet${i + 1} ---\n`, typeof xml === 'string' ? xml : xml.toString()); + files[`/xl/worksheets/sheet${i + 1}.xml`] = xml; Paths[this.worksheets[i].id] = `worksheets/sheet${i + 1}.xml`; files[`/xl/worksheets/_rels/sheet${i + 1}.xml.rels`] = this.worksheets[i].relations.toXML(); } diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index f008972..78548ca 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -649,38 +649,6 @@ export class Worksheet { * Returns worksheet XML header (everything before ) */ getWorksheetXmlHeader(): string { - // const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'worksheet'); - // const worksheet = doc.documentElement; - // worksheet.setAttribute('xmlns:r', Util.schemas.relationships); - // worksheet.setAttribute('xmlns:mc', Util.schemas.markupCompat); - - // let maxX = 0; - // const data = this.data; - // const columns = this.columns || []; - // for (let row = 0, l = data.length; row < l; row++) { - // const cellCount = data[row].length; - // maxX = cellCount > maxX ? cellCount : maxX; - // } - - // if (maxX !== 0) { - // worksheet.appendChild( - // Util.createElement(doc, 'dimension', [ - // ['ref', `${Util.positionToLetterRef(1, 1)}:${Util.positionToLetterRef(maxX, String(data.length))}`], - // ]), - // ); - // } else { - // worksheet.appendChild(Util.createElement(doc, 'dimension', [['ref', Util.positionToLetterRef(1, 1)]])); - // } - - // worksheet.appendChild(this.sheetView.exportXML(doc)); - - // if (this.columns.length) { - // worksheet.appendChild(this.exportColumns(doc)); - // } - - // // Add start tag - // const xml = doc.toString(); - // return xml.substring(0, xml.indexOf('') + ''.length); return ` ) */ getWorksheetXmlFooter(): string { - // const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'worksheet'); - // const worksheet = doc.documentElement; - - // // Add all elements after - // if (this.sheetProtection) { - // worksheet.appendChild(this.sheetProtection.exportXML(doc)); - // } - - // if (this.hyperlinks.length > 0) { - // const hyperlinksEl = doc.createElement('hyperlinks'); - // const hyperlinks = this.hyperlinks; - // for (let i = 0, l = hyperlinks.length; i < l; i++) { - // const hyperlinkEl = doc.createElement('hyperlink'); - // const hyperlink: any = hyperlinks[i]; - // hyperlinkEl.setAttribute('ref', String(hyperlink.cell)); - // hyperlink.id = Util.uniqueId('hyperlink'); - // this.relations.addRelation( - // { - // id: hyperlink.id, - // target: hyperlink.location, - // targetMode: hyperlink.targetMode || 'External', - // }, - // 'hyperlink', - // ); - // hyperlinkEl.setAttribute('r:id', this.relations.getRelationshipId(hyperlink)); - // hyperlinksEl.appendChild(hyperlinkEl); - // } - // worksheet.appendChild(hyperlinksEl); - // } - - // if (this.mergedCells.length > 0) { - // const mergeCells = doc.createElement('mergeCells'); - // for (let i = 0, l = this.mergedCells.length; i < l; i++) { - // const mergeCell = doc.createElement('mergeCell'); - // mergeCell.setAttribute('ref', `${this.mergedCells[i][0]}:${this.mergedCells[i][1]}`); - // mergeCells.appendChild(mergeCell); - // } - // worksheet.appendChild(mergeCells); - // } - - // this.exportPageSettings(doc, worksheet); - - // if (this._headers.length > 0 || this._footers.length > 0) { - // const headerFooter = doc.createElement('headerFooter'); - // if (this._headers.length > 0) { - // headerFooter.appendChild(this.exportHeader(doc)); - // } - // if (this._footers.length > 0) { - // headerFooter.appendChild(this.exportFooter(doc)); - // } - // worksheet.appendChild(headerFooter); - // } - - // for (let i = 0, l = this._drawings.length; i < l; i++) { - // const drawing = doc.createElement('drawing'); - // drawing.setAttribute('r:id', this.relations.getRelationshipId(this._drawings[i])); - // worksheet.appendChild(drawing); - // } - - // if (this._tables.length > 0) { - // const tables = doc.createElement('tableParts'); - // tables.setAttribute('count', this._tables.length); - // for (let i = 0, l = this._tables.length; i < l; i++) { - // const table = doc.createElement('tablePart'); - // table.setAttribute('r:id', this.relations.getRelationshipId(this._tables[i])); - // tables.appendChild(table); - // } - // worksheet.appendChild(tables); - // } - - // // Get everything after - // const xml = doc.toString(); - // return xml.substring(xml.indexOf('') + ''.length); return ''; } @@ -772,14 +667,13 @@ export class Worksheet { * Serialize a chunk of rows to XML (same logic as in toXML) */ serializeRows(rows: (number | string | boolean | Date | null | ExcelColumnMetadata)[][], startRow = 0): string { - const columns = this.columns || []; let xml = ''; for (let row = 0, l = rows.length; row < l; row++) { const dataRow = rows[row]; const cellCount = dataRow.length; let rowXml = ``; for (let c = 0; c < cellCount; c++) { - let cellValue = dataRow[c]; + const cellValue = dataRow[c]; let cellType: any = typeof cellValue; // Always treat first row as text if (startRow + row === 0) { diff --git a/packages/excel-builder-vanilla/src/streaming.ts b/packages/excel-builder-vanilla/src/streaming.ts index 1ea69f6..7cafbba 100644 --- a/packages/excel-builder-vanilla/src/streaming.ts +++ b/packages/excel-builder-vanilla/src/streaming.ts @@ -1,25 +1,90 @@ -import { strToU8, zip } from 'fflate'; - +import { strToU8, zipSync } from 'fflate'; import type { Workbook } from './Excel/Workbook.js'; import { base64ToUint8Array } from './factory.js'; export interface ExcelFileStreamOptions { chunkSize?: number; - outputType?: 'Uint8Array' | 'Blob'; + outputType?: 'Uint8Array' | 'Blob' | 'stream'; fileFormat?: 'xlsx' | 'xls'; mimeType?: string; } /** - * Async generator that yields zipped Excel file chunks. - * @param workbook Workbook instance - * @param options {chunkSize} Number of rows per chunk + * Environment-aware streaming Excel file generator. + * Yields zipped chunks for browser (ReadableStream) or NodeJS (async generator). + */ +export function createExcelFileStream(workbook: Workbook, options?: ExcelFileStreamOptions) { + const isBrowser = typeof window !== 'undefined' && typeof window.ReadableStream !== 'undefined'; + const isNode = typeof process !== 'undefined' && process.versions?.node; + if (isBrowser) { + return browserExcelStream(workbook, options); + } + if (isNode) { + return nodeExcelStream(workbook, options); + } + throw new Error('Streaming is only supported in browser or NodeJS environments.'); +} + +/** + * Browser: returns a ReadableStream of zipped Excel file chunks. */ -export async function* createExcelFileStream(workbook: Workbook, options?: ExcelFileStreamOptions) { +function browserExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { const chunkSize = options?.chunkSize ?? 1000; - const files = await workbook.generateFiles(); + const stream = new ReadableStream({ + async start(controller) { + const files = await workbook.generateFiles(); + for (let i = 0; i < workbook.worksheets.length; i++) { + const worksheet = workbook.worksheets[i]; + let worksheetXml = ''; + worksheetXml += worksheet.getWorksheetXmlHeader(); + let rowIndex = 0; + const totalRows = worksheet.data.length; + while (rowIndex < totalRows) { + const rowsChunk = worksheet.data.slice(rowIndex, rowIndex + chunkSize); + worksheetXml += worksheet.serializeRows(rowsChunk, rowIndex); + rowIndex += chunkSize; + await new Promise(r => setTimeout(r, 0)); + } + worksheetXml += ''; + worksheetXml += worksheet.getWorksheetXmlFooter(); + worksheetXml += ''; + // Ensure footer does NOT include tag, only append it once here + // If getWorksheetXmlFooter() includes , remove it from that method + const wsPath = `/xl/worksheets/sheet${i + 1}.xml`; + files[wsPath] = worksheetXml; + } + // Convert files to Uint8Array + const zipObj: { [name: string]: Uint8Array } = {}; + for (const [path, content] of Object.entries(files)) { + const outPath = path.startsWith('/') ? path.substr(1) : path; + if (path.indexOf('.xml') !== -1 || path.indexOf('.rel') !== -1) { + zipObj[outPath] = strToU8(String(content)); + } else { + zipObj[outPath] = base64ToUint8Array(String(content)); + } + } + // Synchronous zip for browser, split into chunks + const zipped: Uint8Array = zipSync(zipObj); + const chunkByteSize = 64 * 1024; // 64KB per chunk + let offset = 0; + while (offset < zipped.length) { + const chunk = zipped.subarray(offset, offset + chunkByteSize); + controller.enqueue(chunk); + offset += chunkByteSize; + await new Promise(r => setTimeout(r, 0)); + } + controller.close(); + }, + }); + return stream; +} - // Replace worksheet XML with streamed version +/** + * NodeJS: returns an async generator yielding zipped Excel file chunks. + */ +async function* nodeExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { + const chunkSize = options?.chunkSize ?? 1000; + const files = await workbook.generateFiles(); for (let i = 0; i < workbook.worksheets.length; i++) { const worksheet = workbook.worksheets[i]; let worksheetXml = ''; @@ -35,41 +100,29 @@ export async function* createExcelFileStream(workbook: Workbook, options?: Excel worksheetXml += ''; worksheetXml += worksheet.getWorksheetXmlFooter(); worksheetXml += ''; - - // Use the same path as generateFiles + // Ensure footer does NOT include tag, only append it once here + // If getWorksheetXmlFooter() includes , remove it from that method const wsPath = `/xl/worksheets/sheet${i + 1}.xml`; files[wsPath] = worksheetXml; } - // Convert files to Uint8Array const zipObj: { [name: string]: Uint8Array } = {}; for (const [path, content] of Object.entries(files)) { const outPath = path.startsWith('/') ? path.substr(1) : path; if (path.indexOf('.xml') !== -1 || path.indexOf('.rel') !== -1) { - zipObj[outPath] = strToU8(content); + zipObj[outPath] = strToU8(String(content)); } else { - zipObj[outPath] = base64ToUint8Array(content); + zipObj[outPath] = base64ToUint8Array(String(content)); } } - - // Zip and yield - const zipped: Uint8Array = await new Promise((resolve, reject) => { - zip(zipObj, (err, data) => { - if (err) reject(err); - else resolve(data); - }); - }); - - const outputType = options?.outputType ?? 'Uint8Array'; - if (outputType === 'Uint8Array') { - yield zipped; - } else { - const format = options?.fileFormat ?? 'xlsx'; - let mimeType = options?.mimeType; - if (mimeType === undefined) { - mimeType = format === 'xls' ? 'application/vnd.ms-excel' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - } - const arrayBuffer = zipped.buffer.slice(zipped.byteOffset, zipped.byteOffset + zipped.byteLength); - yield new Blob([arrayBuffer as BlobPart], { type: mimeType }); + // Synchronous zip for Node, split into chunks + const zipped: Uint8Array = zipSync(zipObj); + const chunkByteSize = 64 * 1024; // 64KB per chunk + let offset = 0; + while (offset < zipped.length) { + const chunk = zipped.subarray(offset, offset + chunkByteSize); + yield chunk; + offset += chunkByteSize; + await new Promise(r => setTimeout(r, 0)); } } From 30b4be5fc7bc118b11b6ea4785f6f3515bbb5543 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 18:18:46 -0400 Subject: [PATCH 03/16] chore: add a fully working Streaming API with new Examples --- .gitignore | 5 +- docs/TOC.md | 3 +- docs/streaming.md | 64 + docs/workbook-create.md | 17 + docs/worksheet-add-data.md | 22 + docs/worksheet-create.md | 22 + docs/worksheet-headers-footers.md | 24 + .../examples/example15-node-streaming.mjs | 51 + packages/demo/package.json | 3 +- packages/demo/src/app-routing.ts | 2 + packages/demo/src/examples/example09.html | 8 +- packages/demo/src/examples/example15.html | 95 +- packages/demo/src/examples/example15.ts | 56 +- packages/demo/src/examples/example16.html | 123 ++ packages/demo/src/examples/example16.scss | 23 + packages/demo/src/examples/example16.ts | 140 ++ .../dist/index.d.ts | 1132 +++++++++++++++++ .../excel-builder-vanilla-types/dist/index.js | 1 + .../src/Excel/Workbook.ts | 2 - .../src/Excel/Worksheet.ts | 17 +- .../excel-builder-vanilla/src/streaming.ts | 51 +- 21 files changed, 1778 insertions(+), 83 deletions(-) create mode 100644 docs/streaming.md create mode 100644 packages/demo/examples/example15-node-streaming.mjs create mode 100644 packages/demo/src/examples/example16.html create mode 100644 packages/demo/src/examples/example16.scss create mode 100644 packages/demo/src/examples/example16.ts create mode 100644 packages/excel-builder-vanilla-types/dist/index.d.ts create mode 100644 packages/excel-builder-vanilla-types/dist/index.js diff --git a/.gitignore b/.gitignore index c95225c..d783913 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,7 @@ tsconfig.tsbuildinfo # Playwright playwright-report test-results -/playwright/.cache \ No newline at end of file +/playwright/.cache + +# Ignore temp folder for streaming demo +**/temp/ \ No newline at end of file diff --git a/docs/TOC.md b/docs/TOC.md index 5a1278f..2a8c17e 100644 --- a/docs/TOC.md +++ b/docs/TOC.md @@ -18,4 +18,5 @@ - [Theming Tables](theming-tables.md) - [Tables Summaries](tables-summaries.md) - [Adding Headers and Footers to a Worksheet](worksheet-headers-footers.md) -- [Inserting images into spreadsheets](inserting-pictures.md) + - [Inserting images into spreadsheets](inserting-pictures.md) + - [Streaming Excel Export](streaming.md) diff --git a/docs/streaming.md b/docs/streaming.md new file mode 100644 index 0000000..0683cd0 --- /dev/null +++ b/docs/streaming.md @@ -0,0 +1,64 @@ +# Streaming Excel Export + +Streaming export is designed for large datasets, providing better performance and memory efficiency in both browser and NodeJS environments. The API and features are the same as the regular export and the features like formulas, alignment, borders, and more are all supported. + +## Why Streaming? + +Traditional export methods generate the entire Excel file in memory, which can hang the browser or consume excessive resources for large datasets. Streaming solves this by generating and delivering the file in chunks. + +## Usage in the Browser + +Use `createExcelFileStream` to export data as a stream. You can process chunks and update progress as needed. + +```ts +import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +const worksheet = workbook.createWorksheet({ name: 'Demo' }); +worksheet.setData([ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], + // ... more rows +]); +workbook.addWorksheet(worksheet); + +const stream = createExcelFileStream(workbook, { chunkSize: 1000 }); +const chunks: Uint8Array[] = []; +for await (const chunk of stream as AsyncIterable) { + chunks.push(chunk); + // Optionally update progress bar here +} +const blob = new Blob(chunks, { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); +const url = URL.createObjectURL(blob); +// Download with anchor tag +``` + +## Usage in NodeJS + +Streaming in NodeJS works similarly, but you can pipe the output directly to a file stream. + +```js +import fs from 'node:fs'; +import { createWorkbook, createExcelFileStream } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +// ... add data and worksheets + +const output = fs.createWriteStream('output.xlsx'); +for await (const chunk of createExcelFileStream(workbook, { chunkSize: 1000 })) { + output.write(chunk); +} +output.end(); +``` + +## Supported Features + +All features such as formulas, alignment, borders, styles, and images work with streaming export. The only difference is how the file is delivered. + +## See Also + +- [Formulas](formulas.md) +- [Alignment](alignment.md) +- [Borders](fonts-and-colors.md) +- [Tables](tables.md) +- [Headers/Footers](worksheet-headers-footers.md) diff --git a/docs/workbook-create.md b/docs/workbook-create.md index 2da9e06..21c01d5 100644 --- a/docs/workbook-create.md +++ b/docs/workbook-create.md @@ -21,3 +21,20 @@ const workbook = new Workbook(); This will eventually require you to include the 'excel-builder' module so you can export the workbook, so it's more verbose. However, this is also the best option for creating templates and the like. Workbooks with no worksheet (i.e. data) will build, but Excel will throw an error while attempting to open it. + +--- + +## NodeJS Usage Example + +You can use excel-builder-vanilla in NodeJS to generate and save Excel files directly to disk: + +```js +import fs from 'node:fs'; +import { createWorkbook, createExcelFile } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +// ... add worksheets and data + +const buffer = createExcelFile(workbook); +fs.writeFileSync('output.xlsx', buffer); +``` \ No newline at end of file diff --git a/docs/worksheet-add-data.md b/docs/worksheet-add-data.md index 86ef988..31c388a 100644 --- a/docs/worksheet-add-data.md +++ b/docs/worksheet-add-data.md @@ -25,3 +25,25 @@ artistWorkbook.addWorksheet(albumList); const data = createExcelFile(artistWorkbook); downloader('Artist WB.xlsx', data); ``` + +--- + +## NodeJS Usage Example + +You can add data to a worksheet and export in NodeJS: + +```js +import fs from 'node:fs'; +import { createWorkbook, createExcelFile } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +const sheet = workbook.createWorksheet({ name: 'Demo' }); +sheet.setData([ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], +]); +workbook.addWorksheet(sheet); + +const buffer = createExcelFile(workbook); +fs.writeFileSync('output.xlsx', buffer); +``` \ No newline at end of file diff --git a/docs/worksheet-create.md b/docs/worksheet-create.md index 5b96a63..75ca4a0 100644 --- a/docs/worksheet-create.md +++ b/docs/worksheet-create.md @@ -39,3 +39,25 @@ This will set the 'name' of the worksheet to 'Account Summary' so it doesn't sho ```ts const accountSummarySheet = workbook.createWorksheet({ name: 'Account Summary' }); ``` + +--- + +## NodeJS Usage Example + +Worksheets can be created and exported in NodeJS just like in the browser: + +```js +import fs from 'node:fs'; +import { createWorkbook, createExcelFile } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +const sheet = workbook.createWorksheet({ name: 'Demo' }); +sheet.setData([ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], +]); +workbook.addWorksheet(sheet); + +const buffer = createExcelFile(workbook); +fs.writeFileSync('output.xlsx', buffer); +``` \ No newline at end of file diff --git a/docs/worksheet-headers-footers.md b/docs/worksheet-headers-footers.md index b8035c8..21b34ef 100644 --- a/docs/worksheet-headers-footers.md +++ b/docs/worksheet-headers-footers.md @@ -49,3 +49,27 @@ artistWorkbook.addWorksheet(albumList); const data = createExcelFile(artistWorkbook); downloader('Artist WB.xlsx', data); ``` + +--- + +## NodeJS Usage Example + +Headers and footers work in NodeJS as well: + +```js +import fs from 'node:fs'; +import { createWorkbook, createExcelFile } from 'excel-builder-vanilla'; + +const workbook = createWorkbook(); +const sheet = workbook.createWorksheet({ name: 'Demo' }); +sheet.setData([ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], +]); +sheet.setHeader(['Left', 'Center', 'Right']); +sheet.setFooter(['Date: &D', '&A', 'Page &P of &N']); +workbook.addWorksheet(sheet); + +const buffer = createExcelFile(workbook); +fs.writeFileSync('output.xlsx', buffer); +``` \ No newline at end of file diff --git a/packages/demo/examples/example15-node-streaming.mjs b/packages/demo/examples/example15-node-streaming.mjs new file mode 100644 index 0000000..0c22070 --- /dev/null +++ b/packages/demo/examples/example15-node-streaming.mjs @@ -0,0 +1,51 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createExcelFileStream, createWorkbook } from 'excel-builder-vanilla'; + +// Build data array (same as browser example) +const ROWS = 1000; +const dataArray = []; + +// Add header row at row 0, merged and styled +const workbook = createWorkbook(); +const worksheet = workbook.createWorksheet({ name: 'Demo Streaming' }); + +// Create a format for the header row +const stylesheet = workbook.getStyleSheet(); +const headerFormat = stylesheet.createFormat({ + alignment: { horizontal: 'center' }, + font: { bold: true, color: 'FF2b995d', size: 13 }, +}); + +dataArray.push([{ value: 'NodeJS Streaming Output', metadata: { style: headerFormat.id } }]); +dataArray.push(['ID', 'Name', 'Score']); +for (let i = 1; i <= ROWS; i++) { + dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]); +} +dataArray.push(['', 'Total', '=SUM(C2:C1001)']); + +worksheet.setData(dataArray); +worksheet.mergeCells('A1', 'C1'); +workbook.addWorksheet(worksheet); + +// Ensure temp folder exists +const tempDir = path.resolve(process.cwd(), 'temp'); +if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); +} +const outputPath = path.join(tempDir, 'node-streaming-example15.xlsx'); +const output = fs.createWriteStream(outputPath); + +(async () => { + for await (const chunk of createExcelFileStream(workbook, { + zipOptions: {}, + outputType: 'Uint8Array', + fileFormat: 'xlsx', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + downloadType: 'node', + })) { + output.write(chunk); + } + output.end(); + console.log(`Excel file written to ${outputPath}`); +})(); diff --git a/packages/demo/package.json b/packages/demo/package.json index 655b46a..1058c5c 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "stream:excel": "node ./examples/example15-node-streaming.mjs" }, "dependencies": { "@excel-builder-vanilla/types": "workspace:*", diff --git a/packages/demo/src/app-routing.ts b/packages/demo/src/app-routing.ts index 775db44..4457d8a 100644 --- a/packages/demo/src/app-routing.ts +++ b/packages/demo/src/app-routing.ts @@ -13,6 +13,7 @@ import Example12 from './examples/example12.js'; import Example13 from './examples/example13.js'; import Example14 from './examples/example14.js'; import Example15 from './examples/example15.js'; +import Example16 from './examples/example16.js'; import GettingStarted from './getting-started.js'; export const navbarRouting = [ @@ -44,6 +45,7 @@ export const exampleRouting = [ { name: 'example13', view: '/src/examples/example13.html', viewModel: Example13, title: '13- Pictures with 2 anchors' }, { name: 'example14', view: '/src/examples/example14.html', viewModel: Example14, title: '14- Pictures with different anchors' }, { name: 'example15', view: '/src/examples/example15.html', viewModel: Example15, title: '15- Streaming Excel Export' }, + { name: 'example16', view: '/src/examples/example16.html', viewModel: Example16, title: '16- Streaming Features Demo' }, ], }, ]; diff --git a/packages/demo/src/examples/example09.html b/packages/demo/src/examples/example09.html index 3661b2d..bf795da 100644 --- a/packages/demo/src/examples/example09.html +++ b/packages/demo/src/examples/example09.html @@ -19,10 +19,10 @@

- Tables are a feature that is apparently new to Office 2007+, with a comparable feature called a listin 2003 and below. - Basically, by putting data in a table, it gives the user some ways to filter and sort the data through UI. There are also some - formula benefits. Creating a table takes a few extra steps, mostly because of how a table's definition is really detached from a - worksheet. + Tables are a feature that is apparently new to Office 2007+, with a comparable feature called a list + in 2003 and below. Basically, by putting data in a table, it gives the user some ways to filter and sort the data through UI. There + are also some formula benefits. Creating a table takes a few extra steps, mostly because of how a table's definition is really + detached from a worksheet.
diff --git a/packages/demo/src/examples/example15.html b/packages/demo/src/examples/example15.html index 74ad083..ca731b1 100644 --- a/packages/demo/src/examples/example15.html +++ b/packages/demo/src/examples/example15.html @@ -1,9 +1,13 @@
-

Example 15: Streaming Excel Export (100,000 rows)

+

+ Example 15: Streaming Excel Export (100,000 rows) +

- This example demonstrates streaming export for large datasets using createExcelFileStream. Progress is shown below. + For large datasets, streaming export is significantly more performant and memory-efficient than non-streaming export. This example + demonstrates streaming using createExcelFileStream. The export also includes Header & Footer. Export progress is + shown below.
@@ -14,4 +18,91 @@

Example 15: Streaming Excel Export (100,000 rows)

+ +
+ Header +
+
+
+ + + + + + + +
This will be on the left + In the middle I shall be + Right, underlined and size of 16
+ +
+ Body +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArtistAlbumPrice
Artist 1Album 1$8.99
Artist 2Album 2$13.99
Artist 3Album 3$11.34
Artist 4Album 4$10.54
Artist 5Album 5$10.64
Artist 6Album 6$8.99
Artist 7Album 7$9.99
Artist 8Album 8$12.49
Artist 9Album 9$7.99
Artist 10Album 10$15.00
.........
+ diff --git a/packages/demo/src/examples/example15.ts b/packages/demo/src/examples/example15.ts index 26d4a61..e9fe42b 100644 --- a/packages/demo/src/examples/example15.ts +++ b/packages/demo/src/examples/example15.ts @@ -1,4 +1,6 @@ -import { createExcelFileStream, createWorkbook } from 'excel-builder-vanilla'; +import { createExcelFileStream, createWorkbook, type ExcelColumnMetadata } from 'excel-builder-vanilla'; + +const ROWS = 100_000; export default class Example { exportBtnElm!: HTMLButtonElement; @@ -15,15 +17,38 @@ export default class Example { } async startProcess() { - const ROWS = 100_000; - const originalData: (number | string | boolean | Date | null)[][] = [['Artist', 'Album', 'Price']]; + const originalData: (number | string | boolean | Date | null | ExcelColumnMetadata)[][] = [ + ['Artist', 'Album', { value: 'Price', metadata: {} }], + ]; for (let i = 0; i < ROWS; i++) { - originalData.push([`Artist ${i}`, `Album ${i}`, Math.round(Math.random() * 10000) / 100]); + const price = Math.round(Math.random() * 10000) / 100; + originalData.push([`Artist ${i}`, `Album ${i}`, { value: price, metadata: {} }]); } const artistWorkbook = createWorkbook(); const albumList = artistWorkbook.createWorksheet({ name: 'Artists' }); + // Apply currency format for Price column + const stylesheet = artistWorkbook.getStyleSheet(); + const currencyFormat = stylesheet.createFormat({ format: '$#,##0.00' }); + // Update header to use currency style + const headerCell = originalData[0][2]; + if (typeof headerCell === 'object' && headerCell !== null && 'metadata' in headerCell && headerCell.metadata) { + headerCell.metadata.style = currencyFormat.id; + } + // Update all rows to use currency style for Price + for (let i = 1; i < originalData.length; i++) { + const cell = originalData[i][2]; + if (typeof cell === 'object' && cell !== null && 'metadata' in cell && cell.metadata) { + cell.metadata.style = currencyFormat.id; + } + } albumList.setData(originalData); + albumList.setHeader([ + 'This will be on the left', + ['In the middle ', { text: 'I shall be', bold: true }], + { text: 'Right, underlined and size of 16', font: 16, underline: true }, + ]); + albumList.setFooter(['Date of print: &D &T', '&A', 'Page &P of &N']); artistWorkbook.addWorksheet(albumList); // Streaming export @@ -31,24 +56,11 @@ export default class Example { const chunks: Uint8Array[] = []; let processed = 0; - if (typeof window !== 'undefined' && stream && typeof stream.getReader === 'function') { - // Browser: ReadableStream - const reader = stream.getReader(); - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - processed += value.length; - const rowsExported = Math.floor(processed / (chunks.length > 0 ? chunks[0].length : 1)) * 1000; - this.progressElm.textContent = `Exported ~${rowsExported} / ${ROWS} rows...`; - } - } else { - // Node/fallback: async generator - for await (const chunk of stream) { - chunks.push(chunk); - processed += 1000; - this.progressElm.textContent = `Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`; - } + // Use async iterator for both browser and Node + for await (const chunk of stream as AsyncIterable) { + chunks.push(chunk); + processed += 1000; + this.progressElm.textContent = `Exported ${Math.min(processed, ROWS)} / ${ROWS} rows...`; } // Combine chunks and trigger download diff --git a/packages/demo/src/examples/example16.html b/packages/demo/src/examples/example16.html new file mode 100644 index 0000000..e1b4f33 --- /dev/null +++ b/packages/demo/src/examples/example16.html @@ -0,0 +1,123 @@ +
+

+ Example 16: Streaming Features Demo (50,000 rows) +

+
merged Example 02-08
+
+ This demo showcases merged features: merged header, row height, styles, fonts, colors, borders, number/date formatting alignment and + formulas (from Example 02-08 but using Streaming Export). +
+ + +
+
0%
+
+

Excel Output Preview

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Merged Header
ArtistAlbum (hidden column)PriceQuantityTotal
Artist 1Album 1$8.995$44.95
Artist 2Album 2$13.997$97.93
Artist 3Album 3$11.349$102.06
Artist 4Album 4$10.543$31.62
Artist 5Album 5$10.641$10.64
Artist 6Album 6$8.9956$503.44
Artist 7Album 7$9.992$19.98
Artist 8Album 8$12.494$49.96
Artist 9Album 9$7.998$63.92
Artist 10Album 10$15.003$45.00
...............
+
+
diff --git a/packages/demo/src/examples/example16.scss b/packages/demo/src/examples/example16.scss new file mode 100644 index 0000000..b7bf3cd --- /dev/null +++ b/packages/demo/src/examples/example16.scss @@ -0,0 +1,23 @@ +/* Merged styles from example02.scss to example06.scss */ + +.export-btn { + background: #2b995d; + color: #fff; + font-weight: bold; + border-radius: 4px; + padding: 0.5em 1em; + border: none; + cursor: pointer; +} + +.export-btn:hover { + background: #237a4a; +} + +.progress { + margin-top: 1em; + font-size: 1.1em; + color: #2b995d; +} + +/* Add any other useful styles from example02-06 as needed */ diff --git a/packages/demo/src/examples/example16.ts b/packages/demo/src/examples/example16.ts new file mode 100644 index 0000000..deb11d3 --- /dev/null +++ b/packages/demo/src/examples/example16.ts @@ -0,0 +1,140 @@ +import { createExcelFileStream, createWorkbook, type ExcelColumnMetadata } from 'excel-builder-vanilla'; + +import './example16.scss'; + +const ROWS = 50_000; + +export default class Example { + exportBtnElm!: HTMLButtonElement; + + mount() { + this.exportBtnElm = document.querySelector('#export') as HTMLButtonElement; + this.exportBtnElm.addEventListener('click', this.startProcess.bind(this)); + } + + unmount() { + // remove event listeners to avoid DOM leaks + this.exportBtnElm.removeEventListener('click', this.startProcess.bind(this)); + } + + startProcess() { + const artistWorkbook = createWorkbook(); + const albumList = artistWorkbook.createWorksheet({ name: 'Album List' }); + + // Merged header row with style + albumList.mergeCells('A1', 'D1'); + const stylesheet = artistWorkbook.getStyleSheet(); + const header = stylesheet.createFormat({ + alignment: { horizontal: 'center' }, + font: { bold: true, color: 'FF2b995d', size: 13 }, + }); + + // Row height and style + const boldRow = stylesheet.createFormat({ font: { italic: true, underline: true } }); + albumList.setRowInstructions(2, { height: 40, style: boldRow.id }); + + // Fonts, colors, borders + const red = 'FFFF0000'; + const importantFormatter = stylesheet.createFormat({ + font: { bold: true, color: red }, + border: { + bottom: { color: red, style: 'thin' }, + top: { color: red, style: 'thin' }, + left: { color: red, style: 'thin' }, + right: { color: red, style: 'dotted' }, + }, + }); + const themeColor = stylesheet.createFormat({ font: { bold: true, color: { theme: 3 } } }); + + // Number/date formatting + const currency = stylesheet.createFormat({ format: '$#,##0.00' }); + + // Alignment + const centerAlign = stylesheet.createFormat({ alignment: { horizontal: 'center' } }); + + // Build large random dataset for export only, asynchronously + const originalData: (number | string | boolean | Date | null | ExcelColumnMetadata)[][] = [ + [{ value: 'Merged Header', metadata: { style: header.id } }, '', '', '', '', ''], + [ + { value: 'Artist', metadata: { style: importantFormatter.id } }, + { value: 'Album', metadata: { style: themeColor.id } }, + { value: 'Price', metadata: { style: themeColor.id } }, + { value: 'Quantity', metadata: { style: themeColor.id } }, + { value: 'Total', metadata: { style: themeColor.id } }, + ], + ]; + async function generateDataAsync() { + const batchSize = 2000; + for (let i = 0; i < ROWS; i += batchSize) { + for (let j = 0; j < batchSize && i + j < ROWS; j++) { + const idx = i + j; + const artist = `Artist ${idx + 1}`; + const album = `Album ${idx + 1}`; + const price = Math.round(Math.random() * 10000) / 100; + const quantity = Math.floor(Math.random() * 10) + 1; + // Excel formula for Total: =C{row}+D{row} + const rowNum = idx + 3; // +3 for header rows + originalData.push([ + { value: artist, metadata: { style: centerAlign.id } }, + { value: album, metadata: { style: centerAlign.id } }, + { value: price, metadata: { style: currency.id } }, + { value: quantity, metadata: { style: centerAlign.id } }, + { value: `C${rowNum}*D${rowNum}`, metadata: { type: 'formula', style: currency.id } }, + ]); + } + await new Promise(requestAnimationFrame); + } + } + + (async () => { + // Reset progress bar at the start of export, with a small delay for UI + const progressElm = document.getElementById('progress') as HTMLDivElement; + const progressBar = progressElm ? (progressElm.querySelector('.progress-bar') as HTMLDivElement) : null; + if (progressElm && progressBar) { + progressBar.style.width = '0%'; + progressBar.textContent = ''; + progressElm.setAttribute('aria-valuenow', '0'); + await new Promise(resolve => setTimeout(resolve, 50)); + } + + // Generate data asynchronously + await generateDataAsync(); + albumList.setData(originalData); + albumList.setColumns([{ width: 30 }, { width: 20 }, { width: 10 }, { width: 10 }, { width: 15 }]); + artistWorkbook.addWorksheet(albumList); + + // Streaming export with progress bar + const stream = createExcelFileStream(artistWorkbook, { chunkSize: 10 }); + const chunks: Uint8Array[] = []; + let processed = 0; + const totalRows = ROWS; + + for await (const chunk of stream as AsyncIterable) { + chunks.push(chunk); + processed += chunk.length; + if (progressElm && progressBar) { + const percent = Math.min((processed / totalRows) * 100, 100); + progressBar.style.width = `${percent}%`; + progressElm.setAttribute('aria-valuenow', percent.toString()); + progressBar.textContent = `${percent.toFixed(1)}%`; + void progressBar.offsetWidth; + } + // Artificial delay for demo purposes ONLY. Remove this in production for best performance. + // In a real implementation, use: await new Promise(requestAnimationFrame); + await new Promise(resolve => setTimeout(resolve, 30)); + } + // Combine chunks and trigger download + const blob = new Blob( + chunks.map(chunk => chunk.slice()), + { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + ); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'Artist WB - Streaming Features.xlsx'; + a.click(); + URL.revokeObjectURL(url); + // Do not reset progress bar after export; leave at 100% until next export + })(); + } +} diff --git a/packages/excel-builder-vanilla-types/dist/index.d.ts b/packages/excel-builder-vanilla-types/dist/index.d.ts new file mode 100644 index 0000000..985173f --- /dev/null +++ b/packages/excel-builder-vanilla-types/dist/index.d.ts @@ -0,0 +1,1132 @@ +// Generated by dts-bundle-generator v9.5.1 + +import { ZipOptions } from 'fflate'; + +export type XMLNodeOption = { + attributes?: { + [key: string]: any; + }; + children?: XMLNode[]; + nodeName: string; + nodeValue?: string; + type?: string; +}; +export declare class XMLDOM { + documentElement: XMLNode; + constructor(ns: string | null, rootNodeName: string); + createElement(name: string): XMLNode; + createTextNode(text: string): TextNode; + toString(): string; + static Node: { + Create: (config: any) => XMLNode | TextNode | null; + }; +} +declare class TextNode { + nodeValue: any; + constructor(text: string); + toJSON(): { + nodeValue: any; + type: string; + }; + toString(): string; +} +export declare class XMLNode { + nodeName: string; + children: XMLNode[]; + nodeValue: string; + attributes: { + [key: string]: any; + }; + firstChild?: XMLNode; + constructor(config: XMLNodeOption); + toString(): string; + toJSON(): { + nodeName: string; + children: any[]; + nodeValue: string; + attributes: { + [key: string]: any; + }; + type: string; + }; + setAttribute(name: string, val: any): void; + appendChild(child: any): void; + cloneNode(_deep?: boolean): XMLNode; +} +/** + * + * @param {Object} config + * @param {Number} config.x The cell column number that the top left of the picture will start in + * @param {Number} config.y The cell row number that the top left of the picture will start in + * @param {Number} config.width Width in EMU's + * @param {Number} config.height Height in EMU's + * @constructor + */ +export declare class OneCellAnchor { + x: number | null; + y: number | null; + xOff: boolean | null; + yOff: boolean | null; + width: number | null; + height: number | null; + constructor(config: AnchorOption); + setPos(x: number, y: number, xOff?: boolean, yOff?: boolean): void; + setDimensions(width: number, height: number): void; + toXML(xmlDoc: XMLDOM, content: any): XMLNode; +} +export declare class TwoCellAnchor { + from: any; + to: any; + constructor(config: DualAnchorOption); + setFrom(x: number, y: number, xOff?: boolean, yOff?: boolean): void; + setTo(x: number, y: number, xOff?: boolean, yOff?: boolean): void; + toXML(xmlDoc: XMLDOM, content: any): XMLNode; +} +export interface AnchorOption { + /** X offset in EMU's */ + x: number; + /** Y offset in EMU's */ + y: number; + xOff?: boolean; + yOff?: boolean; + /** Width in EMU's */ + height: number; + /** Height in EMU's */ + width: number; + drawing?: Drawing; +} +export interface DualAnchorOption { + to: AnchorOption; + from: AnchorOption; + drawing?: Drawing; +} +/** + * This is mostly a global spot where all of the relationship managers can get and set + * path information from/to. + * @module Excel/Drawing + */ +export declare class Drawing { + anchor: AbsoluteAnchor | OneCellAnchor | TwoCellAnchor; + id: string; + /** + * + * @param {String} type Can be 'absoluteAnchor', 'oneCellAnchor', or 'twoCellAnchor'. + * @param {Object} config Shorthand - pass the created anchor coords that can normally be used to construct it. + * @returns {Anchor} + */ + createAnchor(type: "absoluteAnchor" | "oneCellAnchor" | "twoCellAnchor", config: any): AbsoluteAnchor | OneCellAnchor | TwoCellAnchor; +} +/** + * + * @param {Object} config + * @param {Number} config.x X offset in EMU's + * @param {Number} config.y Y offset in EMU's + * @param {Number} config.width Width in EMU's + * @param {Number} config.height Height in EMU's + * @constructor + */ +export declare class AbsoluteAnchor { + x: number | null; + y: number | null; + width: number | null; + height: number | null; + constructor(config: AnchorOption); + /** + * Sets the X and Y offsets. + * + * @param {Number} x + * @param {Number} y + * @returns {undefined} + */ + setPos(x: number, y: number): void; + /** + * Sets the width and height of the image. + * + * @param {Number} width + * @param {Number} height + * @returns {undefined} + */ + setDimensions(width: number, height: number): void; + toXML(xmlDoc: XMLDOM, content: any): XMLNode; +} +export declare class Chart { +} +/** + * @module Excel/Util + */ +export declare class Util { + static _idSpaces: { + [space: string]: number; + }; + /** + * Returns a number based on a namespace. So, running with 'Picture' will return 1. Run again, you will get 2. Run with 'Foo', you'll get 1. + * @param {String} space + * @returns {Number} + */ + static uniqueId(space: string): number; + /** + * Attempts to create an XML document. After some investigation, using the 'fake' document + * is significantly faster than creating an actual XML document, so we're going to go with + * that. Besides, it just makes it easier to port to node. + * + * Takes a namespace to start the xml file in, as well as the root element + * of the xml file. + * + * @param {type} ns + * @param {type} base + * @returns {@new;XMLDOM} + */ + static createXmlDoc(ns: string, base: string): XMLDOM; + /** + * Creates an xml node (element). Used to simplify some calls, as IE is + * very particular about namespaces and such. + * + * @param {XMLDOM} doc An xml document (actual DOM or fake DOM, not a string) + * @param {type} name The name of the element + * @param {type} attributes + * @returns {XML Node} + */ + static createElement(doc: XMLDOM, name: string, attributes?: any): XMLNode; + /** + * This is sort of slow, but it's a huge convenience method for the code. It probably shouldn't be used + * in high repetition areas. + * + * @param {XMLDoc} doc + * @param {Object} attrs + */ + static setAttributesOnDoc(doc: XMLNode, attrs: { + [key: string]: any; + }): void; + static LETTER_REFS: any; + static positionToLetterRef(x: number, y: number | string): any; + static schemas: { + worksheet: string; + sharedStrings: string; + stylesheet: string; + relationships: string; + relationshipPackage: string; + contentTypes: string; + spreadsheetml: string; + markupCompat: string; + x14ac: string; + officeDocument: string; + package: string; + table: string; + spreadsheetDrawing: string; + drawing: string; + drawingRelationship: string; + image: string; + chart: string; + hyperlink: string; + }; +} +export type Relation = { + [id: string]: { + id: string; + schema: string; + object: any; + data?: { + id: number; + schema: string; + object: any; + }; + }; +}; +/** + * @module Excel/RelationshipManager + */ +export declare class RelationshipManager { + relations: Relation; + lastId: number; + constructor(); + importData(data: { + relations: Relation; + lastId: number; + }): void; + exportData(): { + relations: Relation; + lastId: number; + }; + addRelation(object: { + id: string; + }, type: keyof typeof Util.schemas): string; + getRelationshipId(object: { + id: string; + }): string | null; + toXML(): XMLDOM; +} +/** + * @module Excel/Drawings + */ +export declare class Drawings { + drawings: (Drawing | Picture)[]; + relations: RelationshipManager; + id: string; + /** + * Adds a drawing (more likely a subclass of a Drawing) to the 'Drawings' for a particular worksheet. + * + * @param {Drawing} drawing + * @returns {undefined} + */ + addDrawing(drawing: Drawing): void; + getCount(): number; + toXML(): XMLDOM; +} +/** + * @module Excel/SharedStrings + */ +export declare class SharedStrings { + strings: { + [key: string]: number; + }; + stringArray: string[]; + id: string; + /** + * Adds a string to the shared string file, and returns the ID of the + * string which can be used to reference it in worksheets. + * + * @param str {String} + * @return int + */ + addString(str: string): number; + exportData(): { + [key: string]: number; + }; + toXML(): XMLDOM; +} +/** + * Excel Color in ARGB format, for color aren't transparent just use "FF" as prefix. + * For example if the color you want to add is a blue with HTML color "#0000FF", then the excel color we need to add is "FF0000FF" + * Online tool: https://www.myfixguide.com/color-converter/ + */ +export type ExcelColorStyle = string | { + theme: number; +}; +export interface ExcelAlignmentStyle { + horizontal?: "center" | "fill" | "general" | "justify" | "left" | "right"; + justifyLastLine?: boolean; + readingOrder?: string; + relativeIndent?: boolean; + shrinkToFit?: boolean; + textRotation?: string | number; + vertical?: "bottom" | "distributed" | "center" | "justify" | "top"; + wrapText?: boolean; +} +export type ExcelBorderLineStyle = "continuous" | "dash" | "dashDot" | "dashDotDot" | "dotted" | "double" | "lineStyleNone" | "medium" | "slantDashDot" | "thin" | "thick"; +export interface ExcelBorderStyle { + bottom?: { + color?: ExcelColorStyle; + style?: ExcelBorderLineStyle; + }; + top?: { + color?: ExcelColorStyle; + style?: ExcelBorderLineStyle; + }; + left?: { + color?: ExcelColorStyle; + style?: ExcelBorderLineStyle; + }; + right?: { + color?: ExcelColorStyle; + style?: ExcelBorderLineStyle; + }; + diagonal?: any; + outline?: boolean; + diagonalUp?: boolean; + diagonalDown?: boolean; +} +export interface ExcelColumn { + bestFit?: boolean; + customWidth?: number; + hidden?: boolean; + min?: number; + max?: number; + width?: number; +} +export type ExcelColumnFormat = "bestFit" | "collapsed" | "customWidth" | "hidden" | "max" | "min" | "outlineLevel" | "phonetic" | "style" | "width"; +export interface ExcelTableColumn { + name: string; + dataCellStyle?: any; + dataDxfId?: number; + headerRowCellStyle?: ExcelStyleInstruction; + headerRowDxfId?: number; + totalsRowCellStyle?: ExcelStyleInstruction; + totalsRowDxfId?: number; + totalsRowFunction?: any; + totalsRowLabel?: string; + columnFormula?: string; + columnFormulaIsArrayType?: boolean; + totalFormula?: string; + totalFormulaIsArrayType?: boolean; +} +export interface ExcelFillStyle { + type?: "gradient" | "pattern"; + patternType?: string; + degree?: number; + fgColor?: ExcelColorStyle; + start?: ExcelColorStyle; + end?: { + pureAt?: number; + color?: ExcelColorStyle; + }; +} +export interface ExcelFontStyle { + bold?: boolean; + color?: ExcelColorStyle; + fontName?: string; + italic?: boolean; + outline?: boolean; + size?: number; + shadow?: boolean; + strike?: boolean; + subscript?: boolean; + superscript?: boolean; + underline?: boolean | "single" | "double" | "singleAccounting" | "doubleAccounting"; +} +export interface ExcelMetadata { + type?: string; + style?: number; +} +export interface ExcelColumnMetadata { + value: any; + metadata?: ExcelMetadata; +} +export interface ExcelMargin { + top: number; + bottom: number; + left: number; + right: number; + header: number; + footer: number; +} +export interface ExcelSortState { + caseSensitive?: boolean; + dataRange?: any; + columnSort?: boolean; + sortDirection?: "ascending" | "descending"; + sortRange?: any; +} +/** Excel custom formatting that will be applied to a column */ +export interface ExcelStyleInstruction { + id?: number; + alignment?: ExcelAlignmentStyle; + border?: ExcelBorderStyle; + borderId?: number; + fill?: ExcelFillStyle; + fillId?: number; + font?: ExcelFontStyle; + fontId?: number; + format?: string; + height?: number; + numFmt?: string; + numFmtId?: number; + width?: number; + xfId?: number; + protection?: { + locked?: boolean; + hidden?: boolean; + }; + /** style id */ + style?: number; +} +/** + * @module Excel/StyleSheet + */ +declare class StyleSheet$1 { + id: string; + cellStyles: { + name: string; + xfId: string; + builtinId: string; + }[]; + defaultTableStyle: boolean; + differentialStyles: any[]; + masterCellFormats: any[]; + masterCellStyles: any[]; + fonts: ExcelFontStyle[]; + numberFormatters: any[]; + fills: any[]; + borders: any[]; + tableStyles: any[]; + createSimpleFormatter(type: string): { + [id: string]: number; + }; + createFill(fillInstructions: any): any; + createNumberFormatter(formatInstructions: any): { + id: number; + formatCode: any; + }; + /** + * alignment: { + * horizontal: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_HorizontalAlignment.html + * vertical: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_VerticalAlignment.html + * @param {Object} styleInstructions + */ + createFormat(styleInstructions: ExcelStyleInstruction): any; + createDifferentialStyle(styleInstructions: ExcelStyleInstruction): ExcelStyleInstruction; + /** + * Should be an object containing keys that match with one of the keys from this list: + * http://www.schemacentral.com/sc/ooxml/t-ssml_ST_TableStyleType.html + * + * The value should be a reference to a differential format (dxf) + * @param {Object} instructions + */ + createTableStyle(instructions: any): void; + /** + * All params optional + * Expects: { + * top: {}, + * left: {}, + * right: {}, + * bottom: {}, + * diagonal: {}, + * outline: boolean, + * diagonalUp: boolean, + * diagonalDown: boolean + * } + * Each border should follow: + * { + * style: styleString, http://www.schemacentral.com/sc/ooxml/t-ssml_ST_BorderStyle.html + * color: ARBG color (requires the A, so for example FF006666) + * } + * @param {Object} border + */ + createBorderFormatter(border: any): any; + /** + * Supported font styles: + * bold + * italic + * underline (single, double, singleAccounting, doubleAccounting) + * size + * color + * fontName + * strike (strikethrough) + * outline (does this actually do anything?) + * shadow (does this actually do anything?) + * superscript + * subscript + * + * Color is a future goal - at the moment it's looking a bit complicated + * @param {Object} instructions + */ + createFontStyle(instructions: ExcelFontStyle): any; + exportBorders(doc: XMLDOM): XMLNode; + exportBorder(doc: XMLDOM, data: any): XMLNode; + exportColor(doc: XMLDOM, color: any): XMLNode; + exportMasterCellFormats(doc: XMLDOM): XMLNode; + exportMasterCellStyles(doc: XMLDOM): XMLNode; + exportCellFormatElement(doc: XMLDOM, styleInstructions: ExcelStyleInstruction): XMLNode; + exportAlignment(doc: XMLDOM, alignmentData: any): XMLNode; + exportFonts(doc: XMLDOM): XMLNode; + exportFont(doc: XMLDOM, fd: any): XMLNode; + exportFills(doc: XMLDOM): XMLNode; + exportFill(doc: XMLDOM, fd: any): XMLNode; + exportGradientFill(doc: XMLDOM, data: any): XMLNode; + /** + * Pattern types: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_PatternType.html + * @param {XMLDoc} doc + * @param {Object} data + */ + exportPatternFill(doc: XMLDOM, data: any): XMLNode; + exportNumberFormatters(doc: XMLDOM): XMLNode; + exportNumberFormatter(doc: XMLDOM, fd: any): XMLNode; + exportCellStyles(doc: XMLDOM): XMLNode; + exportDifferentialStyles(doc: XMLDOM): XMLNode; + exportDFX(doc: XMLDOM, style: any): XMLNode; + exportTableStyles(doc: XMLDOM): XMLNode; + exportTableStyle(doc: XMLDOM, style: { + name: string; + wholeTable?: number; + headerRow?: number; + }): XMLNode; + exportProtection(doc: XMLDOM, protectionData: any): XMLNode; + toXML(): XMLDOM; +} +/** + * @module Excel/Table + */ +export declare class Table { + name: string; + id: string; + tableId: string; + displayName: string; + dataCellStyle: any; + dataDfxId: number | null; + headerRowBorderDxfId: number | null; + headerRowCellStyle: any; + headerRowCount: number; + headerRowDxfId: number | null; + insertRow: boolean; + insertRowShift: boolean; + ref: any; + tableBorderDxfId: number | null; + totalsRowBorderDxfId: number | null; + totalsRowCellStyle: any; + totalsRowCount: number; + totalsRowDxfId: number | null; + tableColumns: any; + autoFilter: any; + sortState: any; + styleInfo: any; + constructor(config?: any); + initialize(config: any): void; + setReferenceRange(start: number[], end: number[]): void; + setTableColumns(columns: Array): void; + /** + * Expects an object with the following optional properties: + * name (required) + * dataCellStyle + * dataDxfId + * headerRowCellStyle + * headerRowDxfId + * totalsRowCellStyle + * totalsRowDxfId + * totalsRowFunction + * totalsRowLabel + * columnFormula + * columnFormulaIsArrayType (boolean) + * totalFormula + * totalFormulaIsArrayType (boolean) + */ + addTableColumn(column: ExcelTableColumn | string): void; + /** + * Expects an object with the following properties: + * caseSensitive (boolean) + * dataRange + * columnSort (assumes true) + * sortDirection + * sortRange (defaults to dataRange) + */ + setSortState(state: ExcelSortState): void; + toXML(): XMLDOM; + exportTableColumns(doc: XMLDOM): XMLNode; + exportAutoFilter(doc: XMLDOM): XMLNode; + exportTableStyleInfo(doc: XMLDOM): XMLNode; + addAutoFilter(startRef: any, endRef: any): void; +} +export declare class Pane { + state: null | "split" | "frozen" | "frozenSplit"; + xSplit: number | null; + ySplit: number | null; + activePane: string; + topLeftCell: number | string | null; + _freezePane: { + xSplit: number; + ySplit: number; + cell: string; + }; + freezePane(column: number, row: number, cell: string): void; + exportXML(doc: XMLDOM): XMLNode; +} +export interface SheetViewOption { + pane?: Pane; +} +/** + * @module Excel/SheetView + * https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.sheetview%28v=office.14%29.aspx + * + */ +export declare class SheetView { + pane: Pane; + showZeros: boolean | null; + defaultGridColor: string | null; + colorId: number | null; + rightToLeft: boolean | null; + showFormulas: boolean | null; + showGridLines: boolean | null; + showOutlineSymbols: boolean | null; + showRowColHeaders: boolean | null; + showRuler: boolean | null; + showWhiteSpace: boolean | null; + tabSelected: boolean | null; + topLeftCell: boolean | null; + viewType: null; + windowProtection: boolean | null; + zoomScale: boolean | null; + zoomScaleNormal: any; + zoomScalePageLayoutView: any; + zoomScaleSheetLayoutView: any; + constructor(config?: SheetViewOption); + /** + * Added froze pane + * @param column - column number: 0, 1, 2 ... + * @param row - row number: 0, 1, 2 ... + * @param cell - 'A1' + * @deprecated + */ + freezePane(column: number, row: number, cell: string): void; + exportXML(doc: XMLDOM): XMLNode; +} +export interface CharType { + font?: string; + bold?: boolean; + fontSize?: number; + text?: string; + underline?: boolean; +} +export interface WorksheetOption { + name?: string; + sheetView?: SheetView; +} +/** + * This module represents an excel worksheet in its basic form - no tables, charts, etc. Its purpose is + * to hold data, the data's link to how it should be styled, and any links to other outside resources. + * + * @module Excel/Worksheet + */ +export declare class Worksheet { + name: string; + id: string; + _timezoneOffset: number; + relations: any; + columnFormats: ExcelColumnFormat[]; + data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][]; + mergedCells: string[][]; + columns: ExcelColumn[]; + sheetProtection: any; + _headers: [ + left?: any, + center?: any, + right?: any + ]; + _footers: [ + left?: any, + center?: any, + right?: any + ]; + _tables: Table[]; + _drawings: Array; + _orientation?: string; + _margin?: ExcelMargin; + _rowInstructions: any; + _freezePane: { + xSplit?: number; + ySplit?: number; + cell?: string; + }; + sharedStrings: SharedStrings | null; + hyperlinks: Array<{ + cell: string; + id: string; + location?: string; + targetMode?: string; + }>; + sheetView: SheetView; + showZeros: any; + constructor(config: WorksheetOption); + initialize(config: any): void; + /** + * Returns an object that can be consumed by a Worksheet/Export/Worker + * @returns {Object} + */ + exportData(): { + relations: any; + columnFormats: ExcelColumnFormat[]; + data: (string | number | boolean | Date | ExcelColumnMetadata | null)[][]; + columns: ExcelColumn[]; + mergedCells: string[][]; + _headers: [ + left?: any, + center?: any, + right?: any + ]; + _footers: [ + left?: any, + center?: any, + right?: any + ]; + _tables: Table[]; + _rowInstructions: any; + _freezePane: { + xSplit?: number; + ySplit?: number; + cell?: string; + }; + name: string; + id: string; + }; + /** + * Imports data - to be used while inside of a WorksheetExportWorker. + * @param {Object} data + */ + importData(data: any): void; + setSharedStringCollection(stringCollection: SharedStrings): void; + addTable(table: Table): void; + addDrawings(drawings: Drawings): void; + setRowInstructions(rowIndex: number, instructions: ExcelStyleInstruction): void; + /** + * Expects an array length of three. + * + * @see Excel/Worksheet compilePageDetailPiece + * @see Adding headers and footers to a worksheet + * + * @param {Array} headers [left, center, right] + */ + setHeader(headers: [ + left: any, + center: any, + right: any + ]): void; + /** + * Expects an array length of three. + * + * @see Excel/Worksheet compilePageDetailPiece + * @see Adding headers and footers to a worksheet + * + * @param {Array} footers [left, center, right] + */ + setFooter(footers: [ + left: any, + center: any, + right: any + ]): void; + /** + * Turns page header/footer details into the proper format for Excel. + * @param {type} data + * @returns {String} + */ + compilePageDetailPackage(data: any): string; + /** + * Turns instructions on page header/footer details into something + * usable by Excel. + * + * @param {type} data + * @returns {String|@exp;_@call;reduce} + */ + compilePageDetailPiece(data: string | CharType | any[]): any; + /** + * Creates the header node. + * + * @todo implement the ability to do even/odd headers + * @param {XML Doc} doc + * @returns {XML Node} + */ + exportHeader(doc: XMLDOM): XMLNode; + /** + * Creates the footer node. + * + * @todo implement the ability to do even/odd footers + * @param {XML Doc} doc + * @returns {XML Node} + */ + exportFooter(doc: XMLDOM): XMLNode; + /** + * This creates some nodes ahead of time, which cuts down on generation time due to + * most cell definitions being essentially the same, but having multiple nodes that need + * to be created. Cloning takes less time than creation. + * + * @private + * @param {XML Doc} doc + * @returns {_L8.Anonym$0._buildCache.Anonym$2} + */ + _buildCache(doc: XMLDOM): { + number: XMLNode; + date: XMLNode; + string: XMLNode; + formula: XMLNode; + }; + /** + * Runs through the XML document and grabs all of the strings that will + * be sent to the 'shared strings' document. + * + * @returns {Array} + */ + collectSharedStrings(): string[]; + toXML(): XMLDOM; + /** + * + * @param {XML Doc} doc + * @returns {XML Node} + */ + exportColumns(doc: XMLDOM): XMLNode; + /** + * Sets the page settings on a worksheet node. + * + * @param {XML Doc} doc + * @param {XML Node} worksheet + * @returns {undefined} + */ + exportPageSettings(doc: XMLDOM, worksheet: XMLNode): void; + /** + * http://www.schemacentral.com/sc/ooxml/t-ssml_ST_Orientation.html + * + * Can be one of 'portrait' or 'landscape'. + * + * @param {'default' | 'portrait' | 'landscape'} orientation + * @returns {undefined} + */ + setPageOrientation(orientation: "default" | "portrait" | "landscape"): void; + /** + * Set page details in inches. + * use this structure: + * { + * top: 0.7 + * , bottom: 0.7 + * , left: 0.7 + * , right: 0.7 + * , header: 0.3 + * , footer: 0.3 + * } + * + * @returns {undefined} + */ + setPageMargin(input: ExcelMargin): void; + /** + * Expects an array of column definitions. Each column definition needs to have a width assigned to it. + * + * @param {Array} columns + */ + setColumns(columns: ExcelColumn[]): void; + /** + * Expects an array of data to be translated into cells. + * + * @param {Array} data Two dimensional array - [ [A1, A2], [B1, B2] ] + * @see Adding data to a worksheet + */ + setData(data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][]): void; + /** + * Merge cells in given range + * + * @param cell1 - A1, A2... + * @param cell2 - A2, A3... + */ + mergeCells(cell1: string, cell2: string): void; + /** + * Added frozen pane + * @param column - column number: 0, 1, 2 ... + * @param row - row number: 0, 1, 2 ... + * @param cell - 'A1' + * @deprecated + */ + freezePane(column: number, row: number, cell: string): void; + /** + * Expects an array containing an object full of column format definitions. + * http://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx + * bestFit + * collapsed + * customWidth + * hidden + * max + * min + * outlineLevel + * phonetic + * style + * width + * @param {Array} columnFormats + */ + setColumnFormats(columnFormats: ExcelColumnFormat[]): void; + /** + * Returns worksheet XML header (everything before ) + */ + getWorksheetXmlHeader(): string; + /** + * Returns worksheet XML footer (everything after ) + */ + getWorksheetXmlFooter(): string; + /** + * Serialize a chunk of rows to XML (same logic as in toXML) + */ + serializeRows(rows: (number | string | boolean | Date | null | ExcelColumnMetadata)[][], startRow?: number): string; +} +export interface MediaMeta { + id: string; + data: string; + fileName: string; + contentType: string | null; + extension: string; + rId?: string; +} +/** + * @module Excel/Workbook + */ +export declare class Workbook { + id: string; + styleSheet: StyleSheet$1; + sharedStrings: SharedStrings; + relations: RelationshipManager; + worksheets: Worksheet[]; + tables: Table[]; + drawings: Drawings[]; + media: { + [filename: string]: MediaMeta; + }; + printTitles: any; + constructor(); + initialize(): void; + createWorksheet(config?: any): Worksheet; + getStyleSheet(): StyleSheet$1; + addTable(table: Table): void; + addDrawings(drawings: Drawings): void; + /** + * Set number of rows to repeat for this sheet. + * + * @param {String} sheet name + * @param {int} number of rows to repeat from the top + * @returns {undefined} + */ + setPrintTitleTop(inSheet: string, inRowCount: number): void; + /** + * Set number of rows to repeat for this sheet. + * + * @param {String} sheet name + * @param {int} number of columns to repeat from the left + * @returns {undefined} + */ + setPrintTitleLeft(inSheet: string, inRowCount: number): void; + addMedia(_type: string, fileName: string, fileData: any, contentType?: string | null): MediaMeta; + addWorksheet(worksheet: Worksheet): void; + createContentTypes(): XMLDOM; + toXML(): XMLDOM; + createWorkbookRelationship(): XMLDOM; + _generateCorePaths(files: any): void; + _prepareFilesForPackaging(files: { + [path: string]: XMLDOM | string; + }): void; + generateFiles(): Promise<{ + [path: string]: string; + }>; + serializeHeader(): string; + serializeFooter(): string; +} +export declare class Picture extends Drawing { + id: string; + pictureId: number; + fill: any; + mediaData: MediaMeta | null; + description: string; + constructor(); + setMedia(mediaRef: MediaMeta): void; + setDescription(description: string): void; + setFillType(type: string): void; + setFillConfig(config: any): void; + getMediaType(): keyof typeof Util.schemas; + getMediaData(): MediaMeta; + setRelationshipId(rId: string): void; + toXML(xmlDoc: XMLDOM): XMLNode; +} +/** + * This is mostly a global spot where all of the relationship managers can get and set + * path information from/to. + * @module Excel/Paths + */ +export declare const Paths: { + [path: string]: string; +}; +/** + * Converts pixel sizes to 'EMU's, which is what Open XML uses. + * + * @todo clean this up. Code borrowed from http://polymathprogrammer.com/2009/10/22/english-metric-units-and-open-xml/, + * but not sure that it's going to be as accurate as it needs to be. + * + * @param int pixels + * @returns int + */ +export declare class Positioning { + static pixelsToEMUs(pixels: number): number; +} +export type InferOutputByType = T extends "Blob" ? Blob : T extends "Uint8Array" ? Uint8Array : any; +/** + * Creates a new workbook. + */ +export declare function createWorkbook(): Workbook; +/** + * Turns a workbook into a downloadable file, you can between a 'Blob' or 'Uint8Array', + * and if nothing is provided then 'Blob' will be the default + * @param {Excel/Workbook} workbook - The workbook that is being converted + * @param {'Uint8Array' | 'Blob'} [outputType='Blob'] - defaults to 'Blob' + * @param {Object} [options] + * - `fileFormat` defaults to "xlsx" + * - `mimeType`: a mime type can be provided by the user or auto-detect the mime when undefined (by file extension .xls/.xlsx) + * (user can pass an empty string to completely cancel the mime type altogether) + * - `zipOptions` to specify any `fflate` options to modify how the zip is created. + * @returns {Promise} + */ +export declare function createExcelFile(workbook: Workbook, outputType?: T, options?: { + fileFormat?: "xls" | "xlsx"; + mimeType?: string; + zipOptions?: ZipOptions; +}): Promise>; +/** + * Download Excel file, currently only supports a "browser" as `downloadType` + * but it could be expended in the future to also other type of platform like NodeJS for example. + * @param {Workbook} workbook + * @param {String} filename - filename (must also include file extension, xls/xlsx) + * @param {Object} [options] + * - `downloadType`: download type (browser/node), currently only a "browser" download as a Blob + * - `mimeType`: a mime type can be provided by the user or auto-detect the mime when undefined (by file extension .xls/.xlsx) + * (user can pass an empty string to completely cancel the mime type altogether) + * - `zipOptions` to specify any `fflate` options to modify how the zip is created. + */ +export declare function downloadExcelFile(workbook: Workbook, filename: string, options?: { + downloadType?: "browser" | "node"; + mimeType?: string; + zipOptions?: ZipOptions; +}): Promise; +export interface ExcelFileStreamOptions { + chunkSize?: number; + outputType?: "Uint8Array" | "Blob" | "stream"; + fileFormat?: "xlsx" | "xls"; + mimeType?: string; + zipOptions?: import("fflate").ZipOptions; + downloadType?: "browser" | "node"; +} +/** + * Environment-aware streaming Excel file generator. + * Yields zipped chunks for browser (ReadableStream) or NodeJS (async generator). + */ +export declare function createExcelFileStream(workbook: Workbook, options?: ExcelFileStreamOptions): ReadableStream> | AsyncGenerator, void, unknown>; +/** + * Converts the characters "&", "<", ">", '"', and "'" in `string` to their + * corresponding HTML entities. + * + * **Note:** No other characters are escaped. To escape additional + * characters use a third-party library like [_he_](https://mths.be/he). + * + * Though the ">" character is escaped for symmetry, characters like + * ">" and "/" don't need escaping in HTML and have no special meaning + * unless they're part of a tag or unquoted attribute value. See + * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) + * (under "semi-related fun fact") for more details. + * + * When working with HTML you should always + * [quote attribute values](http://wonko.com/post/html-escaping) to reduce + * XSS vectors. + * + * @since 0.1.0 + * @category String + * @param {string} [str=''] The string to escape. + * @returns {string} Returns the escaped string. + * @see escapeRegExp, unescape + * @example + * + * escape('fred, barney, & pebbles') + * // => 'fred, barney, & pebbles' + */ +export declare const htmlEscape: (str: string) => string; +export declare function isObject(value: unknown): value is object; +export declare function isPlainObject(value: unknown): boolean; +export declare function isString(value: any): value is string; +export declare function pick(object: any, keys: string[]): any; +/** + * Generates a unique ID. If `prefix` is given, the ID is appended to it. + * + * @since 0.1.0 + * @category Util + * @param {string} [prefix=''] The value to prefix the ID with. + * @returns {string} Returns the unique ID. + * @see random + * @example + * + * uniqueId('contact_') + * // => 'contact_104' + * + * uniqueId() + * // => '105' + */ +export declare function uniqueId(prefix?: string): string; + +export { + StyleSheet$1 as StyleSheet, +}; + +export {}; diff --git a/packages/excel-builder-vanilla-types/dist/index.js b/packages/excel-builder-vanilla-types/dist/index.js new file mode 100644 index 0000000..a726efc --- /dev/null +++ b/packages/excel-builder-vanilla-types/dist/index.js @@ -0,0 +1 @@ +'use strict'; \ No newline at end of file diff --git a/packages/excel-builder-vanilla/src/Excel/Workbook.ts b/packages/excel-builder-vanilla/src/Excel/Workbook.ts index ad8b47e..0632e51 100644 --- a/packages/excel-builder-vanilla/src/Excel/Workbook.ts +++ b/packages/excel-builder-vanilla/src/Excel/Workbook.ts @@ -347,8 +347,6 @@ export class Workbook { for (let i = 0, l = this.worksheets.length; i < l; i++) { const xml = this.worksheets[i].toXML(); - // Log non-streaming worksheet XML for comparison - console.log(`--- Non-streaming Worksheet XML for sheet${i + 1} ---\n`, typeof xml === 'string' ? xml : xml.toString()); files[`/xl/worksheets/sheet${i + 1}.xml`] = xml; Paths[this.worksheets[i].id] = `worksheets/sheet${i + 1}.xml`; files[`/xl/worksheets/_rels/sheet${i + 1}.xml.rels`] = this.worksheets[i].relations.toXML(); diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index 78548ca..d2fe59f 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -48,7 +48,7 @@ export class Worksheet { _freezePane: { xSplit?: number; ySplit?: number; cell?: string } = {}; sharedStrings: SharedStrings | null = null; - hyperlinks = []; + hyperlinks: Array<{ cell: string; id: string; location?: string; targetMode?: string }> = []; sheetView: SheetView; showZeros: any = null; @@ -354,7 +354,6 @@ export class Worksheet { cell = cellCache.formula.cloneNode(true); cell.firstChild.firstChild.nodeValue = cellValue as string; break; - // biome-ignore lint: original implementation case 'text': /*falls through*/ default: { @@ -652,14 +651,24 @@ export class Worksheet { return ` - `; + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">`; } /** * Returns worksheet XML footer (everything after ) */ getWorksheetXmlFooter(): string { + if (this._headers.length > 0 || this._footers.length > 0) { + let xml = ''; + if (this._headers.length > 0) { + xml += `${this.compilePageDetailPackage(this._headers)}`; + } + if (this._footers.length > 0) { + xml += `${this.compilePageDetailPackage(this._footers)}`; + } + xml += ''; + return xml; + } return ''; } diff --git a/packages/excel-builder-vanilla/src/streaming.ts b/packages/excel-builder-vanilla/src/streaming.ts index 7cafbba..deac487 100644 --- a/packages/excel-builder-vanilla/src/streaming.ts +++ b/packages/excel-builder-vanilla/src/streaming.ts @@ -7,6 +7,8 @@ export interface ExcelFileStreamOptions { outputType?: 'Uint8Array' | 'Blob' | 'stream'; fileFormat?: 'xlsx' | 'xls'; mimeType?: string; + zipOptions?: import('fflate').ZipOptions; + downloadType?: 'browser' | 'node'; } /** @@ -29,31 +31,10 @@ export function createExcelFileStream(workbook: Workbook, options?: ExcelFileStr * Browser: returns a ReadableStream of zipped Excel file chunks. */ function browserExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { - const chunkSize = options?.chunkSize ?? 1000; const stream = new ReadableStream({ async start(controller) { + // Use workbook.generateFiles() to get all required files const files = await workbook.generateFiles(); - for (let i = 0; i < workbook.worksheets.length; i++) { - const worksheet = workbook.worksheets[i]; - let worksheetXml = ''; - worksheetXml += worksheet.getWorksheetXmlHeader(); - let rowIndex = 0; - const totalRows = worksheet.data.length; - while (rowIndex < totalRows) { - const rowsChunk = worksheet.data.slice(rowIndex, rowIndex + chunkSize); - worksheetXml += worksheet.serializeRows(rowsChunk, rowIndex); - rowIndex += chunkSize; - await new Promise(r => setTimeout(r, 0)); - } - worksheetXml += ''; - worksheetXml += worksheet.getWorksheetXmlFooter(); - worksheetXml += ''; - // Ensure footer does NOT include tag, only append it once here - // If getWorksheetXmlFooter() includes , remove it from that method - const wsPath = `/xl/worksheets/sheet${i + 1}.xml`; - files[wsPath] = worksheetXml; - } - // Convert files to Uint8Array const zipObj: { [name: string]: Uint8Array } = {}; for (const [path, content] of Object.entries(files)) { const outPath = path.startsWith('/') ? path.substr(1) : path; @@ -64,7 +45,7 @@ function browserExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions } } // Synchronous zip for browser, split into chunks - const zipped: Uint8Array = zipSync(zipObj); + const zipped: Uint8Array = zipSync(zipObj, options?.zipOptions || {}); const chunkByteSize = 64 * 1024; // 64KB per chunk let offset = 0; while (offset < zipped.length) { @@ -83,29 +64,7 @@ function browserExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions * NodeJS: returns an async generator yielding zipped Excel file chunks. */ async function* nodeExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { - const chunkSize = options?.chunkSize ?? 1000; const files = await workbook.generateFiles(); - for (let i = 0; i < workbook.worksheets.length; i++) { - const worksheet = workbook.worksheets[i]; - let worksheetXml = ''; - worksheetXml += worksheet.getWorksheetXmlHeader(); - let rowIndex = 0; - const totalRows = worksheet.data.length; - while (rowIndex < totalRows) { - const rowsChunk = worksheet.data.slice(rowIndex, rowIndex + chunkSize); - worksheetXml += worksheet.serializeRows(rowsChunk, rowIndex); - rowIndex += chunkSize; - await new Promise(r => setTimeout(r, 0)); - } - worksheetXml += ''; - worksheetXml += worksheet.getWorksheetXmlFooter(); - worksheetXml += ''; - // Ensure footer does NOT include tag, only append it once here - // If getWorksheetXmlFooter() includes , remove it from that method - const wsPath = `/xl/worksheets/sheet${i + 1}.xml`; - files[wsPath] = worksheetXml; - } - // Convert files to Uint8Array const zipObj: { [name: string]: Uint8Array } = {}; for (const [path, content] of Object.entries(files)) { const outPath = path.startsWith('/') ? path.substr(1) : path; @@ -116,7 +75,7 @@ async function* nodeExcelStream(workbook: Workbook, options?: ExcelFileStreamOpt } } // Synchronous zip for Node, split into chunks - const zipped: Uint8Array = zipSync(zipObj); + const zipped: Uint8Array = zipSync(zipObj, options?.zipOptions || {}); const chunkByteSize = 64 * 1024; // 64KB per chunk let offset = 0; while (offset < zipped.length) { From 6f63b63c4983e0cc4dba724cea308c51fd1434ef Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 18:28:51 -0400 Subject: [PATCH 04/16] chore: add few Streaming unit tests --- .../src/__tests__/streaming.spec.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts diff --git a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts new file mode 100644 index 0000000..d31022b --- /dev/null +++ b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; + +import { createWorkbook } from '../factory.js'; +import { createExcelFileStream } from '../streaming.js'; +import { Worksheet } from '../Excel/Worksheet.js'; + +// Basic streaming test for NodeJS and browser-like environments + +describe('Streaming API', () => { + it('should stream Excel file chunks', async () => { + const workbook = createWorkbook(); + const worksheet = workbook.createWorksheet({ name: 'Sheet1' }); + worksheet.setData([ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], + ['Crystal Method', 'Vegas', 10.54], + ]); + workbook.addWorksheet(worksheet); + + const chunks: Uint8Array[] = []; + for await (const chunk of createExcelFileStream(workbook, { chunkSize: 1024 })) { + expect(chunk).toBeInstanceOf(Uint8Array); + chunks.push(chunk); + } + // Should produce a non-empty file + const totalSize = chunks.reduce((sum, c) => sum + c.length, 0); + expect(totalSize).toBeGreaterThan(0); + }); + + it('should stream Excel file with formulas and styles', async () => { + const workbook = createWorkbook(); + const worksheet = workbook.createWorksheet({ name: 'Sheet2' }); + worksheet.setData([ + [{ value: 'Artist' }, { value: 'Price' }, { value: 'Total' }], + ['Buckethead', 8.99, { value: 'B2*2', metadata: { type: 'formula' } }], + ['Crystal Method', 10.54, { value: 'B3*2', metadata: { type: 'formula' } }], + ]); + workbook.addWorksheet(worksheet); + + const chunks: Uint8Array[] = []; + for await (const chunk of createExcelFileStream(workbook, { chunkSize: 512 })) { + expect(chunk).toBeInstanceOf(Uint8Array); + chunks.push(chunk); + } + const totalSize = chunks.reduce((sum, c) => sum + c.length, 0); + expect(totalSize).toBeGreaterThan(0); + }); + + describe('Worksheet XML helpers', () => { + it('getWorksheetXmlHeader returns correct header', () => { + const ws = new Worksheet({ name: 'Test' }); + const header = ws.getWorksheetXmlHeader(); + expect(header).toContain(''); + expect(header).toContain(' { + const ws = new Worksheet({ name: 'Test' }); + expect(ws.getWorksheetXmlFooter()).toBe(''); + }); + + it('getWorksheetXmlFooter returns headerFooter XML if header/footer set', () => { + const ws = new Worksheet({ name: 'Test' }); + ws.setHeader(['Left', 'Center', 'Right']); + ws.setFooter(['FLeft', 'FCenter', 'FRight']); + const xml = ws.getWorksheetXmlFooter(); + expect(xml).toContain(''); + expect(xml).toContain(''); + expect(xml).toContain(''); + expect(xml).toContain('Left'); + expect(xml).toContain('FLeft'); + }); + }); + + describe('serializeRows', () => { + it('serializes text and number rows correctly', () => { + const ws = new Worksheet({ name: 'Test' }); + ws.sharedStrings = { strings: {}, addString: () => 0 } as any; + const rows = [ + ['Header1', 'Header2'], + [123, 'abc'], + ]; + const xml = ws.serializeRows(rows); + expect(xml).toContain(''); + expect(xml).toContain('0'); + expect(xml).toContain('0'); + }); + + it('serializes formula cells', () => { + const ws = new Worksheet({ name: 'Test' }); + ws.sharedStrings = { strings: {}, addString: () => 0 } as any; + const rows = [ + ['Header1', 'Header2'], + [{ value: 'A2+B2', metadata: { type: 'formula' } }, 42], + ]; + const xml = ws.serializeRows(rows); + expect(xml).toContain('0'); + expect(xml).toContain('42'); + }); + }); +}); From c8c0e4fb639142d94f5dbaf9f3c264171d4c19c4 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 18:40:57 -0400 Subject: [PATCH 05/16] chore: add more test coverage --- .../src/Excel/Drawing/__tests__/Chart.spec.ts | 10 ++ .../src/__tests__/streaming.spec.ts | 103 ++++++++++++++++++ .../excel-builder-vanilla/src/streaming.ts | 2 +- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 packages/excel-builder-vanilla/src/Excel/Drawing/__tests__/Chart.spec.ts diff --git a/packages/excel-builder-vanilla/src/Excel/Drawing/__tests__/Chart.spec.ts b/packages/excel-builder-vanilla/src/Excel/Drawing/__tests__/Chart.spec.ts new file mode 100644 index 0000000..2d9efa1 --- /dev/null +++ b/packages/excel-builder-vanilla/src/Excel/Drawing/__tests__/Chart.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; + +import { Chart } from '../Chart.js'; + +describe('Chart', () => { + it('can be instantiated', () => { + const chart = new Chart(); + expect(chart).toBeInstanceOf(Chart); + }); +}); diff --git a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts index d31022b..c8194d0 100644 --- a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts +++ b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts @@ -1,3 +1,4 @@ +import { unzipSync } from 'fflate'; import { describe, expect, it } from 'vitest'; import { createWorkbook } from '../factory.js'; @@ -7,6 +8,108 @@ import { Worksheet } from '../Excel/Worksheet.js'; // Basic streaming test for NodeJS and browser-like environments describe('Streaming API', () => { + describe('Streaming API edge cases', () => { + describe('NodeJS streaming', () => { + it('createExcelFileStream delegates to nodeExcelStream in NodeJS', async () => { + // Simulate NodeJS environment + const originalWindow = globalThis.window; + const originalProcess = globalThis.process; + // @ts-ignore + delete globalThis.window; + globalThis.process = { versions: { node: '18.0.0' } } as any; + const { createExcelFileStream } = await import('../streaming.js'); + let called = false; + const fakeWorkbook: any = { + async generateFiles() { + called = true; + return { + 'xl/worksheet.xml': 'sheet', + 'xl/media/image.png': btoa('fakebinary'), + }; + }, + }; + const result = createExcelFileStream(fakeWorkbook, {}); + const chunks: Uint8Array[] = []; + for await (const chunk of result) { + chunks.push(chunk); + } + expect(called).toBe(true); + // Restore + globalThis.window = originalWindow; + globalThis.process = originalProcess; + }); + + it('nodeExcelStream yields zipped chunks and covers non-XML file', async () => { + // Mock workbook with generateFiles returning both XML and non-XML + const workbook: any = { + async generateFiles() { + return { + 'xl/worksheet.xml': 'sheet', + 'xl/media/image.png': btoa('fakebinary'), + }; + }, + }; + // Import nodeExcelStream directly + const { nodeExcelStream } = await import('../streaming.js'); + const chunks: Uint8Array[] = []; + for await (const chunk of nodeExcelStream(workbook)) { + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); + // Concatenate chunks correctly for unzipSync + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); + const zipped = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + zipped.set(chunk, offset); + offset += chunk.length; + } + const files = unzipSync(zipped); + expect(Object.keys(files)).toContain('xl/worksheet.xml'); + expect(Object.keys(files)).toContain('xl/media/image.png'); + }); + }); + + it('throws on unsupported environment', () => { + // Simulate an unsupported environment + const originalWindow = globalThis.window; + const originalProcess = globalThis.process; + // @ts-ignore + delete globalThis.window; + // @ts-ignore + delete globalThis.process; + const workbook = createWorkbook(); + expect(() => createExcelFileStream(workbook)).toThrow(); + // Restore + globalThis.window = originalWindow; + globalThis.process = originalProcess; + }); + + it('handles empty workbook', async () => { + const workbook = createWorkbook(); + const chunks: Uint8Array[] = []; + for await (const chunk of createExcelFileStream(workbook)) { + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); + }); + + it('respects chunkSize option', async () => { + const workbook = createWorkbook(); + const ws = workbook.createWorksheet({ name: 'Sheet1' }); + ws.setData([ + ['A', 'B'], + [1, 2], + ]); + workbook.addWorksheet(ws); + const chunks: Uint8Array[] = []; + for await (const chunk of createExcelFileStream(workbook, { chunkSize: 1 })) { + chunks.push(chunk); + } + expect(chunks.length).toBeGreaterThan(0); + }); + }); + it('should stream Excel file chunks', async () => { const workbook = createWorkbook(); const worksheet = workbook.createWorksheet({ name: 'Sheet1' }); diff --git a/packages/excel-builder-vanilla/src/streaming.ts b/packages/excel-builder-vanilla/src/streaming.ts index deac487..b260296 100644 --- a/packages/excel-builder-vanilla/src/streaming.ts +++ b/packages/excel-builder-vanilla/src/streaming.ts @@ -63,7 +63,7 @@ function browserExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions /** * NodeJS: returns an async generator yielding zipped Excel file chunks. */ -async function* nodeExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { +export async function* nodeExcelStream(workbook: Workbook, options?: ExcelFileStreamOptions) { const files = await workbook.generateFiles(); const zipObj: { [name: string]: Uint8Array } = {}; for (const [path, content] of Object.entries(files)) { From dd9d1045629da4f6886b4c6efa901a467cb57ec8 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 18:43:35 -0400 Subject: [PATCH 06/16] chore: add more test coverage --- .../src/__tests__/streaming.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts index c8194d0..b99c45c 100644 --- a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts +++ b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { createWorkbook } from '../factory.js'; import { createExcelFileStream } from '../streaming.js'; import { Worksheet } from '../Excel/Worksheet.js'; +import { Workbook } from '../Excel/Workbook.js'; // Basic streaming test for NodeJS and browser-like environments @@ -203,3 +204,15 @@ describe('Streaming API', () => { }); }); }); + +describe('Workbook XML serialization', () => { + it('serializeHeader returns correct XML header', () => { + const wb = new Workbook(); + expect(wb.serializeHeader()).toBe(''); + }); + + it('serializeFooter returns correct XML footer', () => { + const wb = new Workbook(); + expect(wb.serializeFooter()).toBe(''); + }); +}); From 181a24720dbdd98e5b5e57b32148ed4d8ebf9b6b Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 18:53:44 -0400 Subject: [PATCH 07/16] chore: add new Streaming example with images --- packages/demo/src/app-routing.ts | 2 + packages/demo/src/examples/example13.html | 3 +- packages/demo/src/examples/example14.html | 5 +- packages/demo/src/examples/example17.html | 128 ++++++++++++++++++++++ packages/demo/src/examples/example17.ts | 79 +++++++++++++ 5 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 packages/demo/src/examples/example17.html create mode 100644 packages/demo/src/examples/example17.ts diff --git a/packages/demo/src/app-routing.ts b/packages/demo/src/app-routing.ts index 4457d8a..be95935 100644 --- a/packages/demo/src/app-routing.ts +++ b/packages/demo/src/app-routing.ts @@ -14,6 +14,7 @@ import Example13 from './examples/example13.js'; import Example14 from './examples/example14.js'; import Example15 from './examples/example15.js'; import Example16 from './examples/example16.js'; +import Example17 from './examples/example17.js'; import GettingStarted from './getting-started.js'; export const navbarRouting = [ @@ -46,6 +47,7 @@ export const exampleRouting = [ { name: 'example14', view: '/src/examples/example14.html', viewModel: Example14, title: '14- Pictures with different anchors' }, { name: 'example15', view: '/src/examples/example15.html', viewModel: Example15, title: '15- Streaming Excel Export' }, { name: 'example16', view: '/src/examples/example16.html', viewModel: Example16, title: '16- Streaming Features Demo' }, + { name: 'example17', view: '/src/examples/example17.html', viewModel: Example17, title: '17- Streaming Export with Images' }, ], }, ]; diff --git a/packages/demo/src/examples/example13.html b/packages/demo/src/examples/example13.html index 8db27ee..b90c02e 100644 --- a/packages/demo/src/examples/example13.html +++ b/packages/demo/src/examples/example13.html @@ -19,7 +19,8 @@

- You can insert pictures/images in Excel but it must be provided in base64format. + You can insert pictures/images in Excel but it must be provided in base64 + format.
diff --git a/packages/demo/src/examples/example14.html b/packages/demo/src/examples/example14.html index 9c93925..d5bd78b 100644 --- a/packages/demo/src/examples/example14.html +++ b/packages/demo/src/examples/example14.html @@ -19,8 +19,9 @@

- You can insert pictures/images in Excel but it must be provided in base64format. There are multiple type of anchors - that you can use: oneCellAnchor/twoCellAnchor/absoluteAnchorcell anchors. + You can insert pictures/images in Excel but it must be provided in base64 format. There are multiple type of + anchors that you can use: oneCellAnchor  / twoCellAnchor / absoluteAnchor + cell anchors.
diff --git a/packages/demo/src/examples/example17.html b/packages/demo/src/examples/example17.html new file mode 100644 index 0000000..4019762 --- /dev/null +++ b/packages/demo/src/examples/example17.html @@ -0,0 +1,128 @@ +
+
+
+

+ Example 17: Streaming Export with Images + + Code + + html + | + ts + + +

+
+ Streaming Excel export with images using the new Streaming API. Images must be provided in base64format. +
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ A + + B + + C + + D + + E + + F + + G + + H +
+ Artist + + Album + + Price +
BucketheadAlbino Slug8.99   
BucketheadElectric Tears13.99 + +
BucketheadColma11.34
Crystal MethodVegas10.54
Crystal MethodTweekend10.64
Crystal MethodDivided By Night8.99
+ + + diff --git a/packages/demo/src/examples/example17.ts b/packages/demo/src/examples/example17.ts new file mode 100644 index 0000000..5bfcaf8 --- /dev/null +++ b/packages/demo/src/examples/example17.ts @@ -0,0 +1,79 @@ +import { createWorkbook, createExcelFileStream, Drawings, Picture, Table } from 'excel-builder-vanilla'; + +import './example13.scss'; + +export default class Example { + exportBtnElm!: HTMLButtonElement; + githubLogoBase64 = + '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'; + + mount() { + this.exportBtnElm = document.querySelector('#export') as HTMLButtonElement; + this.exportBtnElm.addEventListener('click', this.startProcess.bind(this)); + document.querySelector('#pic1')!.src = `data:image/png;base64,${this.githubLogoBase64}`; + } + + unmount() { + this.exportBtnElm.removeEventListener('click', this.startProcess.bind(this)); + } + + async startProcess() { + const workbook = createWorkbook(); + const worksheet = workbook.createWorksheet({ name: 'TestSheet' }); + + const originalData = [ + ['Artist', 'Album', 'Price'], + ['Buckethead', 'Albino Slug', 8.99], + ['Buckethead', 'Electric Tears', 13.99], + ['Buckethead', 'Colma', 11.34], + ['Crystal Method', 'Vegas', 10.54], + ['Crystal Method', 'Tweekend', 10.64], + ['Crystal Method', 'Divided By Night', 8.99], + ]; + + const albumTable = new Table(); + albumTable.styleInfo.themeStyle = 'TableStyleDark2'; + albumTable.setReferenceRange([1, 1], [3, originalData.length]); + albumTable.setTableColumns(['Artist', 'Album', 'Price']); + + worksheet.sheetView.showGridLines = false; + worksheet.setData(originalData); + workbook.addWorksheet(worksheet); + + worksheet.addTable(albumTable); + workbook.addTable(albumTable); + + const drawings = new Drawings(); + const picRef = workbook.addMedia('image', 'logo.png', this.githubLogoBase64); + const picture = new Picture(); + picture.createAnchor('twoCellAnchor', { + from: { + x: 5, + y: 2, + }, + to: { + x: 7, + y: 8, + }, + }); + + picture.setMedia(picRef); + drawings.addDrawing(picture); + worksheet.addDrawings(drawings); + workbook.addDrawings(drawings); + + // Streaming export + const stream = createExcelFileStream(workbook, { chunkSize: 1024 }); + const chunks: Uint8Array[] = []; + for await (const chunk of stream as AsyncIterable) { + chunks.push(chunk); + } + const blob = new Blob(chunks as BlobPart[], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'Fruits-Streaming.xlsx'; + a.click(); + URL.revokeObjectURL(url); + } +} From 32cb6ec6a19142510e00960950605d8ed5d9ccd2 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 19:10:00 -0400 Subject: [PATCH 08/16] chore: add non-streaming NodeJS demo --- README.md | 1 + package.json | 4 ++ .../demo/examples/node-non-streaming-demo.mjs | 52 +++++++++++++++++++ ...-streaming.mjs => node-streaming-demo.mjs} | 0 packages/demo/package.json | 9 ++-- 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 packages/demo/examples/node-non-streaming-demo.mjs rename packages/demo/examples/{example15-node-streaming.mjs => node-streaming-demo.mjs} (100%) diff --git a/README.md b/README.md index d97ddc5..0a5a63e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ The modernization steps: - bump version to `v3.0.0` as a `major` release (_the original project version was in the `2.x` range._) - note that the changelog did not exists prior to `v3.0.0` - v4.x is now ESM-Only +- new Streaming API for large datasets The project now requires only 1 small dependency which is [fflate](https://github.com/101arrowz/fflate). diff --git a/package.json b/package.json index 02943fa..035e5a5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,10 @@ "serve:demo": "pnpm -r --stream --filter \"./packages/demo/**\" dev", "test": "vitest --watch --config ./vitest/vitest.config.mts", "test:coverage": "vitest --coverage --config ./vitest/vitest.config.mts" + , + "demo:node:streaming": "node ./packages/demo/examples/node-streaming-demo.mjs" + , + "demo:node:non-streaming": "node ./packages/demo/examples/node-non-streaming-demo.mjs" }, "engines": { "node": "^20.17.0 || >=22.9.0", diff --git a/packages/demo/examples/node-non-streaming-demo.mjs b/packages/demo/examples/node-non-streaming-demo.mjs new file mode 100644 index 0000000..e31cd6b --- /dev/null +++ b/packages/demo/examples/node-non-streaming-demo.mjs @@ -0,0 +1,52 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createExcelFile, createWorkbook } from 'excel-builder-vanilla'; + +// Build data array (same as streaming example) +const ROWS = 1000; +const dataArray = []; + +// Add header row at row 0, merged and styled +const workbook = createWorkbook(); +const worksheet = workbook.createWorksheet({ name: 'Demo Non-Streaming' }); + +// Create a format for the header row +const stylesheet = workbook.getStyleSheet(); +const headerFormat = stylesheet.createFormat({ + alignment: { horizontal: 'center' }, + font: { bold: true, color: 'FF2b995d', size: 13 }, +}); + +dataArray.push([{ value: 'NodeJS Non-Streaming Output', metadata: { style: headerFormat.id } }]); +dataArray.push(['ID', 'Name', 'Score']); +for (let i = 1; i <= ROWS; i++) { + dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]); +} +// Add a formula cell for the total score +dataArray.push(['', 'Total', { value: 'SUM(C2:C1001)', metadata: { type: 'formula' } }]); + +worksheet.setData(dataArray); +worksheet.mergeCells('A1', 'C1'); +workbook.addWorksheet(worksheet); + +// Ensure temp folder exists +const tempDir = path.resolve(process.cwd(), 'temp'); +if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); +} +const outputPath = path.join(tempDir, 'node-non-streaming-example15.xlsx'); + +(async () => { + let buffer = await createExcelFile(workbook, { + outputType: 'Uint8Array', + fileFormat: 'xlsx', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + downloadType: 'node', + }); + // If buffer is a Blob (browser fallback), convert to ArrayBuffer + if (buffer instanceof Blob) { + buffer = new Uint8Array(await buffer.arrayBuffer()); + } + fs.writeFileSync(outputPath, buffer); + console.log(`Excel file written to ${outputPath}`); +})(); diff --git a/packages/demo/examples/example15-node-streaming.mjs b/packages/demo/examples/node-streaming-demo.mjs similarity index 100% rename from packages/demo/examples/example15-node-streaming.mjs rename to packages/demo/examples/node-streaming-demo.mjs diff --git a/packages/demo/package.json b/packages/demo/package.json index 1058c5c..00512d4 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -4,10 +4,11 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "stream:excel": "node ./examples/example15-node-streaming.mjs" + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "stream:excel": "node ./examples/node-streaming-demo.mjs", + "nonstream:excel": "node ./examples/node-non-streaming-demo.mjs" }, "dependencies": { "@excel-builder-vanilla/types": "workspace:*", From c572212268e1b15a8aa29ec53fbf3450f3b064df Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 19:24:31 -0400 Subject: [PATCH 09/16] chore: add more tests and update interface --- .../dist/index.d.ts | 13 ++++++++----- .../src/Excel/Worksheet.ts | 6 +++--- .../src/__tests__/streaming.spec.ts | 10 ++++++++++ .../excel-builder-vanilla/src/interfaces.ts | 18 +++++------------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/excel-builder-vanilla-types/dist/index.d.ts b/packages/excel-builder-vanilla-types/dist/index.d.ts index 985173f..f090faf 100644 --- a/packages/excel-builder-vanilla-types/dist/index.d.ts +++ b/packages/excel-builder-vanilla-types/dist/index.d.ts @@ -337,13 +337,16 @@ export interface ExcelBorderStyle { } export interface ExcelColumn { bestFit?: boolean; + collapsed?: boolean; customWidth?: number; hidden?: boolean; - min?: number; max?: number; + min?: number; + outlineLevel?: number; + phonetic?: boolean; + style?: number; width?: number; } -export type ExcelColumnFormat = "bestFit" | "collapsed" | "customWidth" | "hidden" | "max" | "min" | "outlineLevel" | "phonetic" | "style" | "width"; export interface ExcelTableColumn { name: string; dataCellStyle?: any; @@ -679,7 +682,7 @@ export declare class Worksheet { id: string; _timezoneOffset: number; relations: any; - columnFormats: ExcelColumnFormat[]; + columnFormats: ExcelColumn[]; data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][]; mergedCells: string[][]; columns: ExcelColumn[]; @@ -721,7 +724,7 @@ export declare class Worksheet { */ exportData(): { relations: any; - columnFormats: ExcelColumnFormat[]; + columnFormats: ExcelColumn[]; data: (string | number | boolean | Date | ExcelColumnMetadata | null)[][]; columns: ExcelColumn[]; mergedCells: string[][]; @@ -914,7 +917,7 @@ export declare class Worksheet { * width * @param {Array} columnFormats */ - setColumnFormats(columnFormats: ExcelColumnFormat[]): void; + setColumnFormats(columnFormats: ExcelColumn[]): void; /** * Returns worksheet XML header (everything before ) */ diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index d2fe59f..e7ef887 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -1,4 +1,4 @@ -import type { ExcelColumn, ExcelColumnFormat, ExcelColumnMetadata, ExcelMargin, ExcelStyleInstruction } from '../interfaces.js'; +import type { ExcelColumn, ExcelColumnMetadata, ExcelMargin, ExcelStyleInstruction } from '../interfaces.js'; import { isObject, isString } from '../utilities/isTypeOf.js'; import { uniqueId } from '../utilities/uniqueId.js'; import type { Drawings } from './Drawings.js'; @@ -33,7 +33,7 @@ export class Worksheet { id = uniqueId('Worksheet'); _timezoneOffset: number; relations: any = null; - columnFormats: ExcelColumnFormat[] = []; + columnFormats: ExcelColumn[] = []; data: (number | string | boolean | Date | null | ExcelColumnMetadata)[][] = []; mergedCells: string[][] = []; columns: ExcelColumn[] = []; @@ -640,7 +640,7 @@ export class Worksheet { * width * @param {Array} columnFormats */ - setColumnFormats(columnFormats: ExcelColumnFormat[]) { + setColumnFormats(columnFormats: ExcelColumn[]) { this.columnFormats = columnFormats; } diff --git a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts index b99c45c..b5a5ca1 100644 --- a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts +++ b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts @@ -6,6 +6,16 @@ import { createExcelFileStream } from '../streaming.js'; import { Worksheet } from '../Excel/Worksheet.js'; import { Workbook } from '../Excel/Workbook.js'; +describe('Worksheet.setColumnFormats', () => { + it('sets columnFormats property', () => { + const ws = new Worksheet({ name: 'TestSheet' }); + // Use valid ExcelColumn objects + const formats = [{ width: 20 }, { hidden: true }]; + ws.setColumnFormats(formats); + expect(ws.columnFormats).toBe(formats); + }); +}); + // Basic streaming test for NodeJS and browser-like environments describe('Streaming API', () => { diff --git a/packages/excel-builder-vanilla/src/interfaces.ts b/packages/excel-builder-vanilla/src/interfaces.ts index db83a0c..84558e1 100644 --- a/packages/excel-builder-vanilla/src/interfaces.ts +++ b/packages/excel-builder-vanilla/src/interfaces.ts @@ -41,25 +41,17 @@ export interface ExcelBorderStyle { export interface ExcelColumn { bestFit?: boolean; + collapsed?: boolean; customWidth?: number; hidden?: boolean; - min?: number; max?: number; + min?: number; + outlineLevel?: number; + phonetic?: boolean; + style?: number; width?: number; } -export type ExcelColumnFormat = - | 'bestFit' - | 'collapsed' - | 'customWidth' - | 'hidden' - | 'max' - | 'min' - | 'outlineLevel' - | 'phonetic' - | 'style' - | 'width'; - export interface ExcelTableColumn { name: string; dataCellStyle?: any; From 4585aa7e48a0ec2104a17880431cd7943e48fc5b Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 20:09:22 -0400 Subject: [PATCH 10/16] chore: update Node script demos & add them to docs --- docs/streaming.md | 2 ++ docs/workbook-create.md | 4 +++- docs/worksheet-add-data.md | 4 +++- docs/worksheet-create.md | 4 +++- docs/worksheet-headers-footers.md | 4 +++- package.json | 8 +++----- .../node-non-streaming-demo.mjs | 2 +- .../node-streaming-demo.mjs | 2 +- packages/demo/package.json | 10 +++++----- packages/demo/src/examples/example15.html | 6 +++--- 10 files changed, 27 insertions(+), 19 deletions(-) rename packages/demo/{examples => node-examples}/node-non-streaming-demo.mjs (94%) rename packages/demo/{examples => node-examples}/node-streaming-demo.mjs (94%) diff --git a/docs/streaming.md b/docs/streaming.md index 0683cd0..50c9f50 100644 --- a/docs/streaming.md +++ b/docs/streaming.md @@ -51,6 +51,8 @@ for await (const chunk of createExcelFileStream(workbook, { chunkSize: 1000 })) output.end(); ``` +> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder. + ## Supported Features All features such as formulas, alignment, borders, styles, and images work with streaming export. The only difference is how the file is delivered. diff --git a/docs/workbook-create.md b/docs/workbook-create.md index 21c01d5..897b0e0 100644 --- a/docs/workbook-create.md +++ b/docs/workbook-create.md @@ -37,4 +37,6 @@ const workbook = createWorkbook(); const buffer = createExcelFile(workbook); fs.writeFileSync('output.xlsx', buffer); -``` \ No newline at end of file +``` + +> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder. \ No newline at end of file diff --git a/docs/worksheet-add-data.md b/docs/worksheet-add-data.md index 31c388a..88f00e1 100644 --- a/docs/worksheet-add-data.md +++ b/docs/worksheet-add-data.md @@ -46,4 +46,6 @@ workbook.addWorksheet(sheet); const buffer = createExcelFile(workbook); fs.writeFileSync('output.xlsx', buffer); -``` \ No newline at end of file +``` + +> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder. \ No newline at end of file diff --git a/docs/worksheet-create.md b/docs/worksheet-create.md index 75ca4a0..200ee15 100644 --- a/docs/worksheet-create.md +++ b/docs/worksheet-create.md @@ -60,4 +60,6 @@ workbook.addWorksheet(sheet); const buffer = createExcelFile(workbook); fs.writeFileSync('output.xlsx', buffer); -``` \ No newline at end of file +``` + +> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder. \ No newline at end of file diff --git a/docs/worksheet-headers-footers.md b/docs/worksheet-headers-footers.md index 21b34ef..984faad 100644 --- a/docs/worksheet-headers-footers.md +++ b/docs/worksheet-headers-footers.md @@ -72,4 +72,6 @@ workbook.addWorksheet(sheet); const buffer = createExcelFile(workbook); fs.writeFileSync('output.xlsx', buffer); -``` \ No newline at end of file +``` + +> **Note:** a Node script can be found in the [packages/demo/node-examples/](https://github.com/ghiscoding/excel-builder-vanilla/tree/main/packages/demo/node-examples/) folder. \ No newline at end of file diff --git a/package.json b/package.json index 035e5a5..e5480c3 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,9 @@ "roll-new-release": "pnpm build && pnpm new-version && pnpm new-publish", "serve:demo": "pnpm -r --stream --filter \"./packages/demo/**\" dev", "test": "vitest --watch --config ./vitest/vitest.config.mts", - "test:coverage": "vitest --coverage --config ./vitest/vitest.config.mts" - , - "demo:node:streaming": "node ./packages/demo/examples/node-streaming-demo.mjs" - , - "demo:node:non-streaming": "node ./packages/demo/examples/node-non-streaming-demo.mjs" + "test:coverage": "vitest --coverage --config ./vitest/vitest.config.mts", + "demo:node:streaming": "node ./packages/demo/node-examples/node-streaming-demo.mjs", + "demo:node:non-streaming": "node ./packages/demo/node-examples/node-non-streaming-demo.mjs" }, "engines": { "node": "^20.17.0 || >=22.9.0", diff --git a/packages/demo/examples/node-non-streaming-demo.mjs b/packages/demo/node-examples/node-non-streaming-demo.mjs similarity index 94% rename from packages/demo/examples/node-non-streaming-demo.mjs rename to packages/demo/node-examples/node-non-streaming-demo.mjs index e31cd6b..af14fc8 100644 --- a/packages/demo/examples/node-non-streaming-demo.mjs +++ b/packages/demo/node-examples/node-non-streaming-demo.mjs @@ -23,7 +23,7 @@ for (let i = 1; i <= ROWS; i++) { dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]); } // Add a formula cell for the total score -dataArray.push(['', 'Total', { value: 'SUM(C2:C1001)', metadata: { type: 'formula' } }]); +dataArray.push(['', 'Total', { value: `SUM(C2:C${ROWS + 2})`, metadata: { type: 'formula' } }]); worksheet.setData(dataArray); worksheet.mergeCells('A1', 'C1'); diff --git a/packages/demo/examples/node-streaming-demo.mjs b/packages/demo/node-examples/node-streaming-demo.mjs similarity index 94% rename from packages/demo/examples/node-streaming-demo.mjs rename to packages/demo/node-examples/node-streaming-demo.mjs index 0c22070..35b8f3f 100644 --- a/packages/demo/examples/node-streaming-demo.mjs +++ b/packages/demo/node-examples/node-streaming-demo.mjs @@ -22,7 +22,7 @@ dataArray.push(['ID', 'Name', 'Score']); for (let i = 1; i <= ROWS; i++) { dataArray.push([i, `User ${i}`, Math.floor(Math.random() * 100)]); } -dataArray.push(['', 'Total', '=SUM(C2:C1001)']); +dataArray.push(['', 'Total', { value: `SUM(C2:C${ROWS + 2})`, metadata: { type: 'formula' } }]); worksheet.setData(dataArray); worksheet.mergeCells('A1', 'C1'); diff --git a/packages/demo/package.json b/packages/demo/package.json index 00512d4..181e3d4 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -4,11 +4,11 @@ "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "stream:excel": "node ./examples/node-streaming-demo.mjs", - "nonstream:excel": "node ./examples/node-non-streaming-demo.mjs" + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "stream:excel": "node ./node-examples/node-streaming-demo.mjs", + "nonstream:excel": "node ./node-examples/node-non-streaming-demo.mjs" }, "dependencies": { "@excel-builder-vanilla/types": "workspace:*", diff --git a/packages/demo/src/examples/example15.html b/packages/demo/src/examples/example15.html index ca731b1..67e3221 100644 --- a/packages/demo/src/examples/example15.html +++ b/packages/demo/src/examples/example15.html @@ -5,9 +5,9 @@

Example 15: Streaming Excel Export (100,000 rows)

- For large datasets, streaming export is significantly more performant and memory-efficient than non-streaming export. This example - demonstrates streaming using createExcelFileStream. The export also includes Header & Footer. Export progress is - shown below. + For large datasets, streaming export is significantly more performant and memory-efficient compared to non-streaming export. This + example demonstrates streaming using createExcelFileStream. The export also includes Header & Footer. Export + progress is shown below.
From dba9a979af83f8c95fd44274208af259f7b3c9b9 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 20:28:21 -0400 Subject: [PATCH 11/16] chore: fix some typos and update examples --- packages/demo/src/examples/example01.ts | 1 - packages/demo/src/examples/example02.html | 6 ++++-- packages/demo/src/examples/example03.html | 4 ++-- packages/demo/src/examples/example04.html | 6 ++++-- packages/demo/src/examples/example05.html | 4 ++-- packages/demo/src/examples/example07.html | 4 ++-- packages/demo/src/examples/example09.ts | 2 +- packages/demo/src/examples/example10.html | 3 ++- packages/demo/src/examples/example10.ts | 2 +- packages/demo/src/examples/example11.ts | 2 +- packages/demo/src/examples/example13.ts | 2 +- packages/demo/src/examples/example14.ts | 3 ++- packages/demo/src/examples/example15.ts | 6 +++++- packages/demo/src/examples/example16.html | 1 - packages/demo/src/examples/example16.ts | 2 -- packages/demo/src/examples/example17.html | 12 ++++++++---- packages/demo/src/examples/example17.ts | 2 +- packages/demo/src/getting-started.html | 4 ++-- 18 files changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/demo/src/examples/example01.ts b/packages/demo/src/examples/example01.ts index dd80e03..db4b0a2 100644 --- a/packages/demo/src/examples/example01.ts +++ b/packages/demo/src/examples/example01.ts @@ -1,5 +1,4 @@ import { downloadExcelFile, Workbook } from 'excel-builder-vanilla'; -// import type { ExcelStyleInstruction } from '@excel-builder-vanilla/types'; import './example01.scss'; diff --git a/packages/demo/src/examples/example02.html b/packages/demo/src/examples/example02.html index 4e2db67..c82585b 100644 --- a/packages/demo/src/examples/example02.html +++ b/packages/demo/src/examples/example02.html @@ -19,8 +19,10 @@

- The column widthattribute will set a width. The hiddenattribute will hide the column in Excel. The example - below has the "Artist" column wider and the next column "Album" to be hidden in the exported Excel file. + The column width + attribute will set a width. The hidden + attribute will hide the column in Excel. The example below has the "Artist" column wider and the next column "Album" to be hidden in + the exported Excel file.
diff --git a/packages/demo/src/examples/example03.html b/packages/demo/src/examples/example03.html index 547806d..3ad8ec6 100644 --- a/packages/demo/src/examples/example03.html +++ b/packages/demo/src/examples/example03.html @@ -19,8 +19,8 @@

- Set different row options via setRowInstructions()method. For example, we changed the row height of the first row and - change the text style to italic. + Set different row options via setRowInstructions() + method. For example, we changed the row height of the first row and change the text style to italic.
diff --git a/packages/demo/src/examples/example04.html b/packages/demo/src/examples/example04.html index 897f104..bf3fab8 100644 --- a/packages/demo/src/examples/example04.html +++ b/packages/demo/src/examples/example04.html @@ -19,8 +19,10 @@

- Set different fonts and colors via the createFormat()method, we can provide an object with the fontand - borderproperties. + Set different fonts and colors via the createFormat() + method, we can provide an object with the font + and border + properties.
diff --git a/packages/demo/src/examples/example05.html b/packages/demo/src/examples/example05.html index 5b4844a..57b988c 100644 --- a/packages/demo/src/examples/example05.html +++ b/packages/demo/src/examples/example05.html @@ -19,8 +19,8 @@

- We can create custom format by using the createFormat()method, in this example we formatted the "Price" column as - currency and the Modified Date is a Date format. + We can create custom format by using the createFormat() + method, in this example we formatted the "Price" column as currency and the Modified Date is a Date format.
diff --git a/packages/demo/src/examples/example07.html b/packages/demo/src/examples/example07.html index 1df9dd0..0598da3 100644 --- a/packages/demo/src/examples/example07.html +++ b/packages/demo/src/examples/example07.html @@ -19,8 +19,8 @@

- Set different background filling by using fillproperty which accepts a wide range of options like background color type - of gradient or pattern and different colors. + Set different background filling by using fill + property which accepts a wide range of options like background color type of gradient or pattern and different colors.
diff --git a/packages/demo/src/examples/example09.ts b/packages/demo/src/examples/example09.ts index 5c20c2a..12cd310 100644 --- a/packages/demo/src/examples/example09.ts +++ b/packages/demo/src/examples/example09.ts @@ -1,4 +1,4 @@ -import { Table, createWorkbook, downloadExcelFile } from 'excel-builder-vanilla'; +import { createWorkbook, downloadExcelFile, Table } from 'excel-builder-vanilla'; import './example09.scss'; diff --git a/packages/demo/src/examples/example10.html b/packages/demo/src/examples/example10.html index 3cc56a2..40a9358 100644 --- a/packages/demo/src/examples/example10.html +++ b/packages/demo/src/examples/example10.html @@ -20,7 +20,8 @@

Every once in a while you need a table theme that isn't available from the custom themes. You can use - createTableStyle()to change style for a section like the header row and/or the whole table. + createTableStyle() + to change style for a section like the header row and/or the whole table.
diff --git a/packages/demo/src/examples/example10.ts b/packages/demo/src/examples/example10.ts index a9aa315..b520481 100644 --- a/packages/demo/src/examples/example10.ts +++ b/packages/demo/src/examples/example10.ts @@ -1,4 +1,4 @@ -import { Table, createWorkbook, downloadExcelFile } from 'excel-builder-vanilla'; +import { createWorkbook, downloadExcelFile, Table } from 'excel-builder-vanilla'; import './example10.scss'; diff --git a/packages/demo/src/examples/example11.ts b/packages/demo/src/examples/example11.ts index 2761f90..c7c1bc9 100644 --- a/packages/demo/src/examples/example11.ts +++ b/packages/demo/src/examples/example11.ts @@ -1,4 +1,4 @@ -import { Table, createWorkbook, downloadExcelFile } from 'excel-builder-vanilla'; +import { createWorkbook, downloadExcelFile, Table } from 'excel-builder-vanilla'; import './example11.scss'; diff --git a/packages/demo/src/examples/example13.ts b/packages/demo/src/examples/example13.ts index 7859c60..59e6b4a 100644 --- a/packages/demo/src/examples/example13.ts +++ b/packages/demo/src/examples/example13.ts @@ -1,4 +1,4 @@ -import { createWorkbook, downloadExcelFile, Drawings, Picture, Table } from 'excel-builder-vanilla'; +import { createWorkbook, Drawings, downloadExcelFile, Picture, Table } from 'excel-builder-vanilla'; import './example13.scss'; diff --git a/packages/demo/src/examples/example14.ts b/packages/demo/src/examples/example14.ts index 53addbf..45be3f1 100644 --- a/packages/demo/src/examples/example14.ts +++ b/packages/demo/src/examples/example14.ts @@ -1,4 +1,5 @@ -import { createWorkbook, downloadExcelFile, Drawings, Picture, Positioning } from 'excel-builder-vanilla'; +import { createWorkbook, Drawings, downloadExcelFile, Picture, Positioning } from 'excel-builder-vanilla'; + import strawberryImageData from '../images/strawberry.jpg?base64'; // images must be provided in the `base64` format, use a Vite loader plugin import strawberryUrl from '../images/strawberry.jpg?url'; diff --git a/packages/demo/src/examples/example15.ts b/packages/demo/src/examples/example15.ts index e9fe42b..aa13536 100644 --- a/packages/demo/src/examples/example15.ts +++ b/packages/demo/src/examples/example15.ts @@ -27,14 +27,17 @@ export default class Example { const artistWorkbook = createWorkbook(); const albumList = artistWorkbook.createWorksheet({ name: 'Artists' }); + // Apply currency format for Price column const stylesheet = artistWorkbook.getStyleSheet(); const currencyFormat = stylesheet.createFormat({ format: '$#,##0.00' }); + // Update header to use currency style const headerCell = originalData[0][2]; if (typeof headerCell === 'object' && headerCell !== null && 'metadata' in headerCell && headerCell.metadata) { headerCell.metadata.style = currencyFormat.id; } + // Update all rows to use currency style for Price for (let i = 1; i < originalData.length; i++) { const cell = originalData[i][2]; @@ -42,6 +45,7 @@ export default class Example { cell.metadata.style = currencyFormat.id; } } + albumList.setData(originalData); albumList.setHeader([ 'This will be on the left', @@ -74,6 +78,6 @@ export default class Example { a.download = 'LargeArtistWB.xlsx'; a.click(); URL.revokeObjectURL(url); - this.progressElm.textContent = 'Export complete!'; + this.progressElm.textContent = `Export successfully ${ROWS} rows!`; } } diff --git a/packages/demo/src/examples/example16.html b/packages/demo/src/examples/example16.html index e1b4f33..3ee32e4 100644 --- a/packages/demo/src/examples/example16.html +++ b/packages/demo/src/examples/example16.html @@ -2,7 +2,6 @@

Example 16: Streaming Features Demo (50,000 rows)

-
merged Example 02-08
This demo showcases merged features: merged header, row height, styles, fonts, colors, borders, number/date formatting alignment and formulas (from Example 02-08 but using Streaming Export). diff --git a/packages/demo/src/examples/example16.ts b/packages/demo/src/examples/example16.ts index deb11d3..4eb9aa3 100644 --- a/packages/demo/src/examples/example16.ts +++ b/packages/demo/src/examples/example16.ts @@ -72,7 +72,6 @@ export default class Example { const album = `Album ${idx + 1}`; const price = Math.round(Math.random() * 10000) / 100; const quantity = Math.floor(Math.random() * 10) + 1; - // Excel formula for Total: =C{row}+D{row} const rowNum = idx + 3; // +3 for header rows originalData.push([ { value: artist, metadata: { style: centerAlign.id } }, @@ -134,7 +133,6 @@ export default class Example { a.download = 'Artist WB - Streaming Features.xlsx'; a.click(); URL.revokeObjectURL(url); - // Do not reset progress bar after export; leave at 100% until next export })(); } } diff --git a/packages/demo/src/examples/example17.html b/packages/demo/src/examples/example17.html index 4019762..3f7b709 100644 --- a/packages/demo/src/examples/example17.html +++ b/packages/demo/src/examples/example17.html @@ -19,7 +19,8 @@

- Streaming Excel export with images using the new Streaming API. Images must be provided in base64format. + Streaming Excel export with images using the new Streaming API. Images must be provided in base64 + format.
@@ -63,13 +64,16 @@

- Artist + Artist + - Album + Album + - Price + Price + diff --git a/packages/demo/src/examples/example17.ts b/packages/demo/src/examples/example17.ts index 5bfcaf8..fa12721 100644 --- a/packages/demo/src/examples/example17.ts +++ b/packages/demo/src/examples/example17.ts @@ -1,4 +1,4 @@ -import { createWorkbook, createExcelFileStream, Drawings, Picture, Table } from 'excel-builder-vanilla'; +import { createExcelFileStream, createWorkbook, Drawings, Picture, Table } from 'excel-builder-vanilla'; import './example13.scss'; diff --git a/packages/demo/src/getting-started.html b/packages/demo/src/getting-started.html index 3adae83..5286a33 100644 --- a/packages/demo/src/getting-started.html +++ b/packages/demo/src/getting-started.html @@ -20,8 +20,8 @@

CDN

- jsDelivrgraciously provide CDNs for many JavaScript libraries including - Excel-Builder-Vanilla. Just use the following CDN links. + jsDelivr + graciously provide CDNs for many JavaScript libraries including Excel-Builder-Vanilla. Just use the following CDN links.

The project now ships as ESM-Only, if you still wish to use the legacy CommonJS (CJS) format with require(), then use From f1b8f12153f831ce62d09e858d311dda9b37b829 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 20:43:42 -0400 Subject: [PATCH 12/16] chore: fix some typos --- packages/demo/src/examples/example16.scss | 4 ---- packages/demo/src/examples/example16.ts | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/demo/src/examples/example16.scss b/packages/demo/src/examples/example16.scss index b7bf3cd..89612ed 100644 --- a/packages/demo/src/examples/example16.scss +++ b/packages/demo/src/examples/example16.scss @@ -1,5 +1,3 @@ -/* Merged styles from example02.scss to example06.scss */ - .export-btn { background: #2b995d; color: #fff; @@ -19,5 +17,3 @@ font-size: 1.1em; color: #2b995d; } - -/* Add any other useful styles from example02-06 as needed */ diff --git a/packages/demo/src/examples/example16.ts b/packages/demo/src/examples/example16.ts index 4eb9aa3..5f969c5 100644 --- a/packages/demo/src/examples/example16.ts +++ b/packages/demo/src/examples/example16.ts @@ -63,6 +63,7 @@ export default class Example { { value: 'Total', metadata: { style: themeColor.id } }, ], ]; + async function generateDataAsync() { const batchSize = 2000; for (let i = 0; i < ROWS; i += batchSize) { From 3722dae84eb2e9cb5c5ab9017e51f3bfebe6b18a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 20:48:35 -0400 Subject: [PATCH 13/16] chore: use already defined schema templates --- packages/excel-builder-vanilla/src/Excel/Worksheet.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index e7ef887..2733106 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -649,9 +649,9 @@ export class Worksheet { */ getWorksheetXmlHeader(): string { return ` -`; +`; } /** From 8376f4a0c341e27cd7025cd8c57b631647f589ad Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 20:51:22 -0400 Subject: [PATCH 14/16] chore: fix some typos --- packages/excel-builder-vanilla/src/Excel/Workbook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/excel-builder-vanilla/src/Excel/Workbook.ts b/packages/excel-builder-vanilla/src/Excel/Workbook.ts index 0632e51..f972625 100644 --- a/packages/excel-builder-vanilla/src/Excel/Workbook.ts +++ b/packages/excel-builder-vanilla/src/Excel/Workbook.ts @@ -358,13 +358,13 @@ export class Workbook { }); } + /** Return workbook XML header */ serializeHeader(): string { - // Return workbook XML header return ''; } + /** Return workbook XML footer */ serializeFooter(): string { - // Return workbook XML footer return ''; } } From e124514c61582cb4c5e2c53ecbf3a0728dc3f533 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 21:08:31 -0400 Subject: [PATCH 15/16] chore: simplify code a bit more --- packages/excel-builder-vanilla-types/dist/index.d.ts | 2 ++ packages/excel-builder-vanilla/src/Excel/Worksheet.ts | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/excel-builder-vanilla-types/dist/index.d.ts b/packages/excel-builder-vanilla-types/dist/index.d.ts index f090faf..884d0f7 100644 --- a/packages/excel-builder-vanilla-types/dist/index.d.ts +++ b/packages/excel-builder-vanilla-types/dist/index.d.ts @@ -988,7 +988,9 @@ export declare class Workbook { generateFiles(): Promise<{ [path: string]: string; }>; + /** Return workbook XML header */ serializeHeader(): string; + /** Return workbook XML footer */ serializeFooter(): string; } export declare class Picture extends Drawing { diff --git a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts index 2733106..5b30246 100644 --- a/packages/excel-builder-vanilla/src/Excel/Worksheet.ts +++ b/packages/excel-builder-vanilla/src/Excel/Worksheet.ts @@ -683,11 +683,7 @@ export class Worksheet { let rowXml = ``; for (let c = 0; c < cellCount; c++) { const cellValue = dataRow[c]; - let cellType: any = typeof cellValue; - // Always treat first row as text - if (startRow + row === 0) { - cellType = 'text'; - } + const cellType: any = typeof cellValue || 'text'; let cellXml = ''; const rAttr = ` r="${String.fromCharCode(65 + c)}${startRow + row + 1}"`; switch (cellType) { From 29d13d085efb6380b87c153bb8bbd35718cf6fc2 Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Fri, 15 Aug 2025 21:20:07 -0400 Subject: [PATCH 16/16] chore: more cleanup and code improvements --- .../src/__tests__/streaming.spec.ts | 2 +- packages/excel-builder-vanilla/src/index.ts | 2 +- .../excel-builder-vanilla/src/interfaces.ts | 20 +------------------ .../excel-builder-vanilla/src/streaming.ts | 6 +++--- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts index b5a5ca1..6388b9e 100644 --- a/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts +++ b/packages/excel-builder-vanilla/src/__tests__/streaming.spec.ts @@ -90,7 +90,7 @@ describe('Streaming API', () => { // @ts-ignore delete globalThis.process; const workbook = createWorkbook(); - expect(() => createExcelFileStream(workbook)).toThrow(); + expect(() => createExcelFileStream(workbook)).toThrow('Streaming is only supported in browser or NodeJS environments.'); // Restore globalThis.window = originalWindow; globalThis.process = originalProcess; diff --git a/packages/excel-builder-vanilla/src/index.ts b/packages/excel-builder-vanilla/src/index.ts index b96089d..481560a 100644 --- a/packages/excel-builder-vanilla/src/index.ts +++ b/packages/excel-builder-vanilla/src/index.ts @@ -19,7 +19,7 @@ export { Worksheet } from './Excel/Worksheet.js'; export { XMLDOM, XMLNode } from './Excel/XMLDOM.js'; export { createExcelFile, createWorkbook, downloadExcelFile } from './factory.js'; export { createExcelFileStream } from './streaming.js'; -export * from './interfaces.js'; +export type * from './interfaces.js'; export { htmlEscape } from './utilities/escape.js'; export { isObject, isPlainObject, isString } from './utilities/isTypeOf.js'; export { pick } from './utilities/pick.js'; diff --git a/packages/excel-builder-vanilla/src/interfaces.ts b/packages/excel-builder-vanilla/src/interfaces.ts index 84558e1..f5d6906 100644 --- a/packages/excel-builder-vanilla/src/interfaces.ts +++ b/packages/excel-builder-vanilla/src/interfaces.ts @@ -91,21 +91,6 @@ export interface ExcelFontStyle { underline?: boolean | 'single' | 'double' | 'singleAccounting' | 'doubleAccounting'; } -// type ExcelMetadataType = -// | 'general' -// | 'number' -// | 'currency' -// | 'accounting' -// | 'date' -// | 'time' -// | 'percentage' -// | 'formula' -// | 'fraction' -// | 'scientific' -// | 'text' -// | 'special' -// | 'custom'; - export interface ExcelMetadata { type?: string; style?: number; @@ -151,10 +136,7 @@ export interface ExcelStyleInstruction { numFmtId?: number; width?: number; xfId?: number; - protection?: { - locked?: boolean; - hidden?: boolean; - }; + protection?: { locked?: boolean; hidden?: boolean }; /** style id */ style?: number; } diff --git a/packages/excel-builder-vanilla/src/streaming.ts b/packages/excel-builder-vanilla/src/streaming.ts index b260296..0f6fffa 100644 --- a/packages/excel-builder-vanilla/src/streaming.ts +++ b/packages/excel-builder-vanilla/src/streaming.ts @@ -1,13 +1,13 @@ -import { strToU8, zipSync } from 'fflate'; +import { strToU8, type ZipOptions, zipSync } from 'fflate'; import type { Workbook } from './Excel/Workbook.js'; import { base64ToUint8Array } from './factory.js'; export interface ExcelFileStreamOptions { chunkSize?: number; - outputType?: 'Uint8Array' | 'Blob' | 'stream'; + outputType?: 'Blob' | 'Uint8Array' | 'stream'; fileFormat?: 'xlsx' | 'xls'; mimeType?: string; - zipOptions?: import('fflate').ZipOptions; + zipOptions?: ZipOptions; downloadType?: 'browser' | 'node'; }