From f90e84a88d7dbefabb7cc3befe0ae043ee9a1870 Mon Sep 17 00:00:00 2001 From: Dolan Miu Date: Mon, 1 Mar 2021 23:35:52 +0000 Subject: [PATCH] #627 Add footnotes relationships --- demo/35-hyperlinks.ts | 20 +++++++- src/export/packer/next-compiler.spec.ts | 7 +-- src/export/packer/next-compiler.ts | 7 ++- src/file/document-wrapper.spec.ts | 52 ++++---------------- src/file/document-wrapper.ts | 3 +- src/file/file.spec.ts | 2 +- src/file/file.ts | 13 +++-- src/file/footnotes-wrapper.spec.ts | 16 ++++++ src/file/footnotes-wrapper.ts | 21 ++++++++ src/file/footnotes/footnote/footnote.spec.ts | 9 +++- src/file/footnotes/footnote/footnote.ts | 11 +++-- src/file/footnotes/footnotes.ts | 12 +++-- 12 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 src/file/footnotes-wrapper.spec.ts create mode 100644 src/file/footnotes-wrapper.ts diff --git a/demo/35-hyperlinks.ts b/demo/35-hyperlinks.ts index c9ef863aa4..3e45e4c342 100644 --- a/demo/35-hyperlinks.ts +++ b/demo/35-hyperlinks.ts @@ -1,9 +1,24 @@ // 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 { Document, ExternalHyperlink, Footer, Media, Packer, Paragraph, TextRun } from "../build"; +import { Document, ExternalHyperlink, Footer, FootnoteReferenceRun, Media, Packer, Paragraph, TextRun } from "../build"; -const doc = new Document({}); +const doc = new Document({ + footnotes: [ + 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")); @@ -54,6 +69,7 @@ doc.addSection({ }), link: "http://www.example.com", }), + new FootnoteReferenceRun(1) ], }), new Paragraph({ diff --git a/src/export/packer/next-compiler.spec.ts b/src/export/packer/next-compiler.spec.ts index 7c9bcda904..b96492827c 100644 --- a/src/export/packer/next-compiler.spec.ts +++ b/src/export/packer/next-compiler.spec.ts @@ -22,13 +22,14 @@ describe("Compiler", () => { const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); - expect(fileNames).has.length(14); + expect(fileNames).has.length(15); expect(fileNames).to.include("word/document.xml"); expect(fileNames).to.include("word/styles.xml"); expect(fileNames).to.include("docProps/core.xml"); expect(fileNames).to.include("docProps/app.xml"); expect(fileNames).to.include("word/numbering.xml"); expect(fileNames).to.include("word/footnotes.xml"); + expect(fileNames).to.include("word/_rels/footnotes.xml.rels"); expect(fileNames).to.include("word/settings.xml"); expect(fileNames).to.include("word/_rels/document.xml.rels"); expect(fileNames).to.include("[Content_Types].xml"); @@ -62,7 +63,7 @@ describe("Compiler", () => { const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); expect(fileNames).is.an.instanceof(Array); - expect(fileNames).has.length(22); + expect(fileNames).has.length(23); expect(fileNames).to.include("word/header1.xml"); expect(fileNames).to.include("word/_rels/header1.xml.rels"); @@ -88,7 +89,7 @@ describe("Compiler", () => { const spy = sinon.spy(compiler["formatter"], "format"); compiler.compile(file); - expect(spy.callCount).to.equal(10); + expect(spy.callCount).to.equal(11); }); }); }); diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index aed3951ab5..29469f8910 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -25,6 +25,7 @@ interface IXmlifyedFileMapping { readonly ContentTypes: IXmlifyedFile; readonly AppProperties: IXmlifyedFile; readonly FootNotes: IXmlifyedFile; + readonly FootNotesRelationships: IXmlifyedFile; readonly Settings: IXmlifyedFile; } @@ -184,9 +185,13 @@ export class Compiler { path: "docProps/app.xml", }, FootNotes: { - data: xml(this.formatter.format(file.FootNotes, file.Document), prettify), + data: xml(this.formatter.format(file.FootNotes.View, file.FootNotes), prettify), path: "word/footnotes.xml", }, + FootNotesRelationships: { + data: xml(this.formatter.format(file.FootNotes.Relationships, file.FootNotes), prettify), + path: "word/_rels/footnotes.xml.rels", + }, Settings: { data: xml(this.formatter.format(file.Settings, file.Document), prettify), path: "word/settings.xml", diff --git a/src/file/document-wrapper.spec.ts b/src/file/document-wrapper.spec.ts index 2a32ad7968..344fded379 100644 --- a/src/file/document-wrapper.spec.ts +++ b/src/file/document-wrapper.spec.ts @@ -1,50 +1,16 @@ 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"; +import { DocumentWrapper } from "./document-wrapper"; -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({})); +describe("DocumentWrapper", () => { + describe("#constructor", () => { + it("should create", () => { + const file = new DocumentWrapper({ background: {} }); - 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); + // tslint:disable-next-line: no-unused-expression + expect(file.View).to.be.ok; + // tslint:disable-next-line: no-unused-expression + expect(file.Relationships).to.be.ok; }); }); }); diff --git a/src/file/document-wrapper.ts b/src/file/document-wrapper.ts index 3054f2c30e..a699136727 100644 --- a/src/file/document-wrapper.ts +++ b/src/file/document-wrapper.ts @@ -1,10 +1,11 @@ import { Document, IDocumentOptions } from "./document"; import { Footer } from "./footer"; +import { FootNotes } from "./footnotes"; import { Header } from "./header/header"; import { Relationships } from "./relationships"; export interface IViewWrapper { - readonly View: Document | Footer | Header; + readonly View: Document | Footer | Header | FootNotes; readonly Relationships: Relationships; } diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 952fd6a35c..3fc389e4bf 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -242,7 +242,7 @@ describe("File", () => { footnotes: [new Paragraph("hello")], }); - const tree = new Formatter().format(wrapper.FootNotes); + const tree = new Formatter().format(wrapper.FootNotes.View); expect(tree).to.deep.equal({ "w:footnotes": [ diff --git a/src/file/file.ts b/src/file/file.ts index 9ec8316e9f..57cbc069c8 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -11,7 +11,7 @@ import { import { IPageMarginAttributes } from "./document/body/section-properties/page-margin/page-margin-attributes"; import { IFileProperties } from "./file-properties"; import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; -import { FootNotes } from "./footnotes"; +import { FootnotesWrapper } from "./footnotes-wrapper"; import { Footer, Header } from "./header"; import { HeaderWrapper, IDocumentHeader } from "./header-wrapper"; import { Media } from "./media"; @@ -53,7 +53,7 @@ export class File { private readonly numbering: Numbering; private readonly media: Media; private readonly fileRelationships: Relationships; - private readonly footNotes: FootNotes; + private readonly footnotesWrapper: FootnotesWrapper; private readonly settings: Settings; private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; @@ -76,10 +76,9 @@ export class File { config: [], }, ); - // this.documentWrapper.Relationships = new Relationships(); this.fileRelationships = new Relationships(); this.appProperties = new AppProperties(); - this.footNotes = new FootNotes(); + this.footnotesWrapper = new FootnotesWrapper(); this.contentTypes = new ContentTypes(); this.documentWrapper = new DocumentWrapper({ background: options.background || {} }); this.settings = new Settings({ @@ -138,7 +137,7 @@ export class File { if (options.footnotes) { for (const paragraph of options.footnotes) { - this.footNotes.createFootNote(paragraph); + this.footnotesWrapper.View.createFootNote(paragraph); } } @@ -305,8 +304,8 @@ export class File { return this.appProperties; } - public get FootNotes(): FootNotes { - return this.footNotes; + public get FootNotes(): FootnotesWrapper { + return this.footnotesWrapper; } public get Settings(): Settings { diff --git a/src/file/footnotes-wrapper.spec.ts b/src/file/footnotes-wrapper.spec.ts new file mode 100644 index 0000000000..6427de6ee6 --- /dev/null +++ b/src/file/footnotes-wrapper.spec.ts @@ -0,0 +1,16 @@ +import { expect } from "chai"; + +import { FootnotesWrapper } from "./footnotes-wrapper"; + +describe("FootnotesWrapper", () => { + describe("#constructor", () => { + it("should create", () => { + const file = new FootnotesWrapper(); + + // tslint:disable-next-line: no-unused-expression + expect(file.View).to.be.ok; + // tslint:disable-next-line: no-unused-expression + expect(file.Relationships).to.be.ok; + }); + }); +}); diff --git a/src/file/footnotes-wrapper.ts b/src/file/footnotes-wrapper.ts new file mode 100644 index 0000000000..c76a8957c2 --- /dev/null +++ b/src/file/footnotes-wrapper.ts @@ -0,0 +1,21 @@ +import { IViewWrapper } from "./document-wrapper"; +import { FootNotes } from "./footnotes/footnotes"; +import { Relationships } from "./relationships"; + +export class FootnotesWrapper implements IViewWrapper { + private readonly footnotess: FootNotes; + private readonly relationships: Relationships; + + constructor() { + this.footnotess = new FootNotes(); + this.relationships = new Relationships(); + } + + public get View(): FootNotes { + return this.footnotess; + } + + public get Relationships(): Relationships { + return this.relationships; + } +} diff --git a/src/file/footnotes/footnote/footnote.spec.ts b/src/file/footnotes/footnote/footnote.spec.ts index 8594cd75fd..49e497d988 100644 --- a/src/file/footnotes/footnote/footnote.spec.ts +++ b/src/file/footnotes/footnote/footnote.spec.ts @@ -7,7 +7,10 @@ import { Footnote, FootnoteType } from "./footnote"; describe("Footnote", () => { describe("#constructor", () => { it("should create a footnote with a footnote type", () => { - const footnote = new Footnote(1, FootnoteType.SEPERATOR); + const footnote = new Footnote({ + id: 1, + type: FootnoteType.SEPERATOR, + }); const tree = new Formatter().format(footnote); expect(Object.keys(tree)).to.deep.equal(["w:footnote"]); @@ -15,7 +18,9 @@ describe("Footnote", () => { }); it("should create a footnote without a footnote type", () => { - const footnote = new Footnote(1); + const footnote = new Footnote({ + id: 1, + }); const tree = new Formatter().format(footnote); expect(Object.keys(tree)).to.deep.equal(["w:footnote"]); diff --git a/src/file/footnotes/footnote/footnote.ts b/src/file/footnotes/footnote/footnote.ts index 60e6e533c8..4ef653ddfb 100644 --- a/src/file/footnotes/footnote/footnote.ts +++ b/src/file/footnotes/footnote/footnote.ts @@ -9,13 +9,18 @@ export enum FootnoteType { CONTINUATION_SEPERATOR = "continuationSeparator", } +export interface IFootnoteOptions { + readonly id: number; + readonly type?: FootnoteType; +} + export class Footnote extends XmlComponent { - constructor(id: number, type?: FootnoteType) { + constructor(options: IFootnoteOptions) { super("w:footnote"); this.root.push( new FootnoteAttributes({ - type: type, - id: id, + type: options.type, + id: options.id, }), ); } diff --git a/src/file/footnotes/footnotes.ts b/src/file/footnotes/footnotes.ts index 91ba1d199d..09ed315f4b 100644 --- a/src/file/footnotes/footnotes.ts +++ b/src/file/footnotes/footnotes.ts @@ -36,7 +36,10 @@ export class FootNotes extends XmlComponent { }), ); - const begin = new Footnote(-1, FootnoteType.SEPERATOR); + const begin = new Footnote({ + id: -1, + type: FootnoteType.SEPERATOR, + }); begin.add( new Paragraph({ spacing: { @@ -49,7 +52,10 @@ export class FootNotes extends XmlComponent { ); this.root.push(begin); - const spacing = new Footnote(0, FootnoteType.CONTINUATION_SEPERATOR); + const spacing = new Footnote({ + id: 0, + type: FootnoteType.CONTINUATION_SEPERATOR, + }); spacing.add( new Paragraph({ spacing: { @@ -64,7 +70,7 @@ export class FootNotes extends XmlComponent { } public createFootNote(paragraph: Paragraph): void { - const footnote = new Footnote(this.currentId); + const footnote = new Footnote({ id: this.currentId }); footnote.add(paragraph); this.root.push(footnote);