Skip to content
Open
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
89 changes: 89 additions & 0 deletions src/export/packer/next-compiler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,95 @@ describe("Compiler", () => {
expect(commentsExtendedText).to.contain("w15:paraIdParent");
});

it("should not include commentsIds.xml when comments have no durableId", () => {
const file = new File({
sections: [],
comments: {
children: [{ id: 0, children: [new Paragraph("comment")] }],
},
});
const zipFile = compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).to.not.include("word/commentsIds.xml");
});

it("should include commentsIds.xml for a single (non-threaded) comment with durableId", { timeout: 99999999 }, async () => {
const file = new File({
sections: [],
comments: {
children: [{ id: 0, children: [new Paragraph("comment")], durableId: "12AB34CD" }],
},
});
const zipFile = compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).to.include("word/commentsIds.xml");

const commentsIdsText = await zipFile.file("word/commentsIds.xml")?.async("text");
expect(commentsIdsText).to.contain("w16cid:commentsIds");
expect(commentsIdsText).to.contain("w16cid:commentId");
expect(commentsIdsText).to.contain('w16cid:paraId="00000001"');
expect(commentsIdsText).to.contain('w16cid:durableId="12AB34CD"');

// Content type override is registered
const contentTypesText = await zipFile.file("[Content_Types].xml")?.async("text");
expect(contentTypesText).to.contain("application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml");
expect(contentTypesText).to.contain("/word/commentsIds.xml");

// Relationship is registered
const relsText = await zipFile.file("word/_rels/document.xml.rels")?.async("text");
expect(relsText).to.contain("http://schemas.microsoft.com/office/2016/09/relationships/commentsIds");
expect(relsText).to.contain("commentsIds.xml");
});

it("should include commentsIds.xml mapping paraId to durableId for threaded comments", { timeout: 99999999 }, async () => {
const file = new File({
sections: [],
comments: {
children: [
{ id: 0, children: [new Paragraph("parent")], durableId: "11112222" },
{ id: 1, children: [new Paragraph("reply")], parentId: 0, durableId: "33334444" },
],
},
});
const zipFile = compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).to.include("word/commentsIds.xml");
expect(fileNames).to.include("word/commentsExtended.xml");

const commentsIdsText = await zipFile.file("word/commentsIds.xml")?.async("text");
expect(commentsIdsText).to.contain('w16cid:paraId="00000001"');
expect(commentsIdsText).to.contain('w16cid:durableId="11112222"');
expect(commentsIdsText).to.contain('w16cid:paraId="00000002"');
expect(commentsIdsText).to.contain('w16cid:durableId="33334444"');
});

it("should emit commentsIds.xml falling back to paraId for comments without a durableId", { timeout: 99999999 }, async () => {
const file = new File({
sections: [],
comments: {
children: [
{ id: 0, children: [new Paragraph("with durable")], durableId: "12AB34CD" },
{ id: 1, children: [new Paragraph("without durable")] },
],
},
});
const zipFile = compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);

expect(fileNames).to.include("word/commentsIds.xml");

const commentsIdsText = await zipFile.file("word/commentsIds.xml")?.async("text");
// Comment WITH a durableId keeps its durableId (paraId 00000001 for id 0)
expect(commentsIdsText).to.contain('w16cid:paraId="00000001"');
expect(commentsIdsText).to.contain('w16cid:durableId="12AB34CD"');
// Comment WITHOUT a durableId falls back to its generated paraId (00000002 for id 1)
expect(commentsIdsText).to.contain('w16cid:paraId="00000002"');
expect(commentsIdsText).to.contain('w16cid:durableId="00000002"');
});

