diff --git a/demo/21-bookmarks.ts b/demo/21-bookmarks.ts index 1ad50eb9b1..632fcb212f 100644 --- a/demo/21-bookmarks.ts +++ b/demo/21-bookmarks.ts @@ -1,7 +1,7 @@ // This demo shows how to create bookmarks then link to them with internal hyperlinks // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Bookmark, Document, HeadingLevel, HyperlinkRef, HyperlinkType, Packer, PageBreak, Paragraph } from "../build"; +import { Bookmark, Document, Footer, HeadingLevel, InternalHyperlink, Packer, PageBreak, Paragraph, TextRun } from "../build"; const LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam mi velit, convallis convallis scelerisque nec, faucibus nec leo. Phasellus at posuere mauris, tempus dignissim velit. Integer et tortor dolor. Duis auctor efficitur mattis. Vivamus ut metus accumsan tellus auctor sollicitudin venenatis et nibh. Cras quis massa ac metus fringilla venenatis. Proin rutrum mauris purus, ut suscipit magna consectetur id. Integer consectetur sollicitudin ante, vitae faucibus neque efficitur in. Praesent ultricies nibh lectus. Mauris pharetra id odio eget iaculis. Duis dictum, risus id pellentesque rutrum, lorem quam malesuada massa, quis ullamcorper turpis urna a diam. Cras vulputate metus vel massa porta ullamcorper. Etiam porta condimentum nulla nec tristique. Sed nulla urna, pharetra non tortor sed, sollicitudin molestie diam. Maecenas enim leo, feugiat eget vehicula id, sollicitudin vitae ante."; @@ -10,15 +10,26 @@ const doc = new Document({ creator: "Clippy", title: "Sample Document", description: "A brief example of using docx with bookmarks and internal hyperlinks", - hyperlinks: { - myAnchorId: { - text: "Hyperlink", - type: HyperlinkType.INTERNAL, - }, - }, }); doc.addSection({ + footers: { + default: new Footer({ + children: [ + new Paragraph({ + children: [ + new InternalHyperlink({ + child: new TextRun({ + text: "Click here!", + style: "Hyperlink", + }), + anchor: "myAnchorId", + }), + ], + }), + ], + }), + }, children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, @@ -30,7 +41,15 @@ doc.addSection({ children: [new PageBreak()], }), new Paragraph({ - children: [new HyperlinkRef("myAnchorId")], + children: [ + new InternalHyperlink({ + child: new TextRun({ + text: "Anchor Text", + style: "Hyperlink", + }), + anchor: "myAnchorId", + }), + ], }), ], }); diff --git a/demo/35-hyperlinks.ts b/demo/35-hyperlinks.ts index 65ed9390d4..8346cd1028 100644 --- a/demo/35-hyperlinks.ts +++ b/demo/35-hyperlinks.ts @@ -1,32 +1,39 @@ // 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, HyperlinkRef, HyperlinkType, Packer, Paragraph, Media } from "../build"; +import { ExternalHyperlink, Document, Packer, Paragraph, Media, TextRun } from "../build"; -const doc = new Document({ - hyperlinks: { - myCoolLink: { - link: "http://www.example.com", - text: "Hyperlink", - type: HyperlinkType.EXTERNAL, - }, - myOtherLink: { - link: "http://www.google.com", - text: "Google Link", - type: HyperlinkType.EXTERNAL, - }, - }, -}); +const doc = new Document({}); const image1 = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); doc.addSection({ children: [ new Paragraph({ - children: [new HyperlinkRef("myCoolLink")], + children: [ + new ExternalHyperlink({ + child: new TextRun({ + text: "Anchor Text", + style: "Hyperlink", + }), + link: "http://www.example.com", + }), + ], }), new Paragraph({ - children: [image1, new HyperlinkRef("myOtherLink")], + children: [ + new ExternalHyperlink({ + child: image1, + link: "http://www.google.com", + }), + new ExternalHyperlink({ + child: new TextRun({ + text: "BBC News Link", + style: "Hyperlink", + }), + link: "https://www.bbc.co.uk/news", + }), + ], }), ], }); diff --git a/package-lock.json b/package-lock.json index 51c6df7417..6d7d1eef60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4884,7 +4884,7 @@ }, "jsesc": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, @@ -8149,9 +8149,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", + "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 562c48b57c..1671bfdfb3 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "tslint": "^6.1.3", "tslint-immutable": "^6.0.1", "typedoc": "^0.16.11", - "typescript": "2.9.2", + "typescript": "4.2.2", "webpack": "^3.10.0" }, "engines": { diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index 369e119697..8e3921df0c 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -3,21 +3,10 @@ import { IDocumentBackgroundOptions } from "../document"; import { DocumentAttributes } from "../document/document-attributes"; import { INumberingOptions } from "../numbering"; -import { HyperlinkType, Paragraph } from "../paragraph"; +import { Paragraph } from "../paragraph"; import { IStylesOptions } from "../styles"; import { Created, Creator, Description, Keywords, LastModifiedBy, Modified, Revision, Subject, Title } from "./components"; -export interface IInternalHyperlinkDefinition { - readonly text: string; - readonly type: HyperlinkType.INTERNAL; -} - -export interface IExternalHyperlinkDefinition { - readonly link: string; - readonly text: string; - readonly type: HyperlinkType.EXTERNAL; -} - export interface IPropertiesOptions { readonly title?: string; readonly subject?: string; @@ -30,9 +19,6 @@ export interface IPropertiesOptions { readonly styles?: IStylesOptions; readonly numbering?: INumberingOptions; readonly footnotes?: Paragraph[]; - readonly hyperlinks?: { - readonly [key: string]: IInternalHyperlinkDefinition | IExternalHyperlinkDefinition; - }; readonly background?: IDocumentBackgroundOptions; readonly features?: { readonly trackRevisions?: boolean; diff --git a/src/file/document/document.ts b/src/file/document/document.ts index fb0f579568..93d4035fa1 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -1,6 +1,6 @@ // http://officeopenxml.com/WPdocument.php import { XmlComponent } from "file/xml-components"; -import { Hyperlink, Paragraph } from "../paragraph"; +import { ConcreteHyperlink, Paragraph } from "../paragraph"; import { Table } from "../table"; import { TableOfContents } from "../table-of-contents"; import { Body } from "./body"; @@ -42,7 +42,7 @@ export class Document extends XmlComponent { this.root.push(this.body); } - public add(item: Paragraph | Table | TableOfContents | Hyperlink): Document { + public add(item: Paragraph | Table | TableOfContents | ConcreteHyperlink): Document { this.body.push(item); return this; } diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 9b93a63db1..9632b80897 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -5,7 +5,7 @@ import { Formatter } from "export/formatter"; import { File } from "./file"; import { Footer, Header } from "./header"; -import { HyperlinkRef, HyperlinkType, Paragraph } from "./paragraph"; +import { Paragraph } from "./paragraph"; import { Table, TableCell, TableRow } from "./table"; import { TableOfContents } from "./table-of-contents"; @@ -164,16 +164,6 @@ describe("File", () => { ], }); }); - - it("should add hyperlink child", () => { - const doc = new File(undefined, undefined, [ - { - children: [new HyperlinkRef("test")], - }, - ]); - - expect(doc.HyperlinkCache).to.deep.equal({}); - }); }); describe("#addSection", () => { @@ -187,16 +177,6 @@ describe("File", () => { expect(spy.called).to.equal(true); }); - it("should add hyperlink child", () => { - const doc = new File(); - - doc.addSection({ - children: [new HyperlinkRef("test")], - }); - - expect(doc.HyperlinkCache).to.deep.equal({}); - }); - it("should call the underlying document's add when adding a Table", () => { const file = new File(); const spy = sinon.spy(file.Document, "add"); @@ -256,29 +236,6 @@ describe("File", () => { }); }); - describe("#HyperlinkCache", () => { - it("should initially have empty hyperlink cache", () => { - const file = new File(); - - expect(file.HyperlinkCache).to.deep.equal({}); - }); - - it("should have hyperlink cache when option is added", () => { - const file = new File({ - hyperlinks: { - myCoolLink: { - link: "http://www.example.com", - text: "Hyperlink", - type: HyperlinkType.EXTERNAL, - }, - }, - }); - - // tslint:disable-next-line: no-unused-expression no-string-literal - expect(file.HyperlinkCache["myCoolLink"]).to.exist; - }); - }); - describe("#createFootnote", () => { it("should create footnote", () => { const wrapper = new File({ diff --git a/src/file/file.ts b/src/file/file.ts index 2a2c3982fa..20d292f91b 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -1,4 +1,3 @@ -import * as shortid from "shortid"; import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; @@ -17,9 +16,8 @@ import { Footer, Header } from "./header"; import { HeaderWrapper, IDocumentHeader } from "./header-wrapper"; import { Media } from "./media"; import { Numbering } from "./numbering"; -import { Hyperlink, HyperlinkRef, HyperlinkType, Paragraph } from "./paragraph"; +import { Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; -import { TargetModeType } from "./relationships/relationship/relationship"; import { Settings } from "./settings"; import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; @@ -41,7 +39,7 @@ export interface ISectionOptions { readonly size?: IPageSizeAttributes; readonly margins?: IPageMarginAttributes; readonly properties?: SectionPropertiesOptions; - readonly children: (Paragraph | Table | TableOfContents | HyperlinkRef)[]; + readonly children: (Paragraph | Table | TableOfContents)[]; } export class File { @@ -61,7 +59,6 @@ export class File { private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; private readonly styles: Styles; - private readonly hyperlinkCache: { readonly [key: string]: Hyperlink } = {}; constructor( options: IPropertiesOptions = { @@ -136,12 +133,6 @@ export class File { this.document.Body.addSection(section.properties ? section.properties : {}); for (const child of section.children) { - if (child instanceof HyperlinkRef) { - const hyperlink = this.hyperlinkCache[child.id]; - this.document.add(hyperlink); - continue; - } - this.document.add(child); } } @@ -152,27 +143,6 @@ export class File { } } - if (options.hyperlinks) { - const cache = {}; - - for (const key in options.hyperlinks) { - if (!options.hyperlinks[key]) { - continue; - } - - const hyperlinkRef = options.hyperlinks[key]; - - const hyperlink = - hyperlinkRef.type === HyperlinkType.EXTERNAL - ? this.createHyperlink(hyperlinkRef.link, hyperlinkRef.text) - : this.createInternalHyperLink(key, hyperlinkRef.text); - - cache[key] = hyperlink; - } - - this.hyperlinkCache = cache; - } - if (options.features) { if (options.features.trackRevisions) { this.settings.addTrackRevisions(); @@ -205,12 +175,6 @@ export class File { }); for (const child of children) { - if (child instanceof HyperlinkRef) { - const hyperlink = this.hyperlinkCache[child.id]; - this.document.add(hyperlink); - continue; - } - this.document.add(child); } } @@ -221,24 +185,6 @@ export class File { } } - private createHyperlink(link: string, text: string = link): Hyperlink { - const hyperlink = new Hyperlink(text, shortid.generate().toLowerCase()); - this.docRelationships.createRelationship( - hyperlink.linkId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - link, - TargetModeType.EXTERNAL, - ); - return hyperlink; - } - - private createInternalHyperLink(anchor: string, text: string = anchor): Hyperlink { - const hyperlink = new Hyperlink(text, shortid.generate().toLowerCase(), anchor); - // NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark - // we don't need to create a new relationship. - return hyperlink; - } - private createHeader(header: Header): HeaderWrapper { const wrapper = new HeaderWrapper(this.media, this.currentRelationshipId++); @@ -371,8 +317,4 @@ export class File { public get Settings(): Settings { return this.settings; } - - public get HyperlinkCache(): { readonly [key: string]: Hyperlink } { - return this.hyperlinkCache; - } } diff --git a/src/file/media/media.ts b/src/file/media/media.ts index 71e7c6f60e..6168fbc362 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -82,7 +82,7 @@ export class Media { return imageData; } - public get Array(): IMediaData[] { + public get Array(): readonly IMediaData[] { const array = new Array(); this.map.forEach((data) => { diff --git a/src/file/paragraph/links/hyperlink.spec.ts b/src/file/paragraph/links/hyperlink.spec.ts index 4b06a933f5..50d6aa76aa 100644 --- a/src/file/paragraph/links/hyperlink.spec.ts +++ b/src/file/paragraph/links/hyperlink.spec.ts @@ -2,14 +2,20 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; -import { Hyperlink } from "./"; -import { HyperlinkRef } from "./hyperlink"; +import { TextRun } from "../run"; +import { ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./hyperlink"; -describe("Hyperlink", () => { - let hyperlink: Hyperlink; +describe("ConcreteHyperlink", () => { + let hyperlink: ConcreteHyperlink; beforeEach(() => { - hyperlink = new Hyperlink("https://example.com", "superid"); + hyperlink = new ConcreteHyperlink( + new TextRun({ + text: "https://example.com", + style: "Hyperlink", + }), + "superid", + ); }); describe("#constructor()", () => { @@ -35,7 +41,14 @@ describe("Hyperlink", () => { describe("with optional anchor parameter", () => { beforeEach(() => { - hyperlink = new Hyperlink("Anchor Text", "superid2", "anchor"); + hyperlink = new ConcreteHyperlink( + new TextRun({ + text: "Anchor Text", + style: "Hyperlink", + }), + "superid2", + "anchor", + ); }); it("should create an internal link with anchor tag", () => { @@ -61,10 +74,53 @@ describe("Hyperlink", () => { }); }); -describe("HyperlinkRef", () => { +describe("ExternalHyperlink", () => { describe("#constructor()", () => { - const hyperlinkRef = new HyperlinkRef("test-id"); + it("should create", () => { + const externalHyperlink = new ExternalHyperlink({ + child: new TextRun("test"), + link: "http://www.google.com", + }); - expect(hyperlinkRef.id).to.equal("test-id"); + expect(externalHyperlink.options.link).to.equal("http://www.google.com"); + }); + }); +}); + +describe("InternalHyperlink", () => { + describe("#constructor()", () => { + it("should create", () => { + const internalHyperlink = new InternalHyperlink({ + child: new TextRun("test"), + anchor: "test-id", + }); + + const tree = new Formatter().format(internalHyperlink); + + expect(tree).to.deep.equal({ + "w:hyperlink": [ + { + _attr: { + "w:anchor": "test-id", + "w:history": 1, + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "test", + ], + }, + ], + }, + ], + }); + }); }); }); diff --git a/src/file/paragraph/links/hyperlink.ts b/src/file/paragraph/links/hyperlink.ts index 302acfd603..a7512e7beb 100644 --- a/src/file/paragraph/links/hyperlink.ts +++ b/src/file/paragraph/links/hyperlink.ts @@ -1,6 +1,9 @@ // http://officeopenxml.com/WPhyperlink.php +import * as shortid from "shortid"; + import { XmlComponent } from "file/xml-components"; -import { TextRun } from "../run"; + +import { ParagraphChild } from "../paragraph"; import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes"; export enum HyperlinkType { @@ -8,15 +11,10 @@ export enum HyperlinkType { EXTERNAL = "EXTERNAL", } -export class HyperlinkRef { - constructor(public readonly id: string) {} -} - -export class Hyperlink extends XmlComponent { +export class ConcreteHyperlink extends XmlComponent { public readonly linkId: string; - private readonly textRun: TextRun; - constructor(text: string, relationshipId: string, anchor?: string) { + constructor(child: ParagraphChild, relationshipId: string, anchor?: string) { super("w:hyperlink"); this.linkId = relationshipId; @@ -29,14 +27,16 @@ export class Hyperlink extends XmlComponent { const attributes = new HyperlinkAttributes(props); this.root.push(attributes); - this.textRun = new TextRun({ - text: text, - style: "Hyperlink", - }); - this.root.push(this.textRun); - } - - public get TextRun(): TextRun { - return this.textRun; + this.root.push(child); } } + +export class InternalHyperlink extends ConcreteHyperlink { + constructor(options: { readonly child: ParagraphChild; readonly anchor: string }) { + super(options.child, shortid.generate().toLowerCase(), options.anchor); + } +} + +export class ExternalHyperlink { + constructor(public readonly options: { readonly child: ParagraphChild; readonly link: string }) {} +} diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index a368684d40..57590f6313 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -8,8 +8,9 @@ import { EMPTY_OBJECT } from "file/xml-components"; import { File } from "../file"; import { ShadingType } from "../table/shading"; import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting"; -import { Bookmark, HyperlinkRef } from "./links"; +import { Bookmark, ExternalHyperlink } from "./links"; import { Paragraph } from "./paragraph"; +import { TextRun } from "./run"; describe("Paragraph", () => { describe("#constructor()", () => { @@ -763,7 +764,7 @@ describe("Paragraph", () => { }); describe("#shading", () => { - it("should set paragraph outline level to the given value", () => { + it("should set shading to the given value", () => { const paragraph = new Paragraph({ shading: { type: ShadingType.REVERSE_DIAGONAL_STRIPE, @@ -793,20 +794,49 @@ describe("Paragraph", () => { }); describe("#prepForXml", () => { - it("should set paragraph outline level to the given value", () => { + it("should set Internal Hyperlink", () => { const paragraph = new Paragraph({ - children: [new HyperlinkRef("myAnchorId")], + children: [ + new ExternalHyperlink({ + child: new TextRun("test"), + link: "http://www.google.com", + }), + ], }); const fileMock = ({ - HyperlinkCache: { - myAnchorId: "test", + DocumentRelationships: { + createRelationship: () => ({}), }, - // tslint:disable-next-line: no-any - } as any) as File; + } as unknown) as File; paragraph.prepForXml(fileMock); const tree = new Formatter().format(paragraph); expect(tree).to.deep.equal({ - "w:p": ["test"], + "w:p": [ + { + "w:hyperlink": [ + { + _attr: { + "r:id": "rIdtest-unique-id", + "w:history": 1, + }, + }, + { + "w:r": [ + { + "w:t": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "test", + ], + }, + ], + }, + ], + }, + ], }); }); }); diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index 60efb0da34..dcaf2c480d 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -1,30 +1,35 @@ // http://officeopenxml.com/WPparagraph.php +import * as shortid from "shortid"; + import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run"; import { IXmlableObject, XmlComponent } from "file/xml-components"; import { File } from "../file"; +import { TargetModeType } from "../relationships/relationship/relationship"; import { DeletedTextRun, InsertedTextRun } from "../track-revision"; import { PageBreak } from "./formatting/page-break"; -import { Bookmark, HyperlinkRef } from "./links"; +import { Bookmark, ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./links"; import { Math } from "./math"; import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties"; import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run"; +export type ParagraphChild = + | TextRun + | PictureRun + | SymbolRun + | Bookmark + | PageBreak + | SequentialIdentifier + | FootnoteReferenceRun + | InternalHyperlink + | ExternalHyperlink + | InsertedTextRun + | DeletedTextRun + | Math; + export interface IParagraphOptions extends IParagraphPropertiesOptions { readonly text?: string; - readonly children?: ( - | TextRun - | PictureRun - | SymbolRun - | Bookmark - | PageBreak - | SequentialIdentifier - | FootnoteReferenceRun - | HyperlinkRef - | InsertedTextRun - | DeletedTextRun - | Math - )[]; + readonly children?: ParagraphChild[]; } export class Paragraph extends XmlComponent { @@ -71,9 +76,16 @@ export class Paragraph extends XmlComponent { public prepForXml(file: File): IXmlableObject | undefined { for (const element of this.root) { - if (element instanceof HyperlinkRef) { + if (element instanceof ExternalHyperlink) { const index = this.root.indexOf(element); - this.root[index] = file.HyperlinkCache[element.id]; + const concreteHyperlink = new ConcreteHyperlink(element.options.child, shortid.generate().toLowerCase()); + file.DocumentRelationships.createRelationship( + concreteHyperlink.linkId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + element.options.link, + TargetModeType.EXTERNAL, + ); + this.root[index] = concreteHyperlink; } }