Skip to content

Commit 0803d04

Browse files
committed
refactor: streamline document identifier handling in Editor and SuperConverter
- Updated the Editor class to simplify document modification tracking and promote to GUID when necessary. - Refactored methods for retrieving document identifiers, ensuring clarity and consistency in handling GUIDs and hashes. - Deprecated outdated methods for backward compatibility while enhancing telemetry data retrieval. - Adjusted related tests to accommodate asynchronous changes in document identifier generation.
1 parent 7211c69 commit 0803d04

3 files changed

Lines changed: 100 additions & 89 deletions

File tree

packages/super-editor/src/core/Editor.js

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ export class Editor extends EventEmitter {
917917
return SuperConverter.getDocumentGuid(doc);
918918
}
919919

920-
// Deprecated method for backward compatibility
920+
// Deprecated
921921
/**
922922
* @deprecated use setDocumentVersion instead
923923
*/
@@ -1361,8 +1361,14 @@ export class Editor extends EventEmitter {
13611361
return;
13621362
}
13631363

1364-
// Document modified - promote to GUID if needed
1365-
this.#ensureDocumentGuid();
1364+
// Track document modifications and promote to GUID if needed
1365+
if (transaction.docChanged && this.converter) {
1366+
if (!this.converter.documentGuid) {
1367+
this.converter.promoteToGuid();
1368+
console.debug('Document modified - assigned GUID:', this.converter.documentGuid);
1369+
}
1370+
this.converter.documentModified = true;
1371+
}
13661372

