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,28 @@
import { Writable } from "stream";
export class BufferStream extends Writable {
private readonly data: Buffer[];
constructor() {
super();
this.data = [];
}
// tslint:disable-next-line:no-any
public _write(chunk: any, _: string, next: (err?: Error) => void): void {
this.data.push(Buffer.from(chunk));
next();
}
// tslint:disable-next-line:ban-types
public end(cb?: Function): void {
super.end(cb);
this.emit("close");
}
public get Buffer(): Buffer {
return Buffer.concat(this.data);
}
}

View File

@ -0,0 +1,44 @@
/* tslint:disable:typedef space-before-function-paren */
import { assert } from "chai";
import { stub } from "sinon";
import { BufferPacker } from "../../export/packer/buffer";
import { File, Paragraph } from "../../file";
describe("BufferPacker", () => {
let packer: BufferPacker;
beforeEach(() => {
const file = new File({
creator: "Dolan Miu",
revision: "1",
lastModifiedBy: "Dolan Miu",
});
const paragraph = new Paragraph("test text");
const heading = new Paragraph("Hello world").heading1();
file.addParagraph(new Paragraph("title").title());
file.addParagraph(heading);
file.addParagraph(new Paragraph("heading 2").heading2());
file.addParagraph(paragraph);
packer = new BufferPacker(file);
});
describe("#pack()", () => {
it("should create a standard docx file", async function() {
this.timeout(99999999);
const buffer = await packer.pack();
assert.isDefined(buffer);
assert.isTrue(buffer.byteLength > 0);
});
it("should handle exception if it throws any", () => {
// tslint:disable-next-line:no-any
const compiler = stub((packer as any).packer, "compile");
compiler.throwsException();
return packer.pack().catch((error) => {
assert.isDefined(error);
});
});
});
});

View File

@ -0,0 +1,20 @@
import { File } from "../../file";
import { BufferStream } from "./buffer-stream";
import { Compiler } from "./compiler";
import { IPacker } from "./packer";
export class BufferPacker implements IPacker {
private readonly packer: Compiler;
constructor(file: File) {
this.packer = new Compiler(file);
}
public async pack(): Promise<Buffer> {
const stream = new BufferStream();
await this.packer.compile(stream);
return stream.Buffer;
}
}

View File

