feat(comments): Support comment pictures (#3032)

* feat(comments): Support comment pictures

* fix(demo): Fix the image path to load the image correctly

* fix(test): Update the number of files asserted in the compiler test case

* feat(comments): Support comment pictures

* fix(demo): Fix the image path to load the image correctly

* fix(test): Update the number of files asserted in the compiler test case

* style(src): Format the code and remove extra blank lines

---------

Co-authored-by: Dolan <dolan_miu@hotmail.com>
This commit is contained in:
Terry Sargent
2025-04-16 15:45:36 +08:00
committed by GitHub
parent 5af1045a59
commit b19025881b
4 changed files with 77 additions and 19 deletions

View File

@ -1,7 +1,7 @@
// Simple example to add comments to a document // Simple example to add comments to a document
import * as fs from "fs"; import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun, CommentRangeStart, CommentRangeEnd, CommentReference } from "docx"; import { Document, Packer, Paragraph, TextRun, CommentRangeStart, CommentRangeEnd, CommentReference, ImageRun } from "docx";
const doc = new Document({ const doc = new Document({
comments: { comments: {
@ -20,6 +20,14 @@ const doc = new Document({
}), }),
new Paragraph({ new Paragraph({
children: [ children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/cat.jpg"),
transformation: {
width: 100,
height: 100,
},
}),
new TextRun({ new TextRun({
text: "comment text content", text: "comment text content",
}), }),

View File

@ -36,7 +36,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array); expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(19); expect(fileNames).has.length(20);
expect(fileNames).to.include("word/document.xml"); expect(fileNames).to.include("word/document.xml");
expect(fileNames).to.include("word/styles.xml"); expect(fileNames).to.include("word/styles.xml");
expect(fileNames).to.include("docProps/core.xml"); expect(fileNames).to.include("docProps/core.xml");
@ -96,7 +96,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array); expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(27); expect(fileNames).has.length(28);
expect(fileNames).to.include("word/header1.xml"); expect(fileNames).to.include("word/header1.xml");
expect(fileNames).to.include("word/_rels/header1.xml.rels"); expect(fileNames).to.include("word/_rels/header1.xml.rels");
@ -131,7 +131,7 @@ describe("Compiler", () => {
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array); expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(20); expect(fileNames).has.length(21);
expect(fileNames).to.include("word/comments.xml"); expect(fileNames).to.include("word/comments.xml");
expect(fileNames).to.include("word/commentsExtended.xml"); expect(fileNames).to.include("word/commentsExtended.xml");
@ -163,7 +163,7 @@ describe("Compiler", () => {
const spy = vi.spyOn(compiler["formatter"], "format"); const spy = vi.spyOn(compiler["formatter"], "format");
compiler.compile(file); compiler.compile(file);
expect(spy).toBeCalledTimes(15); expect(spy).toBeCalledTimes(16);
}); });
it("should work with media datas", () => { it("should work with media datas", () => {

View File

@ -32,6 +32,7 @@ type IXmlifyedFileMapping = {
readonly FootNotesRelationships: IXmlifyedFile; readonly FootNotesRelationships: IXmlifyedFile;
readonly Settings: IXmlifyedFile; readonly Settings: IXmlifyedFile;
readonly Comments?: IXmlifyedFile; readonly Comments?: IXmlifyedFile;
readonly CommentsRelationships?: IXmlifyedFile;
readonly FontTable?: IXmlifyedFile; readonly FontTable?: IXmlifyedFile;
readonly FontTableRelationships?: IXmlifyedFile; readonly FontTableRelationships?: IXmlifyedFile;
}; };
@ -104,7 +105,28 @@ export class Compiler {
}, },
}, },
); );
const commentRelationshipCount = file.Comments.Relationships.RelationshipCount + 1;
const commentXmlData = xml(
this.formatter.format(file.Comments, {
viewWrapper: {
View: file.Comments,
Relationships: file.Comments.Relationships,
},
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
);
const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media); const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media);
const commentMediaDatas = this.imageReplacer.getMediaData(commentXmlData, file.Media);
return { return {
Relationships: { Relationships: {
@ -450,21 +472,40 @@ export class Compiler {
path: "word/settings.xml", path: "word/settings.xml",
}, },
Comments: { Comments: {
data: xml( data: (() => {
this.formatter.format(file.Comments, { const xmlData = this.imageReplacer.replace(commentXmlData, commentMediaDatas, commentRelationshipCount);
viewWrapper: file.Document, const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return referenedXmlData;
})(),
path: "word/comments.xml",
},
CommentsRelationships: {
data: (() => {
commentMediaDatas.forEach((mediaData, i) => {
file.Comments.Relationships.createRelationship(
commentRelationshipCount + i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
});
return xml(
this.formatter.format(file.Comments.Relationships, {
viewWrapper: {
View: file.Comments,
Relationships: file.Comments.Relationships,
},
file, file,
stack: [], stack: [],
}), }),
{ {
indent: prettify, indent: prettify,
declaration: { declaration: {
standalone: "yes",
encoding: "UTF-8", encoding: "UTF-8",
}, },
}, },
), );
path: "word/comments.xml", })(),
path: "word/_rels/comments.xml.rels",
}, },
FontTable: { FontTable: {
data: xml( data: xml(

View File

@ -1,4 +1,5 @@
import { FileChild } from "@file/file-child"; import { FileChild } from "@file/file-child";
import { Relationships } from "@file/relationships";
import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
export type ICommentOptions = { export type ICommentOptions = {
@ -136,6 +137,8 @@ export class Comment extends XmlComponent {
} }
} }
export class Comments extends XmlComponent { export class Comments extends XmlComponent {
private readonly relationships: Relationships;
public constructor({ children }: ICommentsOptions) { public constructor({ children }: ICommentsOptions) {
super("w:comments"); super("w:comments");
@ -178,5 +181,11 @@ export class Comments extends XmlComponent {
for (const child of children) { for (const child of children) {
this.root.push(new Comment(child)); this.root.push(new Comment(child));
} }
this.relationships = new Relationships();
}
public get Relationships(): Relationships {
return this.relationships;
} }
} }