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", + }; +}