@ -0,0 +1,76 @@
/* tslint:disable:typedef space-before-function-paren */
import * as fs from "fs";
import * as JSZip from "jszip";
import { expect } from "chai";
import { File } from "../../file";
import { Compiler } from "./compiler";
describe("Compiler", () => {
let compiler: Compiler;
let file: File;
beforeEach(() => {
file = new File();
compiler = new Compiler(file);
});
describe("#compile()", () => {
it("should pack all the content", async function() {
this.timeout(99999999);
const fileName = "build/tests/test.docx";
await compiler.compile(fs.createWriteStream(fileName));
const docxFile = fs.readFileSync(fileName);
const zipFile: JSZip = await JSZip.loadAsync(docxFile);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(13);
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/footnotes.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(21);
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

@ -1,6 +1,5 @@
import * as archiver from "archiver";
import * as express from "express";
import * as fs from "fs";
import { Writable } from "stream";
import * as xml from "xml";
@ -9,9 +8,9 @@ import { Formatter } from "../formatter";
export class Compiler {
protected archive: archiver.Archiver;
private formatter: Formatter;
private readonly formatter: Formatter;
constructor(private file: File) {
constructor(private readonly file: File) {
this.formatter = new Formatter();
this.archive = archiver.create("zip", {});
@ -23,7 +22,7 @@ export class Compiler {
public async compile(output: Writable | express.Response): Promise<void> {
this.archive.pipe(output);
const xmlDocument = xml(this.formatter.format(this.file.Document), true);
const xmlDocument = xml(this.formatter.format(this.file.Document));
const xmlStyles = xml(this.formatter.format(this.file.Styles));
const xmlProperties = xml(this.formatter.format(this.file.CoreProperties), {
declaration: {
@ -34,12 +33,9 @@ export class Compiler {
const xmlNumbering = xml(this.formatter.format(this.file.Numbering));
const xmlRelationships = xml(this.formatter.format(this.file.DocumentRelationships));
const xmlFileRelationships = xml(this.formatter.format(this.file.FileRelationships));
const xmlHeader = xml(this.formatter.format(this.file.Header.Header));
const xmlFooter = xml(this.formatter.format(this.file.Footer.Footer));
const xmlHeaderRelationships = xml(this.formatter.format(this.file.Header.Relationships));
const xmlFooterRelationships = xml(this.formatter.format(this.file.Footer.Relationships));
const xmlContentTypes = xml(this.formatter.format(this.file.ContentTypes));
const xmlAppProperties = xml(this.formatter.format(this.file.AppProperties));
const xmlFootnotes = xml(this.formatter.format(this.file.FootNotes));
this.archive.append(xmlDocument, {
name: "word/document.xml",
@ -61,26 +57,38 @@ export class Compiler {
name: "word/numbering.xml",
});
this.archive.append(xmlHeader, {
name: "word/header1.xml",
});
// headers
for (let i = 0; i < this.file.Headers.length; i++) {
const element = this.file.Headers[i];
this.archive.append(xml(this.formatter.format(element.Header)), {
name: `word/header${i + 1}.xml`,
});
this.archive.append(xmlFooter, {
name: "word/footer1.xml",
this.archive.append(xml(this.formatter.format(element.Relationships)), {
name: `word/_rels/header${i + 1}.xml.rels`,
});
}
// footers
for (let i = 0; i < this.file.Footers.length; i++) {
const element = this.file.Footers[i];
this.archive.append(xml(this.formatter.format(element.Footer)), {
name: `word/footer${i + 1}.xml`,
});
this.archive.append(xml(this.formatter.format(element.Relationships)), {
name: `word/_rels/footer${i + 1}.xml.rels`,
});
}
this.archive.append(xmlFootnotes, {
name: "word/footnotes.xml",
});
this.archive.append(xmlRelationships, {
name: "word/_rels/document.xml.rels",
});
this.archive.append(xmlHeaderRelationships, {
name: "word/_rels/header1.xml.rels",
});
this.archive.append(xmlFooterRelationships, {
name: "word/_rels/footer1.xml.rels",
});
this.archive.append(xmlContentTypes, {
name: "[Content_Types].xml",
});
@ -89,8 +97,8 @@ export class Compiler {
name: "_rels/.rels",
});
for (const data of this.file.Media.array) {
this.archive.append(fs.createReadStream(data.path), {
for (const data of this.file.Media.Array) {
this.archive.append(data.stream, {
name: `word/media/${data.fileName}`,
});
}

View File

@ -0,0 +1,43 @@
// tslint:disable:typedef space-before-function-paren
// tslint:disable:no-empty
// tslint:disable:no-any
import { assert } from "chai";
import { stub } from "sinon";
import { ExpressPacker } from "../../export/packer/express";
import { File, Paragraph } from "../../file";
describe("LocalPacker", () => {
let packer: ExpressPacker;
beforeEach(() => {
const file = new File({
creator: "Dolan Miu",
revision: "1",
lastModifiedBy: "Dolan Miu",
});
const paragraph = new Paragraph("test text");
const heading = new Paragraph("Hello world").heading1();
file.addParagraph(new Paragraph("title").title());
file.addParagraph(heading);
file.addParagraph(new Paragraph("heading 2").heading2());
file.addParagraph(paragraph);
const expressResMock = {
on: () => {},
attachment: () => {},
};
packer = new ExpressPacker(file, expressResMock as any);
});
describe("#pack()", () => {
it("should handle exception if it throws any", () => {
const compiler = stub((packer as any).packer, "compile");
compiler.throwsException();
return packer.pack("build/tests/test").catch((error) => {
assert.isDefined(error);
});
});
});
});

View File

@ -4,6 +4,9 @@ import { File } from "file";
import { Compiler } from "./compiler";
import { IPacker } from "./packer";
/**
* @deprecated ExpressPacker is now deprecated. Please use the StreamPacker instead and pipe that to `express`' `res` object
*/
export class ExpressPacker implements IPacker {
private readonly packer: Compiler;

View File

@ -1,5 +1,7 @@
/* tslint:disable:typedef space-before-function-paren */
import { assert } from "chai";
import * as fs from "fs";
import { stub } from "sinon";
import { LocalPacker } from "../../export/packer/local";
import { File, Paragraph } from "../../file";
@ -29,14 +31,36 @@ describe("LocalPacker", () => {
await packer.pack("build/tests/test");
fs.statSync("build/tests/test.docx");
});
it("should handle exception if it throws any", () => {
// tslint:disable-next-line:no-any
const compiler = stub((packer as any).packer, "compile");
compiler.throwsException();
return packer.pack("build/tests/test").catch((error) => {
assert.isDefined(error);
});
});
});
describe("#packPdf", () => {
it("should create a standard PDF file", async function() {
this.timeout(99999999);
// tslint:disable-next-line:no-any
const pdfConverterConvert = stub((packer as any).pdfConverter, "convert");
pdfConverterConvert.returns("Test PDF Contents");
await packer.packPdf("build/tests/pdf-test");
fs.statSync("build/tests/pdf-test.pdf");
});
it("should handle exception if it throws any", () => {
// tslint:disable-next-line:no-any
const compiler = stub((packer as any).packer, "compile");
compiler.throwsException();
return packer.packPdf("build/tests/pdf-test").catch((error) => {
assert.isDefined(error);
});
});
});
});

View File

@ -20,7 +20,7 @@ export class LocalPacker implements IPacker {
filePath = filePath.replace(/.docx$/, "");
const zip = await this.packer.compile();
const zipData = await zip.generateAsync({ type: "base64" }) as string;
const zipData = (await zip.generateAsync({ type: "base64" })) as string;
await this.writeToFile(`${filePath}.docx`, zipData);
}
@ -32,7 +32,7 @@ export class LocalPacker implements IPacker {
const tempPath = path.join(os.tmpdir(), `${fileName}.docx`);
const zip = await this.packer.compile();
const zipData = await zip.generateAsync({ type: "base64" }) as string;
const zipData = (await zip.generateAsync({ type: "base64" })) as string;
await this.writeToFile(tempPath, zipData);
const text = await this.pdfConverter.convert(tempPath);

View File

@ -1,4 +1,3 @@
import * as fs from "fs";
import * as JSZip from "jszip";
import * as xml from "xml";
@ -26,9 +25,9 @@ interface IXmlifyedFileMapping {
}
export class Compiler {
private formatter: Formatter;
private readonly formatter: Formatter;
constructor(private file: File) {
constructor(private readonly file: File) {
this.formatter = new Formatter();
}
@ -47,8 +46,8 @@ export class Compiler {
zip.file(xmlifiedFile.path, xmlifiedFile.data);
}
for (const data of this.file.Media.array) {
const mediaData = await this.readFile(data.path);
for (const data of this.file.Media.Array) {
const mediaData = data.stream;
zip.file(`word/media/${data.fileName}`, mediaData);
}
@ -112,17 +111,4 @@ export class Compiler {
},
};
}
private readFile(path: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) {
reject();
return;
}
resolve(data);
});
});
}
}

View File

@ -3,4 +3,7 @@ export interface IPacker {
}
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**
* @ignore
*/
export const WORKAROUND = "";

View File

@ -1,5 +1,3 @@
/* tslint:disable:object-literal-key-quotes */
// This tslint disable is needed, or it simply won't work
import * as fs from "fs";
import * as request from "request-promise";
@ -11,6 +9,7 @@ export class PdfConvertWrapper {
public convert(filePath: string): request.RequestPromise {
return request.post({
url: "http://mirror1.convertonlinefree.com",
// tslint:disable-next-line:no-null-keyword
encoding: null,
headers: {
"User-Agent":