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
49 changes: 49 additions & 0 deletions packages/super-editor/src/core/DocxZipper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getContentTypesFromXml, base64ToUint8Array, detectImageType } from './s
import { ensureXmlString, isXmlLike } from './encoding-helpers.js';
import { DOCX } from '@superdoc/common';
import { COMMENT_FILE_BASENAMES } from './super-converter/constants.js';
import { syncPackageMetadata } from './opc/sync-package-metadata.js';

/** Image file extensions recognized during import and export. */
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'emf', 'wmf', 'svg', 'webp']);
Expand Down Expand Up @@ -313,6 +314,47 @@ class DocxZipper {
docx.file(contentTypesPath, updatedContentTypesXml);
}

/**
* Run the OPC package metadata synchronizer against a JSZip instance.
*
* Reads [Content_Types].xml and _rels/.rels from the zip, reconciles
* managed package-level parts, and writes the corrected files back.
*
* The assembled zip is treated as the single source of truth — no stale
* updatedDocs are passed, so the synchronizer sees exactly what
* updateContentTypes() already wrote.
*
* @param {JSZip} zip - The fully assembled zip to reconcile.
*/
async #syncPackageMetadataInZip(zip) {
// Build a base-files map from the zip's current listing.
// At this point the zip already contains all base + updated + media entries.
const baseForSync = {};
zip.forEach((path) => {
baseForSync[path] = ''; // non-null signals "exists"
});

// Read the two metadata files the synchronizer needs to parse.
// Use JSZip's async API to correctly handle all internal storage formats.
const ctEntry = zip.file('[Content_Types].xml');
if (ctEntry) {
baseForSync['[Content_Types].xml'] = await ctEntry.async('string');
}
const rlEntry = zip.file('_rels/.rels');
if (rlEntry) {
baseForSync['_rels/.rels'] = await rlEntry.async('string');
}

// Pass an empty updatedDocs — the zip is already the assembled truth.
const { contentTypesXml, relsXml } = syncPackageMetadata({
baseFiles: baseForSync,
updatedDocs: {},
});

zip.file('[Content_Types].xml', contentTypesXml);
zip.file('_rels/.rels', relsXml);
}

async unzip(file) {
const zip = await this.zip.loadAsync(file);
return zip;
Expand Down Expand Up @@ -374,6 +416,10 @@ class DocxZipper {
}

await this.updateContentTypes(zip, media, false, updatedDocs);

// Reconcile package-level singleton metadata as a final safety pass.
await this.#syncPackageMetadataInZip(zip);

return zip;
}

Expand Down Expand Up @@ -413,6 +459,9 @@ class DocxZipper {

await this.updateContentTypes(unzippedOriginalDocx, media, false, updatedDocs);

// Reconcile package-level singleton metadata as a final safety pass.
await this.#syncPackageMetadataInZip(unzippedOriginalDocx);

return unzippedOriginalDocx;
}

Expand Down
15 changes: 15 additions & 0 deletions packages/super-editor/src/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import type { DocumentApi } from '@superdoc/document-api';
import { createDocumentApi } from '@superdoc/document-api';
import { getDocumentApiAdapters } from '../document-api-adapters/index.js';
import { initPartsRuntime } from './parts/init-parts-runtime.js';
import { syncPackageMetadata } from './opc/sync-package-metadata.js';

declare const __APP_VERSION__: string;
declare const version: string | undefined;
Expand Down Expand Up @@ -2742,6 +2743,20 @@ export class Editor extends EventEmitter<EditorEventMap> {
true,
updatedDocs,
);

// Reconcile package-level singleton metadata (content-type overrides
// and root relationships) against the final set of output entries.
// this.options.content is DocxFileEntry[] | Record<string, unknown> | string | null.
// The synchronizer accepts an array of {name, content} or a key→content map.
const content = this.options.content;
const baseFiles = Array.isArray(content) || (content && typeof content === 'object') ? content : null;
const { contentTypesXml, relsXml } = syncPackageMetadata({
baseFiles: baseFiles as Parameters<typeof syncPackageMetadata>[0]['baseFiles'],
updatedDocs,
});
updatedDocs['[Content_Types].xml'] = contentTypesXml;
updatedDocs['_rels/.rels'] = relsXml;

return updatedDocs;
}

Expand Down
37 changes: 37 additions & 0 deletions packages/super-editor/src/core/opc/managed-parts-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Registry of OPC package-level singleton parts and their required metadata.
*
* Each entry defines a part that must have exactly one content-type Override in
* [Content_Types].xml and exactly one root Relationship in _rels/.rels when the
* part exists in the final package. When the part is absent, both registrations
* must be removed.
*
* Values sourced from ECMA-376 / ISO 29500 and verified against real DOCX
* fixtures produced by Microsoft Word.
*/

/** @typedef {import('./sync-package-metadata.js').ManagedPartEntry} ManagedPartEntry */

/** @type {ManagedPartEntry[]} */
export const MANAGED_PACKAGE_PARTS = [
{
zipPath: 'word/document.xml',
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',
relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
},
{
zipPath: 'docProps/core.xml',
contentType: 'application/vnd.openxmlformats-package.core-properties+xml',
relationshipType: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
},
{
zipPath: 'docProps/app.xml',
contentType: 'application/vnd.openxmlformats-officedocument.extended-properties+xml',
relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
},
{
zipPath: 'docProps/custom.xml',
contentType: 'application/vnd.openxmlformats-officedocument.custom-properties+xml',
relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties',
},
];
Loading
Loading