Merge pull request #3 from h4buli/feature/import-styles
styles: support for external styles. parsing external styles.
This commit is contained in:
@ -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"
|
||||
},
|
||||
|
@ -10,6 +10,7 @@ export interface IPropertiesOptions {
|
||||
description?: string;
|
||||
lastModifiedBy?: string;
|
||||
revision?: string;
|
||||
externalStyles?: string;
|
||||
}
|
||||
|
||||
export class CoreProperties extends XmlComponent {
|
||||
|
@ -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();
|
||||
|
@ -5,3 +5,4 @@ export * from "./numbering";
|
||||
export * from "./media";
|
||||
export * from "./drawing";
|
||||
export * from "./styles";
|
||||
export * from "./xml-components";
|
147
src/file/styles/external-styles-factory.spec.ts
Normal file
147
src/file/styles/external-styles-factory.spec.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { expect } from "chai";
|
||||
|
||||
import { ExternalStylesFactory } from "./external-styles-factory";
|
||||
|
||||
describe("External styles factory", () => {
|
||||
let externalStyles;
|
||||
|
||||
beforeEach(() => {
|
||||
externalStyles = `
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:styles xmlns:mc="first" xmlns:r="second">
|
||||
<w:docDefaults>
|
||||
</w:docDefaults>
|
||||
|
||||
<w:latentStyles w:defLockedState="1" w:defUIPriority="99">
|
||||
</w:latentStyles>
|
||||
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||||
<w:name w:val="Normal"/>
|
||||
<w:qFormat/>
|
||||
</w:style>
|
||||
|
||||
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||
<w:name w:val="heading 1"/>
|
||||
<w:basedOn w:val="Normal"/>
|
||||
<w:pPr>
|
||||
<w:keepNext/>
|
||||
<w:keepLines/>
|
||||
|
||||
<w:pBdr>
|
||||
<w:bottom w:val="single" w:sz="4" w:space="1" w:color="auto"/>
|
||||
</w:pBdr>
|
||||
</w:pPr>
|
||||
</w:style>
|
||||
</w:styles>`;
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
64
src/file/styles/external-styles-factory.ts
Normal file
64
src/file/styles/external-styles-factory.ts
Normal file
@ -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:
|
||||
* <?xml version="1.0">
|
||||
* <w:styles xmlns:mc="some schema" ...>
|
||||
*
|
||||
* <w:style w:type="paragraph" w:styleId="Heading1">
|
||||
* <w:name w:val="heading 1"/>
|
||||
* .....
|
||||
* </w:style>
|
||||
*
|
||||
* <w:style w:type="paragraph" w:styleId="Heading2">
|
||||
* <w:name w:val="heading 2"/>
|
||||
* .....
|
||||
* </w:style>
|
||||
*
|
||||
* <w:docDefaults>Or any other element will be parsed to</w:docDefaults>
|
||||
*
|
||||
* </w:styles>
|
||||
* @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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
34
src/file/xml-components/imported-xml-component.spec.ts
Normal file
34
src/file/xml-components/imported-xml-component.spec.ts
Normal file
@ -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": [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
71
src/file/xml-components/imported-xml-component.ts
Normal file
71
src/file/xml-components/imported-xml-component.ts
Normal file
@ -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 someAttr="1" otherAttr="11">
|
||||
* <w:child childAttr="2">
|
||||
* </w:child>
|
||||
* </w:someKey>
|
||||
* {
|
||||
* '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,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export * from "./xml-component";
|
||||
export * from "./attributes";
|
||||
export * from "./default-attributes";
|
||||
export * from './imported-xml-component';
|
||||
export * from "./xmlable-object";
|
||||
|
Reference in New Issue
Block a user