Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/super-editor/src/core/DocxZipper.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ class DocxZipper {
* @returns {Promise<JSZip>} 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
Expand Down
16 changes: 16 additions & 0 deletions packages/super-editor/src/core/DocxZipper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': '<w:document/>',
},
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();

Expand Down
68 changes: 51 additions & 17 deletions packages/super-editor/src/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,22 +805,8 @@ export class Editor extends EventEmitter<EditorEventMap> {
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 ?? {}),
Expand Down Expand Up @@ -1736,6 +1722,49 @@ export class Editor extends EventEmitter<EditorEventMap> {
}
}

async #loadBlankDocxTemplate(): Promise<{
content: DocxFileEntry[];
mediaFiles: Record<string, unknown>;
fonts: Record<string, unknown>;
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<DocxFileEntry[]> {
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.
*/
Expand Down Expand Up @@ -2808,8 +2837,13 @@ export class Editor extends EventEmitter<EditorEventMap> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<?xml${Object.entries(declaration)
.map(([key, value]) => ` ${key}="${value}"`)
.join('')}?>`;
Expand Down
16 changes: 16 additions & 0 deletions packages/super-editor/src/tests/export/jsonDeclaration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
Loading