From 98f33d380fe9cda02f6def9a9024585c031cc997 Mon Sep 17 00:00:00 2001 From: Luccas Correa Date: Tue, 17 Mar 2026 14:39:43 -0300 Subject: [PATCH] fix: seed base docx package for collaboration exports --- packages/super-editor/src/core/DocxZipper.js | 4 ++ .../super-editor/src/core/DocxZipper.test.js | 16 +++++ packages/super-editor/src/core/Editor.ts | 68 ++++++++++++++----- .../src/core/super-converter/exporter.js | 6 +- .../src/tests/export/jsonDeclaration.test.js | 16 +++++ 5 files changed, 92 insertions(+), 18 deletions(-) diff --git a/packages/super-editor/src/core/DocxZipper.js b/packages/super-editor/src/core/DocxZipper.js index 3c1d6b6d54..29b17d10cb 100644 --- a/packages/super-editor/src/core/DocxZipper.js +++ b/packages/super-editor/src/core/DocxZipper.js @@ -409,6 +409,10 @@ class DocxZipper { * @returns {Promise} The unzipped but updated docx file ready for zipping */ async exportFromCollaborativeDocx(docx, updatedDocs, media, fonts) { + if (!Array.isArray(docx)) { + throw new Error('Collaborative DOCX export requires base package entries'); + } + const zip = new JSZip(); // Rebuild original files diff --git a/packages/super-editor/src/core/DocxZipper.test.js b/packages/super-editor/src/core/DocxZipper.test.js index 5d262a03f1..902994963c 100644 --- a/packages/super-editor/src/core/DocxZipper.test.js +++ b/packages/super-editor/src/core/DocxZipper.test.js @@ -431,6 +431,22 @@ describe('DocxZipper - updateContentTypes', () => { }); describe('DocxZipper - exportFromCollaborativeDocx media handling', () => { + it('throws when collaborative export has no original docx entries to rebuild from', async () => { + const zipper = new DocxZipper(); + + await expect( + zipper.updateZip({ + docx: null, + updatedDocs: { + 'word/document.xml': '', + }, + media: {}, + fonts: {}, + isHeadless: true, + }), + ).rejects.toThrow('Collaborative DOCX export requires base package entries'); + }); + it('handles both base64 string and ArrayBuffer media values', async () => { const zipper = new DocxZipper(); diff --git a/packages/super-editor/src/core/Editor.ts b/packages/super-editor/src/core/Editor.ts index d1e5769bf0..0e38a63582 100644 --- a/packages/super-editor/src/core/Editor.ts +++ b/packages/super-editor/src/core/Editor.ts @@ -805,22 +805,8 @@ export class Editor extends EventEmitter { resolvedMode === 'docx' && !options?.content && !options?.html && !options?.markdown; if (shouldLoadBlankDocx) { - // Decode base64 blank.docx without fetch - const arrayBuffer = await getArrayBufferFromUrl(BLANK_DOCX_DATA_URI); - const isNodeRuntime = typeof process !== 'undefined' && !!process.versions?.node; - const canUseBuffer = isNodeRuntime && typeof Buffer !== 'undefined'; - // Use Uint8Array to ensure compatibility with both Node Buffer and browser Blob - const uint8Array = new Uint8Array(arrayBuffer); - let fileSource: File | Blob | Buffer; - if (canUseBuffer) { - fileSource = Buffer.from(uint8Array); - } else if (typeof Blob !== 'undefined') { - fileSource = new Blob([uint8Array as BlobPart]); - } else { - throw new Error('Blob is not available to create blank DOCX'); - } - const [docx, _media, mediaFiles, fonts] = (await Editor.loadXmlData(fileSource, canUseBuffer))!; - resolvedOptions.content = docx; + const { content, mediaFiles, fonts, fileSource } = await this.#loadBlankDocxTemplate(); + resolvedOptions.content = content; resolvedOptions.mediaFiles = { ...mediaFiles, ...(options?.mediaFiles ?? {}), @@ -1736,6 +1722,49 @@ export class Editor extends EventEmitter { } } + async #loadBlankDocxTemplate(): Promise<{ + content: DocxFileEntry[]; + mediaFiles: Record; + fonts: Record; + fileSource: File | Blob | Buffer; + }> { + const arrayBuffer = await getArrayBufferFromUrl(BLANK_DOCX_DATA_URI); + const isNodeRuntime = typeof process !== 'undefined' && !!process.versions?.node; + const canUseBuffer = isNodeRuntime && typeof Buffer !== 'undefined'; + const uint8Array = new Uint8Array(arrayBuffer); + + let fileSource: File | Blob | Buffer; + if (canUseBuffer) { + fileSource = Buffer.from(uint8Array); + } else if (typeof Blob !== 'undefined') { + fileSource = new Blob([uint8Array as BlobPart]); + } else { + throw new Error('Blob is not available to create blank DOCX'); + } + + const [content, _media, mediaFiles, fonts] = (await Editor.loadXmlData(fileSource, canUseBuffer))!; + return { content, mediaFiles, fonts, fileSource }; + } + + async #getBaseDocxEntriesForExport(): Promise { + if (Array.isArray(this.options.content)) { + return this.options.content as DocxFileEntry[]; + } + + const blankDocx = await this.#loadBlankDocxTemplate(); + this.options.content = blankDocx.content; + this.options.mediaFiles = { + ...blankDocx.mediaFiles, + ...(this.options.mediaFiles ?? {}), + }; + this.options.fonts = { + ...blankDocx.fonts, + ...(this.options.fonts ?? {}), + }; + + return blankDocx.content; + } + /** * Initialize media. */ @@ -2808,8 +2837,13 @@ export class Editor extends EventEmitter { return updatedDocs; } + const baseDocxEntries = + !this.options.fileSource && !Array.isArray(this.options.content) + ? await this.#getBaseDocxEntriesForExport() + : this.options.content; + const result = await zipper.updateZip({ - docx: this.options.content, + docx: baseDocxEntries, updatedDocs: updatedDocs, originalDocxFile: this.options.fileSource, media, diff --git a/packages/super-editor/src/core/super-converter/exporter.js b/packages/super-editor/src/core/super-converter/exporter.js index c13c45afe8..ef5288efbb 100644 --- a/packages/super-editor/src/core/super-converter/exporter.js +++ b/packages/super-editor/src/core/super-converter/exporter.js @@ -577,7 +577,11 @@ export class DocxExporter { #generate_xml_as_list(data, debug = false) { const json = JSON.parse(JSON.stringify(data)); - const declaration = this.converter.declaration.attributes; + const declaration = this.converter.declaration?.attributes ?? { + version: '1.0', + encoding: 'UTF-8', + standalone: 'yes', + }; const xmlTag = ` ` ${key}="${value}"`) .join('')}?>`; diff --git a/packages/super-editor/src/tests/export/jsonDeclaration.test.js b/packages/super-editor/src/tests/export/jsonDeclaration.test.js index 166666236b..4687055a42 100644 --- a/packages/super-editor/src/tests/export/jsonDeclaration.test.js +++ b/packages/super-editor/src/tests/export/jsonDeclaration.test.js @@ -53,4 +53,20 @@ describe('Json override export', () => { editor.destroy(); } }); + + it('exports a DOCX when base package entries are missing before export', async () => { + const editor = await Editor.open(undefined, { json: SAMPLE_JSON }); + + try { + editor.options.fileSource = null; + editor.options.content = ''; + + const exported = await editor.exportDocx(); + expect(Buffer.isBuffer(exported)).toBe(true); + expect(exported.length).toBeGreaterThan(0); + expect(Array.isArray(editor.options.content)).toBe(true); + } finally { + editor.destroy(); + } + }); });