implement sample version of Comment feature.

This commit is contained in:
Chen Yuncai
2022-03-03 09:59:09 +08:00
parent dc7f199345
commit b2a09b512f
12 changed files with 300 additions and 12 deletions

41
demo/72-comment.ts Normal file
View File

@ -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);
});

View File

@ -28,6 +28,7 @@ interface IXmlifyedFileMapping {
readonly FootNotes: IXmlifyedFile; readonly FootNotes: IXmlifyedFile;
readonly FootNotesRelationships: IXmlifyedFile; readonly FootNotesRelationships: IXmlifyedFile;
readonly Settings: IXmlifyedFile; readonly Settings: IXmlifyedFile;
readonly Comments?: IXmlifyedFile;
} }
export class Compiler { export class Compiler {
@ -41,7 +42,7 @@ export class Compiler {
this.numberingReplacer = new NumberingReplacer(); this.numberingReplacer = new NumberingReplacer();
} }
public compile(file: File, prettifyXml?: boolean): JSZip { public compile(file: File, prettifyXml?: boolean | string): JSZip {
const zip = new JSZip(); const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml); const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml);
const map = new Map<string, IXmlifyedFile | IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping)); const map = new Map<string, IXmlifyedFile | IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping));
@ -64,7 +65,7 @@ export class Compiler {
return zip; 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 documentRelationshipCount = file.Document.Relationships.RelationshipCount + 1;
const documentXmlData = xml( const documentXmlData = xml(
@ -112,7 +113,6 @@ export class Compiler {
data: (() => { data: (() => {
const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount); const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount);
const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering); const referenedXmlData = this.numberingReplacer.replace(xmlData, file.Numbering.ConcreteNumbering);
return referenedXmlData; return referenedXmlData;
})(), })(),
path: "word/document.xml", path: "word/document.xml",
@ -399,6 +399,25 @@ export class Compiler {
), ),
path: "word/settings.xml", 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",
},
}; };
} }
} }

View File

