Merge branch 'feature/multiple-sections' of https://github.com/h4buli/docx into feat/multiple-headers

# Conflicts:
#	src/export/packer/compiler.ts
#	src/file/content-types/content-types.ts
#	src/file/document/body/section-properties/header-reference/header-reference.ts
#	src/file/document/body/section-properties/section-properties.ts
#	src/file/file.ts
#	src/file/media/media.ts
This commit is contained in:
Dolan Miu
2018-06-22 22:59:38 +01:00
33 changed files with 621 additions and 105 deletions

View File

@ -65,6 +65,7 @@
"awesome-typescript-loader": "^3.4.1", "awesome-typescript-loader": "^3.4.1",
"chai": "^3.5.0", "chai": "^3.5.0",
"glob": "^7.1.2", "glob": "^7.1.2",
"jszip": "^3.1.5",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"mocha-webpack": "^1.0.1", "mocha-webpack": "^1.0.1",
"prettier": "^1.12.1", "prettier": "^1.12.1",

View File

@ -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");
});
});
});

View File

@ -33,11 +33,7 @@ export class Compiler {
const xmlNumbering = xml(this.formatter.format(this.file.Numbering)); const xmlNumbering = xml(this.formatter.format(this.file.Numbering));
const xmlRelationships = xml(this.formatter.format(this.file.DocumentRelationships)); const xmlRelationships = xml(this.formatter.format(this.file.DocumentRelationships));
const xmlFileRelationships = xml(this.formatter.format(this.file.FileRelationships)); 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 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 xmlContentTypes = xml(this.formatter.format(this.file.ContentTypes));
const xmlAppProperties = xml(this.formatter.format(this.file.AppProperties)); const xmlAppProperties = xml(this.formatter.format(this.file.AppProperties));
@ -61,30 +57,38 @@ export class Compiler {
name: "word/numbering.xml", name: "word/numbering.xml",
}); });
this.archive.append(xmlHeader, { // headers
name: "word/header1.xml", 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, { this.archive.append(xmlHeader2, {
name: "word/header2.xml", name: "word/header2.xml",
}); });
this.archive.append(xmlFooter, { this.archive.append(xml(this.formatter.format(element.Relationships)), {
name: "word/footer1.xml", 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, { this.archive.append(xmlRelationships, {
name: "word/_rels/document.xml.rels", 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, { this.archive.append(xmlContentTypes, {
name: "[Content_Types].xml", name: "[Content_Types].xml",
}); });

View File

@ -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",
},
},
],
});
});
});
});

View File

@ -25,12 +25,22 @@ export class ContentTypes extends XmlComponent {
new Override("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "/word/document.xml"), 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.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-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-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.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.numbering+xml", "/word/numbering.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`),
);
}
} }

View File

@ -1,39 +1,42 @@
// import { assert } from "chai"; import { expect } from "chai";
// import { Utility } from "../../../tests/utility"; import { Formatter } from "../../../export/formatter";
// import { Body } from "./"; import { Body } from "./body";
describe("Body", () => { describe("Body", () => {
// let body: Body; let body: Body;
beforeEach(() => { beforeEach(() => {
// body = new Body(); body = new Body();
}); });
// describe("#constructor()", () => { describe("#constructor()", () => {
// it("should create the Section Properties", () => { it("should create default section", () => {
// const newJson = Utility.jsonify(body); const formatted = new Formatter().format(body)["w:body"][0];
// assert.equal(newJson.root[0].rootKey, "w:sectPr"); 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", () => { describe("addSection", () => {
// const newJson = Utility.jsonify(body); it("should add section with options", () => {
// assert.equal(newJson.root[1].rootKey, "w:pgSz"); body.addSection({
// }); width: 10000,
height: 10000,
});
// it("should create the Page Margin", () => { const formatted = new Formatter().format(body)["w:body"];
// const newJson = Utility.jsonify(body); expect(formatted).to.be.an.instanceof(Array);
// assert.equal(newJson.root[2].rootKey, "w:pgMar"); const defaultSectionPr = formatted[0]["w:p"][1]["w:pPr"][0]["w:sectPr"];
// });
// it("should create the Columns", () => { // check that this is the default section and added first in paragraph
// const newJson = Utility.jsonify(body); expect(defaultSectionPr[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 16838, "w:w": 11906, "w:orient": "portrait" } }] });
// assert.equal(newJson.root[3].rootKey, "w:cols");
// });
// it("should create the Document Grid", () => { // check for new section (since it's the last one, it's direct child of body)
// const newJson = Utility.jsonify(body); const newSection = formatted[1]["w:sectPr"];
// assert.equal(newJson.root[4].rootKey, "w:docGrid"); expect(newSection[0]).to.deep.equal({ "w:pgSz": [{ _attr: { "w:h": 10000, "w:w": 10000, "w:orient": "portrait" } }] });
// }); });
// }); });
}); });