13671373
this.emit('update', {
13681374
editor: this,
@@ -1371,34 +1377,19 @@ export class Editor extends EventEmitter {
13711377
}
13721378

13731379
/**
1374-
* Ensure document has a permanent GUID if it's been modified
1375-
* @private
1376-
* @returns {void}
1380+
* Get document identifier for telemetry (async - may generate hash)
1381+
* @returns {Promise<string>} GUID for modified docs, hash for unmodified
13771382
*/
1378-
#ensureDocumentGuid() {
1379-
if (!this.converter) return;
1380-
1381-
// If we only have a hash, promote to GUID
1382-
if (this.converter.hasTemporaryId()) {
1383-
this.converter.promoteToGuid();
1384-
console.debug('Document modified - assigned permanent GUID');
1385-
}
1383+
async getDocumentIdentifier() {
1384+
return (await this.converter?.getDocumentIdentifier()) || null;
13861385
}
13871386

13881387
/**
1389-
* Get the document GUID
1390-
* @returns {string|null} The unique document GUID
1388+
* Get permanent document GUID (sync - only for modified documents)
1389+
* @returns {string|null} GUID or null if document hasn't been modified
13911390
*/
13921391
getDocumentGuid() {
1393-
return this.converter?.getDocumentGuid() || null;
1394-
}
1395-
1396-
/**
1397-
* Get document identifier for telemetry (works for both modified and unmodified)
1398-
* @returns {string|null} GUID for modified docs, hash for unmodified
1399-
*/
1400-
getDocumentIdentifier() {
1401-
return this.converter?.getDocumentIdentifier() || null;
1392+
return this.converter?.documentGuid || null;
14021393
}
14031394

14041395
/**
@@ -1410,18 +1401,23 @@ export class Editor extends EventEmitter {
14101401
}
14111402

14121403
/**
1413-
* Get telemetry data including document identifier
1414-
* @returns {Object} Telemetry data object
1404+
* Get telemetry data (async because of lazy hash generation)
14151405
*/
1416-
getTelemetryData() {
1406+
async getTelemetryData() {
14171407
return {
1418-
documentId: this.getDocumentIdentifier(), // Hash or GUID
1408+
documentId: await this.getDocumentIdentifier(),
14191409
isModified: this.isDocumentModified(),
1420-
isPermanentId: !this.converter?.hasTemporaryId(),
1410+
isPermanentId: !!this.converter?.documentGuid,
14211411
version: this.converter?.getSuperdocVersion(),
14221412
};
14231413
}
14241414

1415+
// Deprecated for backward compatibility
1416+
getDocumentId() {
1417+
console.warn('getDocumentId is deprecated, use getDocumentGuid instead');
1418+
return this.getDocumentGuid();
1419+
}
1420+
14251421
/**
14261422
* Get attrs of the currently selected node or mark.
14271423
* @param {String} nameOrType
@@ -1457,8 +1453,7 @@ export class Editor extends EventEmitter {
14571453
return {
14581454
...json,
14591455
metadata: {
1460-
documentId: this.getDocumentIdentifier(),
1461-
documentGuid: this.converter?.documentGuid || null, // Only if modified
1456+
documentGuid: this.converter?.documentGuid || null,
14621457
isModified: this.isDocumentModified(),
14631458
version: this.converter?.getSuperdocVersion() || null,
14641459
},
@@ -1618,6 +1613,7 @@ export class Editor extends EventEmitter {
16181613
const json = this.#prepareDocumentForExport(comments);
16191614

16201615
// Export the document to DOCX
1616+
// GUID will be handled automatically in converter.exportToDocx if document was modified
16211617
const documentXml = await this.converter.exportToDocx(
16221618
json,
16231619
this.schema,

packages/super-editor/src/core/super-converter/SuperConverter.js

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ class SuperConverter {
132132
this.fileSource = params?.fileSource || null;
133133
this.documentId = params?.documentId || null;
134134

135-
// Two-tier identification system
135+
// Document identification
136136
this.documentGuid = null; // Permanent GUID for modified documents
137137
this.documentHash = null; // Temporary hash for unmodified documents
138-
this.documentModified = false; // Track if document has been modified
138+
this.documentModified = false; // Track if document has been edited
139139

140140
// Parse the initial XML, if provided
141141
if (this.docx.length || this.xml) this.parseFromXml();
@@ -166,8 +166,8 @@ class SuperConverter {
166166
if (!this.initialJSON) this.initialJSON = this.parseXmlToJson(this.xml);
167167
this.declaration = this.initialJSON?.declaration;
168168

169-
// Resolve document identifier (GUID or hash)
170-
this.resolveDocumentIdentifier();
169+
// Only resolve existing GUIDs synchronously (no hash generation yet)
170+
this.resolveDocumentGuid();
171171
}
172172

173173
parseXmlToJson(xml) {
@@ -322,90 +322,96 @@ class SuperConverter {
322322
}
323323

324324
/**
325-
* Resolve document identifier - check for existing GUID or generate hash
325+
* Resolve existing document GUID (synchronous)
326326
*/
327-
resolveDocumentIdentifier() {
328-
// Only run this once during initialization
329-
if (this.documentGuid || this.documentHash) return;
330-
331-
// 1. Check for Microsoft's docId (READ ONLY - never modify settings.xml)
332-
try {
333-
this.getDocumentInternalId(); // This sets this.documentInternalId
334-
if (this.documentInternalId) {
335-
this.documentGuid = this.documentInternalId.replace(/[{}]/g, '');
336-
return;
337-
}
338-
} catch {
339-
// Continue to next check
327+
resolveDocumentGuid() {
328+
// 1. Check Microsoft's docId (READ ONLY)
329+
const microsoftGuid = this.getMicrosoftDocId();
330+
if (microsoftGuid) {
331+
this.documentGuid = microsoftGuid;
332+
return;
340333
}
341334

342335
// 2. Check our custom property
343336
const customGuid = SuperConverter.getStoredCustomProperty(this.docx, 'DocumentGuid');
344337
if (customGuid) {
345338
this.documentGuid = customGuid;
346-
return;
347339
}
340+
// Don't generate hash here - do it lazily when needed
341+
}
348342

349-
// 3. Generate hash for unmodified document (telemetry only)
350-
this.documentHash = this.generateDocumentHash();
343+
/**
344+
* Get Microsoft's docId from settings.xml (READ ONLY)
345+
*/
346+
getMicrosoftDocId() {
347+
this.getDocumentInternalId(); // Existing method
348+
if (this.documentInternalId) {
349+
return this.documentInternalId.replace(/[{}]/g, '');
350+
}
351+
return null;
351352
}
352353

353354
/**
354-
* Generate hash for unmodified documents
355-
* @returns {string|null} Hash-based temporary identifier
355+
* Generate document hash for telemetry (async, lazy)
356356
*/
357-
generateDocumentHash() {
358-
if (!this.fileSource) return null;
357+
async generateDocumentHash() {
358+
if (!this.fileSource) return `HASH-${Date.now()}`;
359359

360360
try {
361361
let buffer;
362+
362363
if (Buffer.isBuffer(this.fileSource)) {
363364
buffer = this.fileSource;
364365
} else if (this.fileSource instanceof ArrayBuffer) {
365366
buffer = Buffer.from(this.fileSource);
367+
} else if (this.fileSource instanceof Blob || this.fileSource instanceof File) {
368+
const arrayBuffer = await this.fileSource.arrayBuffer();
369+
buffer = Buffer.from(arrayBuffer);
366370
} else {
367-
// For File or Blob objects
368-
buffer = Buffer.from(this.fileSource);
371+
return `HASH-${Date.now()}`;
369372
}
370373

371374
const hash = crc32(buffer);
372375
return `HASH-${hash.toString('hex').toUpperCase()}`;
373376
} catch (e) {
374377
console.warn('Could not generate document hash:', e);
375-
return `HASH-${Date.now()}`; // Fallback
378+
return `HASH-${Date.now()}`;
376379
}
377380
}
378381

379382
/**
380-
* Get document identifier (GUID or hash)
381-
* @returns {string|null} Either permanent GUID or temporary hash
383+
* Get document identifier (GUID or hash) - async for lazy hash generation
382384
*/
383-
getDocumentIdentifier() {
384-
return this.documentGuid || this.documentHash || null;
385+
async getDocumentIdentifier() {
386+
if (this.documentGuid) {
387+
return this.documentGuid;
388+
}
389+
390+
if (!this.documentHash && this.fileSource) {
391+
this.documentHash = await this.generateDocumentHash();
392+
}
393+
394+
return this.documentHash;
385395
}
386396

387397
/**
388-
* Check if this is a temporary identifier
389-
* @returns {boolean}
398+
* Check if using temporary ID
390399
*/
391400
hasTemporaryId() {
392-
const id = this.getDocumentIdentifier();
393-
return id && id.startsWith('HASH-');
401+
// Has temporary ID if no GUID but has hash (or could generate one)
402+
return !this.documentGuid && !!(this.documentHash || this.fileSource);
394403
}
395404

396405
/**
397-
* Convert temporary hash to permanent GUID
398-
* @returns {string} The new or existing GUID
406+
* Promote from hash to GUID on first edit
399407
*/
400408
promoteToGuid() {
401409
if (this.documentGuid) return this.documentGuid;
402410

403-
// Generate new GUID
404-
this.documentGuid = uuidv4();
411+
this.documentGuid = this.getMicrosoftDocId() || uuidv4();
405412
this.documentModified = true;
406413
this.documentHash = null; // Clear temporary hash
407414

408-
console.debug('Document promoted from hash to GUID:', this.documentGuid);
409415
return this.documentGuid;
410416
}
411417

@@ -650,18 +656,17 @@ class SuperConverter {
650656
// Update the rels table
651657
this.#exportProcessNewRelationships([...params.relationships, ...commentsRels, ...headFootRels]);
652658

653-
// Only store GUID in custom.xml if document was modified
654-
if (this.documentModified && this.documentGuid) {
655-
// Store SuperDoc version (indicates document was edited)
656-
SuperConverter.setStoredSuperdocVersion(this.convertedXml);
657-
658-
// Store document GUID in custom.xml (never modify settings.xml)
659-
this.documentGuid = SuperConverter.setStoredCustomProperty(
660-
this.convertedXml,
661-
'DocumentGuid',
662-
this.documentGuid,
663-
true, // preserve existing
664-
);
659+
// Store SuperDoc version
660+
SuperConverter.setStoredSuperdocVersion(this.convertedXml);
661+
662+
// Store document GUID if document was modified
663+
if (this.documentModified || this.documentGuid) {
664+
if (!this.documentGuid) {
665+
this.documentGuid = this.getMicrosoftDocId() || uuidv4();
666+
}
667+
668+
// Always store in custom.xml (never modify settings.xml)
669+
SuperConverter.setStoredCustomProperty(this.convertedXml, 'DocumentGuid', this.documentGuid, true);
665670
}
666671

667672
// Update the numbering.xml
@@ -921,6 +926,12 @@ class SuperConverter {
921926
this.addedMedia = processedData;
922927
}
923928

929+
// Deprecated methods for backward compatibility
930+
static getStoredSuperdocId(docx) {
931+
console.warn('getStoredSuperdocId is deprecated, use getDocumentGuid instead');
932+
return SuperConverter.getDocumentGuid(docx);
933+
}
934+
924935
static updateDocumentVersion(docx, version) {
925936
console.warn('updateDocumentVersion is deprecated, use setStoredSuperdocVersion instead');
926937
return SuperConverter.setStoredSuperdocVersion(docx, version);

packages/super-editor/src/core/super-converter/SuperConverter.test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,29 +72,33 @@ describe('SuperConverter Document GUID', () => {
7272
expect(converter.hasTemporaryId()).toBe(false);
7373
});
7474

75-
it('generates hash for unmodified document without GUID', () => {
75+
it('generates hash for unmodified document without GUID', async () => {
7676
const fileSource = Buffer.from('test file content');
7777
const converter = new SuperConverter({
7878
docx: mockDocx,
7979
fileSource,
8080
});
8181

82-
const identifier = converter.getDocumentIdentifier();
82+
// getDocumentIdentifier is now async
83+
const identifier = await converter.getDocumentIdentifier();
8384
expect(identifier).toMatch(/^HASH-/);
8485
expect(converter.hasTemporaryId()).toBe(true);
8586
expect(converter.getDocumentGuid()).toBeNull();
8687
});
8788
});
8889

8990
describe('GUID Promotion', () => {
90-
it('promotes hash to GUID when document is modified', () => {
91+
it('promotes hash to GUID when document is modified', async () => {
9192
const fileSource = Buffer.from('test file content');
9293
const converter = new SuperConverter({
9394
docx: mockDocx,
9495
fileSource,
9596
});
9697

97-
// Initially has hash
98+
// Generate hash first (async)
99+
await converter.getDocumentIdentifier();
100+
101+
// Now check if has temporary ID
98102
expect(converter.hasTemporaryId()).toBe(true);
99103

100104
// Promote to GUID

0 commit comments

Comments
 (0)