diff --git a/demo/35-hyperlinks.ts b/demo/35-hyperlinks.ts index 8346cd1028..85fa5fc1f3 100644 --- a/demo/35-hyperlinks.ts +++ b/demo/35-hyperlinks.ts @@ -1,13 +1,31 @@ // Example on how to add hyperlinks to websites // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { ExternalHyperlink, Document, Packer, Paragraph, Media, TextRun } from "../build"; +import { ExternalHyperlink, Document, Packer, Paragraph, Media, TextRun, Footer } from "../build"; const doc = new Document({}); const image1 = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); doc.addSection({ + footers: { + default: new Footer({ + children: [ + new Paragraph({ + children: [ + new TextRun("Click here for the "), + new ExternalHyperlink({ + child: new TextRun({ + text: "Footer external hyperlink", + style: "Hyperlink", + }), + link: "http://www.example.com", + }), + ], + }), + ], + }), + }, children: [ new Paragraph({ children: [ diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index a7b5fb617e..a0c73090fc 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -69,23 +69,23 @@ export class Compiler { private xmlifyFile(file: File, prettify?: boolean): IXmlifyedFileMapping { file.verifyUpdateFields(); - const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1; + const documentRelationshipCount = file.Document.Relationships.RelationshipCount + 1; - const documentXmlData = xml(this.formatter.format(file.Document, file), prettify); + const documentXmlData = xml(this.formatter.format(file.Document.View, file), prettify); const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media); return { Relationships: { data: (() => { documentMediaDatas.forEach((mediaData, i) => { - file.DocumentRelationships.createRelationship( + file.Document.Relationships.createRelationship( documentRelationshipCount + i, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, ); }); - return xml(this.formatter.format(file.DocumentRelationships, file), prettify); + return xml(this.formatter.format(file.Document.Relationships, file), prettify); })(), path: "word/_rels/document.xml.rels", }, @@ -120,7 +120,7 @@ export class Compiler { path: "_rels/.rels", }, HeaderRelationships: file.Headers.map((headerWrapper, index) => { - const xmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify); + const xmlData = xml(this.formatter.format(headerWrapper.View, file), prettify); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); mediaDatas.forEach((mediaData, i) => { @@ -137,7 +137,7 @@ export class Compiler { }; }), FooterRelationships: file.Footers.map((footerWrapper, index) => { - const xmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify); + const xmlData = xml(this.formatter.format(footerWrapper.View, file), prettify); const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media); mediaDatas.forEach((mediaData, i) => { @@ -154,7 +154,7 @@ export class Compiler { }; }), Headers: file.Headers.map((headerWrapper, index) => { - const tempXmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify); + const tempXmlData = xml(this.formatter.format(headerWrapper.View, 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); @@ -165,7 +165,7 @@ export class Compiler { }; }), Footers: file.Footers.map((footerWrapper, index) => { - const tempXmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify); + const tempXmlData = xml(this.formatter.format(footerWrapper.View, 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); diff --git a/src/file/document-wrapper.spec.ts b/src/file/document-wrapper.spec.ts new file mode 100644 index 0000000000..2a32ad7968 --- /dev/null +++ b/src/file/document-wrapper.spec.ts @@ -0,0 +1,50 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { FooterWrapper } from "./footer-wrapper"; +import { Media } from "./media"; +import { Paragraph } from "./paragraph"; +import { Table, TableCell, TableRow } from "./table"; + +describe("FooterWrapper", () => { + describe("#add", () => { + it("should call the underlying footer's addParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.View, "add"); + file.add(new Paragraph({})); + + expect(spy.called).to.equal(true); + }); + + it("should call the underlying footer's addParagraph", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.View, "add"); + file.add( + new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("hello")], + }), + ], + }), + ], + }), + ); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addChildElement", () => { + it("should call the underlying footer's addChildElement", () => { + const file = new FooterWrapper(new Media(), 1); + const spy = sinon.spy(file.View, "addChildElement"); + // tslint:disable-next-line:no-any + file.addChildElement({} as any); + + expect(spy.called).to.equal(true); + }); + }); +}); diff --git a/src/file/document-wrapper.ts b/src/file/document-wrapper.ts new file mode 100644 index 0000000000..3054f2c30e --- /dev/null +++ b/src/file/document-wrapper.ts @@ -0,0 +1,27 @@ +import { Document, IDocumentOptions } from "./document"; +import { Footer } from "./footer"; +import { Header } from "./header/header"; +import { Relationships } from "./relationships"; + +export interface IViewWrapper { + readonly View: Document | Footer | Header; + readonly Relationships: Relationships; +} + +export class DocumentWrapper implements IViewWrapper { + private readonly document: Document; + private readonly relationships: Relationships; + + constructor(options: IDocumentOptions) { + this.document = new Document(options); + this.relationships = new Relationships(); + } + + public get View(): Document { + return this.document; + } + + public get Relationships(): Relationships { + return this.relationships; + } +} diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index 133051cb6e..e3fb1be2a4 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -145,7 +145,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new HeaderReference({ headerType: HeaderReferenceType.DEFAULT, - headerId: headers.default.Header.ReferenceId, + headerId: headers.default.View.ReferenceId, }), ); } @@ -154,7 +154,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new HeaderReference({ headerType: HeaderReferenceType.FIRST, - headerId: headers.first.Header.ReferenceId, + headerId: headers.first.View.ReferenceId, }), ); } @@ -163,7 +163,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new HeaderReference({ headerType: HeaderReferenceType.EVEN, - headerId: headers.even.Header.ReferenceId, + headerId: headers.even.View.ReferenceId, }), ); } @@ -176,7 +176,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new FooterReference({ footerType: FooterReferenceType.DEFAULT, - footerId: footers.default.Footer.ReferenceId, + footerId: footers.default.View.ReferenceId, }), ); } @@ -185,7 +185,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new FooterReference({ footerType: FooterReferenceType.FIRST, - footerId: footers.first.Footer.ReferenceId, + footerId: footers.first.View.ReferenceId, }), ); } @@ -194,7 +194,7 @@ export class SectionProperties extends XmlComponent { this.root.push( new FooterReference({ footerType: FooterReferenceType.EVEN, - footerId: footers.even.Footer.ReferenceId, + footerId: footers.even.View.ReferenceId, }), ); } diff --git a/src/file/document/document.ts b/src/file/document/document.ts index 93d4035fa1..cc33709076 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -7,7 +7,7 @@ import { Body } from "./body"; import { DocumentAttributes } from "./document-attributes"; import { DocumentBackground, IDocumentBackgroundOptions } from "./document-background"; -interface IDocumentOptions { +export interface IDocumentOptions { readonly background: IDocumentBackgroundOptions; } diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 9632b80897..952fd6a35c 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -18,7 +18,7 @@ describe("File", () => { children: [], }); - const tree = new Formatter().format(doc.Document.Body); + const tree = new Formatter().format(doc.Document.View.Body); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default"); @@ -37,7 +37,7 @@ describe("File", () => { children: [], }); - const tree = new Formatter().format(doc.Document.Body); + const tree = new Formatter().format(doc.Document.View.Body); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][5]["w:footerReference"]._attr["w:type"]).to.equal("default"); @@ -56,7 +56,7 @@ describe("File", () => { children: [], }); - const tree = new Formatter().format(doc.Document.Body); + const tree = new Formatter().format(doc.Document.View.Body); expect(tree["w:body"][0]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first"); expect(tree["w:body"][0]["w:sectPr"][7]["w:footerReference"]._attr["w:type"]).to.equal("first"); @@ -79,7 +79,7 @@ describe("File", () => { children: [], }); - const tree = new Formatter().format(doc.Document.Body); + const tree = new Formatter().format(doc.Document.View.Body); expect(tree["w:body"][0]["w:sectPr"][4]["w:headerReference"]._attr["w:type"]).to.equal("default"); expect(tree["w:body"][0]["w:sectPr"][5]["w:headerReference"]._attr["w:type"]).to.equal("first"); @@ -97,7 +97,7 @@ describe("File", () => { }, ]); - const tree = new Formatter().format(doc.Document.Body); + const tree = new Formatter().format(doc.Document.View.Body); expect(tree).to.deep.equal({ "w:body": [ @@ -169,7 +169,7 @@ describe("File", () => { describe("#addSection", () => { it("should call the underlying document's add a Paragraph", () => { const file = new File(); - const spy = sinon.spy(file.Document, "add"); + const spy = sinon.spy(file.Document.View, "add"); file.addSection({ children: [new Paragraph({})], }); @@ -179,7 +179,7 @@ describe("File", () => { it("should call the underlying document's add when adding a Table", () => { const file = new File(); - const spy = sinon.spy(file.Document, "add"); + const spy = sinon.spy(file.Document.View, "add"); file.addSection({ children: [ new Table({ @@ -201,7 +201,7 @@ describe("File", () => { it("should call the underlying document's add when adding an Image (paragraph)", () => { const file = new File(); - const spy = sinon.spy(file.Document, "add"); + const spy = sinon.spy(file.Document.View, "add"); // tslint:disable-next-line:no-any file.addSection({ children: [new Paragraph("")], @@ -214,7 +214,7 @@ describe("File", () => { describe("#addSection", () => { it("should call the underlying document's add", () => { const file = new File(); - const spy = sinon.spy(file.Document, "add"); + const spy = sinon.spy(file.Document.View, "add"); file.addSection({ children: [new TableOfContents()], }); diff --git a/src/file/file.ts b/src/file/file.ts index 20d292f91b..9ec8316e9f 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -1,7 +1,7 @@ import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; -import { Document } from "./document"; +import { DocumentWrapper } from "./document-wrapper"; import { FooterReferenceType, HeaderReferenceType, @@ -46,10 +46,9 @@ export class File { // tslint:disable-next-line:readonly-keyword private currentRelationshipId: number = 1; - private readonly document: Document; + private readonly documentWrapper: DocumentWrapper; private readonly headers: IDocumentHeader[] = []; private readonly footers: IDocumentFooter[] = []; - private readonly docRelationships: Relationships; private readonly coreProperties: CoreProperties; private readonly numbering: Numbering; private readonly media: Media; @@ -77,12 +76,12 @@ export class File { config: [], }, ); - this.docRelationships = new Relationships(); + // this.documentWrapper.Relationships = new Relationships(); this.fileRelationships = new Relationships(); this.appProperties = new AppProperties(); this.footNotes = new FootNotes(); this.contentTypes = new ContentTypes(); - this.document = new Document({ background: options.background || {} }); + this.documentWrapper = new DocumentWrapper({ background: options.background || {} }); this.settings = new Settings({ compatabilityModeVersion: options.compatabilityModeVersion, }); @@ -130,10 +129,10 @@ export class File { } for (const section of sections) { - this.document.Body.addSection(section.properties ? section.properties : {}); + this.documentWrapper.View.Body.addSection(section.properties ? section.properties : {}); for (const child of section.children) { - this.document.add(child); + this.documentWrapper.View.add(child); } } @@ -158,7 +157,7 @@ export class File { properties, children, }: ISectionOptions): void { - this.document.Body.addSection({ + this.documentWrapper.View.Body.addSection({ ...properties, headers: { default: headers.default ? this.createHeader(headers.default) : this.createHeader(new Header()), @@ -175,12 +174,12 @@ export class File { }); for (const child of children) { - this.document.add(child); + this.documentWrapper.View.add(child); } } public verifyUpdateFields(): void { - if (this.document.getTablesOfContents().length) { + if (this.documentWrapper.View.getTablesOfContents().length) { this.settings.addUpdateFields(); } } @@ -209,8 +208,8 @@ export class File { private addHeaderToDocument(header: HeaderWrapper, type: HeaderReferenceType = HeaderReferenceType.DEFAULT): void { this.headers.push({ header, type }); - this.docRelationships.createRelationship( - header.Header.ReferenceId, + this.documentWrapper.Relationships.createRelationship( + header.View.ReferenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", `header${this.headers.length}.xml`, ); @@ -219,8 +218,8 @@ export class File { private addFooterToDocument(footer: FooterWrapper, type: FooterReferenceType = FooterReferenceType.DEFAULT): void { this.footers.push({ footer, type }); - this.docRelationships.createRelationship( - footer.Footer.ReferenceId, + this.documentWrapper.Relationships.createRelationship( + footer.View.ReferenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", `footer${this.footers.length}.xml`, ); @@ -244,30 +243,30 @@ export class File { "docProps/app.xml", ); - this.docRelationships.createRelationship( + this.documentWrapper.Relationships.createRelationship( this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "styles.xml", ); - this.docRelationships.createRelationship( + this.documentWrapper.Relationships.createRelationship( this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml", ); - this.docRelationships.createRelationship( + this.documentWrapper.Relationships.createRelationship( this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "footnotes.xml", ); - this.docRelationships.createRelationship( + this.documentWrapper.Relationships.createRelationship( this.currentRelationshipId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings", "settings.xml", ); } - public get Document(): Document { - return this.document; + public get Document(): DocumentWrapper { + return this.documentWrapper; } public get Styles(): Styles { @@ -286,10 +285,6 @@ export class File { return this.media; } - public get DocumentRelationships(): Relationships { - return this.docRelationships; - } - public get FileRelationships(): Relationships { return this.fileRelationships; } diff --git a/src/file/footer-wrapper.spec.ts b/src/file/footer-wrapper.spec.ts index 4f3e95acdb..2a32ad7968 100644 --- a/src/file/footer-wrapper.spec.ts +++ b/src/file/footer-wrapper.spec.ts @@ -10,7 +10,7 @@ describe("FooterWrapper", () => { describe("#add", () => { it("should call the underlying footer's addParagraph", () => { const file = new FooterWrapper(new Media(), 1); - const spy = sinon.spy(file.Footer, "add"); + const spy = sinon.spy(file.View, "add"); file.add(new Paragraph({})); expect(spy.called).to.equal(true); @@ -18,7 +18,7 @@ describe("FooterWrapper", () => { it("should call the underlying footer's addParagraph", () => { const file = new FooterWrapper(new Media(), 1); - const spy = sinon.spy(file.Footer, "add"); + const spy = sinon.spy(file.View, "add"); file.add( new Table({ rows: [ @@ -40,7 +40,7 @@ describe("FooterWrapper", () => { describe("#addChildElement", () => { it("should call the underlying footer's addChildElement", () => { const file = new FooterWrapper(new Media(), 1); - const spy = sinon.spy(file.Footer, "addChildElement"); + const spy = sinon.spy(file.View, "addChildElement"); // tslint:disable-next-line:no-any file.addChildElement({} as any); diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index 8390887ba4..e16a72e974 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -1,6 +1,7 @@ import { XmlComponent } from "file/xml-components"; import { FooterReferenceType } from "./document"; +import { IViewWrapper } from "./document-wrapper"; import { Footer } from "./footer/footer"; import { Media } from "./media"; import { Paragraph } from "./paragraph"; @@ -12,7 +13,7 @@ export interface IDocumentFooter { readonly type: FooterReferenceType; } -export class FooterWrapper { +export class FooterWrapper implements IViewWrapper { private readonly footer: Footer; private readonly relationships: Relationships; @@ -29,7 +30,7 @@ export class FooterWrapper { this.footer.addChildElement(childElement); } - public get Footer(): Footer { + public get View(): Footer { return this.footer; } diff --git a/src/file/header-wrapper.spec.ts b/src/file/header-wrapper.spec.ts index 00ee776a95..c6a7272d16 100644 --- a/src/file/header-wrapper.spec.ts +++ b/src/file/header-wrapper.spec.ts @@ -10,7 +10,7 @@ describe("HeaderWrapper", () => { describe("#add", () => { it("should call the underlying header's addChildElement for Paragraph", () => { const wrapper = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(wrapper.Header, "add"); + const spy = sinon.spy(wrapper.View, "add"); wrapper.add(new Paragraph({})); expect(spy.called).to.equal(true); @@ -18,7 +18,7 @@ describe("HeaderWrapper", () => { it("should call the underlying header's addChildElement for Table", () => { const wrapper = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(wrapper.Header, "add"); + const spy = sinon.spy(wrapper.View, "add"); wrapper.add( new Table({ rows: [ @@ -40,7 +40,7 @@ describe("HeaderWrapper", () => { describe("#addChildElement", () => { it("should call the underlying header's addChildElement", () => { const file = new HeaderWrapper(new Media(), 1); - const spy = sinon.spy(file.Header, "addChildElement"); + const spy = sinon.spy(file.View, "addChildElement"); // tslint:disable-next-line:no-any file.addChildElement({} as any); diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 407399a8fe..945a9d674a 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -1,6 +1,7 @@ import { XmlComponent } from "file/xml-components"; import { HeaderReferenceType } from "./document"; +import { IViewWrapper } from "./document-wrapper"; import { Header } from "./header/header"; import { Media } from "./media"; import { Paragraph } from "./paragraph"; @@ -12,7 +13,7 @@ export interface IDocumentHeader { readonly type: HeaderReferenceType; } -export class HeaderWrapper { +export class HeaderWrapper implements IViewWrapper { private readonly header: Header; private readonly relationships: Relationships; @@ -31,7 +32,7 @@ export class HeaderWrapper { this.header.addChildElement(childElement); } - public get Header(): Header { + public get View(): Header { return this.header; } diff --git a/src/file/media/media.ts b/src/file/media/media.ts index 6168fbc362..65a7f1bf58 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -1,14 +1,12 @@ import { IDrawingOptions } from "../drawing"; import { File } from "../file"; -import { FooterWrapper } from "../footer-wrapper"; -import { HeaderWrapper } from "../header-wrapper"; import { PictureRun } from "../paragraph"; import { IMediaData } from "./data"; // import { Image } from "./image"; export class Media { public static addImage( - file: File | HeaderWrapper | FooterWrapper, + file: File, buffer: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number, diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index 57590f6313..211697b850 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -804,8 +804,10 @@ describe("Paragraph", () => { ], }); const fileMock = ({ - DocumentRelationships: { - createRelationship: () => ({}), + Document: { + Relationships: { + createRelationship: () => ({}), + }, }, } as unknown) as File; paragraph.prepForXml(fileMock); diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index dcaf2c480d..2e75c77f4e 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -79,7 +79,7 @@ export class Paragraph extends XmlComponent { if (element instanceof ExternalHyperlink) { const index = this.root.indexOf(element); const concreteHyperlink = new ConcreteHyperlink(element.options.child, shortid.generate().toLowerCase()); - file.DocumentRelationships.createRelationship( + file.Document.Relationships.createRelationship( concreteHyperlink.linkId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", element.options.link,