@ -2,7 +2,7 @@ import { File } from "file";
import { Compiler } from "./next-compiler"; import { Compiler } from "./next-compiler";
export class Packer { export class Packer {
public static async toBuffer(file: File, prettify?: boolean): Promise<Buffer> { public static async toBuffer(file: File, prettify?: boolean | string): Promise<Buffer> {
const zip = this.compiler.compile(file, prettify); const zip = this.compiler.compile(file, prettify);
const zipData = await zip.generateAsync({ const zipData = await zip.generateAsync({
type: "nodebuffer", type: "nodebuffer",

View File

@ -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.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.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.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 { public addFooter(index: number): void {

View File

@ -24,6 +24,22 @@ export interface IDocumentAttributesProperties {
readonly dcmitype?: string; readonly dcmitype?: string;
readonly xsi?: string; readonly xsi?: string;
readonly type?: 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<IDocumentAttributesProperties> { export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> {
@ -51,5 +67,21 @@ export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttribute
dcmitype: "xmlns:dcmitype", dcmitype: "xmlns:dcmitype",
xsi: "xmlns:xsi", xsi: "xmlns:xsi",
type: "xsi:type", type: "xsi:type",
cx: "xmlns:cx",
cx1: "xmlns:cx1",
cx2: "xmlns:cx2",
cx3: "xmlns:cx3",
cx4: "xmlns:cx4",
cx5: "xmlns:cx5",
cx6: "xmlns:cx6",
cx7: "xmlns:cx7",
cx8: "xmlns:cx8",
aink: "xmlns:aink",
am3d: "xmlns:am3d",
w16cex: "xmlns:w16cex",
w16cid: "xmlns:w16cid",
w16: "xmlns:w16",
w16sdtdh: "xmlns:w16sdtdh",
w16se: "xmlns:w16se",
}; };
} }

View File

@ -53,6 +53,22 @@ export class Document extends XmlComponent {
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
wne: "http://schemas.microsoft.com/office/word/2006/wordml", wne: "http://schemas.microsoft.com/office/word/2006/wordml",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
w16: "http://schemas.microsoft.com/office/word/2018/wordml",
w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
Ignorable: "w14 w15 wp14", Ignorable: "w14 w15 wp14",
}), }),
); );

View File

@ -1,3 +1,4 @@
import { Comments } from "./paragraph/run/comment-run";
import { AppProperties } from "./app-properties/app-properties"; import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types"; import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CoreProperties, IPropertiesOptions } from "./core-properties";
@ -52,6 +53,7 @@ export class File {
private readonly customProperties: CustomProperties; private readonly customProperties: CustomProperties;
private readonly appProperties: AppProperties; private readonly appProperties: AppProperties;
private readonly styles: Styles; private readonly styles: Styles;
private readonly comments: Comments;
constructor(options: IPropertiesOptions, fileProperties: IFileProperties = {}) { constructor(options: IPropertiesOptions, fileProperties: IFileProperties = {}) {
this.coreProperties = new CoreProperties({ this.coreProperties = new CoreProperties({
@ -92,7 +94,7 @@ export class File {
if (fileProperties.template && options.externalStyles) { if (fileProperties.template && options.externalStyles) {
throw Error("can not use both template and external styles"); throw Error("can not use both template and external styles");
} }
if (fileProperties.template) { if (fileProperties.template && fileProperties.template.styles) {
const stylesFactory = new ExternalStylesFactory(); const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(fileProperties.template.styles); this.styles = stylesFactory.newInstance(fileProperties.template.styles);
} else if (options.externalStyles) { } else if (options.externalStyles) {
@ -134,6 +136,9 @@ export class File {
this.footnotesWrapper.View.createFootNote(parseFloat(key), options.footnotes[key].children); this.footnotesWrapper.View.createFootNote(parseFloat(key), options.footnotes[key].children);
} }
} }
if (fileProperties.template && fileProperties.template.comments) {
this.comments = fileProperties.template.comments;
}
} }
private addSection({ headers = {}, footers = {}, children, properties }: ISectionOptions): void { private addSection({ headers = {}, footers = {}, children, properties }: ISectionOptions): void {
@ -240,6 +245,11 @@ export class File {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
"settings.xml", "settings.xml",
); );
this.documentWrapper.Relationships.createRelationship(
this.currentRelationshipId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments",
"comments.xml",
);
} }
public get Document(): DocumentWrapper { public get Document(): DocumentWrapper {
@ -293,4 +303,8 @@ export class File {
public get Settings(): Settings { public get Settings(): Settings {
return this.settings; return this.settings;
} }
public get Comments(): Comments {
return this.comments;
}
} }

View File

@ -1,3 +1,4 @@
import { CommentRangeStart, CommentRangeEnd, CommentReference, Comments, Comment } from "./run/comment-run";
// http://officeopenxml.com/WPparagraph.php // http://officeopenxml.com/WPparagraph.php
import { uniqueId } from "convenience-functions"; import { uniqueId } from "convenience-functions";
@ -27,7 +28,12 @@ export type ParagraphChild =
| DeletedTextRun | DeletedTextRun
| Math | Math
| SimpleField | SimpleField
| SimpleMailMergeField; | SimpleMailMergeField
| Comments
| Comment
| CommentRangeStart
| CommentRangeEnd
| CommentReference;
export interface IParagraphOptions extends IParagraphPropertiesOptions { export interface IParagraphOptions extends IParagraphPropertiesOptions {
readonly text?: string; readonly text?: string;

View File

@ -0,0 +1,155 @@
import { TextRun } from "./text-run";
import { Paragraph } from "./../paragraph";
import { XmlAttributeComponent, XmlComponent } from "../../../file/xml-components";
interface ICommentOptions {
readonly id: string;
readonly initials?: string;
readonly author?: string;
readonly date?: string;
}
class CommentAttributes extends XmlAttributeComponent<ICommentOptions> {
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<ICommentsAttrs> {
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);
});
}
}

View File

@ -9,3 +9,4 @@ export * from "./underline";
export * from "./emphasis-mark"; export * from "./emphasis-mark";
export * from "./tab"; export * from "./tab";
export * from "./simple-field"; export * from "./simple-field";
export * from "./comment-run";

View File

@ -16,7 +16,8 @@ export type RelationshipType =
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" | "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/extended-properties"
| "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" | "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 { export enum TargetModeType {
EXTERNAL = "External", EXTERNAL = "External",

View File

@ -1,3 +1,4 @@
import { Comments } from "./../file/paragraph/run/comment-run";
import * as JSZip from "jszip"; import * as JSZip from "jszip";
import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js"; import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js";
@ -37,11 +38,12 @@ interface IRelationshipFileInfo {
// https://fileinfo.com/extension/dotx // https://fileinfo.com/extension/dotx
export interface IDocumentTemplate { export interface IDocumentTemplate {
readonly currentRelationshipId: number; readonly currentRelationshipId: number;
readonly headers: IDocumentHeader[]; readonly headers?: IDocumentHeader[];
readonly footers: IDocumentFooter[]; readonly footers?: IDocumentFooter[];
readonly styles: string; readonly styles?: string;
readonly titlePageIsDefined: boolean; readonly titlePageIsDefined?: boolean;
readonly media: Media; readonly media?: Media;
readonly comments?: Comments;
} }
export class ImportDotx { export class ImportDotx {