diff --git a/package.json b/package.json index cfebd90317..2a6ec009e3 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,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..c87a39a0e8 --- /dev/null +++ b/src/export/packer/compiler.spec.ts @@ -0,0 +1,76 @@ +/* tslint:disable:typedef space-before-function-paren */ +import * as fs from "fs"; + +import { Compiler } from "./compiler"; +import { File } from "../../file"; +import { expect } from "chai"; + +import * as JSZip from "jszip"; + +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 44c7817249..456976d9d6 100644 --- a/src/export/packer/compiler.ts +++ b/src/export/packer/compiler.ts @@ -33,10 +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 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)); @@ -60,26 +56,34 @@ 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(xmlFooter, { - name: "word/footer1.xml", - }); + this.archive.append(xml(this.formatter.format(element.Relationships)), { + name: `word/_rels/header${i + 1}.xml.rels`, + }); + } + + // 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(xmlRelationships, { 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 4ce020a918..bfc7f6d8d0 100644 --- a/src/file/content-types/content-types.ts +++ b/src/file/content-types/content-types.ts @@ -24,11 +24,21 @@ export class ContentTypes extends XmlComponent { this.root.push( 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.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")); } + + addFooter(index: number) { + this.root.push( + new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", `/word/footer${index}.xml`), + ); + } + + addHeader(index: number) { + 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..691dfd7518 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -1,14 +1,59 @@ -import { XmlComponent } from "file/xml-components"; +import { XmlComponent, IXmlableObject } from "file/xml-components"; import { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties"; +import { Paragraph, ParagraphProperties } from "../.."; 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 + */ + addSection(section: SectionPropertiesOptions | SectionProperties) { + const currentSection = this.sections.pop() as SectionProperties; + this.root.push(this.createSectionParagraph(currentSection)); + if (section instanceof SectionProperties) { + this.sections.push(section); + } else { + this.sections.push(new SectionProperties(section)); + } + } + 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() { + return this.defaultSection; + } + + private createSectionParagraph(section: SectionProperties) { + 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..3805676841 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 FooterOptions { + footerType?: FooterReferenceType; + footerId?: number; +} export class FooterReference extends XmlComponent { - constructor() { + constructor(options: FooterOptions) { 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 3809047bef..4065ed5253 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 HeaderOptions { + headerType?: HeaderReferenceType; + headerId?: number; +} export class HeaderReference extends XmlComponent { - constructor() { + constructor(options: HeaderOptions) { super("w:headerReference"); this.root.push( new HeaderReferenceAttributes({ - type: "default", - id: `rId${3}`, + 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..918b1e644c --- /dev/null +++ b/src/file/document/body/section-properties/page-number/page-number.ts @@ -0,0 +1,41 @@ +// http://officeopenxml.com/WPSectionPgNumType.php +import { XmlComponent, XmlAttributeComponent } 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..bf5499d3f1 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 { PageSizeAttributes, PageOrientation } 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 27d3f0bd99..993812bd5e 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -4,16 +4,25 @@ 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, FooterOptions } from "./footer-reference/footer-reference"; +import { HeaderReference, HeaderOptions } from "./header-reference/header-reference"; 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 { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; +import { FooterReferenceType, IPageNumberTypeAttributes, PageNumberType, PageNumberFormat } from "."; +import { HeaderReferenceType } from "./header-reference/header-reference-attributes"; -export type SectionPropertiesOptions = IPageSizeAttributes & IPageMarginAttributes & IColumnsAttributes & IDocGridAttributesProperties; +export type SectionPropertiesOptions = IPageSizeAttributes & + IPageMarginAttributes & + IColumnsAttributes & + IDocGridAttributesProperties & + HeaderOptions & + FooterOptions & + IPageNumberTypeAttributes; export class SectionProperties extends XmlComponent { + private options: SectionPropertiesOptions; constructor(options?: SectionPropertiesOptions) { super("w:sectPr"); @@ -29,7 +38,13 @@ export class SectionProperties extends XmlComponent { gutter: 0, space: 708, linePitch: 360, - orientation: "portrait", + orientation: PageOrientation.PORTRAIT, + headerType: HeaderReferenceType.DEFAULT, + headerId: 0, + footerType: FooterReferenceType.DEFAULT, + footerId: 0, + pageNumberStart: undefined, + pageNumberFormatType: PageNumberFormat.DECIMAL, }; const mergedOptions = { @@ -51,7 +66,25 @@ export class SectionProperties extends XmlComponent { ); this.root.push(new Columns(mergedOptions.space)); this.root.push(new DocumentGrid(mergedOptions.linePitch)); - this.root.push(new HeaderReference()); - this.root.push(new FooterReference()); + 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 PageNumberType(mergedOptions.pageNumberStart, mergedOptions.pageNumberFormatType)); + + this.options = mergedOptions; + } + + get Options() { + 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 f4d580b9ca..a71bcbffe9 100644 --- a/src/file/document/document.ts +++ b/src/file/document/document.ts @@ -70,4 +70,8 @@ export class Document extends XmlComponent { return; } + + get 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/file.ts b/src/file/file.ts index dce3ef24b1..a42e542e33 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -14,6 +14,7 @@ import { DefaultStylesFactory } from "./styles/factory"; import { ExternalStylesFactory } from "./styles/external-styles-factory"; import { Table } from "./table"; import { IMediaData } from "index"; +import { FooterReferenceType, HeaderReferenceType } from "./document/body/section-properties"; export class File { private readonly document: Document; @@ -23,14 +24,14 @@ export class File { private readonly media: Media; private readonly docRelationships: Relationships; private readonly fileRelationships: Relationships; - private readonly headerWrapper: HeaderWrapper; - private readonly footerWrapper: FooterWrapper; + private readonly headerWrapper: HeaderWrapper[] = []; + 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", @@ -51,29 +52,36 @@ 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.contentTypes = new ContentTypes(); + this.media = new Media(); + + const header = new HeaderWrapper(this.media, this.nextId++); + this.headerWrapper.push(header); this.docRelationships.createRelationship( - 3, + header.Header.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - "header1.xml", + `header1.xml`, ); + this.contentTypes.addHeader(this.headerWrapper.length); + + const footer = new FooterWrapper(this.media, this.nextId++); + this.footerWrapper.push(footer); this.docRelationships.createRelationship( - 4, + footer.Footer.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", "footer1.xml", ); - this.media = new Media(); - this.headerWrapper = new HeaderWrapper(this.media); - this.footerWrapper = new FooterWrapper(this.media); - this.contentTypes = new ContentTypes(); + this.contentTypes.addFooter(this.footerWrapper.length); + this.fileRelationships = new Relationships(); this.fileRelationships.createRelationship( 1, @@ -91,6 +99,19 @@ export class File { "docProps/app.xml", ); this.appProperties = new AppProperties(); + + 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 { @@ -110,7 +131,7 @@ export class File { } public createImage(image: string): void { - 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", @@ -120,7 +141,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", @@ -129,6 +150,40 @@ export class File { return mediaData; } + public addSection(sectionPropertiesOptions: SectionPropertiesOptions) { + this.document.Body.addSection(sectionPropertiesOptions); + } + + /** + * 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 get Document(): Document { return this.document; } @@ -158,13 +213,33 @@ export class File { } public get Header(): HeaderWrapper { + return this.headerWrapper[0]; + } + + public get Headers(): HeaderWrapper[] { return this.headerWrapper; } + 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 47d0f73928..8e0ab04496 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -9,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(); } diff --git a/src/file/footer/footer.ts b/src/file/footer/footer.ts index 9a64909caa..34ee22a43e 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 { ); } + get referenceId() { + 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 250919b489..5c1a389a24 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -9,8 +9,8 @@ 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(); } diff --git a/src/file/header/header.ts b/src/file/header/header.ts index b4dd0918ef..f1a9f566c4 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() { + 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 69435eb21f..7f384f9f98 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -11,9 +11,9 @@ export class Media { this.map = new Map(); } - private createMedia(key: string, relationshipsCount, dimensions, data: fs.ReadStream | Buffer, filePath?: string) { + private createMedia(key: string, referenceId, dimensions, data: fs.ReadStream | Buffer, filePath?: string) { const imageData = { - referenceId: this.map.size + relationshipsCount + 1, + referenceId: referenceId, stream: data, path: filePath, fileName: key, @@ -42,13 +42,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?, height?): IMediaData { + public addMediaWithData(fileName: string, data: Buffer, referenceId: number, width?, height?): IMediaData { const key = fileName; let dimensions; if (width && height) { @@ -60,7 +60,7 @@ export class Media { dimensions = sizeOf(data); } - return this.createMedia(key, relationshipsCount, dimensions, data); + return this.createMedia(key, referenceId, dimensions, data); } public get array(): IMediaData[] {