diff --git a/demo/54-track-revisions.ts b/demo/54-track-revisions.ts
new file mode 100644
index 0000000000..01d96082b8
--- /dev/null
+++ b/demo/54-track-revisions.ts
@@ -0,0 +1,132 @@
+// Track Revisions aka. "Track Changes"
+// Import from 'docx' rather than '../build' if you install from npm
+import * as fs from "fs";
+import { Document, Packer, Paragraph, TextRun, ShadingType, DeletedTextRun, InsertedTextRun, Footer, PageNumber, AlignmentType, FootnoteReferenceRun } from "../build";
+
+/*
+ For reference, see
+ - https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.insertedrun
+ - https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.deletedrun
+
+ The method `addTrackRevisions()` adds an element `` to the `settings.xml` file. This specifies that the application shall track *new* revisions made to the existing document.
+ See also https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.trackrevisions
+
+ Note that this setting enables to track *new changes* after teh file is generated, so this example will still show inserted and deleted text runs when you remove it.
+*/
+
+const doc = new Document({
+ footnotes: [
+ new Paragraph({
+ children:[
+ new TextRun("This is a footnote"),
+ new DeletedTextRun({
+ text: " with some extra text which was deleted",
+ id: 0,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:05:00Z",
+ }),
+ new InsertedTextRun({
+ text: " and new content",
+ id: 1,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:05:00Z",
+ })
+ ]
+ }),
+ ],
+});
+
+doc.Settings.addTrackRevisions()
+
+const paragraph = new Paragraph({
+ children: [
+ new TextRun("This is a simple demo "),
+ new TextRun({
+ text: "on how to "
+ }),
+ new InsertedTextRun({
+ text: "mark a text as an insertion ",
+ id: 0,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ }),
+ new DeletedTextRun({
+ text: "or a deletion.",
+ id: 1,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ })
+ ],
+});
+
+doc.addSection({
+ properties: {},
+ children: [
+ paragraph,
+ new Paragraph({
+ children: [
+ new TextRun("This is a demo "),
+ new DeletedTextRun({
+ text: "in order",
+ color: "red",
+ bold: true,
+ size: 24,
+ font: {
+ name: "Garamond",
+ },
+ shading: {
+ type: ShadingType.REVERSE_DIAGONAL_STRIPE,
+ color: "00FFFF",
+ fill: "FF0000",
+ },
+ id: 2,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ }).break(),
+ new InsertedTextRun({
+ text: "to show how to ",
+ bold: false,
+ id: 3,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:05:00Z",
+ }),
+ new TextRun({
+ bold: true,
+ children: [ "\tuse Inserted and Deleted TextRuns.", new FootnoteReferenceRun(1) ],
+ }),
+ ],
+ }),
+ ],
+ footers: {
+ default: new Footer({
+ children: [
+ new Paragraph({
+ alignment: AlignmentType.CENTER,
+ children: [
+ new TextRun("Awesome LLC"),
+ new TextRun({
+ children: ["Page Number: ", PageNumber.CURRENT],
+ }),
+ new DeletedTextRun({
+ children: [" to ", PageNumber.TOTAL_PAGES],
+ id: 4,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:05:00Z",
+ }),
+ new InsertedTextRun({
+ children: [" from ", PageNumber.TOTAL_PAGES],
+ bold: true,
+ id: 5,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:05:00Z",
+ }),
+ ],
+ }),
+ ],
+ }),
+ },
+});
+
+Packer.toBuffer(doc).then((buffer) => {
+ fs.writeFileSync("My Document.docx", buffer);
+});
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index a63970dff9..5d2ba31551 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -20,6 +20,7 @@
* [Tab Stops](usage/tab-stops.md)
* [Table of Contents](usage/table-of-contents.md)
* [Page Numbers](usage/page-numbers.md)
+ * [Change Tracking](usage/change-tracking.md)
* Styling
* [Styling with JS](usage/styling-with-js.md)
* [Styling with XML](usage/styling-with-xml.md)
@@ -28,4 +29,3 @@
* [Packers](usage/packers.md)
* [Contribution Guidelines](contribution-guidelines.md)
-
diff --git a/docs/usage/change-tracking.md b/docs/usage/change-tracking.md
new file mode 100644
index 0000000000..6f81e4d0d7
--- /dev/null
+++ b/docs/usage/change-tracking.md
@@ -0,0 +1,58 @@
+# Change Tracking
+
+> Instead of adding a `TextRun` into a `Paragraph`, you can also add an `InsertedTextRun` or `DeletedTextRun` where you need to supply an `id`, `author` and `date` for the change.
+
+```ts
+import { Paragraph, TextRun, InsertedTextRun, DeletedTextRun } from "docx";
+
+const paragraph = new Paragraph({
+ children: [
+ new TextRun("This is a simple demo "),
+ new TextRun({
+ text: "on how to "
+ }),
+ new InsertedTextRun({
+ text: "mark a text as an insertion ",
+ id: 0,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ }),
+ new DeletedTextRun({
+ text: "or a deletion.",
+ id: 1,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ })
+ ],
+});
+```
+
+Note that for a `InsertedTextRun` and `DeletedTextRun`, it is not possible to simply call it with only a text as in `new TextRun("some text")`, since the additonal fields for change tracking need to be provided. Similar to a normal `TextRun` you can add additional text properties.
+
+```ts
+import { Paragraph, TextRun, InsertedTextRun, DeletedTextRun } from "docx";
+
+const paragraph = new Paragraph({
+ children: [
+ new TextRun("This is a simple demo"),
+ new DeletedTextRun({
+ text: "with a deletion.",
+ color: "red",
+ bold: true,
+ size: 24,
+ id: 0,
+ author: "Firstname Lastname",
+ date: "2020-10-06T09:00:00Z",
+ })
+ ],
+});
+```
+
+In addtion to marking text as inserted or deleted, change tracking can also be added via the document settings. This will enable new changes to be tracked as well.
+
+```ts
+import { Document } from "docx";
+
+const doc = new Document({});
+doc.Settings.addTrackRevisions()
+```
\ No newline at end of file
diff --git a/src/file/index.ts b/src/file/index.ts
index c4ce99e345..18f0a595e4 100644
--- a/src/file/index.ts
+++ b/src/file/index.ts
@@ -13,3 +13,4 @@ export * from "./header-wrapper";
export * from "./footer-wrapper";
export * from "./header";
export * from "./footnotes";
+export * from "./track-revision";
diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts
index e1db0ec910..856c34aacf 100644
--- a/src/file/paragraph/paragraph.ts
+++ b/src/file/paragraph/paragraph.ts
@@ -3,6 +3,7 @@ import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { File } from "../file";
+import { InsertedTextRun, DeletedTextRun } from "../track-revision";
import { PageBreak } from "./formatting/page-break";
import { Bookmark, HyperlinkRef } from "./links";
import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties";
@@ -19,6 +20,8 @@ export interface IParagraphOptions extends IParagraphPropertiesOptions {
| SequentialIdentifier
| FootnoteReferenceRun
| HyperlinkRef
+ | InsertedTextRun
+ | DeletedTextRun
)[];
}
diff --git a/src/file/settings/settings.spec.ts b/src/file/settings/settings.spec.ts
index 1e34c781c2..9be90669aa 100644
--- a/src/file/settings/settings.spec.ts
+++ b/src/file/settings/settings.spec.ts
@@ -79,4 +79,47 @@ describe("Settings", () => {
expect(keys[0]).to.be.equal("w:compat");
});
});
+ describe("#addTrackRevisions", () => {
+ it("should add an empty Track Revisions", () => {
+ const settings = new Settings();
+ settings.addTrackRevisions();
+
+ const tree = new Formatter().format(settings);
+ let keys: string[] = Object.keys(tree);
+ expect(keys[0]).to.be.equal("w:settings");
+ const rootArray = tree["w:settings"];
+ expect(rootArray).is.an.instanceof(Array);
+ expect(rootArray).has.length(2);
+ keys = Object.keys(rootArray[0]);
+ expect(keys).is.an.instanceof(Array);
+ expect(keys).has.length(1);
+ expect(keys[0]).to.be.equal("_attr");
+ keys = Object.keys(rootArray[1]);
+ expect(keys).is.an.instanceof(Array);
+ expect(keys).has.length(1);
+ expect(keys[0]).to.be.equal("w:trackRevisions");
+ });
+ });
+ describe("#addTrackRevisionsTwice", () => {
+ it("should add an empty Track Revisions if called twice", () => {
+ const settings = new Settings();
+ settings.addTrackRevisions();
+ settings.addTrackRevisions();
+
+ const tree = new Formatter().format(settings);
+ let keys: string[] = Object.keys(tree);
+ expect(keys[0]).to.be.equal("w:settings");
+ const rootArray = tree["w:settings"];
+ expect(rootArray).is.an.instanceof(Array);
+ expect(rootArray).has.length(2);
+ keys = Object.keys(rootArray[0]);
+ expect(keys).is.an.instanceof(Array);
+ expect(keys).has.length(1);
+ expect(keys[0]).to.be.equal("_attr");
+ keys = Object.keys(rootArray[1]);
+ expect(keys).is.an.instanceof(Array);
+ expect(keys).has.length(1);
+ expect(keys[0]).to.be.equal("w:trackRevisions");
+ });
+ });
});
diff --git a/src/file/settings/settings.ts b/src/file/settings/settings.ts
index abae0e61d4..2fa4a27f0a 100644
--- a/src/file/settings/settings.ts
+++ b/src/file/settings/settings.ts
@@ -1,6 +1,7 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
import { Compatibility } from "./compatibility";
import { UpdateFields } from "./update-fields";
+import { TrackRevisions } from "./track-revisions";
export interface ISettingsAttributesProperties {
readonly wpc?: string;
@@ -46,6 +47,7 @@ export class SettingsAttributes extends XmlAttributeComponent child instanceof TrackRevisions)) {
+ this.addChildElement(this.trackRevisions);
+ }
+
+ return this.trackRevisions;
+ }
}
diff --git a/src/file/settings/track-revisions.spec.ts b/src/file/settings/track-revisions.spec.ts
new file mode 100644
index 0000000000..3875d51f0a
--- /dev/null
+++ b/src/file/settings/track-revisions.spec.ts
@@ -0,0 +1,16 @@
+import { expect } from "chai";
+import { Formatter } from "export/formatter";
+import { TrackRevisions } from "file/settings/track-revisions";
+
+import { EMPTY_OBJECT } from "file/xml-components";
+
+describe("TrackRevisions", () => {
+ describe("#constructor", () => {
+ it("creates an initially empty property object", () => {
+ const trackRevisions = new TrackRevisions();
+
+ const tree = new Formatter().format(trackRevisions);
+ expect(tree).to.deep.equal({ "w:trackRevisions": EMPTY_OBJECT });
+ });
+ });
+});
diff --git a/src/file/settings/track-revisions.ts b/src/file/settings/track-revisions.ts
new file mode 100644
index 0000000000..2da692827e
--- /dev/null
+++ b/src/file/settings/track-revisions.ts
@@ -0,0 +1,7 @@
+import { XmlComponent } from "file/xml-components";
+
+export class TrackRevisions extends XmlComponent {
+ constructor() {
+ super("w:trackRevisions");
+ }
+}
diff --git a/src/file/track-revision/index.ts b/src/file/track-revision/index.ts
new file mode 100644
index 0000000000..eb2465d8fe
--- /dev/null
+++ b/src/file/track-revision/index.ts
@@ -0,0 +1,2 @@
+export * from "./track-revision-components/inserted-text-run";
+export * from "./track-revision-components/deleted-text-run";
diff --git a/src/file/track-revision/track-revision-components/deleted-page-number.spec.ts b/src/file/track-revision/track-revision-components/deleted-page-number.spec.ts
new file mode 100644
index 0000000000..5e0238da96
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-page-number.spec.ts
@@ -0,0 +1,30 @@
+import { expect } from "chai";
+import { Formatter } from "export/formatter";
+import { DeletedNumberOfPages, DeletedNumberOfPagesSection, DeletedPage } from "./deleted-page-number";
+
+describe("Deleted Page", () => {
+ describe("#constructor()", () => {
+ it("uses the font name for both ascii and hAnsi", () => {
+ const tree = new Formatter().format(new DeletedPage());
+ expect(tree).to.deep.equal({ "w:delInstrText": [{ _attr: { "xml:space": "preserve" } }, "PAGE"] });
+ });
+ });
+});
+
+describe("Delted NumberOfPages", () => {
+ describe("#constructor()", () => {
+ it("uses the font name for both ascii and hAnsi", () => {
+ const tree = new Formatter().format(new DeletedNumberOfPages());
+ expect(tree).to.deep.equal({ "w:delInstrText": [{ _attr: { "xml:space": "preserve" } }, "NUMPAGES"] });
+ });
+ });
+});
+
+describe("Deleted NumberOfPagesSection", () => {
+ describe("#constructor()", () => {
+ it("uses the font name for both ascii and hAnsi", () => {
+ const tree = new Formatter().format(new DeletedNumberOfPagesSection());
+ expect(tree).to.deep.equal({ "w:delInstrText": [{ _attr: { "xml:space": "preserve" } }, "SECTIONPAGES"] });
+ });
+ });
+});
diff --git a/src/file/track-revision/track-revision-components/deleted-page-number.ts b/src/file/track-revision/track-revision-components/deleted-page-number.ts
new file mode 100644
index 0000000000..6ce6266f13
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-page-number.ts
@@ -0,0 +1,30 @@
+import { SpaceType } from "file/space-type";
+import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
+
+class TextAttributes extends XmlAttributeComponent<{ readonly space: SpaceType }> {
+ protected readonly xmlKeys = { space: "xml:space" };
+}
+
+export class DeletedPage extends XmlComponent {
+ constructor() {
+ super("w:delInstrText");
+ this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
+ this.root.push("PAGE");
+ }
+}
+
+export class DeletedNumberOfPages extends XmlComponent {
+ constructor() {
+ super("w:delInstrText");
+ this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
+ this.root.push("NUMPAGES");
+ }
+}
+
+export class DeletedNumberOfPagesSection extends XmlComponent {
+ constructor() {
+ super("w:delInstrText");
+ this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
+ this.root.push("SECTIONPAGES");
+ }
+}
diff --git a/src/file/track-revision/track-revision-components/deleted-text-run.spec.ts b/src/file/track-revision/track-revision-components/deleted-text-run.spec.ts
new file mode 100644
index 0000000000..7931e50406
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-text-run.spec.ts
@@ -0,0 +1,371 @@
+import { expect } from "chai";
+import { Formatter } from "export/formatter";
+import { DeletedTextRun } from "./deleted-text-run";
+import { FootnoteReferenceRun, PageNumber } from "../../index";
+
+describe("DeletedTextRun", () => {
+ describe("#constructor", () => {
+ it("should create a deleted text run", () => {
+ const deletedTextRun = new DeletedTextRun({ text: "some text", id: 0, date: "123", author: "Author" });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "some text",
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+
+ describe("#constructor with formatting", () => {
+ it("should create a deleted text run", () => {
+ const deletedTextRun = new DeletedTextRun({ text: "some text", bold: true, id: 0, date: "123", author: "Author" });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:rPr": [
+ {
+ "w:b": {
+ _attr: {
+ "w:val": true,
+ },
+ },
+ },
+ {
+ "w:bCs": {
+ _attr: {
+ "w:val": true,
+ },
+ },
+ },
+ ],
+ },
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "some text",
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+
+ describe("#break()", () => {
+ it("should add a break", () => {
+ const deletedTextRun = new DeletedTextRun({
+ children: ["some text"],
+ id: 0,
+ date: "123",
+ author: "Author",
+ }).break();
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:br": {},
+ },
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "some text",
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+
+ describe("page numbering", () => {
+ it("should be able to delete the total pages", () => {
+ const deletedTextRun = new DeletedTextRun({
+ children: [" to ", PageNumber.TOTAL_PAGES],
+ id: 0,
+ date: "123",
+ author: "Author",
+ });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ " to ",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "begin",
+ },
+ },
+ },
+ {
+ "w:delInstrText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "NUMPAGES",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "separate",
+ },
+ },
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "end",
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should be able to delete the total pages in section", () => {
+ const deletedTextRun = new DeletedTextRun({
+ children: [" to ", PageNumber.TOTAL_PAGES_IN_SECTION],
+ id: 0,
+ date: "123",
+ author: "Author",
+ });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ " to ",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "begin",
+ },
+ },
+ },
+ {
+ "w:delInstrText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "SECTIONPAGES",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "separate",
+ },
+ },
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "end",
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should be able to delete the current page", () => {
+ const deletedTextRun = new DeletedTextRun({
+ children: [" to ", PageNumber.CURRENT],
+ id: 0,
+ date: "123",
+ author: "Author",
+ });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ " to ",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "begin",
+ },
+ },
+ },
+ {
+ "w:delInstrText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "PAGE",
+ ],
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "separate",
+ },
+ },
+ },
+ {
+ "w:fldChar": {
+ _attr: {
+ "w:fldCharType": "end",
+ },
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+
+ describe("footnote references", () => {
+ it("should add a valid footnote reference", () => {
+ const deletedTextRun = new DeletedTextRun({
+ children: ["some text", new FootnoteReferenceRun(1)],
+ id: 0,
+ date: "123",
+ author: "Author",
+ });
+ const tree = new Formatter().format(deletedTextRun);
+ expect(tree).to.deep.equal({
+ "w:del": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:delText": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "some text",
+ ],
+ },
+ {
+ "w:r": [
+ { "w:rPr": [{ "w:rStyle": { _attr: { "w:val": "FootnoteReference" } } }] },
+ { "w:footnoteReference": { _attr: { "w:id": 1 } } },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+});
diff --git a/src/file/track-revision/track-revision-components/deleted-text-run.ts b/src/file/track-revision/track-revision-components/deleted-text-run.ts
new file mode 100644
index 0000000000..2342f70104
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-text-run.ts
@@ -0,0 +1,83 @@
+import { IChangedAttributesProperties, ChangeAttributes } from "../track-revision";
+import { XmlComponent } from "file/xml-components";
+import { IRunOptions, RunProperties, IRunPropertiesOptions, FootnoteReferenceRun } from "../../index";
+
+import { Break } from "../../paragraph/run/break";
+import { Begin, Separate, End } from "../../paragraph/run/field";
+import { PageNumber } from "../../paragraph/run/run";
+
+import { DeletedPage, DeletedNumberOfPages, DeletedNumberOfPagesSection } from "./deleted-page-number";
+import { DeletedText } from "./deleted-text";
+
+interface IDeletedRunOptions extends IRunPropertiesOptions, IChangedAttributesProperties {
+ readonly children?: (Begin | Separate | End | PageNumber | FootnoteReferenceRun | string)[];
+ readonly text?: string;
+}
+
+export class DeletedTextRun extends XmlComponent {
+ protected readonly deletedTextRunWrapper: DeletedTextRunWrapper;
+
+ constructor(options: IDeletedRunOptions) {
+ super("w:del");
+ this.root.push(
+ new ChangeAttributes({
+ id: options.id,
+ author: options.author,
+ date: options.date,
+ }),
+ );
+ this.deletedTextRunWrapper = new DeletedTextRunWrapper(options as IRunOptions);
+ this.addChildElement(this.deletedTextRunWrapper);
+ }
+
+ public break(): DeletedTextRun {
+ this.deletedTextRunWrapper.break();
+ return this;
+ }
+}
+
+class DeletedTextRunWrapper extends XmlComponent {
+ constructor(options: IRunOptions) {
+ super("w:r");
+ this.root.push(new RunProperties(options));
+
+ if (options.children) {
+ for (const child of options.children) {
+ if (typeof child === "string") {
+ switch (child) {
+ case PageNumber.CURRENT:
+ this.root.push(new Begin());
+ this.root.push(new DeletedPage());
+ this.root.push(new Separate());
+ this.root.push(new End());
+ break;
+ case PageNumber.TOTAL_PAGES:
+ this.root.push(new Begin());
+ this.root.push(new DeletedNumberOfPages());
+ this.root.push(new Separate());
+ this.root.push(new End());
+ break;
+ case PageNumber.TOTAL_PAGES_IN_SECTION:
+ this.root.push(new Begin());
+ this.root.push(new DeletedNumberOfPagesSection());
+ this.root.push(new Separate());
+ this.root.push(new End());
+ break;
+ default:
+ this.root.push(new DeletedText(child));
+ break;
+ }
+ continue;
+ }
+
+ this.root.push(child);
+ }
+ } else if (options.text) {
+ this.root.push(new DeletedText(options.text));
+ }
+ }
+
+ public break(): void {
+ this.root.splice(1, 0, new Break());
+ }
+}
diff --git a/src/file/track-revision/track-revision-components/deleted-text.spec.ts b/src/file/track-revision/track-revision-components/deleted-text.spec.ts
new file mode 100644
index 0000000000..d9c8de46bf
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-text.spec.ts
@@ -0,0 +1,15 @@
+import { expect } from "chai";
+import { Formatter } from "export/formatter";
+import { DeletedText } from "./deleted-text";
+
+describe("Deleted Text", () => {
+ describe("#constructor", () => {
+ it("adds the passed in text to the component", () => {
+ const t = new DeletedText(" this is\n text");
+ const f = new Formatter().format(t);
+ expect(f).to.deep.equal({
+ "w:delText": [{ _attr: { "xml:space": "preserve" } }, " this is\n text"],
+ });
+ });
+ });
+});
diff --git a/src/file/track-revision/track-revision-components/deleted-text.ts b/src/file/track-revision/track-revision-components/deleted-text.ts
new file mode 100644
index 0000000000..408b47304d
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/deleted-text.ts
@@ -0,0 +1,15 @@
+import { SpaceType } from "file/space-type";
+import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
+
+class TextAttributes extends XmlAttributeComponent<{ readonly space: SpaceType }> {
+ protected readonly xmlKeys = { space: "xml:space" };
+}
+
+export class DeletedText extends XmlComponent {
+ constructor(text: string) {
+ super("w:delText");
+ this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
+
+ this.root.push(text);
+ }
+}
diff --git a/src/file/track-revision/track-revision-components/inserted-text-run.spec.ts b/src/file/track-revision/track-revision-components/inserted-text-run.spec.ts
new file mode 100644
index 0000000000..c23069fbba
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/inserted-text-run.spec.ts
@@ -0,0 +1,37 @@
+import { expect } from "chai";
+import { Formatter } from "export/formatter";
+import { InsertedTextRun } from "./inserted-text-run";
+
+describe("InsertedTextRun", () => {
+ describe("#constructor", () => {
+ it("should create a inserted text run", () => {
+ const insertedTextRun = new InsertedTextRun({ text: "some text", id: 0, date: "123", author: "Author" });
+ const tree = new Formatter().format(insertedTextRun);
+ expect(tree).to.deep.equal({
+ "w:ins": [
+ {
+ _attr: {
+ "w:author": "Author",
+ "w:date": "123",
+ "w:id": 0,
+ },
+ },
+ {
+ "w:r": [
+ {
+ "w:t": [
+ {
+ _attr: {
+ "xml:space": "preserve",
+ },
+ },
+ "some text",
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+});
diff --git a/src/file/track-revision/track-revision-components/inserted-text-run.ts b/src/file/track-revision/track-revision-components/inserted-text-run.ts
new file mode 100644
index 0000000000..49bd7f53ae
--- /dev/null
+++ b/src/file/track-revision/track-revision-components/inserted-text-run.ts
@@ -0,0 +1,19 @@
+import { IChangedAttributesProperties, ChangeAttributes } from "../track-revision";
+import { XmlComponent } from "file/xml-components";
+import { TextRun, IRunOptions } from "../../index";
+
+interface IInsertedRunOptions extends IChangedAttributesProperties, IRunOptions {}
+
+export class InsertedTextRun extends XmlComponent {
+ constructor(options: IInsertedRunOptions) {
+ super("w:ins");
+ this.root.push(
+ new ChangeAttributes({
+ id: options.id,
+ author: options.author,
+ date: options.date,
+ }),
+ );
+ this.addChildElement(new TextRun(options as IRunOptions));
+ }
+}
diff --git a/src/file/track-revision/track-revision.ts b/src/file/track-revision/track-revision.ts
new file mode 100644
index 0000000000..4318e9a468
--- /dev/null
+++ b/src/file/track-revision/track-revision.ts
@@ -0,0 +1,15 @@
+import { XmlAttributeComponent } from "file/xml-components";
+
+export interface IChangedAttributesProperties {
+ readonly id: number;
+ readonly author: string;
+ readonly date: string;
+}
+
+export class ChangeAttributes extends XmlAttributeComponent {
+ protected readonly xmlKeys = {
+ id: "w:id",
+ author: "w:author",
+ date: "w:date",
+ };
+}