diff --git a/demo/assets/custom-styles.xml b/demo/assets/custom-styles.xml
new file mode 100644
index 0000000000..76159f2985
--- /dev/null
+++ b/demo/assets/custom-styles.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/demo/demo13.js b/demo/demo13.js
new file mode 100644
index 0000000000..f141547d7a
--- /dev/null
+++ b/demo/demo13.js
@@ -0,0 +1,26 @@
+// This example shows 3 styles
+const fs = require('fs');
+const docx = require('../build');
+
+const styles = fs.readFileSync('./demo/assets/custom-styles.xml', 'utf-8');
+const doc = new docx.Document({
+ title: 'Title',
+ externalStyles: styles
+});
+
+doc.createParagraph('Cool Heading Text').heading1();
+
+let paragraph = new docx.Paragraph('This is a custom named style from the template "MyFancyStyle"');
+paragraph.style('MyFancyStyle');
+doc.addParagraph(paragraph);
+
+doc.createParagraph('Some normal text')
+
+doc.createParagraph('MyFancyStyle again').style('MyFancyStyle');
+paragraph.style('MyFancyStyle');
+doc.addParagraph(paragraph);
+
+var exporter = new docx.LocalPacker(doc);
+exporter.pack('My Document');
+
+console.log('Document created successfully at project root!');
diff --git a/package.json b/package.json
index 3cf27302b0..a4baaeda30 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"@types/image-size": "0.0.29",
"@types/request-promise": "^4.1.41",
"archiver": "^2.1.1",
+ "fast-xml-parser": "^3.3.6",
"image-size": "^0.6.2",
"request": "^2.83.0",
"request-promise": "^4.2.2",
diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts
index cd2395c27b..aa9d3aec51 100644
--- a/src/file/core-properties/properties.ts
+++ b/src/file/core-properties/properties.ts
@@ -10,6 +10,7 @@ export interface IPropertiesOptions {
description?: string;
lastModifiedBy?: string;
revision?: string;
+ externalStyles?: string;
}
export class CoreProperties extends XmlComponent {
diff --git a/src/file/file.ts b/src/file/file.ts
index 39a9e3d210..2ab3f159fc 100644
--- a/src/file/file.ts
+++ b/src/file/file.ts
@@ -1,3 +1,4 @@
+import { IMediaData } from "file/media";
import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties";
@@ -10,6 +11,7 @@ import { Numbering } from "./numbering";
import { Hyperlink, Paragraph, PictureRun } from "./paragraph";
import { Relationships } from "./relationships";
import { Styles } from "./styles";
+import { ExternalStylesFactory } from "./styles/external-styles-factory";
import { DefaultStylesFactory } from "./styles/factory";
import { Table } from "./table";
@@ -28,8 +30,6 @@ export class File {
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
this.document = new Document(sectionPropertiesOptions);
- const stylesFactory = new DefaultStylesFactory();
- this.styles = stylesFactory.newInstance();
if (!options) {
options = {
@@ -39,6 +39,14 @@ export class File {
};
}
+ if (options.externalStyles) {
+ const stylesFactory = new ExternalStylesFactory();
+ this.styles = stylesFactory.newInstance(options.externalStyles);
+ } else {
+ const stylesFactory = new DefaultStylesFactory();
+ this.styles = stylesFactory.newInstance();
+ }
+
this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering();
this.docRelationships = new Relationships();
@@ -111,6 +119,16 @@ export class File {
return this.document.createDrawing(mediaData);
}
+ public createImageData(imageName: string, data: Buffer, width?: number, height?: number): IMediaData {
+ const mediaData = this.media.addMediaWithData(imageName, data, this.docRelationships.RelationshipCount, width, height);
+ this.docRelationships.createRelationship(
+ mediaData.referenceId,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
+ `media/${mediaData.fileName}`,
+ );
+ return mediaData;
+ }
+
public createHyperlink(link: string, text?: string): Hyperlink {
text = text === undefined ? link : text;
const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount);
diff --git a/src/file/index.ts b/src/file/index.ts
index aae389e50a..42a9ca3b76 100644
--- a/src/file/index.ts
+++ b/src/file/index.ts
@@ -5,3 +5,4 @@ export * from "./numbering";
export * from "./media";
export * from "./drawing";
export * from "./styles";
+export * from "./xml-components";
diff --git a/src/file/media/data.ts b/src/file/media/data.ts
index 00836ed962..cbc7d8c5bf 100644
--- a/src/file/media/data.ts
+++ b/src/file/media/data.ts
@@ -13,8 +13,8 @@ export interface IMediaDataDimensions {
export interface IMediaData {
referenceId: number;
- stream: fs.ReadStream;
- path: string;
+ stream: fs.ReadStream | Buffer;
+ path?: string;
fileName: string;
dimensions: IMediaDataDimensions;
}
diff --git a/src/file/media/media.ts b/src/file/media/media.ts
index 5ac8905bc7..ee4ca9ae15 100644
--- a/src/file/media/media.ts
+++ b/src/file/media/media.ts
@@ -24,10 +24,34 @@ export class Media {
public addMedia(filePath: string, relationshipsCount: number): IMediaData {
const key = path.basename(filePath);
const dimensions = sizeOf(filePath);
+ return this.createMedia(key, relationshipsCount, dimensions, fs.createReadStream(filePath), filePath);
+ }
+ public addMediaWithData(fileName: string, data: Buffer, relationshipsCount: number, width?: number, height?: number): IMediaData {
+ const key = fileName;
+ let dimensions;
+ if (width && height) {
+ dimensions = {
+ width: width,
+ height: height,
+ };
+ } else {
+ dimensions = sizeOf(data);
+ }
+
+ return this.createMedia(key, relationshipsCount, dimensions, data);
+ }
+
+ private createMedia(
+ key: string,
+ relationshipsCount: number,
+ dimensions: { width: number; height: number },
+ data: fs.ReadStream | Buffer,
+ filePath?: string,
+ ): IMediaData {
const imageData = {
referenceId: this.map.size + relationshipsCount + 1,
- stream: fs.createReadStream(filePath),
+ stream: data,
path: filePath,
fileName: key,
dimensions: {
diff --git a/src/file/numbering/index.ts b/src/file/numbering/index.ts
index 33832de65b..a861d336ee 100644
--- a/src/file/numbering/index.ts
+++ b/src/file/numbering/index.ts
@@ -1 +1,2 @@
export * from "./numbering";
+export * from "./abstract-numbering";
diff --git a/src/file/numbering/numbering.ts b/src/file/numbering/numbering.ts
index 427fcec05a..0a886e5598 100644
--- a/src/file/numbering/numbering.ts
+++ b/src/file/numbering/numbering.ts
@@ -1,12 +1,15 @@
-import { XmlComponent } from "file/xml-components";
+import { Indent } from "file/paragraph";
+import { IXmlableObject, XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
-import { Indent } from "../paragraph/formatting";
import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num";
export class Numbering extends XmlComponent {
private nextId: number;
+ private abstractNumbering: XmlComponent[] = [];
+ private concreteNumbering: XmlComponent[] = [];
+
constructor() {
super("w:numbering");
this.root.push(
@@ -58,13 +61,19 @@ export class Numbering extends XmlComponent {
public createAbstractNumbering(): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++);
- this.root.push(num);
+ this.abstractNumbering.push(num);
return num;
}
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id);
- this.root.push(num);
+ this.concreteNumbering.push(num);
return num;
}
+
+ public prepForXml(): IXmlableObject {
+ this.abstractNumbering.forEach((x) => this.root.push(x));
+ this.concreteNumbering.forEach((x) => this.root.push(x));
+ return super.prepForXml();
+ }
}
diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts
index d8659470b0..2f75422077 100644
--- a/src/file/paragraph/paragraph.ts
+++ b/src/file/paragraph/paragraph.ts
@@ -147,6 +147,11 @@ export class Paragraph extends XmlComponent {
return this;
}
+ public setCustomNumbering(numberId: number, indentLevel: number): Paragraph {
+ this.properties.push(new NumberProperties(numberId, indentLevel));
+ return this;
+ }
+
public style(styleId: string): Paragraph {
this.properties.push(new Style(styleId));
return this;
diff --git a/src/file/styles/external-styles-factory.spec.ts b/src/file/styles/external-styles-factory.spec.ts
new file mode 100644
index 0000000000..861a6f05d8
--- /dev/null
+++ b/src/file/styles/external-styles-factory.spec.ts
@@ -0,0 +1,159 @@
+import { expect } from "chai";
+
+import { ExternalStylesFactory } from "./external-styles-factory";
+
+describe("External styles factory", () => {
+ let externalStyles;
+
+ beforeEach(() => {
+ externalStyles = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ });
+
+ describe("#parse", () => {
+ it("should parse w:styles attributes", () => {
+ const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
+
+ expect(importedStyle.rootKey).to.equal("w:styles");
+ expect(importedStyle.root[0]._attr).to.eql({
+ "xmlns:mc": "first",
+ "xmlns:r": "second",
+ });
+ });
+
+ it("should parse other child elements of w:styles", () => {
+ const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
+
+ expect(importedStyle.root.length).to.equal(5);
+ expect(importedStyle.root[1]).to.eql({
+ deleted: false,
+ root: [],
+ rootKey: "w:docDefaults",
+ });
+ expect(importedStyle.root[2]).to.eql({
+ _attr: {
+ "w:defLockedState": "1",
+ "w:defUIPriority": "99",
+ },
+ deleted: false,
+ root: [],
+ rootKey: "w:latentStyles",
+ });
+ });
+
+ it("should parse styles elements", () => {
+ const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
+
+ expect(importedStyle.root.length).to.equal(5);
+ expect(importedStyle.root[3]).to.eql({
+ _attr: {
+ "w:default": "1",
+ "w:styleId": "Normal",
+ "w:type": "paragraph",
+ },
+ deleted: false,
+ root: [
+ {
+ _attr: {
+ "w:val": "Normal",
+ },
+ deleted: false,
+ root: [],
+ rootKey: "w:name",
+ },
+ {
+ deleted: false,
+ root: [],
+ rootKey: "w:qFormat",
+ },
+ ],
+ rootKey: "w:style",
+ });
+
+ expect(importedStyle.root[4]).to.eql({
+ _attr: {
+ "w:styleId": "Heading1",
+ "w:type": "paragraph",
+ },
+ deleted: false,
+ root: [
+ {
+ _attr: {
+ "w:val": "heading 1",
+ },
+ deleted: false,
+ root: [],
+ rootKey: "w:name",
+ },
+ {
+ _attr: {
+ "w:val": "Normal",
+ },
+ deleted: false,
+ root: [],
+ rootKey: "w:basedOn",
+ },
+ {
+ deleted: false,
+ root: [
+ {
+ deleted: false,
+ root: [],
+ rootKey: "w:keepNext",
+ },
+ {
+ deleted: false,
+ root: [],
+ rootKey: "w:keepLines",
+ },
+ {
+ deleted: false,
+ root: [
+ {
+ _attr: {
+ "w:color": "auto",
+ "w:space": "1",
+ "w:sz": "4",
+ "w:val": "single",
+ },
+ deleted: false,
+ root: [],
+ rootKey: "w:bottom",
+ },
+ ],
+ rootKey: "w:pBdr",
+ },
+ ],
+ rootKey: "w:pPr",
+ },
+ ],
+ rootKey: "w:style",
+ });
+ });
+ });
+});
diff --git a/src/file/styles/external-styles-factory.ts b/src/file/styles/external-styles-factory.ts
new file mode 100644
index 0000000000..f1bc9c68c6
--- /dev/null
+++ b/src/file/styles/external-styles-factory.ts
@@ -0,0 +1,64 @@
+import * as fastXmlParser from "fast-xml-parser";
+
+import { Styles } from "./";
+import { ImportedRootElementAttributes, ImportedXmlComponent } from "./../../file/xml-components";
+
+const parseOptions = {
+ ignoreAttributes: false,
+ attributeNamePrefix: "",
+ attrNodeName: "_attr",
+};
+
+export class ExternalStylesFactory {
+ /**
+ * Creates new Style based on the given styles.
+ * Parses the styles and convert them to XmlComponent.
+ * Example content from styles.xml:
+ *
+ *
+ *
+ *
+ *
+ * .....
+ *
+ *
+ *
+ *
+ * .....
+ *
+ *
+ * Or any other element will be parsed to
+ *
+ *
+ * @param externalStyles context from styles.xml
+ */
+ public newInstance(externalStyles: string): Styles {
+ const xmlStyles = fastXmlParser.parse(externalStyles, parseOptions)["w:styles"];
+ // create styles with attributes from the parsed xml
+ const importedStyle = new Styles(new ImportedRootElementAttributes(xmlStyles._attr));
+
+ // convert other elements (not styles definitions, but default styles and so on ...)
+ Object.keys(xmlStyles)
+ .filter((element) => element !== "_attr" && element !== "w:style")
+ .forEach((element) => {
+ importedStyle.push(new ImportedXmlComponent(element, xmlStyles[element]._attr));
+ });
+
+ // convert the styles one by one
+ xmlStyles["w:style"].map((style) => this.convertElement("w:style", style)).forEach(importedStyle.push.bind(importedStyle));
+
+ return importedStyle;
+ }
+
+ // tslint:disable-next-line:no-any
+ public convertElement(elementName: string, element: any): ImportedXmlComponent {
+ const xmlElement = new ImportedXmlComponent(elementName, element._attr);
+ if (typeof element === "object") {
+ Object.keys(element)
+ .filter((key) => key !== "_attr")
+ .map((item) => this.convertElement(item, element[item]))
+ .forEach(xmlElement.push.bind(xmlElement));
+ }
+ return xmlElement;
+ }
+}
diff --git a/src/file/styles/factory.ts b/src/file/styles/factory.ts
index a45f37d6b4..f00038fac0 100644
--- a/src/file/styles/factory.ts
+++ b/src/file/styles/factory.ts
@@ -1,7 +1,7 @@
+import { DocumentAttributes } from "../document/document-attributes";
import { Color, Italics, Size } from "../paragraph/run/formatting";
-
import { Styles } from "./";
-// import { DocumentDefaults } from "./defaults";
+
import {
Heading1Style,
Heading2Style,
@@ -16,7 +16,15 @@ import {
export class DefaultStylesFactory {
public newInstance(): Styles {
- const styles = new Styles();
+ const documentAttributes = new DocumentAttributes({
+ mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
+ r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
+ w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
+ w14: "http://schemas.microsoft.com/office/word/2010/wordml",
+ w15: "http://schemas.microsoft.com/office/word/2012/wordml",
+ Ignorable: "w14 w15",
+ });
+ const styles = new Styles(documentAttributes);
styles.createDocumentDefaults();
const titleStyle = new TitleStyle();
diff --git a/src/file/styles/index.ts b/src/file/styles/index.ts
index 5d8e89ee10..30f81c0012 100644
--- a/src/file/styles/index.ts
+++ b/src/file/styles/index.ts
@@ -1,21 +1,13 @@
-import { XmlComponent } from "file/xml-components";
-import { DocumentAttributes } from "../document/document-attributes";
+import { BaseXmlComponent, XmlComponent } from "file/xml-components";
import { DocumentDefaults } from "./defaults";
import { ParagraphStyle } from "./style";
export class Styles extends XmlComponent {
- constructor() {
+ constructor(initialStyles?: BaseXmlComponent) {
super("w:styles");
- this.root.push(
- new DocumentAttributes({
- mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
- r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
- w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
- w14: "http://schemas.microsoft.com/office/word/2010/wordml",
- w15: "http://schemas.microsoft.com/office/word/2012/wordml",
- Ignorable: "w14 w15",
- }),
- );
+ if (initialStyles) {
+ this.root.push(initialStyles);
+ }
}
public push(style: XmlComponent): Styles {
diff --git a/src/file/table/index.ts b/src/file/table/index.ts
index 0e948df9e8..dfba175857 100644
--- a/src/file/table/index.ts
+++ b/src/file/table/index.ts
@@ -1 +1,2 @@
export * from "./table";
+export * from "./table-cell";
diff --git a/src/file/table/table-cell.spec.ts b/src/file/table/table-cell.spec.ts
new file mode 100644
index 0000000000..01c81848bb
--- /dev/null
+++ b/src/file/table/table-cell.spec.ts
@@ -0,0 +1,181 @@
+import { expect } from "chai";
+
+import { TableCellBorders, BorderStyle, TableCellWidth, WidthType } from "./table-cell";
+import { Formatter } from "../../export/formatter";
+
+describe("TableCellBorders", () => {
+ describe("#prepForXml", () => {
+ it("should not add empty borders element if there are no borders defined", () => {
+ const tb = new TableCellBorders();
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal("");
+ });
+ });
+
+ describe("#addingBorders", () => {
+ it("should add top border", () => {
+ const tb = new TableCellBorders();
+ tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF");
+
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal({
+ "w:tcBorders": [
+ {
+ "w:top": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 1,
+ "w:val": "dotted",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should add start(left) border", () => {
+ const tb = new TableCellBorders();
+ tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF");
+
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal({
+ "w:tcBorders": [
+ {
+ "w:start": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 2,
+ "w:val": "single",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should add bottom border", () => {
+ const tb = new TableCellBorders();
+ tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF");
+
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal({
+ "w:tcBorders": [
+ {
+ "w:bottom": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 1,
+ "w:val": "double",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should add end(right) border", () => {
+ const tb = new TableCellBorders();
+ tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF");
+
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal({
+ "w:tcBorders": [
+ {
+ "w:end": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 3,
+ "w:val": "thick",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("should add multiple borders", () => {
+ const tb = new TableCellBorders();
+ tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF");
+ tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF");
+ tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF");
+ tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF");
+
+ const tree = new Formatter().format(tb);
+ expect(tree).to.deep.equal({
+ "w:tcBorders": [
+ {
+ "w:top": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 1,
+ "w:val": "dotted",
+ },
+ },
+ ],
+ },
+ {
+ "w:end": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 3,
+ "w:val": "thick",
+ },
+ },
+ ],
+ },
+ {
+ "w:bottom": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 1,
+ "w:val": "double",
+ },
+ },
+ ],
+ },
+ {
+ "w:start": [
+ {
+ _attr: {
+ "w:color": "FF00FF",
+ "w:sz": 2,
+ "w:val": "single",
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+ });
+});
+
+describe("TableCellWidth", () => {
+ describe("#constructor", () => {
+ it("should create object", () => {
+ const tcWidth = new TableCellWidth(100, WidthType.DXA);
+ const tree = new Formatter().format(tcWidth);
+ expect(tree).to.deep.equal({
+ "w:tcW": [
+ {
+ _attr: {
+ "w:type": "dxa",
+ "w:w": 100,
+ },
+ },
+ ],
+ });
+ });
+ });
+});
diff --git a/src/file/table/table-cell.ts b/src/file/table/table-cell.ts
new file mode 100644
index 0000000000..5e64cc04b6
--- /dev/null
+++ b/src/file/table/table-cell.ts
@@ -0,0 +1,207 @@
+import { IXmlableObject, XmlAttributeComponent, XmlComponent } from "file/xml-components";
+
+export enum BorderStyle {
+ SINGLE = "single",
+ DASH_DOT_STROKED = "dashDotStroked",
+ DASHED = "dashed",
+ DASH_SMALL_GAP = "dashSmallGap",
+ DOT_DASH = "dotDash",
+ DOT_DOT_DASH = "dotDotDash",
+ DOTTED = "dotted",
+ DOUBLE = "double",
+ DOUBLE_WAVE = "doubleWave",
+ INSET = "inset",
+ NIL = "nil",
+ NONE = "none",
+ OUTSET = "outset",
+ THICK = "thick",
+ THICK_THIN_LARGE_GAP = "thickThinLargeGap",
+ THICK_THIN_MEDIUM_GAP = "thickThinMediumGap",
+ THICK_THIN_SMALL_GAP = "thickThinSmallGap",
+ THIN_THICK_LARGE_GAP = "thinThickLargeGap",
+ THIN_THICK_MEDIUM_GAP = "thinThickMediumGap",
+ THIN_THICK_SMALL_GAP = "thinThickSmallGap",
+ THIN_THICK_THIN_LARGE_GAP = "thinThickThinLargeGap",
+ THIN_THICK_THIN_MEDIUM_GAP = "thinThickThinMediumGap",
+ THIN_THICK_THIN_SMALL_GAP = "thinThickThinSmallGap",
+ THREE_D_EMBOSS = "threeDEmboss",
+ THREE_D_ENGRAVE = "threeDEngrave",
+ TRIPLE = "triple",
+ WAVE = "wave",
+}
+
+interface ICellBorder {
+ style: BorderStyle;
+ size: number;
+ color: string;
+}
+
+class CellBorderAttributes extends XmlAttributeComponent {
+ protected xmlKeys = { style: "w:val", size: "w:sz", color: "w:color" };
+}
+
+class BaseTableCellBorder extends XmlComponent {
+ public setProperties(style: BorderStyle, size: number, color: string): BaseTableCellBorder {
+ const attrs = new CellBorderAttributes({
+ style: style,
+ size: size,
+ color: color,
+ });
+ this.root.push(attrs);
+
+ return this;
+ }
+}
+
+export class TableCellBorders extends XmlComponent {
+ constructor() {
+ super("w:tcBorders");
+ }
+
+ public prepForXml(): IXmlableObject {
+ return this.root.length > 0 ? super.prepForXml() : "";
+ }
+
+ public addTopBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
+ const top = new BaseTableCellBorder("w:top");
+ top.setProperties(style, size, color);
+ this.root.push(top);
+
+ return this;
+ }
+
+ public addStartBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
+ const start = new BaseTableCellBorder("w:start");
+ start.setProperties(style, size, color);
+ this.root.push(start);
+
+ return this;
+ }
+
+ public addBottomBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
+ const bottom = new BaseTableCellBorder("w:bottom");
+ bottom.setProperties(style, size, color);
+ this.root.push(bottom);
+
+ return this;
+ }
+
+ public addEndBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
+ const end = new BaseTableCellBorder("w:end");
+ end.setProperties(style, size, color);
+ this.root.push(end);
+
+ return this;
+ }
+}
+
+/**
+ * Attributes fot the GridSpan element.
+ */
+class GridSpanAttributes extends XmlAttributeComponent<{ val: number }> {
+ protected xmlKeys = { val: "w:val" };
+}
+
+/**
+ * GridSpan element. Should be used in a table cell. Pass the number of columns that this cell need to span.
+ */
+export class GridSpan extends XmlComponent {
+ constructor(value: number) {
+ super("w:gridSpan");
+
+ this.root.push(
+ new GridSpanAttributes({
+ val: value,
+ }),
+ );
+ }
+}
+
+/**
+ * Vertical merge types.
+ */
+export enum VMergeType {
+ /**
+ * Cell that is merged with upper one.
+ */
+ CONTINUE = "continue",
+ /**
+ * Cell that is starting the vertical merge.
+ */
+ RESTART = "restart",
+}
+
+class VMergeAttributes extends XmlAttributeComponent<{ val: VMergeType }> {
+ protected xmlKeys = { val: "w:val" };
+}
+
+/**
+ * Vertical merge element. Should be used in a table cell.
+ */
+export class VMerge extends XmlComponent {
+ constructor(value: VMergeType) {
+ super("w:vMerge");
+
+ this.root.push(
+ new VMergeAttributes({
+ val: value,
+ }),
+ );
+ }
+}
+
+export enum VerticalAlign {
+ BOTTOM = "bottom",
+ CENTER = "center",
+ TOP = "top",
+}
+
+class VAlignAttributes extends XmlAttributeComponent<{ val: VerticalAlign }> {
+ protected xmlKeys = { val: "w:val" };
+}
+
+/**
+ * Vertical align element.
+ */
+export class VAlign extends XmlComponent {
+ constructor(value: VerticalAlign) {
+ super("w:vAlign");
+
+ this.root.push(
+ new VAlignAttributes({
+ val: value,
+ }),
+ );
+ }
+}
+
+export enum WidthType {
+ /** Auto. */
+ AUTO = "auto",
+ /** Value is in twentieths of a point */
+ DXA = "dxa",
+ /** No (empty) value. */
+ NIL = "nil",
+ /** Value is in percentage. */
+ PERCENTAGE = "pct",
+}
+
+class TableCellWidthAttributes extends XmlAttributeComponent<{ type: WidthType; width: string | number }> {
+ protected xmlKeys = { width: "w:w", type: "w:type" };
+}
+
+/**
+ * Table cell width element.
+ */
+export class TableCellWidth extends XmlComponent {
+ constructor(value: string | number, type: WidthType) {
+ super("w:tcW");
+
+ this.root.push(
+ new TableCellWidthAttributes({
+ width: value,
+ type: type,
+ }),
+ );
+ }
+}
diff --git a/src/file/table/table.ts b/src/file/table/table.ts
index 8fdec3d37c..2d19086baa 100644
--- a/src/file/table/table.ts
+++ b/src/file/table/table.ts
@@ -1,3 +1,4 @@
+import { GridSpan, TableCellBorders, TableCellWidth, VAlign, VerticalAlign, VMerge, VMergeType, WidthType } from "file/table/table-cell";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { TableGrid } from "./grid";
@@ -8,27 +9,32 @@ export class Table extends XmlComponent {
private readonly rows: TableRow[];
private readonly grid: TableGrid;
- constructor(rows: number, cols: number) {
+ constructor(rows: number, cols: number, colSizes?: number[]) {
super("w:tbl");
this.properties = new TableProperties();
this.root.push(this.properties);
this.properties.setBorder();
- const gridCols: number[] = [];
- for (let i = 0; i < cols; i++) {
- /*
- 0-width columns don't get rendered correctly, so we need
- to give them some value. A reasonable default would be
- ~6in / numCols, but if we do that it becomes very hard
- to resize the table using setWidth, unless the layout
- algorithm is set to 'fixed'. Instead, the approach here
- means even in 'auto' layout, setting a width on the
- table will make it look reasonable, as the layout
- algorithm will expand columns to fit its content
- */
- gridCols.push(1);
+ if (colSizes && colSizes.length > 0) {
+ this.grid = new TableGrid(colSizes);
+ } else {
+ const gridCols: number[] = [];
+ for (let i = 0; i < cols; i++) {
+ /*
+ 0-width columns don't get rendered correctly, so we need
+ to give them some value. A reasonable default would be
+ ~6in / numCols, but if we do that it becomes very hard
+ to resize the table using setWidth, unless the layout
+ algorithm is set to 'fixed'. Instead, the approach here
+ means even in 'auto' layout, setting a width on the
+ table will make it look reasonable, as the layout
+ algorithm will expand columns to fit its content
+ */
+ gridCols.push(1);
+ }
+ this.grid = new TableGrid(gridCols);
}
- this.grid = new TableGrid(gridCols);
+
this.root.push(this.grid);
this.rows = [];
@@ -112,10 +118,45 @@ export class TableCell extends XmlComponent {
this.addContent(para);
return para;
}
+
+ get cellProperties(): TableCellProperties {
+ return this.properties;
+ }
}
export class TableCellProperties extends XmlComponent {
+ private cellBorder: TableCellBorders;
constructor() {
super("w:tcPr");
+ this.cellBorder = new TableCellBorders();
+ this.root.push(this.cellBorder);
+ }
+
+ get borders(): TableCellBorders {
+ return this.cellBorder;
+ }
+
+ public addGridSpan(cellSpan: number): TableCellProperties {
+ this.root.push(new GridSpan(cellSpan));
+
+ return this;
+ }
+
+ public addVerticalMerge(type: VMergeType): TableCellProperties {
+ this.root.push(new VMerge(type));
+
+ return this;
+ }
+
+ public setVerticalAlign(vAlignType: VerticalAlign): TableCellProperties {
+ this.root.push(new VAlign(vAlignType));
+
+ return this;
+ }
+
+ public setWidth(width: string | number, type: WidthType): TableCellProperties {
+ this.root.push(new TableCellWidth(width, type));
+
+ return this;
}
}
diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts
index d634a418a9..0dd5333765 100644
--- a/src/file/xml-components/base.ts
+++ b/src/file/xml-components/base.ts
@@ -2,10 +2,15 @@ import { IXmlableObject } from "./xmlable-object";
export abstract class BaseXmlComponent {
protected rootKey: string;
+ protected deleted: boolean = false;
constructor(rootKey: string) {
this.rootKey = rootKey;
}
public abstract prepForXml(): IXmlableObject;
+
+ public get isDeleted(): boolean {
+ return this.deleted;
+ }
}
diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts
new file mode 100644
index 0000000000..d7b638ba9f
--- /dev/null
+++ b/src/file/xml-components/imported-xml-component.spec.ts
@@ -0,0 +1,34 @@
+import { expect } from "chai";
+import { ImportedXmlComponent } from "./";
+
+describe("ImportedXmlComponent", () => {
+ let importedXmlComponent: ImportedXmlComponent;
+
+ beforeEach(() => {
+ const attributes = {
+ someAttr: "1",
+ otherAttr: "2",
+ };
+ importedXmlComponent = new ImportedXmlComponent("w:test", attributes);
+ importedXmlComponent.push(new ImportedXmlComponent("w:child"));
+ });
+
+ describe("#prepForXml()", () => {
+ it("should transform for xml", () => {
+ const converted = importedXmlComponent.prepForXml();
+ expect(converted).to.eql({
+ "w:test": [
+ {
+ _attr: {
+ someAttr: "1",
+ otherAttr: "2",
+ },
+ },
+ {
+ "w:child": [],
+ },
+ ],
+ });
+ });
+ });
+});
diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts
new file mode 100644
index 0000000000..853c84b462
--- /dev/null
+++ b/src/file/xml-components/imported-xml-component.ts
@@ -0,0 +1,74 @@
+// tslint:disable:no-any
+// tslint:disable:variable-name
+import { IXmlableObject, XmlComponent } from "./";
+
+/**
+ * Represents imported xml component from xml file.
+ */
+export class ImportedXmlComponent extends XmlComponent {
+ private _attr: any;
+
+ constructor(rootKey: string, attr?: any) {
+ super(rootKey);
+
+ if (attr) {
+ this._attr = attr;
+ }
+ }
+
+ /**
+ * Transforms the object so it can be converted to xml. Example:
+ *
+ *
+ *
+ *
+ * {
+ * 'w:someKey': [
+ * {
+ * _attr: {
+ * someAttr: "1",
+ * otherAttr: "11"
+ * }
+ * },
+ * {
+ * 'w:child': [
+ * {
+ * _attr: {
+ * childAttr: "2"
+ * }
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ */
+ public prepForXml(): IXmlableObject {
+ const result = super.prepForXml();
+ if (!!this._attr) {
+ if (!Array.isArray(result[this.rootKey])) {
+ result[this.rootKey] = [result[this.rootKey]];
+ }
+ result[this.rootKey].unshift({ _attr: this._attr });
+ }
+ return result;
+ }
+
+ public push(xmlComponent: XmlComponent): void {
+ this.root.push(xmlComponent);
+ }
+}
+
+/**
+ * Used for the attributes of root element that is being imported.
+ */
+export class ImportedRootElementAttributes extends XmlComponent {
+ constructor(private _attr: any) {
+ super("");
+ }
+
+ public prepForXml(): IXmlableObject {
+ return {
+ _attr: this._attr,
+ };
+ }
+}
diff --git a/src/file/xml-components/index.ts b/src/file/xml-components/index.ts
index 5d20da53d2..917933869e 100644
--- a/src/file/xml-components/index.ts
+++ b/src/file/xml-components/index.ts
@@ -1,4 +1,5 @@
export * from "./xml-component";
export * from "./attributes";
export * from "./default-attributes";
+export * from "./imported-xml-component";
export * from "./xmlable-object";
diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts
index 17d3d4d1cb..8b4f983388 100644
--- a/src/file/xml-components/xml-component.spec.ts
+++ b/src/file/xml-components/xml-component.spec.ts
@@ -18,4 +18,15 @@ describe("XmlComponent", () => {
assert.equal(newJson.rootKey, "w:test");
});
});
+
+ describe("#prepForXml()", () => {
+ it("should skip deleted elements", () => {
+ const child = new TestComponent("w:test1");
+ child.delete();
+ xmlComponent.addChildElement(child);
+
+ const xml = xmlComponent.prepForXml();
+ assert.equal(xml["w:test"].length, 0);
+ });
+ });
});
diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts
index d3705478cb..fa7709a043 100644
--- a/src/file/xml-components/xml-component.ts
+++ b/src/file/xml-components/xml-component.ts
@@ -12,6 +12,12 @@ export abstract class XmlComponent extends BaseXmlComponent {
public prepForXml(): IXmlableObject {
const children = this.root
+ .filter((c) => {
+ if (c instanceof BaseXmlComponent) {
+ return !c.isDeleted;
+ }
+ return true;
+ })
.map((comp) => {
if (comp instanceof BaseXmlComponent) {
return comp.prepForXml();
@@ -23,4 +29,15 @@ export abstract class XmlComponent extends BaseXmlComponent {
[this.rootKey]: children,
};
}
+
+ // TODO: Unused method
+ public addChildElement(child: XmlComponent | string): XmlComponent {
+ this.root.push(child);
+
+ return this;
+ }
+
+ public delete(): void {
+ this.deleted = true;
+ }
}