#773 Better hyperlink and bookmark syntax
Allow for images to be hyperlinked as well
This commit is contained in:
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export class Media {
|
||||
return imageData;
|
||||
}
|
||||
|
||||
public get Array(): IMediaData[] {
|
||||
public get Array(): readonly IMediaData[] {
|
||||
const array = new Array<IMediaData>();
|
||||
|
||||
this.map.forEach((data) => {
|
||||
|
@ -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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 }) {}
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user