Merge pull request #478 from dolanmiu/feat/right-indent

Declarative bookmarks
This commit is contained in:
Dolan
2019-12-27 01:43:48 +00:00
committed by GitHub
24 changed files with 353 additions and 107 deletions

View File

@ -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 { Document, HeadingLevel, Packer, PageBreak, Paragraph } from "../build";
import { Bookmark, Document, HeadingLevel, HyperlinkRef, HyperlinkType, Packer, PageBreak, Paragraph } 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,19 +10,19 @@ 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,
},
},
});
const anchorId = "anchorID";
// First create the bookmark
const bookmark = doc.createBookmark(anchorId, "Lorem Ipsum");
const hyperlink = doc.createInternalHyperLink(anchorId, `Click me!`);
doc.addSection({
children: [
new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [bookmark],
children: [new Bookmark("myAnchorId", "Lorem Ipsum")],
}),
new Paragraph("\n"),
new Paragraph(LOREM_IPSUM),
@ -30,7 +30,7 @@ doc.addSection({
children: [new PageBreak()],
}),
new Paragraph({
children: [hyperlink],
children: [new HyperlinkRef("myAnchorId")],
}),
],
});

View File

@ -1,15 +1,22 @@
// 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, Packer, Paragraph } from "../build";
import { Document, HyperlinkRef, HyperlinkType, Packer, Paragraph } from "../build";
const doc = new Document();
const link = doc.createHyperlink("http://www.example.com", "Hyperlink");
const doc = new Document({
hyperlinks: {
myCoolLink: {
link: "http://www.example.com",
text: "Hyperlink",
type: HyperlinkType.EXTERNAL,
},
},
});
doc.addSection({
children: [
new Paragraph({
children: [link],
children: [new HyperlinkRef("myCoolLink")],
}),
],
});

6
package-lock.json generated
View File

@ -355,6 +355,12 @@
"@types/node": "*"
}
},
"@types/shortid": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=",
"dev": true
},
"@types/sinon": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",

View File

@ -65,6 +65,7 @@
"@types/chai": "^3.4.35",
"@types/mocha": "^2.2.39",
"@types/request-promise": "^4.1.42",
"@types/shortid": "0.0.29",
"@types/sinon": "^4.3.1",
"@types/webpack": "^4.4.24",
"awesome-typescript-loader": "^3.4.1",

View File

