From 3f257bf5a4784d9d5a355724e9ac9c59a93f01d0 Mon Sep 17 00:00:00 2001 From: Thomas Gerbet Date: Tue, 3 Aug 2021 15:58:26 +0200 Subject: [PATCH] Add the pageref element This instruction is useful if you want to get the number of the page containing a specific bookmark. --- demo/21-bookmarks.ts | 8 +- docs/usage/hyperlinks.md | 11 +++ src/file/paragraph/links/index.ts | 1 + .../links/pageref-field-instruction.spec.ts | 24 ++++++ .../links/pageref-field-instruction.ts | 25 ++++++ .../paragraph/links/pageref-properties.ts | 16 ++++ src/file/paragraph/links/pageref.spec.ts | 76 +++++++++++++++++++ src/file/paragraph/links/pageref.ts | 13 ++++ 8 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/file/paragraph/links/pageref-field-instruction.spec.ts create mode 100644 src/file/paragraph/links/pageref-field-instruction.ts create mode 100644 src/file/paragraph/links/pageref-properties.ts create mode 100644 src/file/paragraph/links/pageref.spec.ts create mode 100644 src/file/paragraph/links/pageref.ts diff --git a/demo/21-bookmarks.ts b/demo/21-bookmarks.ts index 5455ee8875..6c673f1520 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, Footer, HeadingLevel, InternalHyperlink, Packer, PageBreak, Paragraph, TextRun } from "../build"; +import { Bookmark, Document, Footer, HeadingLevel, InternalHyperlink, Packer, PageBreak, Paragraph, TextRun, PageRef } 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."; @@ -55,6 +55,12 @@ const doc = new Document({ }), ], }), + new Paragraph({ + children: [ + new TextRun("The bookmark can be seen on page "), + new PageRef("myAnchorId"), + ] + }), ], }, ], diff --git a/docs/usage/hyperlinks.md b/docs/usage/hyperlinks.md index 82bfb366f9..f8d53416ee 100644 --- a/docs/usage/hyperlinks.md +++ b/docs/usage/hyperlinks.md @@ -24,6 +24,17 @@ const link = this.doc.createInternalHyperLink('anchorForChapter1', 'This is a li paragraph.addHyperLink(link); ``` +You can also get the page number of the bookmark by creating a page reference to it: + +```ts +new Paragraph({ + children: [ + new TextRun("The chapter1 can be seen on page "), + new PageRef("anchorForChapter1"), + ] +}) +``` + ## External To create an external hyperlink you just need to specify the url and the text of the link, then add it to a paragraph with `doc.createHyperlink(url, text)`: diff --git a/src/file/paragraph/links/index.ts b/src/file/paragraph/links/index.ts index 82715b62e8..f3b83733bd 100644 --- a/src/file/paragraph/links/index.ts +++ b/src/file/paragraph/links/index.ts @@ -1,3 +1,4 @@ export * from "./hyperlink"; export * from "./bookmark"; export * from "./outline-level"; +export * from "./pageref"; diff --git a/src/file/paragraph/links/pageref-field-instruction.spec.ts b/src/file/paragraph/links/pageref-field-instruction.spec.ts new file mode 100644 index 0000000000..de1a28863c --- /dev/null +++ b/src/file/paragraph/links/pageref-field-instruction.spec.ts @@ -0,0 +1,24 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; +import { PageRefFieldInstruction } from "./pageref-field-instruction"; + +describe("PageRef field instruction", () => { + describe("#constructor()", () => { + it("should construct a pageref field instruction without options", () => { + const instruction = new PageRefFieldInstruction("anchor"); + const tree = new Formatter().format(instruction); + + expect(tree).to.be.deep.equal({ + "w:instrText": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "PAGEREF anchor", + ], + }); + }); + }); +}); diff --git a/src/file/paragraph/links/pageref-field-instruction.ts b/src/file/paragraph/links/pageref-field-instruction.ts new file mode 100644 index 0000000000..198f5427c9 --- /dev/null +++ b/src/file/paragraph/links/pageref-field-instruction.ts @@ -0,0 +1,25 @@ +import { SpaceType } from "file/space-type"; +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IPageRefOptions } from "./pageref-properties"; + +class TextAttributes extends XmlAttributeComponent<{ readonly space: SpaceType }> { + protected readonly xmlKeys = { space: "xml:space" }; +} + +export class PageRefFieldInstruction extends XmlComponent { + constructor(bookmarkId: string, options: IPageRefOptions = {}) { + super("w:instrText"); + this.root.push(new TextAttributes({ space: SpaceType.PRESERVE })); + + let instruction = `PAGEREF ${bookmarkId}`; + + if (options.hyperlink) { + instruction = `${instruction} \\h`; + } + if (options.useRelativePosition) { + instruction = `${instruction} \\p`; + } + + this.root.push(instruction); + } +} diff --git a/src/file/paragraph/links/pageref-properties.ts b/src/file/paragraph/links/pageref-properties.ts new file mode 100644 index 0000000000..963c5331d6 --- /dev/null +++ b/src/file/paragraph/links/pageref-properties.ts @@ -0,0 +1,16 @@ +// Options according to https://www.ecma-international.org/publications/standards/Ecma-376.htm (at Part 1, Page 1234) + +export interface IPageRefOptions { + /** + * \h option - Creates a hyperlink to the bookmarked paragraph. + */ + readonly hyperlink?: boolean; + /** + * \p option - Causes the field to display its position relative to the source + * bookmark. If the PAGEREF field is on the same page as the + * bookmark, it omits "on page #" and returns "above" or "below" + * only. If the PAGEREF field is not on the same page as the + * bookmark, the string "on page #" is used. + */ + readonly useRelativePosition?: boolean; +} diff --git a/src/file/paragraph/links/pageref.spec.ts b/src/file/paragraph/links/pageref.spec.ts new file mode 100644 index 0000000000..459429373b --- /dev/null +++ b/src/file/paragraph/links/pageref.spec.ts @@ -0,0 +1,76 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; +import { PageRef } from "./pageref"; + +describe("PageRef", () => { + describe("#constructor()", () => { + it("should construct a pageref without options", () => { + const pageref = new PageRef("some_bookmark"); + const tree = new Formatter().format(pageref); + expect(tree).to.be.deep.equal({ + "w:r": [ + { + "w:fldChar": { + _attr: { + "w:dirty": true, + "w:fldCharType": "begin", + }, + }, + }, + { + "w:instrText": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "PAGEREF some_bookmark", + ], + }, + { + "w:fldChar": { + _attr: { + "w:fldCharType": "end", + }, + }, + }, + ], + }); + }); + + it("should construct a pageref with all the options", () => { + const pageref = new PageRef("some_bookmark", { hyperlink: true, useRelativePosition: true }); + const tree = new Formatter().format(pageref); + expect(tree).to.be.deep.equal({ + "w:r": [ + { + "w:fldChar": { + _attr: { + "w:dirty": true, + "w:fldCharType": "begin", + }, + }, + }, + { + "w:instrText": [ + { + _attr: { + "xml:space": "preserve", + }, + }, + "PAGEREF some_bookmark \\h \\p", + ], + }, + { + "w:fldChar": { + _attr: { + "w:fldCharType": "end", + }, + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/paragraph/links/pageref.ts b/src/file/paragraph/links/pageref.ts new file mode 100644 index 0000000000..7cb6acfeec --- /dev/null +++ b/src/file/paragraph/links/pageref.ts @@ -0,0 +1,13 @@ +// See https://www.ecma-international.org/publications/standards/Ecma-376.htm (at Part 1, Page 1234) +import { Begin, End } from "file/paragraph/run/field"; +import { Run } from "../run"; +import { PageRefFieldInstruction } from "./pageref-field-instruction"; +import type { IPageRefOptions } from "./pageref-properties"; + +export class PageRef extends Run { + constructor(bookmarkId: string, options: IPageRefOptions = {}) { + super({ + children: [new Begin(true), new PageRefFieldInstruction(bookmarkId, options), new End()], + }); + } +}