View File

@ -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 { SectionProperties, SectionPropertiesOptions } from "./section-properties/section-properties";
import { Paragraph, ParagraphProperties } from "../..";
export class Body extends XmlComponent { export class Body extends XmlComponent {
private defaultSection: SectionProperties;
private sections: SectionProperties[] = [];
constructor(sectionPropertiesOptions?: SectionPropertiesOptions) { constructor(sectionPropertiesOptions?: SectionPropertiesOptions) {
super("w:body"); 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 { public push(component: XmlComponent): void {
this.root.push(component); 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;
}
} }

View File

@ -1 +1,2 @@
export * from "./body"; export * from "./body";
export * from "./section-properties";

View File

@ -1,5 +1,11 @@
import { XmlAttributeComponent } from "file/xml-components"; import { XmlAttributeComponent } from "file/xml-components";
export enum FooterReferenceType {
DEFAULT = "default",
FIRST = "first",
EVEN = "even",
}
export interface IFooterReferenceAttributes { export interface IFooterReferenceAttributes {
type: string; type: string;
id: string; id: string;

View File

@ -1,13 +1,19 @@
import { XmlComponent } from "file/xml-components"; 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 { export class FooterReference extends XmlComponent {
constructor() { constructor(options: FooterOptions) {
super("w:footerReference"); super("w:footerReference");
this.root.push( this.root.push(
new FooterReferenceAttributes({ new FooterReferenceAttributes({
type: "default", type: options.footerType || FooterReferenceType.DEFAULT,
id: `rId${4}`, id: `rId${options.footerId}`,
}), }),
); );
} }

View File

@ -0,0 +1,2 @@
export * from "./footer-reference";
export * from "./footer-reference-attributes";

View File

@ -1,5 +1,11 @@
import { XmlAttributeComponent } from "file/xml-components"; import { XmlAttributeComponent } from "file/xml-components";
export enum HeaderReferenceType {
DEFAULT = "default",
FIRST = "first",
EVEN = "even",
}
export interface IHeaderReferenceAttributes { export interface IHeaderReferenceAttributes {
type: string; type: string;
id: string; id: string;

View File

@ -1,13 +1,18 @@
import { XmlComponent } from "file/xml-components"; 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 { export class HeaderReference extends XmlComponent {
constructor(order: string, refID: number) { constructor(options: HeaderOptions) {
super("w:headerReference"); super("w:headerReference");
this.root.push( this.root.push(
new HeaderReferenceAttributes({ new HeaderReferenceAttributes({
type: order, type: options.headerType || HeaderReferenceType.DEFAULT,
id: `rId${refID}`, id: `rId${options.headerId}`,
}), }),
); );
} }

View File

@ -0,0 +1,2 @@
export * from "./header-reference";
export * from "./header-reference-attributes";

View File

@ -0,0 +1,5 @@
export * from "./section-properties";
export * from "./footer-reference";
export * from "./header-reference";
export * from "./page-size";
export * from "./page-number";

View File

@ -0,0 +1 @@
export * from "./page-number";

View File

@ -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<IPageNumberTypeAttributes> {
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,
}),
);
}
}

View File

@ -0,0 +1,2 @@
export * from "./page-size";
export * from "./page-size-attributes";

View File

@ -1,9 +1,14 @@
import { XmlAttributeComponent } from "file/xml-components"; import { XmlAttributeComponent } from "file/xml-components";
export enum PageOrientation {
PORTRAIT = "portrait",
LANDSCAPE = "landscape",
}
export interface IPageSizeAttributes { export interface IPageSizeAttributes {
width?: number; width?: number;
height?: number; height?: number;
orientation?: string; orientation?: PageOrientation;
} }
export class PageSizeAttributes extends XmlAttributeComponent<IPageSizeAttributes> { export class PageSizeAttributes extends XmlAttributeComponent<IPageSizeAttributes> {

View File

@ -2,11 +2,12 @@ import { expect } from "chai";
import { Formatter } from "../../../../../export/formatter"; import { Formatter } from "../../../../../export/formatter";
import { PageSize } from "./page-size"; import { PageSize } from "./page-size";
import { PageOrientation } from "./page-size-attributes";
describe("PageSize", () => { describe("PageSize", () => {
describe("#constructor()", () => { describe("#constructor()", () => {
it("should create page size with portrait", () => { 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); const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); 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", () => { 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); const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]); expect(Object.keys(tree)).to.deep.equal(["w:pgSz"]);

View File

@ -1,11 +1,11 @@
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { PageSizeAttributes } from "./page-size-attributes"; import { PageSizeAttributes, PageOrientation } from "./page-size-attributes";
export class PageSize extends XmlComponent { export class PageSize extends XmlComponent {
constructor(width: number, height: number, orientation: string) { constructor(width: number, height: number, orientation: PageOrientation) {
super("w:pgSz"); super("w:pgSz");
const flip = orientation === "landscape"; const flip = orientation === PageOrientation.LANDSCAPE;
this.root.push( this.root.push(
new PageSizeAttributes({ new PageSizeAttributes({

View File

@ -2,6 +2,7 @@ import { expect } from "chai";
import { Formatter } from "../../../../export/formatter"; import { Formatter } from "../../../../export/formatter";
import { SectionProperties } from "./section-properties"; import { SectionProperties } from "./section-properties";
import { FooterReferenceType, PageNumberFormat } from ".";
describe("SectionProperties", () => { describe("SectionProperties", () => {
describe("#constructor()", () => { describe("#constructor()", () => {
@ -18,6 +19,11 @@ describe("SectionProperties", () => {
gutter: 0, gutter: 0,
space: 708, space: 708,
linePitch: 360, linePitch: 360,
headerId: 100,
footerId: 200,
footerType: FooterReferenceType.EVEN,
pageNumberStart: 10,
pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT,
}); });
const tree = new Formatter().format(properties); const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); 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", () => { 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", () => { it("should create section properties with changed options", () => {

View File

@ -1,20 +1,29 @@
// http://officeopenxml.com/WPsection.php // http://officeopenxml.com/WPsection.php
import { XmlComponent } from "file/xml-components"; import { XmlComponent } from "file/xml-components";
import { FooterReferenceType, IPageNumberTypeAttributes, PageNumberFormat, PageNumberType } from "./";
import { Columns } from "./columns/columns"; import { Columns } from "./columns/columns";
import { IColumnsAttributes } from "./columns/columns-attributes"; import { IColumnsAttributes } from "./columns/columns-attributes";
import { DocumentGrid } from "./doc-grid/doc-grid"; import { DocumentGrid } from "./doc-grid/doc-grid";
import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes"; import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes";
import { FooterReference } from "./footer-reference/footer-reference"; import { FooterOptions, FooterReference } from "./footer-reference/footer-reference";
import { HeaderReference } from "./header-reference/header-reference"; import { HeaderOptions, HeaderReference } from "./header-reference/header-reference";
import { HeaderReferenceType } from "./header-reference/header-reference-attributes";
import { PageMargin } from "./page-margin/page-margin"; import { PageMargin } from "./page-margin/page-margin";
import { IPageMarginAttributes } from "./page-margin/page-margin-attributes"; import { IPageMarginAttributes } from "./page-margin/page-margin-attributes";
import { PageSize } from "./page-size/page-size"; 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 { TitlePage } from "./title-page/title-page"; import { TitlePage } from "./title-page/title-page";
export type SectionPropertiesOptions = IPageSizeAttributes & IPageMarginAttributes & IColumnsAttributes & IDocGridAttributesProperties; export type SectionPropertiesOptions = IPageSizeAttributes &
IPageMarginAttributes &
IColumnsAttributes &
IDocGridAttributesProperties &
HeaderOptions &
FooterOptions &
IPageNumberTypeAttributes;
export class SectionProperties extends XmlComponent { export class SectionProperties extends XmlComponent {
private options: SectionPropertiesOptions;
constructor(options?: SectionPropertiesOptions) { constructor(options?: SectionPropertiesOptions) {
super("w:sectPr"); super("w:sectPr");
@ -30,8 +39,14 @@ export class SectionProperties extends XmlComponent {
gutter: 0, gutter: 0,
space: 708, space: 708,
linePitch: 360, linePitch: 360,
orientation: "portrait", orientation: PageOrientation.PORTRAIT,
differentFirstPageHeader: false, differentFirstPageHeader: false,
headerType: HeaderReferenceType.DEFAULT,
headerId: 0,
footerType: FooterReferenceType.DEFAULT,
footerId: 0,
pageNumberStart: undefined,
pageNumberFormatType: PageNumberFormat.DECIMAL,
}; };
const mergedOptions = { const mergedOptions = {
@ -53,13 +68,36 @@ export class SectionProperties extends XmlComponent {
); );
this.root.push(new Columns(mergedOptions.space)); this.root.push(new Columns(mergedOptions.space));
this.root.push(new DocumentGrid(mergedOptions.linePitch)); this.root.push(new DocumentGrid(mergedOptions.linePitch));
this.root.push(new HeaderReference("default", 3));
if (mergedOptions.differentFirstPageHeader) { if (mergedOptions.differentFirstPageHeader) {
this.root.push(new HeaderReference("first", 5)); this.root.push(
new HeaderReference({
headerType: HeaderReferenceType.FIRST,
headerId: 5,
}),
);
this.root.push(new TitlePage()); this.root.push(new TitlePage());
} }
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(): SectionPropertiesOptions {
return this.options;
} }
} }

View File

@ -23,6 +23,11 @@ describe("Document", () => {
} }
assert.isTrue(true); 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", () => { describe("#createParagraph", () => {
@ -33,7 +38,7 @@ describe("Document", () => {
expect(body) expect(body)
.to.be.an("array") .to.be.an("array")
.which.has.length.at.least(1); .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", () => { it("should use the text given to create a run in the paragraph", () => {
@ -43,7 +48,7 @@ describe("Document", () => {
expect(body) expect(body)
.to.be.an("array") .to.be.an("array")
.which.has.length.at.least(1); .which.has.length.at.least(1);
expect(body[1]) expect(body[0])
.to.have.property("w:p") .to.have.property("w:p")
.which.includes({ .which.includes({
"w:r": [{ "w:rPr": [] }, { "w:t": [{ _attr: { "xml:space": "preserve" } }, "sample paragraph text"] }], "w:r": [{ "w:rPr": [] }, { "w:t": [{ _attr: { "xml:space": "preserve" } }, "sample paragraph text"] }],
@ -59,7 +64,7 @@ describe("Document", () => {
expect(body) expect(body)
.to.be.an("array") .to.be.an("array")
.which.has.length.at.least(1); .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", () => { it("should create a table with the correct dimensions", () => {
@ -68,7 +73,7 @@ describe("Document", () => {
expect(body) expect(body)
.to.be.an("array") .to.be.an("array")
.which.has.length.at.least(1); .which.has.length.at.least(1);
expect(body[1]) expect(body[0])
.to.have.property("w:tbl") .to.have.property("w:tbl")
.which.includes({ .which.includes({
"w:tblGrid": [ "w:tblGrid": [
@ -77,7 +82,7 @@ describe("Document", () => {
{ "w:gridCol": [{ _attr: { "w:w": 1 } }] }, { "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);
}); });
}); });
}); });

View File

@ -70,4 +70,8 @@ export class Document extends XmlComponent {
return run; return run;
} }
get Body() {
return this.body;
}
} }

View File

@ -1 +1,2 @@
export * from "./document"; export * from "./document";
export * from "./body";

View File

@ -3,6 +3,7 @@ import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types"; import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CoreProperties, IPropertiesOptions } from "./core-properties";
import { Document } from "./document"; import { Document } from "./document";
import { FooterReferenceType, HeaderReferenceType } from "./document/body/section-properties";
import { SectionPropertiesOptions } from "./document/body/section-properties/section-properties"; import { SectionPropertiesOptions } from "./document/body/section-properties/section-properties";
import { FooterWrapper } from "./footer-wrapper"; import { FooterWrapper } from "./footer-wrapper";
import { FirstPageHeaderWrapper, HeaderWrapper } from "./header-wrapper"; import { FirstPageHeaderWrapper, HeaderWrapper } from "./header-wrapper";
@ -23,17 +24,16 @@ export class File {
private readonly media: Media; private readonly media: Media;
private readonly docRelationships: Relationships; private readonly docRelationships: Relationships;
private readonly fileRelationships: Relationships; private readonly fileRelationships: Relationships;
private readonly headerWrapper: HeaderWrapper; private readonly headerWrapper: HeaderWrapper[] = [];
private readonly footerWrapper: FooterWrapper[] = [];
private readonly firstPageHeaderWrapper: FirstPageHeaderWrapper; private readonly firstPageHeaderWrapper: FirstPageHeaderWrapper;
private readonly footerWrapper: FooterWrapper;
private readonly contentTypes: ContentTypes; private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties; private readonly appProperties: AppProperties;
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { private nextId: number = 1;
this.document = new Document(sectionPropertiesOptions);
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
if (!options) { if (!options) {
options = { options = {
creator: "Un-named", creator: "Un-named",
@ -54,20 +54,29 @@ export class File {
this.numbering = new Numbering(); this.numbering = new Numbering();
this.docRelationships = new Relationships(); this.docRelationships = new Relationships();
this.docRelationships.createRelationship( this.docRelationships.createRelationship(
1, this.nextId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml", "styles.xml",
); );
this.docRelationships.createRelationship( this.docRelationships.createRelationship(
2, this.nextId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
"numbering.xml", "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( this.docRelationships.createRelationship(
3, header.Header.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", "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( this.docRelationships.createRelationship(
5, 5,
@ -76,17 +85,14 @@ export class File {
); );
this.docRelationships.createRelationship( this.docRelationships.createRelationship(
4, footer.Footer.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
"footer1.xml", "footer1.xml",
); );
this.media = new Media(); this.contentTypes.addFooter(this.footerWrapper.length);
this.headerWrapper = new HeaderWrapper(this.media); this.firstPageHeaderWrapper = new FirstPageHeaderWrapper(this.media, this.nextId++);
this.firstPageHeaderWrapper = new FirstPageHeaderWrapper(this.media);
this.footerWrapper = new FooterWrapper(this.media);
this.contentTypes = new ContentTypes();
this.fileRelationships = new Relationships(); this.fileRelationships = new Relationships();
this.fileRelationships.createRelationship( this.fileRelationships.createRelationship(
1, 1,
@ -104,6 +110,19 @@ export class File {
"docProps/app.xml", "docProps/app.xml",
); );
this.appProperties = new AppProperties(); 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 { public addParagraph(paragraph: Paragraph): void {
@ -123,7 +142,7 @@ export class File {
} }
public createImage(image: string): PictureRun { 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( this.docRelationships.createRelationship(
mediaData.referenceId, mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
@ -133,7 +152,7 @@ export class File {
} }
public createImageData(imageName: string, data: Buffer, width?: number, height?: number): IMediaData { 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( this.docRelationships.createRelationship(
mediaData.referenceId, mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
@ -154,6 +173,40 @@ export class File {
return hyperlink; return hyperlink;
} }
public addSection(sectionPropertiesOptions: SectionPropertiesOptions): void {
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 { public get Document(): Document {
return this.document; return this.document;
} }
@ -183,6 +236,10 @@ export class File {
} }
public get Header(): HeaderWrapper { public get Header(): HeaderWrapper {
return this.headerWrapper[0];
}
public get Headers(): HeaderWrapper[] {
return this.headerWrapper; return this.headerWrapper;
} }
@ -190,10 +247,30 @@ export class File {
return this.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 { public get Footer(): FooterWrapper {
return this.footerWrapper[0];
}
public get Footers(): FooterWrapper[] {
return this.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 { public get ContentTypes(): ContentTypes {
return this.contentTypes; return this.contentTypes;
} }

View File

@ -9,8 +9,8 @@ export class FooterWrapper {
private readonly footer: Footer; private readonly footer: Footer;
private readonly relationships: Relationships; private readonly relationships: Relationships;
constructor(private readonly media: Media) { constructor(private readonly media: Media, referenceId: number) {
this.footer = new Footer(); this.footer = new Footer(referenceId);
this.relationships = new Relationships(); this.relationships = new Relationships();
} }

View File

@ -6,8 +6,10 @@ import { Table } from "../table";
import { FooterAttributes } from "./footer-attributes"; import { FooterAttributes } from "./footer-attributes";
export class Footer extends XmlComponent { export class Footer extends XmlComponent {
constructor() { private refId: number;
constructor(referenceNumber: number) {
super("w:ftr"); super("w:ftr");
this.refId = referenceNumber;
this.root.push( this.root.push(
new FooterAttributes({ new FooterAttributes({
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", 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 { public addParagraph(paragraph: Paragraph): void {
this.root.push(paragraph); this.root.push(paragraph);
} }

View File

@ -9,8 +9,8 @@ export class FirstPageHeaderWrapper {
private readonly header: Header; private readonly header: Header;
private readonly relationships: Relationships; private readonly relationships: Relationships;
constructor(private readonly media: Media) { constructor(private readonly media: Media, referenceId: number) {
this.header = new Header(); this.header = new Header(referenceId);
this.relationships = new Relationships(); this.relationships = new Relationships();
} }
@ -59,8 +59,8 @@ export class HeaderWrapper {
private readonly header: Header; private readonly header: Header;
private readonly relationships: Relationships; private readonly relationships: Relationships;
constructor(private readonly media: Media) { constructor(private readonly media: Media, referenceId: number) {
this.header = new Header(); this.header = new Header(referenceId);
this.relationships = new Relationships(); this.relationships = new Relationships();
} }

View File

@ -6,8 +6,10 @@ import { Table } from "../table";
import { HeaderAttributes } from "./header-attributes"; import { HeaderAttributes } from "./header-attributes";
export class Header extends XmlComponent { export class Header extends XmlComponent {
constructor() { private refId: number;
constructor(referenceNumber: number) {
super("w:hdr"); super("w:hdr");
this.refId = referenceNumber;
this.root.push( this.root.push(
new HeaderAttributes({ new HeaderAttributes({
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", 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 { public addParagraph(paragraph: Paragraph): void {
this.root.push(paragraph); this.root.push(paragraph);
} }

View File

@ -4,5 +4,6 @@ export * from "./file";
export * from "./numbering"; export * from "./numbering";
export * from "./media"; export * from "./media";
export * from "./drawing"; export * from "./drawing";
export * from "./document";
export * from "./styles"; export * from "./styles";
export * from "./xml-components"; export * from "./xml-components";

View File

@ -21,13 +21,13 @@ export class Media {
return data; return data;
} }
public addMedia(filePath: string, relationshipsCount: number): IMediaData { public addMedia(filePath: string, referenceId: number): IMediaData {
const key = path.basename(filePath); const key = path.basename(filePath);
const dimensions = sizeOf(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; const key = fileName;
let dimensions; let dimensions;
if (width && height) { if (width && height) {
@ -39,7 +39,7 @@ export class Media {
dimensions = sizeOf(data); dimensions = sizeOf(data);
} }
return this.createMedia(key, relationshipsCount, dimensions, data); return this.createMedia(key, referenceId, dimensions, data);
} }
private createMedia( private createMedia(