Merge branch 'master' into feat/browser-packer

# Conflicts:
#	demo/demo13.js
#	package.json
#	src/export/packer/compiler.ts
#	src/file/media/data.ts
#	src/file/media/media.ts
This commit is contained in:
Dolan
2018-08-10 01:40:29 +01:00
196 changed files with 5665 additions and 407 deletions

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"][14]).to.deep.equal({
Override: [
{
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
PartName: "/word/footer101.xml",
},
},
],
});
expect(tree["Types"][15]).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"][14]).to.deep.equal({
Override: [
{
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
PartName: "/word/header201.xml",
},
},
],
});
expect(tree["Types"][15]).to.deep.equal({
Override: [
{
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
PartName: "/word/header202.xml",
},
},
],
});
});
});
});

View File

@ -24,11 +24,23 @@ 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"));
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`),
);
}
}

View File

@ -10,6 +10,7 @@ export interface IPropertiesOptions {
description?: string;
lastModifiedBy?: string;
revision?: string;
externalStyles?: string;
}
export class CoreProperties extends XmlComponent {

View File

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

View File

@ -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 readonly defaultSection: SectionProperties;
private readonly 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);
}
public 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;
}
}

View File

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

View File

@ -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;

View File

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

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";
export enum HeaderReferenceType {
DEFAULT = "default",
FIRST = "first",
EVEN = "even",
}
export interface IHeaderReferenceAttributes {
type: string;
id: string;

View File

@ -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() {
constructor(options: IHeaderOptions) {
super("w:headerReference");
this.root.push(
new HeaderReferenceAttributes({
type: "default",
id: `rId${3}`,
type: options.headerType || HeaderReferenceType.DEFAULT,
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";
export enum PageOrientation {
PORTRAIT = "portrait",
LANDSCAPE = "landscape",
}
export interface IPageSizeAttributes {
width?: number;
height?: number;
orientation?: string;
orientation?: PageOrientation;
}
export class PageSizeAttributes extends XmlAttributeComponent<IPageSizeAttributes> {

View File

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

View File

@ -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({

View File

@ -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", () => {

View File

@ -1,19 +1,30 @@
// 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 { 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 readonly options: SectionPropertiesOptions;
constructor(options?: SectionPropertiesOptions) {
super("w:sectPr");
@ -29,7 +40,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 +68,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());
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;
}
public get Options(): SectionPropertiesOptions {
return this.options;
}
}

View File

@ -0,0 +1,11 @@
import { XmlAttributeComponent } from "file/xml-components";
export interface IHeaderReferenceAttributes {
value: string;
}
export class TitlePageAttributes extends XmlAttributeComponent<IHeaderReferenceAttributes> {
protected xmlKeys = {
value: "w:val",
};
}

View File

@ -0,0 +1,17 @@
import { expect } from "chai";
import { Formatter } from "../../../../../export/formatter";
import { TitlePage } from "./title-page";
describe("PageSize", () => {
describe("#constructor()", () => {
it("should create title page property for different first page header", () => {
const properties = new TitlePage();
const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:titlePg"]);
expect(tree["w:titlePg"]).to.be.an.instanceof(Array);
expect(tree["w:titlePg"][0]).to.deep.equal({ _attr: { "w:val": "1" } });
});
});
});

View File

@ -0,0 +1,13 @@
import { XmlComponent } from "file/xml-components";
import { TitlePageAttributes } from "./title-page-attributes";
export class TitlePage extends XmlComponent {
constructor() {
super("w:titlePg");
this.root.push(
new TitlePageAttributes({
value: "1",
}),
);
}
}

View File

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

View File

@ -1,7 +1,6 @@
// http://officeopenxml.com/WPdocument.php
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components";
import { Paragraph, PictureRun } from "../paragraph";
import { Paragraph } from "../paragraph";
import { Table } from "../table";
import { Body } from "./body";
import { SectionPropertiesOptions } from "./body/section-properties/section-properties";
@ -37,8 +36,9 @@ export class Document extends XmlComponent {
this.root.push(this.body);
}
public addParagraph(paragraph: Paragraph): void {
public addParagraph(paragraph: Paragraph): Document {
this.body.push(paragraph);
return this;
}
public createParagraph(text?: string): Paragraph {
@ -57,17 +57,7 @@ export class Document extends XmlComponent {
return table;
}
public addDrawing(pictureParagraph: Paragraph): void {
this.body.push(pictureParagraph);
}
public createDrawing(imageData: IMediaData): PictureRun {
const paragraph = new Paragraph();
const run = new PictureRun(imageData);
paragraph.addRun(run);
this.addDrawing(paragraph);
return run;
public get Body(): Body {
return this.body;
}
}

View File

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

View File

@ -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<IAnchorAttributes> {
protected xmlKeys = {
distT: "distT",
distB: "distB",
distL: "distL",
distR: "distR",
allowOverlap: "allowOverlap",
behindDoc: "behindDoc",
layoutInCell: "layoutInCell",
locked: "locked",
relativeHeight: "relativeHeight",
simplePos: "simplePos",
};
}

View File

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

View File

@ -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 !== undefined) {
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));
}
}

View File

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

View File

@ -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,
path: path,
@ -23,14 +21,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");
});
});
});

View File

@ -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;
private readonly 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 {

View File

@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components";
import { ExtentAttributes } from "./extent-attributes";
export class Extent extends XmlComponent {
private attributes: ExtentAttributes;
private readonly attributes: ExtentAttributes;
constructor(x: number, y: number) {
super("wp:extent");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export * from "./floating-position";
export * from "./simple-pos";
export * from "./horizontal-position";
export * from "./vertical-position";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,3 @@
export { Drawing } from "./drawing";
export * from "./drawing";
export * from "./text-wrap";
export * from "./floating";

View File

@ -3,7 +3,7 @@ import { GraphicDataAttributes } from "./graphic-data-attribute";
import { Pic } from "./pic";
export class GraphicData extends XmlComponent {
private pic: Pic;
private readonly pic: Pic;
constructor(referenceId: number, x: number, y: number) {
super("a:graphicData");

View File

@ -6,7 +6,7 @@ import { PicAttributes } from "./pic-attributes";
import { ShapeProperties } from "./shape-properties/shape-properties";
export class Pic extends XmlComponent {
private shapeProperties: ShapeProperties;
private readonly shapeProperties: ShapeProperties;
constructor(referenceId: number, x: number, y: number) {
super("pic:pic");

View File

@ -3,7 +3,7 @@ import { XmlComponent } from "file/xml-components";
import { ExtentsAttributes } from "./extents-attributes";
export class Extents extends XmlComponent {
private attributes: ExtentsAttributes;
private readonly attributes: ExtentsAttributes;
constructor(x: number, y: number) {
super("a:ext");

View File

@ -4,7 +4,7 @@ import { Extents } from "./extents/extents";
import { Offset } from "./offset/off";
export class Form extends XmlComponent {
private extents: Extents;
private readonly extents: Extents;
constructor(x: number, y: number) {
super("a:xfrm");

View File

@ -7,7 +7,7 @@ import { PresetGeometry } from "./preset-geometry/preset-geometry";
import { ShapePropertiesAttributes } from "./shape-properties-attributes";
export class ShapeProperties extends XmlComponent {
private form: Form;
private readonly form: Form;
constructor(x: number, y: number) {
super("pic:spPr");

View File

@ -12,7 +12,7 @@ class GraphicAttributes extends XmlAttributeComponent<IGraphicProperties> {
}
export class Graphic extends XmlComponent {
private data: GraphicData;
private readonly data: GraphicData;
constructor(referenceId: number, x: number, y: number) {
super("a:graphic");

View File

@ -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<IInlineAttributes> {
protected xmlKeys = {

View File

@ -1,18 +1,18 @@
// 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 {
private extent: Extent;
private graphic: Graphic;
private readonly extent: Extent;
private readonly graphic: Graphic;
constructor(referenceId: number, private dimensions: IMediaDataDimensions) {
constructor(referenceId: number, private readonly dimensions: IMediaDataDimensions) {
super("wp:inline");
this.root.push(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,14 +2,17 @@ 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 { HeaderWrapper } from "./header-wrapper";
import { Media } from "./media";
import { Image, Media } from "./media";
import { Numbering } from "./numbering";
import { Paragraph, PictureRun } from "./paragraph";
import { Bookmark, Hyperlink, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Styles } from "./styles";
import { ExternalStylesFactory } from "./styles/external-styles-factory";
import { DefaultStylesFactory } from "./styles/factory";
import { Table } from "./table";
@ -21,16 +24,16 @@ 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 footNotes: FootNotes;
private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties;
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
this.document = new Document(sectionPropertiesOptions);
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
private currentRelationshipId: number = 1;
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
if (!options) {
options = {
creator: "Un-named",
@ -39,33 +42,39 @@ export class File {
};
}
if (options.externalStyles) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else {
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
}
this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering();
this.docRelationships = new Relationships();
this.docRelationships.createRelationship(
1,
this.currentRelationshipId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml",
);
this.docRelationships.createRelationship(
2,
this.currentRelationshipId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
"numbering.xml",
);
this.contentTypes = new ContentTypes();
this.docRelationships.createRelationship(
3,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
"header1.xml",
);
this.docRelationships.createRelationship(
4,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
"footer1.xml",
this.currentRelationshipId++,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
"footnotes.xml",
);
this.media = new Media();
this.headerWrapper = new HeaderWrapper(this.media);
this.footerWrapper = new FooterWrapper(this.media);
this.contentTypes = new ContentTypes();
const header = this.createHeader();
const footer = this.createFooter();
this.fileRelationships = new Relationships();
this.fileRelationships.createRelationship(
1,
@ -83,6 +92,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 {
@ -101,14 +124,94 @@ export class File {
return this.document.createTable(rows, cols);
}
public createImage(image: string): PictureRun {
const mediaData = this.media.addMedia(image, this.docRelationships.RelationshipCount);
public createImage(filePath: string): Image {
const image = Media.addImage(this, filePath);
this.document.addParagraph(image.Paragraph);
return image;
}
public addImage(image: Image): File {
this.document.addParagraph(image.Paragraph);
return this;
}
public createImageFromBuffer(buffer: Buffer, width?: number, height?: number): Image {
const image = Media.addImageFromBuffer(this, buffer, width, height);
this.document.addParagraph(image.Paragraph);
return image;
}
public createHyperlink(link: string, text?: string): Hyperlink {
text = text === undefined ? link : text;
const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount);
this.docRelationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
hyperlink.linkId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
link,
"External",
);
return this.document.createDrawing(mediaData);
return hyperlink;
}
public createInternalHyperLink(anchor: string, text?: string): Hyperlink {
text = text === undefined ? anchor : text;
const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount, anchor);
// NOTE: unlike File#createHyperlink(), since the link is to an internal bookmark
// we don't need to create a new relationship.
return hyperlink;
}
public createBookmark(name: string, text?: string): Bookmark {
text = text === undefined ? name : text;
const bookmark = new Bookmark(name, text, this.docRelationships.RelationshipCount);
return bookmark;
}
public addSection(sectionPropertiesOptions: SectionPropertiesOptions): void {
this.document.Body.addSection(sectionPropertiesOptions);
}
public createFootnote(paragraph: Paragraph): void {
this.footNotes.createFootNote(paragraph);
}
public createHeader(): HeaderWrapper {
const header = new HeaderWrapper(this.media, this.currentRelationshipId++);
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;
}
public createFooter(): FooterWrapper {
const footer = new FooterWrapper(this.media, this.currentRelationshipId++);
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 {
@ -140,13 +243,37 @@ 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;
}
@ -154,4 +281,8 @@ export class File {
public get AppProperties(): AppProperties {
return this.appProperties;
}
public get FootNotes(): FootNotes {
return this.footNotes;
}
}

View File

@ -1,6 +1,7 @@
import { XmlComponent } from "file/xml-components";
import { Footer } from "./footer/footer";
import { IMediaData, Media } from "./media";
import { Paragraph } from "./paragraph";
import { Image, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
@ -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();
}
@ -31,8 +32,8 @@ export class FooterWrapper {
return this.footer.createTable(rows, cols);
}
public addDrawing(imageData: IMediaData): void {
this.footer.addDrawing(imageData);
public addChildElement(childElement: XmlComponent | string): void {
this.footer.addChildElement(childElement);
}
public createImage(image: string): void {
@ -42,7 +43,12 @@ export class FooterWrapper {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
this.addDrawing(mediaData);
this.addImage(new Image(new ImageParagraph(mediaData)));
}
public addImage(image: Image): FooterWrapper {
this.footer.addParagraph(image.Paragraph);
return this;
}
public get Footer(): Footer {

View File

@ -1,13 +1,15 @@
// http://officeopenxml.com/WPfooters.php
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components";
import { Paragraph, PictureRun } from "../paragraph";
import { Paragraph } from "../paragraph";
import { Table } from "../table";
import { FooterAttributes } from "./footer-attributes";
export class Footer extends XmlComponent {
constructor() {
private readonly 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,8 +32,14 @@ export class Footer extends XmlComponent {
);
}
public addParagraph(paragraph: Paragraph): void {
public get ReferenceId(): number {
return this.refId;
}
public addParagraph(paragraph: Paragraph): Footer {
this.root.push(paragraph);
return this;
}
public createParagraph(text?: string): Paragraph {
@ -49,12 +57,4 @@ export class Footer extends XmlComponent {
this.addTable(table);
return table;
}
public addDrawing(imageData: IMediaData): void {
const paragraph = new Paragraph();
const run = new PictureRun(imageData);
paragraph.addRun(run);
this.root.push(paragraph);
}
}

View File

@ -0,0 +1,13 @@
import { XmlAttributeComponent } from "file/xml-components";
export interface IFootnoteAttributesProperties {
type?: string;
id: number;
}
export class FootnoteAttributes extends XmlAttributeComponent<IFootnoteAttributesProperties> {
protected xmlKeys = {
type: "w:type",
id: "w:id",
};
}

View File

@ -0,0 +1,25 @@
import { expect } from "chai";
import { Formatter } from "../../../export/formatter";
import { Footnote, FootnoteType } from "./footnote";
describe("Footnote", () => {
describe("#constructor", () => {
it("should create a footnote with a footnote type", () => {
const footnote = new Footnote(1, FootnoteType.SEPERATOR);
const tree = new Formatter().format(footnote);
expect(Object.keys(tree)).to.deep.equal(["w:footnote"]);
expect(tree["w:footnote"]).to.be.an.instanceof(Array);
expect(tree["w:footnote"][0]).to.deep.equal({ _attr: { "w:type": "separator", "w:id": 1 } });
});
it("should create a footnote without a footnote type", () => {
const footnote = new Footnote(1);
const tree = new Formatter().format(footnote);
expect(Object.keys(tree)).to.deep.equal(["w:footnote"]);
expect(tree["w:footnote"]).to.be.an.instanceof(Array);
expect(tree["w:footnote"][0]).to.deep.equal({ _attr: { "w:id": 1 } });
});
});
});

View File

@ -0,0 +1,26 @@
import { XmlComponent } from "file/xml-components";
import { Paragraph } from "../../paragraph";
import { FootnoteAttributes } from "./footnote-attributes";
import { FootnoteRefRun } from "./run/footnote-ref-run";
export enum FootnoteType {
SEPERATOR = "separator",
CONTINUATION_SEPERATOR = "continuationSeparator",
}
export class Footnote extends XmlComponent {
constructor(id: number, type?: FootnoteType) {
super("w:footnote");
this.root.push(
new FootnoteAttributes({
type: type,
id: id,
}),
);
}
public addParagraph(paragraph: Paragraph): void {
paragraph.addRunToFront(new FootnoteRefRun());
this.root.push(paragraph);
}
}

View File

@ -0,0 +1,10 @@
import { Run } from "file/paragraph";
import { ContinuationSeperator } from "./continuation-seperator";
export class ContinuationSeperatorRun extends Run {
constructor() {
super();
this.root.push(new ContinuationSeperator());
}
}

View File

@ -0,0 +1,7 @@
import { XmlComponent } from "file/xml-components";
export class ContinuationSeperator extends XmlComponent {
constructor() {
super("w:continuationSeparator");
}
}

View File

@ -0,0 +1,11 @@
import { Run } from "file/paragraph";
import { FootnoteRef } from "./footnote-ref";
export class FootnoteRefRun extends Run {
constructor() {
super();
this.style("FootnoteReference");
this.root.push(new FootnoteRef());
}
}

View File

@ -0,0 +1,7 @@
import { XmlComponent } from "file/xml-components";
export class FootnoteRef extends XmlComponent {
constructor() {
super("w:footnoteRef");
}
}

View File

@ -0,0 +1,35 @@
import { Run } from "file/paragraph/run";
import { Style } from "file/paragraph/run/style";
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export interface IFootNoteReferenceRunAttributesProperties {
id: number;
}
export class FootNoteReferenceRunAttributes extends XmlAttributeComponent<IFootNoteReferenceRunAttributesProperties> {
protected xmlKeys = {
id: "w:id",
};
}
export class FootnoteReference extends XmlComponent {
constructor(id: number) {
super("w:footnoteReference");
this.root.push(
new FootNoteReferenceRunAttributes({
id: id,
}),
);
}
}
export class FootnoteReferenceRun extends Run {
constructor(id: number) {
super();
this.properties.push(new Style("FootnoteReference"));
this.root.push(new FootnoteReference(id));
}
}

View File

@ -0,0 +1,10 @@
import { Run } from "file/paragraph";
import { Seperator } from "./seperator";
export class SeperatorRun extends Run {
constructor() {
super();
this.root.push(new Seperator());
}
}

View File

@ -0,0 +1,7 @@
import { XmlComponent } from "file/xml-components";
export class Seperator extends XmlComponent {
constructor() {
super("w:separator");
}
}

View File

@ -0,0 +1,43 @@
import { XmlAttributeComponent } from "file/xml-components";
export interface IFootnotesAttributesProperties {
wpc?: string;
mc?: string;
o?: string;
r?: string;
m?: string;
v?: string;
wp14?: string;
wp?: string;
w10?: string;
w?: string;
w14?: string;
w15?: string;
wpg?: string;
wpi?: string;
wne?: string;
wps?: string;
Ignorable?: string;
}
export class FootnotesAttributes extends XmlAttributeComponent<IFootnotesAttributesProperties> {
protected xmlKeys = {
wpc: "xmlns:wpc",
mc: "xmlns:mc",
o: "xmlns:o",
r: "xmlns:r",
m: "xmlns:m",
v: "xmlns:v",
wp14: "xmlns:wp14",
wp: "xmlns:wp",
w10: "xmlns:w10",
w: "xmlns:w",
w14: "xmlns:w14",
w15: "xmlns:w15",
wpg: "xmlns:wpg",
wpi: "xmlns:wpi",
wne: "xmlns:wne",
wps: "xmlns:wps",
Ignorable: "mc:Ignorable",
};
}

View File

@ -0,0 +1,70 @@
import { XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { Footnote, FootnoteType } from "./footnote/footnote";
import { ContinuationSeperatorRun } from "./footnote/run/continuation-seperator-run";
import { SeperatorRun } from "./footnote/run/seperator-run";
import { FootnotesAttributes } from "./footnotes-attributes";
export class FootNotes extends XmlComponent {
private counter: number;
constructor() {
super("w:footnotes");
this.counter = 1;
this.root.push(
new FootnotesAttributes({
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
o: "urn:schemas-microsoft-com:office:office",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
v: "urn:schemas-microsoft-com:vml",
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
w10: "urn:schemas-microsoft-com:office:word",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
Ignorable: "w14 w15 wp14",
}),
);
const begin = new Footnote(-1, FootnoteType.SEPERATOR);
begin.addParagraph(
new Paragraph()
.spacing({
after: 0,
line: 240,
lineRule: "auto",
})
.addRun(new SeperatorRun()),
);
this.root.push(begin);
const spacing = new Footnote(0, FootnoteType.CONTINUATION_SEPERATOR);
spacing.addParagraph(
new Paragraph()
.spacing({
after: 0,
line: 240,
lineRule: "auto",
})
.addRun(new ContinuationSeperatorRun()),
);
this.root.push(spacing);
}
public createFootNote(paragraph: Paragraph): void {
const footnote = new Footnote(this.counter);
footnote.addParagraph(paragraph);
this.root.push(footnote);
this.counter++;
}
}

View File

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

View File

@ -1,6 +1,7 @@
import { XmlComponent } from "file/xml-components";
import { Header } from "./header/header";
import { IMediaData, Media } from "./media";
import { Paragraph } from "./paragraph";
import { Image, Media } from "./media";
import { ImageParagraph, Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
@ -8,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();
}
@ -31,8 +32,8 @@ export class HeaderWrapper {
return this.header.createTable(rows, cols);
}
public addDrawing(imageData: IMediaData): void {
this.header.addDrawing(imageData);
public addChildElement(childElement: XmlComponent | string): void {
this.header.addChildElement(childElement);
}
public createImage(image: string): void {
@ -42,7 +43,12 @@ export class HeaderWrapper {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
this.addDrawing(mediaData);
this.addImage(new Image(new ImageParagraph(mediaData)));
}
public addImage(image: Image): HeaderWrapper {
this.header.addParagraph(image.Paragraph);
return this;
}
public get Header(): Header {

View File

@ -1,13 +1,16 @@
// http://officeopenxml.com/WPheaders.php
import { IMediaData } from "file/media";
import { XmlComponent } from "file/xml-components";
import { Paragraph, PictureRun } from "../paragraph";
import { Paragraph } from "../paragraph";
import { Table } from "../table";
import { HeaderAttributes } from "./header-attributes";
export class Header extends XmlComponent {
constructor() {
private readonly 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 +33,10 @@ export class Header extends XmlComponent {
);
}
public get ReferenceId(): number {
return this.refId;
}
public addParagraph(paragraph: Paragraph): void {
this.root.push(paragraph);
}
@ -49,12 +56,4 @@ export class Header extends XmlComponent {
this.addTable(table);
return table;
}
public addDrawing(imageData: IMediaData): void {
const paragraph = new Paragraph();
const run = new PictureRun(imageData);
paragraph.addRun(run);
this.root.push(paragraph);
}
}

View File

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

View File

@ -1,3 +1,5 @@
import * as fs from "fs";
export interface IMediaDataDimensions {
pixels: {
x: number;
@ -11,10 +13,14 @@ export interface IMediaDataDimensions {
export interface IMediaData {
referenceId: number;
path: string;
stream: fs.ReadStream | Buffer;
path?: string;
fileName: string;
dimensions: IMediaDataDimensions;
}
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**
* @ignore
*/
export const WORKAROUND2 = "";

17
src/file/media/image.ts Normal file
View File

@ -0,0 +1,17 @@
import { ImageParagraph, PictureRun } from "../paragraph";
export class Image {
constructor(private readonly paragraph: ImageParagraph) {}
public get Paragraph(): ImageParagraph {
return this.paragraph;
}
public get Run(): PictureRun {
return this.paragraph.Run;
}
public scale(factorX: number, factorY?: number): void {
this.paragraph.Run.scale(factorX, factorY);
}
}

View File

@ -1,2 +1,3 @@
export * from "./media";
export * from "./data";
export * from "./image";

View File

@ -1,9 +1,60 @@
import * as fs from "fs";
import * as sizeOf from "image-size";
import * as path from "path";
import { File } from "../file";
import { ImageParagraph } from "../paragraph";
import { IMediaData } from "./data";
import { Image } from "./image";
interface IHackedFile {
currentRelationshipId: number;
}
export class Media {
public static addImage(file: File, filePath: string): Image {
// Workaround to expose id without exposing to API
const exposedFile = (file as {}) as IHackedFile;
const mediaData = file.Media.addMedia(filePath, exposedFile.currentRelationshipId++);
file.DocumentRelationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
return new Image(new ImageParagraph(mediaData));
}
public static addImageFromBuffer(file: File, buffer: Buffer, width?: number, height?: number): Image {
// Workaround to expose id without exposing to API
const exposedFile = (file as {}) as IHackedFile;
const mediaData = file.Media.addMediaFromBuffer(
`${Media.generateId()}.png`,
buffer,
exposedFile.currentRelationshipId++,
width,
height,
);
file.DocumentRelationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
return new Image(new ImageParagraph(mediaData));
}
private static generateId(): string {
// https://gist.github.com/6174/6062387
return (
Math.random()
.toString(36)
.substring(2, 15) +
Math.random()
.toString(36)
.substring(2, 15)
);
}
private readonly map: Map<string, IMediaData>;
constructor() {
@ -20,12 +71,37 @@ 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, referenceId, dimensions, fs.createReadStream(filePath), filePath);
}
public addMediaFromBuffer(fileName: string, buffer: Buffer, referenceId: number, width?: number, height?: number): IMediaData {
const key = fileName;
let dimensions;
if (width && height) {
dimensions = {
width: width,
height: height,
};
} else {
dimensions = sizeOf(buffer);
}
return this.createMedia(key, referenceId, dimensions, buffer);
}
private createMedia(
key: string,
relationshipsCount: number,
dimensions: { width: number; height: number },
data: fs.ReadStream | Buffer,
filePath?: string,
): IMediaData {
const imageData = {
referenceId: this.map.size + relationshipsCount + 1,
stream: data,
path: filePath,
fileName: key,
dimensions: {
@ -39,12 +115,13 @@ export class Media {
},
},
};
this.map.set(key, imageData);
return imageData;
}
public get array(): IMediaData[] {
public get Array(): IMediaData[] {
const array = new Array<IMediaData>();
this.map.forEach((data) => {

View File

@ -1 +1,2 @@
export * from "./numbering";
export * from "./abstract-numbering";

View File

@ -56,7 +56,7 @@ export class LevelOverride extends XmlComponent {
}
}
get level(): LevelForOverride {
public get Level(): LevelForOverride {
let lvl: LevelForOverride;
if (!this.lvl) {
lvl = new LevelForOverride(this.levelNum);

View File

@ -34,7 +34,7 @@ describe("Numbering", () => {
]);
// Once chai 4.0.0 lands and #644 is resolved, we can add the following to the test:
// {"w:lvlText": [{"_attr": {"w:val": "•"}}]},
// {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]},
// {"w:rPr": [{"w:rFonts": [{"_attr": {"w:ascii": "Symbol", "w:cs": "Symbol", "w:eastAsia": "Symbol", "w:hAnsi": "Symbol", "w:hint": "default"}}]}]},
// {"w:pPr": [{"_attr": {}},
// {"w:ind": [{"_attr": {"w:left": 720, "w:hanging": 360}}]}]},
});
@ -297,7 +297,9 @@ describe("AbstractNumbering", () => {
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").font("Times");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:hAnsi": "Times" } }] }],
"w:rPr": [
{ "w:rFonts": [{ _attr: { "w:ascii": "Times", "w:cs": "Times", "w:eastAsia": "Times", "w:hAnsi": "Times" } }] },
],
});
});
@ -385,9 +387,9 @@ describe("concrete numbering", () => {
});
});
it("sets the lvl element if overrideLevel.level is accessed", () => {
it("sets the lvl element if overrideLevel.Level is accessed", () => {
const ol = concreteNumbering.overrideLevel(1);
expect(ol.level).to.be.instanceof(LevelForOverride);
expect(ol.Level).to.be.instanceof(LevelForOverride);
const tree = new Formatter().format(concreteNumbering);
expect(tree["w:num"]).to.include({
"w:lvlOverride": [

View File

@ -1,12 +1,15 @@
import { XmlComponent } from "file/xml-components";
import { Indent } from "file/paragraph";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { Indent } from "../paragraph/formatting";
import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num";
export class Numbering extends XmlComponent {
private nextId: number;
private readonly abstractNumbering: XmlComponent[] = [];
private readonly concreteNumbering: XmlComponent[] = [];
constructor() {
super("w:numbering");
this.root.push(
@ -58,13 +61,19 @@ export class Numbering extends XmlComponent {
public createAbstractNumbering(): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++);
this.root.push(num);
this.abstractNumbering.push(num);
return num;
}
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id);
this.root.push(num);
this.concreteNumbering.push(num);
return num;
}
public prepForXml(): IXmlableObject {
this.abstractNumbering.forEach((x) => this.root.push(x));
this.concreteNumbering.forEach((x) => this.root.push(x));
return super.prepForXml();
}
}

View File

@ -1,7 +1,7 @@
// http://officeopenxml.com/WPalignment.php
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export type AlignmentOptions = "left" | "center" | "right" | "both";
export type AlignmentOptions = "start" | "end" | "center" | "both" | "distribute" | "left" | "right";
export class AlignmentAttributes extends XmlAttributeComponent<{ val: AlignmentOptions }> {
protected xmlKeys = { val: "w:val" };

View File

@ -0,0 +1,7 @@
import { XmlComponent } from "file/xml-components";
export class Bidirectional extends XmlComponent {
constructor() {
super("w:bidi");
}
}

View File

@ -1,7 +1,7 @@
import { assert } from "chai";
import { Utility } from "../../../tests/utility";
import { PageBreak } from "./page-break";
import { PageBreak, PageBreakBefore } from "./page-break";
describe("PageBreak", () => {
let pageBreak: PageBreak;
@ -30,3 +30,11 @@ describe("PageBreak", () => {
});
});
});
describe("PageBreakBefore", () => {
it("should create page break before", () => {
const pageBreakBefore = new PageBreakBefore();
const newJson = Utility.jsonify(pageBreakBefore);
assert.equal(newJson.rootKey, "w:pageBreakBefore");
});
});

View File

@ -19,3 +19,12 @@ export class PageBreak extends Run {
this.root.push(new Break());
}
}
/**
* Add page break before the paragraph if there is no one added before.
*/
export class PageBreakBefore extends XmlComponent {
constructor() {
super("w:pageBreakBefore");
}
}

Some files were not shown because too many files have changed in this diff Show More