Merge pull request #644 from branzillacorp/track-revisions
basic support for track revisions
This commit is contained in:
132
demo/54-track-revisions.ts
Normal file
132
demo/54-track-revisions.ts
Normal 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);
|
||||||
|
});
|
@ -20,6 +20,7 @@
|
|||||||
* [Tab Stops](usage/tab-stops.md)
|
* [Tab Stops](usage/tab-stops.md)
|
||||||
* [Table of Contents](usage/table-of-contents.md)
|
* [Table of Contents](usage/table-of-contents.md)
|
||||||
* [Page Numbers](usage/page-numbers.md)
|
* [Page Numbers](usage/page-numbers.md)
|
||||||
|
* [Change Tracking](usage/change-tracking.md)
|
||||||
* Styling
|
* Styling
|
||||||
* [Styling with JS](usage/styling-with-js.md)
|
* [Styling with JS](usage/styling-with-js.md)
|
||||||
* [Styling with XML](usage/styling-with-xml.md)
|
* [Styling with XML](usage/styling-with-xml.md)
|
||||||
@ -28,4 +29,3 @@
|
|||||||
* [Packers](usage/packers.md)
|
* [Packers](usage/packers.md)
|
||||||
|
|
||||||
* [Contribution Guidelines](contribution-guidelines.md)
|
* [Contribution Guidelines](contribution-guidelines.md)
|
||||||
|
|
||||||
|
58
docs/usage/change-tracking.md
Normal file
58
docs/usage/change-tracking.md
Normal 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()
|
||||||
|
```
|
@ -13,3 +13,4 @@ export * from "./header-wrapper";
|
|||||||
export * from "./footer-wrapper";
|
export * from "./footer-wrapper";
|
||||||
export * from "./header";
|
export * from "./header";
|
||||||
export * from "./footnotes";
|
export * from "./footnotes";
|
||||||
|
export * from "./track-revision";
|
||||||
|
@ -3,6 +3,7 @@ import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"
|
|||||||
import { IXmlableObject, XmlComponent } from "file/xml-components";
|
import { IXmlableObject, XmlComponent } from "file/xml-components";
|
||||||
|
|
||||||
import { File } from "../file";
|
import { File } from "../file";
|
||||||
|
import { InsertedTextRun, DeletedTextRun } from "../track-revision";
|
||||||
import { PageBreak } from "./formatting/page-break";
|
import { PageBreak } from "./formatting/page-break";
|
||||||
import { Bookmark, HyperlinkRef } from "./links";
|
import { Bookmark, HyperlinkRef } from "./links";
|
||||||
import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties";
|
import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties";
|
||||||
@ -19,6 +20,8 @@ export interface IParagraphOptions extends IParagraphPropertiesOptions {
|
|||||||
| SequentialIdentifier
|
| SequentialIdentifier
|
||||||
| FootnoteReferenceRun
|
| FootnoteReferenceRun
|
||||||
| HyperlinkRef
|
| HyperlinkRef
|
||||||
|
| InsertedTextRun
|
||||||
|
| DeletedTextRun
|
||||||
)[];
|
)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,4 +79,47 @@ describe("Settings", () => {
|
|||||||
expect(keys[0]).to.be.equal("w:compat");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
|
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
|
||||||
import { Compatibility } from "./compatibility";
|
import { Compatibility } from "./compatibility";
|
||||||
import { UpdateFields } from "./update-fields";
|
import { UpdateFields } from "./update-fields";
|
||||||
|
import { TrackRevisions } from "./track-revisions";
|
||||||
|
|
||||||
export interface ISettingsAttributesProperties {
|
export interface ISettingsAttributesProperties {
|
||||||
readonly wpc?: string;
|
readonly wpc?: string;
|
||||||
@ -46,6 +47,7 @@ export class SettingsAttributes extends XmlAttributeComponent<ISettingsAttribute
|
|||||||
|
|
||||||
export class Settings extends XmlComponent {
|
export class Settings extends XmlComponent {
|
||||||
private readonly compatibility;
|
private readonly compatibility;
|
||||||
|
private readonly trackRevisions;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super("w:settings");
|
super("w:settings");
|
||||||
@ -71,6 +73,7 @@ export class Settings extends XmlComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
this.compatibility = new Compatibility();
|
this.compatibility = new Compatibility();
|
||||||
|
this.trackRevisions = new TrackRevisions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUpdateFields(): void {
|
public addUpdateFields(): void {
|
||||||
@ -86,4 +89,12 @@ export class Settings extends XmlComponent {
|
|||||||
|
|
||||||
return this.compatibility;
|
return this.compatibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addTrackRevisions(): TrackRevisions {
|
||||||
|
if (!this.root.find((child) => child instanceof TrackRevisions)) {
|
||||||
|
this.addChildElement(this.trackRevisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.trackRevisions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
16
src/file/settings/track-revisions.spec.ts
Normal file
16
src/file/settings/track-revisions.spec.ts
Normal file
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
7
src/file/settings/track-revisions.ts
Normal file
7
src/file/settings/track-revisions.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { XmlComponent } from "file/xml-components";
|
||||||
|
|
||||||
|
export class TrackRevisions extends XmlComponent {
|
||||||
|
constructor() {
|
||||||
|
super("w:trackRevisions");
|
||||||
|
}
|
||||||
|
}
|
2
src/file/track-revision/index.ts
Normal file
2
src/file/track-revision/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./track-revision-components/inserted-text-run";
|
||||||
|
export * from "./track-revision-components/deleted-text-run";
|
@ -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"] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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 } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
15
src/file/track-revision/track-revision.ts
Normal file
15
src/file/track-revision/track-revision.ts
Normal file
@ -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<IChangedAttributesProperties> {
|
||||||
|
protected readonly xmlKeys = {
|
||||||
|
id: "w:id",
|
||||||
|
author: "w:author",
|
||||||
|
date: "w:date",
|
||||||
|
};
|
||||||
|
}
|
Reference in New Issue
Block a user