diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index b4807c6724..ec78431916 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -910,7 +910,8 @@ export class Editor extends EventEmitter { const customXml = this.converter.schemaToXml(this.converter.convertedXml['docProps/custom.xml'].elements[0]); const styles = this.converter.schemaToXml(this.converter.convertedXml['word/styles.xml'].elements[0]); const customSettings = this.converter.schemaToXml(this.converter.convertedXml['word/settings.xml'].elements[0]); - + const contentTypes = this.converter.schemaToXml(this.converter.convertedXml['[Content_Types].xml'].elements[0]); + const originalCommentsXml = this.converter.convertedXml['word/comments.xml']; const updatedCommentsXml = originalCommentsXml ? this.converter.schemaToXml(originalCommentsXml.elements[0]) : null; const media = this.converter.addedMedia; @@ -922,6 +923,7 @@ export class Editor extends EventEmitter { 'word/settings.xml': String(customSettings), // Replace & with & in styles.xml as DOCX viewers can't handle it 'word/styles.xml': String(styles).replace(/&/gi, '&'), + '[Content_Types].xml': String(contentTypes), }; // Add comments.xml to the list of files to update if we have any comments diff --git a/packages/super-editor/src/core/super-converter/SuperConverter.js b/packages/super-editor/src/core/super-converter/SuperConverter.js index 5e404495d2..5db8eece5e 100644 --- a/packages/super-editor/src/core/super-converter/SuperConverter.js +++ b/packages/super-editor/src/core/super-converter/SuperConverter.js @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DocxExporter, exportSchemaToJson } from './exporter'; import { createDocumentJson, addDefaultStylesIfMissing } from './v2/importer/docxImporter.js'; import { getArrayBufferFromUrl } from './helpers.js'; -import { getCommentDefinition, updateCommentsXml } from './v2/exporter/commentsExporter.js'; +import { getCommentDefinition, updateCommentsXml, updateContentTypes } from './v2/exporter/commentsExporter.js'; import { DEFAULT_CUSTOM_XML, SETTINGS_CUSTOM_XML } from './exporter-docx-defs.js'; import { COMMENTS_XML } from './exporter-docx-defs.js'; @@ -346,6 +346,9 @@ class SuperConverter { // Update the comments.xml file this.#updateCommentsFiles(params.exportedCommentDefs, commentsExportType); + + // Update content types + this.#prepareCommentsForExport(); // Store the SuperDoc version storeSuperdocVersion(this.convertedXml); @@ -353,6 +356,13 @@ class SuperConverter { return xml; } + /** + * Update [Content_Types].xml and docment.xml.rels for comments + */ + #prepareCommentsForExport() { + this.convertedXml = updateContentTypes(this.convertedXml); + } + /** * Update the comments.xml file with the exported comments if necessary * diff --git a/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js b/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js index 22d02ec9c3..974eb38370 100644 --- a/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js +++ b/packages/super-editor/src/core/super-converter/v2/exporter/commentsExporter.js @@ -121,3 +121,141 @@ export const updateCommentsXml = (commentDefs = [], commentsXml) => { newCommentsXml.elements[0].elements = commentDefs; return newCommentsXml; }; + +/** + * Find a content type by the part name in the [Content_Types].xml file + * + * @param {Strign} name The part name to find + * @param {Object} convertedXml The converted XML object + * @returns {Object | undefined} The content type object + */ +const findContentTypeByPartName = (name, convertedXml) => { + const def = convertedXml['[Content_Types].xml']; + const types = def.elements[0].elements; + return types.find((t) => { + const { PartName } = t.attributes; + if (PartName === name) return true; + }); +}; + +/** + * Update document.xml.rels and [Content_Types].xml to include comments + * + * @param {Object} convertedXml The converted XML object + * @returns {Object} The updated XML object + */ +export const updateContentTypes = (convertedXml) => { + const def = convertedXml['[Content_Types].xml']; + const types = def.elements[0].elements; + + // Rels file + const relsData = convertedXml['word/_rels/document.xml.rels']; + const relationships = relsData?.elements?.find((x) => x.name === 'Relationships') || []; + const maxId = Math.max(...relationships.elements.map((el) => Number(el.attributes.Id.replace('rId', '')))); + let currentId = maxId + 1; + + const hasCommentsOverride = findContentTypeByPartName('/word/comments.xml', convertedXml); + if (!hasCommentsOverride) { + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/comments.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml', + }, + }); + }; + + const commentsRel = relationships.elements.find((el) => el.attributes.Target === 'comments.xml'); + if (!commentsRel) { + relationships.elements.push({ + type: 'element', + name: 'Relationship', + attributes: { + Id: `rId${currentId}`, + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments', + Target: 'comments.xml', + }, + }); + } + + const hasCommentsExtendedOverride = findContentTypeByPartName('/word/commentsExtended.xml', convertedXml); + if (!hasCommentsExtendedOverride) { + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsExtended.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml', + }, + }); + }; + + const commentsExtendedRel = relationships.elements.find((el) => el.attributes.Target === 'commentsExtended.xml'); + if (!commentsExtendedRel) { + currentId += 1; + relationships.elements.push({ + type: 'element', + name: 'Relationship', + attributes: { + Id: `rId1${currentId}`, + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentsExtended', + Target: 'commentsExtended.xml', + }, + }); + } + + const hasCommentsIdsOverride = findContentTypeByPartName('/word/commentsIds.xml', convertedXml); + if (!hasCommentsIdsOverride) { + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsIds.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml', + }, + }); + }; + + const commentsIdsRel = relationships.elements.find((el) => el.attributes.Target === 'commentsIds.xml'); + if (!commentsIdsRel) { + currentId += 1; + relationships.elements.push({ + type: 'element', + name: 'Relationship', + attributes: { + Id: `rId1${currentId}`, + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentsIds', + Target: 'commentsIds.xml', + }, + }); + } + + const hasCommentsExtensibleOverride = findContentTypeByPartName('/word/commentsExtensible.xml', convertedXml); + if (!hasCommentsExtensibleOverride) { + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsExtensible.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml', + }, + }); + }; + + const commentsExtensibleRel = relationships.elements.find((el) => el.attributes.Target === 'commentsExtensible.xml'); + if (!commentsExtensibleRel) { + currentId += 1; + relationships.elements.push({ + type: 'element', + name: 'Relationship', + attributes: { + Id: `rId1${currentId}`, + Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentsExtensible', + Target: 'commentsExtensible.xml', + }, + }); + } + + return convertedXml; +}; diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index 67a44b740a..bc0f14e97c 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { HocuspocusProviderWebsocket } from '@hocuspocus/provider'; import { DOCX, PDF, HTML } from '@harbour-enterprises/common'; -import { SuperToolbar } from '@harbour-enterprises/super-editor'; +import { SuperToolbar, createZip } from '@harbour-enterprises/super-editor'; import { SuperComments } from '../components/CommentsLayer/commentsList/super-comments-list.js'; import { createSuperdocVueApp } from './create-app'; import { shuffleArray } from '@harbour-enterprises/common/collaboration/awareness.js'; @@ -471,12 +471,18 @@ export class SuperDoc extends EventEmitter { this.emit('locked', { isLocked, lockedBy }); } - async export({ exportType = ['docx'], commentsType, exportedName }) { + async export({ + exportType = ['docx'], + commentsType, + exportedName, + additionalFiles = [], + additionalFileNames = [] + }) { // Get the docx files first const baseFileName = exportedName ? cleanName(exportedName) : cleanName(this.config.title); const docxFiles = await this.exportEditorsToDOCX({ commentsType }); - const blobsToZip = []; - const filenames = []; + const blobsToZip = [...additionalFiles]; + const filenames = [...additionalFileNames]; // If we are exporting docx files, add them to the zip if (exportType.includes('docx')) {