From 40251a76f63a2e2421d7e531a802e366cc067e51 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Wed, 21 Mar 2018 10:46:22 +0100 Subject: [PATCH 01/20] setup: rename npm package --- package.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 72d3c057a0..4fe76ee5f5 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "docx", + "name": "docx-h4", "version": "3.2.0", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", @@ -24,7 +24,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/dolanmiu/docx.git" + "url": "git+https://github.com:h4buli/docx.git" }, "keywords": [ "docx", @@ -50,12 +50,8 @@ "request-promise": "^4.2.2", "xml": "^1.0.1" }, - "author": "Dolan Miu", + "author": "Igor Bulovski", "license": "MIT", - "bugs": { - "url": "https://github.com/dolanmiu/docx/issues" - }, - "homepage": "https://github.com/dolanmiu/docx#readme", "devDependencies": { "@types/chai": "^3.4.35", "@types/mocha": "^2.2.39", From 114c429aed62d4af54ed0c87ebb203296926f945 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Wed, 21 Mar 2018 10:53:07 +0100 Subject: [PATCH 02/20] pdf-export: remove option to export to pdf --- package.json | 3 -- src/export/packer/local.spec.ts | 9 ------ src/export/packer/local.ts | 26 ------------------ src/export/packer/pdf-convert-wrapper.ts | 35 ------------------------ 4 files changed, 73 deletions(-) delete mode 100644 src/export/packer/pdf-convert-wrapper.ts diff --git a/package.json b/package.json index 4fe76ee5f5..eab1220339 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,8 @@ "@types/archiver": "^2.1.0", "@types/express": "^4.0.35", "@types/image-size": "0.0.29", - "@types/request-promise": "^4.1.41", "archiver": "^2.1.1", "image-size": "^0.6.2", - "request": "^2.83.0", - "request-promise": "^4.2.2", "xml": "^1.0.1" }, "author": "Igor Bulovski", diff --git a/src/export/packer/local.spec.ts b/src/export/packer/local.spec.ts index 16fb99705b..e678c2939f 100644 --- a/src/export/packer/local.spec.ts +++ b/src/export/packer/local.spec.ts @@ -30,13 +30,4 @@ describe("LocalPacker", () => { fs.statSync("build/tests/test.docx"); }); }); - - describe("#packPdf", () => { - it("should create a standard PDF file", async function() { - this.timeout(99999999); - - await packer.packPdf("build/tests/pdf-test"); - fs.statSync("build/tests/pdf-test.pdf"); - }); - }); }); diff --git a/src/export/packer/local.ts b/src/export/packer/local.ts index 5d0c05fe81..b8c88b1b65 100644 --- a/src/export/packer/local.ts +++ b/src/export/packer/local.ts @@ -1,19 +1,14 @@ import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; import { File } from "../../file"; import { Compiler } from "./compiler"; import { IPacker } from "./packer"; -import { PdfConvertWrapper } from "./pdf-convert-wrapper"; export class LocalPacker implements IPacker { private stream: fs.WriteStream; - private readonly pdfConverter: PdfConvertWrapper; private readonly packer: Compiler; constructor(file: File) { - this.pdfConverter = new PdfConvertWrapper(); this.packer = new Compiler(file); } @@ -23,25 +18,4 @@ export class LocalPacker implements IPacker { this.stream = fs.createWriteStream(`${filePath}.docx`); await this.packer.compile(this.stream); } - - public async packPdf(filePath: string): Promise { - filePath = filePath.replace(/.pdf$/, ""); - - const fileName = path.basename(filePath, path.extname(filePath)); - const tempPath = path.join(os.tmpdir(), `${fileName}.docx`); - this.stream = fs.createWriteStream(tempPath); - await this.packer.compile(this.stream); - const text = await this.pdfConverter.convert(tempPath); - // const writeFile = util.promisify(fs.writeFile); --use this in future, in 3 years time. Only in node 8 - // return writeFile(`${filePath}.pdf`, text); - return new Promise((resolve, reject) => { - fs.writeFile(`${filePath}.pdf`, text, (err) => { - if (err) { - reject(err); - return; - } - resolve(); - }); - }); - } } diff --git a/src/export/packer/pdf-convert-wrapper.ts b/src/export/packer/pdf-convert-wrapper.ts deleted file mode 100644 index 5067082d12..0000000000 --- a/src/export/packer/pdf-convert-wrapper.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* tslint:disable:object-literal-key-quotes */ -// This tslint disable is needed, or it simply won't work -import * as fs from "fs"; -import * as request from "request-promise"; - -export interface IConvertOutput { - data: string; -} - -export class PdfConvertWrapper { - public convert(filePath: string): request.RequestPromise { - return request.post({ - url: "http://mirror1.convertonlinefree.com", - encoding: null, - headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36", - }, - formData: { - __EVENTTARGET: "", - __EVENTARGUMENT: "", - __VIEWSTATE: "", - ctl00$MainContent$fu: { - value: fs.readFileSync(filePath), - options: { - filename: "output.docx", - contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }, - }, - ctl00$MainContent$btnConvert: "Convert", - ctl00$MainContent$fuZip: "", - }, - }); - } -} From a0e00b8eff8c211e75d4433ea9000a38a5c249aa Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Fri, 23 Mar 2018 12:18:31 +0100 Subject: [PATCH 03/20] (styles): add support to provide external styles (as complete file content) --- package.json | 1 + src/export/packer/compiler.spec.ts | 65 ++++++++++++++++++++++++++++++ src/export/packer/compiler.ts | 11 ++++- src/file/file.ts | 9 +++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/export/packer/compiler.spec.ts diff --git a/package.json b/package.json index eab1220339..e381d6b8ab 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "awesome-typescript-loader": "^3.4.1", "chai": "^3.5.0", "glob": "^7.1.2", + "jszip": "^3.1.5", "mocha": "^3.2.0", "mocha-webpack": "^1.0.1", "prettier": "^1.10.2", diff --git a/src/export/packer/compiler.spec.ts b/src/export/packer/compiler.spec.ts new file mode 100644 index 0000000000..dcdbb2afc6 --- /dev/null +++ b/src/export/packer/compiler.spec.ts @@ -0,0 +1,65 @@ +/* tslint:disable:typedef space-before-function-paren */ +import * as fs from "fs"; +import * as os from 'os'; +import { expect } from "chai"; + +import { File, Paragraph } from "../../file"; +import {Compiler} from './compiler'; +import * as jszip from 'jszip'; + +async function getDocxXmlFileContent(filePath: string, xmlFileName: string): Promise { + let zipFile = fs.readFileSync(filePath); + const zipData = await jszip.loadAsync(zipFile).then(zip => zip); + return zipData.files[xmlFileName].async('text'); +} + +describe("compiler", () => { + let compiler: Compiler; + let file: File; + let externalStyles: string; + + beforeEach(() => { + file = new File({ + creator: "Dolan Miu", + revision: "1", + lastModifiedBy: "Dolan Miu", + }); + const paragraph = new Paragraph("test text"); + const heading = new Paragraph("Hello world").heading1(); + file.addParagraph(new Paragraph("title").title()); + file.addParagraph(heading); + file.addParagraph(new Paragraph("heading 2").heading2()); + file.addParagraph(paragraph); + + file.Styles.createParagraphStyle("testStyle").basedOn("Normal").bold(); + + externalStyles = "Some external styles"; + file.setExternalStyles(externalStyles); + + compiler = new Compiler(file); + }); + + describe("#compile()", () => { + + it("should use document styles when they are no external styles provided", async function() { + file.setExternalStyles(''); + const filePath = `${os.tmpdir()}/test-compile.zip`; + let stream = fs.createWriteStream(filePath); + + await compiler.compile(stream); + + const styles = await getDocxXmlFileContent(filePath, 'word/styles.xml') + expect(styles).not.to.equal(externalStyles); + }); + + it("should use provided external styles", async function() { + const filePath = `${os.tmpdir()}/test-compile.zip`; + let stream = fs.createWriteStream(filePath); + + await compiler.compile(stream); + + const styles = await getDocxXmlFileContent(filePath, 'word/styles.xml') + expect(styles).to.equal(externalStyles); + }); + }); +}); diff --git a/src/export/packer/compiler.ts b/src/export/packer/compiler.ts index 44c7817249..935c0a354b 100644 --- a/src/export/packer/compiler.ts +++ b/src/export/packer/compiler.ts @@ -23,7 +23,7 @@ export class Compiler { this.archive.pipe(output); const xmlDocument = xml(this.formatter.format(this.file.Document), true); - const xmlStyles = xml(this.formatter.format(this.file.Styles)); + const xmlStyles = this.resolveStyles(); const xmlProperties = xml(this.formatter.format(this.file.CoreProperties), { declaration: { standalone: "yes", @@ -102,4 +102,13 @@ export class Compiler { }); }); } + + private resolveStyles(): string { + if (this.file.ExternalStyles) { + return this.file.ExternalStyles; + } else { + return xml(this.formatter.format(this.file.Styles)); + } + + } } diff --git a/src/file/file.ts b/src/file/file.ts index 81cc8405fb..f3aab5d24f 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -25,6 +25,8 @@ export class File { private readonly footerWrapper: FooterWrapper; private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; + private externalStyles: string; + constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { this.document = new Document(sectionPropertiesOptions); @@ -111,6 +113,10 @@ export class File { this.document.createDrawing(mediaData); } + public setExternalStyles(styles: string): void { + this.externalStyles = styles; + } + public get Document(): Document { return this.document; } @@ -154,4 +160,7 @@ export class File { public get AppProperties(): AppProperties { return this.appProperties; } + public get ExternalStyles(): string { + return this.externalStyles; + } } From ce306aef0768a524dc729053e50eee7fc0eeac10 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Fri, 23 Mar 2018 12:25:30 +0100 Subject: [PATCH 04/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e381d6b8ab..191127d18c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.0", + "version": "3.2.1", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From 3fb563f9c83e9f1fee51376d56d7bd1ae604005b Mon Sep 17 00:00:00 2001 From: h4buli <34742290+h4buli@users.noreply.github.com> Date: Mon, 26 Mar 2018 16:28:40 +0200 Subject: [PATCH 05/20] =?UTF-8?q?Revert=20"(styles):=20add=20support=20to?= =?UTF-8?q?=20provide=20external=20styles=20(as=20complete=20file=20co?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - src/export/packer/compiler.spec.ts | 65 ------------------------------ src/export/packer/compiler.ts | 11 +---- src/file/file.ts | 9 ----- 4 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 src/export/packer/compiler.spec.ts diff --git a/package.json b/package.json index 191127d18c..639d230273 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "awesome-typescript-loader": "^3.4.1", "chai": "^3.5.0", "glob": "^7.1.2", - "jszip": "^3.1.5", "mocha": "^3.2.0", "mocha-webpack": "^1.0.1", "prettier": "^1.10.2", diff --git a/src/export/packer/compiler.spec.ts b/src/export/packer/compiler.spec.ts deleted file mode 100644 index dcdbb2afc6..0000000000 --- a/src/export/packer/compiler.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* tslint:disable:typedef space-before-function-paren */ -import * as fs from "fs"; -import * as os from 'os'; -import { expect } from "chai"; - -import { File, Paragraph } from "../../file"; -import {Compiler} from './compiler'; -import * as jszip from 'jszip'; - -async function getDocxXmlFileContent(filePath: string, xmlFileName: string): Promise { - let zipFile = fs.readFileSync(filePath); - const zipData = await jszip.loadAsync(zipFile).then(zip => zip); - return zipData.files[xmlFileName].async('text'); -} - -describe("compiler", () => { - let compiler: Compiler; - let file: File; - let externalStyles: string; - - beforeEach(() => { - file = new File({ - creator: "Dolan Miu", - revision: "1", - lastModifiedBy: "Dolan Miu", - }); - const paragraph = new Paragraph("test text"); - const heading = new Paragraph("Hello world").heading1(); - file.addParagraph(new Paragraph("title").title()); - file.addParagraph(heading); - file.addParagraph(new Paragraph("heading 2").heading2()); - file.addParagraph(paragraph); - - file.Styles.createParagraphStyle("testStyle").basedOn("Normal").bold(); - - externalStyles = "Some external styles"; - file.setExternalStyles(externalStyles); - - compiler = new Compiler(file); - }); - - describe("#compile()", () => { - - it("should use document styles when they are no external styles provided", async function() { - file.setExternalStyles(''); - const filePath = `${os.tmpdir()}/test-compile.zip`; - let stream = fs.createWriteStream(filePath); - - await compiler.compile(stream); - - const styles = await getDocxXmlFileContent(filePath, 'word/styles.xml') - expect(styles).not.to.equal(externalStyles); - }); - - it("should use provided external styles", async function() { - const filePath = `${os.tmpdir()}/test-compile.zip`; - let stream = fs.createWriteStream(filePath); - - await compiler.compile(stream); - - const styles = await getDocxXmlFileContent(filePath, 'word/styles.xml') - expect(styles).to.equal(externalStyles); - }); - }); -}); diff --git a/src/export/packer/compiler.ts b/src/export/packer/compiler.ts index 935c0a354b..44c7817249 100644 --- a/src/export/packer/compiler.ts +++ b/src/export/packer/compiler.ts @@ -23,7 +23,7 @@ export class Compiler { this.archive.pipe(output); const xmlDocument = xml(this.formatter.format(this.file.Document), true); - const xmlStyles = this.resolveStyles(); + const xmlStyles = xml(this.formatter.format(this.file.Styles)); const xmlProperties = xml(this.formatter.format(this.file.CoreProperties), { declaration: { standalone: "yes", @@ -102,13 +102,4 @@ export class Compiler { }); }); } - - private resolveStyles(): string { - if (this.file.ExternalStyles) { - return this.file.ExternalStyles; - } else { - return xml(this.formatter.format(this.file.Styles)); - } - - } } diff --git a/src/file/file.ts b/src/file/file.ts index f3aab5d24f..81cc8405fb 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -25,8 +25,6 @@ export class File { private readonly footerWrapper: FooterWrapper; private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; - private externalStyles: string; - constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { this.document = new Document(sectionPropertiesOptions); @@ -113,10 +111,6 @@ export class File { this.document.createDrawing(mediaData); } - public setExternalStyles(styles: string): void { - this.externalStyles = styles; - } - public get Document(): Document { return this.document; } @@ -160,7 +154,4 @@ export class File { public get AppProperties(): AppProperties { return this.appProperties; } - public get ExternalStyles(): string { - return this.externalStyles; - } } From 5242f7d55cef34e647ff3117d0a1a77cf852998c Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Wed, 28 Mar 2018 14:54:07 +0200 Subject: [PATCH 06/20] styles: support for external styles. parsing external styles. --- package.json | 1 + src/file/core-properties/properties.ts | 1 + src/file/file.ts | 11 +- src/file/index.ts | 1 + .../styles/external-styles-factory.spec.ts | 147 ++++++++++++++++++ src/file/styles/external-styles-factory.ts | 64 ++++++++ src/file/styles/factory.ts | 13 +- src/file/styles/index.ts | 18 +-- .../imported-xml-component.spec.ts | 34 ++++ .../xml-components/imported-xml-component.ts | 71 +++++++++ src/file/xml-components/index.ts | 1 + 11 files changed, 345 insertions(+), 17 deletions(-) create mode 100644 src/file/styles/external-styles-factory.spec.ts create mode 100644 src/file/styles/external-styles-factory.ts create mode 100644 src/file/xml-components/imported-xml-component.spec.ts create mode 100644 src/file/xml-components/imported-xml-component.ts 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"; From 323f91dd6816eea11c420ff2543e0d3eb3d97a88 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Wed, 28 Mar 2018 16:31:44 +0200 Subject: [PATCH 07/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3edbf6d1ae..23d233f46e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.1", + "version": "3.2.2", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From c92cab5e5bd5f66af83aec6fc9f1a3b45844873a Mon Sep 17 00:00:00 2001 From: h4buli <34742290+h4buli@users.noreply.github.com> Date: Tue, 17 Apr 2018 15:33:53 +0200 Subject: [PATCH 08/20] Fix Numbering (#4) * Version bump * (fix): fixed issue with numbering in document --- package.json | 2 +- src/file/numbering/numbering.ts | 66 ++++++--------------------------- src/file/paragraph/paragraph.ts | 5 +++ 3 files changed, 18 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 3edbf6d1ae..23d233f46e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.1", + "version": "3.2.2", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { diff --git a/src/file/numbering/numbering.ts b/src/file/numbering/numbering.ts index 64ec0b5a9c..517766987a 100644 --- a/src/file/numbering/numbering.ts +++ b/src/file/numbering/numbering.ts @@ -1,13 +1,14 @@ -import { XmlComponent } from "file/xml-components"; +import { XmlComponent, IXmlableObject } from "file/xml-components"; import { DocumentAttributes } from "../document/document-attributes"; -import { Indent } from "../paragraph/formatting"; -import { RunFonts } from "../paragraph/run/run-fonts"; import { AbstractNumbering } from "./abstract-numbering"; import { Num } from "./num"; export class Numbering extends XmlComponent { private nextId: number; + private abstractNumbering: Array = []; + private concreteNumbering: Array = []; + constructor() { super("w:numbering"); this.root.push( @@ -33,66 +34,23 @@ export class Numbering extends XmlComponent { ); this.nextId = 0; - - const abstractNumbering = this.createAbstractNumbering(); - - abstractNumbering - .createLevel(0, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 720, hanging: 360 })) - .addRunProperty(new RunFonts("Symbol", "default")); - - abstractNumbering - .createLevel(1, "bullet", "o", "left") - .addParagraphProperty(new Indent({ left: 1440, hanging: 360 })) - .addRunProperty(new RunFonts("Courier New", "default")); - - abstractNumbering - .createLevel(2, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 2160, hanging: 360 })) - .addRunProperty(new RunFonts("Wingdings", "default")); - - abstractNumbering - .createLevel(3, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 2880, hanging: 360 })) - .addRunProperty(new RunFonts("Symbol", "default")); - - abstractNumbering - .createLevel(4, "bullet", "o", "left") - .addParagraphProperty(new Indent({ left: 3600, hanging: 360 })) - .addRunProperty(new RunFonts("Courier New", "default")); - - abstractNumbering - .createLevel(5, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 4320, hanging: 360 })) - .addRunProperty(new RunFonts("Wingdings", "default")); - - abstractNumbering - .createLevel(6, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 5040, hanging: 360 })) - .addRunProperty(new RunFonts("Symbol", "default")); - - abstractNumbering - .createLevel(7, "bullet", "o", "left") - .addParagraphProperty(new Indent({ left: 5760, hanging: 360 })) - .addRunProperty(new RunFonts("Courier New", "default")); - - abstractNumbering - .createLevel(8, "bullet", "•", "left") - .addParagraphProperty(new Indent({ left: 6480, hanging: 360 })) - .addRunProperty(new RunFonts("Wingdings", "default")); - - this.createConcreteNumbering(abstractNumbering); } public createAbstractNumbering(): AbstractNumbering { const num = new AbstractNumbering(this.nextId++); - this.root.push(num); + this.abstractNumbering.push(num); return num; } public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num { const num = new Num(this.nextId++, abstractNumbering.id); - this.root.push(num); + this.concreteNumbering.push(num); return num; } + + public prepForXml(): IXmlableObject { + this.abstractNumbering.forEach(x => this.root.push(x)); + this.concreteNumbering.forEach(x => this.root.push(x)); + return super.prepForXml(); + } } diff --git a/src/file/paragraph/paragraph.ts b/src/file/paragraph/paragraph.ts index 0bf128acd3..ac779de094 100644 --- a/src/file/paragraph/paragraph.ts +++ b/src/file/paragraph/paragraph.ts @@ -136,6 +136,11 @@ export class Paragraph extends XmlComponent { return this; } + public setCustomNumbering(numberId: number, indentLevel: number): Paragraph { + this.properties.push(new NumberProperties(numberId, indentLevel)); + return this; + } + public style(styleId: string): Paragraph { this.properties.push(new Style(styleId)); return this; From 124aac4888f20116bc8afdbc3a63219db2811d9d Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Tue, 17 Apr 2018 15:38:51 +0200 Subject: [PATCH 09/20] fixed failing tests - removed test for the ctor for Numbering class --- src/file/numbering/numbering.spec.ts | 29 ---------------------------- 1 file changed, 29 deletions(-) diff --git a/src/file/numbering/numbering.spec.ts b/src/file/numbering/numbering.spec.ts index b695d5d1d4..6f8da904b8 100644 --- a/src/file/numbering/numbering.spec.ts +++ b/src/file/numbering/numbering.spec.ts @@ -12,35 +12,6 @@ describe("Numbering", () => { numbering = new Numbering(); }); - describe("#constructor", () => { - it("creates a default numbering with one abstract and one concrete instance", () => { - const tree = new Formatter().format(numbering); - expect(Object.keys(tree)).to.deep.equal(["w:numbering"]); - const abstractNums = tree["w:numbering"].filter((el) => el["w:abstractNum"]); - expect(abstractNums).to.have.lengthOf(1); - expect(abstractNums[0]["w:abstractNum"]).to.deep.include.members([ - { _attr: { "w:abstractNumId": 0, "w15:restartNumberingAfterBreak": 0 } }, - { "w:multiLevelType": [{ _attr: { "w:val": "hybridMultilevel" } }] }, - ]); - - abstractNums.filter((el) => el["w:lvl"]).forEach((el, ix) => { - expect(Object.keys(el)).to.have.lengthOf(1); - expect(Object.keys(el["w:lvl"]).sort()).to.deep.equal(["_attr", "w:start", "w:lvlJc", "w:numFmt", "w:pPr", "w:rPr"]); - expect(el["w:lvl"]).to.have.deep.members([ - { _attr: { "w:ilvl": ix, "w15:tentative": 1 } }, - { "w:start": [{ _attr: { "w:val": 1 } }] }, - { "w:lvlJc": [{ _attr: { "w:val": "left" } }] }, - { "w:numFmt": [{ _attr: { "w:val": "bullet" } }] }, - ]); - // Once chai 4.0.0 lands and #644 is resolved, we can add the following to the test: - // {"w:lvlText": [{"_attr": {"w:val": "•"}}]}, - // {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]}, - // {"w:pPr": [{"_attr": {}}, - // {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]}, - }); - }); - }); - describe("#createAbstractNumbering", () => { it("returns a new AbstractNumbering instance", () => { const a2 = numbering.createAbstractNumbering(); From bebfec7755617c372601b6a405b6183ca12543a7 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Tue, 17 Apr 2018 15:39:57 +0200 Subject: [PATCH 10/20] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23d233f46e..67ba87d372 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.2", + "version": "3.2.3", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From 8b11140be24c1c5fb9a0c10fb9754058cf593f6c Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Tue, 17 Apr 2018 16:31:36 +0200 Subject: [PATCH 11/20] export AbstractNumbering class --- src/file/numbering/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/file/numbering/index.ts b/src/file/numbering/index.ts index 33832de65b..d7a38258d3 100644 --- a/src/file/numbering/index.ts +++ b/src/file/numbering/index.ts @@ -1 +1,2 @@ export * from "./numbering"; +export * from "./abstract-numbering"; \ No newline at end of file From c618ca753996bb38448d28653cfff64321bdfa15 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Tue, 17 Apr 2018 16:33:08 +0200 Subject: [PATCH 12/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 67ba87d372..1ddc4833b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.3", + "version": "3.2.4", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From 2119ae769b738ca18e10015404d6357ae66f4f0a Mon Sep 17 00:00:00 2001 From: h4buli <34742290+h4buli@users.noreply.github.com> Date: Fri, 20 Apr 2018 15:59:06 +0200 Subject: [PATCH 13/20] Images: Extend API for working with images (#5) * extend creating image using buffer and dimensions from outside * remove empty space --- src/file/file.ts | 11 ++++++++++ src/file/media/data.ts | 4 ++-- src/file/media/media.ts | 47 ++++++++++++++++++++++++++++------------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/file/file.ts b/src/file/file.ts index e7e14c207d..dce3ef24b1 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -13,6 +13,7 @@ import { Styles } from "./styles"; import { DefaultStylesFactory } from "./styles/factory"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { Table } from "./table"; +import { IMediaData } from "index"; export class File { private readonly document: Document; @@ -118,6 +119,16 @@ export class File { this.document.createDrawing(mediaData); } + public createImageData(imageName: string, data: Buffer, width?: number, height?: number): IMediaData { + const mediaData = this.media.addMediaWithData(imageName, data, this.docRelationships.RelationshipCount, width, height); + this.docRelationships.createRelationship( + mediaData.referenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", + `media/${mediaData.fileName}`, + ); + return mediaData; + } + public get Document(): Document { return this.document; } diff --git a/src/file/media/data.ts b/src/file/media/data.ts index 00836ed962..cbc7d8c5bf 100644 --- a/src/file/media/data.ts +++ b/src/file/media/data.ts @@ -13,8 +13,8 @@ export interface IMediaDataDimensions { export interface IMediaData { referenceId: number; - stream: fs.ReadStream; - path: string; + stream: fs.ReadStream | Buffer; + path?: string; fileName: string; dimensions: IMediaDataDimensions; } diff --git a/src/file/media/media.ts b/src/file/media/media.ts index 5ac8905bc7..7f5c960a55 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -11,23 +11,10 @@ export class Media { this.map = new Map(); } - public getMedia(key: string): IMediaData { - const data = this.map.get(key); - - if (data === undefined) { - throw new Error(`Cannot find image with the key ${key}`); - } - - return data; - } - - public addMedia(filePath: string, relationshipsCount: number): IMediaData { - const key = path.basename(filePath); - const dimensions = sizeOf(filePath); - + private createMedia(key: string, relationshipsCount, dimensions, data: fs.ReadStream | Buffer, filePath?: string, ) { const imageData = { referenceId: this.map.size + relationshipsCount + 1, - stream: fs.createReadStream(filePath), + stream: data, path: filePath, fileName: key, dimensions: { @@ -45,6 +32,36 @@ export class Media { return imageData; } + public getMedia(key: string): IMediaData { + const data = this.map.get(key); + + if (data === undefined) { + throw new Error(`Cannot find image with the key ${key}`); + } + + return data; + } + + public addMedia(filePath: string, relationshipsCount: number): IMediaData { + const key = path.basename(filePath); + const dimensions = sizeOf(filePath); + return this.createMedia(key, relationshipsCount, dimensions, fs.createReadStream(filePath), filePath); + } + + public addMediaWithData(fileName: string, data: Buffer, relationshipsCount: number, width?, height?): IMediaData { + const key = fileName; + let dimensions; + if (width && height) { + dimensions = { + width: width, + height: height + } + } else { + dimensions = sizeOf(data); + } + + return this.createMedia(key, relationshipsCount, dimensions, data); + } public get array(): IMediaData[] { const array = new Array(); From 20ba0813083ca32bb2e06da52c5fa3550ad24282 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Fri, 20 Apr 2018 16:00:44 +0200 Subject: [PATCH 14/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ddc4833b4..6411f9600d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.4", + "version": "3.2.5", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From 3691d79a4ac66f3dc562b507163e0690e5cf8326 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Mon, 23 Apr 2018 11:49:57 +0200 Subject: [PATCH 15/20] add method to add child to an element --- src/file/xml-components/xml-component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index d3705478cb..833e368f23 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -23,4 +23,8 @@ export abstract class XmlComponent extends BaseXmlComponent { [this.rootKey]: children, }; } + + public addChildElement(child: XmlComponent | string) { + this.root.push(child); + } } From e67f5f80e1e9c05e1d74b8a4f1b40cc71591fa12 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Mon, 23 Apr 2018 11:50:40 +0200 Subject: [PATCH 16/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6411f9600d..80bcf9bd50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.5", + "version": "3.2.6", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From dc136daeab6ada648de4838be26cbf360b7c7bc0 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Thu, 26 Apr 2018 14:16:02 +0200 Subject: [PATCH 17/20] tables: add option to pass column size when creating a table - add optionto the XmlComponent to `delete`/skip elements when exporting to xml --- .../styles/external-styles-factory.spec.ts | 13 +++++++ src/file/table/table.ts | 39 ++++++++++++------- src/file/xml-components/base.ts | 5 +++ src/file/xml-components/xml-component.spec.ts | 11 ++++++ src/file/xml-components/xml-component.ts | 10 +++++ 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/file/styles/external-styles-factory.spec.ts b/src/file/styles/external-styles-factory.spec.ts index 802b6c8bb0..0957c25d9f 100644 --- a/src/file/styles/external-styles-factory.spec.ts +++ b/src/file/styles/external-styles-factory.spec.ts @@ -51,6 +51,7 @@ describe("External styles factory", () => { expect(importedStyle.root.length).to.equal(5); expect(importedStyle.root[1]).to.eql({ + deleted: false, root: [], rootKey: "w:docDefaults", }); @@ -59,6 +60,7 @@ describe("External styles factory", () => { "w:defLockedState": "1", "w:defUIPriority": "99", }, + deleted: false, root: [], rootKey: "w:latentStyles", }); @@ -74,15 +76,18 @@ describe("External styles factory", () => { "w:styleId": "Normal", "w:type": "paragraph", }, + deleted: false, root: [ { _attr: { "w:val": "Normal", }, + deleted: false, root: [], rootKey: "w:name", }, { + deleted: false, root: [], rootKey: "w:qFormat", }, @@ -95,11 +100,13 @@ describe("External styles factory", () => { "w:styleId": "Heading1", "w:type": "paragraph", }, + deleted: false, root: [ { _attr: { "w:val": "heading 1", }, + deleted: false, root: [], rootKey: "w:name", }, @@ -107,20 +114,25 @@ describe("External styles factory", () => { _attr: { "w:val": "Normal", }, + deleted: false, root: [], rootKey: "w:basedOn", }, { + deleted: false, root: [ { + deleted: false, root: [], rootKey: "w:keepNext", }, { + deleted: false, root: [], rootKey: "w:keepLines", }, { + deleted: false, root: [ { _attr: { @@ -129,6 +141,7 @@ describe("External styles factory", () => { "w:sz": "4", "w:val": "single", }, + deleted: false, root: [], rootKey: "w:bottom", }, diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 6a7bb1a928..aa6e760d27 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -8,26 +8,31 @@ export class Table extends XmlComponent { private readonly rows: TableRow[]; private readonly grid: TableGrid; - constructor(rows: number, cols: number) { + constructor(rows: number, cols: number, colSizes?: number[]) { super("w:tbl"); this.properties = new TableProperties(); this.root.push(this.properties); - const gridCols: number[] = []; - for (let i = 0; i < cols; i++) { - /* - 0-width columns don't get rendered correctly, so we need - to give them some value. A reasonable default would be - ~6in / numCols, but if we do that it becomes very hard - to resize the table using setWidth, unless the layout - algorithm is set to 'fixed'. Instead, the approach here - means even in 'auto' layout, setting a width on the - table will make it look reasonable, as the layout - algorithm will expand columns to fit its content - */ - gridCols.push(1); + if (colSizes && colSizes.length > 0) { + this.grid = new TableGrid(colSizes); + } else { + const gridCols: number[] = []; + for (let i = 0; i < cols; i++) { + /* + 0-width columns don't get rendered correctly, so we need + to give them some value. A reasonable default would be + ~6in / numCols, but if we do that it becomes very hard + to resize the table using setWidth, unless the layout + algorithm is set to 'fixed'. Instead, the approach here + means even in 'auto' layout, setting a width on the + table will make it look reasonable, as the layout + algorithm will expand columns to fit its content + */ + gridCols.push(1); + } + this.grid = new TableGrid(gridCols); } - this.grid = new TableGrid(gridCols); + this.root.push(this.grid); this.rows = []; @@ -111,6 +116,10 @@ export class TableCell extends XmlComponent { this.addContent(para); return para; } + + get cellProperties() { + return this.properties; + } } export class TableCellProperties extends XmlComponent { diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts index d634a418a9..f6382f5e7e 100644 --- a/src/file/xml-components/base.ts +++ b/src/file/xml-components/base.ts @@ -2,10 +2,15 @@ import { IXmlableObject } from "./xmlable-object"; export abstract class BaseXmlComponent { protected rootKey: string; + protected deleted: boolean = false; constructor(rootKey: string) { this.rootKey = rootKey; } public abstract prepForXml(): IXmlableObject; + + get isDeleted() { + return this.deleted; + } } diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts index 17d3d4d1cb..25bf442bc9 100644 --- a/src/file/xml-components/xml-component.spec.ts +++ b/src/file/xml-components/xml-component.spec.ts @@ -18,4 +18,15 @@ describe("XmlComponent", () => { assert.equal(newJson.rootKey, "w:test"); }); }); + + describe("#prepForXml()", () => { + it("should skip deleted elements", () => { + const child = new TestComponent("w:test1"); + child.delete(); + xmlComponent.addChildElement(child); + + const xml = xmlComponent.prepForXml(); + 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 833e368f23..b41bb01267 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -12,6 +12,12 @@ export abstract class XmlComponent extends BaseXmlComponent { public prepForXml(): IXmlableObject { const children = this.root + .filter(c => { + if (c instanceof BaseXmlComponent) { + return !c.isDeleted; + } + return true; + }) .map((comp) => { if (comp instanceof BaseXmlComponent) { return comp.prepForXml(); @@ -27,4 +33,8 @@ export abstract class XmlComponent extends BaseXmlComponent { public addChildElement(child: XmlComponent | string) { this.root.push(child); } + + public delete() { + this.deleted = true; + } } From 21bb8f90167a8d7d68f2f837ce45643e5644a836 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Thu, 26 Apr 2018 14:18:22 +0200 Subject: [PATCH 18/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80bcf9bd50..0667ad1ef0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.6", + "version": "3.2.7", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": { From 753287d9d19f3ce9b608adaba3451bfe7484c1dc Mon Sep 17 00:00:00 2001 From: h4buli <34742290+h4buli@users.noreply.github.com> Date: Fri, 4 May 2018 15:56:28 +0200 Subject: [PATCH 19/20] extend table and table cell support for cell merge, span, borders ... (#6) * extend table and table cell support for cell merge, span, borders ... * added tests for table cell borders and table cell width, renamed some variables, added jsdoc --- src/file/table/index.ts | 1 + src/file/table/table-cell.spec.ts | 181 +++++++++++++++++++++++++++ src/file/table/table-cell.ts | 197 ++++++++++++++++++++++++++++++ src/file/table/table.ts | 23 ++++ 4 files changed, 402 insertions(+) create mode 100644 src/file/table/table-cell.spec.ts create mode 100644 src/file/table/table-cell.ts diff --git a/src/file/table/index.ts b/src/file/table/index.ts index 0e948df9e8..ef3d91a47b 100644 --- a/src/file/table/index.ts +++ b/src/file/table/index.ts @@ -1 +1,2 @@ export * from "./table"; +export * from './table-cell'; \ No newline at end of file diff --git a/src/file/table/table-cell.spec.ts b/src/file/table/table-cell.spec.ts new file mode 100644 index 0000000000..596150d6d5 --- /dev/null +++ b/src/file/table/table-cell.spec.ts @@ -0,0 +1,181 @@ +import { expect } from "chai"; + +import { TableCellBorders, BorderStyle, TableCellWidth, WidthType } from "./table-cell"; +import { Formatter } from "../../export/formatter"; + +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(""); + }); + }); + + describe("#addingBorders", () => { + it("should add top border", () => { + const tb = new TableCellBorders(); + tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:top": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "dotted", + }, + }, + ], + }, + ], + }); + }); + + it("should add start(left) border", () => { + const tb = new TableCellBorders(); + tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:start": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 2, + "w:val": "single", + }, + }, + ], + }, + ], + }); + }); + + it("should add bottom border", () => { + const tb = new TableCellBorders(); + tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:bottom": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "double", + }, + }, + ], + }, + ], + }); + }); + + it("should add end(right) border", () => { + const tb = new TableCellBorders(); + tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:end": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 3, + "w:val": "thick", + }, + }, + ], + }, + ], + }); + }); + + it("should add multiple borders", () => { + const tb = new TableCellBorders(); + tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF"); + tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF"); + tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF"); + tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF"); + + const tree = new Formatter().format(tb); + expect(tree).to.deep.equal({ + "w:tcBorders": [ + { + "w:top": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "dotted", + }, + }, + ], + }, + { + "w:end": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 3, + "w:val": "thick", + }, + }, + ], + }, + { + "w:bottom": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 1, + "w:val": "double", + }, + }, + ], + }, + { + "w:start": [ + { + _attr: { + "w:color": "FF00FF", + "w:sz": 2, + "w:val": "single", + }, + }, + ], + }, + ], + }); + }); + }); +}); + +describe("TableCellWidth", () => { + describe("#constructor", () => { + it("should create object", () => { + const tcWidth = new TableCellWidth(100, WidthType.DXA); + const tree = new Formatter().format(tcWidth); + expect(tree).to.deep.equal({ + "w:tcW": [ + { + "_attr": { + "w:type": "dxa", + "w:w": 100 + } + } + ] + }); + }); + }); +}); \ No newline at end of file diff --git a/src/file/table/table-cell.ts b/src/file/table/table-cell.ts new file mode 100644 index 0000000000..99282f1125 --- /dev/null +++ b/src/file/table/table-cell.ts @@ -0,0 +1,197 @@ +import { XmlComponent, XmlAttributeComponent, IXmlableObject } from "file/xml-components"; + +export enum BorderStyle { + SINGLE = "single", + DASH_DOT_STROKED = "dashDotStroked", + DASHED = "dashed", + DASH_SMALL_GAP = "dashSmallGap", + DOT_DASH = "dotDash", + DOT_DOT_DASH = "dotDotDash", + DOTTED = "dotted", + DOUBLE = "double", + DOUBLE_WAVE = "doubleWave", + INSET = "inset", + NIL = "nil", + NONE = "none", + OUTSET = "outset", + THICK = "thick", + THICK_THIN_LARGE_GAP = "thickThinLargeGap", + THICK_THIN_MEDIUM_GAP = "thickThinMediumGap", + THICK_THIN_SMALL_GAP = "thickThinSmallGap", + THIN_THICK_LARGE_GAP = "thinThickLargeGap", + THIN_THICK_MEDIUM_GAP = "thinThickMediumGap", + THIN_THICK_SMALL_GAP = "thinThickSmallGap", + THIN_THICK_THIN_LARGE_GAP = "thinThickThinLargeGap", + THIN_THICK_THIN_MEDIUM_GAP = "thinThickThinMediumGap", + THIN_THICK_THIN_SMALL_GAP = "thinThickThinSmallGap", + THREE_D_EMBOSS = "threeDEmboss", + THREE_D_ENGRAVE = "threeDEngrave", + TRIPLE = "triple", + WAVE = "wave", +} + +interface ICellBorder { + style: BorderStyle; + size: number; + color: string; +} + +class CellBorderAttributes extends XmlAttributeComponent { + protected xmlKeys = { style: "w:val", size: "w:sz", color: "w:color" }; +} + +class BaseTableCellBorder extends XmlComponent { + setProperties(style: BorderStyle, size: number, color: string) { + let attrs = new CellBorderAttributes({ + style: style, + size: size, + color: color, + }); + this.root.push(attrs); + } +} + +export class TableCellBorders extends XmlComponent { + constructor() { + super("w:tcBorders"); + } + + public prepForXml(): IXmlableObject { + return this.root.length > 0 ? super.prepForXml() : ""; + } + + addTopBorder(style: BorderStyle, size: number, color: string) { + const top = new BaseTableCellBorder("w:top"); + top.setProperties(style, size, color); + this.root.push(top); + } + + addStartBorder(style: BorderStyle, size: number, color: string) { + const start = new BaseTableCellBorder("w:start"); + start.setProperties(style, size, color); + this.root.push(start); + } + + addBottomBorder(style: BorderStyle, size: number, color: string) { + const bottom = new BaseTableCellBorder("w:bottom"); + bottom.setProperties(style, size, color); + this.root.push(bottom); + } + + addEndBorder(style: BorderStyle, size: number, color: string) { + const end = new BaseTableCellBorder("w:end"); + end.setProperties(style, size, color); + this.root.push(end); + } +} + +/** + * Attributes fot the GridSpan element. + */ +class GridSpanAttributes extends XmlAttributeComponent<{ val: number }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * GridSpan element. Should be used in a table cell. Pass the number of columns that this cell need to span. + */ +export class GridSpan extends XmlComponent { + constructor(value: number) { + super("w:gridSpan"); + + this.root.push( + new GridSpanAttributes({ + val: value, + }), + ); + } +} + +/** + * Vertical merge types. + */ +export enum VMergeType { + /** + * Cell that is merged with upper one. + */ + CONTINUE = "continue", + /** + * Cell that is starting the vertical merge. + */ + RESTART = "restart", +} + +class VMergeAttributes extends XmlAttributeComponent<{ val: VMergeType }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * Vertical merge element. Should be used in a table cell. + */ +export class VMerge extends XmlComponent { + constructor(value: VMergeType) { + super("w:vMerge"); + + this.root.push( + new VMergeAttributes({ + val: value, + }), + ); + } +} + +export enum VerticalAlign { + BOTTOM = "bottom", + CENTER = "center", + TOP = "top", +} + +class VAlignAttributes extends XmlAttributeComponent<{ val: VerticalAlign }> { + protected xmlKeys = { val: "w:val" }; +} + +/** + * Vertical align element. + */ +export class VAlign extends XmlComponent { + constructor(value: VerticalAlign) { + super("w:vAlign"); + + this.root.push( + new VAlignAttributes({ + val: value, + }), + ); + } +} + +export enum WidthType { + /** Auto. */ + AUTO = "auto", + /** Value is in twentieths of a point */ + DXA = "dxa", + /** No (empty) value. */ + NIL = "nil", + /** Value is in percentage. */ + PERCENTAGE = "pct", +} + +class TableCellWidthAttributes extends XmlAttributeComponent<{ type: WidthType; width: string | number }> { + protected xmlKeys = { width: "w:w", type: "w:type" }; +} + +/** + * Table cell width element. + */ +export class TableCellWidth extends XmlComponent { + constructor(value: string | number, type: WidthType) { + super("w:tcW"); + + this.root.push( + new TableCellWidthAttributes({ + width: value, + type: type, + }), + ); + } +} diff --git a/src/file/table/table.ts b/src/file/table/table.ts index aa6e760d27..452ef92549 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -2,6 +2,7 @@ import { IXmlableObject, XmlComponent } from "file/xml-components"; import { Paragraph } from "../paragraph"; import { TableGrid } from "./grid"; import { TableProperties, WidthTypes } from "./properties"; +import { TableCellBorders, GridSpan, VMerge, VMergeType, VerticalAlign, VAlign, TableCellWidth, WidthType } from "file/table/table-cell"; export class Table extends XmlComponent { private readonly properties: TableProperties; @@ -123,7 +124,29 @@ export class TableCell extends XmlComponent { } export class TableCellProperties extends XmlComponent { + private cellBorder: TableCellBorders; constructor() { super("w:tcPr"); + this.cellBorder = new TableCellBorders(); + this.root.push(this.cellBorder); + } + + get borders() { + return this.cellBorder; + } + addGridSpan(cellSpan: number) { + this.root.push(new GridSpan(cellSpan)); + } + + addVerticalMerge(type: VMergeType) { + this.root.push(new VMerge(type)); + } + + setVerticalAlign(vAlignType: VerticalAlign) { + this.root.push(new VAlign(vAlignType)); + } + + setWidth(width: string | number, type: WidthType) { + this.root.push(new TableCellWidth(width, type)); } } From 84e298e7ee87dfedae3bffd05ec426a39cc4d2c9 Mon Sep 17 00:00:00 2001 From: Igor Bulovski Date: Fri, 4 May 2018 15:59:46 +0200 Subject: [PATCH 20/20] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0667ad1ef0..cee754834f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docx-h4", - "version": "3.2.7", + "version": "3.2.8", "description": "Generate .docx documents with JavaScript (formerly Office-Clippy)", "main": "build/index.js", "scripts": {