mirror of
https://github.com/optilude/xlsx-template.git
synced 2026-07-02 00:17:39 +08:00
Fix: copySheet() now properly copies sheets with comments (#207)
- Copy comment-related files (comments.xml, threadedComment.xml, vmlDrawing.vml) - Generate unique UUIDs for copied comments while preserving personId - Add Content-Types declarations for new files - Preserve UTF-8 encoding using binary mode
This commit is contained in:
+101
-5
@@ -104,16 +104,23 @@ class Workbook {
|
||||
// Copy sheet file
|
||||
self.archive.file(arcName, etree.tostring(sheet.root));
|
||||
self.archive.files[arcName].options.binary = binary;
|
||||
|
||||
// Add content type for the new sheet
|
||||
var sheetContentType = etree.SubElement(self.contentTypes, 'Override');
|
||||
sheetContentType.attrib.PartName = '/' + arcName;
|
||||
sheetContentType.attrib.ContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
|
||||
|
||||
// copy sheet name in workbook
|
||||
var newSheet = etree.SubElement(self.workbook.find('sheets'), 'sheet');
|
||||
newSheet.attrib.name = copyName || 'Sheet' + newSheetIndex;
|
||||
const finalSheetName = copyName || 'Sheet' + newSheetIndex;
|
||||
newSheet.attrib.name = finalSheetName;
|
||||
newSheet.attrib.sheetId = newSheetIndex;
|
||||
newSheet.attrib['r:id'] = 'rId' + newSheetIndex;
|
||||
// Copy definedName if any
|
||||
self.workbook.findall('definedNames/definedName').forEach(element => {
|
||||
if (element.text && element.text.split("!").length && element.text.split("!")[0] == sheetName) {
|
||||
var newDefinedName = etree.SubElement(self.workbook.find('definedNames'), 'definedName', element.attrib);
|
||||
newDefinedName.text = `${copyName}!${element.text.split("!")[1]}`;
|
||||
newDefinedName.text = `${finalSheetName}!${element.text.split("!")[1]}`;
|
||||
newDefinedName.attrib.localSheetId = newSheetIndex - 1;
|
||||
}
|
||||
});
|
||||
@@ -122,11 +129,85 @@ class Workbook {
|
||||
newRel.attrib.Type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
|
||||
newRel.attrib.Target = fileName;
|
||||
|
||||
//Copy rels sheet - TODO : Maybe we can copy also the 'Target' files in rels, but Excel make this automaticly
|
||||
// Copy sheet relationships and their target files
|
||||
var sourceRels = self.loadSheetRels(sheet.filename);
|
||||
var relFileName = 'worksheets' + '/_rels/' + 'sheet' + newSheetIndex + '.xml.rels';
|
||||
var relArcName = self.prefix + '/' + relFileName;
|
||||
self.archive.file(relArcName, etree.tostring(self.loadSheetRels(sheet.filename).root));
|
||||
self.archive.files[relArcName].options.binary = true;
|
||||
var newRelsRoot = self.cloneElement(sourceRels.root, true);
|
||||
|
||||
// Generate a new UUID for comments (shared between comments.xml and threadedComment.xml)
|
||||
var newCommentUuid = self.generateUUID();
|
||||
|
||||
// Process each relationship to copy target files with unique names
|
||||
sourceRels.root.findall('Relationship').forEach(function(rel, index) {
|
||||
var relType = rel.attrib.Type;
|
||||
var target = rel.attrib.Target;
|
||||
|
||||
// Relationship types that require copying the target file
|
||||
var needsFileCopy = [
|
||||
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
|
||||
'http://schemas.microsoft.com/office/2017/10/relationships/threadedComment',
|
||||
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing'
|
||||
];
|
||||
|
||||
if (needsFileCopy.indexOf(relType) !== -1) {
|
||||
var sheetDirectory = path.dirname(sheet.filename);
|
||||
var sourceFilePath = path.join(sheetDirectory, target).replace(/\\/g, '/');
|
||||
var sourceFile = self.archive.file(sourceFilePath);
|
||||
|
||||
if (sourceFile) {
|
||||
// Generate unique file name based on newSheetIndex
|
||||
var fileExtension = path.extname(target);
|
||||
var fileBaseName = path.basename(target, fileExtension);
|
||||
var fileDir = path.dirname(target);
|
||||
var baseNameWithoutNumber = fileBaseName.replace(/\d+$/, '');
|
||||
var newFileName = baseNameWithoutNumber + newSheetIndex + fileExtension;
|
||||
var newTarget = path.join(fileDir, newFileName).replace(/\\/g, '/');
|
||||
var newFilePath = path.join(sheetDirectory, newTarget).replace(/\\/g, '/');
|
||||
|
||||
// Copy file in binary mode to preserve UTF-8 encoding
|
||||
var binaryContent = sourceFile.asBinary();
|
||||
|
||||
// Apply file-specific transformations
|
||||
if (relType === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing') {
|
||||
// Update VML data attribute to make it unique per sheet
|
||||
binaryContent = binaryContent.replace(/data="\d+"/, 'data="' + newSheetIndex + '"');
|
||||
|
||||
} else if (relType === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments') {
|
||||
// Replace comment UUIDs in both <author>tc= and xr:uid= attributes
|
||||
var uuidWithoutBraces = newCommentUuid.replace(/[{}]/g, '');
|
||||
binaryContent = binaryContent.replace(/(<author>tc=\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}<\/author>)/gi, '$1' + uuidWithoutBraces + '$2');
|
||||
binaryContent = binaryContent.replace(/(xr:uid="\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}\")/gi, '$1' + uuidWithoutBraces + '$2');
|
||||
|
||||
var commentsContentType = etree.SubElement(self.contentTypes, 'Override');
|
||||
commentsContentType.attrib.PartName = '/' + newFilePath;
|
||||
commentsContentType.attrib.ContentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml';
|
||||
|
||||
} else if (relType === 'http://schemas.microsoft.com/office/2017/10/relationships/threadedComment') {
|
||||
// Replace only the comment UUID in id= attribute (preserve personId)
|
||||
var uuidWithoutBraces = newCommentUuid.replace(/[{}]/g, '');
|
||||
binaryContent = binaryContent.replace(/(\sid="\{)[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}(\}")/gi, '$1' + uuidWithoutBraces + '$2');
|
||||
|
||||
var threadedCommentContentType = etree.SubElement(self.contentTypes, 'Override');
|
||||
threadedCommentContentType.attrib.PartName = '/' + newFilePath;
|
||||
threadedCommentContentType.attrib.ContentType = 'application/vnd.ms-excel.threadedcomments+xml';
|
||||
}
|
||||
|
||||
self.archive.file(newFilePath, binaryContent);
|
||||
self.archive.files[newFilePath].options.binary = true; // Force binary mode to preserve UTF-8 encoding
|
||||
|
||||
// Update relationship target in the copied rels file
|
||||
var newRelInRels = newRelsRoot.findall('Relationship')[index];
|
||||
if (newRelInRels) {
|
||||
newRelInRels.attrib.Target = newTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.archive.file(relArcName, etree.tostring(newRelsRoot));
|
||||
self.archive.files[relArcName].options.binary = binary;
|
||||
self.archive.file('[Content_Types].xml', etree.tostring(self.contentTypes));
|
||||
|
||||
self._rebuild();
|
||||
return self;
|
||||
@@ -901,6 +982,21 @@ class Workbook {
|
||||
|
||||
return str;
|
||||
}
|
||||
// Generate a UUID v4 for comment IDs
|
||||
generateUUID() {
|
||||
// Format: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
|
||||
var hexDigits = '0123456789ABCDEF';
|
||||
var uuid = '{';
|
||||
for (var i = 0; i < 36; i++) {
|
||||
if (i === 8 || i === 13 || i === 18 || i === 23) {
|
||||
uuid += '-';
|
||||
} else {
|
||||
uuid += hexDigits[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
}
|
||||
uuid += '}';
|
||||
return uuid;
|
||||
}
|
||||
// Is ref a range?
|
||||
isRange(ref) {
|
||||
return ref.indexOf(':') !== -1;
|
||||
|
||||
@@ -1530,6 +1530,84 @@ describe("CRUD operations", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("copies sheets with comments", function(done) {
|
||||
|
||||
fs.readFile(path.join(__dirname, "templates", "test-copy-sheet-with-comments.xlsx"), function(err, data) {
|
||||
expect(err).toBeNull();
|
||||
try {
|
||||
var t = new XlsxTemplate(data);
|
||||
t.copySheet("Feuil1", "Feuil3", true);
|
||||
var newData = t.generate();
|
||||
|
||||
// Verify that each sheet has its own comment files
|
||||
var sheet1Rels = etree.parse(t.archive.file("xl/worksheets/_rels/sheet1.xml.rels").asText()).getroot();
|
||||
var sheet3Rels = etree.parse(t.archive.file("xl/worksheets/_rels/sheet3.xml.rels").asText()).getroot();
|
||||
var sheet1CommentsRel = sheet1Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments']");
|
||||
var sheet3CommentsRel = sheet3Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments']");
|
||||
var sheet1ThreadedCommentsRel = sheet1Rels.find("./Relationship[@Type='http://schemas.microsoft.com/office/2017/10/relationships/threadedComment']");
|
||||
var sheet3ThreadedCommentsRel = sheet3Rels.find("./Relationship[@Type='http://schemas.microsoft.com/office/2017/10/relationships/threadedComment']");
|
||||
var sheet1VmlDrawingRel = sheet1Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing']");
|
||||
var sheet3VmlDrawingRel = sheet3Rels.find("./Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing']");
|
||||
|
||||
// Relationship Target must be different
|
||||
expect(sheet1CommentsRel.attrib.Target).not.toEqual(sheet3CommentsRel.attrib.Target);
|
||||
expect(sheet1ThreadedCommentsRel.attrib.Target).not.toEqual(sheet3ThreadedCommentsRel.attrib.Target);
|
||||
expect(sheet1VmlDrawingRel.attrib.Target).not.toEqual(sheet3VmlDrawingRel.attrib.Target);
|
||||
|
||||
// Verify copied files exist with correct numeric naming
|
||||
expect(t.archive.file("xl/comments3.xml")).not.toBeNull();
|
||||
expect(t.archive.file("xl/threadedComments/threadedComment3.xml")).not.toBeNull();
|
||||
expect(t.archive.file("xl/drawings/vmlDrawing3.vml")).not.toBeNull();
|
||||
|
||||
// Verify Content Types are registered
|
||||
var contentTypes = etree.parse(t.archive.file("[Content_Types].xml").asText()).getroot();
|
||||
expect(contentTypes.find("./Override[@PartName='/xl/worksheets/sheet3.xml']")).not.toBeNull();
|
||||
expect(contentTypes.find("./Override[@PartName='/xl/comments3.xml']")).not.toBeNull();
|
||||
expect(contentTypes.find("./Override[@PartName='/xl/threadedComments/threadedComment3.xml']")).not.toBeNull();
|
||||
|
||||
|
||||
// Verify UUIDs are consistent across comment files
|
||||
var comments3Content = t.archive.file("xl/comments3.xml").asText();
|
||||
var threadedComment3Content = t.archive.file("xl/threadedComments/threadedComment3.xml").asText();
|
||||
var threadedComment1Content = t.archive.file("xl/threadedComments/threadedComment1.xml").asText();
|
||||
|
||||
var commentsUuidMatch = comments3Content.match(/xr:uid="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i);
|
||||
expect(commentsUuidMatch).not.toBeNull();
|
||||
var commentsUuid = commentsUuidMatch ? commentsUuidMatch[1] : null;
|
||||
|
||||
var commentsAuthorUuidMatch = comments3Content.match(/<author>tc=\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}<\/author>/i);
|
||||
expect(commentsAuthorUuidMatch).not.toBeNull();
|
||||
var commentsAuthorUuid = commentsAuthorUuidMatch ? commentsAuthorUuidMatch[1] : null;
|
||||
|
||||
var threadedCommentUuidMatch = threadedComment3Content.match(/\sid="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i);
|
||||
expect(threadedCommentUuidMatch).not.toBeNull();
|
||||
var threadedCommentUuid = threadedCommentUuidMatch ? threadedCommentUuidMatch[1] : null;
|
||||
|
||||
// All three UUIDs must match
|
||||
expect(commentsUuid).toEqual(commentsAuthorUuid);
|
||||
expect(commentsUuid).toEqual(threadedCommentUuid);
|
||||
|
||||
// Verify personId is preserved from original sheet (references persons/person.xml)
|
||||
var personIdMatch = threadedComment3Content.match(/personId="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i);
|
||||
expect(personIdMatch).not.toBeNull();
|
||||
var personId = personIdMatch ? personIdMatch[1] : null;
|
||||
expect(personId).not.toEqual(commentsUuid);
|
||||
|
||||
var personId1Match = threadedComment1Content.match(/personId="\{([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\}"/i);
|
||||
expect(personId1Match).not.toBeNull();
|
||||
var personId1 = personId1Match ? personId1Match[1] : null;
|
||||
expect(personId).toEqual(personId1);
|
||||
|
||||
fs.writeFileSync("test/output/copy-sheet-with-comments.xlsx", newData, "binary");
|
||||
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rebuild file", function() {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user