improved signature for deleted text runs, added demo 54 and added documentation for change tracking

This commit is contained in:
Thomas Jansen
2020-10-07 11:44:23 +02:00
parent 065c17de74
commit cae6405d9a
14 changed files with 796 additions and 138 deletions

132
demo/54-track-revisions.ts Normal file
View File

@ -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 `<w:trackRevisions />` 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);
});

View File

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

View File

@ -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()
```

View File

@ -1 +1,2 @@
export * from "./track-revision";
export * from "./track-revision-components/inserted-text-run";
export * from "./track-revision-components/deleted-text-run";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ITrackRevisionAttributesProperties> {
export class ChangeAttributes extends XmlAttributeComponent<IChangedAttributesProperties> {
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));
}
}