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";