diff --git a/demo/72-comment.ts b/demo/72-comment.ts new file mode 100644 index 0000000000..787cfe0e63 --- /dev/null +++ b/demo/72-comment.ts @@ -0,0 +1,41 @@ +// Simple example to add text to a document +// Import from 'docx' rather than '../build' if you install from npm +import * as fs from "fs"; +import { Document, Packer, Paragraph, TextRun, CommentRangeStart, CommentRangeEnd, Comments, Comment, CommentReference } from "../build"; + +const doc = new Document({ + sections: [ + { + properties: {}, + children: [ + new Paragraph({ + children: [ + new TextRun("Hello World"), + new CommentRangeStart({ id: "0" }), + new TextRun({ + text: "Foo Bar", + bold: true, + }), + new CommentRangeEnd({ id: "0" }), + new TextRun({ + children: [ + new CommentReference({ id: "0" }) + ], + bold: true, + }), + ], + }), + ], + }, + ], +}, { + template: { + currentRelationshipId: 1, + // global comments data, every comment has a unique id + comments: new Comments([new Comment({ id: '0', author: 'Ray Chen', date: new Date().toISOString() }, 'comment text content')]), + } +} +); +Packer.toBuffer(doc, ' ').then((buffer) => { + fs.writeFileSync("document-comments.docx", buffer); +}); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index 2afe0fde98..88ec2cfa56 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -28,6 +28,7 @@ interface IXmlifyedFileMapping { readonly FootNotes: IXmlifyedFile; readonly FootNotesRelationships: IXmlifyedFile; readonly Settings: IXmlifyedFile; + readonly Comments?: IXmlifyedFile; } export class Compiler { @@ -41,7 +42,7 @@ export class Compiler { this.numberingReplacer = new NumberingReplacer(); } - public compile(file: File, prettifyXml?: boolean): JSZip { + public compile(file: File, prettifyXml?: boolean | string): JSZip { const zip = new JSZip(); const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml); const map = new Map(Object.entries(xmlifiedFileMapping)); @@ -64,7 +65,7 @@ export class Compiler { return zip; } - private xmlifyFile(file: File, prettify?: boolean): IXmlifyedFileMapping { + private xmlifyFile(file: File, prettify?: boolean | string): IXmlifyedFileMapping { const documentRelationshipCount = file.Document.Relationships.RelationshipCount + 1; const documentXmlData = xml( @@ -112,7 +113,6 @@ export class Compiler { data: (() => { const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount); const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering); - return referenedXmlData; })(), path: "word/document.xml", @@ -399,6 +399,25 @@ export class Compiler { ), path: "word/settings.xml", }, + Comments: { + data: (() => { + const data = xml( + this.formatter.format(file.Comments, { + viewWrapper: file.Document, + file, + }), + { + indent: prettify, + declaration: { + standalone: "yes", + encoding: "UTF-8", + }, + }, + ); + return data; + })(), + path: "word/comments.xml", + }, }; } } diff --git a/src/export/packer/packer.ts b/src/export/packer/packer.ts index 1b5a219190..d9be2315aa 100644 --- a/src/export/packer/packer.ts +++ b/src/export/packer/packer.ts @@ -2,7 +2,7 @@ import { File } from "file"; import { Compiler } from "./next-compiler"; export class Packer { - public static async toBuffer(file: File, prettify?: boolean): Promise { + public static async toBuffer(file: File, prettify?: boolean | string): Promise { const zip = this.compiler.compile(file, prettify); const zipData = await zip.generateAsync({ type: "nodebuffer", diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts index 34e495d3c0..d2331c2cbd 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -32,6 +32,7 @@ export class ContentTypes extends XmlComponent { this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", "/word/numbering.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", "/word/settings.xml")); + this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", "/word/comments.xml")); } public addFooter(index: number): void { diff --git a/src/file/document/document-attributes.ts b/src/file/document/document-attributes.ts index a61b37eba0..dc759faea0 100644 --- a/src/file/document/document-attributes.ts +++ b/src/file/document/document-attributes.ts @@ -24,6 +24,22 @@ export interface IDocumentAttributesProperties { readonly dcmitype?: string; readonly xsi?: string; readonly type?: string; + readonly cx?: string; + readonly cx1?: string; + readonly cx2?: string; + readonly cx3?: string; + readonly cx4?: string; + readonly cx5?: string; + readonly cx6?: string; + readonly cx7?: string; + readonly cx8?: string; + readonly aink?: string; + readonly am3d?: string; + readonly w16cex?: string; + readonly w16cid?: string; + readonly w16?: string; + readonly w16sdtdh?: string; + readonly w16se?: string; } export class DocumentAttributes extends XmlAttributeComponent { @@ -51,5 +67,21 @@ export class DocumentAttributes extends XmlAttributeComponent { + protected readonly xmlKeys = { id: "w:id", initias: "w:initials", author: "w:author", date: "w:date" }; +} + +const COMMENT_ATTRIBUTES_MAP = { + "xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex", + "xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", + "xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", + "xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", + "xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", + "xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", + "xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", + "xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", + "xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", + "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "xmlns:aink": "http://schemas.microsoft.com/office/drawing/2016/ink", + "xmlns:am3d": "http://schemas.microsoft.com/office/drawing/2017/model3d", + "xmlns:o": "urn:schemas-microsoft-com:office:office", + "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math", + "xmlns:v": "urn:schemas-microsoft-com:vml", + "xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", + "xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "xmlns:w10": "urn:schemas-microsoft-com:office:word", + "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "xmlns:w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex", + "xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid", + "xmlns:w16": "http://schemas.microsoft.com/office/word/2018/wordml", + "xmlns:w16sdtdh": "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", + "xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex", + "xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", + "xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", + "xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml", + "xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", +}; +class RootCommentsAttributes extends XmlAttributeComponent { + protected readonly xmlKeys = { + "xmlns:cx": "xmlns:cx", + "xmlns:cx1": "xmlns:cx1", + "xmlns:cx2": "xmlns:cx2", + "xmlns:cx3": "xmlns:cx3", + "xmlns:cx4": "xmlns:cx4", + "xmlns:cx5": "xmlns:cx5", + "xmlns:cx6": "xmlns:cx6", + "xmlns:cx7": "xmlns:cx7", + "xmlns:cx8": "xmlns:cx8", + "xmlns:mc": "xmlns:mc", + "xmlns:aink": "xmlns:aink", + "xmlns:am3d": "xmlns:am3d", + "xmlns:o": "xmlns:o", + "xmlns:r": "xmlns:r", + "xmlns:m": "xmlns:m", + "xmlns:v": "xmlns:v", + "xmlns:wp14": "xmlns:wp14", + "xmlns:wp": "xmlns:wp", + "xmlns:w10": "xmlns:w10", + "xmlns:w": "xmlns:w", + "xmlns:w14": "xmlns:w14", + "xmlns:w15": "xmlns:w15", + "xmlns:w16cex": "xmlns:w16cex", + "xmlns:w16cid": "xmlns:w16cid", + "xmlns:w16": "xmlns:w16", + "xmlns:w16sdtdh": "xmlns:w16sdtdh", + "xmlns:w16se": "xmlns:w16se", + "xmlns:wpg": "xmlns:wpg", + "xmlns:wpi": "xmlns:wpi", + "xmlns:wne": "xmlns:wne", + "xmlns:wps": "xmlns:wps", + }; +} + +interface ICommentsAttrs { + readonly "xmlns:cx"?: string; + readonly "xmlns:cx1"?: string; + readonly "xmlns:cx2"?: string; + readonly "xmlns:cx3"?: string; + readonly "xmlns:cx4"?: string; + readonly "xmlns:cx5"?: string; + readonly "xmlns:cx6"?: string; + readonly "xmlns:cx7"?: string; + readonly "xmlns:cx8"?: string; + readonly "xmlns:mc"?: string; + readonly "xmlns:aink"?: string; + readonly "xmlns:am3d"?: string; + readonly "xmlns:o"?: string; + readonly "xmlns:r"?: string; + readonly "xmlns:m"?: string; + readonly "xmlns:v"?: string; + readonly "xmlns:wp14"?: string; + readonly "xmlns:wp"?: string; + readonly "xmlns:w10"?: string; + readonly "xmlns:w"?: string; + readonly "xmlns:w14"?: string; + readonly "xmlns:w15"?: string; + readonly "xmlns:w16cex"?: string; + readonly "xmlns:w16cid"?: string; + readonly "xmlns:w16"?: string; + readonly "xmlns:w16sdtdh"?: string; + readonly "xmlns:w16se"?: string; + readonly "xmlns:wpg": string; + readonly "xmlns:wpi"?: string; + readonly "xmlns:wne"?: string; + readonly "xmlns:wps"?: string; +} + +export class CommentRangeStart extends XmlComponent { + constructor(options: ICommentOptions) { + super("w:commentRangeStart"); + this.root.push(new CommentAttributes(options)); + } +} + +export class CommentRangeEnd extends XmlComponent { + constructor(options: ICommentOptions) { + super("w:commentRangeEnd"); + this.root.push(new CommentAttributes(options)); + } +} + +export class CommentReference extends XmlComponent { + constructor(options: ICommentOptions) { + super("w:commentReference"); + this.root.push(new CommentAttributes(options)); + } +} + +export class Comment extends XmlComponent { + constructor(options: ICommentOptions, text: string) { + super("w:comment"); + this.root.push(new CommentAttributes(options)); + this.addChildElement(new Paragraph({ children: [new TextRun(text)] })); + } +} +export class Comments extends XmlComponent { + constructor(comments: Comment[]) { + super("w:comments"); + this.root.push(new RootCommentsAttributes(COMMENT_ATTRIBUTES_MAP)); + comments.forEach((comment) => { + this.addChildElement(comment); + }); + } +} diff --git a/src/file/paragraph/run/index.ts b/src/file/paragraph/run/index.ts index da4b9e4f2e..34819e2c02 100644 --- a/src/file/paragraph/run/index.ts +++ b/src/file/paragraph/run/index.ts @@ -9,3 +9,4 @@ export * from "./underline"; export * from "./emphasis-mark"; export * from "./tab"; export * from "./simple-field"; +export * from "./comment-run"; diff --git a/src/file/relationships/relationship/relationship.ts b/src/file/relationships/relationship/relationship.ts index 2b928de890..79204e4ff8 100644 --- a/src/file/relationships/relationship/relationship.ts +++ b/src/file/relationships/relationship/relationship.ts @@ -16,7 +16,8 @@ export type RelationshipType = | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" - | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"; + | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" + | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; export enum TargetModeType { EXTERNAL = "External", diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts index 3ef73208d8..774f27a534 100644 --- a/src/import-dotx/import-dotx.ts +++ b/src/import-dotx/import-dotx.ts @@ -1,3 +1,4 @@ +import { Comments } from "./../file/paragraph/run/comment-run"; import * as JSZip from "jszip"; import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js"; @@ -37,11 +38,12 @@ interface IRelationshipFileInfo { // https://fileinfo.com/extension/dotx export interface IDocumentTemplate { readonly currentRelationshipId: number; - readonly headers: IDocumentHeader[]; - readonly footers: IDocumentFooter[]; - readonly styles: string; - readonly titlePageIsDefined: boolean; - readonly media: Media; + readonly headers?: IDocumentHeader[]; + readonly footers?: IDocumentFooter[]; + readonly styles?: string; + readonly titlePageIsDefined?: boolean; + readonly media?: Media; + readonly comments?: Comments; } export class ImportDotx {