From 6b03fa713bfd1d99a80e1155e27a3477215753fe Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 4 Mar 2025 14:36:10 -0800 Subject: [PATCH 1/3] Fix PDF export --- packages/superdoc/src/core/SuperDoc.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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')) { From ae67b5e44028e3a76167cc55282a9ee851fadc5a Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 4 Mar 2025 15:22:53 -0800 Subject: [PATCH 2/3] Fix comment export with files that didn't have previous comments infra --- packages/super-editor/src/core/Editor.js | 4 +- .../core/super-converter/SuperConverter.js | 12 +- .../v2/exporter/commentsExporter.js | 136 ++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) 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..66516d9fbc 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,139 @@ 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; + const hasCommentsOverride = findContentTypeByPartName('/word/comments.xml', convertedXml); + + // 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; + + if (!hasCommentsOverride) { + 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', + }, + }); + } + + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/comments.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml', + }, + }); + }; + + const hasCommentsExtendedOverride = findContentTypeByPartName('/word/commentsExtended.xml', convertedXml); + if (!hasCommentsExtendedOverride) { + 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', + }, + }); + } + + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsExtended.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml', + }, + }); + }; + + const hasCommentsIdsOverride = findContentTypeByPartName('/word/commentsIds.xml', convertedXml); + if (!hasCommentsIdsOverride) { + 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', + }, + }); + } + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsIds.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml', + }, + }); + }; + + const hasCommentsExtensibleOverride = findContentTypeByPartName('/word/commentsExtensible.xml', convertedXml); + if (!hasCommentsExtensibleOverride) { + 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', + }, + }); + } + types.push({ + type: 'element', + name: 'Override', + attributes: { + PartName: '/word/commentsExtensible.xml', + ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml', + }, + }); + }; + + return convertedXml; +}; From aa8ccedd05030218917aa104df053d27f64eda7f Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Tue, 4 Mar 2025 16:13:25 -0800 Subject: [PATCH 3/3] Update comments export --- .../v2/exporter/commentsExporter.js | 114 +++++++++--------- 1 file changed, 58 insertions(+), 56 deletions(-) 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 66516d9fbc..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 @@ -147,7 +147,6 @@ const findContentTypeByPartName = (name, convertedXml) => { export const updateContentTypes = (convertedXml) => { const def = convertedXml['[Content_Types].xml']; const types = def.elements[0].elements; - const hasCommentsOverride = findContentTypeByPartName('/word/comments.xml', convertedXml); // Rels file const relsData = convertedXml['word/_rels/document.xml.rels']; @@ -155,20 +154,8 @@ export const updateContentTypes = (convertedXml) => { const maxId = Math.max(...relationships.elements.map((el) => Number(el.attributes.Id.replace('rId', '')))); let currentId = maxId + 1; - if (!hasCommentsOverride) { - 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 hasCommentsOverride = findContentTypeByPartName('/word/comments.xml', convertedXml); + if (!hasCommentsOverride) { types.push({ type: 'element', name: 'Override', @@ -179,22 +166,21 @@ export const updateContentTypes = (convertedXml) => { }); }; + 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) { - 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', - }, - }); - } - types.push({ type: 'element', name: 'Override', @@ -205,21 +191,22 @@ export const updateContentTypes = (convertedXml) => { }); }; + 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) { - 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', - }, - }); - } types.push({ type: 'element', name: 'Override', @@ -229,22 +216,23 @@ export const updateContentTypes = (convertedXml) => { }, }); }; - + + 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) { - 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', - }, - }); - } types.push({ type: 'element', name: 'Override', @@ -255,5 +243,19 @@ export const updateContentTypes = (convertedXml) => { }); }; + 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; };