diff --git a/.nycrc b/.nycrc index 7c878bae51..4cd54fc2c3 100644 --- a/.nycrc +++ b/.nycrc @@ -1,9 +1,9 @@ { "check-coverage": true, - "lines": 84.56, - "functions": 77.50, - "branches": 71.40, - "statements": 84.30, + "lines": 87.17, + "functions": 83.12, + "branches": 71.22, + "statements": 86.95, "include": [ "src/**/*.ts" ], diff --git a/package.json b/package.json index 32fdc7a467..c111fec200 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,10 @@ "dependencies": { "@types/image-size": "0.0.29", "@types/jszip": "^3.1.4", - "fast-xml-parser": "^3.3.6", "image-size": "^0.6.2", "jszip": "^3.1.5", - "xml": "^1.0.1" + "xml": "^1.0.1", + "xml-js": "^1.6.8" }, "author": "Dolan Miu", "license": "MIT", diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts index 9ba95f33d0..137ef298ba 100644 --- a/src/file/document/body/body.spec.ts +++ b/src/file/document/body/body.spec.ts @@ -21,7 +21,7 @@ describe("Body", () => { }); }); - describe("addSection", () => { + describe("#addSection", () => { it("should add section with options", () => { body.addSection({ width: 10000, @@ -39,5 +39,93 @@ describe("Body", () => { const newSection = formatted[1]["w:sectPr"]; expect(newSection[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 10000, "w:w": 10000, "w:orient": "portrait" } }] }); }); + + it("should add section with default parameters", () => { + body.addSection({ + width: 10000, + height: 10000, + }); + + const tree = new Formatter().format(body); + + expect(tree).to.deep.equal({ + "w:body": [ + { + "w:p": [ + { "w:pPr": [] }, + { + "w:pPr": [ + { + "w:sectPr": [ + { "w:pgSz": [{ _attr: { "w:w": 11906, "w:h": 16838, "w:orient": "portrait" } }] }, + { + "w:pgMar": [ + { + _attr: { + "w:top": 1440, + "w:right": 1440, + "w:bottom": 1440, + "w:left": 1440, + "w:header": 708, + "w:footer": 708, + "w:gutter": 0, + "w:mirrorMargins": false, + }, + }, + ], + }, + { "w:cols": [{ _attr: { "w:space": 708 } }] }, + { "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }, + { "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }, + ], + }, + ], + }, + ], + }, + { + "w:sectPr": [ + { "w:pgSz": [{ _attr: { "w:w": 10000, "w:h": 10000, "w:orient": "portrait" } }] }, + { + "w:pgMar": [ + { + _attr: { + "w:top": 1440, + "w:right": 1440, + "w:bottom": 1440, + "w:left": 1440, + "w:header": 708, + "w:footer": 708, + "w:gutter": 0, + "w:mirrorMargins": false, + }, + }, + ], + }, + { "w:cols": [{ _attr: { "w:space": 708 } }] }, + { "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }, + { "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }, + ], + }, + ], + }); + }); + }); + + describe("#getParagraphs", () => { + it("should get no paragraphs", () => { + const paragraphs = body.getParagraphs(); + + expect(paragraphs).to.be.an.instanceof(Array); + }); + }); + + describe("#DefaultSection", () => { + it("should get section", () => { + const section = body.DefaultSection; + + const tree = new Formatter().format(section); + expect(tree["w:sectPr"]).to.be.an.instanceof(Array); + }); }); }); diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index 1d19669588..cc8cd9c192 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -38,8 +38,6 @@ export class Body extends XmlComponent { public prepForXml(): IXmlableObject | undefined { if (this.sections.length === 1) { this.root.push(this.sections[0]); - } else if (this.sections.length > 1) { - throw new Error("Invalid usage of sections. At the end of the body element there must be ONE section."); } return super.prepForXml(); diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts index cf93b19f7b..a42d914243 100644 --- a/src/file/drawing/anchor/anchor.spec.ts +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -103,7 +103,7 @@ describe("Anchor", () => { assert.equal(graphic.rootKey, "a:graphic"); }); - it("should create a Drawing with text wrapping", () => { + it("should create a Drawing with square text wrapping", () => { anchor = createDrawing({ textWrapping: { textWrapStyle: TextWrapStyle.SQUARE, @@ -116,5 +116,44 @@ describe("Anchor", () => { const textWrap = newJson.root[6]; assert.equal(textWrap.rootKey, "wp:wrapSquare"); }); + + it("should create a Drawing with no text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.NONE, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapNone"); + }); + + it("should create a Drawing with tight text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.TIGHT, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapTight"); + }); + + it("should create a Drawing with tight text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.TOP_AND_BOTTOM, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapTopAndBottom"); + }); }); }); diff --git a/src/file/header-wrapper.spec.ts b/src/file/header-wrapper.spec.ts new file mode 100644 index 0000000000..4985887e33 --- /dev/null +++ b/src/file/header-wrapper.spec.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { HeaderWrapper } from "./header-wrapper"; +import { Media } from "./media"; +import { Paragraph } from "./paragraph"; +import { Table } from "./table"; + +describe("HeaderWrapper", () => { + describe("#addParagraph", () => { + it("should call the underlying header's addParagraph", () => { + const file = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(file.Header, "addParagraph"); + file.addParagraph(new Paragraph()); + + expect(spy.called).to.equal(true); + }); + }); + + describe("#addTable", () => { + it("should call the underlying header's addParagraph", () => { + const file = new HeaderWrapper(new Media(), 1); + const spy = sinon.spy(file.Header, "addTable"); + file.addTable(new Table(1, 1)); + + expect(spy.called).to.equal(true); + }); + }); +}); diff --git a/src/file/media/media.spec.ts b/src/file/media/media.spec.ts new file mode 100644 index 0000000000..b42529f1c3 --- /dev/null +++ b/src/file/media/media.spec.ts @@ -0,0 +1,112 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { File } from "../file"; +import { Media } from "./media"; + +describe("Media", () => { + describe("#addImage", () => { + it("Add image", () => { + // tslint:disable-next-line:no-any + const file = new File(); + const image = Media.addImage(file, ""); + + let tree = new Formatter().format(image.Paragraph); + expect(tree["w:p"]).to.be.an.instanceof(Array); + + tree = new Formatter().format(image.Run); + expect(tree["w:r"]).to.be.an.instanceof(Array); + }); + }); + + describe("#addMedia", () => { + it("should add media", () => { + // tslint:disable-next-line:no-any + (Media as any).generateId = () => "test"; + + const image = new Media().addMedia("", 1); + expect(image.fileName).to.equal("test.png"); + expect(image.referenceId).to.equal(1); + expect(image.dimensions).to.deep.equal({ + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 952500, + y: 952500, + }, + }); + }); + + it("should return UInt8Array if atob is present", () => { + // tslint:disable-next-line + ((process as any).atob as any) = () => "atob result"; + // tslint:disable-next-line:no-any + (Media as any).generateId = () => "test"; + + const image = new Media().addMedia("", 1); + expect(image.stream).to.be.an.instanceof(Uint8Array); + }); + }); + + describe("#getMedia", () => { + it("should get media", () => { + // tslint:disable-next-line:no-any + (Media as any).generateId = () => "test"; + + const media = new Media(); + media.addMedia("", 1); + + const image = media.getMedia("test.png"); + + expect(image.fileName).to.equal("test.png"); + expect(image.referenceId).to.equal(1); + expect(image.dimensions).to.deep.equal({ + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 952500, + y: 952500, + }, + }); + }); + + it("Get media", () => { + const media = new Media(); + + expect(() => media.getMedia("test.png")).to.throw(); + }); + }); + + describe("#Array", () => { + it("Get images as array", () => { + // tslint:disable-next-line:no-any + (Media as any).generateId = () => "test"; + + const media = new Media(); + media.addMedia("", 1); + + const array = media.Array; + expect(array).to.be.an.instanceof(Array); + expect(array.length).to.equal(1); + + const image = array[0]; + expect(image.fileName).to.equal("test.png"); + expect(image.referenceId).to.equal(1); + expect(image.dimensions).to.deep.equal({ + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 952500, + y: 952500, + }, + }); + }); + }); +}); diff --git a/src/file/paragraph/formatting/spacing.spec.ts b/src/file/paragraph/formatting/spacing.spec.ts index b6ba808866..ea4dafe1b8 100644 --- a/src/file/paragraph/formatting/spacing.spec.ts +++ b/src/file/paragraph/formatting/spacing.spec.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; -import { Spacing } from "./spacing"; +import { ContextualSpacing, Spacing } from "./spacing"; describe("Spacing", () => { describe("#constructor", () => { @@ -23,3 +23,23 @@ describe("Spacing", () => { }); }); }); + +describe("ContextualSpacing", () => { + describe("#constructor", () => { + it("should create", () => { + const spacing = new ContextualSpacing(true); + const tree = new Formatter().format(spacing); + expect(tree).to.deep.equal({ + "w:contextualSpacing": [{ _attr: { "w:val": 1 } }], + }); + }); + + it("should create with value of 0 if param is false", () => { + const spacing = new ContextualSpacing(false); + const tree = new Formatter().format(spacing); + expect(tree).to.deep.equal({ + "w:contextualSpacing": [{ _attr: { "w:val": 0 } }], + }); + }); + }); +}); diff --git a/src/file/styles/external-styles-factory.ts b/src/file/styles/external-styles-factory.ts index 301211bdc7..cbfa4a3c90 100644 --- a/src/file/styles/external-styles-factory.ts +++ b/src/file/styles/external-styles-factory.ts @@ -1,5 +1,5 @@ -import * as fastXmlParser from "fast-xml-parser"; -import { convertToXmlComponent, ImportedRootElementAttributes, parseOptions } from "file/xml-components"; +import { convertToXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent } from "file/xml-components"; +import { Element as XMLElement, xml2js } from "xml-js"; import { Styles } from "./"; export class ExternalStylesFactory { @@ -25,25 +25,24 @@ export class ExternalStylesFactory { * * @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)); + public newInstance(xmlData: string): Styles { + const xmlObj = xml2js(xmlData, { compact: false }) as XMLElement; - // convert other elements (not styles definitions, but default styles and so on ...) - Object.keys(xmlStyles) - .filter((element) => element !== "_attr" && element !== "w:style") - .forEach((element) => { - const converted = convertToXmlComponent(element, xmlStyles[element]); - if (Array.isArray(converted)) { - converted.forEach((c) => importedStyle.push(c)); - } else { - importedStyle.push(converted); - } - }); + let stylesXmlElement: XMLElement | undefined; + for (const xmlElm of xmlObj.elements || []) { + if (xmlElm.name === "w:styles") { + stylesXmlElement = xmlElm; + } + } + if (stylesXmlElement === undefined) { + throw new Error("can not find styles element"); + } - // convert the styles one by one - xmlStyles["w:style"].map((style) => convertToXmlComponent("w:style", style)).forEach(importedStyle.push.bind(importedStyle)); + const importedStyle = new Styles(new ImportedRootElementAttributes(stylesXmlElement.attributes)); + const stylesElements = stylesXmlElement.elements || []; + for (const childElm of stylesElements) { + importedStyle.push(convertToXmlComponent(childElm) as ImportedXmlComponent); + } return importedStyle; } } diff --git a/src/file/styles/sample/default-style.spec.ts b/src/file/styles/sample/default-style.spec.ts new file mode 100644 index 0000000000..899dd4f47e --- /dev/null +++ b/src/file/styles/sample/default-style.spec.ts @@ -0,0 +1,12 @@ +import { expect } from "chai"; + +import { DefaultStyle } from "./default-style"; + +describe("DefaultStyle", () => { + it("creates an initially empty property object", () => { + const style = DefaultStyle(); + expect(style).to.have.property("w:styles"); + expect(style["w:styles"].length).to.be.greaterThan(1); + expect(style["w:styles"][1]).to.have.property("w:docDefaults"); + }); +}); diff --git a/src/file/styles/sample/default-style.ts b/src/file/styles/sample/default-style.ts new file mode 100644 index 0000000000..29e2dea3cb --- /dev/null +++ b/src/file/styles/sample/default-style.ts @@ -0,0 +1,211 @@ +/* tslint:disable */ +function createLsdException(name, uiPriority, qFormat?, semiHidden?, unhideWhenUsed?) { + "use strict"; + + return [ + { + _attr: { + "w:name": name, + "w:uiPriority": uiPriority, + "w:qFormat": qFormat, + "w:semiHidden": semiHidden, + "w:unhideWhenUsed": unhideWhenUsed, + }, + }, + ]; +} + +export function DefaultStyle() { + var style = { + "w:styles": [ + { + _attr: { + "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", + "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", + "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", + "mc:Ignorable": "w14 w15", + }, + }, + { + "w:docDefaults": [ + { + "w:rPrDefault": [ + { + "w:rPr": [ + { + "w:rFonts": [ + { + _attr: { + "w:asciiTheme": "minorHAnsi", + "w:eastAsiaTheme": "minorHAnsi", + "w:hAnsiTheme": "minorHAnsi", + "w:cstheme": "minorBidi", + }, + }, + ], + }, + { + "w:sz": [ + { + _attr: { + "w:val": "22", + }, + }, + ], + }, + { + "w:szCs": [ + { + _attr: { + "w:val": "22", + }, + }, + ], + }, + { + "w:lang": [ + { + _attr: { + "w:val": "en-GB", + "w:eastAsia": "en-US", + "w:bidi": "ar-SA", + }, + }, + ], + }, + ], + }, + ], + }, + { + "w:pPrDefault": [ + { + "w:pPr": [ + { + "w:spacing": [ + { + _attr: { + "w:after": "160", + "w:line": "259", + "w:lineRule": "auto", + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + "w:latentStyles": [ + { + _attr: { + "w:defLockedState": "0", + "w:defUIPriority": "99", + "w:defSemiHidden": "0", + "w:defUnhideWhenUsed": "0", + "w:defQFormat": "0", + "w:count": "371", + }, + }, + { + "w:lsdException": createLsdException("Normal", 0, 1), + }, + { + "w:lsdException": createLsdException("heading 1", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 2", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 3", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 4", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 5", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 6", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 7", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 8", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("heading 9", 9, 1, 1, 1), + }, + { + "w:lsdException": createLsdException("index 1", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 2", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 3", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 4", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 5", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 6", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 7", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 8", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("index 9", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 1", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 2", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 3", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 4", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 5", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 6", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 7", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 8", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("toc 9", 39, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("Normal Indent", undefined, undefined, 1, 1), + }, + { + "w:lsdException": createLsdException("footnote text", undefined, undefined, 1, 1), + }, + ], + }, + ], + }; + + return style; +} diff --git a/src/file/styles/sample/index.ts b/src/file/styles/sample/index.ts index 29e2dea3cb..12a14b5a4d 100644 --- a/src/file/styles/sample/index.ts +++ b/src/file/styles/sample/index.ts @@ -1,211 +1 @@ -/* tslint:disable */ -function createLsdException(name, uiPriority, qFormat?, semiHidden?, unhideWhenUsed?) { - "use strict"; - - return [ - { - _attr: { - "w:name": name, - "w:uiPriority": uiPriority, - "w:qFormat": qFormat, - "w:semiHidden": semiHidden, - "w:unhideWhenUsed": unhideWhenUsed, - }, - }, - ]; -} - -export function DefaultStyle() { - var style = { - "w:styles": [ - { - _attr: { - "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006", - "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", - "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", - "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml", - "xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml", - "mc:Ignorable": "w14 w15", - }, - }, - { - "w:docDefaults": [ - { - "w:rPrDefault": [ - { - "w:rPr": [ - { - "w:rFonts": [ - { - _attr: { - "w:asciiTheme": "minorHAnsi", - "w:eastAsiaTheme": "minorHAnsi", - "w:hAnsiTheme": "minorHAnsi", - "w:cstheme": "minorBidi", - }, - }, - ], - }, - { - "w:sz": [ - { - _attr: { - "w:val": "22", - }, - }, - ], - }, - { - "w:szCs": [ - { - _attr: { - "w:val": "22", - }, - }, - ], - }, - { - "w:lang": [ - { - _attr: { - "w:val": "en-GB", - "w:eastAsia": "en-US", - "w:bidi": "ar-SA", - }, - }, - ], - }, - ], - }, - ], - }, - { - "w:pPrDefault": [ - { - "w:pPr": [ - { - "w:spacing": [ - { - _attr: { - "w:after": "160", - "w:line": "259", - "w:lineRule": "auto", - }, - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - "w:latentStyles": [ - { - _attr: { - "w:defLockedState": "0", - "w:defUIPriority": "99", - "w:defSemiHidden": "0", - "w:defUnhideWhenUsed": "0", - "w:defQFormat": "0", - "w:count": "371", - }, - }, - { - "w:lsdException": createLsdException("Normal", 0, 1), - }, - { - "w:lsdException": createLsdException("heading 1", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 2", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 3", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 4", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 5", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 6", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 7", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 8", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("heading 9", 9, 1, 1, 1), - }, - { - "w:lsdException": createLsdException("index 1", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 2", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 3", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 4", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 5", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 6", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 7", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 8", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("index 9", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 1", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 2", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 3", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 4", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 5", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 6", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 7", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 8", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("toc 9", 39, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("Normal Indent", undefined, undefined, 1, 1), - }, - { - "w:lsdException": createLsdException("footnote text", undefined, undefined, 1, 1), - }, - ], - }, - ], - }; - - return style; -} +export * from "./default-style"; diff --git a/src/file/table/table-cell/table-cell-components.ts b/src/file/table/table-cell/table-cell-components.ts index a81a66b3ae..7072654680 100644 --- a/src/file/table/table-cell/table-cell-components.ts +++ b/src/file/table/table-cell/table-cell-components.ts @@ -179,7 +179,7 @@ export class TableCellWidth extends XmlComponent { } } -interface ITableCellShadingAttributesProperties { +export interface ITableCellShadingAttributesProperties { fill?: string; color?: string; val?: string; @@ -197,7 +197,7 @@ class TableCellShadingAttributes extends XmlAttributeComponent { + describe("#constructor", () => { + it("creates an initially empty property object", () => { + const cellMargain = new TableCellProperties(); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [] }); + }); + }); + + describe("#addGridSpan", () => { + it("adds grid span", () => { + const cellMargain = new TableCellProperties(); + cellMargain.addGridSpan(1); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:gridSpan": [{ _attr: { "w:val": 1 } }] }] }); + }); + }); + + describe("#addVerticalMerge", () => { + it("adds vertical merge", () => { + const cellMargain = new TableCellProperties(); + cellMargain.addVerticalMerge(VMergeType.CONTINUE); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:vMerge": [{ _attr: { "w:val": "continue" } }] }] }); + }); + }); + + describe("#setVerticalAlign", () => { + it("sets vertical align", () => { + const cellMargain = new TableCellProperties(); + cellMargain.setVerticalAlign(VerticalAlign.BOTTOM); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:vAlign": [{ _attr: { "w:val": "bottom" } }] }] }); + }); + }); + + describe("#setWidth", () => { + it("sets width", () => { + const cellMargain = new TableCellProperties(); + cellMargain.setWidth(1, WidthType.DXA); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:tcW": [{ _attr: { "w:type": "dxa", "w:w": 1 } }] }] }); + }); + }); + + describe("#setShading", () => { + it("sets shading", () => { + const cellMargain = new TableCellProperties(); + cellMargain.setShading({ + fill: "test", + color: "000", + }); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:shd": [{ _attr: { "w:fill": "test", "w:color": "000" } }] }] }); + }); + }); +}); diff --git a/src/file/table/table-cell/table-cell-properties.ts b/src/file/table/table-cell/table-cell-properties.ts index 639541d4cd..1f653b53bb 100644 --- a/src/file/table/table-cell/table-cell-properties.ts +++ b/src/file/table/table-cell/table-cell-properties.ts @@ -2,6 +2,7 @@ import { XmlComponent } from "file/xml-components"; import { GridSpan, + ITableCellShadingAttributesProperties, TableCellBorders, TableCellShading, TableCellWidth, @@ -49,7 +50,7 @@ export class TableCellProperties extends XmlComponent { return this; } - public setShading(attrs: object): TableCellProperties { + public setShading(attrs: ITableCellShadingAttributesProperties): TableCellProperties { this.root.push(new TableCellShading(attrs)); return this; diff --git a/src/file/table/table-properties/table-cell-margin.spec.ts b/src/file/table/table-properties/table-cell-margin.spec.ts new file mode 100644 index 0000000000..82bc60b74b --- /dev/null +++ b/src/file/table/table-properties/table-cell-margin.spec.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { WidthType } from "../table-cell"; +import { TableCellMargin } from "./table-cell-margin"; + +describe("TableCellMargin", () => { + describe("#constructor", () => { + it("should throw an error if theres no child elements", () => { + const cellMargain = new TableCellMargin(); + expect(() => new Formatter().format(cellMargain)).to.throw(); + }); + }); + + describe("#addTopMargin", () => { + it("adds a table cell top margin", () => { + const cellMargain = new TableCellMargin(); + cellMargain.addTopMargin(1234, WidthType.DXA); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:top": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }); + }); + }); + + describe("#addLeftMargin", () => { + it("adds a table cell left margin", () => { + const cellMargain = new TableCellMargin(); + cellMargain.addLeftMargin(1234, WidthType.DXA); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:left": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }); + }); + }); + + describe("#addBottomMargin", () => { + it("adds a table cell bottom margin", () => { + const cellMargain = new TableCellMargin(); + cellMargain.addBottomMargin(1234, WidthType.DXA); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:bottom": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }); + }); + }); + + describe("#addRightMargin", () => { + it("adds a table cell right margin", () => { + const cellMargain = new TableCellMargin(); + cellMargain.addRightMargin(1234, WidthType.DXA); + const tree = new Formatter().format(cellMargain); + expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:right": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }); + }); + }); +}); diff --git a/src/file/table/table-properties/table-properties.spec.ts b/src/file/table/table-properties/table-properties.spec.ts index 1ffa43e6ff..8b9656d643 100644 --- a/src/file/table/table-properties/table-properties.spec.ts +++ b/src/file/table/table-properties/table-properties.spec.ts @@ -43,5 +43,14 @@ describe("TableProperties", () => { "w:tblPr": [{ "w:tblCellMar": [{ "w:top": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }], }); }); + + it("adds a table cell left margin", () => { + const tp = new TableProperties(); + tp.CellMargin.addLeftMargin(1234, WidthType.DXA); + const tree = new Formatter().format(tp); + expect(tree).to.deep.equal({ + "w:tblPr": [{ "w:tblCellMar": [{ "w:left": [{ _attr: { "w:sz": "dxa", "w:w": 1234 } }] }] }], + }); + }); }); }); diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts index beed8cf09a..e896633850 100644 --- a/src/file/xml-components/imported-xml-component.spec.ts +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -1,5 +1,8 @@ import { expect } from "chai"; -import { convertToXmlComponent, ImportedXmlComponent } from "./"; +import { Element, xml2js } from "xml-js"; + +import { ImportedXmlComponent } from "./"; +import { convertToXmlComponent } from "./imported-xml-component"; const xmlString = ` @@ -15,39 +18,21 @@ const xmlString = ` `; -// tslint:disable:object-literal-key-quotes -const importedXmlElement = { - "w:p": { - _attr: { "w:one": "value 1", "w:two": "value 2" }, - "w:rPr": { "w:noProof": "some value" }, - "w:r": [{ _attr: { active: "true" }, "w:t": "Text 1" }, { _attr: { active: "true" }, "w:t": "Text 2" }], - }, -}; -// tslint:enable:object-literal-key-quotes - const convertedXmlElement = { deleted: false, - rootKey: "w:p", root: [ { deleted: false, - rootKey: "w:rPr", - root: [{ deleted: false, rootKey: "w:noProof", root: ["some value"] }], - }, - { - deleted: false, - rootKey: "w:r", - root: [{ deleted: false, rootKey: "w:t", root: ["Text 1"] }], - _attr: { active: "true" }, - }, - { - deleted: false, - rootKey: "w:r", - root: [{ deleted: false, rootKey: "w:t", root: ["Text 2"] }], - _attr: { active: "true" }, + rootKey: "w:p", + root: [ + { deleted: false, rootKey: "w:rPr", root: [{ deleted: false, rootKey: "w:noProof", root: ["some value"] }] }, + { deleted: false, rootKey: "w:r", root: [{ deleted: false, rootKey: "w:t", root: ["Text 1"] }], _attr: { active: "true" } }, + { deleted: false, rootKey: "w:r", root: [{ deleted: false, rootKey: "w:t", root: ["Text 2"] }], _attr: { active: "true" } }, + ], + _attr: { "w:one": "value 1", "w:two": "value 2" }, }, ], - _attr: { "w:one": "value 1", "w:two": "value 2" }, + rootKey: undefined, }; describe("ImportedXmlComponent", () => { @@ -88,7 +73,8 @@ describe("ImportedXmlComponent", () => { describe("convertToXmlComponent", () => { it("should convert to xml component", () => { - const converted = convertToXmlComponent("w:p", importedXmlElement["w:p"]); + const xmlObj = xml2js(xmlString, { compact: false }) as Element; + const converted = convertToXmlComponent(xmlObj); expect(converted).to.eql(convertedXmlElement); }); }); diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts index eb318a3d64..7485c651e0 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -1,53 +1,30 @@ // tslint:disable:no-any -import * as fastXmlParser from "fast-xml-parser"; -import { flatMap } from "lodash"; +import { Element as XmlElement, xml2js } from "xml-js"; import { IXmlableObject, XmlComponent } from "."; -export const parseOptions = { - ignoreAttributes: false, - attributeNamePrefix: "", - attrNodeName: "_attr", -}; - /** * Converts the given xml element (in json format) into XmlComponent. - * Note: If element is array, them it will return ImportedXmlComponent[]. Example for given: - * element = [ - * { w:t: "val 1"}, - * { w:t: "val 2"} - * ] - * will return - * [ - * ImportedXmlComponent { rootKey: "w:t", root: [ "val 1" ]}, - * ImportedXmlComponent { rootKey: "w:t", root: [ "val 2" ]} - * ] - * - * @param elementName name (rootKey) of the XmlComponent * @param element the xml element in json presentation */ -export function convertToXmlComponent(elementName: string, element: any): ImportedXmlComponent | ImportedXmlComponent[] { - const xmlElement = new ImportedXmlComponent(elementName, element._attr); - if (Array.isArray(element)) { - const out: any[] = []; - element.forEach((itemInArray) => { - out.push(convertToXmlComponent(elementName, itemInArray)); - }); - return flatMap(out); - } else if (typeof element === "object") { - Object.keys(element) - .filter((key) => key !== "_attr") - .map((item) => convertToXmlComponent(item, element[item])) - .forEach((converted) => { - if (Array.isArray(converted)) { - converted.forEach(xmlElement.push.bind(xmlElement)); - } else { - xmlElement.push(converted); + +export function convertToXmlComponent(element: XmlElement): ImportedXmlComponent | string | undefined { + switch (element.type) { + case undefined: + case "element": + const xmlComponent = new ImportedXmlComponent(element.name as string, element.attributes); + const childElments = element.elements || []; + for (const childElm of childElments) { + const child = convertToXmlComponent(childElm); + if (child !== undefined) { + xmlComponent.push(child); } - }); - } else if (element !== "") { - xmlElement.push(element); + } + return xmlComponent; + case "text": + return element.text as string; + default: + return undefined; } - return xmlElement; } /** @@ -60,16 +37,14 @@ export class ImportedXmlComponent extends XmlComponent { * @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; + const xmlObj = xml2js(importedContent, { compact: false }) as XmlElement; + return convertToXmlComponent(xmlObj) as ImportedXmlComponent; } + /** + * Converts the xml string to a XmlComponent tree. + * + * @param importedContent xml content of the imported component + */ // tslint:disable-next-line:variable-name private readonly _attr: any; @@ -123,7 +98,7 @@ export class ImportedXmlComponent extends XmlComponent { return result; } - public push(xmlComponent: XmlComponent): void { + public push(xmlComponent: XmlComponent | string): void { this.root.push(xmlComponent); } } diff --git a/src/file/xml-components/xmlable-object.ts b/src/file/xml-components/xmlable-object.ts index 255df14fc2..33d41e2af1 100644 --- a/src/file/xml-components/xmlable-object.ts +++ b/src/file/xml-components/xmlable-object.ts @@ -1,5 +1,8 @@ +export interface IXmlAttribute { + [key: string]: string | number | boolean; +} export interface IXmlableObject extends Object { - _attr?: { [key: string]: string | number | boolean }; + _attr?: IXmlAttribute; } // Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 diff --git a/src/import-dotx/import-dotx.spec.ts b/src/import-dotx/import-dotx.spec.ts new file mode 100644 index 0000000000..f89131abf5 --- /dev/null +++ b/src/import-dotx/import-dotx.spec.ts @@ -0,0 +1,26 @@ +import { expect } from "chai"; + +import { ImportDotx } from "./import-dotx"; + +describe("ImportDotx", () => { + describe("#constructor", () => { + it("should create", () => { + const file = new ImportDotx(); + + expect(file).to.deep.equal({ currentRelationshipId: 1 }); + }); + }); + + // describe("#extract", () => { + // it("should create", async () => { + // const file = new ImportDotx(); + // const filePath = "./demo/dotx/template.dotx"; + + // const templateDocument = await file.extract(data); + + // await file.extract(data); + + // expect(templateDocument).to.be.equal({ currentRelationshipId: 1 }); + // }); + // }); +}); diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts index 1d87c71e23..0b3ce692e2 100644 --- a/src/import-dotx/import-dotx.ts +++ b/src/import-dotx/import-dotx.ts @@ -1,20 +1,15 @@ -import * as fastXmlParser from "fast-xml-parser"; import * as JSZip from "jszip"; +import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js"; 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 { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components"; + 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", @@ -69,35 +64,48 @@ export class ImportDotx { const headers: IDocumentHeader[] = []; for (const headerRef of documentRefs.headers) { - const headerKey = "w:hdr"; - const relationshipFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); - if (!relationshipFileInfo) { + const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); + if (relationFileInfo === null || !relationFileInfo) { 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 xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); + const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; + let headerXmlElement: XMLElement | undefined; + for (const xmlElm of xmlObj.elements || []) { + if (xmlElm.name === "w:hdr") { + headerXmlElement = xmlElm; + } + } + if (headerXmlElement === undefined) { + continue; + } + const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent; const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationshipFileInfo, zipContent, header); + await this.addRelationToWrapper(relationFileInfo, 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) { + const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); + if (relationFileInfo === null || !relationFileInfo) { 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 xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text"); + const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; + let footerXmlElement: XMLElement | undefined; + for (const xmlElm of xmlObj.elements || []) { + if (xmlElm.name === "w:ftr") { + footerXmlElement = xmlElm; + } + } + if (footerXmlElement === undefined) { + continue; + } + const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent; const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); - await this.addRelationToWrapper(relationshipFileInfo, zipContent, footer); + await this.addRelationToWrapper(relationFileInfo, zipContent, footer); footers.push({ type: footerRef.type, footer }); } @@ -134,16 +142,19 @@ export class ImportDotx { } public findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { - const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); - const relationshipXmlArray = Array.isArray(xmlObj.Relationships.Relationship) + const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; + const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship) ? xmlObj.Relationships.Relationship : [xmlObj.Relationships.Relationship]; - const relationships: IRelationshipFileInfo[] = relationshipXmlArray - .map((item) => { + const relationships: IRelationshipFileInfo[] = relationXmlArray + .map((item: XMLElementCompact) => { + if (item._attributes === undefined) { + throw Error("relationship element has no attributes"); + } return { - id: this.parseRefId(item._attr.Id), - type: schemeToType[item._attr.Type], - target: item._attr.Target, + id: this.parseRefId(item._attributes.Id as string), + type: schemeToType[item._attributes.Type as string], + target: item._attributes.Target as string, }; }) .filter((item) => item.type !== null); @@ -151,14 +162,11 @@ export class ImportDotx { } public extractDocumentRefs(xmlData: string): IDocumentRefs { - interface IAttributedXML { - _attr: object; - } - const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; - const headerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:headerReference"]; - let headersXmlArray: IAttributedXML[]; + const headerProps: XMLElementCompact = sectionProp["w:headerReference"]; + let headersXmlArray: XMLElementCompact[]; if (headerProps === undefined) { headersXmlArray = []; } else if (Array.isArray(headerProps)) { @@ -167,14 +175,17 @@ export class ImportDotx { headersXmlArray = [headerProps]; } const headers = headersXmlArray.map((item) => { + if (item._attributes === undefined) { + throw Error("header referecne element has no attributes"); + } return { - type: item._attr["w:type"], - id: this.parseRefId(item._attr["r:id"]), + type: item._attributes["w:type"] as HeaderReferenceType, + id: this.parseRefId(item._attributes["r:id"] as string), }; }); - const footerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:footerReference"]; - let footersXmlArray: IAttributedXML[]; + const footerProps: XMLElementCompact = sectionProp["w:footerReference"]; + let footersXmlArray: XMLElementCompact[]; if (footerProps === undefined) { footersXmlArray = []; } else if (Array.isArray(footerProps)) { @@ -184,9 +195,12 @@ export class ImportDotx { } const footers = footersXmlArray.map((item) => { + if (item._attributes === undefined) { + throw Error("footer referecne element has no attributes"); + } return { - type: item._attr["w:type"], - id: this.parseRefId(item._attr["r:id"]), + type: item._attributes["w:type"] as FooterReferenceType, + id: this.parseRefId(item._attributes["r:id"] as string), }; }); @@ -194,7 +208,7 @@ export class ImportDotx { } public titlePageIsDefined(xmlData: string): boolean { - const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; return sectionProp["w:titlePg"] !== undefined; }