diff --git a/demo/demo21.js b/demo/demo21.js new file mode 100644 index 0000000000..fd74e61b31 --- /dev/null +++ b/demo/demo21.js @@ -0,0 +1,31 @@ +/** This demo shows how to create bookmarks then link to them with internal hyperlinks */ + +const docx = require("../build"); + +const loremIpsum = "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."; + +const doc = new docx.Document({ + creator: 'Clippy', + title: 'Sample Document', + description: 'A brief example of using docx with bookmarks and internal hyperlinks', +}); + +const anchorId = "anchorID"; + +// First create the bookmark +const bookmark = doc.createBookmark(anchorId, "Lorem Ipsum"); +// That has header styling +doc.createParagraph().addBookmark(bookmark).heading1(); +doc.createParagraph("\n"); + +doc.createParagraph(loremIpsum); +doc.createParagraph().pageBreak(); + +// Now the link back up to the bookmark +const hyperlink = doc.createInternalHyperLink(anchorId, `Click me!`); +doc.createParagraph().addHyperLink(hyperlink); + +var exporter = new docx.LocalPacker(doc); +exporter.pack("My Document"); + +console.log("Document created successfully at project root!"); diff --git a/src/file/file.ts b/src/file/file.ts index ae9cf9deac..866ddd1480 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -10,7 +10,7 @@ import { FootNotes } from "./footnotes"; import { HeaderWrapper } from "./header-wrapper"; import { Media } from "./media"; import { Numbering } from "./numbering"; -import { Hyperlink, Paragraph, PictureRun } from "./paragraph"; +import { Bookmark, Hyperlink, Paragraph, PictureRun } from "./paragraph"; import { Relationships } from "./relationships"; import { Styles } from "./styles"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; @@ -157,6 +157,20 @@ export class File { return hyperlink; } + public createInternalHyperLink(anchor: string, text?: string): Hyperlink { + text = text === undefined ? anchor : text; + const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount, anchor); + // NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark + // we don't need to create a new relationship. + return hyperlink; + } + + public createBookmark(name: string, text?: string): Bookmark { + text = text === undefined ? name : text; + const bookmark = new Bookmark(name, text, this.docRelationships.RelationshipCount); + return bookmark; + } + public addSection(sectionPropertiesOptions: SectionPropertiesOptions): void { this.document.Body.addSection(sectionPropertiesOptions); } diff --git a/src/file/paragraph/links/bookmark-attributes.ts b/src/file/paragraph/links/bookmark-attributes.ts new file mode 100644 index 0000000000..670c462306 --- /dev/null +++ b/src/file/paragraph/links/bookmark-attributes.ts @@ -0,0 +1,23 @@ +import { XmlAttributeComponent } from "file/xml-components"; + +export interface IBookmarkStartAttributesProperties { + id: string; + name: string; +} + +export class BookmarkStartAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "w:id", + name: "w:name", + }; +} + +export interface IBookmarkEndAttributesProperties { + id: string; +} + +export class BookmarkEndAttributes extends XmlAttributeComponent { + protected xmlKeys = { + id: "w:id", + }; +} diff --git a/src/file/paragraph/links/bookmark.spec.ts b/src/file/paragraph/links/bookmark.spec.ts new file mode 100644 index 0000000000..08d0aec91c --- /dev/null +++ b/src/file/paragraph/links/bookmark.spec.ts @@ -0,0 +1,42 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { Bookmark } from "./"; + +describe("Bookmark", () => { + let bookmark: Bookmark; + + beforeEach(() => { + bookmark = new Bookmark("anchor", "Internal Link", 0); + }); + + it("should create a bookmark with three root elements", () => { + const newJson = Utility.jsonify(bookmark); + assert.equal(newJson.rootKey, undefined); + assert.equal(newJson.start.rootKey, "w:bookmarkStart"); + assert.equal(newJson.text.rootKey, "w:r"); + assert.equal(newJson.end.rootKey, "w:bookmarkEnd"); + }); + + it("should create a bookmark with the correct attributes on the bookmark start element", () => { + const newJson = Utility.jsonify(bookmark); + const attributes = { + name: "anchor", + id: "1", + }; + assert.equal(JSON.stringify(newJson.start.root[0].root), JSON.stringify(attributes)); + }); + + it("should create a bookmark with the correct attributes on the text element", () => { + const newJson = Utility.jsonify(bookmark); + assert.equal(JSON.stringify(newJson.text.root[1].root[1]), JSON.stringify("Internal Link")); + }); + + it("should create a bookmark with the correct attributes on the bookmark end element", () => { + const newJson = Utility.jsonify(bookmark); + const attributes = { + id: "1", + }; + assert.equal(JSON.stringify(newJson.end.root[0].root), JSON.stringify(attributes)); + }); +}); diff --git a/src/file/paragraph/links/bookmark.ts b/src/file/paragraph/links/bookmark.ts new file mode 100644 index 0000000000..de4fabdaa2 --- /dev/null +++ b/src/file/paragraph/links/bookmark.ts @@ -0,0 +1,54 @@ +// http://officeopenxml.com/WPbookmark.php + +import { XmlComponent } from "file/xml-components"; +import { TextRun } from "../run"; +import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes"; + +export class Bookmark { + public linkId: number; + + public readonly start: BookmarkStart; + + public readonly text: TextRun; + + public readonly end: BookmarkEnd; + + constructor(name: string, text: string, relationshipsCount: number) { + this.linkId = relationshipsCount + 1; + + this.start = new BookmarkStart(name, this.linkId); + this.text = new TextRun(text); + this.end = new BookmarkEnd(this.linkId); + } +} + +export class BookmarkStart extends XmlComponent { + public linkId: number; + + constructor(name: string, relationshipsCount: number) { + super("w:bookmarkStart"); + + this.linkId = relationshipsCount; + const id = `${this.linkId}`; + const attributes = new BookmarkStartAttributes({ + name, + id, + }); + this.root.push(attributes); + } +} + +export class BookmarkEnd extends XmlComponent { + public linkId: number; + + constructor(relationshipsCount: number) { + super("w:bookmarkEnd"); + + this.linkId = relationshipsCount; + const id = `${this.linkId}`; + const attributes = new BookmarkEndAttributes({ + id, + }); + this.root.push(attributes); + } +} diff --git a/src/file/paragraph/links/hyperlink-attributes.ts b/src/file/paragraph/links/hyperlink-attributes.ts index d51f5a8b65..6d68e265b4 100644 --- a/src/file/paragraph/links/hyperlink-attributes.ts +++ b/src/file/paragraph/links/hyperlink-attributes.ts @@ -2,6 +2,7 @@ import { XmlAttributeComponent } from "file/xml-components"; export interface IHyperlinkAttributesProperties { id?: string; + anchor?: string; history: number; } @@ -9,5 +10,6 @@ export class HyperlinkAttributes extends XmlAttributeComponent { it("should create a hyperlink with right attributes", () => { const newJson = Utility.jsonify(hyperlink); const attributes = { - id: "rId1", history: 1, + id: "rId1", }; assert.equal(JSON.stringify(newJson.root[0].root), JSON.stringify(attributes)); }); @@ -36,5 +36,20 @@ describe("Hyperlink", () => { }; expect(tree["w:hyperlink"][1]).to.deep.equal(runJson); }); + + describe("with optional anchor parameter", () => { + beforeEach(() => { + hyperlink = new Hyperlink("Anchor Text", 0, "anchor"); + }); + + it("should create an internal link with anchor tag", () => { + const newJson = Utility.jsonify(hyperlink); + const attributes = { + history: 1, + anchor: "anchor", + }; + assert.equal(JSON.stringify(newJson.root[0].root), JSON.stringify(attributes)); + }); + }); }); }); diff --git a/src/file/paragraph/links/hyperlink.ts b/src/file/paragraph/links/hyperlink.ts index 53f72e72bd..b6fe1b221b 100644 --- a/src/file/paragraph/links/hyperlink.ts +++ b/src/file/paragraph/links/hyperlink.ts @@ -2,19 +2,27 @@ import { XmlComponent } from "file/xml-components"; import { TextRun } from "../run"; -import { HyperlinkAttributes } from "./hyperlink-attributes"; +import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes"; export class Hyperlink extends XmlComponent { public linkId: number; - constructor(text: string, relationshipsCount: number) { + constructor(text: string, relationshipsCount: number, anchor?: string) { super("w:hyperlink"); this.linkId = relationshipsCount + 1; - const attributes = new HyperlinkAttributes({ - id: `rId${this.linkId}`, + + const props: IHyperlinkAttributesProperties = { history: 1, - }); + }; + + if (anchor) { + props.anchor = anchor; + } else { + props.id = `rId${this.linkId}`; + } + + const attributes = new HyperlinkAttributes(props); this.root.push(attributes); this.root.push(new TextRun(text).style("Hyperlink")); } diff --git a/src/file/paragraph/links/index.ts b/src/file/paragraph/links/index.ts index 619d1bee30..09084aa8c7 100644 --- a/src/file/paragraph/links/index.ts +++ b/src/file/paragraph/links/index.ts @@ -1 +1,2 @@ export * from "./hyperlink"; +export * from "./bookmark"; diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index 187814762e..f46905207e 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -13,7 +13,7 @@ import { ISpacingProperties, Spacing } from "./formatting/spacing"; import { Style } from "./formatting/style"; import { CenterTabStop, LeftTabStop, MaxRightTabStop, RightTabStop } from "./formatting/tab-stop"; import { NumberProperties } from "./formatting/unordered-list"; -import { Hyperlink } from "./links"; +import { Bookmark, Hyperlink } from "./links"; import { ParagraphProperties } from "./properties"; import { PictureRun, Run, TextRun } from "./run"; @@ -39,6 +39,14 @@ export class Paragraph extends XmlComponent { return this; } + public addBookmark(bookmark: Bookmark): Paragraph { + // Bookmarks by spec have three components, a start, text, and end + this.root.push(bookmark.start); + this.root.push(bookmark.text); + this.root.push(bookmark.end); + return this; + } + public createTextRun(text: string): TextRun { const run = new TextRun(text); this.addRun(run);