diff --git a/README.md b/README.md index 6c4e820db4..60da1a934b 100644 --- a/README.md +++ b/README.md @@ -88,18 +88,22 @@ exporter.packPdf("My First Document"); ## Examples -Check [the Wiki](https://github.com/dolanmiu/docx/wiki/Examples) for examples. +Check [the Wiki](https://github.com/dolanmiu/docx/wiki/Examples) and the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples. # Contributing Read the contribution guidelines [here](https://github.com/dolanmiu/docx/wiki/Contributing-Guidelines). +# Honoured Mentions + +[@felipeochoa](https://github.com/felipeochoa) + +[@h4buli](https://github.com/h4buli) + --- Made with 💖 -Huge thanks to [@felipeochoa](https://github.com/felipeochoa) for awesome contributions to this project - [npm-image]: https://badge.fury.io/js/docx.svg [npm-url]: https://npmjs.org/package/docx [downloads-image]: https://img.shields.io/npm/dm/docx.svg diff --git a/demo/demo14.js b/demo/demo14.js index 0e3859eadf..b2262f106a 100644 --- a/demo/demo14.js +++ b/demo/demo14.js @@ -1,6 +1,6 @@ const docx = require('../build'); -var doc = new docx.Document(undefined,{differentFirstPageHeader:true}); +var doc = new docx.Document(); doc.createParagraph("First Page").pageBreak() doc.createParagraph("Second Page"); @@ -10,7 +10,8 @@ var pageNumber = new docx.TextRun().pageNumber() var pageoneheader = new docx.Paragraph("First Page Header ").right(); pageoneheader.addRun(pageNumber); -doc.firstPageHeader.addParagraph(pageoneheader); +var firstPageHeader = doc.createFirstPageHeader(); +firstPageHeader.addParagraph(pageoneheader); var pagetwoheader = new docx.Paragraph("My Title ").right(); diff --git a/demo/demo16.js b/demo/demo16.js index ea6308eb46..ce0b162dc1 100644 --- a/demo/demo16.js +++ b/demo/demo16.js @@ -1,14 +1,36 @@ -const docx = require('../build'); +const docx = require("../build"); var doc = new docx.Document(); -var paragraph = new docx.Paragraph("Hello World").referenceFootnote(1); +var paragraph = new docx.Paragraph("Hello World").pageBreak(); doc.addParagraph(paragraph); -doc.createFootnote(new docx.Paragraph("Test")); +var header = doc.createHeader(); +header.createParagraph("Header on another page"); +var footer = doc.createFooter(); +footer.createParagraph("Footer on another page"); + +doc.addSection({ + headerId: header.Header.referenceId, + footerId: footer.Footer.referenceId, + pageNumberStart: 1, + pageNumberFormatType: docx.PageNumberFormat.DECIMAL, +}); + +doc.createParagraph("hello"); + +doc.addSection({ + headerId: header.Header.referenceId, + footerId: footer.Footer.referenceId, + pageNumberStart: 1, + pageNumberFormatType: docx.PageNumberFormat.DECIMAL, + orientation: docx.PageOrientation.LANDSCAPE, +}); + +doc.createParagraph("hello in landscape"); var exporter = new docx.LocalPacker(doc); -exporter.pack('My Document'); +exporter.pack("My Document"); -console.log('Document created successfully at project root!'); +console.log("Document created successfully at project root!"); diff --git a/package.json b/package.json index f09aaf0c50..55f9e00fd5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,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.12.1", diff --git a/src/export/packer/compiler.spec.ts b/src/export/packer/compiler.spec.ts new file mode 100644 index 0000000000..0bf691ebcb --- /dev/null +++ b/src/export/packer/compiler.spec.ts @@ -0,0 +1,75 @@ +/* tslint:disable:typedef space-before-function-paren */ +import * as fs from "fs"; +import * as JSZip from "jszip"; + +import { expect } from "chai"; +import { File } from "../../file"; +import { Compiler } from "./compiler"; + +describe("Compiler", () => { + let compiler: Compiler; + let file: File; + + beforeEach(() => { + file = new File(); + compiler = new Compiler(file); + }); + + describe("#compile()", () => { + it("should pack all the content", async function() { + this.timeout(99999999); + const fileName = "build/tests/test.docx"; + await compiler.compile(fs.createWriteStream(fileName)); + + const docxFile = fs.readFileSync(fileName); + const zipFile: JSZip = await JSZip.loadAsync(docxFile); + const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); + + expect(fileNames).is.an.instanceof(Array); + expect(fileNames).has.length(12); + expect(fileNames).to.include("word/document.xml"); + expect(fileNames).to.include("word/styles.xml"); + expect(fileNames).to.include("docProps/core.xml"); + expect(fileNames).to.include("docProps/app.xml"); + expect(fileNames).to.include("word/numbering.xml"); + expect(fileNames).to.include("word/header1.xml"); + expect(fileNames).to.include("word/_rels/header1.xml.rels"); + expect(fileNames).to.include("word/footer1.xml"); + expect(fileNames).to.include("word/_rels/footer1.xml.rels"); + expect(fileNames).to.include("word/_rels/document.xml.rels"); + expect(fileNames).to.include("[Content_Types].xml"); + expect(fileNames).to.include("_rels/.rels"); + }); + + it("should pack all additional headers and footers", async function() { + file.createFooter(); + file.createFooter(); + file.createHeader(); + file.createHeader(); + + this.timeout(99999999); + const fileName = "build/tests/test2.docx"; + await compiler.compile(fs.createWriteStream(fileName)); + + const docxFile = fs.readFileSync(fileName); + const zipFile: JSZip = await JSZip.loadAsync(docxFile); + const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name); + + expect(fileNames).is.an.instanceof(Array); + expect(fileNames).has.length(20); + + expect(fileNames).to.include("word/header1.xml"); + expect(fileNames).to.include("word/_rels/header1.xml.rels"); + expect(fileNames).to.include("word/header2.xml"); + expect(fileNames).to.include("word/_rels/header2.xml.rels"); + expect(fileNames).to.include("word/header3.xml"); + expect(fileNames).to.include("word/_rels/header3.xml.rels"); + expect(fileNames).to.include("word/footer1.xml"); + expect(fileNames).to.include("word/_rels/footer1.xml.rels"); + expect(fileNames).to.include("word/footer2.xml"); + expect(fileNames).to.include("word/_rels/footer2.xml.rels"); + expect(fileNames).to.include("word/footer3.xml"); + expect(fileNames).to.include("word/_rels/footer3.xml.rels"); + }); + }); +}); diff --git a/src/export/packer/compiler.ts b/src/export/packer/compiler.ts index c912054510..99821442fa 100644 --- a/src/export/packer/compiler.ts +++ b/src/export/packer/compiler.ts @@ -33,11 +33,6 @@ export class Compiler { const xmlNumbering = xml(this.formatter.format(this.file.Numbering)); const xmlRelationships = xml(this.formatter.format(this.file.DocumentRelationships)); const xmlFileRelationships = xml(this.formatter.format(this.file.FileRelationships)); - const xmlHeader = xml(this.formatter.format(this.file.Header.Header)); - const xmlHeader2 = xml(this.formatter.format(this.file.firstPageHeader.Header)); - const xmlFooter = xml(this.formatter.format(this.file.Footer.Footer)); - const xmlHeaderRelationships = xml(this.formatter.format(this.file.Header.Relationships)); - const xmlFooterRelationships = xml(this.formatter.format(this.file.Footer.Relationships)); const xmlContentTypes = xml(this.formatter.format(this.file.ContentTypes)); const xmlAppProperties = xml(this.formatter.format(this.file.AppProperties)); const xmlFootnotes = xml(this.formatter.format(this.file.FootNotes)); @@ -62,17 +57,29 @@ export class Compiler { name: "word/numbering.xml", }); - this.archive.append(xmlHeader, { - name: "word/header1.xml", - }); + // headers + for (let i = 0; i < this.file.Headers.length; i++) { + const element = this.file.Headers[i]; + this.archive.append(xml(this.formatter.format(element.Header)), { + name: `word/header${i + 1}.xml`, + }); - this.archive.append(xmlHeader2, { - name: "word/header2.xml", - }); + this.archive.append(xml(this.formatter.format(element.Relationships)), { + name: `word/_rels/header${i + 1}.xml.rels`, + }); + } - this.archive.append(xmlFooter, { - name: "word/footer1.xml", - }); + // footers + for (let i = 0; i < this.file.Footers.length; i++) { + const element = this.file.Footers[i]; + this.archive.append(xml(this.formatter.format(element.Footer)), { + name: `word/footer${i + 1}.xml`, + }); + + this.archive.append(xml(this.formatter.format(element.Relationships)), { + name: `word/_rels/footer${i + 1}.xml.rels`, + }); + } this.archive.append(xmlFootnotes, { name: "word/footnotes.xml", @@ -82,14 +89,6 @@ export class Compiler { name: "word/_rels/document.xml.rels", }); - this.archive.append(xmlHeaderRelationships, { - name: "word/_rels/header1.xml.rels", - }); - - this.archive.append(xmlFooterRelationships, { - name: "word/_rels/footer1.xml.rels", - }); - this.archive.append(xmlContentTypes, { name: "[Content_Types].xml", }); diff --git a/src/file/content-types/content-types.spec.ts b/src/file/content-types/content-types.spec.ts new file mode 100644 index 0000000000..8a755d3924 --- /dev/null +++ b/src/file/content-types/content-types.spec.ts @@ -0,0 +1,139 @@ +import { expect } from "chai"; +import { Formatter } from "../../export/formatter"; +import { ContentTypes } from "./content-types"; +describe("ContentTypes", () => { + let contentTypes: ContentTypes; + + beforeEach(() => { + contentTypes = new ContentTypes(); + }); + + describe("#constructor()", () => { + it("should create default content types", () => { + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"]).to.be.an.instanceof(Array); + + expect(tree["Types"][0]).to.deep.equal({ _attr: { xmlns: "http://schemas.openxmlformats.org/package/2006/content-types" } }); + expect(tree["Types"][1]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/png", Extension: "png" } }] }); + expect(tree["Types"][2]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/jpeg", Extension: "jpeg" } }] }); + expect(tree["Types"][3]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/jpeg", Extension: "jpg" } }] }); + expect(tree["Types"][4]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/bmp", Extension: "bmp" } }] }); + expect(tree["Types"][5]).to.deep.equal({ Default: [{ _attr: { ContentType: "image/gif", Extension: "gif" } }] }); + expect(tree["Types"][6]).to.deep.equal({ + Default: [{ _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } }], + }); + expect(tree["Types"][7]).to.deep.equal({ Default: [{ _attr: { ContentType: "application/xml", Extension: "xml" } }] }); + expect(tree["Types"][8]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + PartName: "/word/document.xml", + }, + }, + ], + }); + expect(tree["Types"][9]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", + PartName: "/word/styles.xml", + }, + }, + ], + }); + expect(tree["Types"][10]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-package.core-properties+xml", + PartName: "/docProps/core.xml", + }, + }, + ], + }); + expect(tree["Types"][11]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml", + PartName: "/docProps/app.xml", + }, + }, + ], + }); + expect(tree["Types"][12]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + PartName: "/word/numbering.xml", + }, + }, + ], + }); + }); + }); + + describe("#addFooter()", () => { + it("should add footer", () => { + contentTypes.addFooter(101); + contentTypes.addFooter(102); + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"][13]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", + PartName: "/word/footer101.xml", + }, + }, + ], + }); + + expect(tree["Types"][14]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", + PartName: "/word/footer102.xml", + }, + }, + ], + }); + }); + }); + + describe("#addHeader()", () => { + it("should add header", () => { + contentTypes.addHeader(201); + contentTypes.addHeader(202); + const tree = new Formatter().format(contentTypes); + + expect(tree["Types"][13]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", + PartName: "/word/header201.xml", + }, + }, + ], + }); + + expect(tree["Types"][14]).to.deep.equal({ + Override: [ + { + _attr: { + ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", + PartName: "/word/header202.xml", + }, + }, + ], + }); + }); + }); +}); diff --git a/src/file/content-types/content-types.ts b/src/file/content-types/content-types.ts index cbb078d058..8735d3d9e2 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -25,13 +25,22 @@ export class ContentTypes extends XmlComponent { new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "/word/document.xml"), ); - this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "/word/header1.xml")); - this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", "/word/header2.xml")); - this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", "/word/footer1.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", "/word/styles.xml")); this.root.push(new Override("application/vnd.openxmlformats-package.core-properties+xml", "/docProps/core.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.extended-properties+xml", "/docProps/app.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", "/word/numbering.xml")); this.root.push(new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml", "/word/footnotes.xml")); } + + public addFooter(index: number): void { + this.root.push( + new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", `/word/footer${index}.xml`), + ); + } + + public addHeader(index: number): void { + this.root.push( + new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml", `/word/header${index}.xml`), + ); + } } diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts index c5498bc99a..31c1e49e32 100644 --- a/src/file/document/body/body.spec.ts +++ b/src/file/document/body/body.spec.ts @@ -1,39 +1,42 @@ -// import { assert } from "chai"; +import { expect } from "chai"; -// import { Utility } from "../../../tests/utility"; -// import { Body } from "./"; +import { Formatter } from "../../../export/formatter"; +import { Body } from "./body"; describe("Body", () => { - // let body: Body; + let body: Body; beforeEach(() => { - // body = new Body(); + body = new Body(); }); - // describe("#constructor()", () => { - // it("should create the Section Properties", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[0].rootKey, "w:sectPr"); - // }); + describe("#constructor()", () => { + it("should create default section", () => { + const formatted = new Formatter().format(body)["w:body"][0]; + expect(formatted) + .to.have.property("w:sectPr") + .and.to.be.an.instanceof(Array); + expect(formatted["w:sectPr"]).to.have.length(7); + }); + }); - // it("should create the Page Size", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[1].rootKey, "w:pgSz"); - // }); + describe("addSection", () => { + it("should add section with options", () => { + body.addSection({ + width: 10000, + height: 10000, + }); - // it("should create the Page Margin", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[2].rootKey, "w:pgMar"); - // }); + const formatted = new Formatter().format(body)["w:body"]; + expect(formatted).to.be.an.instanceof(Array); + const defaultSectionPr = formatted[0]["w:p"][1]["w:pPr"][0]["w:sectPr"]; - // it("should create the Columns", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[3].rootKey, "w:cols"); - // }); + // check that this is the default section and added first in paragraph + expect(defaultSectionPr[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 16838, "w:w": 11906, "w:orient": "portrait" } }] }); - // it("should create the Document Grid", () => { - // const newJson = Utility.jsonify(body); - // assert.equal(newJson.root[4].rootKey, "w:docGrid"); - // }); - // }); + // check for new section (since it's the last one, it's direct child of 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" } }] }); + }); + }); }); diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index fa30099f75..1d7fcdac43 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -1,14 +1,63 @@ -import { XmlComponent } from "file/xml-components"; -import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; +import { IXmlableObject, XmlComponent } from "file/xml-components"; +import { Paragraph, ParagraphProperties } from "../.."; +import { SectionProperties, SectionPropertiesOptions } from "./section-properties"; export class Body extends XmlComponent { + private defaultSection: SectionProperties; + + private sections: SectionProperties[] = []; + constructor(sectionPropertiesOptions?: SectionPropertiesOptions) { super("w:body"); - this.root.push(new SectionProperties(sectionPropertiesOptions)); + this.defaultSection = new SectionProperties(sectionPropertiesOptions); + this.sections.push(this.defaultSection); + } + + /** + * Adds new section properties. + * Note: Previous section is created in paragraph after the current element, and then new section will be added. + * The spec says: + * - section element should be in the last paragraph of the section + * - last section should be direct child of body + * @param section new section + */ + public addSection(section: SectionPropertiesOptions | SectionProperties): void { + const currentSection = this.sections.pop() as SectionProperties; + this.root.push(this.createSectionParagraph(currentSection)); + if (section instanceof SectionProperties) { + this.sections.push(section); + } else { + const params = { + ...this.defaultSection.Options, + ...section, + }; + this.sections.push(new SectionProperties(params)); + } + } + public prepForXml(): IXmlableObject { + 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(); } public push(component: XmlComponent): void { this.root.push(component); } + + get DefaultSection(): SectionProperties { + return this.defaultSection; + } + + private createSectionParagraph(section: SectionProperties): Paragraph { + const paragraph = new Paragraph(); + const properties = new ParagraphProperties(); + properties.addChildElement(section); + paragraph.addChildElement(properties); + return paragraph; + } } diff --git a/src/file/document/body/index.ts b/src/file/document/body/index.ts index 93f4529388..83678ef8ea 100644 --- a/src/file/document/body/index.ts +++ b/src/file/document/body/index.ts @@ -1 +1,2 @@ export * from "./body"; +export * from "./section-properties"; diff --git a/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts b/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts index 0097de0dbb..763053e36a 100644 --- a/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts +++ b/src/file/document/body/section-properties/footer-reference/footer-reference-attributes.ts @@ -1,5 +1,11 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum FooterReferenceType { + DEFAULT = "default", + FIRST = "first", + EVEN = "even", +} + export interface IFooterReferenceAttributes { type: string; id: string; diff --git a/src/file/document/body/section-properties/footer-reference/footer-reference.ts b/src/file/document/body/section-properties/footer-reference/footer-reference.ts index 6b7012b6d3..6c2786432d 100644 --- a/src/file/document/body/section-properties/footer-reference/footer-reference.ts +++ b/src/file/document/body/section-properties/footer-reference/footer-reference.ts @@ -1,13 +1,19 @@ import { XmlComponent } from "file/xml-components"; -import { FooterReferenceAttributes } from "./footer-reference-attributes"; +import { FooterReferenceAttributes, FooterReferenceType } from "./footer-reference-attributes"; + +export interface IFooterOptions { + footerType?: FooterReferenceType; + footerId?: number; +} export class FooterReference extends XmlComponent { - constructor() { + constructor(options: IFooterOptions) { super("w:footerReference"); + this.root.push( new FooterReferenceAttributes({ - type: "default", - id: `rId${4}`, + type: options.footerType || FooterReferenceType.DEFAULT, + id: `rId${options.footerId}`, }), ); } diff --git a/src/file/document/body/section-properties/footer-reference/index.ts b/src/file/document/body/section-properties/footer-reference/index.ts new file mode 100644 index 0000000000..9673319fba --- /dev/null +++ b/src/file/document/body/section-properties/footer-reference/index.ts @@ -0,0 +1,2 @@ +export * from "./footer-reference"; +export * from "./footer-reference-attributes"; diff --git a/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts b/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts index b0407c4a0a..5569fa76b9 100644 --- a/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts +++ b/src/file/document/body/section-properties/header-reference/header-reference-attributes.ts @@ -1,5 +1,11 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum HeaderReferenceType { + DEFAULT = "default", + FIRST = "first", + EVEN = "even", +} + export interface IHeaderReferenceAttributes { type: string; id: string; diff --git a/src/file/document/body/section-properties/header-reference/header-reference.ts b/src/file/document/body/section-properties/header-reference/header-reference.ts index cd9d8e7349..8464a33a92 100644 --- a/src/file/document/body/section-properties/header-reference/header-reference.ts +++ b/src/file/document/body/section-properties/header-reference/header-reference.ts @@ -1,13 +1,18 @@ import { XmlComponent } from "file/xml-components"; -import { HeaderReferenceAttributes } from "./header-reference-attributes"; +import { HeaderReferenceAttributes, HeaderReferenceType } from "./header-reference-attributes"; + +export interface IHeaderOptions { + headerType?: HeaderReferenceType; + headerId?: number; +} export class HeaderReference extends XmlComponent { - constructor(order: string, refID: number) { + constructor(options: IHeaderOptions) { super("w:headerReference"); this.root.push( new HeaderReferenceAttributes({ - type: order, - id: `rId${refID}`, + type: options.headerType || HeaderReferenceType.DEFAULT, + id: `rId${options.headerId}`, }), ); } diff --git a/src/file/document/body/section-properties/header-reference/index.ts b/src/file/document/body/section-properties/header-reference/index.ts new file mode 100644 index 0000000000..80239ad98e --- /dev/null +++ b/src/file/document/body/section-properties/header-reference/index.ts @@ -0,0 +1,2 @@ +export * from "./header-reference"; +export * from "./header-reference-attributes"; diff --git a/src/file/document/body/section-properties/index.ts b/src/file/document/body/section-properties/index.ts new file mode 100644 index 0000000000..f1b5eabb84 --- /dev/null +++ b/src/file/document/body/section-properties/index.ts @@ -0,0 +1,5 @@ +export * from "./section-properties"; +export * from "./footer-reference"; +export * from "./header-reference"; +export * from "./page-size"; +export * from "./page-number"; diff --git a/src/file/document/body/section-properties/page-number/index.ts b/src/file/document/body/section-properties/page-number/index.ts new file mode 100644 index 0000000000..57e81d8724 --- /dev/null +++ b/src/file/document/body/section-properties/page-number/index.ts @@ -0,0 +1 @@ +export * from "./page-number"; diff --git a/src/file/document/body/section-properties/page-number/page-number.ts b/src/file/document/body/section-properties/page-number/page-number.ts new file mode 100644 index 0000000000..d6239d3be4 --- /dev/null +++ b/src/file/document/body/section-properties/page-number/page-number.ts @@ -0,0 +1,41 @@ +// http://officeopenxml.com/WPSectionPgNumType.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +export enum PageNumberFormat { + CARDINAL_TEXT = "cardinalText", + DECIMAL = "decimal", + DECIMAL_ENCLOSED_CIRCLE = "decimalEnclosedCircle", + DECIMAL_ENCLOSED_FULL_STOP = "decimalEnclosedFullstop", + DECIMAL_ENCLOSED_PAREN = "decimalEnclosedParen", + DECIMAL_ZERO = "decimalZero", + LOWER_LETTER = "lowerLetter", + LOWER_ROMAN = "lowerRoman", + NONE = "none", + ORDINAL_TEXT = "ordinalText", + UPPER_LETTER = "upperLetter", + UPPER_ROMAN = "upperRoman", +} + +export interface IPageNumberTypeAttributes { + pageNumberStart?: number; + pageNumberFormatType?: PageNumberFormat; +} + +export class PageNumberTypeAttributes extends XmlAttributeComponent { + protected xmlKeys = { + pageNumberStart: "w:start", + pageNumberFormatType: "w:fmt", + }; +} + +export class PageNumberType extends XmlComponent { + constructor(start?: number, numberFormat?: PageNumberFormat) { + super("w:pgNumType"); + this.root.push( + new PageNumberTypeAttributes({ + pageNumberStart: start, + pageNumberFormatType: numberFormat, + }), + ); + } +} diff --git a/src/file/document/body/section-properties/page-size/index.ts b/src/file/document/body/section-properties/page-size/index.ts new file mode 100644 index 0000000000..567f7c2d58 --- /dev/null +++ b/src/file/document/body/section-properties/page-size/index.ts @@ -0,0 +1,2 @@ +export * from "./page-size"; +export * from "./page-size-attributes"; diff --git a/src/file/document/body/section-properties/page-size/page-size-attributes.ts b/src/file/document/body/section-properties/page-size/page-size-attributes.ts index 5a3cd90907..4af206ac2d 100644 --- a/src/file/document/body/section-properties/page-size/page-size-attributes.ts +++ b/src/file/document/body/section-properties/page-size/page-size-attributes.ts @@ -1,9 +1,14 @@ import { XmlAttributeComponent } from "file/xml-components"; +export enum PageOrientation { + PORTRAIT = "portrait", + LANDSCAPE = "landscape", +} + export interface IPageSizeAttributes { width?: number; height?: number; - orientation?: string; + orientation?: PageOrientation; } export class PageSizeAttributes extends XmlAttributeComponent { diff --git a/src/file/document/body/section-properties/page-size/page-size.spec.ts b/src/file/document/body/section-properties/page-size/page-size.spec.ts index 4ebcf989c0..bd5b17aaa5 100644 --- a/src/file/document/body/section-properties/page-size/page-size.spec.ts +++ b/src/file/document/body/section-properties/page-size/page-size.spec.ts @@ -2,11 +2,12 @@ import { expect } from "chai"; import { Formatter } from "../../../../../export/formatter"; import { PageSize } from "./page-size"; +import { PageOrientation } from "./page-size-attributes"; describe("PageSize", () => { describe("#constructor()", () => { it("should create page size with portrait", () => { - const properties = new PageSize(100, 200, "portrait"); + const properties = new PageSize(100, 200, PageOrientation.PORTRAIT); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); @@ -15,7 +16,7 @@ describe("PageSize", () => { }); it("should create page size with horizontal and invert the lengths", () => { - const properties = new PageSize(100, 200, "landscape"); + const properties = new PageSize(100, 200, PageOrientation.LANDSCAPE); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); diff --git a/src/file/document/body/section-properties/page-size/page-size.ts b/src/file/document/body/section-properties/page-size/page-size.ts index ea93e1ebc0..6aa400bea2 100644 --- a/src/file/document/body/section-properties/page-size/page-size.ts +++ b/src/file/document/body/section-properties/page-size/page-size.ts @@ -1,11 +1,11 @@ import { XmlComponent } from "file/xml-components"; -import { PageSizeAttributes } from "./page-size-attributes"; +import { PageOrientation, PageSizeAttributes } from "./page-size-attributes"; export class PageSize extends XmlComponent { - constructor(width: number, height: number, orientation: string) { + constructor(width: number, height: number, orientation: PageOrientation) { super("w:pgSz"); - const flip = orientation === "landscape"; + const flip = orientation === PageOrientation.LANDSCAPE; this.root.push( new PageSizeAttributes({ diff --git a/src/file/document/body/section-properties/section-properties.spec.ts b/src/file/document/body/section-properties/section-properties.spec.ts index 994db13891..05a49d09d9 100644 --- a/src/file/document/body/section-properties/section-properties.spec.ts +++ b/src/file/document/body/section-properties/section-properties.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import { Formatter } from "../../../../export/formatter"; import { SectionProperties } from "./section-properties"; +import { FooterReferenceType, PageNumberFormat } from "."; describe("SectionProperties", () => { describe("#constructor()", () => { @@ -18,6 +19,11 @@ describe("SectionProperties", () => { gutter: 0, space: 708, linePitch: 360, + headerId: 100, + footerId: 200, + footerType: FooterReferenceType.EVEN, + pageNumberStart: 10, + pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT, }); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); @@ -38,6 +44,12 @@ describe("SectionProperties", () => { }, ], }); + + expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] }); + expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }); + expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId100", "w:type": "default" } }] }); + expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId200", "w:type": "even" } }] }); + expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "cardinalText", "w:start": 10 } }] }); }); it("should create section properties with no options", () => { @@ -61,6 +73,11 @@ describe("SectionProperties", () => { }, ], }); + expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] }); + expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }); + expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); + expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); + expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }); }); it("should create section properties with changed options", () => { diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index 4cd6e35656..bb12cda3c3 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -1,20 +1,29 @@ // http://officeopenxml.com/WPsection.php import { XmlComponent } from "file/xml-components"; +import { FooterReferenceType, IPageNumberTypeAttributes, PageNumberFormat, PageNumberType } from "./"; import { Columns } from "./columns/columns"; import { IColumnsAttributes } from "./columns/columns-attributes"; import { DocumentGrid } from "./doc-grid/doc-grid"; import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes"; -import { FooterReference } from "./footer-reference/footer-reference"; -import { HeaderReference } from "./header-reference/header-reference"; +import { FooterReference, IFooterOptions } from "./footer-reference/footer-reference"; +import { HeaderReference, IHeaderOptions } from "./header-reference/header-reference"; +import { HeaderReferenceType } from "./header-reference/header-reference-attributes"; import { PageMargin } from "./page-margin/page-margin"; import { IPageMarginAttributes } from "./page-margin/page-margin-attributes"; import { PageSize } from "./page-size/page-size"; -import { IPageSizeAttributes } from "./page-size/page-size-attributes"; -import { TitlePage } from "./title-page/title-page"; +import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; +// import { TitlePage } from "./title-page/title-page"; -export type SectionPropertiesOptions = IPageSizeAttributes & IPageMarginAttributes & IColumnsAttributes & IDocGridAttributesProperties; +export type SectionPropertiesOptions = IPageSizeAttributes & + IPageMarginAttributes & + IColumnsAttributes & + IDocGridAttributesProperties & + IHeaderOptions & + IFooterOptions & + IPageNumberTypeAttributes; export class SectionProperties extends XmlComponent { + private options: SectionPropertiesOptions; constructor(options?: SectionPropertiesOptions) { super("w:sectPr"); @@ -30,8 +39,13 @@ export class SectionProperties extends XmlComponent { gutter: 0, space: 708, linePitch: 360, - orientation: "portrait", - differentFirstPageHeader: false, + orientation: PageOrientation.PORTRAIT, + headerType: HeaderReferenceType.DEFAULT, + headerId: 0, + footerType: FooterReferenceType.DEFAULT, + footerId: 0, + pageNumberStart: undefined, + pageNumberFormatType: PageNumberFormat.DECIMAL, }; const mergedOptions = { @@ -53,13 +67,26 @@ export class SectionProperties extends XmlComponent { ); this.root.push(new Columns(mergedOptions.space)); this.root.push(new DocumentGrid(mergedOptions.linePitch)); - this.root.push(new HeaderReference("default", 3)); - if (mergedOptions.differentFirstPageHeader) { - this.root.push(new HeaderReference("first", 5)); - this.root.push(new TitlePage()); - } + this.root.push( + new HeaderReference({ + headerType: mergedOptions.headerType, + headerId: mergedOptions.headerId, + }), + ); + this.root.push( + new FooterReference({ + footerType: mergedOptions.footerType, + footerId: mergedOptions.footerId, + }), + ); - this.root.push(new FooterReference()); + this.root.push(new PageNumberType(mergedOptions.pageNumberStart, mergedOptions.pageNumberFormatType)); + + this.options = mergedOptions; + } + + get Options(): SectionPropertiesOptions { + return this.options; } } diff --git a/src/file/document/document.spec.ts b/src/file/document/document.spec.ts index 218ff55a98..e2e8781d7a 100644 --- a/src/file/document/document.spec.ts +++ b/src/file/document/document.spec.ts @@ -23,6 +23,11 @@ describe("Document", () => { } assert.isTrue(true); }); + + it("should create default section", () => { + const body = new Formatter().format(document)["w:document"][1]["w:body"]; + expect(body[0]).to.have.property("w:sectPr"); + }); }); describe("#createParagraph", () => { @@ -33,7 +38,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]).to.have.property("w:p"); + expect(body[0]).to.have.property("w:p"); }); it("should use the text given to create a run in the paragraph", () => { @@ -43,7 +48,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]) + expect(body[0]) .to.have.property("w:p") .which.includes({ "w:r": [{ "w:rPr": [] }, { "w:t": [{ _attr: { "xml:space": "preserve" } }, "sample paragraph text"] }], @@ -59,7 +64,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]).to.have.property("w:tbl"); + expect(body[0]).to.have.property("w:tbl"); }); it("should create a table with the correct dimensions", () => { @@ -68,7 +73,7 @@ describe("Document", () => { expect(body) .to.be.an("array") .which.has.length.at.least(1); - expect(body[1]) + expect(body[0]) .to.have.property("w:tbl") .which.includes({ "w:tblGrid": [ @@ -77,7 +82,7 @@ describe("Document", () => { { "w:gridCol": [{ _attr: { "w:w": 1 } }] }, ], }); - expect(body[1]["w:tbl"].filter((x) => x["w:tr"])).to.have.length(2); + expect(body[0]["w:tbl"].filter((x) => x["w:tr"])).to.have.length(2); }); }); }); diff --git a/src/file/document/document.ts b/src/file/document/document.ts index fcf52ec27d..d8db84adf3 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -70,4 +70,8 @@ export class Document extends XmlComponent { return run; } + + get Body(): Body { + return this.body; + } } diff --git a/src/file/document/index.ts b/src/file/document/index.ts index fe6d89c0eb..6b128299f6 100644 --- a/src/file/document/index.ts +++ b/src/file/document/index.ts @@ -1 +1,2 @@ export * from "./document"; +export * from "./body"; diff --git a/src/file/drawing/anchor/anchor-attributes.ts b/src/file/drawing/anchor/anchor-attributes.ts new file mode 100644 index 0000000000..cfd8ad3144 --- /dev/null +++ b/src/file/drawing/anchor/anchor-attributes.ts @@ -0,0 +1,26 @@ +import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +export interface IAnchorAttributes extends IDistance { + allowOverlap?: "0" | "1"; + behindDoc?: "0" | "1"; + layoutInCell?: "0" | "1"; + locked?: "0" | "1"; + relativeHeight?: number; + simplePos?: "0" | "1"; +} + +export class AnchorAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + allowOverlap: "allowOverlap", + behindDoc: "behindDoc", + layoutInCell: "layoutInCell", + locked: "locked", + relativeHeight: "relativeHeight", + simplePos: "simplePos", + }; +} diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts new file mode 100644 index 0000000000..58adefd174 --- /dev/null +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -0,0 +1,118 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { IDrawingOptions, TextWrapStyle } from ".././"; +import { Anchor } from "./"; + +function createDrawing(drawingOptions: IDrawingOptions): Anchor { + return new Anchor( + 1, + { + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 100 * 9525, + y: 100 * 9525, + }, + }, + drawingOptions, + ); +} + +describe("Anchor", () => { + let anchor: Anchor; + + describe("#constructor()", () => { + it("should create a Drawing with correct root key", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.rootKey, "wp:anchor"); + assert.equal(newJson.root.length, 10); + }); + + it("should create a Drawing with all default options", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const anchorAttributes = newJson.root[0].root; + assert.include(anchorAttributes, { + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", + allowOverlap: "1", + behindDoc: "0", + locked: "0", + layoutInCell: "1", + relativeHeight: 952500, + }); + + // 1: simple pos + assert.equal(newJson.root[1].rootKey, "wp:simplePos"); + + // 2: horizontal position + const horizontalPosition = newJson.root[2]; + assert.equal(horizontalPosition.rootKey, "wp:positionH"); + assert.include(horizontalPosition.root[0].root, { + relativeFrom: "column", + }); + assert.equal(horizontalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(horizontalPosition.root[1].root[0], 0); + + // 3: vertical position + const verticalPosition = newJson.root[3]; + assert.equal(verticalPosition.rootKey, "wp:positionV"); + assert.include(verticalPosition.root[0].root, { + relativeFrom: "paragraph", + }); + assert.equal(verticalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(verticalPosition.root[1].root[0], 0); + + // 4: extent + const extent = newJson.root[4]; + assert.equal(extent.rootKey, "wp:extent"); + assert.include(extent.root[0].root, { + cx: 952500, + cy: 952500, + }); + + // 5: effect extent + const effectExtent = newJson.root[5]; + assert.equal(effectExtent.rootKey, "wp:effectExtent"); + + // 6 text wrap: none + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapNone"); + + // 7: doc properties + const docProperties = newJson.root[7]; + assert.equal(docProperties.rootKey, "wp:docPr"); + + // 8: graphic frame properties + const graphicFrame = newJson.root[8]; + assert.equal(graphicFrame.rootKey, "wp:cNvGraphicFramePr"); + + // 9: graphic + const graphic = newJson.root[9]; + assert.equal(graphic.rootKey, "a:graphic"); + }); + + it("should create a Drawing with text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.SQUARE, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + // 6 text wrap: square + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapSquare"); + }); + }); +}); diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts new file mode 100644 index 0000000000..7dbdfca0bc --- /dev/null +++ b/src/file/drawing/anchor/anchor.ts @@ -0,0 +1,88 @@ +// http://officeopenxml.com/drwPicFloating.php +import { IMediaDataDimensions } from "file/media"; +import { XmlComponent } from "file/xml-components"; +import { IDrawingOptions } from "../drawing"; +import { + HorizontalPosition, + HorizontalPositionRelativeFrom, + IFloating, + SimplePos, + VerticalPosition, + VerticalPositionRelativeFrom, +} from "../floating"; +import { Graphic } from "../inline/graphic"; +import { TextWrapStyle, WrapNone, WrapSquare, WrapTight, WrapTopAndBottom } from "../text-wrap"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { AnchorAttributes } from "./anchor-attributes"; + +const defaultOptions: IFloating = { + allowOverlap: true, + behindDocument: false, + lockAnchor: false, + layoutInCell: true, + verticalPosition: { + relative: VerticalPositionRelativeFrom.PARAGRAPH, + offset: 0, + }, + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.COLUMN, + offset: 0, + }, +}; + +export class Anchor extends XmlComponent { + constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { + super("wp:anchor"); + + const floating = { + ...defaultOptions, + ...drawingOptions.floating, + }; + this.root.push( + new AnchorAttributes({ + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", // note: word doesn't fully support - so we use 0 + allowOverlap: floating.allowOverlap === true ? "1" : "0", + behindDoc: floating.behindDocument === true ? "1" : "0", + locked: floating.lockAnchor === true ? "1" : "0", + layoutInCell: floating.layoutInCell === true ? "1" : "0", + relativeHeight: dimensions.emus.y, + }), + ); + + this.root.push(new SimplePos()); + this.root.push(new HorizontalPosition(floating.horizontalPosition)); + this.root.push(new VerticalPosition(floating.verticalPosition)); + this.root.push(new Extent(dimensions.emus.x, dimensions.emus.y)); + this.root.push(new EffectExtent()); + + if (drawingOptions.textWrapping != null) { + switch (drawingOptions.textWrapping.textWrapStyle) { + case TextWrapStyle.SQUARE: + this.root.push(new WrapSquare(drawingOptions.textWrapping)); + break; + case TextWrapStyle.TIGHT: + this.root.push(new WrapTight(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.TOP_AND_BOTTOM: + this.root.push(new WrapTopAndBottom(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.NONE: + default: + this.root.push(new WrapNone()); + } + } else { + this.root.push(new WrapNone()); + } + + this.root.push(new DocProperties()); + this.root.push(new GraphicFrameProperties()); + this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y)); + } +} diff --git a/src/file/drawing/anchor/index.ts b/src/file/drawing/anchor/index.ts new file mode 100644 index 0000000000..57faf47fc0 --- /dev/null +++ b/src/file/drawing/anchor/index.ts @@ -0,0 +1,2 @@ +export * from "./anchor"; +export * from "./anchor-attributes"; diff --git a/src/file/drawing/inline/doc-properties/doc-properties-attributes.ts b/src/file/drawing/doc-properties/doc-properties-attributes.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties-attributes.ts rename to src/file/drawing/doc-properties/doc-properties-attributes.ts diff --git a/src/file/drawing/inline/doc-properties/doc-properties.ts b/src/file/drawing/doc-properties/doc-properties.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties.ts rename to src/file/drawing/doc-properties/doc-properties.ts diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index 9b113da0bf..cf3f926315 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -2,14 +2,12 @@ import { assert } from "chai"; import * as fs from "fs"; import { Utility } from "../../tests/utility"; -import { Drawing } from "./"; +import { Drawing, IDrawingOptions, PlacementPosition } from "./"; -describe("Drawing", () => { - let currentBreak: Drawing; - - beforeEach(() => { - const path = "./demo/images/image1.jpeg"; - currentBreak = new Drawing({ +function createDrawing(drawingOptions?: IDrawingOptions): Drawing { + const path = "./demo/images/image1.jpeg"; + return new Drawing( + { fileName: "test.jpg", referenceId: 1, stream: fs.createReadStream(path), @@ -24,14 +22,33 @@ describe("Drawing", () => { y: 100 * 9525, }, }, - }); - }); + }, + drawingOptions, + ); +} + +describe("Drawing", () => { + let currentBreak: Drawing; describe("#constructor()", () => { it("should create a Drawing with correct root key", () => { + currentBreak = createDrawing(); const newJson = Utility.jsonify(currentBreak); assert.equal(newJson.rootKey, "w:drawing"); - // console.log(JSON.stringify(newJson, null, 2)); + }); + + it("should create a drawing with inline element when there are no options passed", () => { + currentBreak = createDrawing(); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:inline"); + }); + + it("should create a drawing with anchor element when there options are passed", () => { + currentBreak = createDrawing({ + position: PlacementPosition.FLOATING, + }); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:anchor"); }); }); }); diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 61c93592df..4c3db93b3a 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -1,20 +1,53 @@ import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; +import { Anchor } from "./anchor"; +import { IFloating } from "./floating"; import { Inline } from "./inline"; +import { ITextWrapping } from "./text-wrap"; + +export enum PlacementPosition { + INLINE, + FLOATING, +} + +export interface IDistance { + distT?: number; + distB?: number; + distL?: number; + distR?: number; +} + +export interface IDrawingOptions { + position?: PlacementPosition; + textWrapping?: ITextWrapping; + floating?: IFloating; +} + +const defaultDrawingOptions: IDrawingOptions = { + position: PlacementPosition.INLINE, +}; export class Drawing extends XmlComponent { private inline: Inline; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super("w:drawing"); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.inline = new Inline(imageData.referenceId, imageData.dimensions); + const mergedOptions = { + ...defaultDrawingOptions, + ...drawingOptions, + }; - this.root.push(this.inline); + if (mergedOptions.position === PlacementPosition.INLINE) { + this.inline = new Inline(imageData.referenceId, imageData.dimensions); + this.root.push(this.inline); + } else if (mergedOptions.position === PlacementPosition.FLOATING) { + this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, mergedOptions)); + } } public scale(factorX: number, factorY: number): void { diff --git a/src/file/drawing/inline/effect-extent/effect-extent-attributes.ts b/src/file/drawing/effect-extent/effect-extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent-attributes.ts rename to src/file/drawing/effect-extent/effect-extent-attributes.ts diff --git a/src/file/drawing/inline/effect-extent/effect-extent.ts b/src/file/drawing/effect-extent/effect-extent.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent.ts rename to src/file/drawing/effect-extent/effect-extent.ts diff --git a/src/file/drawing/inline/extent/extent-attributes.ts b/src/file/drawing/extent/extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/extent/extent-attributes.ts rename to src/file/drawing/extent/extent-attributes.ts diff --git a/src/file/drawing/inline/extent/extent.ts b/src/file/drawing/extent/extent.ts similarity index 100% rename from src/file/drawing/inline/extent/extent.ts rename to src/file/drawing/extent/extent.ts diff --git a/src/file/drawing/floating/align.spec.ts b/src/file/drawing/floating/align.spec.ts new file mode 100644 index 0000000000..5ec77d6fd0 --- /dev/null +++ b/src/file/drawing/floating/align.spec.ts @@ -0,0 +1,15 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign } from "."; +import { Utility } from "../../../tests/utility"; +import { Align } from "./align"; + +describe("Align", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new Align(VerticalPositionAlign.CENTER)); + assert.equal(newJson.rootKey, "wp:align"); + assert.include(newJson.root[0], VerticalPositionAlign.CENTER); + }); + }); +}); diff --git a/src/file/drawing/floating/align.ts b/src/file/drawing/floating/align.ts new file mode 100644 index 0000000000..2ffa4ac52b --- /dev/null +++ b/src/file/drawing/floating/align.ts @@ -0,0 +1,10 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; +import { HorizontalPositionAlign, VerticalPositionAlign } from "./floating-position"; + +export class Align extends XmlComponent { + constructor(value: HorizontalPositionAlign | VerticalPositionAlign) { + super("wp:align"); + this.root.push(value); + } +} diff --git a/src/file/drawing/floating/floating-position.ts b/src/file/drawing/floating/floating-position.ts new file mode 100644 index 0000000000..7039846bc7 --- /dev/null +++ b/src/file/drawing/floating/floating-position.ts @@ -0,0 +1,60 @@ +// http://officeopenxml.com/drwPicFloating-position.php + +export enum HorizontalPositionRelativeFrom { + CHARACTER = "character", + COLUMN = "column", + INSIDE_MARGIN = "insideMargin", + LEFT_MARGIN = "leftMargin", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + RIGHT_MARGIN = "rightMargin", +} + +export enum VerticalPositionRelativeFrom { + BOTTOM_MARGIN = "bottomMargin", + INSIDE_MARGIN = "insideMargin", + LINE = "line", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + PARAGRAPH = "paragraph", + TOP_MARGIN = "topMargin", +} + +export enum HorizontalPositionAlign { + CENTER = "center", + INSIDE = "inside", + LEFT = "left", + OUTSIDE = "outside", + RIGHT = "right", +} + +export enum VerticalPositionAlign { + BOTTOM = "bottom", + CENTER = "center", + INSIDE = "inside", + OUTSIDE = "outside", + TOP = "top", +} + +export interface IHorizontalPositionOptions { + relative: HorizontalPositionRelativeFrom; + align?: HorizontalPositionAlign; + offset?: number; +} + +export interface IVerticalPositionOptions { + relative: VerticalPositionRelativeFrom; + align?: VerticalPositionAlign; + offset?: number; +} + +export interface IFloating { + horizontalPosition: IHorizontalPositionOptions; + verticalPosition: IVerticalPositionOptions; + allowOverlap?: boolean; + lockAnchor?: boolean; + behindDocument?: boolean; + layoutInCell?: boolean; +} diff --git a/src/file/drawing/floating/horizontal-position.spec.ts b/src/file/drawing/floating/horizontal-position.spec.ts new file mode 100644 index 0000000000..1b139b47be --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { HorizontalPositionAlign, HorizontalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { HorizontalPosition } from "./horizontal-position"; + +describe("HorizontalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + align: HorizontalPositionAlign.CENTER, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "center"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/horizontal-position.ts b/src/file/drawing/floating/horizontal-position.ts new file mode 100644 index 0000000000..f0725aa857 --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { HorizontalPositionRelativeFrom, IHorizontalPositionOptions } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IHorizontalPositionAttributes { + relativeFrom: HorizontalPositionRelativeFrom; +} + +class HorizontalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class HorizontalPosition extends XmlComponent { + constructor(horizontalPosition: IHorizontalPositionOptions) { + super("wp:positionH"); + + this.root.push( + new HorizontalPositionAttributes({ + relativeFrom: horizontalPosition.relative, + }), + ); + + if (horizontalPosition.align) { + this.root.push(new Align(horizontalPosition.align)); + } else if (horizontalPosition.offset !== undefined) { + this.root.push(new PositionOffset(horizontalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/floating/index.ts b/src/file/drawing/floating/index.ts new file mode 100644 index 0000000000..80061d16e1 --- /dev/null +++ b/src/file/drawing/floating/index.ts @@ -0,0 +1,4 @@ +export * from "./floating-position"; +export * from "./simple-pos"; +export * from "./horizontal-position"; +export * from "./vertical-position"; diff --git a/src/file/drawing/floating/position-offset.spec.ts b/src/file/drawing/floating/position-offset.spec.ts new file mode 100644 index 0000000000..74aebaebc2 --- /dev/null +++ b/src/file/drawing/floating/position-offset.spec.ts @@ -0,0 +1,14 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { PositionOffset } from "./position-offset"; + +describe("PositionOffset", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new PositionOffset(50)); + assert.equal(newJson.rootKey, "wp:posOffset"); + assert.equal(newJson.root[0], 50); + }); + }); +}); diff --git a/src/file/drawing/floating/position-offset.ts b/src/file/drawing/floating/position-offset.ts new file mode 100644 index 0000000000..4d3aa96b07 --- /dev/null +++ b/src/file/drawing/floating/position-offset.ts @@ -0,0 +1,9 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; + +export class PositionOffset extends XmlComponent { + constructor(offsetValue: number) { + super("wp:posOffset"); + this.root.push(offsetValue.toString()); + } +} diff --git a/src/file/drawing/floating/simple-pos.spec.ts b/src/file/drawing/floating/simple-pos.spec.ts new file mode 100644 index 0000000000..a86739b7b0 --- /dev/null +++ b/src/file/drawing/floating/simple-pos.spec.ts @@ -0,0 +1,17 @@ +import { assert } from "chai"; + +import { SimplePos } from "./simple-pos"; +import { Utility } from "../../../tests/utility"; + +describe("SimplePos", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new SimplePos()); + assert.equal(newJson.rootKey, "wp:simplePos"); + assert.include(newJson.root[0].root, { + x: 0, + y: 0, + }); + }); + }); +}); diff --git a/src/file/drawing/floating/simple-pos.ts b/src/file/drawing/floating/simple-pos.ts new file mode 100644 index 0000000000..6330f6660a --- /dev/null +++ b/src/file/drawing/floating/simple-pos.ts @@ -0,0 +1,28 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +interface ISimplePosAttributes { + x: number; + y: number; +} + +class SimplePosAttributes extends XmlAttributeComponent { + protected xmlKeys = { + x: "x", + y: "y", + }; +} + +export class SimplePos extends XmlComponent { + constructor() { + super("wp:simplePos"); + + // NOTE: It's not fully supported in Microsoft Word, but this element is needed anyway + this.root.push( + new SimplePosAttributes({ + x: 0, + y: 0, + }), + ); + } +} diff --git a/src/file/drawing/floating/vertical-position.spec.ts b/src/file/drawing/floating/vertical-position.spec.ts new file mode 100644 index 0000000000..a9d7ed65f8 --- /dev/null +++ b/src/file/drawing/floating/vertical-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign, VerticalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { VerticalPosition } from "./vertical-position"; + +describe("VerticalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + align: VerticalPositionAlign.INSIDE, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "inside"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/vertical-position.ts b/src/file/drawing/floating/vertical-position.ts new file mode 100644 index 0000000000..10b6d6028f --- /dev/null +++ b/src/file/drawing/floating/vertical-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { IVerticalPositionOptions, VerticalPositionRelativeFrom } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IVerticalPositionAttributes { + relativeFrom: VerticalPositionRelativeFrom; +} + +class VerticalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class VerticalPosition extends XmlComponent { + constructor(verticalPosition: IVerticalPositionOptions) { + super("wp:positionV"); + + this.root.push( + new VerticalPositionAttributes({ + relativeFrom: verticalPosition.relative, + }), + ); + + if (verticalPosition.align) { + this.root.push(new Align(verticalPosition.align)); + } else if (verticalPosition.offset !== undefined) { + this.root.push(new PositionOffset(verticalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts b/src/file/drawing/graphic-frame/graphic-frame-properties.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts rename to src/file/drawing/graphic-frame/graphic-frame-properties.ts diff --git a/src/file/drawing/index.ts b/src/file/drawing/index.ts index ba96e11de9..8a1a62a201 100644 --- a/src/file/drawing/index.ts +++ b/src/file/drawing/index.ts @@ -1 +1,3 @@ -export { Drawing } from "./drawing"; +export * from "./drawing"; +export * from "./text-wrap"; +export * from "./floating"; diff --git a/src/file/drawing/inline/inline-attributes.ts b/src/file/drawing/inline/inline-attributes.ts index 1a4ef74e3c..5f7489188c 100644 --- a/src/file/drawing/inline/inline-attributes.ts +++ b/src/file/drawing/inline/inline-attributes.ts @@ -1,11 +1,8 @@ import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; -export interface IInlineAttributes { - distT?: number; - distB?: number; - distL?: number; - distR?: number; -} +// tslint:disable-next-line:no-empty-interface +export interface IInlineAttributes extends IDistance {} export class InlineAttributes extends XmlAttributeComponent { protected xmlKeys = { diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index 0205eb3090..6e5be2ba13 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,11 +1,11 @@ // http://officeopenxml.com/drwPicInline.php import { IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { DocProperties } from "./doc-properties/doc-properties"; -import { EffectExtent } from "./effect-extent/effect-extent"; -import { Extent } from "./extent/extent"; -import { Graphic } from "./graphic"; -import { GraphicFrameProperties } from "./graphic-frame/graphic-frame-properties"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { Graphic } from "./../inline/graphic"; import { InlineAttributes } from "./inline-attributes"; export class Inline extends XmlComponent { diff --git a/src/file/drawing/text-wrap/index.ts b/src/file/drawing/text-wrap/index.ts new file mode 100644 index 0000000000..ce8c0bbd13 --- /dev/null +++ b/src/file/drawing/text-wrap/index.ts @@ -0,0 +1,5 @@ +export * from "./text-wrapping"; +export * from "./wrap-none"; +export * from "./wrap-square"; +export * from "./wrap-tight"; +export * from "./wrap-top-and-bottom"; diff --git a/src/file/drawing/text-wrap/text-wrapping.ts b/src/file/drawing/text-wrap/text-wrapping.ts new file mode 100644 index 0000000000..7fc14a52fd --- /dev/null +++ b/src/file/drawing/text-wrap/text-wrapping.ts @@ -0,0 +1,22 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { IDistance } from "../drawing"; + +export enum TextWrapStyle { + NONE, + SQUARE, + TIGHT, + TOP_AND_BOTTOM, +} + +export enum WrapTextOption { + BOTH_SIDES = "bothSides", + LEFT = "left", + RIGHT = "right", + LARGEST = "largest", +} + +export interface ITextWrapping { + textWrapStyle: TextWrapStyle; + wrapTextOption?: WrapTextOption; + distanceFromText?: IDistance; +} diff --git a/src/file/drawing/text-wrap/wrap-none.ts b/src/file/drawing/text-wrap/wrap-none.ts new file mode 100644 index 0000000000..0ac4c632f0 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-none.ts @@ -0,0 +1,8 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlComponent } from "file/xml-components"; + +export class WrapNone extends XmlComponent { + constructor() { + super("wp:wrapNone"); + } +} diff --git a/src/file/drawing/text-wrap/wrap-square.ts b/src/file/drawing/text-wrap/wrap-square.ts new file mode 100644 index 0000000000..08ed108209 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-square.ts @@ -0,0 +1,31 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { ITextWrapping, WrapTextOption } from "."; +import { IDistance } from "../drawing"; + +interface IWrapSquareAttributes extends IDistance { + wrapText?: WrapTextOption; +} + +class WrapSquareAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + wrapText: "wrapText", + }; +} + +export class WrapSquare extends XmlComponent { + constructor(textWrapping: ITextWrapping) { + super("wp:wrapSquare"); + + this.root.push( + new WrapSquareAttributes({ + wrapText: textWrapping.wrapTextOption || WrapTextOption.BOTH_SIDES, + ...textWrapping.distanceFromText, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-tight.ts b/src/file/drawing/text-wrap/wrap-tight.ts new file mode 100644 index 0000000000..cda9a20194 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-tight.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTightAttributes { + distT?: number; + distB?: number; +} + +class WrapTightAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTight extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTight"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTightAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-top-and-bottom.ts b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts new file mode 100644 index 0000000000..bf6a5c3cae --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTopAndBottomAttributes { + distT?: number; + distB?: number; +} + +class WrapTopAndBottomAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTopAndBottom extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTopAndBottom"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTopAndBottomAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/file.ts b/src/file/file.ts index e3250d93a6..ae9cf9deac 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -3,10 +3,11 @@ import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { Document } from "./document"; +import { FooterReferenceType, HeaderReference, HeaderReferenceType } from "./document/body/section-properties"; import { SectionPropertiesOptions } from "./document/body/section-properties/section-properties"; import { FooterWrapper } from "./footer-wrapper"; import { FootNotes } from "./footnotes"; -import { FirstPageHeaderWrapper, HeaderWrapper } from "./header-wrapper"; +import { HeaderWrapper } from "./header-wrapper"; import { Media } from "./media"; import { Numbering } from "./numbering"; import { Hyperlink, Paragraph, PictureRun } from "./paragraph"; @@ -24,18 +25,16 @@ export class File { private readonly media: Media; private readonly docRelationships: Relationships; private readonly fileRelationships: Relationships; - private readonly headerWrapper: HeaderWrapper; + private readonly headerWrapper: HeaderWrapper[] = []; + private readonly footerWrapper: FooterWrapper[] = []; private readonly footNotes: FootNotes; - private readonly firstPageHeaderWrapper: FirstPageHeaderWrapper; - - private readonly footerWrapper: FooterWrapper; private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; - constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { - this.document = new Document(sectionPropertiesOptions); + private nextId: number = 1; + constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { if (!options) { options = { creator: "Un-named", @@ -56,45 +55,27 @@ export class File { this.numbering = new Numbering(); this.docRelationships = new Relationships(); this.docRelationships.createRelationship( - 1, + this.nextId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "styles.xml", ); this.docRelationships.createRelationship( - 2, + this.nextId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "numbering.xml", ); - this.docRelationships.createRelationship( - 3, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - "header1.xml", - ); + this.contentTypes = new ContentTypes(); this.docRelationships.createRelationship( - 5, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - "header2.xml", - ); - - this.docRelationships.createRelationship( - 4, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", - "footer1.xml", - ); - - this.docRelationships.createRelationship( - 6, + this.nextId++, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", "footnotes.xml", ); this.media = new Media(); - this.headerWrapper = new HeaderWrapper(this.media); - this.firstPageHeaderWrapper = new FirstPageHeaderWrapper(this.media); + const header = this.createHeader(); + const footer = this.createFooter(); - this.footerWrapper = new FooterWrapper(this.media); - this.contentTypes = new ContentTypes(); this.fileRelationships = new Relationships(); this.fileRelationships.createRelationship( 1, @@ -112,7 +93,20 @@ export class File { "docProps/app.xml", ); this.appProperties = new AppProperties(); + this.footNotes = new FootNotes(); + if (!sectionPropertiesOptions) { + sectionPropertiesOptions = { + footerType: FooterReferenceType.DEFAULT, + headerType: HeaderReferenceType.DEFAULT, + headerId: header.Header.referenceId, + footerId: footer.Footer.referenceId, + }; + } else { + sectionPropertiesOptions.headerId = header.Header.referenceId; + sectionPropertiesOptions.footerId = footer.Footer.referenceId; + } + this.document = new Document(sectionPropertiesOptions); } public addParagraph(paragraph: Paragraph): void { @@ -132,7 +126,7 @@ export class File { } public createImage(image: string): PictureRun { - const mediaData = this.media.addMedia(image, this.docRelationships.RelationshipCount); + const mediaData = this.media.addMedia(image, this.nextId++); this.docRelationships.createRelationship( mediaData.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", @@ -142,7 +136,7 @@ export class File { } public createImageData(imageName: string, data: Buffer, width?: number, height?: number): IMediaData { - const mediaData = this.media.addMediaWithData(imageName, data, this.docRelationships.RelationshipCount, width, height); + const mediaData = this.media.addMediaWithData(imageName, data, this.nextId++, width, height); this.docRelationships.createRelationship( mediaData.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", @@ -163,10 +157,57 @@ export class File { return hyperlink; } + public addSection(sectionPropertiesOptions: SectionPropertiesOptions): void { + this.document.Body.addSection(sectionPropertiesOptions); + } + public createFootnote(paragraph: Paragraph): void { this.footNotes.createFootNote(paragraph); } + /** + * Creates new header. + */ + public createHeader(): HeaderWrapper { + const header = new HeaderWrapper(this.media, this.nextId++); + this.headerWrapper.push(header); + this.docRelationships.createRelationship( + header.Header.referenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", + `header${this.headerWrapper.length}.xml`, + ); + this.contentTypes.addHeader(this.headerWrapper.length); + return header; + } + + /** + * Creates new footer. + */ + public createFooter(): FooterWrapper { + const footer = new FooterWrapper(this.media, this.nextId++); + this.footerWrapper.push(footer); + this.docRelationships.createRelationship( + footer.Footer.referenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", + `footer${this.footerWrapper.length}.xml`, + ); + this.contentTypes.addFooter(this.footerWrapper.length); + return footer; + } + + public createFirstPageHeader(): HeaderWrapper { + const headerWrapper = this.createHeader(); + + this.document.Body.DefaultSection.addChildElement( + new HeaderReference({ + headerType: HeaderReferenceType.FIRST, + headerId: headerWrapper.Header.referenceId, + }), + ); + + return headerWrapper; + } + public get Document(): Document { return this.document; } @@ -196,17 +237,37 @@ export class File { } public get Header(): HeaderWrapper { + return this.headerWrapper[0]; + } + + public get Headers(): HeaderWrapper[] { return this.headerWrapper; } - public get firstPageHeader(): FirstPageHeaderWrapper { - return this.firstPageHeaderWrapper; + public HeaderByRefNumber(refId: number): HeaderWrapper { + const entry = this.headerWrapper.find((h) => h.Header.referenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no header with given reference id ${refId}`); } public get Footer(): FooterWrapper { + return this.footerWrapper[0]; + } + + public get Footers(): FooterWrapper[] { return this.footerWrapper; } + public FooterByRefNumber(refId: number): FooterWrapper { + const entry = this.footerWrapper.find((h) => h.Footer.referenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no footer with given reference id ${refId}`); + } + public get ContentTypes(): ContentTypes { return this.contentTypes; } diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index bb6efbe25a..63c8a58978 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -1,3 +1,4 @@ +import { XmlComponent } from "file/xml-components"; import { Footer } from "./footer/footer"; import { IMediaData, Media } from "./media"; import { Paragraph } from "./paragraph"; @@ -8,8 +9,8 @@ export class FooterWrapper { private readonly footer: Footer; private readonly relationships: Relationships; - constructor(private readonly media: Media) { - this.footer = new Footer(); + constructor(private readonly media: Media, referenceId: number) { + this.footer = new Footer(referenceId); this.relationships = new Relationships(); } @@ -35,6 +36,10 @@ export class FooterWrapper { this.footer.addDrawing(imageData); } + public addChildElement(childElement: XmlComponent | string): void { + this.footer.addChildElement(childElement); + } + public createImage(image: string): void { const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount); this.relationships.createRelationship( diff --git a/src/file/footer/footer.ts b/src/file/footer/footer.ts index 9a64909caa..cbdd4a544d 100644 --- a/src/file/footer/footer.ts +++ b/src/file/footer/footer.ts @@ -6,8 +6,10 @@ import { Table } from "../table"; import { FooterAttributes } from "./footer-attributes"; export class Footer extends XmlComponent { - constructor() { + private refId: number; + constructor(referenceNumber: number) { super("w:ftr"); + this.refId = referenceNumber; this.root.push( new FooterAttributes({ wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", @@ -30,6 +32,10 @@ export class Footer extends XmlComponent { ); } + public get referenceId(): number { + return this.refId; + } + public addParagraph(paragraph: Paragraph): void { this.root.push(paragraph); } diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 5a0c249b42..c23f4db448 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -1,65 +1,16 @@ +import { XmlComponent } from "file/xml-components"; import { Header } from "./header/header"; import { IMediaData, Media } from "./media"; import { Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; -export class FirstPageHeaderWrapper { - private readonly header: Header; - private readonly relationships: Relationships; - - constructor(private readonly media: Media) { - this.header = new Header(); - this.relationships = new Relationships(); - } - - public addParagraph(paragraph: Paragraph): void { - this.header.addParagraph(paragraph); - } - - public createParagraph(text?: string): Paragraph { - const para = new Paragraph(text); - this.addParagraph(para); - return para; - } - - public addTable(table: Table): void { - this.header.addTable(table); - } - - public createTable(rows: number, cols: number): Table { - return this.header.createTable(rows, cols); - } - - public addDrawing(imageData: IMediaData): void { - this.header.addDrawing(imageData); - } - - public createImage(image: string): void { - const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount); - this.relationships.createRelationship( - mediaData.referenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - this.addDrawing(mediaData); - } - - public get Header(): Header { - return this.header; - } - - public get Relationships(): Relationships { - return this.relationships; - } -} - export class HeaderWrapper { private readonly header: Header; private readonly relationships: Relationships; - constructor(private readonly media: Media) { - this.header = new Header(); + constructor(private readonly media: Media, referenceId: number) { + this.header = new Header(referenceId); this.relationships = new Relationships(); } @@ -85,6 +36,10 @@ export class HeaderWrapper { this.header.addDrawing(imageData); } + public addChildElement(childElement: XmlComponent | string): void { + this.header.addChildElement(childElement); + } + public createImage(image: string): void { const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount); this.relationships.createRelationship( diff --git a/src/file/header/header.ts b/src/file/header/header.ts index b4dd0918ef..b0ffaa8a63 100644 --- a/src/file/header/header.ts +++ b/src/file/header/header.ts @@ -6,8 +6,10 @@ import { Table } from "../table"; import { HeaderAttributes } from "./header-attributes"; export class Header extends XmlComponent { - constructor() { + private refId: number; + constructor(referenceNumber: number) { super("w:hdr"); + this.refId = referenceNumber; this.root.push( new HeaderAttributes({ wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", @@ -30,6 +32,10 @@ export class Header extends XmlComponent { ); } + get referenceId(): number { + return this.refId; + } + public addParagraph(paragraph: Paragraph): void { this.root.push(paragraph); } diff --git a/src/file/index.ts b/src/file/index.ts index 42a9ca3b76..d1e04ffcf7 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -4,5 +4,6 @@ export * from "./file"; export * from "./numbering"; export * from "./media"; export * from "./drawing"; +export * from "./document"; export * from "./styles"; export * from "./xml-components"; diff --git a/src/file/media/media.ts b/src/file/media/media.ts index ee4ca9ae15..54450fb714 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -21,13 +21,13 @@ export class Media { return data; } - public addMedia(filePath: string, relationshipsCount: number): IMediaData { + public addMedia(filePath: string, referenceId: number): IMediaData { const key = path.basename(filePath); const dimensions = sizeOf(filePath); - return this.createMedia(key, relationshipsCount, dimensions, fs.createReadStream(filePath), filePath); + return this.createMedia(key, referenceId, dimensions, fs.createReadStream(filePath), filePath); } - public addMediaWithData(fileName: string, data: Buffer, relationshipsCount: number, width?: number, height?: number): IMediaData { + public addMediaWithData(fileName: string, data: Buffer, referenceId: number, width?: number, height?: number): IMediaData { const key = fileName; let dimensions; if (width && height) { @@ -39,7 +39,7 @@ export class Media { dimensions = sizeOf(data); } - return this.createMedia(key, relationshipsCount, dimensions, data); + return this.createMedia(key, referenceId, dimensions, data); } private createMedia( diff --git a/src/file/paragraph/run/picture-run.ts b/src/file/paragraph/run/picture-run.ts index 1b256ea20d..6a07b3bd9e 100644 --- a/src/file/paragraph/run/picture-run.ts +++ b/src/file/paragraph/run/picture-run.ts @@ -1,18 +1,19 @@ import { Drawing } from "../../drawing"; +import { IDrawingOptions } from "../../drawing/drawing"; import { IMediaData } from "../../media/data"; import { Run } from "../run"; export class PictureRun extends Run { private drawing: Drawing; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super(); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.drawing = new Drawing(imageData); + this.drawing = new Drawing(imageData, drawingOptions); this.root.push(this.drawing); } diff --git a/src/file/styles/external-styles-factory.ts b/src/file/styles/external-styles-factory.ts index f1bc9c68c6..2ebd6323bd 100644 --- a/src/file/styles/external-styles-factory.ts +++ b/src/file/styles/external-styles-factory.ts @@ -1,13 +1,6 @@ import * as fastXmlParser from "fast-xml-parser"; - +import { convertToXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent, parseOptions } from "file/xml-components"; import { Styles } from "./"; -import { ImportedRootElementAttributes, ImportedXmlComponent } from "./../../file/xml-components"; - -const parseOptions = { - ignoreAttributes: false, - attributeNamePrefix: "", - attrNodeName: "_attr", -}; export class ExternalStylesFactory { /** @@ -45,20 +38,7 @@ export class ExternalStylesFactory { }); // convert the styles one by one - xmlStyles["w:style"].map((style) => this.convertElement("w:style", style)).forEach(importedStyle.push.bind(importedStyle)); - + xmlStyles["w:style"].map((style) => convertToXmlComponent("w:style", style)).forEach(importedStyle.push.bind(importedStyle)); return importedStyle; } - - // tslint:disable-next-line:no-any - public 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/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts index d7b638ba9f..e941d3f0ec 100644 --- a/src/file/xml-components/imported-xml-component.spec.ts +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -1,5 +1,52 @@ import { expect } from "chai"; -import { ImportedXmlComponent } from "./"; +import { ImportedXmlComponent, convertToXmlComponent } from "./"; + +const xmlString = ` + + + some value + + + Text 1 + + + Text 2 + + + `; + +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" }], + }, +}; + +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" }, + }, + ], + _attr: { "w:one": "value 1", "w:two": "value 2" }, +}; describe("ImportedXmlComponent", () => { let importedXmlComponent: ImportedXmlComponent; @@ -31,4 +78,16 @@ describe("ImportedXmlComponent", () => { }); }); }); + + it("should create XmlComponent from xml string", () => { + const converted = ImportedXmlComponent.fromXmlString(xmlString); + expect(converted).to.eql(convertedXmlElement); + }); + + describe("convertToXmlComponent", () => { + it("should convert to xml component", () => { + const converted = convertToXmlComponent("w:p", importedXmlElement["w:p"]); + 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 853c84b462..23c4137900 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -1,6 +1,54 @@ -// tslint:disable:no-any -// tslint:disable:variable-name -import { IXmlableObject, XmlComponent } from "./"; +/* tslint:disable */ +import { XmlComponent, IXmlableObject } from "."; +import * as fastXmlParser from "fast-xml-parser"; +import { flatMap } from "lodash"; + +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); + } + }); + } else if (element !== "") { + xmlElement.push(element); + } + return xmlElement; +} /** * Represents imported xml component from xml file. @@ -8,11 +56,10 @@ import { IXmlableObject, XmlComponent } from "./"; export class ImportedXmlComponent extends XmlComponent { private _attr: any; - constructor(rootKey: string, attr?: any) { + constructor(rootKey: string, _attr?: any) { super(rootKey); - - if (attr) { - this._attr = attr; + if (_attr) { + this._attr = _attr; } } @@ -42,7 +89,7 @@ export class ImportedXmlComponent extends XmlComponent { * ] * } */ - public prepForXml(): IXmlableObject { + prepForXml(): IXmlableObject { const result = super.prepForXml(); if (!!this._attr) { if (!Array.isArray(result[this.rootKey])) { @@ -53,9 +100,26 @@ export class ImportedXmlComponent extends XmlComponent { return result; } - public push(xmlComponent: XmlComponent): void { + push(xmlComponent: XmlComponent) { this.root.push(xmlComponent); } + + /** + * Converts the xml string to a XmlComponent tree. + * + * @param importedContent xml content of the imported component + */ + 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; + } } /**