From 6cbe40cecb79ea4ff12a8544e947c0e6bc77e245 Mon Sep 17 00:00:00 2001 From: Thomas Jansen Date: Wed, 23 Sep 2020 11:23:00 +0200 Subject: [PATCH 1/5] add settings option to add trackRevisions --- src/file/settings/settings.ts | 11 +++++++++++ src/file/settings/track-revisions.spec.ts | 16 ++++++++++++++++ src/file/settings/track-revisions.ts | 7 +++++++ 3 files changed, 34 insertions(+) create mode 100644 src/file/settings/track-revisions.spec.ts create mode 100644 src/file/settings/track-revisions.ts 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"); + } +} From 2adfe532dde74234e1f6665f85da4ae18f2775fb Mon Sep 17 00:00:00 2001 From: Thomas Jansen Date: Wed, 23 Sep 2020 12:59:55 +0200 Subject: [PATCH 2/5] add more unit tests for trackRevision settings --- src/file/settings/settings.spec.ts | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) 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"); + }); + }); }); From 09db2c528a15a5442967e7633cfd2fd7b425f7c5 Mon Sep 17 00:00:00 2001 From: Thomas Jansen Date: Thu, 24 Sep 2020 10:24:00 +0200 Subject: [PATCH 3/5] added InsertedTextRun and DeletedTextRun for Revision Tracking --- src/file/index.ts | 1 + src/file/paragraph/paragraph.ts | 3 + src/file/track-revision/index.ts | 1 + .../track-revision/track-revision.spec.ts | 81 +++++++++++++++++++ src/file/track-revision/track-revision.ts | 72 +++++++++++++++++ 5 files changed, 158 insertions(+) create mode 100644 src/file/track-revision/index.ts create mode 100644 src/file/track-revision/track-revision.spec.ts create mode 100644 src/file/track-revision/track-revision.ts diff --git a/src/file/index.ts b/src/file/index.ts index c4ce99e345..62e7e647ac 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/track-revision/index.ts b/src/file/track-revision/index.ts new file mode 100644 index 0000000000..20bb87a229 --- /dev/null +++ b/src/file/track-revision/index.ts @@ -0,0 +1 @@ +export * from "./track-revision"; diff --git a/src/file/track-revision/track-revision.spec.ts b/src/file/track-revision/track-revision.spec.ts new file mode 100644 index 0000000000..faa4d5a565 --- /dev/null +++ b/src/file/track-revision/track-revision.spec.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { InsertedTextRun, DeletedTextRun } from "./track-revision"; +import { TextRun } from "../paragraph"; + +describe("InsertedTestRun", () => { + describe("#constructor", () => { + it("should create a inserted text run", () => { + const textRun = new TextRun({ + text: "some text" + }); + const insertedTextRun = new InsertedTextRun({ child: textRun, 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" + ] + } + ], + } + ] + }); + }); + }); +}); +describe("DeletedTestRun", () => { + describe("#constructor", () => { + it("should create a deleted text run", () => { + + const insertedParagraph = new DeletedTextRun({ text: 'some text', id: 0, date: "123", author: "Author" }) + const tree = new Formatter().format(insertedParagraph); + 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" + ] + } + ], + } + ] + }); + }); + }); +}); diff --git a/src/file/track-revision/track-revision.ts b/src/file/track-revision/track-revision.ts new file mode 100644 index 0000000000..1af9e7dba3 --- /dev/null +++ b/src/file/track-revision/track-revision.ts @@ -0,0 +1,72 @@ +import { SpaceType } from "file/space-type"; +import { XmlComponent, XmlAttributeComponent } from "file/xml-components"; +import { TextRun } from "../index"; + +export interface ITrackRevisionAttributesProperties { + readonly id: number; + readonly author: string; + readonly date: string; +} + +export class TrackRevisionAttributes extends XmlAttributeComponent { + protected readonly xmlKeys = { + id: "w:id", + author: "w:author", + date: "w:date", + }; +} + +export interface IInsertedTextRunOptions extends ITrackRevisionAttributesProperties { + readonly child: TextRun +} + +export interface IDeletedTextRunOptions extends ITrackRevisionAttributesProperties { + readonly text: string +} + +export class InsertedTextRun extends XmlComponent { + constructor(options: IInsertedTextRunOptions) { + super("w:ins"); + this.root.push( + new TrackRevisionAttributes({ + id: options.id, + author: options.author, + date: options.date, + }) + ); + this.addChildElement(options.child); + } +} + +export class DeletedTextRunWrapper extends XmlComponent { + constructor(text: string) { + super("w:r"); + this.root.push(new DeletedText(text)); + } +} + +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); + } +} + +export class DeletedTextRun extends XmlComponent { + constructor(options: IDeletedTextRunOptions) { + super("w:del"); + this.root.push( + new TrackRevisionAttributes({ + id: options.id, + author: options.author, + date: options.date, + }) + ); + this.addChildElement(new DeletedTextRunWrapper(options.text)); + } +} From 065c17de741cb5b79245781cd3dabf164d8afc35 Mon Sep 17 00:00:00 2001 From: Thomas Jansen Date: Thu, 24 Sep 2020 10:43:15 +0200 Subject: [PATCH 4/5] style fixes --- src/file/index.ts | 2 +- .../track-revision/track-revision.spec.ts | 59 +++++++++---------- src/file/track-revision/track-revision.ts | 8 +-- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/file/index.ts b/src/file/index.ts index 62e7e647ac..18f0a595e4 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -13,4 +13,4 @@ export * from "./header-wrapper"; export * from "./footer-wrapper"; export * from "./header"; export * from "./footnotes"; -export * from "./track-revision" +export * from "./track-revision"; diff --git a/src/file/track-revision/track-revision.spec.ts b/src/file/track-revision/track-revision.spec.ts index faa4d5a565..30a684fe9c 100644 --- a/src/file/track-revision/track-revision.spec.ts +++ b/src/file/track-revision/track-revision.spec.ts @@ -9,36 +9,34 @@ describe("InsertedTestRun", () => { describe("#constructor", () => { it("should create a inserted text run", () => { const textRun = new TextRun({ - text: "some text" + text: "some text", }); - const insertedTextRun = new InsertedTextRun({ child: textRun, id: 0, date: "123", author: "Author" }) + const insertedTextRun = new InsertedTextRun({ child: textRun, id: 0, date: "123", author: "Author" }); const tree = new Formatter().format(insertedTextRun); expect(tree).to.deep.equal({ "w:ins": [ { - "_attr": - { + _attr: { "w:author": "Author", - "w:date": "123", - "w:id": 0 - } + "w:date": "123", + "w:id": 0, + }, }, { "w:r": [ { "w:t": [ { - "_attr": - { - "xml:space": "preserve" - } + _attr: { + "xml:space": "preserve", + }, }, - "some text" - ] - } + "some text", + ], + }, ], - } - ] + }, + ], }); }); }); @@ -46,35 +44,32 @@ describe("InsertedTestRun", () => { describe("DeletedTestRun", () => { describe("#constructor", () => { it("should create a deleted text run", () => { - - const insertedParagraph = new DeletedTextRun({ text: 'some text', id: 0, date: "123", author: "Author" }) + const insertedParagraph = new DeletedTextRun({ text: "some text", id: 0, date: "123", author: "Author" }); const tree = new Formatter().format(insertedParagraph); expect(tree).to.deep.equal({ "w:del": [ { - "_attr": - { + _attr: { "w:author": "Author", - "w:date": "123", - "w:id": 0 - } + "w:date": "123", + "w:id": 0, + }, }, { "w:r": [ { "w:delText": [ { - "_attr": - { - "xml:space": "preserve" - } + _attr: { + "xml:space": "preserve", + }, }, - "some text" - ] - } + "some text", + ], + }, ], - } - ] + }, + ], }); }); }); diff --git a/src/file/track-revision/track-revision.ts b/src/file/track-revision/track-revision.ts index 1af9e7dba3..052c7e29ea 100644 --- a/src/file/track-revision/track-revision.ts +++ b/src/file/track-revision/track-revision.ts @@ -17,11 +17,11 @@ export class TrackRevisionAttributes extends XmlAttributeComponent Date: Wed, 7 Oct 2020 11:44:23 +0200 Subject: [PATCH 5/5] improved signature for deleted text runs, added demo 54 and added documentation for change tracking --- demo/54-track-revisions.ts | 132 +++++++ docs/_sidebar.md | 2 +- docs/usage/change-tracking.md | 58 +++ src/file/track-revision/index.ts | 3 +- .../deleted-page-number.spec.ts | 30 ++ .../deleted-page-number.ts | 30 ++ .../deleted-text-run.spec.ts | 371 ++++++++++++++++++ .../deleted-text-run.ts | 83 ++++ .../deleted-text.spec.ts | 15 + .../track-revision-components/deleted-text.ts | 15 + .../inserted-text-run.spec.ts | 37 ++ .../inserted-text-run.ts | 19 + .../track-revision/track-revision.spec.ts | 76 ---- src/file/track-revision/track-revision.ts | 63 +-- 14 files changed, 796 insertions(+), 138 deletions(-) create mode 100644 demo/54-track-revisions.ts create mode 100644 docs/usage/change-tracking.md create mode 100644 src/file/track-revision/track-revision-components/deleted-page-number.spec.ts create mode 100644 src/file/track-revision/track-revision-components/deleted-page-number.ts create mode 100644 src/file/track-revision/track-revision-components/deleted-text-run.spec.ts create mode 100644 src/file/track-revision/track-revision-components/deleted-text-run.ts create mode 100644 src/file/track-revision/track-revision-components/deleted-text.spec.ts create mode 100644 src/file/track-revision/track-revision-components/deleted-text.ts create mode 100644 src/file/track-revision/track-revision-components/inserted-text-run.spec.ts create mode 100644 src/file/track-revision/track-revision-components/inserted-text-run.ts delete mode 100644 src/file/track-revision/track-revision.spec.ts 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/track-revision/index.ts b/src/file/track-revision/index.ts index 20bb87a229..eb2465d8fe 100644 --- a/src/file/track-revision/index.ts +++ b/src/file/track-revision/index.ts @@ -1 +1,2 @@ -export * from "./track-revision"; +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.spec.ts b/src/file/track-revision/track-revision.spec.ts deleted file mode 100644 index 30a684fe9c..0000000000 --- a/src/file/track-revision/track-revision.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect } from "chai"; - -import { Formatter } from "export/formatter"; - -import { InsertedTextRun, DeletedTextRun } from "./track-revision"; -import { TextRun } from "../paragraph"; - -describe("InsertedTestRun", () => { - describe("#constructor", () => { - it("should create a inserted text run", () => { - const textRun = new TextRun({ - text: "some text", - }); - const insertedTextRun = new InsertedTextRun({ child: textRun, 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", - ], - }, - ], - }, - ], - }); - }); - }); -}); -describe("DeletedTestRun", () => { - describe("#constructor", () => { - it("should create a deleted text run", () => { - const insertedParagraph = new DeletedTextRun({ text: "some text", id: 0, date: "123", author: "Author" }); - const tree = new Formatter().format(insertedParagraph); - 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", - ], - }, - ], - }, - ], - }); - }); - }); -}); diff --git a/src/file/track-revision/track-revision.ts b/src/file/track-revision/track-revision.ts index 052c7e29ea..4318e9a468 100644 --- a/src/file/track-revision/track-revision.ts +++ b/src/file/track-revision/track-revision.ts @@ -1,72 +1,15 @@ -import { SpaceType } from "file/space-type"; -import { XmlComponent, XmlAttributeComponent } from "file/xml-components"; -import { TextRun } from "../index"; +import { XmlAttributeComponent } from "file/xml-components"; -export interface ITrackRevisionAttributesProperties { +export interface IChangedAttributesProperties { readonly id: number; readonly author: string; readonly date: string; } -export class TrackRevisionAttributes extends XmlAttributeComponent { +export class ChangeAttributes extends XmlAttributeComponent { protected readonly xmlKeys = { id: "w:id", author: "w:author", date: "w:date", }; } - -export interface IInsertedTextRunOptions extends ITrackRevisionAttributesProperties { - readonly child: TextRun; -} - -export interface IDeletedTextRunOptions extends ITrackRevisionAttributesProperties { - readonly text: string; -} - -export class InsertedTextRun extends XmlComponent { - constructor(options: IInsertedTextRunOptions) { - super("w:ins"); - this.root.push( - new TrackRevisionAttributes({ - id: options.id, - author: options.author, - date: options.date, - }), - ); - this.addChildElement(options.child); - } -} - -export class DeletedTextRunWrapper extends XmlComponent { - constructor(text: string) { - super("w:r"); - this.root.push(new DeletedText(text)); - } -} - -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); - } -} - -export class DeletedTextRun extends XmlComponent { - constructor(options: IDeletedTextRunOptions) { - super("w:del"); - this.root.push( - new TrackRevisionAttributes({ - id: options.id, - author: options.author, - date: options.date, - }), - ); - this.addChildElement(new DeletedTextRunWrapper(options.text)); - } -}