Merge branch 'master' into master

This commit is contained in:
Sergio Mendonça
2018-08-22 08:04:45 -03:00
committed by GitHub
83 changed files with 1157 additions and 1153 deletions

View File

@ -1,5 +1 @@
export * from "./packer/local";
export * from "./packer/express";
export * from "./packer/packer";
export * from "./packer/stream";
export * from "./packer/buffer";

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,114 +0,0 @@
import * as archiver from "archiver";
import * as express from "express";
import { Writable } from "stream";
import * as xml from "xml";
import { File } from "file";
import { Formatter } from "../formatter";
export class Compiler {
protected archive: archiver.Archiver;
private readonly formatter: Formatter;
constructor(private readonly file: File) {
this.formatter = new Formatter();
this.archive = archiver.create("zip", {});
this.archive.on("error", (err) => {
throw err;
});
}
public async compile(output: Writable | express.Response): Promise<void> {
this.archive.pipe(output);
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: {
standalone: "yes",
encoding: "UTF-8",
},
});
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 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",
});
this.archive.append(xmlStyles, {
name: "word/styles.xml",
});
this.archive.append(xmlProperties, {
name: "docProps/core.xml",
});
this.archive.append(xmlAppProperties, {
name: "docProps/app.xml",
});
this.archive.append(xmlNumbering, {
name: "word/numbering.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(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(xmlContentTypes, {
name: "[Content_Types].xml",
});
this.archive.append(xmlFileRelationships, {
name: "_rels/.rels",
});
for (const data of this.file.Media.Array) {
this.archive.append(data.stream, {
name: `word/media/${data.fileName}`,
});
}
this.archive.finalize();
return new Promise<void>((resolve) => {
output.on("close", () => {
resolve();
});
});
}
}

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,47 +0,0 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { File } from "../../file";
import { Compiler } from "./compiler";
import { IPacker } from "./packer";
import { PdfConvertWrapper } from "./pdf-convert-wrapper";
export class LocalPacker implements IPacker {
private stream: fs.WriteStream;
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$/, "");
this.stream = fs.createWriteStream(`${filePath}.docx`);
await this.packer.compile(this.stream);
}
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`);
this.stream = fs.createWriteStream(tempPath);
await this.packer.compile(this.stream);
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);
return new Promise<void>((resolve, reject) => {
fs.writeFile(`${filePath}.pdf`, text, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
}

View File

@ -1,10 +1,7 @@
/* 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";
import { Compiler } from "./next-compiler";
describe("Compiler", () => {
let compiler: Compiler;
@ -12,21 +9,17 @@ describe("Compiler", () => {
beforeEach(() => {
file = new File();
compiler = new Compiler(file);
compiler = new Compiler();
});
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 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(13);
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");
@ -49,15 +42,12 @@ describe("Compiler", () => {
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 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(21);
expect(fileNames).has.length(25);
expect(fileNames).to.include("word/header1.xml");
expect(fileNames).to.include("word/_rels/header1.xml.rels");

View File

@ -0,0 +1,125 @@
import * as JSZip from "jszip";
import * as xml from "xml";
import { File } from "file";
import { Formatter } from "../formatter";
interface IXmlifyedFile {
data: string;
path: string;
}
interface IXmlifyedFileMapping {
Document: IXmlifyedFile;
Styles: IXmlifyedFile;
Properties: IXmlifyedFile;
Numbering: IXmlifyedFile;
Relationships: IXmlifyedFile;
FileRelationships: IXmlifyedFile;
Headers: IXmlifyedFile[];
Footers: IXmlifyedFile[];
HeaderRelationships: IXmlifyedFile[];
FooterRelationships: IXmlifyedFile[];
ContentTypes: IXmlifyedFile;
AppProperties: IXmlifyedFile;
FootNotes: IXmlifyedFile;
}
export class Compiler {
private readonly formatter: Formatter;
constructor() {
this.formatter = new Formatter();
}
public async compile(file: File): Promise<JSZip> {
const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(file);
for (const key in xmlifiedFileMapping) {
if (!xmlifiedFileMapping[key]) {
continue;
}
const obj = xmlifiedFileMapping[key] as IXmlifyedFile | IXmlifyedFile[];
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 file.Media.Array) {
const mediaData = data.stream;
zip.file(`word/media/${data.fileName}`, mediaData);
}
return zip;
}
private xmlifyFile(file: File): IXmlifyedFileMapping {
return {
Document: {
data: xml(this.formatter.format(file.Document), true),
path: "word/document.xml",
},
Styles: {
data: xml(this.formatter.format(file.Styles)),
path: "word/styles.xml",
},
Properties: {
data: xml(this.formatter.format(file.CoreProperties), {
declaration: {
standalone: "yes",
encoding: "UTF-8",
},
}),
path: "docProps/core.xml",
},
Numbering: {
data: xml(this.formatter.format(file.Numbering)),
path: "word/numbering.xml",
},
Relationships: {
data: xml(this.formatter.format(file.DocumentRelationships)),
path: "word/_rels/document.xml.rels",
},
FileRelationships: {
data: xml(this.formatter.format(file.FileRelationships)),
path: "_rels/.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",
},
AppProperties: {
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

@ -1,34 +0,0 @@
import * as fs from "fs";
import * as request from "request-promise";
export interface IConvertOutput {
data: string;
}
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":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.94 Safari/537.36",
},
formData: {
__EVENTTARGET: "",
__EVENTARGUMENT: "",
__VIEWSTATE: "",
ctl00$MainContent$fu: {
value: fs.readFileSync(filePath),
options: {
filename: "output.docx",
contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
},
ctl00$MainContent$btnConvert: "Convert",
ctl00$MainContent$fuZip: "",
},
});
}
}

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

View File

@ -1,16 +1,17 @@
import { assert } from "chai";
import * as fs from "fs";
import { Utility } from "../../tests/utility";
import { Drawing, IDrawingOptions, PlacementPosition } from "./";
const imageBase64Data = `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`;
function createDrawing(drawingOptions?: IDrawingOptions): Drawing {
const path = "./demo/images/image1.jpeg";
return new Drawing(
{
fileName: "test.jpg",
referenceId: 1,
stream: fs.createReadStream(path),
stream: Buffer.from(imageBase64Data, "base64"),
path: path,
dimensions: {
pixels: {

View File

@ -123,20 +123,13 @@ export class File {
return this.document.createTable(rows, cols);
}
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);
public createImage(buffer: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): Image {
const image = Media.addImage(this, buffer, width, height);
this.document.addParagraph(image.Paragraph);
return image;

View File

@ -36,8 +36,8 @@ export class FooterWrapper {
this.footer.addChildElement(childElement);
}
public createImage(image: string): void {
const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount);
public createImage(image: Buffer, width?: number, height?: number): void {
const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height);
this.relationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",

View File

@ -36,8 +36,8 @@ export class HeaderWrapper {
this.header.addChildElement(childElement);
}
public createImage(image: string): void {
const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount);
public createImage(image: Buffer, width?: number, height?: number): void {
const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height);
this.relationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",

View File

@ -1,5 +1,3 @@
import * as fs from "fs";
export interface IMediaDataDimensions {
pixels: {
x: number;
@ -13,7 +11,7 @@ export interface IMediaDataDimensions {
export interface IMediaData {
referenceId: number;
stream: fs.ReadStream | Buffer;
stream: Buffer | Uint8Array | ArrayBuffer;
path?: string;
fileName: string;
dimensions: IMediaDataDimensions;

View File

@ -1,7 +1,3 @@
import * as fs from "fs";
import * as sizeOf from "image-size";
import * as path from "path";
import { IDrawingOptions } from "../drawing";
import { File } from "../file";
import { ImageParagraph } from "../paragraph";
@ -13,28 +9,11 @@ interface IHackedFile {
}
export class Media {
public static addImage(file: File, filePath: string, drawingOptions?: IDrawingOptions): 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, drawingOptions));
}
public static addImageFromBuffer(file: File, buffer: Buffer, width?: number, height?: number, drawingOptions?: IDrawingOptions): Image {
public static addImage(file: File, buffer: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number, drawingOptions?: IDrawingOptions): 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,
);
const mediaData = file.Media.addMedia(buffer, exposedFile.currentRelationshipId++, width, height);
file.DocumentRelationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
@ -72,34 +51,36 @@ export class Media {
return data;
}
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 addMedia(
buffer: Buffer | string | Uint8Array | ArrayBuffer,
referenceId: number,
width: number = 100,
height: number = 100,
): IMediaData {
const key = `${Media.generateId()}.png`;
public addMediaFromBuffer(fileName: string, buffer: Buffer, referenceId: number, width?: number, height?: number): IMediaData {
const key = fileName;
let dimensions;
if (width && height) {
dimensions = {
return this.createMedia(
key,
referenceId,
{
width: width,
height: height,
};
} else {
dimensions = sizeOf(buffer);
}
return this.createMedia(key, referenceId, dimensions, buffer);
},
buffer,
);
}
private createMedia(
key: string,
relationshipsCount: number,
dimensions: { width: number; height: number },
data: fs.ReadStream | Buffer,
data: Buffer | string | Uint8Array | ArrayBuffer,
filePath?: string,
): IMediaData {
if (typeof data === "string") {
data = this.convertDataURIToBinary(data);
}
const imageData = {
referenceId: this.map.size + relationshipsCount + 1,
stream: data,
@ -131,4 +112,23 @@ export class Media {
return array;
}
private convertDataURIToBinary(dataURI: string): Uint8Array {
// https://gist.github.com/borismus/1032746
// https://github.com/mafintosh/base64-to-uint8array
const BASE64_MARKER = ";base64,";
const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
if (typeof atob === "function") {
return new Uint8Array(
atob(dataURI.substring(base64Index))
.split("")
.map((c) => c.charCodeAt(0)),
);
} else {
const b = require("buf" + "fer");
return new b.Buffer(dataURI, "base64");
}
}
}

View File

@ -1,23 +1,63 @@
// http://officeopenxml.com/WPborders.php
import { Attributes, XmlComponent } from "file/xml-components";
class Border extends XmlComponent {
class BorderProperty extends XmlComponent {
public setProperties(color: string, space: string, value: string, size: string): XmlComponent {
const attrs = new Attributes({
color: color,
space: space,
val: value,
sz: size,
});
this.root.push(attrs);
return this;
}
}
export class Border extends XmlComponent {
constructor() {
super("w:bottom");
this.root.push(
new Attributes({
color: "auto",
space: "1",
val: "single",
sz: "6",
}),
);
super("w:pBdr");
}
public addTopBorder(color: string = "auto", space: string = "1", value: string = "single", size: string = "6"): XmlComponent {
const top = new BorderProperty("w:top");
top.setProperties(color, space, value, size);
this.root.push(top);
return this;
}
public addBottomBorder(color: string = "auto", space: string = "1", value: string = "single", size: string = "6"): XmlComponent {
const bottom = new BorderProperty("w:bottom");
bottom.setProperties(color, space, value, size);
this.root.push(bottom);
return this;
}
public addLeftBorder(color: string = "auto", space: string = "1", value: string = "single", size: string = "6"): XmlComponent {
const left = new BorderProperty("w:left");
left.setProperties(color, space, value, size);
this.root.push(left);
return this;
}
public addRightBorder(color: string = "auto", space: string = "1", value: string = "single", size: string = "6"): XmlComponent {
const right = new BorderProperty("w:right");
right.setProperties(color, space, value, size);
this.root.push(right);
return this;
}
}
export class ThematicBreak extends XmlComponent {
constructor() {
super("w:pBdr");
this.root.push(new Border());
const bottom = new BorderProperty("w:bottom");
bottom.setProperties("auto", "1", "single", "6");
this.root.push(bottom);
}
}

View File

@ -1,7 +1,7 @@
// http://officeopenxml.com/WPindentation.php
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
interface IIndentAttributesProperties {
export interface IIndentAttributesProperties {
left?: number;
hanging?: number;
firstLine?: number;
@ -20,7 +20,7 @@ class IndentAttributes extends XmlAttributeComponent<IIndentAttributesProperties
}
export class Indent extends XmlComponent {
constructor(attrs: object) {
constructor(attrs: IIndentAttributesProperties) {
super("w:ind");
this.root.push(new IndentAttributes(attrs));
}

View File

@ -144,6 +144,51 @@ describe("Paragraph", () => {
});
});
describe("#paragraphBorders()", () => {
it("should add a left and right border to a paragraph", () => {
paragraph.createBorder();
paragraph.Borders.addLeftBorder();
paragraph.Borders.addRightBorder();
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
"w:p": [
{
"w:pPr": [
{
"w:pBdr": [
{
"w:left": [
{
_attr: {
"w:color": "auto",
"w:space": "1",
"w:sz": "6",
"w:val": "single",
},
},
],
},
{
"w:right": [
{
_attr: {
"w:color": "auto",
"w:space": "1",
"w:sz": "6",
"w:val": "single",
},
},
],
},
],
},
],
},
],
});
});
});
describe("#pageBreak()", () => {
it("should add page break to JSON", () => {
paragraph.pageBreak();

View File

@ -6,8 +6,8 @@ import { XmlComponent } from "file/xml-components";
import { Alignment } from "./formatting/alignment";
import { Bidirectional } from "./formatting/bidirectional";
import { ThematicBreak } from "./formatting/border";
import { Indent } from "./formatting/indent";
import { Border, ThematicBreak } from "./formatting/border";
import { IIndentAttributesProperties, Indent } from "./formatting/indent";
import { KeepLines, KeepNext } from "./formatting/keep";
import { PageBreak, PageBreakBefore } from "./formatting/page-break";
import { ISpacingProperties, Spacing } from "./formatting/spacing";
@ -30,6 +30,15 @@ export class Paragraph extends XmlComponent {
}
}
public get Borders(): Border {
return this.properties.paragraphBorder;
}
public createBorder(): Paragraph {
this.properties.createBorder();
return this;
}
public addRun(run: Run): Paragraph {
this.root.push(run);
return this;
@ -188,7 +197,7 @@ export class Paragraph extends XmlComponent {
return this;
}
public indent(attrs: object): Paragraph {
public indent(attrs: IIndentAttributesProperties): Paragraph {
this.properties.push(new Indent(attrs));
return this;
}
@ -222,4 +231,8 @@ export class Paragraph extends XmlComponent {
this.properties.push(new Bidirectional());
return this;
}
public get Properties(): ParagraphProperties {
return this.properties;
}
}

View File

@ -1,9 +1,17 @@
// http://officeopenxml.com/WPparagraphProperties.php
import { XmlComponent } from "file/xml-components";
import { Border } from "./formatting/border";
export class ParagraphProperties extends XmlComponent {
public paragraphBorder: Border;
constructor() {
super("w:pPr");
this.paragraphBorder = new Border();
}
public createBorder(): void {
this.push(this.paragraphBorder);
}
public push(item: XmlComponent): void {

View File

@ -76,6 +76,10 @@ export class Table extends XmlComponent {
this.properties.setFixedWidthLayout();
return this;
}
public get Properties(): TableProperties {
return this.properties;
}
}
export class TableRow extends XmlComponent {