diff --git a/package.json b/package.json
index 639d230273..3edbf6d1ae 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"@types/express": "^4.0.35",
"@types/image-size": "0.0.29",
"archiver": "^2.1.1",
+ "fast-xml-parser": "^3.3.6",
"image-size": "^0.6.2",
"xml": "^1.0.1"
},
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 81cc8405fb..e7e14c207d 100644
--- a/src/file/file.ts
+++ b/src/file/file.ts
@@ -11,6 +11,7 @@ import { Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Styles } from "./styles";
import { DefaultStylesFactory } from "./styles/factory";
+import { ExternalStylesFactory } from "./styles/external-styles-factory";
import { Table } from "./table";
export class File {
@@ -28,8 +29,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 +38,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();
diff --git a/src/file/index.ts b/src/file/index.ts
index aae389e50a..3d86dafdb2 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";
\ No newline at end of file
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..802b6c8bb0
--- /dev/null
+++ b/src/file/styles/external-styles-factory.spec.ts
@@ -0,0 +1,147 @@
+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({
+ root: [],
+ rootKey: "w:docDefaults",
+ });
+ expect(importedStyle.root[2]).to.eql({
+ _attr: {
+ "w:defLockedState": "1",
+ "w:defUIPriority": "99",
+ },
+ 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",
+ },
+ root: [
+ {
+ _attr: {
+ "w:val": "Normal",
+ },
+ root: [],
+ rootKey: "w:name",
+ },
+ {
+ root: [],
+ rootKey: "w:qFormat",
+ },
+ ],
+ rootKey: "w:style",
+ });
+
+ expect(importedStyle.root[4]).to.eql({
+ _attr: {
+ "w:styleId": "Heading1",
+ "w:type": "paragraph",
+ },
+ root: [
+ {
+ _attr: {
+ "w:val": "heading 1",
+ },
+ root: [],
+ rootKey: "w:name",
+ },
+ {
+ _attr: {
+ "w:val": "Normal",
+ },
+ root: [],
+ rootKey: "w:basedOn",
+ },
+ {
+ root: [
+ {
+ root: [],
+ rootKey: "w:keepNext",
+ },
+ {
+ root: [],
+ rootKey: "w:keepLines",
+ },
+ {
+ root: [
+ {
+ _attr: {
+ "w:color": "auto",
+ "w:space": "1",
+ "w:sz": "4",
+ "w:val": "single",
+ },
+ 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..703f017922
--- /dev/null
+++ b/src/file/styles/external-styles-factory.ts
@@ -0,0 +1,64 @@
+import { Styles } from "./";
+import * as fastXmlParser from "fast-xml-parser";
+import { ImportedXmlComponent, ImportedRootElementAttributes } 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;
+ }
+
+ 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 0b5faf048d..d7016cb0a5 100644
--- a/src/file/styles/factory.ts
+++ b/src/file/styles/factory.ts
@@ -1,7 +1,8 @@
import { Color, Italics, Size } from "../paragraph/run/formatting";
import { Styles } from "./";
-// import { DocumentDefaults } from "./defaults";
+import { DocumentAttributes } from "../document/document-attributes";
+
import {
Heading1Style,
Heading2Style,
@@ -15,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..5b0bd1b063 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 { XmlComponent, BaseXmlComponent } 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/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..51af733c2e
--- /dev/null
+++ b/src/file/xml-components/imported-xml-component.ts
@@ -0,0 +1,71 @@
+import { XmlComponent, IXmlableObject } 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"
+ * }
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ */
+ 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;
+ }
+
+ push(xmlComponent: XmlComponent) {
+ 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..85e7e383f7 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";