it("should call the format method X times equalling X files to be formatted", () => {
// This test is required because before, there was a case where Document was formatted twice, which was inefficient
// This also caused issues such as running prepForXml multiple times as format() was ran multiple times.
Expand Down
26 changes: 26 additions & 0 deletions src/export/packer/next-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type IXmlifyedFileMapping = {
readonly CommentsRelationships?: IXmlifyedFile;
/** Comments extended for reply threading (word/commentsExtended.xml) */
readonly CommentsExtended?: IXmlifyedFile;
/** Durable comment ids (word/commentsIds.xml) */
readonly CommentsIds?: IXmlifyedFile;
/** Font table (word/fontTable.xml) */
readonly FontTable?: IXmlifyedFile;
/** Font table relationships (word/_rels/fontTable.xml.rels) */
Expand Down Expand Up @@ -661,6 +663,30 @@ export class Compiler {
},
}
: {}),
...(file.CommentsIds
? {
CommentsIds: {
data: xml(
this.formatter.format(file.CommentsIds, {
viewWrapper: {
View: file.CommentsIds,
Relationships: file.Comments.Relationships,
},
file,
stack: [],
}),
{
indent: prettify,
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
},
),
path: "word/commentsIds.xml",
},
}
: {}),
FontTable: {
data: xml(
this.formatter.format(file.FontTable.View, {
Expand Down
9 changes: 9 additions & 0 deletions src/file/content-types/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ export class ContentTypes extends XmlComponent {
);
}

/**
* Registers the commentsIds part in the content types.
*/
public addCommentsIds(): void {
this.root.push(
createOverride("application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml", "/word/commentsIds.xml"),
);
}

/**
* Registers a footer part in the content types.
*
Expand Down
43 changes: 43 additions & 0 deletions src/file/file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,49 @@ describe("File", () => {

expect(doc.CommentsExtended).to.be.undefined;
});

it("should create CommentsIds when a single (non-threaded) comment has durableId", () => {
const doc = new File({
comments: {
children: [{ id: 0, children: [new Paragraph("comment")], durableId: "12AB34CD" }],
},
sections: [],
});

expect(doc.CommentsIds).to.not.be.undefined;

const tree = new Formatter().format(doc.CommentsIds!);
const root = tree["w16cid:commentsIds"] as readonly Record<string, { readonly _attr: Record<string, string> }>[];
const commentId = root.find((entry) => "w16cid:commentId" in entry)?.["w16cid:commentId"];
expect(commentId).to.not.be.undefined;
expect(commentId!._attr["w16cid:durableId"]).to.equal("12AB34CD");
expect(commentId!._attr["w16cid:paraId"]).to.be.a("string");
});

it("should create CommentsIds when threaded comments have durableId", () => {
const doc = new File({
comments: {
children: [
{ id: 0, children: [new Paragraph("parent")], durableId: "11112222" },
{ id: 1, children: [new Paragraph("reply")], parentId: 0, durableId: "33334444" },
],
},
sections: [],
});

expect(doc.CommentsIds).to.not.be.undefined;
});

it("should not create CommentsIds when no durableId", () => {
const doc = new File({
comments: {
children: [{ id: 0, children: [new Paragraph("comment")] }],
},
sections: [],
});

expect(doc.CommentsIds).to.be.undefined;
});
});

describe("#numbering", () => {
Expand Down
23 changes: 22 additions & 1 deletion src/file/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { HeaderWrapper, type IDocumentHeader } from "./header-wrapper";
import { Media } from "./media";
import { Numbering } from "./numbering";
import { Comments } from "./paragraph/run/comment-run";
import { CommentsExtended } from "./paragraph/run/comments-extended";
import { CommentsExtended, CommentsIds } from "./paragraph/run/comments-extended";
import { Relationships } from "./relationships";
import { Settings } from "./settings";
import { Styles } from "./styles";
Expand Down Expand Up @@ -167,6 +167,8 @@ export class File {
private readonly comments: Comments;
/** Extended comment data for reply threading and resolved state (word/commentsExtended.xml). */
private readonly commentsExtended?: CommentsExtended;
/** Durable comment id mapping (word/commentsIds.xml). */
private readonly commentsIds?: CommentsIds;
private readonly fontWrapper: FontWrapper;

public constructor(options: IPropertiesOptions) {
Expand All @@ -184,6 +186,10 @@ export class File {
if (this.comments.ThreadData) {
this.commentsExtended = new CommentsExtended(this.comments.ThreadData);
}
// Build commentsIds.xml when comments carry a durableId
if (this.comments.CommentIdsData) {
this.commentsIds = new CommentsIds(this.comments.CommentIdsData);
}
this.fileRelationships = new Relationships();
this.customProperties = new CustomProperties(options.customProperties ?? []);
this.appProperties = new AppProperties();
Expand Down Expand Up @@ -392,6 +398,16 @@ export class File {
);
this.contentTypes.addCommentsExtended();
}

if (this.commentsIds) {
this.documentWrapper.Relationships.addRelationship(
// eslint-disable-next-line functional/immutable-data
this.currentRelationshipId++,
"http://schemas.microsoft.com/office/2016/09/relationships/commentsIds",
"commentsIds.xml",
);
this.contentTypes.addCommentsIds();
}
}

public get Document(): DocumentWrapper {
Expand Down Expand Up @@ -459,6 +475,11 @@ export class File {
return this.commentsExtended;
}

/** Durable comment id part. Undefined when no comment carries a durableId. */
public get CommentsIds(): CommentsIds | undefined {
return this.commentsIds;
}

public get FontTable(): FontWrapper {
return this.fontWrapper;
}
Expand Down
88 changes: 88 additions & 0 deletions src/file/paragraph/run/comment-run.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,94 @@ describe("Comments", () => {
const serialized = JSON.stringify(tree);
expect(serialized).to.not.contain("w14:paraId");
});

it("should not have CommentIdsData when no durableId is used", () => {
const component = new Comments({
children: [{ id: 0, children: [new Paragraph("comment")], date: new Date("1999-01-01T00:00:00.000Z") }],
});
expect(component.CommentIdsData).to.be.undefined;
});

it("should produce CommentIdsData for a single (non-threaded) comment with durableId", () => {
const component = new Comments({
children: [
{
id: 0,
children: [new Paragraph("comment")],
date: new Date("1999-01-01T00:00:00.000Z"),
durableId: "12AB34CD",
},
],
});
expect(component.CommentIdsData).to.deep.equal([{ paraId: "00000001", durableId: "12AB34CD" }]);
});

it("should fall back to paraId as durableId when durableId is omitted on a durable-bearing document", () => {
const component = new Comments({
children: [
{
id: 0,
children: [new Paragraph("with-durable")],
date: new Date("1999-01-01T00:00:00.000Z"),
durableId: "AAAA1111",
},
{
id: 1,
children: [new Paragraph("without-durable")],
date: new Date("1999-01-01T00:00:00.000Z"),
},
],
});
expect(component.CommentIdsData).to.deep.equal([
{ paraId: "00000001", durableId: "AAAA1111" },
{ paraId: "00000002", durableId: "00000002" },
]);
});

it("should produce both ThreadData and CommentIdsData for threaded comments with durableId", () => {
const component = new Comments({
children: [
{
id: 0,
children: [new Paragraph("parent")],
date: new Date("1999-01-01T00:00:00.000Z"),
durableId: "11112222",
},
{
id: 1,
children: [new Paragraph("reply")],
date: new Date("1999-01-01T00:00:00.000Z"),
parentId: 0,
durableId: "33334444",
},
],
});
expect(component.ThreadData).to.deep.equal([
{ paraId: "00000001", parentParaId: undefined, done: undefined },
{ paraId: "00000002", parentParaId: "00000001", done: undefined },
]);
expect(component.CommentIdsData).to.deep.equal([
{ paraId: "00000001", durableId: "11112222" },
{ paraId: "00000002", durableId: "33334444" },
]);
});

it("should inject w14:paraId when only durableId (no threading) is used", () => {
const component = new Comments({
children: [
{
id: 0,
children: [new Paragraph("comment")],
date: new Date("1999-01-01T00:00:00.000Z"),
durableId: "12AB34CD",
},
],
});
const tree = new Formatter().format(component);
const serialized = JSON.stringify(tree);
expect(serialized).to.contain('"w14:paraId":"00000001"');
expect(serialized).to.contain('"w14:textId":"00000001"');
});
});
});

Expand Down
Loading
Loading