diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000000..2488a642fd
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,24 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "typescript",
+ "tsconfig": "tsconfig.json",
+ "option": "watch",
+ "problemMatcher": [
+ "$tsc-watch"
+ ]
+ },
+ {
+ "type": "npm",
+ "script": "ts-node",
+ "problemMatcher": [],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/demo/demo16.ts b/demo/demo16.ts
index 4b92dc0d7e..6c75218e53 100644
--- a/demo/demo16.ts
+++ b/demo/demo16.ts
@@ -15,8 +15,12 @@ const footer = doc.createFooter();
footer.createParagraph("Footer on another page");
doc.addSection({
- headerId: header.Header.ReferenceId,
- footerId: footer.Footer.ReferenceId,
+ headers: {
+ default: header,
+ },
+ footers: {
+ default: footer,
+ },
pageNumberStart: 1,
pageNumberFormatType: PageNumberFormat.DECIMAL,
});
@@ -24,8 +28,12 @@ doc.addSection({
doc.createParagraph("hello");
doc.addSection({
- headerId: header.Header.ReferenceId,
- footerId: footer.Footer.ReferenceId,
+ headers: {
+ default: header,
+ },
+ footers: {
+ default: footer,
+ },
pageNumberStart: 1,
pageNumberFormatType: PageNumberFormat.DECIMAL,
orientation: PageOrientation.LANDSCAPE,
diff --git a/demo/demo23.ts b/demo/demo23.ts
index 47ee26a6f9..2e230e0cb7 100644
--- a/demo/demo23.ts
+++ b/demo/demo23.ts
@@ -8,11 +8,11 @@ const doc = new Document();
const paragraph = new Paragraph("Hello World");
doc.addParagraph(paragraph);
-const image = Media.addImage(doc, "./demo/images/image1.jpeg");
-const image2 = Media.addImage(doc, "./demo/images/dog.png");
-const image3 = Media.addImage(doc, "./demo/images/cat.jpg");
-const image4 = Media.addImage(doc, "./demo/images/parrots.bmp");
-const image5 = Media.addImage(doc, "./demo/images/pizza.gif");
+const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg"));
+const image2 = Media.addImage(doc, fs.readFileSync("./demo/images/dog.png"));
+const image3 = Media.addImage(doc, fs.readFileSync("./demo/images/cat.jpg"));
+const image4 = Media.addImage(doc, fs.readFileSync("./demo/images/parrots.bmp"));
+const image5 = Media.addImage(doc, fs.readFileSync("./demo/images/pizza.gif"));
const imageBase64Data = `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`;
const image6 = Media.addImage(doc, Buffer.from(imageBase64Data, "base64"), 100, 100);
diff --git a/demo/demo24.ts b/demo/demo24.ts
index 23828aec65..4901477ced 100644
--- a/demo/demo24.ts
+++ b/demo/demo24.ts
@@ -8,7 +8,7 @@ const doc = new Document();
const table = doc.createTable(4, 4);
table.getCell(2, 2).addContent(new Paragraph("Hello"));
-const image = Media.addImage(doc, "./demo/images/image1.jpeg");
+const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg"));
table.getCell(1, 1).addContent(image.Paragraph);
const packer = new Packer();
diff --git a/demo/demo27.ts b/demo/demo27.ts
index e04fc07e02..a233cd9afd 100644
--- a/demo/demo27.ts
+++ b/demo/demo27.ts
@@ -30,5 +30,4 @@ const packer = new Packer();
packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
- console.log("Document created successfully at project root!");
});
diff --git a/demo/demo30.ts b/demo/demo30.ts
new file mode 100644
index 0000000000..a587e42b7a
--- /dev/null
+++ b/demo/demo30.ts
@@ -0,0 +1,29 @@
+import * as fs from "fs";
+import { Document, ImportDotx, Packer, Paragraph } from "../build";
+
+const importDotx = new ImportDotx();
+const filePath = "./demo/dotx/template.dotx";
+
+fs.readFile(filePath, (err, data) => {
+ if (err) {
+ throw new Error(`Failed to read file ${filePath}.`);
+ }
+
+ importDotx.extract(data).then((templateDocument) => {
+ // This any needs fixing
+ const sectionProps = {
+ titlePage: templateDocument.titlePageIsDefined,
+ } as any;
+
+ const doc = new Document(undefined, sectionProps, {
+ template: templateDocument,
+ });
+ const paragraph = new Paragraph("Hello World");
+ doc.addParagraph(paragraph);
+
+ const packer = new Packer();
+ packer.toBuffer(doc).then((buffer) => {
+ fs.writeFileSync("My Document.docx", buffer);
+ });
+ });
+});
diff --git a/demo/dotx/template.dotx b/demo/dotx/template.dotx
new file mode 100644
index 0000000000..c29318ad76
Binary files /dev/null and b/demo/dotx/template.dotx differ
diff --git a/package.json b/package.json
index ee9fa9049a..802c2bfb62 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"types": "./build/index.d.ts",
"dependencies": {
"@types/image-size": "0.0.29",
- "@types/jszip": "^3.1.3",
+ "@types/jszip": "^3.1.4",
"fast-xml-parser": "^3.3.6",
"image-size": "^0.6.2",
"jszip": "^3.1.5",
diff --git a/src/export/formatter.ts b/src/export/formatter.ts
index 199c32185f..0a28070d13 100644
--- a/src/export/formatter.ts
+++ b/src/export/formatter.ts
@@ -2,6 +2,12 @@ import { BaseXmlComponent, IXmlableObject } from "file/xml-components";
export class Formatter {
public format(input: BaseXmlComponent): IXmlableObject {
- return input.prepForXml();
+ const output = input.prepForXml();
+
+ if (output) {
+ return output;
+ } else {
+ throw Error("XMLComponent did not format correctly");
+ }
}
}
diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts
index 1ee02bf589..f04869cd9e 100644
--- a/src/export/packer/next-compiler.ts
+++ b/src/export/packer/next-compiler.ts
@@ -59,6 +59,18 @@ export class Compiler {
zip.file(`word/media/${data.fileName}`, mediaData);
}
+ for (const header of file.Headers) {
+ for (const data of header.Media.Array) {
+ zip.file(`word/media/${data.fileName}`, data.stream);
+ }
+ }
+
+ for (const footer of file.Footers) {
+ for (const data of footer.Media.Array) {
+ zip.file(`word/media/${data.fileName}`, data.stream);
+ }
+ }
+
return zip;
}
@@ -128,4 +140,13 @@ export class Compiler {
},
};
}
+
+ /* By default docx collapse empty tags. -> . this function mimic it
+ so comparing (diff) original docx file and the library output is easier
+ Currently not used, so commenting out */
+ // private collapseEmptyTags(xmlData: string): string {
+ // const regEx = /<(([^ <>]+)[^<>]*)><\/\2>/g;
+ // const collapsed = xmlData.replace(regEx, "<$1/>");
+ // return collapsed;
+ // }
}
diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts
index 31c1e49e32..67b9d9ab21 100644
--- a/src/file/document/body/body.spec.ts
+++ b/src/file/document/body/body.spec.ts
@@ -16,7 +16,7 @@ describe("Body", () => {
expect(formatted)
.to.have.property("w:sectPr")
.and.to.be.an.instanceof(Array);
- expect(formatted["w:sectPr"]).to.have.length(7);
+ expect(formatted["w:sectPr"]).to.have.length(5);
});
});
diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts
index 941a913bf9..1d19669588 100644
--- a/src/file/document/body/body.ts
+++ b/src/file/document/body/body.ts
@@ -35,7 +35,7 @@ export class Body extends XmlComponent {
this.sections.push(new SectionProperties(params));
}
}
- public prepForXml(): IXmlableObject {
+ public prepForXml(): IXmlableObject | undefined {
if (this.sections.length === 1) {
this.root.push(this.sections[0]);
} else if (this.sections.length > 1) {
diff --git a/src/file/document/body/section-properties/page-border/page-borders.spec.ts b/src/file/document/body/section-properties/page-border/page-borders.spec.ts
index 66abb9cc66..0bf22633ee 100644
--- a/src/file/document/body/section-properties/page-border/page-borders.spec.ts
+++ b/src/file/document/body/section-properties/page-border/page-borders.spec.ts
@@ -8,9 +8,7 @@ describe("PageBorders", () => {
describe("#constructor()", () => {
it("should create empty element when no options are passed", () => {
const properties = new PageBorders();
- const tree = new Formatter().format(properties);
-
- expect(tree).to.equal("");
+ expect(() => new Formatter().format(properties)).to.throw();
});
it("should create page borders with some configuration", () => {
diff --git a/src/file/document/body/section-properties/page-border/page-borders.ts b/src/file/document/body/section-properties/page-border/page-borders.ts
index 0af96cfd41..4017fbb7ae 100644
--- a/src/file/document/body/section-properties/page-border/page-borders.ts
+++ b/src/file/document/body/section-properties/page-border/page-borders.ts
@@ -98,7 +98,9 @@ export class PageBorders extends XmlComponent {
}
}
- public prepForXml(): IXmlableObject {
- return this.root.length > 0 ? super.prepForXml() : "";
+ public prepForXml(): IXmlableObject | undefined {
+ if (this.root.length > 0) {
+ return super.prepForXml();
+ }
}
}
diff --git a/src/file/document/body/section-properties/section-properties.spec.ts b/src/file/document/body/section-properties/section-properties.spec.ts
index 9a8645e5b0..8e422f425c 100644
--- a/src/file/document/body/section-properties/section-properties.spec.ts
+++ b/src/file/document/body/section-properties/section-properties.spec.ts
@@ -1,12 +1,17 @@
import { expect } from "chai";
import { Formatter } from "../../../../export/formatter";
-import { FooterReferenceType, PageBorderOffsetFrom, PageNumberFormat } from "./";
+import { FooterWrapper } from "../../../footer-wrapper";
+import { HeaderWrapper } from "../../../header-wrapper";
+import { Media } from "../../../media";
+import { PageBorderOffsetFrom, PageNumberFormat } from "./";
import { SectionProperties } from "./section-properties";
describe("SectionProperties", () => {
describe("#constructor()", () => {
it("should create section properties with options", () => {
+ const media = new Media();
+
const properties = new SectionProperties({
width: 11906,
height: 16838,
@@ -20,9 +25,12 @@ describe("SectionProperties", () => {
mirror: false,
space: 708,
linePitch: 360,
- headerId: 100,
- footerId: 200,
- footerType: FooterReferenceType.EVEN,
+ headers: {
+ default: new HeaderWrapper(media, 100),
+ },
+ footers: {
+ even: new FooterWrapper(media, 200),
+ },
pageNumberStart: 10,
pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT,
});
@@ -78,9 +86,7 @@ describe("SectionProperties", () => {
});
expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] });
expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] });
- expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] });
- expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] });
- expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] });
+ expect(tree["w:sectPr"][4]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] });
});
it("should create section properties with changed options", () => {
@@ -170,7 +176,8 @@ describe("SectionProperties", () => {
});
const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]);
- expect(tree["w:sectPr"][7]).to.deep.equal({
+ const pgBorders = tree["w:sectPr"].find((item) => item["w:pgBorders"] !== undefined);
+ expect(pgBorders).to.deep.equal({
"w:pgBorders": [{ _attr: { "w:offsetFrom": "page" } }],
});
});
diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts
index 5042c8a3ab..ec8a6b6746 100644
--- a/src/file/document/body/section-properties/section-properties.ts
+++ b/src/file/document/body/section-properties/section-properties.ts
@@ -1,115 +1,170 @@
// http://officeopenxml.com/WPsection.php
import { XmlComponent } from "file/xml-components";
-import { FooterReferenceType, IPageBordersOptions, IPageNumberTypeAttributes, PageBorders, PageNumberFormat, PageNumberType } from "./";
+import { FooterWrapper } from "../../../footer-wrapper";
+import { HeaderWrapper } from "../../../header-wrapper";
+import { IPageBordersOptions, IPageNumberTypeAttributes, PageBorders, PageNumberFormat, PageNumberType } from "./";
import { Columns } from "./columns/columns";
import { IColumnsAttributes } from "./columns/columns-attributes";
import { DocumentGrid } from "./doc-grid/doc-grid";
import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes";
-import { FooterReference, IFooterOptions } from "./footer-reference/footer-reference";
-import { HeaderReference, IHeaderOptions } from "./header-reference/header-reference";
-import { HeaderReferenceType } from "./header-reference/header-reference-attributes";
+import { FooterReferenceType } from "./footer-reference";
+import { FooterReference } from "./footer-reference/footer-reference";
+import { HeaderReferenceType } from "./header-reference";
+import { HeaderReference } from "./header-reference/header-reference";
import { PageMargin } from "./page-margin/page-margin";
import { IPageMarginAttributes } from "./page-margin/page-margin-attributes";
import { PageSize } from "./page-size/page-size";
import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes";
+import { TitlePage } from "./title-page/title-page";
+
+export interface IHeaderFooterGroup {
+ default?: T;
+ first?: T;
+ even?: T;
+}
+
+interface IHeadersOptions {
+ headers?: IHeaderFooterGroup;
+}
+
+interface IFootersOptions {
+ footers?: IHeaderFooterGroup;
+}
+
+interface ITitlePageOptions {
+ titlePage?: boolean;
+}
export type SectionPropertiesOptions = IPageSizeAttributes &
IPageMarginAttributes &
IColumnsAttributes &
IDocGridAttributesProperties &
- IHeaderOptions &
- IFooterOptions &
+ IHeadersOptions &
+ IFootersOptions &
IPageNumberTypeAttributes &
- IPageBordersOptions;
+ IPageBordersOptions &
+ ITitlePageOptions;
export class SectionProperties extends XmlComponent {
private readonly options: SectionPropertiesOptions;
- constructor(options?: SectionPropertiesOptions) {
+ constructor(options: SectionPropertiesOptions = {}) {
super("w:sectPr");
- const defaultOptions = {
- width: 11906,
- height: 16838,
- top: 1440,
- right: 1440,
- bottom: 1440,
- left: 1440,
- header: 708,
- footer: 708,
- gutter: 0,
- mirror: false,
- space: 708,
- linePitch: 360,
- orientation: PageOrientation.PORTRAIT,
- headerType: HeaderReferenceType.DEFAULT,
- headerId: 0,
- footerType: FooterReferenceType.DEFAULT,
- footerId: 0,
- pageNumberStart: undefined,
- pageNumberFormatType: PageNumberFormat.DECIMAL,
- pageBorders: undefined,
- pageBorderTop: undefined,
- pageBorderRight: undefined,
- pageBorderBottom: undefined,
- pageBorderLeft: undefined,
- };
+ const {
+ width = 11906,
+ height = 16838,
+ top = 1440,
+ right = 1440,
+ bottom = 1440,
+ left = 1440,
+ header = 708,
+ footer = 708,
+ gutter = 0,
+ mirror = false,
+ space = 708,
+ linePitch = 360,
+ orientation = PageOrientation.PORTRAIT,
+ headers,
+ footers,
+ pageNumberFormatType = PageNumberFormat.DECIMAL,
+ pageNumberStart,
+ pageBorders,
+ pageBorderTop,
+ pageBorderRight,
+ pageBorderBottom,
+ pageBorderLeft,
+ titlePage = false,
+ } = options;
- const mergedOptions = {
- ...defaultOptions,
- ...options,
- };
+ this.options = options;
+ this.root.push(new PageSize(width, height, orientation));
+ this.root.push(new PageMargin(top, right, bottom, left, header, footer, gutter, mirror));
+ this.root.push(new Columns(space));
+ this.root.push(new DocumentGrid(linePitch));
- this.root.push(new PageSize(mergedOptions.width, mergedOptions.height, mergedOptions.orientation));
- this.root.push(
- new PageMargin(
- mergedOptions.top,
- mergedOptions.right,
- mergedOptions.bottom,
- mergedOptions.left,
- mergedOptions.header,
- mergedOptions.footer,
- mergedOptions.gutter,
- mergedOptions.mirror,
- ),
- );
- this.root.push(new Columns(mergedOptions.space));
- this.root.push(new DocumentGrid(mergedOptions.linePitch));
+ this.addHeaders(headers);
+ this.addFooters(footers);
- this.root.push(
- new HeaderReference({
- headerType: mergedOptions.headerType,
- headerId: mergedOptions.headerId,
- }),
- );
- this.root.push(
- new FooterReference({
- footerType: mergedOptions.footerType,
- footerId: mergedOptions.footerId,
- }),
- );
+ this.root.push(new PageNumberType(pageNumberStart, pageNumberFormatType));
- this.root.push(new PageNumberType(mergedOptions.pageNumberStart, mergedOptions.pageNumberFormatType));
-
- if (
- mergedOptions.pageBorders ||
- mergedOptions.pageBorderTop ||
- mergedOptions.pageBorderRight ||
- mergedOptions.pageBorderBottom ||
- mergedOptions.pageBorderLeft
- ) {
+ if (pageBorders || pageBorderTop || pageBorderRight || pageBorderBottom || pageBorderLeft) {
this.root.push(
new PageBorders({
- pageBorders: mergedOptions.pageBorders,
- pageBorderTop: mergedOptions.pageBorderTop,
- pageBorderRight: mergedOptions.pageBorderRight,
- pageBorderBottom: mergedOptions.pageBorderBottom,
- pageBorderLeft: mergedOptions.pageBorderLeft,
+ pageBorders: pageBorders,
+ pageBorderTop: pageBorderTop,
+ pageBorderRight: pageBorderRight,
+ pageBorderBottom: pageBorderBottom,
+ pageBorderLeft: pageBorderLeft,
}),
);
}
- this.options = mergedOptions;
+ if (titlePage) {
+ this.root.push(new TitlePage());
+ }
+ }
+
+ private addHeaders(headers?: IHeaderFooterGroup): void {
+ if (headers) {
+ if (headers.default) {
+ this.root.push(
+ new HeaderReference({
+ headerType: HeaderReferenceType.DEFAULT,
+ headerId: headers.default.Header.ReferenceId,
+ }),
+ );
+ }
+
+ if (headers.first) {
+ this.root.push(
+ new HeaderReference({
+ headerType: HeaderReferenceType.FIRST,
+ headerId: headers.first.Header.ReferenceId,
+ }),
+ );
+ }
+
+ if (headers.even) {
+ this.root.push(
+ new HeaderReference({
+ headerType: HeaderReferenceType.EVEN,
+ headerId: headers.even.Header.ReferenceId,
+ }),
+ );
+ }
+ }
+ }
+
+ private addFooters(footers?: IHeaderFooterGroup): void {
+ if (footers) {
+ if (footers.default) {
+ this.root.push(
+ new FooterReference({
+ footerType: FooterReferenceType.DEFAULT,
+ footerId: footers.default.Footer.ReferenceId,
+ }),
+ );
+ }
+
+ if (footers.first) {
+ this.root.push(
+ new FooterReference({
+ footerType: FooterReferenceType.FIRST,
+ footerId: footers.first.Footer.ReferenceId,
+ }),
+ );
+ }
+
+ if (footers.even) {
+ this.root.push(
+ new FooterReference({
+ footerType: FooterReferenceType.EVEN,
+ footerId: footers.even.Footer.ReferenceId,
+ }),
+ );
+ }
+ }
}
public get Options(): SectionPropertiesOptions {
diff --git a/src/file/file-properties.ts b/src/file/file-properties.ts
new file mode 100644
index 0000000000..3ccf8a5176
--- /dev/null
+++ b/src/file/file-properties.ts
@@ -0,0 +1,11 @@
+import { IDocumentTemplate } from "../import-dotx";
+
+export interface IFileProperties {
+ template?: IDocumentTemplate;
+}
+
+// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
+/**
+ * @ignore
+ */
+export const WORKAROUND = "";
diff --git a/src/file/file.ts b/src/file/file.ts
index afa268a5a7..e18b536490 100644
--- a/src/file/file.ts
+++ b/src/file/file.ts
@@ -2,10 +2,17 @@ import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties";
import { Document } from "./document";
-import { FooterReferenceType, HeaderReference, HeaderReferenceType, SectionPropertiesOptions } from "./document/body/section-properties";
-import { FooterWrapper } from "./footer-wrapper";
+import {
+ FooterReferenceType,
+ HeaderReference,
+ HeaderReferenceType,
+ IHeaderFooterGroup,
+ SectionPropertiesOptions,
+} from "./document/body/section-properties";
+import { IFileProperties } from "./file-properties";
+import { FooterWrapper, IDocumentFooter } from "./footer-wrapper";
import { FootNotes } from "./footnotes";
-import { HeaderWrapper } from "./header-wrapper";
+import { HeaderWrapper, IDocumentHeader } from "./header-wrapper";
import { Image, Media } from "./media";
import { Numbering } from "./numbering";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph";
@@ -18,33 +25,51 @@ import { Table } from "./table";
import { TableOfContents } from "./table-of-contents";
export class File {
+ private currentRelationshipId: number = 1;
+
private readonly document: Document;
- private styles: Styles;
+ private readonly headers: IDocumentHeader[] = [];
+ private readonly footers: IDocumentFooter[] = [];
+ private readonly docRelationships: Relationships;
private readonly coreProperties: CoreProperties;
private readonly numbering: Numbering;
private readonly media: Media;
- private readonly docRelationships: Relationships;
private readonly fileRelationships: Relationships;
- private readonly headerWrapper: HeaderWrapper[] = [];
- private readonly footerWrapper: FooterWrapper[] = [];
private readonly footNotes: FootNotes;
private readonly settings: Settings;
-
private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties;
+ private styles: Styles;
- private currentRelationshipId: number = 1;
+ constructor(
+ options: IPropertiesOptions = {
+ creator: "Un-named",
+ revision: "1",
+ lastModifiedBy: "Un-named",
+ },
+ sectionPropertiesOptions: SectionPropertiesOptions = {},
+ fileProperties: IFileProperties = {},
+ ) {
+ this.coreProperties = new CoreProperties(options);
+ this.numbering = new Numbering();
+ this.docRelationships = new Relationships();
+ this.media = new Media();
+ this.fileRelationships = new Relationships();
+ this.appProperties = new AppProperties();
+ this.footNotes = new FootNotes();
+ this.contentTypes = new ContentTypes();
- constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
- if (!options) {
- options = {
- creator: "Un-named",
- revision: "1",
- lastModifiedBy: "Un-named",
- };
+ if (fileProperties.template) {
+ this.currentRelationshipId = fileProperties.template.currentRelationshipId + 1;
}
- if (options.externalStyles) {
+ // set up styles
+ if (fileProperties.template && options.externalStyles) {
+ throw Error("can not use both template and external styles");
+ }
+ if (fileProperties.template) {
+ this.styles = fileProperties.template.styles;
+ } else if (options.externalStyles) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else {
@@ -52,61 +77,30 @@ export class File {
this.styles = stylesFactory.newInstance();
}
- this.coreProperties = new CoreProperties(options);
- this.numbering = new Numbering();
- this.docRelationships = new Relationships();
- this.docRelationships.createRelationship(
- this.currentRelationshipId++,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
- "styles.xml",
- );
- this.docRelationships.createRelationship(
- this.currentRelationshipId++,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
- "numbering.xml",
- );
- this.contentTypes = new ContentTypes();
+ this.addDefaultRelationships();
- this.docRelationships.createRelationship(
- this.currentRelationshipId++,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
- "footnotes.xml",
- );
- this.media = new Media();
-
- const header = this.createHeader();
- const footer = this.createFooter();
-
- this.fileRelationships = new Relationships();
- this.fileRelationships.createRelationship(
- 1,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
- "word/document.xml",
- );
- this.fileRelationships.createRelationship(
- 2,
- "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
- "docProps/core.xml",
- );
- this.fileRelationships.createRelationship(
- 3,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
- "docProps/app.xml",
- );
- this.appProperties = new AppProperties();
-
- this.footNotes = new FootNotes();
- if (!sectionPropertiesOptions) {
- sectionPropertiesOptions = {
- footerType: FooterReferenceType.DEFAULT,
- headerType: HeaderReferenceType.DEFAULT,
- headerId: header.Header.ReferenceId,
- footerId: footer.Footer.ReferenceId,
- };
+ if (fileProperties.template && fileProperties.template.headers) {
+ for (const templateHeader of fileProperties.template.headers) {
+ this.addHeaderToDocument(templateHeader.header, templateHeader.type);
+ }
} else {
- sectionPropertiesOptions.headerId = header.Header.ReferenceId;
- sectionPropertiesOptions.footerId = footer.Footer.ReferenceId;
+ this.createHeader();
}
+
+ if (fileProperties.template && fileProperties.template.footers) {
+ for (const templateFooter of fileProperties.template.footers) {
+ this.addFooterToDocument(templateFooter.footer, templateFooter.type);
+ }
+ } else {
+ this.createFooter();
+ }
+
+ sectionPropertiesOptions = {
+ ...sectionPropertiesOptions,
+ headers: this.groupHeaders(this.headers, sectionPropertiesOptions.headers),
+ footers: this.groupFooters(this.footers, sectionPropertiesOptions.footers),
+ };
+
this.document = new Document(sectionPropertiesOptions);
this.settings = new Settings();
}
@@ -115,8 +109,9 @@ export class File {
this.document.addTableOfContents(toc);
}
- public addParagraph(paragraph: Paragraph): void {
+ public addParagraph(paragraph: Paragraph): File {
this.document.addParagraph(paragraph);
+ return this;
}
public createParagraph(text?: string): Paragraph {
@@ -179,25 +174,13 @@ export class File {
public createHeader(): HeaderWrapper {
const header = new HeaderWrapper(this.media, this.currentRelationshipId++);
- this.headerWrapper.push(header);
- this.docRelationships.createRelationship(
- header.Header.ReferenceId,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
- `header${this.headerWrapper.length}.xml`,
- );
- this.contentTypes.addHeader(this.headerWrapper.length);
+ this.addHeaderToDocument(header);
return header;
}
public createFooter(): FooterWrapper {
const footer = new FooterWrapper(this.media, this.currentRelationshipId++);
- this.footerWrapper.push(footer);
- this.docRelationships.createRelationship(
- footer.Footer.ReferenceId,
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
- `footer${this.footerWrapper.length}.xml`,
- );
- this.contentTypes.addFooter(this.footerWrapper.length);
+ this.addFooterToDocument(footer);
return footer;
}
@@ -214,6 +197,152 @@ export class File {
return headerWrapper;
}
+ public getFooterByReferenceNumber(refId: number): FooterWrapper {
+ const entry = this.footers.map((item) => item.footer).find((h) => h.Footer.ReferenceId === refId);
+ if (entry) {
+ return entry;
+ }
+ throw new Error(`There is no footer with given reference id ${refId}`);
+ }
+
+ public getHeaderByReferenceNumber(refId: number): HeaderWrapper {
+ const entry = this.headers.map((item) => item.header).find((h) => h.Header.ReferenceId === refId);
+ if (entry) {
+ return entry;
+ }
+ throw new Error(`There is no header with given reference id ${refId}`);
+ }
+
+ public verifyUpdateFields(): void {
+ if (this.document.getTablesOfContents().length) {
+ this.settings.addUpdateFields();
+ }
+ }
+
+ private addHeaderToDocument(header: HeaderWrapper, type: HeaderReferenceType = HeaderReferenceType.DEFAULT): void {
+ this.headers.push({ header, type });
+ this.docRelationships.createRelationship(
+ header.Header.ReferenceId,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
+ `header${this.headers.length}.xml`,
+ );
+ this.contentTypes.addHeader(this.headers.length);
+ }
+
+ private addFooterToDocument(footer: FooterWrapper, type: FooterReferenceType = FooterReferenceType.DEFAULT): void {
+ this.footers.push({ footer, type });
+ this.docRelationships.createRelationship(
+ footer.Footer.ReferenceId,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
+ `footer${this.footers.length}.xml`,
+ );
+ this.contentTypes.addFooter(this.footers.length);
+ }
+
+ private addDefaultRelationships(): void {
+ this.fileRelationships.createRelationship(
+ 1,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
+ "word/document.xml",
+ );
+ this.fileRelationships.createRelationship(
+ 2,
+ "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
+ "docProps/core.xml",
+ );
+ this.fileRelationships.createRelationship(
+ 3,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
+ "docProps/app.xml",
+ );
+
+ this.docRelationships.createRelationship(
+ this.currentRelationshipId++,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
+ "styles.xml",
+ );
+ this.docRelationships.createRelationship(
+ this.currentRelationshipId++,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
+ "numbering.xml",
+ );
+ this.docRelationships.createRelationship(
+ this.currentRelationshipId++,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
+ "footnotes.xml",
+ );
+ }
+
+ private groupHeaders(headers: IDocumentHeader[], group: IHeaderFooterGroup = {}): IHeaderFooterGroup {
+ let newGroup = group;
+
+ for (const header of headers) {
+ switch (header.type) {
+ case HeaderReferenceType.DEFAULT:
+ newGroup = {
+ ...newGroup,
+ default: header.header,
+ };
+ break;
+ case HeaderReferenceType.FIRST:
+ newGroup = {
+ ...newGroup,
+ first: header.header,
+ };
+ break;
+ case HeaderReferenceType.EVEN:
+ newGroup = {
+ ...newGroup,
+ even: header.header,
+ };
+ break;
+ default:
+ newGroup = {
+ ...newGroup,
+ default: header.header,
+ };
+ break;
+ }
+ }
+
+ return newGroup;
+ }
+
+ private groupFooters(footers: IDocumentFooter[], group: IHeaderFooterGroup = {}): IHeaderFooterGroup {
+ let newGroup = group;
+
+ for (const footer of footers) {
+ switch (footer.type) {
+ case FooterReferenceType.DEFAULT:
+ newGroup = {
+ ...newGroup,
+ default: footer.footer,
+ };
+ break;
+ case FooterReferenceType.FIRST:
+ newGroup = {
+ ...newGroup,
+ first: footer.footer,
+ };
+ break;
+ case FooterReferenceType.EVEN:
+ newGroup = {
+ ...newGroup,
+ even: footer.footer,
+ };
+ break;
+ default:
+ newGroup = {
+ ...newGroup,
+ default: footer.footer,
+ };
+ break;
+ }
+ }
+
+ return newGroup;
+ }
+
public get Document(): Document {
return this.document;
}
@@ -247,35 +376,19 @@ export class File {
}
public get Header(): HeaderWrapper {
- return this.headerWrapper[0];
+ return this.headers[0].header;
}
public get Headers(): HeaderWrapper[] {
- return this.headerWrapper;
- }
-
- public HeaderByRefNumber(refId: number): HeaderWrapper {
- const entry = this.headerWrapper.find((h) => h.Header.ReferenceId === refId);
- if (entry) {
- return entry;
- }
- throw new Error(`There is no header with given reference id ${refId}`);
+ return this.headers.map((item) => item.header);
}
public get Footer(): FooterWrapper {
- return this.footerWrapper[0];
+ return this.footers[0].footer;
}
public get Footers(): FooterWrapper[] {
- return this.footerWrapper;
- }
-
- public FooterByRefNumber(refId: number): FooterWrapper {
- const entry = this.footerWrapper.find((h) => h.Footer.ReferenceId === refId);
- if (entry) {
- return entry;
- }
- throw new Error(`There is no footer with given reference id ${refId}`);
+ return this.footers.map((item) => item.footer);
}
public get ContentTypes(): ContentTypes {
@@ -293,10 +406,4 @@ export class File {
public get Settings(): Settings {
return this.settings;
}
-
- public verifyUpdateFields(): void {
- if (this.document.getTablesOfContents().length) {
- this.settings.addUpdateFields();
- }
- }
}
diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts
index afd673c2b2..2c51cf5072 100644
--- a/src/file/footer-wrapper.ts
+++ b/src/file/footer-wrapper.ts
@@ -1,16 +1,23 @@
import { XmlComponent } from "file/xml-components";
+
+import { FooterReferenceType } from "./document";
import { Footer } from "./footer/footer";
-import { Image, Media } from "./media";
+import { Image, IMediaData, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
+export interface IDocumentFooter {
+ footer: FooterWrapper;
+ type: FooterReferenceType;
+}
+
export class FooterWrapper {
private readonly footer: Footer;
private readonly relationships: Relationships;
- constructor(private readonly media: Media, referenceId: number) {
- this.footer = new Footer(referenceId);
+ constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) {
+ this.footer = new Footer(referenceId, initContent);
this.relationships = new Relationships();
}
@@ -32,17 +39,33 @@ export class FooterWrapper {
return this.footer.createTable(rows, cols);
}
- public addChildElement(childElement: XmlComponent | string): void {
+ public addChildElement(childElement: XmlComponent): void {
this.footer.addChildElement(childElement);
}
- public createImage(image: Buffer, width?: number, height?: number): void {
- const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height);
+ public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData {
+ const mediaData = this.media.addMedia(image, refId, width, height);
this.relationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
+ return mediaData;
+ }
+
+ public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void {
+ this.relationships.createRelationship(
+ refId,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
+ target,
+ targetMode,
+ );
+ }
+
+ public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void {
+ // TODO
+ // tslint:disable-next-line:no-any
+ const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height);
this.addImage(new Image(new ImageParagraph(mediaData)));
}
@@ -58,4 +81,8 @@ export class FooterWrapper {
public get Relationships(): Relationships {
return this.relationships;
}
+
+ public get Media(): Media {
+ return this.media;
+ }
}
diff --git a/src/file/footer/footer.ts b/src/file/footer/footer.ts
index 532e662c25..0aa5938d0b 100644
--- a/src/file/footer/footer.ts
+++ b/src/file/footer/footer.ts
@@ -1,14 +1,14 @@
// http://officeopenxml.com/WPfooters.php
-import { XmlComponent } from "file/xml-components";
+import { InitializableXmlComponent, XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { Table } from "../table";
import { FooterAttributes } from "./footer-attributes";
-export class Footer extends XmlComponent {
+export class Footer extends InitializableXmlComponent {
private readonly refId: number;
- constructor(referenceNumber: number) {
- super("w:ftr");
+ constructor(referenceNumber: number, initContent?: XmlComponent) {
+ super("w:ftr", initContent);
this.refId = referenceNumber;
this.root.push(
new FooterAttributes({
diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts
index 79a216610a..f4a403ce27 100644
--- a/src/file/header-wrapper.ts
+++ b/src/file/header-wrapper.ts
@@ -1,16 +1,23 @@
import { XmlComponent } from "file/xml-components";
+
+import { HeaderReferenceType } from "./document";
import { Header } from "./header/header";
-import { Image, Media } from "./media";
+import { Image, IMediaData, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
+export interface IDocumentHeader {
+ header: HeaderWrapper;
+ type: HeaderReferenceType;
+}
+
export class HeaderWrapper {
private readonly header: Header;
private readonly relationships: Relationships;
- constructor(private readonly media: Media, referenceId: number) {
- this.header = new Header(referenceId);
+ constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) {
+ this.header = new Header(referenceId, initContent);
this.relationships = new Relationships();
}
@@ -36,13 +43,29 @@ export class HeaderWrapper {
this.header.addChildElement(childElement);
}
- public createImage(image: Buffer, width?: number, height?: number): void {
- const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height);
+ public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData {
+ const mediaData = this.media.addMedia(image, refId, width, height);
this.relationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
+ return mediaData;
+ }
+
+ public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void {
+ this.relationships.createRelationship(
+ refId,
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
+ target,
+ targetMode,
+ );
+ }
+
+ public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void {
+ // TODO
+ // tslint:disable-next-line:no-any
+ const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height);
this.addImage(new Image(new ImageParagraph(mediaData)));
}
@@ -58,4 +81,8 @@ export class HeaderWrapper {
public get Relationships(): Relationships {
return this.relationships;
}
+
+ public get Media(): Media {
+ return this.media;
+ }
}
diff --git a/src/file/header/header-attributes.ts b/src/file/header/header-attributes.ts
index e47271841c..3e46b4ca94 100644
--- a/src/file/header/header-attributes.ts
+++ b/src/file/header/header-attributes.ts
@@ -23,6 +23,17 @@ export interface IHeaderAttributesProperties {
dcmitype?: string;
xsi?: string;
type?: string;
+ cx?: string;
+ cx1?: string;
+ cx2?: string;
+ cx3?: string;
+ cx4?: string;
+ cx5?: string;
+ cx6?: string;
+ cx7?: string;
+ cx8?: string;
+ w16cid: string;
+ w16se: string;
}
export class HeaderAttributes extends XmlAttributeComponent {
@@ -49,5 +60,16 @@ export class HeaderAttributes extends XmlAttributeComponent this.root.push(x));
this.concreteNumbering.forEach((x) => this.root.push(x));
return super.prepForXml();
diff --git a/src/file/styles/external-styles-factory.spec.ts b/src/file/styles/external-styles-factory.spec.ts
index 295c9644f6..5bc7925e8d 100644
--- a/src/file/styles/external-styles-factory.spec.ts
+++ b/src/file/styles/external-styles-factory.spec.ts
@@ -61,8 +61,6 @@ describe("External styles factory", () => {
it("should parse other child elements of w:styles", () => {
// tslint:disable-next-line:no-any
const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
-
- expect(importedStyle.root.length).to.equal(5);
expect(importedStyle.root[1]).to.eql({
deleted: false,
root: [
diff --git a/src/file/table/table-cell-margin.ts b/src/file/table/table-cell-margin.ts
index 198dc89cbd..2b8fd32049 100644
--- a/src/file/table/table-cell-margin.ts
+++ b/src/file/table/table-cell-margin.ts
@@ -21,8 +21,10 @@ export class TableCellMargin extends XmlComponent {
super("w:tblCellMar");
}
- public prepForXml(): IXmlableObject {
- return this.root.length > 0 ? super.prepForXml() : "";
+ public prepForXml(): IXmlableObject | undefined {
+ if (this.root.length > 0) {
+ return super.prepForXml();
+ }
}
public addTopMargin(value: number, type: WidthType = WidthType.DXA): void {
diff --git a/src/file/table/table-cell.spec.ts b/src/file/table/table-cell.spec.ts
index 7a3072f186..4e761d2e07 100644
--- a/src/file/table/table-cell.spec.ts
+++ b/src/file/table/table-cell.spec.ts
@@ -8,8 +8,7 @@ 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("");
+ expect(() => new Formatter().format(tb)).to.throw();
});
});
diff --git a/src/file/table/table-cell.ts b/src/file/table/table-cell.ts
index 19e3ccc428..046cc8d8aa 100644
--- a/src/file/table/table-cell.ts
+++ b/src/file/table/table-cell.ts
@@ -29,8 +29,10 @@ export class TableCellBorders extends XmlComponent {
super("w:tcBorders");
}
- public prepForXml(): IXmlableObject {
- return this.root.length > 0 ? super.prepForXml() : "";
+ public prepForXml(): IXmlableObject | undefined {
+ if (this.root.length > 0) {
+ return super.prepForXml();
+ }
}
public addTopBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
diff --git a/src/file/table/table.ts b/src/file/table/table.ts
index f6201acedc..3220bce537 100644
--- a/src/file/table/table.ts
+++ b/src/file/table/table.ts
@@ -145,9 +145,13 @@ export class TableCell extends XmlComponent {
return this;
}
- public prepForXml(): IXmlableObject {
+ public prepForXml(): IXmlableObject | undefined {
// Cells must end with a paragraph
const retval = super.prepForXml();
+ if (!retval) {
+ return undefined;
+ }
+
const content = retval["w:tc"];
if (!content[content.length - 1]["w:p"]) {
content.push(new Paragraph().prepForXml());
diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts
index a5f24f3824..d65e5c6f3c 100644
--- a/src/file/xml-components/base.ts
+++ b/src/file/xml-components/base.ts
@@ -8,7 +8,7 @@ export abstract class BaseXmlComponent {
this.rootKey = rootKey;
}
- public abstract prepForXml(): IXmlableObject;
+ public abstract prepForXml(): IXmlableObject | undefined;
public get IsDeleted(): boolean {
return this.deleted;
diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts
index 23c4137900..eb318a3d64 100644
--- a/src/file/xml-components/imported-xml-component.ts
+++ b/src/file/xml-components/imported-xml-component.ts
@@ -1,7 +1,7 @@
-/* tslint:disable */
-import { XmlComponent, IXmlableObject } from ".";
+// tslint:disable:no-any
import * as fastXmlParser from "fast-xml-parser";
import { flatMap } from "lodash";
+import { IXmlableObject, XmlComponent } from ".";
export const parseOptions = {
ignoreAttributes: false,
@@ -54,8 +54,27 @@ export function convertToXmlComponent(elementName: string, element: any): Import
* Represents imported xml component from xml file.
*/
export class ImportedXmlComponent extends XmlComponent {
- private _attr: any;
+ /**
+ * Converts the xml string to a XmlComponent tree.
+ *
+ * @param importedContent xml content of the imported component
+ */
+ public static fromXmlString(importedContent: string): ImportedXmlComponent {
+ const imported = fastXmlParser.parse(importedContent, parseOptions);
+ const elementName = Object.keys(imported)[0];
+ const converted = convertToXmlComponent(elementName, imported[elementName]);
+
+ if (Array.isArray(converted) && converted.length > 1) {
+ throw new Error("Invalid conversion, input must be one element.");
+ }
+ return Array.isArray(converted) ? converted[0] : converted;
+ }
+
+ // tslint:disable-next-line:variable-name
+ private readonly _attr: any;
+
+ // tslint:disable-next-line:variable-name
constructor(rootKey: string, _attr?: any) {
super(rootKey);
if (_attr) {
@@ -89,8 +108,12 @@ export class ImportedXmlComponent extends XmlComponent {
* ]
* }
*/
- prepForXml(): IXmlableObject {
+ public prepForXml(): IXmlableObject | undefined {
const result = super.prepForXml();
+ if (!result) {
+ return undefined;
+ }
+
if (!!this._attr) {
if (!Array.isArray(result[this.rootKey])) {
result[this.rootKey] = [result[this.rootKey]];
@@ -100,33 +123,17 @@ export class ImportedXmlComponent extends XmlComponent {
return result;
}
- push(xmlComponent: XmlComponent) {
+ public push(xmlComponent: XmlComponent): void {
this.root.push(xmlComponent);
}
-
- /**
- * Converts the xml string to a XmlComponent tree.
- *
- * @param importedContent xml content of the imported component
- */
- static fromXmlString(importedContent: string): ImportedXmlComponent {
- const imported = fastXmlParser.parse(importedContent, parseOptions);
- const elementName = Object.keys(imported)[0];
-
- const converted = convertToXmlComponent(elementName, imported[elementName]);
-
- if (Array.isArray(converted) && converted.length > 1) {
- throw new Error("Invalid conversion, input must be one element.");
- }
- return Array.isArray(converted) ? converted[0] : converted;
- }
}
/**
* Used for the attributes of root element that is being imported.
*/
export class ImportedRootElementAttributes extends XmlComponent {
- constructor(private _attr: any) {
+ // tslint:disable-next-line:variable-name
+ constructor(private readonly _attr: any) {
super("");
}
diff --git a/src/file/xml-components/index.ts b/src/file/xml-components/index.ts
index 917933869e..66e9641bfd 100644
--- a/src/file/xml-components/index.ts
+++ b/src/file/xml-components/index.ts
@@ -3,3 +3,4 @@ export * from "./attributes";
export * from "./default-attributes";
export * from "./imported-xml-component";
export * from "./xmlable-object";
+export * from "./initializable-xml-component";
diff --git a/src/file/xml-components/initializable-xml-component.ts b/src/file/xml-components/initializable-xml-component.ts
new file mode 100644
index 0000000000..5d461c007a
--- /dev/null
+++ b/src/file/xml-components/initializable-xml-component.ts
@@ -0,0 +1,11 @@
+import { XmlComponent } from "file/xml-components";
+
+export abstract class InitializableXmlComponent extends XmlComponent {
+ constructor(rootKey: string, initComponent?: InitializableXmlComponent) {
+ super(rootKey);
+
+ if (initComponent) {
+ this.root = initComponent.root;
+ }
+ }
+}
diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts
index 8b4f983388..10e2f44dc4 100644
--- a/src/file/xml-components/xml-component.spec.ts
+++ b/src/file/xml-components/xml-component.spec.ts
@@ -26,6 +26,11 @@ describe("XmlComponent", () => {
xmlComponent.addChildElement(child);
const xml = xmlComponent.prepForXml();
+
+ if (!xml) {
+ return;
+ }
+
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 fae843db29..acbe51cb04 100644
--- a/src/file/xml-components/xml-component.ts
+++ b/src/file/xml-components/xml-component.ts
@@ -7,10 +7,10 @@ export abstract class XmlComponent extends BaseXmlComponent {
constructor(rootKey: string) {
super(rootKey);
- this.root = new Array();
+ this.root = new Array();
}
- public prepForXml(): IXmlableObject {
+ public prepForXml(): IXmlableObject | undefined {
const children = this.root
.filter((c) => {
if (c instanceof BaseXmlComponent) {
@@ -24,7 +24,7 @@ export abstract class XmlComponent extends BaseXmlComponent {
}
return comp;
})
- .filter((comp) => comp); // Exclude null, undefined, and empty strings
+ .filter((comp) => comp !== undefined); // Exclude undefined
return {
[this.rootKey]: children,
};
diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts
new file mode 100644
index 0000000000..1d87c71e23
--- /dev/null
+++ b/src/import-dotx/import-dotx.ts
@@ -0,0 +1,209 @@
+import * as fastXmlParser from "fast-xml-parser";
+import * as JSZip from "jszip";
+
+import { FooterReferenceType } from "file/document/body/section-properties/footer-reference";
+import { HeaderReferenceType } from "file/document/body/section-properties/header-reference";
+import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper";
+import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper";
+import { Media } from "file/media";
+import { Styles } from "file/styles";
+import { ExternalStylesFactory } from "file/styles/external-styles-factory";
+import { convertToXmlComponent, ImportedXmlComponent, parseOptions } from "file/xml-components";
+
+const importParseOptions = {
+ ...parseOptions,
+ textNodeName: "",
+ trimValues: false,
+};
+
+const schemeToType = {
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header",
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer": "footer",
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image": "image",
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink": "hyperlink",
+};
+
+interface IDocumentRefs {
+ headers: Array<{ id: number; type: HeaderReferenceType }>;
+ footers: Array<{ id: number; type: FooterReferenceType }>;
+}
+
+interface IRelationshipFileInfo {
+ id: number;
+ target: string;
+ type: "header" | "footer" | "image" | "hyperlink";
+}
+
+// Document Template
+// https://fileinfo.com/extension/dotx
+export interface IDocumentTemplate {
+ currentRelationshipId: number;
+ headers: IDocumentHeader[];
+ footers: IDocumentFooter[];
+ styles: Styles;
+ titlePageIsDefined: boolean;
+}
+
+export class ImportDotx {
+ private currentRelationshipId: number;
+
+ constructor() {
+ this.currentRelationshipId = 1;
+ }
+
+ public async extract(data: Buffer): Promise {
+ const zipContent = await JSZip.loadAsync(data);
+
+ const stylesContent = await zipContent.files["word/styles.xml"].async("text");
+ const stylesFactory = new ExternalStylesFactory();
+ const styles = stylesFactory.newInstance(stylesContent);
+
+ const documentContent = zipContent.files["word/document.xml"];
+ const documentRefs: IDocumentRefs = this.extractDocumentRefs(await documentContent.async("text"));
+ const titlePageIsDefined = this.titlePageIsDefined(await documentContent.async("text"));
+
+ const relationshipContent = zipContent.files["word/_rels/document.xml.rels"];
+ const documentRelationships: IRelationshipFileInfo[] = this.findReferenceFiles(await relationshipContent.async("text"));
+
+ const media = new Media();
+
+ const headers: IDocumentHeader[] = [];
+ for (const headerRef of documentRefs.headers) {
+ const headerKey = "w:hdr";
+ const relationshipFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id);
+ if (!relationshipFileInfo) {
+ throw new Error(`Can not find target file for id ${headerRef.id}`);
+ }
+
+ const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text");
+ const xmlObj = fastXmlParser.parse(xmlData, importParseOptions);
+
+ const importedComp = convertToXmlComponent(headerKey, xmlObj[headerKey]) as ImportedXmlComponent;
+
+ const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp);
+ await this.addRelationToWrapper(relationshipFileInfo, zipContent, header);
+ headers.push({ type: headerRef.type, header });
+ }
+
+ const footers: IDocumentFooter[] = [];
+ for (const footerRef of documentRefs.footers) {
+ const footerKey = "w:ftr";
+ const relationshipFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id);
+ if (!relationshipFileInfo) {
+ throw new Error(`Can not find target file for id ${footerRef.id}`);
+ }
+ const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text");
+ const xmlObj = fastXmlParser.parse(xmlData, importParseOptions);
+ const importedComp = convertToXmlComponent(footerKey, xmlObj[footerKey]) as ImportedXmlComponent;
+
+ const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp);
+ await this.addRelationToWrapper(relationshipFileInfo, zipContent, footer);
+ footers.push({ type: footerRef.type, footer });
+ }
+
+ const templateDocument: IDocumentTemplate = {
+ headers,
+ footers,
+ currentRelationshipId: this.currentRelationshipId,
+ styles,
+ titlePageIsDefined,
+ };
+ return templateDocument;
+ }
+
+ public async addRelationToWrapper(
+ relationhipFile: IRelationshipFileInfo,
+ zipContent: JSZip,
+ wrapper: HeaderWrapper | FooterWrapper,
+ ): Promise {
+ let wrapperImagesReferences: IRelationshipFileInfo[] = [];
+ let hyperLinkReferences: IRelationshipFileInfo[] = [];
+ const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`];
+ if (refFile) {
+ const xmlRef = await refFile.async("text");
+ wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "image");
+ hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "hyperlink");
+ }
+ for (const r of wrapperImagesReferences) {
+ const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer");
+ wrapper.addImageRelationship(buffer, r.id);
+ }
+ for (const r of hyperLinkReferences) {
+ wrapper.addHyperlinkRelationship(r.target, r.id, "External");
+ }
+ }
+
+ public findReferenceFiles(xmlData: string): IRelationshipFileInfo[] {
+ const xmlObj = fastXmlParser.parse(xmlData, importParseOptions);
+ const relationshipXmlArray = Array.isArray(xmlObj.Relationships.Relationship)
+ ? xmlObj.Relationships.Relationship
+ : [xmlObj.Relationships.Relationship];
+ const relationships: IRelationshipFileInfo[] = relationshipXmlArray
+ .map((item) => {
+ return {
+ id: this.parseRefId(item._attr.Id),
+ type: schemeToType[item._attr.Type],
+ target: item._attr.Target,
+ };
+ })
+ .filter((item) => item.type !== null);
+ return relationships;
+ }
+
+ public extractDocumentRefs(xmlData: string): IDocumentRefs {
+ interface IAttributedXML {
+ _attr: object;
+ }
+ const xmlObj = fastXmlParser.parse(xmlData, importParseOptions);
+ const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
+
+ const headerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:headerReference"];
+ let headersXmlArray: IAttributedXML[];
+ if (headerProps === undefined) {
+ headersXmlArray = [];
+ } else if (Array.isArray(headerProps)) {
+ headersXmlArray = headerProps;
+ } else {
+ headersXmlArray = [headerProps];
+ }
+ const headers = headersXmlArray.map((item) => {
+ return {
+ type: item._attr["w:type"],
+ id: this.parseRefId(item._attr["r:id"]),
+ };
+ });
+
+ const footerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:footerReference"];
+ let footersXmlArray: IAttributedXML[];
+ if (footerProps === undefined) {
+ footersXmlArray = [];
+ } else if (Array.isArray(footerProps)) {
+ footersXmlArray = footerProps;
+ } else {
+ footersXmlArray = [footerProps];
+ }
+
+ const footers = footersXmlArray.map((item) => {
+ return {
+ type: item._attr["w:type"],
+ id: this.parseRefId(item._attr["r:id"]),
+ };
+ });
+
+ return { headers, footers };
+ }
+
+ public titlePageIsDefined(xmlData: string): boolean {
+ const xmlObj = fastXmlParser.parse(xmlData, importParseOptions);
+ const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
+ return sectionProp["w:titlePg"] !== undefined;
+ }
+
+ public parseRefId(str: string): number {
+ const match = /^rId(\d+)$/.exec(str);
+ if (match === null) {
+ throw new Error("Invalid ref id");
+ }
+ return parseInt(match[1], 10);
+ }
+}
diff --git a/src/import-dotx/index.ts b/src/import-dotx/index.ts
new file mode 100644
index 0000000000..2b12220d83
--- /dev/null
+++ b/src/import-dotx/index.ts
@@ -0,0 +1 @@
+export * from "./import-dotx";
diff --git a/src/index.ts b/src/index.ts
index b6d3ecaf25..1e35c87d30 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,3 +3,4 @@
export { File as Document } from "./file";
export * from "./file";
export * from "./export";
+export * from "./import-dotx";