@@ -4,6 +4,7 @@ import { getContentTypesFromXml, base64ToUint8Array, detectImageType } from './s
44import { ensureXmlString , isXmlLike } from './encoding-helpers.js' ;
55import { DOCX } from '@superdoc/common' ;
66import { COMMENT_FILE_BASENAMES } from './super-converter/constants.js' ;
7+ import { syncPackageMetadata } from './opc/sync-package-metadata.js' ;
78
89/** Image file extensions recognized during import and export. */
910const IMAGE_EXTS = new Set ( [ 'png' , 'jpg' , 'jpeg' , 'gif' , 'bmp' , 'tiff' , 'tif' , 'emf' , 'wmf' , 'svg' , 'webp' ] ) ;
@@ -313,6 +314,47 @@ class DocxZipper {
313314 docx . file ( contentTypesPath , updatedContentTypesXml ) ;
314315 }
315316
317+ /**
318+ * Run the OPC package metadata synchronizer against a JSZip instance.
319+ *
320+ * Reads [Content_Types].xml and _rels/.rels from the zip, reconciles
321+ * managed package-level parts, and writes the corrected files back.
322+ *
323+ * The assembled zip is treated as the single source of truth — no stale
324+ * updatedDocs are passed, so the synchronizer sees exactly what
325+ * updateContentTypes() already wrote.
326+ *
327+ * @param {JSZip } zip - The fully assembled zip to reconcile.
328+ */
329+ async #syncPackageMetadataInZip( zip ) {
330+ // Build a base-files map from the zip's current listing.
331+ // At this point the zip already contains all base + updated + media entries.
332+ const baseForSync = { } ;
333+ zip . forEach ( ( path ) => {
334+ baseForSync [ path ] = '' ; // non-null signals "exists"
335+ } ) ;
336+
337+ // Read the two metadata files the synchronizer needs to parse.
338+ // Use JSZip's async API to correctly handle all internal storage formats.
339+ const ctEntry = zip . file ( '[Content_Types].xml' ) ;
340+ if ( ctEntry ) {
341+ baseForSync [ '[Content_Types].xml' ] = await ctEntry . async ( 'string' ) ;
342+ }
343+ const rlEntry = zip . file ( '_rels/.rels' ) ;
344+ if ( rlEntry ) {
345+ baseForSync [ '_rels/.rels' ] = await rlEntry . async ( 'string' ) ;
346+ }
347+
348+ // Pass an empty updatedDocs — the zip is already the assembled truth.
349+ const { contentTypesXml, relsXml } = syncPackageMetadata ( {
350+ baseFiles : baseForSync ,
351+ updatedDocs : { } ,
352+ } ) ;
353+
354+ zip . file ( '[Content_Types].xml' , contentTypesXml ) ;
355+ zip . file ( '_rels/.rels' , relsXml ) ;
356+ }
357+
316358 async unzip ( file ) {
317359 const zip = await this . zip . loadAsync ( file ) ;
318360 return zip ;
@@ -374,6 +416,10 @@ class DocxZipper {
374416 }
375417
376418 await this . updateContentTypes ( zip , media , false , updatedDocs ) ;
419+
420+ // Reconcile package-level singleton metadata as a final safety pass.
421+ await this . #syncPackageMetadataInZip( zip ) ;
422+
377423 return zip ;
378424 }
379425
@@ -413,6 +459,9 @@ class DocxZipper {
413459
414460 await this . updateContentTypes ( unzippedOriginalDocx , media , false , updatedDocs ) ;
415461
462+ // Reconcile package-level singleton metadata as a final safety pass.
463+ await this . #syncPackageMetadataInZip( unzippedOriginalDocx ) ;
464+
416465 return unzippedOriginalDocx ;
417466 }
418467
0 commit comments