Huge refactoring to use new compiler and deprecate all other Packers

Add new PdfPacker
This commit is contained in:
Dolan
2018-08-14 01:46:48 +01:00
parent 675192b86f
commit a38abeb4c2
19 changed files with 760 additions and 345 deletions

View File

@ -1,17 +0,0 @@
import { Compiler } from "./next-compiler";
import { IPacker } from "./packer";
declare var saveAs;
export class BrowserPacker implements IPacker {
private readonly packer: Compiler;
public async pack(filePath: string): Promise<void> {
filePath = filePath.replace(/.docx$/, "");
const zip = await this.packer.compile();
const zipBlob = await zip.generateAsync({ type: "blob" });
saveAs(zipBlob, `${filePath}.docx`);
}
}

View File

@ -1,28 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,43 +0,0 @@
// 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

@ -1,32 +0,0 @@
import * as express from "express";
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;
constructor(file: File, private readonly res: express.Response) {
this.packer = new Compiler(file);
this.res = res;
this.res.on("close", () => {
return res
.status(200)
.send("OK")
.end();
});
}
public async pack(name: string): Promise<void> {
name = name.replace(/.docx$/, "");
this.res.attachment(`${name}.docx`);
await this.packer.compile(this.res);
}
}

View File

@ -1,66 +0,0 @@
/* 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";
describe("LocalPacker", () => {
let packer: LocalPacker;
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 LocalPacker(file);
});
describe("#pack()", () => {
it("should create a standard docx file", async function() {
this.timeout(99999999);
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

@ -1,57 +0,0 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { File } from "../../file";
import { Compiler } from "./next-compiler";
import { IPacker } from "./packer";
import { PdfConvertWrapper } from "./pdf-convert-wrapper";
export class LocalPacker implements IPacker {
private readonly pdfConverter: PdfConvertWrapper;
private readonly packer: Compiler;
constructor(file: File) {
this.pdfConverter = new PdfConvertWrapper();
this.packer = new Compiler(file);
}
public async pack(filePath: string): Promise<void> {
filePath = filePath.replace(/.docx$/, "");
const zip = await this.packer.compile();
const zipData = (await zip.generateAsync({ type: "base64" })) as string;
await this.writeToFile(`${filePath}.docx`, zipData);
}
public async packPdf(filePath: string): Promise<void> {
filePath = filePath.replace(/.pdf$/, "");
const fileName = path.basename(filePath, path.extname(filePath));
const tempPath = path.join(os.tmpdir(), `${fileName}.docx`);
const zip = await this.packer.compile();
const zipData = (await zip.generateAsync({ type: "base64" })) as string;
await this.writeToFile(tempPath, zipData);
const text = await this.pdfConverter.convert(tempPath);
// const writeFile = util.promisify(fs.writeFile); --use this in future, in 3 years time. Only in node 8
// return writeFile(`${filePath}.pdf`, text);
await this.writeToFile(`${filePath}.pdf`, text);
}
private writeToFile(filePath: string, data: string): Promise<void> {
const file = fs.createWriteStream(filePath);
return new Promise((resolve, reject) => {
file.write(data, "base64");
file.end();
file.on("finish", () => {
resolve();
});
file.on("error", reject);
});
}
}

View File

@ -0,0 +1,66 @@
/* tslint:disable:typedef space-before-function-paren */
import { expect } from "chai";
import { File } from "../../file";
import { Compiler } from "./next-compiler";
describe("Compiler", () => {
let compiler: Compiler;
let file: File;
beforeEach(() => {
file = new File();
compiler = new Compiler();
});
describe("#compile()", () => {
it("should pack all the content", async function() {
this.timeout(99999999);
const zipFile = await compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(17);
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 zipFile = await compiler.compile(file);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(25);
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

@ -16,37 +16,44 @@ interface IXmlifyedFileMapping {
Numbering: IXmlifyedFile;
Relationships: IXmlifyedFile;
FileRelationships: IXmlifyedFile;
Header: IXmlifyedFile;
Footer: IXmlifyedFile;
HeaderRelationships: IXmlifyedFile;
FooterRelationships: IXmlifyedFile;
Headers: IXmlifyedFile[];
Footers: IXmlifyedFile[];
HeaderRelationships: IXmlifyedFile[];
FooterRelationships: IXmlifyedFile[];
ContentTypes: IXmlifyedFile;
AppProperties: IXmlifyedFile;
FootNotes: IXmlifyedFile;
}
export class Compiler {
private readonly formatter: Formatter;
constructor(private readonly file: File) {
constructor() {
this.formatter = new Formatter();
}
public async compile(): Promise<JSZip> {
public async compile(file: File): Promise<JSZip> {
const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(this.file);
const xmlifiedFileMapping = this.xmlifyFile(file);
for (const key in xmlifiedFileMapping) {
if (!xmlifiedFileMapping[key]) {
continue;
}
const xmlifiedFile = xmlifiedFileMapping[key];
const obj = xmlifiedFileMapping[key] as IXmlifyedFile | IXmlifyedFile[];
zip.file(xmlifiedFile.path, xmlifiedFile.data);
if (Array.isArray(obj)) {
for (const subFile of obj) {
zip.file(subFile.path, subFile.data);
}
} else {
zip.file(obj.path, obj.data);
}
}
for (const data of this.file.Media.Array) {
for (const data of file.Media.Array) {
const mediaData = data.stream;
zip.file(`word/media/${data.fileName}`, mediaData);
}
@ -85,22 +92,22 @@ export class Compiler {
data: xml(this.formatter.format(file.FileRelationships)),
path: "_rels/.rels",
},
Header: {
data: xml(this.formatter.format(file.Header.Header)),
path: "word/header1.xml",
},
Footer: {
data: xml(this.formatter.format(file.Footer.Footer)),
path: "word/footer1.xml",
},
HeaderRelationships: {
data: xml(this.formatter.format(file.Header.Relationships)),
path: "word/_rels/header1.xml.rels",
},
FooterRelationships: {
data: xml(this.formatter.format(file.Footer.Relationships)),
path: "word/_rels/footer1.xml.rels",
},
Headers: file.Headers.map((headerWrapper, index) => ({
data: xml(this.formatter.format(headerWrapper.Header)),
path: `word/header${index + 1}.xml`,
})),
Footers: file.Footers.map((footerWrapper, index) => ({
data: xml(this.formatter.format(footerWrapper.Footer)),
path: `word/footer${index + 1}.xml`,
})),
HeaderRelationships: file.Headers.map((headerWrapper, index) => ({
data: xml(this.formatter.format(headerWrapper.Relationships)),
path: `word/_rels/header${index + 1}.xml.rels`,
})),
FooterRelationships: file.Footers.map((footerWrapper, index) => ({
data: xml(this.formatter.format(footerWrapper.Relationships)),
path: `word/_rels/footer${index + 1}.xml.rels`,
})),
ContentTypes: {
data: xml(this.formatter.format(file.ContentTypes)),
path: "[Content_Types].xml",
@ -109,6 +116,10 @@ export class Compiler {
data: xml(this.formatter.format(file.AppProperties)),
path: "docProps/app.xml",
},
FootNotes: {
data: xml(this.formatter.format(file.FootNotes)),
path: "word/footnotes.xml",
},
};
}
}

View File

@ -2,41 +2,45 @@
import { assert } from "chai";
import { stub } from "sinon";
import { BufferPacker } from "../../export/packer/buffer";
import { File, Paragraph } from "../../file";
import { Packer } from "./packer";
describe("BufferPacker", () => {
let packer: BufferPacker;
describe("Packer", () => {
let packer: Packer;
let file: File;
beforeEach(() => {
const file = new File({
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);
packer = new Packer();
});
describe("#pack()", () => {
describe("#toBuffer()", () => {
it("should create a standard docx file", async function() {
this.timeout(99999999);
const buffer = await packer.pack();
const buffer = await packer.toBuffer(file);
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");
const compiler = stub((packer as any).compiler, "compile");
compiler.throwsException();
return packer.pack().catch((error) => {
return packer.toBuffer(file).catch((error) => {
assert.isDefined(error);
});
});

View File

@ -1,9 +1,31 @@
export interface IPacker {
pack(path: string): void;
}
import { File } from "file";
import { Compiler } from "./next-compiler";
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**
* @ignore
*/
export const WORKAROUND = "";
export class Packer {
private readonly compiler: Compiler;
constructor() {
this.compiler = new Compiler();
}
public async toBuffer(file: File): Promise<Buffer> {
const zip = await this.compiler.compile(file);
const zipData = (await zip.generateAsync({ type: "nodebuffer" })) as Buffer;
return zipData;
}
public async toBase64String(file: File): Promise<string> {
const zip = await this.compiler.compile(file);
const zipData = (await zip.generateAsync({ type: "base64" })) as string;
return zipData;
}
public async toBlob(file: File): Promise<Blob> {
const zip = await this.compiler.compile(file);
const zipData = (await zip.generateAsync({ type: "blob" })) as Blob;
return zipData;
}
}

View File

@ -0,0 +1,48 @@
/* tslint:disable:typedef space-before-function-paren */
import { assert, expect } from "chai";
import { stub } from "sinon";
import { File, Paragraph } from "../../file";
import { PdfPacker } from "./pdf-packer";
describe("PdfPacker", () => {
let packer: PdfPacker;
let file: File;
beforeEach(() => {
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 PdfPacker();
});
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(new Buffer(""));
const buffer = await packer.toBuffer(file);
expect(buffer).is.an.instanceof(Buffer);
});
it("should handle exception if it throws any", () => {
// tslint:disable-next-line:no-any
const compiler = stub((packer as any).packer, "toBuffer");
compiler.throwsException();
return packer.toBuffer(file).catch((error) => {
assert.isDefined(error);
});
});
});
});

View File

@ -1,12 +1,23 @@
import * as fs from "fs";
import * as request from "request-promise";
export interface IConvertOutput {
data: string;
}
import { File } from "file";
import { Packer } from "./packer";
export class PdfConvertWrapper {
public convert(filePath: string): request.RequestPromise {
export class PdfPacker {
private readonly packer: Packer;
constructor() {
this.packer = new Packer();
}
public async toBuffer(file: File): Promise<Buffer> {
const buffer = await this.packer.toBuffer(file);
const text = await this.convert(buffer);
return text;
}
private convert(buffer: Buffer): request.RequestPromise {
return request.post({
url: "http://mirror1.convertonlinefree.com",
// tslint:disable-next-line:no-null-keyword
@ -20,7 +31,7 @@ export class PdfConvertWrapper {
__EVENTARGUMENT: "",
__VIEWSTATE: "",
ctl00$MainContent$fu: {
value: fs.readFileSync(filePath),
value: buffer,
options: {
filename: "output.docx",
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",

View File

@ -1,25 +0,0 @@
import { Readable, Transform } from "stream";
import { File } from "../../file";
import { Compiler } from "./compiler";
import { IPacker } from "./packer";
class Pipe extends Transform {
public _transform(chunk: Buffer | string, encoding: string, callback: () => void): void {
this.push(chunk, encoding);
callback();
}
}
export class StreamPacker implements IPacker {
private readonly compiler: Compiler;
constructor(file: File) {
this.compiler = new Compiler(file);
}
public pack(): Readable {
const pipe = new Pipe();
this.compiler.compile(pipe);
return pipe;
}
}