diff --git a/demo/35-hyperlinks.ts b/demo/35-hyperlinks.ts index 3e45e4c342..a350198113 100644 --- a/demo/35-hyperlinks.ts +++ b/demo/35-hyperlinks.ts @@ -4,20 +4,24 @@ import * as fs from "fs"; import { Document, ExternalHyperlink, Footer, FootnoteReferenceRun, Media, Packer, Paragraph, TextRun } from "../build"; const doc = new Document({ - footnotes: [ - new Paragraph({ + footnotes: { + 1: { children: [ - new TextRun("Click here for the "), - new ExternalHyperlink({ - child: new TextRun({ - text: "Footnotes external hyperlink", - style: "Hyperlink", - }), - link: "http://www.example.com", + new Paragraph({ + children: [ + new TextRun("Click here for the "), + new ExternalHyperlink({ + child: new TextRun({ + text: "Footnotes external hyperlink", + style: "Hyperlink", + }), + link: "http://www.example.com", + }), + ], }), ], - }), - ], + }, + }, }); const image1 = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); @@ -69,7 +73,7 @@ doc.addSection({ }), link: "http://www.example.com", }), - new FootnoteReferenceRun(1) + new FootnoteReferenceRun(1), ], }), new Paragraph({ diff --git a/demo/60-track-revisions.ts b/demo/60-track-revisions.ts index e035fbb18a..5ed136e27e 100644 --- a/demo/60-track-revisions.ts +++ b/demo/60-track-revisions.ts @@ -27,25 +27,29 @@ import { */ const doc = new Document({ - footnotes: [ - new Paragraph({ + footnotes: { + 1: { 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", + 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", + }), + ], }), ], - }), - ], + }, + }, features: { trackRevisions: true, }, diff --git a/src/export/formatter.ts b/src/export/formatter.ts index a91702212e..cf71dc9ba1 100644 --- a/src/export/formatter.ts +++ b/src/export/formatter.ts @@ -1,9 +1,9 @@ -import { IViewWrapper } from "file/document-wrapper"; -import { BaseXmlComponent, IXmlableObject } from "file/xml-components"; +import { BaseXmlComponent, IContext, IXmlableObject } from "file/xml-components"; export class Formatter { - public format(input: BaseXmlComponent, file?: IViewWrapper): IXmlableObject { - const output = input.prepForXml(file); + // tslint:disable-next-line: no-object-literal-type-assertion + public format(input: BaseXmlComponent, context: IContext = {} as IContext): IXmlableObject { + const output = input.prepForXml(context); if (output) { return output; diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index e5f66c8d96..3d99f93c64 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -73,7 +73,13 @@ export class Compiler { file.verifyUpdateFields(); const documentRelationshipCount = file.Document.Relationships.RelationshipCount + 1; - const documentXmlData = xml(this.formatter.format(file.Document.View, file.Document), prettify); + const documentXmlData = xml( + this.formatter.format(file.Document.View, { + viewWrapper: file.Document, + file, + }), + prettify, + ); const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media); return { @@ -87,7 +93,13 @@ export class Compiler { ); }); - return xml(this.formatter.format(file.Document.Relationships, file.Document), prettify); + return xml( + this.formatter.format(file.Document.Relationships, { + viewWrapper: file.Document, + file, + }), + prettify, + ); })(), path: "word/_rels/document.xml.rels", }, @@ -101,28 +113,58 @@ export class Compiler { path: "word/document.xml", }, Styles: { - data: xml(this.formatter.format(file.Styles, file.Document), prettify), + data: xml( + this.formatter.format(file.Styles, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "word/styles.xml", }, Properties: { - data: xml(this.formatter.format(file.CoreProperties, file.Document), { - declaration: { - standalone: "yes", - encoding: "UTF-8", + data: xml( + this.formatter.format(file.CoreProperties, { + viewWrapper: file.Document, + file, + }), + { + declaration: { + standalone: "yes", + encoding: "UTF-8", + }, }, - }), + ), path: "docProps/core.xml", }, Numbering: { - data: xml(this.formatter.format(file.Numbering, file.Document), prettify), + data: xml( + this.formatter.format(file.Numbering, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "word/numbering.xml", }, FileRelationships: { - data: xml(this.formatter.format(file.FileRelationships, file.Document), prettify), + data: xml( + this.formatter.format(file.FileRelationships, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "_rels/.rels", }, HeaderRelationships: file.Headers.map((headerWrapper, index) => { - const xmlData = xml(this.formatter.format(headerWrapper.View, headerWrapper), prettify); + const xmlData = xml( + this.formatter.format(headerWrapper.View, { + viewWrapper: headerWrapper, + file, + }), + prettify, + ); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); mediaDatas.forEach((mediaData, i) => { @@ -134,12 +176,24 @@ export class Compiler { }); return { - data: xml(this.formatter.format(headerWrapper.Relationships, headerWrapper), prettify), + data: xml( + this.formatter.format(headerWrapper.Relationships, { + viewWrapper: headerWrapper, + file, + }), + prettify, + ), path: `word/_rels/header${index + 1}.xml.rels`, }; }), FooterRelationships: file.Footers.map((footerWrapper, index) => { - const xmlData = xml(this.formatter.format(footerWrapper.View, footerWrapper), prettify); + const xmlData = xml( + this.formatter.format(footerWrapper.View, { + viewWrapper: footerWrapper, + file, + }), + prettify, + ); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); mediaDatas.forEach((mediaData, i) => { @@ -151,12 +205,24 @@ export class Compiler { }); return { - data: xml(this.formatter.format(footerWrapper.Relationships, footerWrapper), prettify), + data: xml( + this.formatter.format(footerWrapper.Relationships, { + viewWrapper: footerWrapper, + file, + }), + prettify, + ), path: `word/_rels/footer${index + 1}.xml.rels`, }; }), Headers: file.Headers.map((headerWrapper, index) => { - const tempXmlData = xml(this.formatter.format(headerWrapper.View, headerWrapper), prettify); + const tempXmlData = xml( + this.formatter.format(headerWrapper.View, { + viewWrapper: headerWrapper, + file, + }), + prettify, + ); const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); // TODO: 0 needs to be changed when headers get relationships of their own const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); @@ -167,7 +233,13 @@ export class Compiler { }; }), Footers: file.Footers.map((footerWrapper, index) => { - const tempXmlData = xml(this.formatter.format(footerWrapper.View, footerWrapper), prettify); + const tempXmlData = xml( + this.formatter.format(footerWrapper.View, { + viewWrapper: footerWrapper, + file, + }), + prettify, + ); const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media); // TODO: 0 needs to be changed when headers get relationships of their own const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0); @@ -178,27 +250,63 @@ export class Compiler { }; }), ContentTypes: { - data: xml(this.formatter.format(file.ContentTypes, file.Document), prettify), + data: xml( + this.formatter.format(file.ContentTypes, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "[Content_Types].xml", }, CustomProperties: { - data: xml(this.formatter.format(file.CustomProperties, file.Document), prettify), + data: xml( + this.formatter.format(file.CustomProperties, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "docProps/custom.xml", }, AppProperties: { - data: xml(this.formatter.format(file.AppProperties, file.Document), prettify), + data: xml( + this.formatter.format(file.AppProperties, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "docProps/app.xml", }, FootNotes: { - data: xml(this.formatter.format(file.FootNotes.View, file.FootNotes), prettify), + data: xml( + this.formatter.format(file.FootNotes.View, { + viewWrapper: file.FootNotes, + file: file, + }), + prettify, + ), path: "word/footnotes.xml", }, FootNotesRelationships: { - data: xml(this.formatter.format(file.FootNotes.Relationships, file.FootNotes), prettify), + data: xml( + this.formatter.format(file.FootNotes.Relationships, { + viewWrapper: file.FootNotes, + file: file, + }), + prettify, + ), path: "word/_rels/footnotes.xml.rels", }, Settings: { - data: xml(this.formatter.format(file.Settings, file.Document), prettify), + data: xml( + this.formatter.format(file.Settings, { + viewWrapper: file.Document, + file, + }), + prettify, + ), path: "word/settings.xml", }, }; diff --git a/src/file/custom-properties/custom-properties.ts b/src/file/custom-properties/custom-properties.ts index c254ceeca8..2ddd01919b 100644 --- a/src/file/custom-properties/custom-properties.ts +++ b/src/file/custom-properties/custom-properties.ts @@ -1,4 +1,4 @@ -import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { CustomPropertiesAttributes } from "./custom-properties-attributes"; import { CustomProperty, ICustomPropertyOptions } from "./custom-property"; @@ -26,9 +26,9 @@ export class CustomProperties extends XmlComponent { } } - public prepForXml(): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { this.properties.forEach((x) => this.root.push(x)); - return super.prepForXml(); + return super.prepForXml(context); } public addCustomProperty(property: ICustomPropertyOptions): void { diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index 4b0e588809..6b965ab268 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -1,5 +1,4 @@ -import { IViewWrapper } from "file/document-wrapper"; -import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { Paragraph, ParagraphProperties, TableOfContents } from "../.."; import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; @@ -26,13 +25,13 @@ export class Body extends XmlComponent { this.sections.push(new SectionProperties(options)); } - public prepForXml(file?: IViewWrapper): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { if (this.sections.length === 1) { this.root.splice(0, 1); this.root.push(this.sections.pop() as SectionProperties); } - return super.prepForXml(file); + return super.prepForXml(context); } public push(component: XmlComponent): void { diff --git a/src/file/numbering/numbering.ts b/src/file/numbering/numbering.ts index 901f13f4bb..bfe0e99f0b 100644 --- a/src/file/numbering/numbering.ts +++ b/src/file/numbering/numbering.ts @@ -1,7 +1,7 @@ // http://officeopenxml.com/WPnumbering.php import { convertInchesToTwip } from "convenience-functions"; import { AlignmentType } from "file/paragraph"; -import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { DocumentAttributes } from "../document/document-attributes"; import { AbstractNumbering } from "./abstract-numbering"; @@ -158,10 +158,10 @@ export class Numbering extends XmlComponent { } } - public prepForXml(): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { this.abstractNumbering.forEach((x) => this.root.push(x)); this.concreteNumbering.forEach((x) => this.root.push(x)); - return super.prepForXml(); + return super.prepForXml(context); } private createConcreteNumbering(abstractNumbering: AbstractNumbering, reference?: string): ConcreteNumbering { diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index 0aa4d98b2d..8536a3e84e 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -6,6 +6,7 @@ import { Formatter } from "export/formatter"; import { EMPTY_OBJECT } from "file/xml-components"; import { IViewWrapper } from "../document-wrapper"; +import { File } from "../file"; import { ShadingType } from "../table/shading"; import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting"; import { Bookmark, ExternalHyperlink } from "./links"; @@ -830,12 +831,17 @@ describe("Paragraph", () => { }), ], }); - const fileMock = ({ + const viewWrapperMock = ({ Relationships: { createRelationship: () => ({}), }, } as unknown) as IViewWrapper; - paragraph.prepForXml(fileMock); + + const file = ({} as unknown) as File; + paragraph.prepForXml({ + viewWrapper: viewWrapperMock, + file: file, + }); const tree = new Formatter().format(paragraph); expect(tree).to.deep.equal({ "w:p": [ diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index ae9d9b5d99..f5a64aee57 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -2,9 +2,8 @@ import * as shortid from "shortid"; import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; -import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; -import { IViewWrapper } from "../document-wrapper"; import { TargetModeType } from "../relationships/relationship/relationship"; import { DeletedTextRun, InsertedTextRun } from "../track-revision"; import { PageBreak } from "./formatting/page-break"; @@ -76,12 +75,12 @@ export class Paragraph extends XmlComponent { } } - public prepForXml(file: IViewWrapper): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { for (const element of this.root) { if (element instanceof ExternalHyperlink) { const index = this.root.indexOf(element); const concreteHyperlink = new ConcreteHyperlink(element.options.child, shortid.generate().toLowerCase()); - file.Relationships.createRelationship( + context.viewWrapper.Relationships.createRelationship( concreteHyperlink.linkId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", element.options.link, @@ -91,7 +90,7 @@ export class Paragraph extends XmlComponent { } } - return super.prepForXml(); + return super.prepForXml(context); } public addRunToFront(run: Run): Paragraph { diff --git a/src/file/table/table-cell/table-cell.ts b/src/file/table/table-cell/table-cell.ts index 9c8e8efd58..c79468e208 100644 --- a/src/file/table/table-cell/table-cell.ts +++ b/src/file/table/table-cell/table-cell.ts @@ -1,8 +1,7 @@ // http://officeopenxml.com/WPtableGrid.php -import { IViewWrapper } from "file/document-wrapper"; import { Paragraph } from "file/paragraph"; import { BorderStyle } from "file/styles"; -import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "file/xml-components"; import { ITableShadingAttributesProperties } from "../shading"; import { Table } from "../table"; @@ -105,11 +104,11 @@ export class TableCell extends XmlComponent { } } - public prepForXml(file?: IViewWrapper): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { // Cells must end with a paragraph if (!(this.root[this.root.length - 1] instanceof Paragraph)) { this.root.push(new Paragraph({})); } - return super.prepForXml(file); + return super.prepForXml(context); } } diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts index 4bb37c8ad9..375e30d910 100644 --- a/src/file/xml-components/base.ts +++ b/src/file/xml-components/base.ts @@ -1,6 +1,12 @@ import { IViewWrapper } from "../document-wrapper"; +import { File } from "../file"; import { IXmlableObject } from "./xmlable-object"; +export interface IContext { + readonly file: File; + readonly viewWrapper: IViewWrapper; +} + export abstract class BaseXmlComponent { protected readonly rootKey: string; // tslint:disable-next-line:readonly-keyword @@ -10,7 +16,7 @@ export abstract class BaseXmlComponent { this.rootKey = rootKey; } - public abstract prepForXml(file?: IViewWrapper): IXmlableObject | undefined; + public abstract prepForXml(context: IContext): IXmlableObject | undefined; public get IsDeleted(): boolean { return this.deleted; diff --git a/src/file/xml-components/default-attributes.ts b/src/file/xml-components/default-attributes.ts index 88f91bf932..04d6a27ab8 100644 --- a/src/file/xml-components/default-attributes.ts +++ b/src/file/xml-components/default-attributes.ts @@ -1,4 +1,4 @@ -import { BaseXmlComponent } from "./base"; +import { BaseXmlComponent, IContext } from "./base"; import { IXmlableObject } from "./xmlable-object"; export type AttributeMap = { [P in keyof T]: string }; @@ -13,7 +13,7 @@ export abstract class XmlAttributeComponent extends BaseXmlComponent { this.root = properties; } - public prepForXml(): IXmlableObject { + public prepForXml(_: IContext): IXmlableObject { const attrs = {}; Object.keys(this.root).forEach((key) => { const value = this.root[key]; diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts index 7ece145cc1..0f590b1286 100644 --- a/src/file/xml-components/imported-xml-component.spec.ts +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { Element, xml2js } from "xml-js"; import { EMPTY_OBJECT, ImportedXmlComponent } from "./"; +import { IContext } from "./base"; import { convertToXmlComponent } from "./imported-xml-component"; const xmlString = ` @@ -63,7 +64,8 @@ describe("ImportedXmlComponent", () => { describe("#prepForXml()", () => { it("should transform for xml", () => { - const converted = importedXmlComponent.prepForXml(); + // tslint:disable-next-line: no-object-literal-type-assertion + const converted = importedXmlComponent.prepForXml({} as IContext); expect(converted).to.deep.equal({ "w:test": [ { diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts index 790ff936ac..0bf29c401e 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -1,6 +1,7 @@ // tslint:disable:no-any import { Element as XmlElement, xml2js } from "xml-js"; import { IXmlableObject, XmlAttributeComponent, XmlComponent } from "."; +import { IContext } from "./base"; /** * Converts the given xml element (in json format) into XmlComponent. @@ -72,7 +73,7 @@ export class ImportedRootElementAttributes extends XmlComponent { super(""); } - public prepForXml(): IXmlableObject { + public prepForXml(_: IContext): IXmlableObject { return { _attr: this._attr, }; diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts index b626289d99..4fa5bfc515 100644 --- a/src/file/xml-components/xml-component.spec.ts +++ b/src/file/xml-components/xml-component.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; import { EMPTY_OBJECT, XmlComponent } from "./"; +import { IContext } from "./base"; class TestComponent extends XmlComponent {} @@ -27,7 +28,8 @@ describe("XmlComponent", () => { child.delete(); xmlComponent.addChildElement(child); - const xml = xmlComponent.prepForXml(); + // tslint:disable-next-line: no-object-literal-type-assertion + const xml = xmlComponent.prepForXml({} as IContext); if (!xml) { return; diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index 84fd5f98e4..a4e77b30a6 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -1,5 +1,4 @@ -import { IViewWrapper } from "../document-wrapper"; -import { BaseXmlComponent } from "./base"; +import { BaseXmlComponent, IContext } from "./base"; import { IXmlableObject } from "./xmlable-object"; export const EMPTY_OBJECT = Object.seal({}); @@ -13,7 +12,7 @@ export abstract class XmlComponent extends BaseXmlComponent { this.root = new Array(); } - public prepForXml(file?: IViewWrapper): IXmlableObject | undefined { + public prepForXml(context: IContext): IXmlableObject | undefined { const children = this.root .filter((c) => { if (c instanceof BaseXmlComponent) { @@ -23,7 +22,7 @@ export abstract class XmlComponent extends BaseXmlComponent { }) .map((comp) => { if (comp instanceof BaseXmlComponent) { - return comp.prepForXml(file); + return comp.prepForXml(context); } return comp; }) @@ -52,8 +51,8 @@ export abstract class XmlComponent extends BaseXmlComponent { } export abstract class IgnoreIfEmptyXmlComponent extends XmlComponent { - public prepForXml(): IXmlableObject | undefined { - const result = super.prepForXml(); + public prepForXml(context: IContext): IXmlableObject | undefined { + const result = super.prepForXml(context); // Ignore the object if its falsey or is an empty object (would produce // an empty XML element if allowed to be included in the output). if (result && (typeof result[this.rootKey] !== "object" || Object.keys(result[this.rootKey]).length)) {