@ -1,8 +1,9 @@
import { BaseXmlComponent, IXmlableObject } from "file/xml-components";
import { File } from "../file";
export class Formatter {
public format(input: BaseXmlComponent): IXmlableObject {
const output = input.prepForXml();
public format(input: BaseXmlComponent, file?: File): IXmlableObject {
const output = input.prepForXml(file);
if (output) {
return output;

View File

@ -71,7 +71,7 @@ export class Compiler {
file.verifyUpdateFields();
const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1;
const documentXmlData = xml(this.formatter.format(file.Document), prettify);
const documentXmlData = xml(this.formatter.format(file.Document, file), prettify);
const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media);
return {
@ -85,7 +85,7 @@ export class Compiler {
);
});
return xml(this.formatter.format(file.DocumentRelationships), prettify);
return xml(this.formatter.format(file.DocumentRelationships, file), prettify);
})(),
path: "word/_rels/document.xml.rels",
},
@ -99,11 +99,11 @@ export class Compiler {
path: "word/document.xml",
},
Styles: {
data: xml(this.formatter.format(file.Styles), prettify),
data: xml(this.formatter.format(file.Styles, file), prettify),
path: "word/styles.xml",
},
Properties: {
data: xml(this.formatter.format(file.CoreProperties), {
data: xml(this.formatter.format(file.CoreProperties, file), {
declaration: {
standalone: "yes",
encoding: "UTF-8",
@ -112,15 +112,15 @@ export class Compiler {
path: "docProps/core.xml",
},
Numbering: {
data: xml(this.formatter.format(file.Numbering), prettify),
data: xml(this.formatter.format(file.Numbering, file), prettify),
path: "word/numbering.xml",
},
FileRelationships: {
data: xml(this.formatter.format(file.FileRelationships), prettify),
data: xml(this.formatter.format(file.FileRelationships, file), prettify),
path: "_rels/.rels",
},
HeaderRelationships: file.Headers.map((headerWrapper, index) => {
const xmlData = xml(this.formatter.format(headerWrapper.Header), prettify);
const xmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => {
@ -132,12 +132,12 @@ export class Compiler {
});
return {
data: xml(this.formatter.format(headerWrapper.Relationships), prettify),
data: xml(this.formatter.format(headerWrapper.Relationships, file), prettify),
path: `word/_rels/header${index + 1}.xml.rels`,
};
}),
FooterRelationships: file.Footers.map((footerWrapper, index) => {
const xmlData = xml(this.formatter.format(footerWrapper.Footer), prettify);
const xmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => {
@ -149,12 +149,12 @@ export class Compiler {
});
return {
data: xml(this.formatter.format(footerWrapper.Relationships), prettify),
data: xml(this.formatter.format(footerWrapper.Relationships, file), prettify),
path: `word/_rels/footer${index + 1}.xml.rels`,
};
}),
Headers: file.Headers.map((headerWrapper, index) => {
const tempXmlData = xml(this.formatter.format(headerWrapper.Header), prettify);
const tempXmlData = xml(this.formatter.format(headerWrapper.Header, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -165,7 +165,7 @@ export class Compiler {
};
}),
Footers: file.Footers.map((footerWrapper, index) => {
const tempXmlData = xml(this.formatter.format(footerWrapper.Footer), prettify);
const tempXmlData = xml(this.formatter.format(footerWrapper.Footer, file), prettify);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
// TODO: 0 needs to be changed when headers get relationships of their own
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, 0);
@ -176,19 +176,19 @@ export class Compiler {
};
}),
ContentTypes: {
data: xml(this.formatter.format(file.ContentTypes), prettify),
data: xml(this.formatter.format(file.ContentTypes, file), prettify),
path: "[Content_Types].xml",
},
AppProperties: {
data: xml(this.formatter.format(file.AppProperties), prettify),
data: xml(this.formatter.format(file.AppProperties, file), prettify),
path: "docProps/app.xml",
},
FootNotes: {
data: xml(this.formatter.format(file.FootNotes), prettify),
data: xml(this.formatter.format(file.FootNotes, file), prettify),
path: "word/footnotes.xml",
},
Settings: {
data: xml(this.formatter.format(file.Settings), prettify),
data: xml(this.formatter.format(file.Settings, file), prettify),
path: "word/settings.xml",
},
};

View File

@ -2,10 +2,21 @@ import { XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { INumberingOptions } from "../numbering";
import { Paragraph } from "../paragraph";
import { HyperlinkType, 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;
@ -18,6 +29,9 @@ export interface IPropertiesOptions {
readonly styles?: IStylesOptions;
readonly numbering?: INumberingOptions;
readonly footnotes?: Paragraph[];
readonly hyperlinks?: {
readonly [key: string]: IInternalHyperlinkDefinition | IExternalHyperlinkDefinition;
};
}
export class CoreProperties extends XmlComponent {

View File

@ -1,5 +1,6 @@
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { Paragraph, ParagraphProperties, TableOfContents } from "../..";
import { File } from "../../../file";
import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties";
export class Body extends XmlComponent {
@ -24,13 +25,13 @@ export class Body extends XmlComponent {
this.sections.push(new SectionProperties(options));
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
if (this.sections.length === 1) {
this.root.splice(0, 1);
this.root.push(this.sections.pop() as SectionProperties);
}
return super.prepForXml();
return super.prepForXml(file);
}
public push(component: XmlComponent): void {

View File

@ -1,6 +1,6 @@
// http://officeopenxml.com/WPdocument.php
import { XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { Hyperlink, Paragraph } from "../paragraph";
import { Table } from "../table";
import { TableOfContents } from "../table-of-contents";
import { Body } from "./body";
@ -36,7 +36,7 @@ export class Document extends XmlComponent {
this.root.push(this.body);
}
public add(item: Paragraph | Table | TableOfContents): Document {
public add(item: Paragraph | Table | TableOfContents | Hyperlink): Document {
this.body.push(item);
return this;
}

View File

@ -5,7 +5,7 @@ import { Formatter } from "export/formatter";
import { File } from "./file";
import { Footer, Header } from "./header";
import { Paragraph } from "./paragraph";
import { HyperlinkRef, Paragraph } from "./paragraph";
import { Table, TableCell, TableRow } from "./table";
import { TableOfContents } from "./table-of-contents";
@ -89,6 +89,91 @@ describe("File", () => {
expect(tree["w:body"][0]["w:sectPr"][8]["w:footerReference"]._attr["w:type"]).to.equal("first");
expect(tree["w:body"][0]["w:sectPr"][9]["w:footerReference"]._attr["w:type"]).to.equal("even");
});
it("should add child", () => {
const doc = new File(undefined, undefined, [
{
children: [new Paragraph("test")],
},
]);
const tree = new Formatter().format(doc.Document.Body);
expect(tree).to.deep.equal({
"w:body": [
{
"w:p": [
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
],
},
{
"w:sectPr": [
{
"w:pgSz": {
_attr: {
"w:h": 16838,
"w:orient": "portrait",
"w:w": 11906,
},
},
},
{
"w:pgMar": {
_attr: {
"w:bottom": 1440,
"w:footer": 708,
"w:gutter": 0,
"w:header": 708,
"w:left": 1440,
"w:mirrorMargins": false,
"w:right": 1440,
"w:top": 1440,
},
},
},
{
"w:cols": {
_attr: {
"w:num": 1,
"w:space": 708,
},
},
},
{
"w:docGrid": {
_attr: {
"w:linePitch": 360,
},
},
},
],
},
],
});
});
it("should add hyperlink child", () => {
const doc = new File(undefined, undefined, [
{
children: [new HyperlinkRef("test")],
},
]);
expect(doc.HyperlinkCache).to.deep.equal({});
});
});
describe("#addSection", () => {
@ -102,6 +187,16 @@ 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");
@ -148,6 +243,14 @@ describe("File", () => {
});
});
describe("#HyperlinkCache", () => {
it("should initially have empty hyperlink cache", () => {
const file = new File();
expect(file.HyperlinkCache).to.deep.equal({});
});
});
describe("#createFootnote", () => {
it("should create footnote", () => {
const wrapper = new File({

View File

@ -17,7 +17,7 @@ import { Footer, Header } from "./header";
import { HeaderWrapper, IDocumentHeader } from "./header-wrapper";
import { Media } from "./media";
import { Numbering } from "./numbering";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph";
import { Hyperlink, HyperlinkRef, HyperlinkType, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { TargetModeType } from "./relationships/relationship/relationship";
import { Settings } from "./settings";
@ -41,7 +41,7 @@ export interface ISectionOptions {
readonly size?: IPageSizeAttributes;
readonly margins?: IPageMarginAttributes;
readonly properties?: SectionPropertiesOptions;
readonly children: Array<Paragraph | Table | TableOfContents>;
readonly children: Array<Paragraph | Table | TableOfContents | HyperlinkRef>;
}
export class File {
@ -61,13 +61,13 @@ 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 = {
creator: "Un-named",
revision: "1",
lastModifiedBy: "Un-named",
footnotes: [],
},
fileProperties: IFileProperties = {},
sections: ISectionOptions[] = [],
@ -134,6 +134,12 @@ 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);
}
}
@ -143,30 +149,27 @@ export class File {
this.footNotes.createFootNote(paragraph);
}
}
if (options.hyperlinks) {
const cache = {};
for (const key in options.hyperlinks) {
if (!options.hyperlinks[key]) {
continue;
}
public createHyperlink(link: string, text?: string): Hyperlink {
const newText = text === undefined ? link : text;
const hyperlink = new Hyperlink(newText, shortid.generate().toLowerCase());
this.docRelationships.createRelationship(
hyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
link,
TargetModeType.EXTERNAL,
);
return hyperlink;
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;
}
public createInternalHyperLink(anchor: string, text?: string): Hyperlink {
const newText = text === undefined ? anchor : text;
const hyperlink = new Hyperlink(newText, 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;
this.hyperlinkCache = cache;
}
public createBookmark(name: string, text: string = name): Bookmark {
return new Bookmark(name, text, this.docRelationships.RelationshipCount);
}
public addSection({
@ -194,6 +197,12 @@ 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);
}
}
@ -204,6 +213,24 @@ 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++);
@ -336,4 +363,8 @@ export class File {
public get Settings(): Settings {
return this.settings;
}
public get HyperlinkCache(): { readonly [key: string]: Hyperlink } {
return this.hyperlinkCache;
}
}

View File

@ -11,11 +11,8 @@ export enum HeadingLevel {
}
export class Style extends XmlComponent {
public readonly styleId: string;
constructor(styleId: string) {
super("w:pStyle");
this.styleId = styleId;
this.root.push(
new Attributes({
val: styleId,

View File

@ -1,4 +1,4 @@
import { assert } from "chai";
import { assert, expect } from "chai";
import { Utility } from "tests/utility";
@ -8,7 +8,7 @@ describe("Bookmark", () => {
let bookmark: Bookmark;
beforeEach(() => {
bookmark = new Bookmark("anchor", "Internal Link", 0);
bookmark = new Bookmark("anchor", "Internal Link");
});
it("should create a bookmark with three root elements", () => {
@ -21,11 +21,8 @@ describe("Bookmark", () => {
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));
assert.equal(newJson.start.root[0].root.name, "anchor");
});
it("should create a bookmark with the correct attributes on the text element", () => {
@ -35,9 +32,6 @@ describe("Bookmark", () => {
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));
expect(newJson.end.root[0].root.id).to.be.a("string");
});
});

View File

@ -1,49 +1,41 @@
// http://officeopenxml.com/WPbookmark.php
import { XmlComponent } from "file/xml-components";
import * as shortid from "shortid";
import { TextRun } from "../run";
import { BookmarkEndAttributes, BookmarkStartAttributes } from "./bookmark-attributes";
export class Bookmark {
public readonly 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;
constructor(name: string, text: string) {
const linkId = shortid.generate().toLowerCase();
this.start = new BookmarkStart(name, this.linkId);
this.start = new BookmarkStart(name, linkId);
this.text = new TextRun(text);
this.end = new BookmarkEnd(this.linkId);
this.end = new BookmarkEnd(linkId);
}
}
export class BookmarkStart extends XmlComponent {
public readonly linkId: number;
constructor(name: string, relationshipsCount: number) {
constructor(name: string, linkId: string) {
super("w:bookmarkStart");
this.linkId = relationshipsCount;
const id = `${this.linkId}`;
const attributes = new BookmarkStartAttributes({
name,
id,
id: linkId,
});
this.root.push(attributes);
}
}
export class BookmarkEnd extends XmlComponent {
public readonly linkId: number;
constructor(relationshipsCount: number) {
constructor(linkId: string) {
super("w:bookmarkEnd");
this.linkId = relationshipsCount;
const id = `${this.linkId}`;
const attributes = new BookmarkEndAttributes({
id,
id: linkId,
});
this.root.push(attributes);
}

View File

@ -3,6 +3,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Hyperlink } from "./";
import { HyperlinkRef } from "./hyperlink";
describe("Hyperlink", () => {
let hyperlink: Hyperlink;
@ -59,3 +60,11 @@ describe("Hyperlink", () => {
});
});
});
describe("HyperlinkRef", () => {
describe("#constructor()", () => {
const hyperlinkRef = new HyperlinkRef("test-id");
expect(hyperlinkRef.id).to.equal("test-id");
});
});

View File

@ -3,6 +3,15 @@ import { XmlComponent } from "file/xml-components";
import { TextRun } from "../run";
import { HyperlinkAttributes, IHyperlinkAttributesProperties } from "./hyperlink-attributes";
export enum HyperlinkType {
INTERNAL = "INTERNAL",
EXTERNAL = "EXTERNAL",
}
export class HyperlinkRef {
constructor(public readonly id: string) {}
}
export class Hyperlink extends XmlComponent {
public readonly linkId: string;
private readonly textRun: TextRun;

View File

@ -1,9 +1,12 @@
import { assert, expect } from "chai";
import * as shortid from "shortid";
import { stub } from "sinon";
import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components";
import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting";
import { Bookmark } from "./links";
import { Paragraph } from "./paragraph";
describe("Paragraph", () => {
@ -638,6 +641,49 @@ describe("Paragraph", () => {
});
});
it("it should add bookmark", () => {
stub(shortid, "generate").callsFake(() => {
return "test-unique-id";
});
const paragraph = new Paragraph({
children: [new Bookmark("test-id", "test")],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
"w:p": [
{
"w:bookmarkStart": {
_attr: {
"w:id": "test-unique-id",
"w:name": "test-id",
},
},
},
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
{
"w:bookmarkEnd": {
_attr: {
"w:id": "test-unique-id",
},
},
},
],
});
});
describe("#style", () => {
it("should set the paragraph style to the given styleId", () => {
const paragraph = new Paragraph({

View File

@ -1,7 +1,8 @@
// http://officeopenxml.com/WPparagraph.php
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { XmlComponent } from "file/xml-components";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { File } from "../file";
import { Alignment, AlignmentType } from "./formatting/alignment";
import { Bidirectional } from "./formatting/bidirectional";
import { IBorderOptions, ThematicBreak } from "./formatting/border";
@ -12,7 +13,7 @@ import { ContextualSpacing, ISpacingProperties, Spacing } from "./formatting/spa
import { HeadingLevel, Style } from "./formatting/style";
import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop";
import { NumberProperties } from "./formatting/unordered-list";
import { Bookmark, Hyperlink, OutlineLevel } from "./links";
import { Bookmark, HyperlinkRef, OutlineLevel } from "./links";
import { ParagraphProperties } from "./properties";
import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run";
@ -45,7 +46,7 @@ export interface IParagraphOptions {
readonly custom?: boolean;
};
readonly children?: Array<
TextRun | PictureRun | Hyperlink | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | FootnoteReferenceRun
TextRun | PictureRun | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | FootnoteReferenceRun | HyperlinkRef
>;
}
@ -159,6 +160,17 @@ export class Paragraph extends XmlComponent {
}
}
public prepForXml(file: File): IXmlableObject | undefined {
for (const element of this.root) {
if (element instanceof HyperlinkRef) {
const index = this.root.indexOf(element);
this.root[index] = file.HyperlinkCache[element.id];
}
}
return super.prepForXml();
}
public addRunToFront(run: Run): Paragraph {
this.root.splice(1, 0, run);
return this;

View File

@ -7,10 +7,6 @@ export class PictureRun extends Run {
constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) {
super({});
if (imageData === undefined) {
throw new Error("imageData cannot be undefined");
}
const drawing = new Drawing(imageData, drawingOptions);
this.root.push(drawing);

View File

@ -132,6 +132,30 @@ describe("Run", () => {
});
});
describe("#subScript()", () => {
it("it should add subScript to the properties", () => {
const run = new Run({
subScript: true,
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "subscript" } } }] }],
});
});
});
describe("#superScript()", () => {
it("it should add superScript to the properties", () => {
const run = new Run({
superScript: true,
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [{ "w:rPr": [{ "w:vertAlign": { _attr: { "w:val": "superscript" } } }] }],
});
});
});
describe("#highlight()", () => {
it("it should add highlight to the properties", () => {
const run = new Run({

View File

@ -3,6 +3,7 @@ import { Paragraph } from "file/paragraph";
import { BorderStyle } from "file/styles";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { File } from "../../file";
import { ITableShadingAttributesProperties } from "../shading";
import { Table } from "../table";
import { ITableCellMarginOptions } from "./cell-margin/table-cell-margins";
@ -110,11 +111,11 @@ export class TableCell extends XmlComponent {
}
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
// Cells must end with a paragraph
if (!(this.root[this.root.length - 1] instanceof Paragraph)) {
this.root.push(new Paragraph({}));
}
return super.prepForXml();
return super.prepForXml(file);
}
}

View File

@ -1,3 +1,4 @@
import { File } from "../file";
import { IXmlableObject } from "./xmlable-object";
export abstract class BaseXmlComponent {
@ -9,7 +10,7 @@ export abstract class BaseXmlComponent {
this.rootKey = rootKey;
}
public abstract prepForXml(): IXmlableObject | undefined;
public abstract prepForXml(file?: File): IXmlableObject | undefined;
public get IsDeleted(): boolean {
return this.deleted;

View File

@ -4,3 +4,4 @@ export * from "./default-attributes";
export * from "./imported-xml-component";
export * from "./xmlable-object";
export * from "./initializable-xml-component";
export * from "./base";

View File

@ -1,19 +1,19 @@
import { File } from "../file";
import { BaseXmlComponent } from "./base";
import { IXmlableObject } from "./xmlable-object";
export { BaseXmlComponent };
export const EMPTY_OBJECT = Object.seal({});
export abstract class XmlComponent extends BaseXmlComponent {
// tslint:disable-next-line:readonly-keyword
protected root: Array<BaseXmlComponent | string>;
// tslint:disable-next-line:readonly-keyword no-any
protected root: Array<BaseXmlComponent | string | any>;
constructor(rootKey: string) {
super(rootKey);
this.root = new Array<BaseXmlComponent | string>();
}
public prepForXml(): IXmlableObject | undefined {
public prepForXml(file?: File): IXmlableObject | undefined {
const children = this.root
.filter((c) => {
if (c instanceof BaseXmlComponent) {
@ -23,7 +23,7 @@ export abstract class XmlComponent extends BaseXmlComponent {
})
.map((comp) => {
if (comp instanceof BaseXmlComponent) {
return comp.prepForXml();
return comp.prepForXml(file);
}
return comp;
})