Merge branch 'master' into feat/math

# Conflicts:
#	src/file/paragraph/index.ts
#	src/file/paragraph/paragraph.ts
This commit is contained in:
Dolan Miu
2019-11-01 02:43:14 +00:00
118 changed files with 13170 additions and 2053 deletions

View File

@ -1,7 +1,8 @@
import { assert, expect } from "chai";
import * as sinon from "sinon";
import { Formatter } from "export/formatter";
import * as file from "file";
import { Paragraph, TextRun } from "file";
import { CoreProperties } from "file/core-properties";
import { Attributes } from "file/xml-components";
@ -14,28 +15,65 @@ describe("Formatter", () => {
describe("#format()", () => {
it("should format simple paragraph", () => {
const paragraph = new file.Paragraph("");
const paragraph = new Paragraph("");
const newJson = formatter.format(paragraph);
assert.isDefined(newJson["w:p"]);
});
it("should remove xmlKeys", () => {
const paragraph = new file.Paragraph("");
const paragraph = new Paragraph("");
const newJson = formatter.format(paragraph);
const stringifiedJson = JSON.stringify(newJson);
assert(stringifiedJson.indexOf("xmlKeys") < 0);
});
it("should format simple paragraph with bold text", () => {
const paragraph = new file.Paragraph("");
paragraph.addRun(
new file.TextRun({
text: "test",
bold: true,
}),
);
const newJson = formatter.format(paragraph);
assert.isDefined(newJson["w:p"][1]["w:r"][0]["w:rPr"][0]["w:b"]._attr["w:val"]);
const paragraph = new Paragraph({
children: [
new TextRun({
text: "test",
bold: true,
}),
],
});
const tree = formatter.format(paragraph);
expect(tree).to.deep.equal({
"w:p": [
{
"w:r": [
{
"w:rPr": [
{
"w:b": {
_attr: {
"w:val": true,
},
},
},
{
"w:bCs": {
_attr: {
"w:val": true,
},
},
},
],
},
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
],
});
});
it("should format attributes (rsidSect)", () => {
@ -63,7 +101,7 @@ describe("Formatter", () => {
});
it("should should change 'p' tag into 'w:p' tag", () => {
const paragraph = new file.Paragraph("");
const paragraph = new Paragraph("");
const newJson = formatter.format(paragraph);
assert.isDefined(newJson["w:p"]);
});
@ -76,5 +114,13 @@ describe("Formatter", () => {
const newJson = formatter.format(properties);
assert.isDefined(newJson["cp:coreProperties"]);
});
it("should call the prep method only once", () => {
const paragraph = new Paragraph("");
const spy = sinon.spy(paragraph, "prepForXml");
formatter.format(paragraph);
expect(spy.calledOnce).to.equal(true);
});
});
});

View File

@ -5,7 +5,7 @@ export class ImageReplacer {
let currentXmlData = xmlData;
mediaData.forEach((image, i) => {
currentXmlData = currentXmlData.replace(`{${image.fileName}}`, (offset + i).toString());
currentXmlData = currentXmlData.replace(new RegExp(`{${image.fileName}}`, "g"), (offset + i).toString());
});
return currentXmlData;

View File

@ -1,7 +1,8 @@
/* tslint:disable:typedef space-before-function-paren */
import { expect } from "chai";
import * as sinon from "sinon";
import { File, Footer, Header } from "file";
import { File, Footer, Header, Paragraph } from "file";
import { Compiler } from "./next-compiler";
@ -72,5 +73,22 @@ describe("Compiler", () => {
expect(fileNames).to.include("word/footer2.xml");
expect(fileNames).to.include("word/_rels/footer2.xml.rels");
});
it("should call the format method X times equalling X files to be formatted", () => {
// This test is required because before, there was a case where Document was formatted twice, which was inefficient
// This also caused issues such as running prepForXml multiple times as format() was ran multiple times.
const paragraph = new Paragraph("");
const doc = new File();
doc.addSection({
properties: {},
children: [paragraph],
});
// tslint:disable-next-line: no-string-literal
const spy = sinon.spy(compiler["formatter"], "format");
compiler.compile(file);
expect(spy.callCount).to.equal(10);
});
});
});

View File

@ -68,13 +68,13 @@ export class Compiler {
file.verifyUpdateFields();
const documentRelationshipCount = file.DocumentRelationships.RelationshipCount + 1;
const documentXmlData = xml(this.formatter.format(file.Document), prettify);
const documentMediaDatas = this.imageReplacer.getMediaData(documentXmlData, file.Media);
return {
Relationships: {
data: (() => {
const xmlData = xml(this.formatter.format(file.Document), prettify);
const mediaDatas = this.imageReplacer.getMediaData(xmlData, file.Media);
mediaDatas.forEach((mediaData, i) => {
documentMediaDatas.forEach((mediaData, i) => {
file.DocumentRelationships.createRelationship(
documentRelationshipCount + i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
@ -88,9 +88,7 @@ export class Compiler {
},
Document: {
data: (() => {
const tempXmlData = xml(this.formatter.format(file.Document), prettify);
const mediaDatas = this.imageReplacer.getMediaData(tempXmlData, file.Media);
const xmlData = this.imageReplacer.replace(tempXmlData, mediaDatas, documentRelationshipCount);
const xmlData = this.imageReplacer.replace(documentXmlData, documentMediaDatas, documentRelationshipCount);
return xmlData;
})(),

View File

@ -4,33 +4,33 @@ import { Compiler } from "./next-compiler";
export class Packer {
public static async toBuffer(file: File, prettify?: boolean): Promise<Buffer> {
const zip = this.compiler.compile(file, prettify);
const zipData = (await zip.generateAsync({
const zipData = await zip.generateAsync({
type: "nodebuffer",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
})) as Buffer;
});
return zipData;
}
public static async toBase64String(file: File, prettify?: boolean): Promise<string> {
const zip = this.compiler.compile(file, prettify);
const zipData = (await zip.generateAsync({
const zipData = await zip.generateAsync({
type: "base64",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
})) as string;
});
return zipData;
}
public static async toBlob(file: File, prettify?: boolean): Promise<Blob> {
const zip = this.compiler.compile(file, prettify);
const zipData = (await zip.generateAsync({
const zipData = await zip.generateAsync({
type: "blob",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
})) as Blob;
});
return zipData;
}

View File

@ -1,5 +1,7 @@
import { XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { IStylesOptions } from "../styles";
import { Created, Creator, Description, Keywords, LastModifiedBy, Modified, Revision, Subject, Title } from "./components";
export interface IPropertiesOptions {
@ -11,6 +13,7 @@ export interface IPropertiesOptions {
readonly lastModifiedBy?: string;
readonly revision?: string;
readonly externalStyles?: string;
readonly styles?: IStylesOptions;
}
export class CoreProperties extends XmlComponent {

View File

@ -0,0 +1,42 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { FooterReference } from "./footer-reference";
import { FooterReferenceType } from "./footer-reference-attributes";
describe("footerReference", () => {
it("should create", () => {
const footer = new FooterReference({
footerType: FooterReferenceType.DEFAULT,
footerId: 1,
});
const tree = new Formatter().format(footer);
expect(tree).to.deep.equal({
"w:footerReference": {
_attr: {
"r:id": "rId1",
"w:type": "default",
},
},
});
});
it("should create without a footer type", () => {
const footer = new FooterReference({
footerId: 1,
});
const tree = new Formatter().format(footer);
expect(tree).to.deep.equal({
"w:footerReference": {
_attr: {
"r:id": "rId1",
"w:type": "default",
},
},
});
});
});

View File

@ -0,0 +1,42 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { HeaderReference } from "./header-reference";
import { HeaderReferenceType } from "./header-reference-attributes";
describe("HeaderReference", () => {
it("should create", () => {
const footer = new HeaderReference({
headerType: HeaderReferenceType.DEFAULT,
headerId: 1,
});
const tree = new Formatter().format(footer);
expect(tree).to.deep.equal({
"w:headerReference": {
_attr: {
"r:id": "rId1",
"w:type": "default",
},
},
});
});
it("should create without a header type", () => {
const footer = new HeaderReference({
headerId: 1,
});
const tree = new Formatter().format(footer);
expect(tree).to.deep.equal({
"w:headerReference": {
_attr: {
"r:id": "rId1",
"w:type": "default",
},
},
});
});
});

View File

@ -38,6 +38,7 @@ describe("SectionProperties", () => {
},
pageNumberStart: 10,
pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT,
titlePage: true,
});
const tree = new Formatter().format(properties);
expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]);

View File

@ -6,7 +6,7 @@ import { Formatter } from "export/formatter";
import { File } from "./file";
import { Footer, Header } from "./header";
import { Paragraph } from "./paragraph";
import { Table } from "./table";
import { Table, TableCell, TableRow } from "./table";
import { TableOfContents } from "./table-of-contents";
describe("File", () => {
@ -108,8 +108,15 @@ describe("File", () => {
file.addSection({
children: [
new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
}),
],
});

View File

@ -46,8 +46,6 @@ export interface ISectionOptions {
export class File {
// tslint:disable-next-line:readonly-keyword
private currentRelationshipId: number = 1;
// tslint:disable-next-line:readonly-keyword
private styles: Styles;
private readonly document: Document;
private readonly headers: IDocumentHeader[] = [];
@ -61,6 +59,7 @@ export class File {
private readonly settings: Settings;
private readonly contentTypes: ContentTypes;
private readonly appProperties: AppProperties;
private readonly styles: Styles;
constructor(
options: IPropertiesOptions = {
@ -97,9 +96,16 @@ export class File {
} else if (options.externalStyles) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else if (options.styles) {
const stylesFactory = new DefaultStylesFactory();
const defaultStyles = stylesFactory.newInstance();
this.styles = new Styles({
...defaultStyles,
...options.styles,
});
} else {
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
this.styles = new Styles(stylesFactory.newInstance());
}
this.addDefaultRelationships();
@ -277,10 +283,6 @@ export class File {
return this.styles;
}
public set Styles(styles: Styles) {
this.styles = styles;
}
public get CoreProperties(): CoreProperties {
return this.coreProperties;
}

View File

@ -4,7 +4,7 @@ import * as sinon from "sinon";
import { FooterWrapper } from "./footer-wrapper";
import { Media } from "./media";
import { Paragraph } from "./paragraph";
import { Table } from "./table";
import { Table, TableCell, TableRow } from "./table";
describe("FooterWrapper", () => {
describe("#add", () => {
@ -21,22 +21,20 @@ describe("FooterWrapper", () => {
const spy = sinon.spy(file.Footer, "add");
file.add(
new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
}),
);
expect(spy.called).to.equal(true);
});
it("should call the underlying footer's addImage", () => {
const file = new FooterWrapper(new Media(), 1);
const spy = sinon.spy(file.Footer, "add");
// tslint:disable-next-line:no-any
file.addImage({} as any);
expect(spy.called).to.equal(true);
});
});
describe("#addChildElement", () => {

View File

@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components";
import { FooterReferenceType } from "./document";
import { Footer } from "./footer/footer";
import { Image, Media } from "./media";
import { Media } from "./media";
import { Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
@ -25,11 +25,6 @@ export class FooterWrapper {
this.footer.add(item);
}
public addImage(image: Image): FooterWrapper {
this.footer.add(image.Paragraph);
return this;
}
public addChildElement(childElement: XmlComponent): void {
this.footer.addChildElement(childElement);
}

View File

@ -0,0 +1,47 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Paragraph } from "../paragraph";
import { Footer } from "./footer";
describe("Footer", () => {
it("should create", () => {
const footer = new Footer(1);
const tree = new Formatter().format(footer);
expect(tree).to.deep.equal({
"w:ftr": {
_attr: {
"xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
"xmlns:o": "urn:schemas-microsoft-com:office:office",
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"xmlns:v": "urn:schemas-microsoft-com:vml",
"xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"xmlns:w10": "urn:schemas-microsoft-com:office:word",
"xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml",
"xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml",
"xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml",
"xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
"xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
"xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
"xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
"xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
},
},
});
});
it("should create with initContent", () => {
const header = new Footer(1, new Paragraph({}));
const tree = new Formatter().format(header);
expect(tree).to.deep.equal({
"w:ftr": {},
});
});
});

View File

@ -44,7 +44,8 @@ export class FootNotes extends XmlComponent {
line: 240,
lineRule: "auto",
},
}).addRun(new SeperatorRun()),
children: [new SeperatorRun()],
}),
);
this.root.push(begin);
@ -56,7 +57,8 @@ export class FootNotes extends XmlComponent {
line: 240,
lineRule: "auto",
},
}).addRun(new ContinuationSeperatorRun()),
children: [new ContinuationSeperatorRun()],
}),
);
this.root.push(spacing);
}

View File

@ -4,7 +4,7 @@ import * as sinon from "sinon";
import { HeaderWrapper } from "./header-wrapper";
import { Media } from "./media";
import { Paragraph } from "./paragraph";
import { Table } from "./table";
import { Table, TableCell, TableRow } from "./table";
describe("HeaderWrapper", () => {
describe("#add", () => {
@ -21,8 +21,15 @@ describe("HeaderWrapper", () => {
const spy = sinon.spy(wrapper.Header, "add");
wrapper.add(
new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
}),
);
@ -30,17 +37,6 @@ describe("HeaderWrapper", () => {
});
});
describe("#addImage", () => {
it("should call the underlying header's addImage", () => {
const file = new HeaderWrapper(new Media(), 1);
const spy = sinon.spy(file.Header, "add");
// tslint:disable-next-line:no-any
file.addImage({} as any);
expect(spy.called).to.equal(true);
});
});
describe("#addChildElement", () => {
it("should call the underlying header's addChildElement", () => {
const file = new HeaderWrapper(new Media(), 1);

View File

@ -2,7 +2,7 @@ import { XmlComponent } from "file/xml-components";
import { HeaderReferenceType } from "./document";
import { Header } from "./header/header";
import { Image, Media } from "./media";
import { Media } from "./media";
import { Paragraph } from "./paragraph";
import { Relationships } from "./relationships";
import { Table } from "./table";
@ -27,11 +27,6 @@ export class HeaderWrapper {
return this;
}
public addImage(image: Image): HeaderWrapper {
this.header.add(image.Paragraph);
return this;
}
public addChildElement(childElement: XmlComponent | string): void {
this.header.addChildElement(childElement);
}

View File

@ -0,0 +1,58 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Paragraph } from "../paragraph";
import { Header } from "./header";
describe("Header", () => {
it("should create", () => {
const header = new Header(1);
const tree = new Formatter().format(header);
expect(tree).to.deep.equal({
"w:hdr": {
_attr: {
"xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex",
"xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
"xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
"xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
"xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
"xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
"xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
"xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
"xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
"xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
"xmlns:o": "urn:schemas-microsoft-com:office:office",
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"xmlns:v": "urn:schemas-microsoft-com:vml",
"xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"xmlns:w10": "urn:schemas-microsoft-com:office:word",
"xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml",
"xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml",
"xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
"xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex",
"xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml",
"xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
"xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
"xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
"xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
"xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
},
},
});
});
it("should create with initContent", () => {
const header = new Header(1, new Paragraph({}));
const tree = new Formatter().format(header);
expect(tree).to.deep.equal({
"w:hdr": {},
});
});
});

View File

@ -1,13 +0,0 @@
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;
}
}

View File

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

View File

@ -80,6 +80,16 @@ describe("Media", () => {
const image = new Media().addMedia("");
expect(image.stream).to.be.an.instanceof(Uint8Array);
});
it("should use data as is if its not a string", () => {
// tslint:disable-next-line
((process as any).atob as any) = () => "atob result";
// tslint:disable-next-line:no-any
(Media as any).generateId = () => "test";
const image = new Media().addMedia(new Buffer(""));
expect(image.stream).to.be.an.instanceof(Uint8Array);
});
});
describe("#getMedia", () => {

View File

@ -7,14 +7,15 @@ import {
ISpacingProperties,
KeepLines,
KeepNext,
LeftTabStop,
MaxRightTabStop,
Spacing,
TabStop,
TabStopType,
ThematicBreak,
} from "../paragraph/formatting";
import { ParagraphProperties } from "../paragraph/properties";
import * as formatting from "../paragraph/run/formatting";
import { RunProperties } from "../paragraph/run/properties";
import { UnderlineType } from "../paragraph/run/underline";
interface ILevelAttributesProperties {
readonly ilvl?: number;
@ -184,7 +185,7 @@ export class LevelBase extends XmlComponent {
return this;
}
public underline(underlineType?: string, color?: string): Level {
public underline(underlineType?: UnderlineType, color?: string): Level {
this.addRunProperty(new formatting.Underline(underlineType, color));
return this;
}
@ -235,13 +236,12 @@ export class LevelBase extends XmlComponent {
return this;
}
public maxRightTabStop(): Level {
this.addParagraphProperty(new MaxRightTabStop());
return this;
public rightTabStop(position: number): Level {
return this.addParagraphProperty(new TabStop(TabStopType.RIGHT, position));
}
public leftTabStop(position: number): Level {
this.addParagraphProperty(new LeftTabStop(position));
this.addParagraphProperty(new TabStop(TabStopType.LEFT, position));
return this;
}

View File

@ -8,6 +8,8 @@ import { Num } from "./num";
import { Numbering } from "./numbering";
import { EMPTY_OBJECT } from "file/xml-components";
import { TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
describe("Numbering", () => {
let numbering: Numbering;
@ -202,7 +204,7 @@ describe("AbstractNumbering", () => {
it("#maxRightTabStop", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").maxRightTabStop();
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").rightTabStop(TabStopPosition.MAX);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:pPr": [
@ -355,7 +357,7 @@ describe("AbstractNumbering", () => {
it("should set the style if given", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline("double");
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE);
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double" } } }],
@ -364,7 +366,7 @@ describe("AbstractNumbering", () => {
it("should set the style and color if given", () => {
const abstractNumbering = new AbstractNumbering(1);
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline("double", "005599");
const level = abstractNumbering.createLevel(0, "lowerRoman", "%0.").underline(UnderlineType.DOUBLE, "005599");
const tree = new Formatter().format(level);
expect(tree["w:lvl"]).to.include({
"w:rPr": [{ "w:u": { _attr: { "w:val": "double", "w:color": "005599" } } }],

View File

@ -1,11 +1,87 @@
import { assert, expect } from "chai";
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { ThematicBreak } from "./border";
import { Border, ThematicBreak } from "./border";
describe("Border", () => {
// TODO: Need tests here
describe("#constructor", () => {
it("should create", () => {
const border = new Border({
top: {
color: "red",
space: 1,
value: "test",
size: 2,
},
bottom: {
color: "red",
space: 3,
value: "test",
size: 4,
},
left: {
color: "red",
space: 5,
value: "test",
size: 6,
},
right: {
color: "red",
space: 7,
value: "test",
size: 8,
},
});
const tree = new Formatter().format(border);
expect(tree).to.deep.equal({
"w:pBdr": [
{
"w:top": {
_attr: {
"w:color": "red",
"w:space": 1,
"w:sz": 2,
"w:val": "test",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "red",
"w:space": 3,
"w:sz": 4,
"w:val": "test",
},
},
},
{
"w:left": {
_attr: {
"w:color": "red",
"w:space": 5,
"w:sz": 6,
"w:val": "test",
},
},
},
{
"w:right": {
_attr: {
"w:color": "red",
"w:space": 7,
"w:sz": 8,
"w:val": "test",
},
},
},
],
});
});
});
});
describe("ThematicBreak", () => {
@ -16,17 +92,6 @@ describe("ThematicBreak", () => {
});
describe("#constructor()", () => {
it("should create valid JSON", () => {
const stringifiedJson = JSON.stringify(thematicBreak);
try {
JSON.parse(stringifiedJson);
} catch (e) {
assert.isTrue(false);
}
assert.isTrue(true);
});
it("should create a Thematic Break with correct border properties", () => {
const tree = new Formatter().format(thematicBreak);
expect(tree).to.deep.equal({

View File

@ -2,13 +2,13 @@ import { assert } from "chai";
import { Utility } from "tests/utility";
import { LeaderType, LeftTabStop, MaxRightTabStop, RightTabStop } from "./tab-stop";
import { LeaderType, TabStop, TabStopType } from "./tab-stop";
describe("LeftTabStop", () => {
let tabStop: LeftTabStop;
let tabStop: TabStop;
beforeEach(() => {
tabStop = new LeftTabStop(100);
tabStop = new TabStop(TabStopType.LEFT, 100);
});
describe("#constructor()", () => {
@ -29,10 +29,10 @@ describe("LeftTabStop", () => {
});
describe("RightTabStop", () => {
let tabStop: RightTabStop;
let tabStop: TabStop;
beforeEach(() => {
tabStop = new RightTabStop(100, LeaderType.DOT);
tabStop = new TabStop(TabStopType.RIGHT, 100, LeaderType.DOT);
});
describe("#constructor()", () => {
@ -52,28 +52,3 @@ describe("RightTabStop", () => {
});
});
});
describe("MaxRightTabStop", () => {
let tabStop: MaxRightTabStop;
beforeEach(() => {
tabStop = new MaxRightTabStop();
});
describe("#constructor()", () => {
it("should create a Tab Stop with correct attributes", () => {
const newJson = Utility.jsonify(tabStop);
const attributes = {
val: "right",
pos: 9026,
};
assert.equal(JSON.stringify(newJson.root[0].root[0].root), JSON.stringify(attributes));
});
it("should create a Tab Stop with w:tab", () => {
const newJson = Utility.jsonify(tabStop);
assert.equal(newJson.root[0].rootKey, "w:tab");
});
});
});

View File

@ -2,13 +2,13 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export class TabStop extends XmlComponent {
constructor(tab: TabStopItem) {
constructor(type: TabStopType, position: number, leader?: LeaderType) {
super("w:tabs");
this.root.push(tab);
this.root.push(new TabStopItem(type, position, leader));
}
}
export enum TabValue {
export enum TabStopType {
LEFT = "left",
RIGHT = "right",
CENTER = "center",
@ -28,8 +28,12 @@ export enum LeaderType {
UNDERSCORE = "underscore",
}
export enum TabStopPosition {
MAX = 9026,
}
export class TabAttributes extends XmlAttributeComponent<{
readonly val: TabValue;
readonly val: TabStopType;
readonly pos: string | number;
readonly leader?: LeaderType;
}> {
@ -37,7 +41,7 @@ export class TabAttributes extends XmlAttributeComponent<{
}
export class TabStopItem extends XmlComponent {
constructor(value: TabValue, position: string | number, leader?: LeaderType) {
constructor(value: TabStopType, position: string | number, leader?: LeaderType) {
super("w:tab");
this.root.push(
new TabAttributes({
@ -48,27 +52,3 @@ export class TabStopItem extends XmlComponent {
);
}
}
export class MaxRightTabStop extends TabStop {
constructor(leader?: LeaderType) {
super(new TabStopItem(TabValue.RIGHT, 9026, leader));
}
}
export class LeftTabStop extends TabStop {
constructor(position: number, leader?: LeaderType) {
super(new TabStopItem(TabValue.LEFT, position, leader));
}
}
export class RightTabStop extends TabStop {
constructor(position: number, leader?: LeaderType) {
super(new TabStopItem(TabValue.RIGHT, position, leader));
}
}
export class CenterTabStop extends TabStop {
constructor(position: number, leader?: LeaderType) {
super(new TabStopItem(TabValue.CENTER, position, leader));
}
}

View File

@ -1,39 +0,0 @@
// tslint:disable:object-literal-key-quotes
import { assert } from "chai";
import { ImageParagraph } from "./image";
describe("Image", () => {
let image: ImageParagraph;
beforeEach(() => {
image = new ImageParagraph({
stream: new Buffer(""),
path: "",
fileName: "test.png",
dimensions: {
pixels: {
x: 10,
y: 10,
},
emus: {
x: 10,
y: 10,
},
},
});
});
describe("#constructor()", () => {
it("should create valid JSON", () => {
const stringifiedJson = JSON.stringify(image);
try {
JSON.parse(stringifiedJson);
} catch (e) {
assert.isTrue(false);
}
assert.isTrue(true);
});
});
});

View File

@ -1,18 +0,0 @@
import { IDrawingOptions } from "../drawing";
import { IMediaData } from "../media";
import { Paragraph } from "./paragraph";
import { PictureRun } from "./run";
export class ImageParagraph extends Paragraph {
private readonly pictureRun: PictureRun;
constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) {
super({});
this.pictureRun = new PictureRun(imageData, drawingOptions);
this.root.push(this.pictureRun);
}
public get Run(): PictureRun {
return this.pictureRun;
}
}

View File

@ -3,5 +3,4 @@ export * from "./paragraph";
export * from "./properties";
export * from "./run";
export * from "./links";
export * from "./image";
export * from "./math";

View File

@ -4,7 +4,7 @@ import { Formatter } from "export/formatter";
import { EMPTY_OBJECT } from "file/xml-components";
import { Numbering } from "../numbering";
import { AlignmentType, HeadingLevel, LeaderType } from "./formatting";
import { AlignmentType, HeadingLevel, LeaderType, PageBreak, TabStopPosition, TabStopType } from "./formatting";
import { Paragraph } from "./paragraph";
describe("Paragraph", () => {
@ -254,11 +254,14 @@ describe("Paragraph", () => {
});
describe("#maxRightTabStop()", () => {
it("should add maxRightTabStop to JSON", () => {
it("should add right tab stop to JSON", () => {
const paragraph = new Paragraph({
tabStop: {
maxRight: {},
},
tabStops: [
{
type: TabStopType.RIGHT,
position: TabStopPosition.MAX,
},
],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
@ -287,12 +290,13 @@ describe("Paragraph", () => {
describe("#leftTabStop()", () => {
it("should add leftTabStop to JSON", () => {
const paragraph = new Paragraph({
tabStop: {
left: {
tabStops: [
{
type: TabStopType.LEFT,
position: 100,
leader: LeaderType.HYPHEN,
},
},
],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
@ -322,12 +326,13 @@ describe("Paragraph", () => {
describe("#rightTabStop()", () => {
it("should add rightTabStop to JSON", () => {
const paragraph = new Paragraph({
tabStop: {
right: {
tabStops: [
{
type: TabStopType.RIGHT,
position: 100,
leader: LeaderType.DOT,
},
},
],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
@ -357,12 +362,13 @@ describe("Paragraph", () => {
describe("#centerTabStop()", () => {
it("should add centerTabStop to JSON", () => {
const paragraph = new Paragraph({
tabStop: {
center: {
tabStops: [
{
type: TabStopType.CENTER,
position: 100,
leader: LeaderType.MIDDLE_DOT,
},
},
],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
@ -492,8 +498,9 @@ describe("Paragraph", () => {
describe("#pageBreak()", () => {
it("should add page break to JSON", () => {
const paragraph = new Paragraph({});
paragraph.pageBreak();
const paragraph = new Paragraph({
children: [new PageBreak()],
});
const tree = new Formatter().format(paragraph);
expect(tree).to.deep.equal({
"w:p": [

View File

@ -1,6 +1,5 @@
// http://officeopenxml.com/WPparagraph.php
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { Image } from "file/media";
import { Num } from "file/numbering/num";
import { XmlComponent } from "file/xml-components";
@ -12,17 +11,12 @@ import { KeepLines, KeepNext } from "./formatting/keep";
import { PageBreak, PageBreakBefore } from "./formatting/page-break";
import { ContextualSpacing, ISpacingProperties, Spacing } from "./formatting/spacing";
import { HeadingLevel, Style } from "./formatting/style";
import { CenterTabStop, LeaderType, LeftTabStop, MaxRightTabStop, RightTabStop } from "./formatting/tab-stop";
import { LeaderType, TabStop, TabStopPosition, TabStopType } from "./formatting/tab-stop";
import { NumberProperties } from "./formatting/unordered-list";
import { Bookmark, Hyperlink, OutlineLevel } from "./links";
import { Math } from "./math";
import { ParagraphProperties } from "./properties";
import { PictureRun, Run, SequentialIdentifier, TextRun } from "./run";
interface ITabStopOptions {
readonly position: number;
readonly leader?: LeaderType;
}
import { PictureRun, Run, SequentialIdentifier, SymbolRun, TextRun } from "./run";
export interface IParagraphOptions {
readonly text?: string;
@ -38,14 +32,11 @@ export interface IParagraphOptions {
readonly indent?: IIndentAttributesProperties;
readonly keepLines?: boolean;
readonly keepNext?: boolean;
readonly tabStop?: {
readonly left?: ITabStopOptions;
readonly right?: ITabStopOptions;
readonly maxRight?: {
readonly leader?: LeaderType;
};
readonly center?: ITabStopOptions;
};
readonly tabStops?: Array<{
readonly position: number | TabStopPosition;
readonly type: TabStopType;
readonly leader?: LeaderType;
}>;
readonly style?: string;
readonly bullet?: {
readonly level: number;
@ -55,7 +46,7 @@ export interface IParagraphOptions {
readonly level: number;
readonly custom?: boolean;
};
readonly children?: Array<TextRun | PictureRun | Hyperlink | Math>;
readonly children?: Array<TextRun | PictureRun | Hyperlink | SymbolRun | Bookmark | PageBreak | SequentialIdentifier | Math>;
}
export class Paragraph extends XmlComponent {
@ -132,21 +123,9 @@ export class Paragraph extends XmlComponent {
this.properties.push(new KeepNext());
}
if (options.tabStop) {
if (options.tabStop.left) {
this.properties.push(new LeftTabStop(options.tabStop.left.position, options.tabStop.left.leader));
}
if (options.tabStop.right) {
this.properties.push(new RightTabStop(options.tabStop.right.position, options.tabStop.right.leader));
}
if (options.tabStop.maxRight) {
this.properties.push(new MaxRightTabStop(options.tabStop.maxRight.leader));
}
if (options.tabStop.center) {
this.properties.push(new CenterTabStop(options.tabStop.center.position, options.tabStop.center.leader));
if (options.tabStops) {
for (const tabStop of options.tabStops) {
this.properties.push(new TabStop(tabStop.type, tabStop.position, tabStop.leader));
}
}
@ -168,41 +147,18 @@ export class Paragraph extends XmlComponent {
if (options.children) {
for (const child of options.children) {
if (child instanceof Bookmark) {
this.root.push(child.start);
this.root.push(child.text);
this.root.push(child.end);
continue;
}
this.root.push(child);
}
}
}
public addRun(run: Run): Paragraph {
this.root.push(run);
return this;
}
public addHyperLink(hyperlink: Hyperlink): Paragraph {
this.root.push(hyperlink);
return this;
}
public addBookmark(bookmark: Bookmark): Paragraph {
// Bookmarks by spec have three components, a start, text, and end
this.root.push(bookmark.start);
this.root.push(bookmark.text);
this.root.push(bookmark.end);
return this;
}
public addImage(image: Image): PictureRun {
const run = image.Run;
this.addRun(run);
return run;
}
public pageBreak(): Paragraph {
this.root.push(new PageBreak());
return this;
}
public referenceFootnote(id: number): Paragraph {
this.root.push(new FootnoteReferenceRun(id));
return this;
@ -212,9 +168,4 @@ export class Paragraph extends XmlComponent {
this.root.splice(1, 0, run);
return this;
}
public addSequentialIdentifier(identifier: string): Paragraph {
this.root.push(new SequentialIdentifier(identifier));
return this;
}
}

View File

@ -1,4 +1,7 @@
export * from "./run";
export * from "./text-run";
export * from "./symbol-run";
export * from "./picture-run";
export * from "./run-fonts";
export * from "./sequential-identifier";
export * from "./underline";

View File

@ -2,7 +2,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { NumberOfPages, Page } from "./page-number";
import { NumberOfPages, NumberOfPagesSection, Page } from "./page-number";
describe("Page", () => {
describe("#constructor()", () => {
@ -21,3 +21,12 @@ describe("NumberOfPages", () => {
});
});
});
describe("NumberOfPagesSection", () => {
describe("#constructor()", () => {
it("uses the font name for both ascii and hAnsi", () => {
const tree = new Formatter().format(new NumberOfPagesSection());
expect(tree).to.deep.equal({ "w:instrText": [{ _attr: { "xml:space": "preserve" } }, "SECTIONPAGES"] });
});
});
});

View File

@ -20,3 +20,11 @@ export class NumberOfPages extends XmlComponent {
this.root.push("NUMPAGES");
}
}
export class NumberOfPagesSection extends XmlComponent {
constructor() {
super("w:instrText");
this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
this.root.push("SECTIONPAGES");
}
}

View File

@ -0,0 +1,28 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Symbol } from "./symbol";
describe("Symbol", () => {
describe("#constructor", () => {
// Note: if no character is given, the output is a MS Windows logo
it("creates an empty symbol run if no character is given", () => {
const s = new Symbol();
const f = new Formatter().format(s);
expect(f).to.deep.equal({ "w:sym": { _attr: { "w:char": "", "w:font": "Wingdings" } } });
});
it("creates the provided symbol with default font", () => {
const s = new Symbol("F071");
const f = new Formatter().format(s);
expect(f).to.deep.equal({ "w:sym": { _attr: { "w:char": "F071", "w:font": "Wingdings" } } });
});
it("creates the provided symbol with the provided font", () => {
const s = new Symbol("F071", "Arial");
const f = new Formatter().format(s);
expect(f).to.deep.equal({ "w:sym": { _attr: { "w:char": "F071", "w:font": "Arial" } } });
});
});
});

View File

@ -0,0 +1,20 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
interface ISymbolAttributesProperties {
readonly char: string;
readonly symbolfont?: string;
}
class SymbolAttributes extends XmlAttributeComponent<ISymbolAttributesProperties> {
protected readonly xmlKeys = {
char: "w:char",
symbolfont: "w:font",
};
}
export class Symbol extends XmlComponent {
constructor(char: string = "", symbolfont: string = "Wingdings") {
super("w:sym");
this.root.push(new SymbolAttributes({ char: char, symbolfont: symbolfont }));
}
}

View File

@ -284,6 +284,22 @@ describe("Run", () => {
});
});
describe("#numberOfTotalPagesSection", () => {
it("should set the run to the RTL mode", () => {
const run = new Run({});
run.numberOfTotalPagesSection();
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
{ "w:fldChar": { _attr: { "w:fldCharType": "begin" } } },
{ "w:instrText": [{ _attr: { "xml:space": "preserve" } }, "SECTIONPAGES"] },
{ "w:fldChar": { _attr: { "w:fldCharType": "separate" } } },
{ "w:fldChar": { _attr: { "w:fldCharType": "end" } } },
],
});
});
});
describe("#pageNumber", () => {
it("should set the run to the RTL mode", () => {
const run = new Run({});

View File

@ -2,6 +2,7 @@
import { ShadingType } from "file/table";
import { XmlComponent } from "file/xml-components";
import { FieldInstruction } from "file/table-of-contents/field-instruction";
import { Break } from "./break";
import { Caps, SmallCaps } from "./caps";
import { Begin, End, Separate } from "./field";
@ -21,7 +22,7 @@ import {
SizeComplexScript,
Strike,
} from "./formatting";
import { NumberOfPages, Page } from "./page-number";
import { NumberOfPages, NumberOfPagesSection, Page } from "./page-number";
import { RunProperties } from "./properties";
import { RunFonts } from "./run-fonts";
import { SubScript, SuperScript } from "./script";
@ -56,6 +57,7 @@ export interface IRunOptions {
readonly fill: string;
readonly color: string;
};
readonly children?: Array<Begin | FieldInstruction | Separate | End>;
}
export class Run extends XmlComponent {
@ -134,6 +136,12 @@ export class Run extends XmlComponent {
this.properties.push(new Shading(options.shading.type, options.shading.fill, options.shading.color));
this.properties.push(new ShadowComplexScript(options.shading.type, options.shading.fill, options.shading.color));
}
if (options.children) {
for (const child of options.children) {
this.root.push(child);
}
}
}
public break(): Run {
@ -161,4 +169,12 @@ export class Run extends XmlComponent {
this.root.push(new End());
return this;
}
public numberOfTotalPagesSection(): Run {
this.root.push(new Begin());
this.root.push(new NumberOfPagesSection());
this.root.push(new Separate());
this.root.push(new End());
return this;
}
}

View File

@ -0,0 +1,76 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { UnderlineType } from "./underline";
import { SymbolRun } from "./symbol-run";
describe("SymbolRun", () => {
let run: SymbolRun;
describe("#constructor()", () => {
it("should create symbol run from text input", () => {
run = new SymbolRun("F071");
const f = new Formatter().format(run);
expect(f).to.deep.equal({
"w:r": [{ "w:sym": { _attr: { "w:char": "F071", "w:font": "Wingdings" } } }],
});
});
it("should create symbol run from object input with just 'char' specified", () => {
run = new SymbolRun({ char: "F071" });
const f = new Formatter().format(run);
expect(f).to.deep.equal({
"w:r": [{ "w:sym": { _attr: { "w:char": "F071", "w:font": "Wingdings" } } }],
});
});
it("should create symbol run from object input with just 'char' specified", () => {
run = new SymbolRun({ char: "F071", symbolfont: "Arial" });
const f = new Formatter().format(run);
expect(f).to.deep.equal({
"w:r": [{ "w:sym": { _attr: { "w:char": "F071", "w:font": "Arial" } } }],
});
});
it("should add other standard run properties", () => {
run = new SymbolRun({
char: "F071",
symbolfont: "Arial",
italics: true,
bold: true,
underline: {
color: "red",
type: UnderlineType.DOUBLE,
},
color: "green",
size: 40,
highlight: "yellow",
});
const f = new Formatter().format(run);
expect(f).to.deep.equal({
"w:r": [
{
"w:rPr": [
{ "w:b": { _attr: { "w:val": true } } },
{ "w:bCs": { _attr: { "w:val": true } } },
{ "w:i": { _attr: { "w:val": true } } },
{ "w:iCs": { _attr: { "w:val": true } } },
{ "w:u": { _attr: { "w:val": "double", "w:color": "red" } } },
{ "w:color": { _attr: { "w:val": "green" } } },
{ "w:sz": { _attr: { "w:val": 40 } } },
{ "w:szCs": { _attr: { "w:val": 40 } } },
{ "w:highlight": { _attr: { "w:val": "yellow" } } },
{ "w:highlightCs": { _attr: { "w:val": "yellow" } } },
],
},
{
"w:sym": { _attr: { "w:char": "F071", "w:font": "Arial" } },
},
],
});
});
});
});

View File

@ -0,0 +1,20 @@
import { IRunOptions, Run } from "./run";
import { Symbol } from "./run-components/symbol";
export interface ISymbolRunOptions extends IRunOptions {
readonly char: string;
readonly symbolfont?: string;
}
export class SymbolRun extends Run {
constructor(options: ISymbolRunOptions | string) {
if (typeof options === "string") {
super({});
this.root.push(new Symbol(options));
return;
}
super(options);
this.root.push(new Symbol(options.char, options.symbolfont));
}
}

View File

@ -16,4 +16,23 @@ describe("TextRun", () => {
});
});
});
describe("#referenceFootnote()", () => {
it("should add a valid footnote reference", () => {
run = new TextRun("test");
run.referenceFootnote(1);
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test"] },
{
"w:r": [
{ "w:rPr": [{ "w:rStyle": { _attr: { "w:val": "FootnoteReference" } } }] },
{ "w:footnoteReference": { _attr: { "w:id": 1 } } },
],
},
],
});
});
});
});

View File

@ -1,3 +1,4 @@
import { FootnoteReferenceRun } from "file/footnotes/footnote/run/reference-run";
import { IRunOptions, Run } from "./run";
import { Text } from "./run-components/text";
@ -16,4 +17,9 @@ export class TextRun extends Run {
super(options);
this.root.push(new Text(options.text));
}
public referenceFootnote(id: number): TextRun {
this.root.push(new FootnoteReferenceRun(id));
return this;
}
}

View File

@ -27,7 +27,7 @@ describe("Underline", () => {
});
it("should use the given style type and color", () => {
const underline = new u.Underline("double", "FF00CC");
const underline = new u.Underline(u.UnderlineType.DOUBLE, "FF00CC");
const tree = new Formatter().format(underline);
expect(tree).to.deep.equal({
"w:u": { _attr: { "w:val": "double", "w:color": "FF00CC" } },

View File

@ -33,7 +33,7 @@ export abstract class BaseUnderline extends XmlComponent {
}
export class Underline extends BaseUnderline {
constructor(underlineType: string = "single", color?: string) {
constructor(underlineType: UnderlineType = UnderlineType.SINGLE, color?: string) {
super(underlineType, color);
}
}

View File

@ -38,11 +38,13 @@ export class ExternalStylesFactory {
throw new Error("can not find styles element");
}
const importedStyle = new Styles(new ImportedRootElementAttributes(stylesXmlElement.attributes));
const stylesElements = stylesXmlElement.elements || [];
for (const childElm of stylesElements) {
importedStyle.push(convertToXmlComponent(childElm) as ImportedXmlComponent);
}
const importedStyle = new Styles({
initialStyles: new ImportedRootElementAttributes(stylesXmlElement.attributes),
importedStyles: stylesElements.map((childElm) => convertToXmlComponent(childElm) as ImportedXmlComponent),
});
return importedStyle;
}
}

View File

@ -1,7 +1,7 @@
import { DocumentAttributes } from "../document/document-attributes";
import { Color, Italics, Size } from "../paragraph/run/formatting";
import { Styles } from "./";
import { IStylesOptions } from "./styles";
import { DocumentDefaults } from "./defaults";
import {
FootnoteReferenceStyle,
FootnoteText,
@ -18,7 +18,7 @@ import {
} from "./style";
export class DefaultStylesFactory {
public newInstance(): Styles {
public newInstance(): IStylesOptions {
const documentAttributes = new DocumentAttributes({
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
@ -27,57 +27,55 @@ export class DefaultStylesFactory {
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
Ignorable: "w14 w15",
});
const styles = new Styles(documentAttributes);
styles.createDocumentDefaults();
const titleStyle = new TitleStyle();
titleStyle.addRunProperty(new Size(56));
styles.push(titleStyle);
const heading1Style = new Heading1Style();
heading1Style.addRunProperty(new Color("2E74B5"));
heading1Style.addRunProperty(new Size(32));
styles.push(heading1Style);
const heading2Style = new Heading2Style();
heading2Style.addRunProperty(new Color("2E74B5"));
heading2Style.addRunProperty(new Size(26));
styles.push(heading2Style);
const heading3Style = new Heading3Style();
heading3Style.addRunProperty(new Color("1F4D78"));
heading3Style.addRunProperty(new Size(24));
styles.push(heading3Style);
const heading4Style = new Heading4Style();
heading4Style.addRunProperty(new Color("2E74B5"));
heading4Style.addRunProperty(new Italics());
styles.push(heading4Style);
const heading5Style = new Heading5Style();
heading5Style.addRunProperty(new Color("2E74B5"));
styles.push(heading5Style);
const heading6Style = new Heading6Style();
heading6Style.addRunProperty(new Color("1F4D78"));
styles.push(heading6Style);
const listParagraph = new ListParagraph();
// listParagraph.addParagraphProperty();
styles.push(listParagraph);
const hyperLinkStyle = new HyperlinkStyle();
styles.push(hyperLinkStyle);
const footnoteReferenceStyle = new FootnoteReferenceStyle();
styles.push(footnoteReferenceStyle);
const footnoteTextStyle = new FootnoteText();
styles.push(footnoteTextStyle);
const footnoteTextCharStyle = new FootnoteTextChar();
styles.push(footnoteTextCharStyle);
return styles;
return {
initialStyles: documentAttributes,
importedStyles: [
new DocumentDefaults(),
new TitleStyle({
run: {
size: 56,
},
}),
new Heading1Style({
run: {
color: "2E74B5",
size: 32,
},
}),
new Heading2Style({
run: {
color: "2E74B5",
size: 26,
},
}),
new Heading3Style({
run: {
color: "1F4D78",
size: 24,
},
}),
new Heading4Style({
run: {
color: "2E74B5",
italics: true,
},
}),
new Heading5Style({
run: {
color: "2E74B5",
},
}),
new Heading6Style({
run: {
color: "1F4D78",
},
}),
new ListParagraph({}),
new HyperlinkStyle({}),
new FootnoteReferenceStyle({}),
new FootnoteText({}),
new FootnoteTextChar({}),
],
};
}
}

View File

@ -1,15 +1,16 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { UnderlineType } from "file/paragraph/run/underline";
import { ShadingType } from "file/table";
import { EMPTY_OBJECT } from "file/xml-components";
import { CharacterStyle } from "./character-style";
import { EMPTY_OBJECT } from "file/xml-components";
describe("CharacterStyle", () => {
describe("#constructor", () => {
it("should set the style type to character and use the given style id", () => {
const style = new CharacterStyle("myStyleId");
const style = new CharacterStyle({ id: "myStyleId" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -17,7 +18,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -29,7 +30,10 @@ describe("CharacterStyle", () => {
});
it("should set the name of the style, if given", () => {
const style = new CharacterStyle("myStyleId", "Style Name");
const style = new CharacterStyle({
id: "myStyleId",
name: "Style Name",
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -38,7 +42,222 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add smallCaps", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
smallCaps: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [{ "w:smallCaps": { _attr: { "w:val": true } } }],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add allCaps", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
allCaps: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [{ "w:caps": { _attr: { "w:val": true } } }],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add strike", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
strike: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [{ "w:strike": { _attr: { "w:val": true } } }],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add double strike", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
doubleStrike: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [{ "w:dstrike": { _attr: { "w:val": true } } }],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add sub script", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
subScript: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [
{
"w:vertAlign": {
_attr: {
"w:val": "subscript",
},
},
},
],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add font", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
font: "test font",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [
{
"w:rFonts": {
_attr: {
"w:ascii": "test font",
"w:cs": "test font",
"w:eastAsia": "test font",
"w:hAnsi": "test font",
},
},
},
],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
},
],
});
});
it("should add character spacing", () => {
const style = new CharacterStyle({
id: "myStyleId",
run: {
characterSpacing: 100,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "myStyleId" } },
{
"w:rPr": [{ "w:spacing": { _attr: { "w:val": 100 } } }],
},
{
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
@ -52,7 +271,7 @@ describe("CharacterStyle", () => {
describe("formatting methods: style attributes", () => {
it("#basedOn", () => {
const style = new CharacterStyle("myStyleId").basedOn("otherId");
const style = new CharacterStyle({ id: "myStyleId", basedOn: "otherId" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -60,7 +279,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -75,7 +294,12 @@ describe("CharacterStyle", () => {
describe("formatting methods: run properties", () => {
it("#size", () => {
const style = new CharacterStyle("myStyleId").size(24);
const style = new CharacterStyle({
id: "myStyleId",
run: {
size: 24,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -86,7 +310,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -99,7 +323,12 @@ describe("CharacterStyle", () => {
describe("#underline", () => {
it("should set underline to 'single' if no arguments are given", () => {
const style = new CharacterStyle("myStyleId").underline();
const style = new CharacterStyle({
id: "myStyleId",
run: {
underline: {},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -110,7 +339,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -122,7 +351,14 @@ describe("CharacterStyle", () => {
});
it("should set the style if given", () => {
const style = new CharacterStyle("myStyleId").underline("double");
const style = new CharacterStyle({
id: "myStyleId",
run: {
underline: {
type: UnderlineType.DOUBLE,
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -133,7 +369,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -145,7 +381,15 @@ describe("CharacterStyle", () => {
});
it("should set the style and color if given", () => {
const style = new CharacterStyle("myStyleId").underline("double", "005599");
const style = new CharacterStyle({
id: "myStyleId",
run: {
underline: {
type: UnderlineType.DOUBLE,
color: "005599",
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -156,7 +400,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -169,7 +413,12 @@ describe("CharacterStyle", () => {
});
it("#superScript", () => {
const style = new CharacterStyle("myStyleId").superScript();
const style = new CharacterStyle({
id: "myStyleId",
run: {
superScript: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -188,7 +437,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -200,7 +449,12 @@ describe("CharacterStyle", () => {
});
it("#color", () => {
const style = new CharacterStyle("myStyleId").color("123456");
const style = new CharacterStyle({
id: "myStyleId",
run: {
color: "123456",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -211,7 +465,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -223,7 +477,12 @@ describe("CharacterStyle", () => {
});
it("#bold", () => {
const style = new CharacterStyle("myStyleId").bold();
const style = new CharacterStyle({
id: "myStyleId",
run: {
bold: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -234,7 +493,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -246,7 +505,12 @@ describe("CharacterStyle", () => {
});
it("#italics", () => {
const style = new CharacterStyle("myStyleId").italics();
const style = new CharacterStyle({
id: "myStyleId",
run: {
italics: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -257,7 +521,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -269,7 +533,7 @@ describe("CharacterStyle", () => {
});
it("#link", () => {
const style = new CharacterStyle("myStyleId").link("MyLink");
const style = new CharacterStyle({ id: "myStyleId", link: "MyLink" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -277,7 +541,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -290,7 +554,7 @@ describe("CharacterStyle", () => {
});
it("#semiHidden", () => {
const style = new CharacterStyle("myStyleId").semiHidden();
const style = new CharacterStyle({ id: "myStyleId", semiHidden: true });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -298,7 +562,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -309,7 +573,12 @@ describe("CharacterStyle", () => {
});
it("#highlight", () => {
const style = new CharacterStyle("myStyleId").highlight("005599");
const style = new CharacterStyle({
id: "myStyleId",
run: {
highlight: "005599",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -320,7 +589,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -332,7 +601,16 @@ describe("CharacterStyle", () => {
});
it("#shadow", () => {
const style = new CharacterStyle("myStyleId").shadow("pct10", "00FFFF", "FF0000");
const style = new CharacterStyle({
id: "myStyleId",
run: {
shadow: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -343,7 +621,7 @@ describe("CharacterStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},

View File

@ -1,69 +1,128 @@
import * as formatting from "file/paragraph/run/formatting";
import { RunProperties } from "file/paragraph/run/properties";
import { XmlComponent } from "file/xml-components";
import { UnderlineType } from "file/paragraph/run/underline";
import { BasedOn, Link, SemiHidden, UiPriority, UnhideWhenUsed } from "./components";
import { Style } from "./style";
export interface IBaseCharacterStyleOptions {
readonly basedOn?: string;
readonly link?: string;
readonly semiHidden?: boolean;
readonly run?: {
readonly size?: number;
readonly bold?: boolean;
readonly italics?: boolean;
readonly smallCaps?: boolean;
readonly allCaps?: boolean;
readonly strike?: boolean;
readonly doubleStrike?: boolean;
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly underline?: {
readonly type?: UnderlineType;
readonly color?: string;
};
readonly color?: string;
readonly font?: string;
readonly characterSpacing?: number;
readonly highlight?: string;
readonly shadow?: {
readonly type: string;
readonly fill: string;
readonly color: string;
};
};
}
export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions {
readonly id: string;
readonly name?: string;
}
export class CharacterStyle extends Style {
private readonly runProperties: RunProperties;
constructor(styleId: string, name?: string) {
super({ type: "character", styleId: styleId }, name);
constructor(options: ICharacterStyleOptions) {
super({ type: "character", styleId: options.id }, options.name);
this.runProperties = new RunProperties();
this.root.push(this.runProperties);
this.root.push(new UiPriority("99"));
this.root.push(new UiPriority(99));
this.root.push(new UnhideWhenUsed());
}
public basedOn(parentId: string): CharacterStyle {
this.root.push(new BasedOn(parentId));
return this;
}
if (options.basedOn) {
this.root.push(new BasedOn(options.basedOn));
}
public addRunProperty(property: XmlComponent): CharacterStyle {
this.runProperties.push(property);
return this;
}
if (options.link) {
this.root.push(new Link(options.link));
}
public color(color: string): CharacterStyle {
return this.addRunProperty(new formatting.Color(color));
}
if (options.semiHidden) {
this.root.push(new SemiHidden());
}
public bold(): CharacterStyle {
return this.addRunProperty(new formatting.Bold());
}
if (options.run) {
if (options.run.size) {
this.runProperties.push(new formatting.Size(options.run.size));
this.runProperties.push(new formatting.SizeComplexScript(options.run.size));
}
public italics(): CharacterStyle {
return this.addRunProperty(new formatting.Italics());
}
if (options.run.bold) {
this.runProperties.push(new formatting.Bold());
}
public underline(underlineType?: string, color?: string): CharacterStyle {
return this.addRunProperty(new formatting.Underline(underlineType, color));
}
if (options.run.italics) {
this.runProperties.push(new formatting.Italics());
}
public superScript(): CharacterStyle {
return this.addRunProperty(new formatting.SuperScript());
}
if (options.run.smallCaps) {
this.runProperties.push(new formatting.SmallCaps());
}
public size(twips: number): CharacterStyle {
return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips));
}
if (options.run.allCaps) {
this.runProperties.push(new formatting.Caps());
}
public link(link: string): CharacterStyle {
this.root.push(new Link(link));
return this;
}
if (options.run.strike) {
this.runProperties.push(new formatting.Strike());
}
public semiHidden(): CharacterStyle {
this.root.push(new SemiHidden());
return this;
}
if (options.run.doubleStrike) {
this.runProperties.push(new formatting.DoubleStrike());
}
public highlight(color: string): CharacterStyle {
return this.addRunProperty(new formatting.Highlight(color));
}
if (options.run.subScript) {
this.runProperties.push(new formatting.SubScript());
}
public shadow(value: string, fill: string, color: string): CharacterStyle {
return this.addRunProperty(new formatting.Shading(value, fill, color));
if (options.run.superScript) {
this.runProperties.push(new formatting.SuperScript());
}
if (options.run.underline) {
this.runProperties.push(new formatting.Underline(options.run.underline.type, options.run.underline.color));
}
if (options.run.color) {
this.runProperties.push(new formatting.Color(options.run.color));
}
if (options.run.font) {
this.runProperties.push(new formatting.RunFonts(options.run.font));
}
if (options.run.characterSpacing) {
this.runProperties.push(new formatting.CharacterSpacing(options.run.characterSpacing));
}
if (options.run.highlight) {
this.runProperties.push(new formatting.Highlight(options.run.highlight));
}
if (options.run.shadow) {
this.runProperties.push(new formatting.Shading(options.run.shadow.type, options.run.shadow.fill, options.run.shadow.color));
}
}
}
}

View File

@ -30,9 +30,9 @@ describe("Style components", () => {
});
it("UiPriority#constructor", () => {
const style = new components.UiPriority("123");
const style = new components.UiPriority(123);
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({ "w:uiPriority": { _attr: { "w:val": "123" } } });
expect(tree).to.deep.equal({ "w:uiPriority": { _attr: { "w:val": 123 } } });
});
it("UnhideWhenUsed#constructor", () => {

View File

@ -1,7 +1,8 @@
// http://officeopenxml.com/WPstyleGenProps.php
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
interface IComponentAttributes {
readonly val: string;
readonly val: string | number;
}
class ComponentAttributes extends XmlAttributeComponent<IComponentAttributes> {
@ -37,7 +38,7 @@ export class Link extends XmlComponent {
}
export class UiPriority extends XmlComponent {
constructor(value: string) {
constructor(value: number) {
super("w:uiPriority");
// TODO: this value should be a ST_DecimalNumber
this.root.push(new ComponentAttributes({ val: value }));

View File

@ -1,12 +1,15 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import * as defaultStyels from "./default-styles";
import * as defaultStyles from "./default-styles";
import { EMPTY_OBJECT } from "file/xml-components";
describe("Default Styles", () => {
it("HeadingStyle#constructor", () => {
const style = new defaultStyels.HeadingStyle("Heading1", "Heading 1");
const style = new defaultStyles.HeadingStyle({
id: "Heading1",
name: "Heading 1",
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -20,7 +23,7 @@ describe("Default Styles", () => {
});
it("TitleStyle#constructor", () => {
const style = new defaultStyels.TitleStyle();
const style = new defaultStyles.TitleStyle({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -34,7 +37,7 @@ describe("Default Styles", () => {
});
it("Heading1Style#constructor", () => {
const style = new defaultStyels.Heading1Style();
const style = new defaultStyles.Heading1Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -48,7 +51,7 @@ describe("Default Styles", () => {
});
it("Heading2Style#constructor", () => {
const style = new defaultStyels.Heading2Style();
const style = new defaultStyles.Heading2Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -62,7 +65,7 @@ describe("Default Styles", () => {
});
it("Heading3Style#constructor", () => {
const style = new defaultStyels.Heading3Style();
const style = new defaultStyles.Heading3Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -76,7 +79,7 @@ describe("Default Styles", () => {
});
it("Heading4Style#constructor", () => {
const style = new defaultStyels.Heading4Style();
const style = new defaultStyles.Heading4Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -90,7 +93,7 @@ describe("Default Styles", () => {
});
it("Heading5Style#constructor", () => {
const style = new defaultStyels.Heading5Style();
const style = new defaultStyles.Heading5Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -104,7 +107,7 @@ describe("Default Styles", () => {
});
it("Heading6Style#constructor", () => {
const style = new defaultStyels.Heading6Style();
const style = new defaultStyles.Heading6Style({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -118,7 +121,7 @@ describe("Default Styles", () => {
});
it("ListParagraph#constructor", () => {
const style = new defaultStyels.ListParagraph();
const style = new defaultStyles.ListParagraph({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -131,7 +134,7 @@ describe("Default Styles", () => {
});
it("FootnoteText#constructor", () => {
const style = new defaultStyels.FootnoteText();
const style = new defaultStyles.FootnoteText({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -171,14 +174,14 @@ describe("Default Styles", () => {
{ "w:basedOn": { _attr: { "w:val": "Normal" } } },
{ "w:link": { _attr: { "w:val": "FootnoteTextChar" } } },
{
"w:uiPriority": {
_attr: {
"w:val": "99",
},
},
"w:semiHidden": EMPTY_OBJECT,
},
{
"w:semiHidden": EMPTY_OBJECT,
"w:uiPriority": {
_attr: {
"w:val": 99,
},
},
},
{
"w:unhideWhenUsed": EMPTY_OBJECT,
@ -188,7 +191,7 @@ describe("Default Styles", () => {
});
it("FootnoteReferenceStyle#constructor", () => {
const style = new defaultStyels.FootnoteReferenceStyle();
const style = new defaultStyles.FootnoteReferenceStyle({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -208,7 +211,7 @@ describe("Default Styles", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -225,7 +228,7 @@ describe("Default Styles", () => {
});
it("FootnoteTextChar#constructor", () => {
const style = new defaultStyels.FootnoteTextChar();
const style = new defaultStyles.FootnoteTextChar({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -252,7 +255,7 @@ describe("Default Styles", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -269,19 +272,28 @@ describe("Default Styles", () => {
});
it("HyperlinkStyle#constructor", () => {
const style = new defaultStyels.HyperlinkStyle();
const style = new defaultStyles.HyperlinkStyle({});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
{ _attr: { "w:type": "character", "w:styleId": "Hyperlink" } },
{ "w:name": { _attr: { "w:val": "Hyperlink" } } },
{
"w:rPr": [{ "w:color": { _attr: { "w:val": "0563C1" } } }, { "w:u": { _attr: { "w:val": "single" } } }],
"w:rPr": [
{ "w:u": { _attr: { "w:val": "single" } } },
{
"w:color": {
_attr: {
"w:val": "0563C1",
},
},
},
],
},
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},

View File

@ -1,106 +1,170 @@
import { CharacterStyle } from "./character-style";
import { ParagraphStyle } from "./paragraph-style";
import { UnderlineType } from "file/paragraph/run/underline";
import { CharacterStyle, IBaseCharacterStyleOptions } from "./character-style";
import { IBaseParagraphStyleOptions, IParagraphStyleOptions, ParagraphStyle } from "./paragraph-style";
export class HeadingStyle extends ParagraphStyle {
constructor(styleId: string, name: string) {
super(styleId, name);
this.basedOn("Normal");
this.next("Normal");
this.quickFormat();
constructor(options: IParagraphStyleOptions) {
super({
...options,
basedOn: "Normal",
next: "Normal",
quickFormat: true,
});
}
}
export class TitleStyle extends HeadingStyle {
constructor() {
super("Title", "Title");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Title",
name: "Title",
});
}
}
export class Heading1Style extends HeadingStyle {
constructor() {
super("Heading1", "Heading 1");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading1",
name: "Heading 1",
});
}
}
export class Heading2Style extends HeadingStyle {
constructor() {
super("Heading2", "Heading 2");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading2",
name: "Heading 2",
});
}
}
export class Heading3Style extends HeadingStyle {
constructor() {
super("Heading3", "Heading 3");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading3",
name: "Heading 3",
});
}
}
export class Heading4Style extends HeadingStyle {
constructor() {
super("Heading4", "Heading 4");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading4",
name: "Heading 4",
});
}
}
export class Heading5Style extends HeadingStyle {
constructor() {
super("Heading5", "Heading 5");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading5",
name: "Heading 5",
});
}
}
export class Heading6Style extends HeadingStyle {
constructor() {
super("Heading6", "Heading 6");
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "Heading6",
name: "Heading 6",
});
}
}
export class ListParagraph extends ParagraphStyle {
constructor() {
super("ListParagraph", "List Paragraph");
this.basedOn("Normal");
this.quickFormat();
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "ListParagraph",
name: "List Paragraph",
basedOn: "Normal",
quickFormat: true,
});
}
}
export class FootnoteText extends ParagraphStyle {
constructor() {
super("FootnoteText", "footnote text");
this.basedOn("Normal")
.link("FootnoteTextChar")
.uiPriority("99")
.semiHidden()
.unhideWhenUsed()
.spacing({
after: 0,
line: 240,
lineRule: "auto",
})
.size(20);
constructor(options: IBaseParagraphStyleOptions) {
super({
...options,
id: "FootnoteText",
name: "footnote text",
link: "FootnoteTextChar",
basedOn: "Normal",
uiPriority: 99,
semiHidden: true,
unhideWhenUsed: true,
paragraph: {
spacing: {
after: 0,
line: 240,
lineRule: "auto",
},
},
run: {
size: 20,
},
});
}
}
export class FootnoteReferenceStyle extends CharacterStyle {
constructor() {
super("FootnoteReference", "footnote reference");
this.basedOn("DefaultParagraphFont")
.semiHidden()
.superScript();
constructor(options: IBaseCharacterStyleOptions) {
super({
...options,
id: "FootnoteReference",
name: "footnote reference",
basedOn: "DefaultParagraphFont",
semiHidden: true,
run: {
superScript: true,
},
});
}
}
export class FootnoteTextChar extends CharacterStyle {
constructor() {
super("FootnoteTextChar", "Footnote Text Char");
this.basedOn("DefaultParagraphFont")
.link("FootnoteText")
.semiHidden()
.size(20);
constructor(options: IBaseCharacterStyleOptions) {
super({
...options,
id: "FootnoteTextChar",
name: "Footnote Text Char",
basedOn: "DefaultParagraphFont",
link: "FootnoteText",
semiHidden: true,
run: {
size: 20,
},
});
}
}
export class HyperlinkStyle extends CharacterStyle {
constructor() {
super("Hyperlink", "Hyperlink");
this.basedOn("DefaultParagraphFont")
.color("0563C1")
.underline("single");
constructor(options: IBaseCharacterStyleOptions) {
super({
...options,
id: "Hyperlink",
name: "Hyperlink",
basedOn: "DefaultParagraphFont",
run: {
color: "0563C1",
underline: {
type: UnderlineType.SINGLE,
},
},
});
}
}

View File

@ -1,15 +1,17 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { AlignmentType, TabStopPosition } from "file/paragraph";
import { UnderlineType } from "file/paragraph/run/underline";
import { ShadingType } from "file/table";
import { EMPTY_OBJECT } from "file/xml-components";
import { ParagraphStyle } from "./paragraph-style";
import { EMPTY_OBJECT } from "file/xml-components";
describe("ParagraphStyle", () => {
describe("#constructor", () => {
it("should set the style type to paragraph and use the given style id", () => {
const style = new ParagraphStyle("myStyleId");
const style = new ParagraphStyle({ id: "myStyleId" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": { _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } },
@ -17,7 +19,10 @@ describe("ParagraphStyle", () => {
});
it("should set the name of the style, if given", () => {
const style = new ParagraphStyle("myStyleId", "Style Name");
const style = new ParagraphStyle({
id: "myStyleId",
name: "Style Name",
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -30,7 +35,7 @@ describe("ParagraphStyle", () => {
describe("formatting methods: style attributes", () => {
it("#basedOn", () => {
const style = new ParagraphStyle("myStyleId").basedOn("otherId");
const style = new ParagraphStyle({ id: "myStyleId", basedOn: "otherId" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -41,7 +46,7 @@ describe("ParagraphStyle", () => {
});
it("#quickFormat", () => {
const style = new ParagraphStyle("myStyleId").quickFormat();
const style = new ParagraphStyle({ id: "myStyleId", quickFormat: true });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:qFormat": EMPTY_OBJECT }],
@ -49,7 +54,7 @@ describe("ParagraphStyle", () => {
});
it("#next", () => {
const style = new ParagraphStyle("myStyleId").next("otherId");
const style = new ParagraphStyle({ id: "myStyleId", next: "otherId" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -62,7 +67,12 @@ describe("ParagraphStyle", () => {
describe("formatting methods: paragraph properties", () => {
it("#indent", () => {
const style = new ParagraphStyle("myStyleId").indent({ left: 720 });
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
indent: { left: 720 },
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -75,7 +85,7 @@ describe("ParagraphStyle", () => {
});
it("#spacing", () => {
const style = new ParagraphStyle("myStyleId").spacing({ before: 50, after: 150 });
const style = new ParagraphStyle({ id: "myStyleId", paragraph: { spacing: { before: 50, after: 150 } } });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -88,7 +98,12 @@ describe("ParagraphStyle", () => {
});
it("#center", () => {
const style = new ParagraphStyle("myStyleId").center();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
alignment: AlignmentType.CENTER,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -101,7 +116,12 @@ describe("ParagraphStyle", () => {
});
it("#character spacing", () => {
const style = new ParagraphStyle("myStyleId").characterSpacing(24);
const style = new ParagraphStyle({
id: "myStyleId",
run: {
characterSpacing: 24,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -114,7 +134,12 @@ describe("ParagraphStyle", () => {
});
it("#left", () => {
const style = new ParagraphStyle("myStyleId").left();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
alignment: AlignmentType.LEFT,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -127,7 +152,12 @@ describe("ParagraphStyle", () => {
});
it("#right", () => {
const style = new ParagraphStyle("myStyleId").right();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
alignment: AlignmentType.RIGHT,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -140,7 +170,12 @@ describe("ParagraphStyle", () => {
});
it("#justified", () => {
const style = new ParagraphStyle("myStyleId").justified();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
alignment: AlignmentType.JUSTIFIED,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -153,7 +188,12 @@ describe("ParagraphStyle", () => {
});
it("#thematicBreak", () => {
const style = new ParagraphStyle("myStyleId").thematicBreak();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
thematicBreak: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -181,7 +221,12 @@ describe("ParagraphStyle", () => {
});
it("#leftTabStop", () => {
const style = new ParagraphStyle("myStyleId").leftTabStop(1200);
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
leftTabStop: 1200,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -198,7 +243,12 @@ describe("ParagraphStyle", () => {
});
it("#maxRightTabStop", () => {
const style = new ParagraphStyle("myStyleId").maxRightTabStop();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
rightTabStop: TabStopPosition.MAX,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -215,7 +265,12 @@ describe("ParagraphStyle", () => {
});
it("#keepLines", () => {
const style = new ParagraphStyle("myStyleId").keepLines();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
keepLines: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [{ "w:keepLines": EMPTY_OBJECT }] }],
@ -223,7 +278,12 @@ describe("ParagraphStyle", () => {
});
it("#keepNext", () => {
const style = new ParagraphStyle("myStyleId").keepNext();
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
keepNext: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:pPr": [{ "w:keepNext": EMPTY_OBJECT }] }],
@ -231,7 +291,12 @@ describe("ParagraphStyle", () => {
});
it("#outlineLevel", () => {
const style = new ParagraphStyle("myStyleId").outlineLevel(1);
const style = new ParagraphStyle({
id: "myStyleId",
paragraph: {
outlineLevel: 1,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -244,7 +309,12 @@ describe("ParagraphStyle", () => {
describe("formatting methods: run properties", () => {
it("#size", () => {
const style = new ParagraphStyle("myStyleId").size(24);
const style = new ParagraphStyle({
id: "myStyleId",
run: {
size: 24,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -257,7 +327,12 @@ describe("ParagraphStyle", () => {
});
it("#smallCaps", () => {
const style = new ParagraphStyle("myStyleId").smallCaps();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
smallCaps: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -270,7 +345,12 @@ describe("ParagraphStyle", () => {
});
it("#allCaps", () => {
const style = new ParagraphStyle("myStyleId").allCaps();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
allCaps: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -283,7 +363,12 @@ describe("ParagraphStyle", () => {
});
it("#strike", () => {
const style = new ParagraphStyle("myStyleId").strike();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
strike: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -296,7 +381,12 @@ describe("ParagraphStyle", () => {
});
it("#doubleStrike", () => {
const style = new ParagraphStyle("myStyleId").doubleStrike();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
doubleStrike: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -309,7 +399,12 @@ describe("ParagraphStyle", () => {
});
it("#subScript", () => {
const style = new ParagraphStyle("myStyleId").subScript();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
subScript: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -322,7 +417,12 @@ describe("ParagraphStyle", () => {
});
it("#superScript", () => {
const style = new ParagraphStyle("myStyleId").superScript();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
superScript: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -335,7 +435,12 @@ describe("ParagraphStyle", () => {
});
it("#font", () => {
const style = new ParagraphStyle("myStyleId").font("Times");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
font: "Times",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -350,7 +455,12 @@ describe("ParagraphStyle", () => {
});
it("#bold", () => {
const style = new ParagraphStyle("myStyleId").bold();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
bold: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -363,7 +473,12 @@ describe("ParagraphStyle", () => {
});
it("#italics", () => {
const style = new ParagraphStyle("myStyleId").italics();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
italics: true,
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -376,7 +491,12 @@ describe("ParagraphStyle", () => {
});
it("#highlight", () => {
const style = new ParagraphStyle("myStyleId").highlight("005599");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
highlight: "005599",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -389,7 +509,16 @@ describe("ParagraphStyle", () => {
});
it("#shadow", () => {
const style = new ParagraphStyle("myStyleId").shadow("pct10", "00FFFF", "FF0000");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
shadow: {
type: ShadingType.PERCENT_10,
fill: "00FFFF",
color: "FF0000",
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -403,7 +532,12 @@ describe("ParagraphStyle", () => {
describe("#underline", () => {
it("should set underline to 'single' if no arguments are given", () => {
const style = new ParagraphStyle("myStyleId").underline();
const style = new ParagraphStyle({
id: "myStyleId",
run: {
underline: {},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -416,7 +550,14 @@ describe("ParagraphStyle", () => {
});
it("should set the style if given", () => {
const style = new ParagraphStyle("myStyleId").underline("double");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
underline: {
type: UnderlineType.DOUBLE,
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -429,7 +570,15 @@ describe("ParagraphStyle", () => {
});
it("should set the style and color if given", () => {
const style = new ParagraphStyle("myStyleId").underline("double", "005599");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
underline: {
type: UnderlineType.DOUBLE,
color: "005599",
},
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -443,7 +592,12 @@ describe("ParagraphStyle", () => {
});
it("#color", () => {
const style = new ParagraphStyle("myStyleId").color("123456");
const style = new ParagraphStyle({
id: "myStyleId",
run: {
color: "123456",
},
});
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -456,7 +610,7 @@ describe("ParagraphStyle", () => {
});
it("#link", () => {
const style = new ParagraphStyle("myStyleId").link("MyLink");
const style = new ParagraphStyle({ id: "myStyleId", link: "MyLink" });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:link": { _attr: { "w:val": "MyLink" } } }],
@ -464,7 +618,7 @@ describe("ParagraphStyle", () => {
});
it("#semiHidden", () => {
const style = new ParagraphStyle("myStyleId").semiHidden();
const style = new ParagraphStyle({ id: "myStyleId", semiHidden: true });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:semiHidden": EMPTY_OBJECT }],
@ -472,7 +626,7 @@ describe("ParagraphStyle", () => {
});
it("#uiPriority", () => {
const style = new ParagraphStyle("myStyleId").uiPriority("99");
const style = new ParagraphStyle({ id: "myStyleId", uiPriority: 99 });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [
@ -480,7 +634,7 @@ describe("ParagraphStyle", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -489,7 +643,7 @@ describe("ParagraphStyle", () => {
});
it("#unhideWhenUsed", () => {
const style = new ParagraphStyle("myStyleId").unhideWhenUsed();
const style = new ParagraphStyle({ id: "myStyleId", unhideWhenUsed: true });
const tree = new Formatter().format(style);
expect(tree).to.deep.equal({
"w:style": [{ _attr: { "w:type": "paragraph", "w:styleId": "myStyleId" } }, { "w:unhideWhenUsed": EMPTY_OBJECT }],

View File

@ -5,188 +5,207 @@ import {
ISpacingProperties,
KeepLines,
KeepNext,
LeftTabStop,
MaxRightTabStop,
OutlineLevel,
ParagraphProperties,
Spacing,
ThematicBreak,
} from "file/paragraph";
import { IIndentAttributesProperties, TabStop, TabStopType } from "file/paragraph/formatting";
import * as formatting from "file/paragraph/run/formatting";
import { RunProperties } from "file/paragraph/run/properties";
import { XmlComponent } from "file/xml-components";
import { UnderlineType } from "file/paragraph/run/underline";
import { ShadingType } from "file/table";
import { BasedOn, Link, Next, QuickFormat, SemiHidden, UiPriority, UnhideWhenUsed } from "./components";
import { Style } from "./style";
export interface IBaseParagraphStyleOptions {
readonly basedOn?: string;
readonly next?: string;
readonly quickFormat?: boolean;
readonly link?: string;
readonly semiHidden?: boolean;
readonly uiPriority?: number;
readonly unhideWhenUsed?: boolean;
readonly run?: {
readonly size?: number;
readonly bold?: boolean;
readonly italics?: boolean;
readonly smallCaps?: boolean;
readonly allCaps?: boolean;
readonly strike?: boolean;
readonly doubleStrike?: boolean;
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly underline?: {
readonly type?: UnderlineType;
readonly color?: string;
};
readonly color?: string;
readonly font?: string;
readonly characterSpacing?: number;
readonly highlight?: string;
readonly shadow?: {
readonly type: ShadingType;
readonly fill: string;
readonly color: string;
};
};
readonly paragraph?: {
readonly alignment?: AlignmentType;
readonly thematicBreak?: boolean;
readonly rightTabStop?: number;
readonly leftTabStop?: number;
readonly indent?: IIndentAttributesProperties;
readonly spacing?: ISpacingProperties;
readonly keepNext?: boolean;
readonly keepLines?: boolean;
readonly outlineLevel?: number;
};
}
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
readonly id: string;
readonly name?: string;
}
export class ParagraphStyle extends Style {
private readonly paragraphProperties: ParagraphProperties;
private readonly runProperties: RunProperties;
constructor(styleId: string, name?: string) {
super({ type: "paragraph", styleId: styleId }, name);
constructor(options: IParagraphStyleOptions) {
super({ type: "paragraph", styleId: options.id }, options.name);
this.paragraphProperties = new ParagraphProperties({});
this.runProperties = new RunProperties();
this.root.push(this.paragraphProperties);
this.root.push(this.runProperties);
}
public addParagraphProperty(property: XmlComponent): ParagraphStyle {
this.paragraphProperties.push(property);
return this;
}
if (options.basedOn) {
this.root.push(new BasedOn(options.basedOn));
}
public outlineLevel(level: number): ParagraphStyle {
this.paragraphProperties.push(new OutlineLevel(level));
return this;
}
if (options.next) {
this.root.push(new Next(options.next));
}
public addRunProperty(property: XmlComponent): ParagraphStyle {
this.runProperties.push(property);
return this;
}
if (options.quickFormat) {
this.root.push(new QuickFormat());
}
public basedOn(parentId: string): ParagraphStyle {
this.root.push(new BasedOn(parentId));
return this;
}
if (options.link) {
this.root.push(new Link(options.link));
}
public quickFormat(): ParagraphStyle {
this.root.push(new QuickFormat());
return this;
}
if (options.semiHidden) {
this.root.push(new SemiHidden());
}
public next(nextId: string): ParagraphStyle {
this.root.push(new Next(nextId));
return this;
}
if (options.uiPriority) {
this.root.push(new UiPriority(options.uiPriority));
}
// ---------- Run formatting ---------------------- //
if (options.unhideWhenUsed) {
this.root.push(new UnhideWhenUsed());
}
public size(twips: number): ParagraphStyle {
return this.addRunProperty(new formatting.Size(twips)).addRunProperty(new formatting.SizeComplexScript(twips));
}
if (options.run) {
if (options.run.size) {
this.runProperties.push(new formatting.Size(options.run.size));
this.runProperties.push(new formatting.SizeComplexScript(options.run.size));
}
public bold(): ParagraphStyle {
return this.addRunProperty(new formatting.Bold());
}
if (options.run.bold) {
this.runProperties.push(new formatting.Bold());
}
public italics(): ParagraphStyle {
return this.addRunProperty(new formatting.Italics());
}
if (options.run.italics) {
this.runProperties.push(new formatting.Italics());
}
public smallCaps(): ParagraphStyle {
return this.addRunProperty(new formatting.SmallCaps());
}
if (options.run.smallCaps) {
this.runProperties.push(new formatting.SmallCaps());
}
public allCaps(): ParagraphStyle {
return this.addRunProperty(new formatting.Caps());
}
if (options.run.allCaps) {
this.runProperties.push(new formatting.Caps());
}
public strike(): ParagraphStyle {
return this.addRunProperty(new formatting.Strike());
}
if (options.run.strike) {
this.runProperties.push(new formatting.Strike());
}
public doubleStrike(): ParagraphStyle {
return this.addRunProperty(new formatting.DoubleStrike());
}
if (options.run.doubleStrike) {
this.runProperties.push(new formatting.DoubleStrike());
}
public subScript(): ParagraphStyle {
return this.addRunProperty(new formatting.SubScript());
}
if (options.run.subScript) {
this.runProperties.push(new formatting.SubScript());
}
public superScript(): ParagraphStyle {
return this.addRunProperty(new formatting.SuperScript());
}
if (options.run.superScript) {
this.runProperties.push(new formatting.SuperScript());
}
public underline(underlineType?: string, color?: string): ParagraphStyle {
return this.addRunProperty(new formatting.Underline(underlineType, color));
}
if (options.run.underline) {
this.runProperties.push(new formatting.Underline(options.run.underline.type, options.run.underline.color));
}
public color(color: string): ParagraphStyle {
return this.addRunProperty(new formatting.Color(color));
}
if (options.run.color) {
this.runProperties.push(new formatting.Color(options.run.color));
}
public font(fontName: string): ParagraphStyle {
return this.addRunProperty(new formatting.RunFonts(fontName));
}
if (options.run.font) {
this.runProperties.push(new formatting.RunFonts(options.run.font));
}
public characterSpacing(value: number): ParagraphStyle {
return this.addRunProperty(new formatting.CharacterSpacing(value));
}
if (options.run.characterSpacing) {
this.runProperties.push(new formatting.CharacterSpacing(options.run.characterSpacing));
}
public highlight(color: string): ParagraphStyle {
return this.addRunProperty(new formatting.Highlight(color));
}
if (options.run.highlight) {
this.runProperties.push(new formatting.Highlight(options.run.highlight));
}
public shadow(value: string, fill: string, color: string): ParagraphStyle {
return this.addRunProperty(new formatting.Shading(value, fill, color));
}
if (options.run.shadow) {
this.runProperties.push(new formatting.Shading(options.run.shadow.type, options.run.shadow.fill, options.run.shadow.color));
}
}
// --------------------- Paragraph formatting ------------------------ //
if (options.paragraph) {
if (options.paragraph.alignment) {
this.paragraphProperties.push(new Alignment(options.paragraph.alignment));
}
public center(): ParagraphStyle {
return this.addParagraphProperty(new Alignment(AlignmentType.CENTER));
}
if (options.paragraph.thematicBreak) {
this.paragraphProperties.push(new ThematicBreak());
}
public left(): ParagraphStyle {
return this.addParagraphProperty(new Alignment(AlignmentType.LEFT));
}
if (options.paragraph.rightTabStop) {
this.paragraphProperties.push(new TabStop(TabStopType.RIGHT, options.paragraph.rightTabStop));
}
public right(): ParagraphStyle {
return this.addParagraphProperty(new Alignment(AlignmentType.RIGHT));
}
if (options.paragraph.leftTabStop) {
this.paragraphProperties.push(new TabStop(TabStopType.LEFT, options.paragraph.leftTabStop));
}
public justified(): ParagraphStyle {
return this.addParagraphProperty(new Alignment(AlignmentType.BOTH));
}
if (options.paragraph.indent) {
this.paragraphProperties.push(new Indent(options.paragraph.indent));
}
public thematicBreak(): ParagraphStyle {
return this.addParagraphProperty(new ThematicBreak());
}
if (options.paragraph.spacing) {
this.paragraphProperties.push(new Spacing(options.paragraph.spacing));
}
public maxRightTabStop(): ParagraphStyle {
return this.addParagraphProperty(new MaxRightTabStop());
}
if (options.paragraph.keepNext) {
this.paragraphProperties.push(new KeepNext());
}
public leftTabStop(position: number): ParagraphStyle {
return this.addParagraphProperty(new LeftTabStop(position));
}
if (options.paragraph.keepLines) {
this.paragraphProperties.push(new KeepLines());
}
public indent(attrs: object): ParagraphStyle {
return this.addParagraphProperty(new Indent(attrs));
}
public spacing(params: ISpacingProperties): ParagraphStyle {
return this.addParagraphProperty(new Spacing(params));
}
public keepNext(): ParagraphStyle {
return this.addParagraphProperty(new KeepNext());
}
public keepLines(): ParagraphStyle {
return this.addParagraphProperty(new KeepLines());
}
/*-------------- Style Properties -----------------*/
public link(link: string): ParagraphStyle {
this.root.push(new Link(link));
return this;
}
public semiHidden(): ParagraphStyle {
this.root.push(new SemiHidden());
return this;
}
public uiPriority(priority: string): ParagraphStyle {
this.root.push(new UiPriority(priority));
return this;
}
public unhideWhenUsed(): ParagraphStyle {
this.root.push(new UnhideWhenUsed());
return this;
if (options.paragraph.outlineLevel) {
this.paragraphProperties.push(new OutlineLevel(options.paragraph.outlineLevel));
}
}
}
}

View File

@ -25,8 +25,4 @@ export class Style extends XmlComponent {
this.root.push(new Name(name));
}
}
public push(styleSegment: XmlComponent): void {
this.root.push(styleSegment);
}
}

View File

@ -1,31 +1,20 @@
import { assert, expect } from "chai";
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { CharacterStyle, ParagraphStyle } from "./style";
import { EMPTY_OBJECT } from "file/xml-components";
import { Styles } from "./styles";
import { EMPTY_OBJECT } from "file/xml-components";
describe("Styles", () => {
let styles: Styles;
beforeEach(() => {
styles = new Styles();
});
describe("#constructor()", () => {
it("should create styles with correct rootKey", () => {
const newJson = JSON.parse(JSON.stringify(styles));
assert.equal(newJson.rootKey, "w:styles");
});
});
describe("#createParagraphStyle", () => {
it("should create a new paragraph style and push it onto this collection", () => {
const pStyle = styles.createParagraphStyle("pStyleId");
expect(pStyle).to.instanceOf(ParagraphStyle);
const styles = new Styles({
paragraphStyles: [
{
id: "pStyleId",
},
],
});
const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr);
expect(tree).to.deep.equal([
{
@ -35,8 +24,14 @@ describe("Styles", () => {
});
it("should set the paragraph name if given", () => {
const pStyle = styles.createParagraphStyle("pStyleId", "Paragraph Style");
expect(pStyle).to.instanceOf(ParagraphStyle);
const styles = new Styles({
paragraphStyles: [
{
id: "pStyleId",
name: "Paragraph Style",
},
],
});
const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr);
expect(tree).to.deep.equal([
{
@ -51,8 +46,13 @@ describe("Styles", () => {
describe("#createCharacterStyle", () => {
it("should create a new character style and push it onto this collection", () => {
const cStyle = styles.createCharacterStyle("pStyleId");
expect(cStyle).to.instanceOf(CharacterStyle);
const styles = new Styles({
characterStyles: [
{
id: "pStyleId",
},
],
});
const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr);
expect(tree).to.deep.equal([
{
@ -61,7 +61,7 @@ describe("Styles", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},
@ -74,8 +74,14 @@ describe("Styles", () => {
});
it("should set the character name if given", () => {
const cStyle = styles.createCharacterStyle("pStyleId", "Character Style");
expect(cStyle).to.instanceOf(CharacterStyle);
const styles = new Styles({
characterStyles: [
{
id: "pStyleId",
name: "Character Style",
},
],
});
const tree = new Formatter().format(styles)["w:styles"].filter((x) => !x._attr);
expect(tree).to.deep.equal([
{
@ -85,7 +91,7 @@ describe("Styles", () => {
{
"w:uiPriority": {
_attr: {
"w:val": "99",
"w:val": 99,
},
},
},

View File

@ -1,36 +1,41 @@
import { BaseXmlComponent, XmlComponent } from "file/xml-components";
import { DocumentDefaults } from "./defaults";
import { BaseXmlComponent, ImportedXmlComponent, XmlComponent } from "file/xml-components";
import { CharacterStyle, ParagraphStyle } from "./style";
import { ICharacterStyleOptions } from "./style/character-style";
import { IParagraphStyleOptions } from "./style/paragraph-style";
export * from "./border";
export interface IStylesOptions {
readonly initialStyles?: BaseXmlComponent;
readonly paragraphStyles?: IParagraphStyleOptions[];
readonly characterStyles?: ICharacterStyleOptions[];
readonly importedStyles?: Array<XmlComponent | ParagraphStyle | CharacterStyle | ImportedXmlComponent>;
}
export class Styles extends XmlComponent {
constructor(initialStyles?: BaseXmlComponent) {
constructor(options: IStylesOptions) {
super("w:styles");
if (initialStyles) {
this.root.push(initialStyles);
if (options.initialStyles) {
this.root.push(options.initialStyles);
}
if (options.importedStyles) {
for (const style of options.importedStyles) {
this.root.push(style);
}
}
if (options.paragraphStyles) {
for (const style of options.paragraphStyles) {
this.root.push(new ParagraphStyle(style));
}
}
if (options.characterStyles) {
for (const style of options.characterStyles) {
this.root.push(new CharacterStyle(style));
}
}
}
public push(style: XmlComponent): Styles {
this.root.push(style);
return this;
}
public createDocumentDefaults(): DocumentDefaults {
const defaults = new DocumentDefaults();
this.push(defaults);
return defaults;
}
public createParagraphStyle(styleId: string, name?: string): ParagraphStyle {
const paragraphStyle = new ParagraphStyle(styleId, name);
this.push(paragraphStyle);
return paragraphStyle;
}
public createCharacterStyle(styleId: string, name?: string): CharacterStyle {
const characterStyle = new CharacterStyle(styleId, name);
this.push(characterStyle);
return characterStyle;
}
}

View File

@ -52,7 +52,7 @@ export class FieldInstruction extends XmlComponent {
instruction = `${instruction} \\s "${this.properties.seqFieldIdentifierForPrefix}"`;
}
if (this.properties.stylesWithLevels && this.properties.stylesWithLevels.length) {
const styles = this.properties.stylesWithLevels.map((sl) => `${sl.styleName};${sl.level}`).join(";");
const styles = this.properties.stylesWithLevels.map((sl) => `${sl.styleName},${sl.level}`).join(",");
instruction = `${instruction} \\t "${styles}"`;
}
if (this.properties.useAppliedParagraphOutlineLevel) {

View File

@ -146,7 +146,7 @@ const COMPLETE_TOC = {
"xml:space": "preserve",
},
},
'TOC \\a "A" \\b "B" \\c "C" \\d "D" \\f "F" \\h \\l "L" \\n "N" \\o "O" \\p "P" \\s "S" \\t "SL;1;SL;2" \\u \\w \\x \\z',
'TOC \\a "A" \\b "B" \\c "C" \\d "D" \\f "F" \\h \\l "L" \\n "N" \\o "O" \\p "P" \\s "S" \\t "SL,1,SL,2" \\u \\w \\x \\z',
],
},
{

View File

@ -16,18 +16,24 @@ export class TableOfContents extends XmlComponent {
const content = new StructuredDocumentTagContent();
const beginParagraph = new Paragraph({});
const beginRun = new Run({});
beginRun.addChildElement(new Begin(true));
beginRun.addChildElement(new FieldInstruction(properties));
beginRun.addChildElement(new Separate());
beginParagraph.addRun(beginRun);
const beginParagraph = new Paragraph({
children: [
new Run({
children: [new Begin(true), new FieldInstruction(properties), new Separate()],
}),
],
});
content.addChildElement(beginParagraph);
const endParagraph = new Paragraph({});
const endRun = new Run({});
endRun.addChildElement(new End());
endParagraph.addRun(endRun);
const endParagraph = new Paragraph({
children: [
new Run({
children: [new End()],
}),
],
});
content.addChildElement(endParagraph);
this.root.push(content);

View File

@ -2,9 +2,11 @@
import { XmlAttributeComponent, XmlComponent } from "file/xml-components";
export class TableGrid extends XmlComponent {
constructor(cols: number[]) {
constructor(widths: number[]) {
super("w:tblGrid");
cols.forEach((col) => this.root.push(new GridCol(col)));
for (const width of widths) {
this.root.push(new GridCol(width));
}
}
}

View File

@ -103,7 +103,7 @@ export class GridSpan extends XmlComponent {
/**
* Vertical merge types.
*/
export enum VMergeType {
export enum VerticalMergeType {
/**
* Cell that is merged with upper one.
*/
@ -114,19 +114,19 @@ export enum VMergeType {
RESTART = "restart",
}
class VMergeAttributes extends XmlAttributeComponent<{ readonly val: VMergeType }> {
class VerticalMergeAttributes extends XmlAttributeComponent<{ readonly val: VerticalMergeType }> {
protected readonly xmlKeys = { val: "w:val" };
}
/**
* Vertical merge element. Should be used in a table cell.
*/
export class VMerge extends XmlComponent {
constructor(value: VMergeType) {
export class VerticalMerge extends XmlComponent {
constructor(value: VerticalMergeType) {
super("w:vMerge");
this.root.push(
new VMergeAttributes({
new VerticalMergeAttributes({
val: value,
}),
);

View File

@ -3,7 +3,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { BorderStyle } from "file/styles";
import { VerticalAlign, VMergeType, WidthType } from "./table-cell-components";
import { VerticalAlign, VerticalMergeType, WidthType } from "./table-cell-components";
import { TableCellProperties } from "./table-cell-properties";
describe("TableCellProperties", () => {
@ -30,7 +30,7 @@ describe("TableCellProperties", () => {
describe("#addVerticalMerge", () => {
it("adds vertical merge", () => {
const properties = new TableCellProperties();
properties.addVerticalMerge(VMergeType.CONTINUE);
properties.addVerticalMerge(VerticalMergeType.CONTINUE);
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "continue" } } }] });
});
@ -73,6 +73,54 @@ describe("TableCellProperties", () => {
});
});
describe("#addMargins", () => {
it("sets shading", () => {
const properties = new TableCellProperties();
properties.addMargins({});
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
"w:tcPr": [
{
"w:tcMar": [
{
"w:top": {
_attr: {
"w:type": "dxa",
"w:w": 0,
},
},
},
{
"w:bottom": {
_attr: {
"w:type": "dxa",
"w:w": 0,
},
},
},
{
"w:end": {
_attr: {
"w:type": "dxa",
"w:w": 0,
},
},
},
{
"w:start": {
_attr: {
"w:type": "dxa",
"w:w": 0,
},
},
},
],
},
],
});
});
});
describe("#Borders", () => {
it("should return the TableCellBorders if Border has borders", () => {
const properties = new TableCellProperties();

View File

@ -2,7 +2,16 @@ import { IgnoreIfEmptyXmlComponent } from "file/xml-components";
import { ITableShadingAttributesProperties, TableShading } from "../shading";
import { ITableCellMarginOptions, TableCellMargin } from "./cell-margin/table-cell-margins";
import { GridSpan, TableCellBorders, TableCellWidth, VAlign, VerticalAlign, VMerge, VMergeType, WidthType } from "./table-cell-components";
import {
GridSpan,
TableCellBorders,
TableCellWidth,
VAlign,
VerticalAlign,
VerticalMerge,
VerticalMergeType,
WidthType,
} from "./table-cell-components";
export class TableCellProperties extends IgnoreIfEmptyXmlComponent {
private readonly cellBorder: TableCellBorders;
@ -23,8 +32,8 @@ export class TableCellProperties extends IgnoreIfEmptyXmlComponent {
return this;
}
public addVerticalMerge(type: VMergeType): TableCellProperties {
this.root.push(new VMerge(type));
public addVerticalMerge(type: VerticalMergeType): TableCellProperties {
this.root.push(new VerticalMerge(type));
return this;
}

View File

@ -3,7 +3,9 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { BorderStyle } from "file/styles";
import { TableCellBorders, TableCellWidth, WidthType } from "./table-cell-components";
import { ShadingType } from "../shading";
import { TableCell } from "./table-cell";
import { TableCellBorders, TableCellWidth, VerticalAlign, VerticalMergeType, WidthType } from "./table-cell-components";
describe("TableCellBorders", () => {
describe("#prepForXml", () => {
@ -222,3 +224,359 @@ describe("TableCellWidth", () => {
});
});
});
describe("TableCell", () => {
describe("#constructor", () => {
it("should create", () => {
const cell = new TableCell({
children: [],
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:p": {},
},
],
});
});
it("should create with vertical align", () => {
const cell = new TableCell({
children: [],
verticalAlign: VerticalAlign.CENTER,
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:vAlign": {
_attr: {
"w:val": "center",
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with vertical merge", () => {
const cell = new TableCell({
children: [],
verticalMerge: VerticalMergeType.RESTART,
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:vMerge": {
_attr: {
"w:val": "restart",
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with margins", () => {
const cell = new TableCell({
children: [],
margins: {
top: 1,
left: 1,
bottom: 1,
right: 1,
},
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:tcMar": [
{
"w:top": {
_attr: {
"w:type": "dxa",
"w:w": 1,
},
},
},
{
"w:bottom": {
_attr: {
"w:type": "dxa",
"w:w": 1,
},
},
},
{
"w:end": {
_attr: {
"w:type": "dxa",
"w:w": 1,
},
},
},
{
"w:start": {
_attr: {
"w:type": "dxa",
"w:w": 1,
},
},
},
],
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with shading", () => {
const cell = new TableCell({
children: [],
shading: {
fill: "red",
color: "blue",
val: ShadingType.PERCENT_10,
},
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:shd": {
_attr: {
"w:color": "blue",
"w:fill": "red",
"w:val": "pct10",
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with width", () => {
const cell = new TableCell({
children: [],
width: { size: 100, type: WidthType.DXA },
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:tcW": {
_attr: {
"w:type": "dxa",
"w:w": 100,
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with column span", () => {
const cell = new TableCell({
children: [],
columnSpan: 2,
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:gridSpan": {
_attr: {
"w:val": 2,
},
},
},
],
},
{
"w:p": {},
},
],
});
});
describe("rowSpan", () => {
it("should not create with row span if its less than 1", () => {
const cell = new TableCell({
children: [],
rowSpan: 0,
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:p": {},
},
],
});
});
it("should create with row span if its greater than 1", () => {
const cell = new TableCell({
children: [],
rowSpan: 2,
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:vMerge": {
_attr: {
"w:val": "restart",
},
},
},
],
},
{
"w:p": {},
},
],
});
});
it("should create with borders", () => {
const cell = new TableCell({
children: [],
borders: {
top: {
style: BorderStyle.DASH_DOT_STROKED,
size: 3,
color: "red",
},
bottom: {
style: BorderStyle.DOUBLE,
size: 3,
color: "blue",
},
left: {
style: BorderStyle.DASH_DOT_STROKED,
size: 3,
color: "green",
},
right: {
style: BorderStyle.DASH_DOT_STROKED,
size: 3,
color: "#ff8000",
},
},
});
const tree = new Formatter().format(cell);
expect(tree).to.deep.equal({
"w:tc": [
{
"w:tcPr": [
{
"w:tcBorders": [
{
"w:top": {
_attr: {
"w:color": "red",
"w:sz": 3,
"w:val": "dashDotStroked",
},
},
},
{
"w:bottom": {
_attr: {
"w:color": "blue",
"w:sz": 3,
"w:val": "double",
},
},
},
{
"w:left": {
_attr: {
"w:color": "green",
"w:sz": 3,
"w:val": "dashDotStroked",
},
},
},
{
"w:right": {
_attr: {
"w:color": "#ff8000",
"w:sz": 3,
"w:val": "dashDotStroked",
},
},
},
],
},
],
},
{
"w:p": {},
},
],
});
});
});
});
});

View File

@ -1,77 +1,120 @@
// http://officeopenxml.com/WPtableGrid.php
import { Paragraph } from "file/paragraph";
import { BorderStyle } from "file/styles";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { ITableShadingAttributesProperties } from "../shading";
import { Table } from "../table";
import { ITableCellMarginOptions } from "./cell-margin/table-cell-margins";
import { TableCellBorders, VerticalAlign, VMergeType } from "./table-cell-components";
import { VerticalAlign, VerticalMergeType, WidthType } from "./table-cell-components";
import { TableCellProperties } from "./table-cell-properties";
export interface ITableCellOptions {
readonly shading?: ITableShadingAttributesProperties;
readonly margins?: ITableCellMarginOptions;
readonly verticalAlign?: VerticalAlign;
readonly verticalMerge?: VerticalMergeType;
readonly width?: {
readonly size: number | string;
readonly type?: WidthType;
};
readonly columnSpan?: number;
readonly rowSpan?: number;
readonly borders?: {
readonly top?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly bottom?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly left?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
readonly right?: {
readonly style: BorderStyle;
readonly size: number;
readonly color: string;
};
};
readonly children: Array<Paragraph | Table>;
}
export class TableCell extends XmlComponent {
private readonly properties: TableCellProperties;
constructor() {
constructor(readonly options: ITableCellOptions) {
super("w:tc");
this.properties = new TableCellProperties();
this.root.push(this.properties);
}
public add(item: Paragraph | Table): TableCell {
this.root.push(item);
for (const child of options.children) {
this.root.push(child);
}
return this;
if (options.verticalAlign) {
this.properties.setVerticalAlign(options.verticalAlign);
}
if (options.verticalMerge) {
this.properties.addVerticalMerge(options.verticalMerge);
}
if (options.margins) {
this.properties.addMargins(options.margins);
}
if (options.shading) {
this.properties.setShading(options.shading);
}
if (options.columnSpan) {
this.properties.addGridSpan(options.columnSpan);
}
if (options.rowSpan && options.rowSpan > 1) {
this.properties.addVerticalMerge(VerticalMergeType.RESTART);
}
if (options.width) {
this.properties.setWidth(options.width.size, options.width.type);
}
if (options.borders) {
if (options.borders.top) {
this.properties.Borders.addTopBorder(options.borders.top.style, options.borders.top.size, options.borders.top.color);
}
if (options.borders.bottom) {
this.properties.Borders.addBottomBorder(
options.borders.bottom.style,
options.borders.bottom.size,
options.borders.bottom.color,
);
}
if (options.borders.left) {
this.properties.Borders.addLeftBorder(options.borders.left.style, options.borders.left.size, options.borders.left.color);
}
if (options.borders.right) {
this.properties.Borders.addRightBorder(
options.borders.right.style,
options.borders.right.size,
options.borders.right.color,
);
}
}
}
public prepForXml(): IXmlableObject | undefined {
// Cells must end with a paragraph
if (!(this.root[this.root.length - 1] instanceof Paragraph)) {
const para = new Paragraph({});
this.add(para);
this.root.push(new Paragraph({}));
}
return super.prepForXml();
}
public setVerticalAlign(type: VerticalAlign): TableCell {
this.properties.setVerticalAlign(type);
return this;
}
public addGridSpan(cellSpan: number): TableCell {
this.properties.addGridSpan(cellSpan);
return this;
}
public addVerticalMerge(type: VMergeType): TableCell {
this.properties.addVerticalMerge(type);
return this;
}
public setMargins(margins: ITableCellMarginOptions): TableCell {
this.properties.addMargins(margins);
return this;
}
public setShading(attrs: ITableShadingAttributesProperties): TableCell {
this.properties.setShading(attrs);
return this;
}
public get Borders(): TableCellBorders {
return this.properties.Borders;
}
public get Properties(): TableCellProperties {
return this.properties;
}
}

View File

@ -1,56 +0,0 @@
import { expect } from "chai";
import { Formatter } from "export/formatter";
import { TableCell } from "./table-cell";
import { TableColumn } from "./table-column";
import { EMPTY_OBJECT } from "file/xml-components";
describe("TableColumn", () => {
let cells: TableCell[];
beforeEach(() => {
cells = [new TableCell(), new TableCell(), new TableCell()];
});
describe("#getCell", () => {
it("should get the correct cell", () => {
const tableColumn = new TableColumn(cells);
const cell = tableColumn.getCell(0);
expect(cell).to.deep.equal(cells[0]);
const cell2 = tableColumn.getCell(1);
expect(cell2).to.deep.equal(cells[1]);
});
it("should throw an error if index is out of bounds", () => {
const tableColumn = new TableColumn(cells);
expect(() => tableColumn.getCell(9)).to.throw();
});
});
describe("#mergeCells", () => {
it("should add vMerge to correct cells", () => {
const tableColumn = new TableColumn(cells);
tableColumn.mergeCells(0, 2);
const tree = new Formatter().format(cells[0]);
expect(tree).to.deep.equal({
"w:tc": [{ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "restart" } } }] }, { "w:p": EMPTY_OBJECT }],
});
const tree2 = new Formatter().format(cells[1]);
expect(tree2).to.deep.equal({
"w:tc": [{ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "continue" } } }] }, { "w:p": EMPTY_OBJECT }],
});
const tree3 = new Formatter().format(cells[2]);
expect(tree3).to.deep.equal({
"w:tc": [{ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "continue" } } }] }, { "w:p": EMPTY_OBJECT }],
});
});
});
});

View File

@ -1,25 +0,0 @@
import { TableCell, VMergeType } from "./table-cell";
export class TableColumn {
constructor(private readonly cells: TableCell[]) {}
public getCell(index: number): TableCell {
const cell = this.cells[index];
if (!cell) {
throw Error("Index out of bounds when trying to get cell on column");
}
return cell;
}
public mergeCells(startIndex: number, endIndex: number): TableCell {
this.cells[startIndex].addVerticalMerge(VMergeType.RESTART);
for (let i = startIndex + 1; i <= endIndex; i++) {
this.cells[i].addVerticalMerge(VMergeType.CONTINUE);
}
return this.cells[startIndex];
}
}

View File

@ -14,38 +14,66 @@ describe("TableCellMargin", () => {
});
describe("#addTopMargin", () => {
it("adds a table cell top margin", () => {
it("should add a table cell top margin", () => {
const cellMargin = new TableCellMargin();
cellMargin.addTopMargin(1234, WidthType.DXA);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:top": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
it("should add a table cell top margin using default width type", () => {
const cellMargin = new TableCellMargin();
cellMargin.addTopMargin(1234);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:top": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
});
describe("#addLeftMargin", () => {
it("adds a table cell left margin", () => {
it("should add a table cell left margin", () => {
const cellMargin = new TableCellMargin();
cellMargin.addLeftMargin(1234, WidthType.DXA);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:left": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
it("should add a table cell left margin using default width type", () => {
const cellMargin = new TableCellMargin();
cellMargin.addLeftMargin(1234);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:left": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
});
describe("#addBottomMargin", () => {
it("adds a table cell bottom margin", () => {
it("should add a table cell bottom margin", () => {
const cellMargin = new TableCellMargin();
cellMargin.addBottomMargin(1234, WidthType.DXA);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:bottom": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
it("should add a table cell bottom margin using default width type", () => {
const cellMargin = new TableCellMargin();
cellMargin.addBottomMargin(1234);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:bottom": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
});
describe("#addRightMargin", () => {
it("adds a table cell right margin", () => {
it("should add a table cell right margin", () => {
const cellMargin = new TableCellMargin();
cellMargin.addRightMargin(1234, WidthType.DXA);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:right": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
it("should add a table cell right margin using default width type", () => {
const cellMargin = new TableCellMargin();
cellMargin.addRightMargin(1234);
const tree = new Formatter().format(cellMargin);
expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:right": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] });
});
});
});

View File

@ -2,6 +2,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { ShadingType } from "../shading";
import { WidthType } from "../table-cell";
import { TableLayoutType } from "./table-layout";
import { TableProperties } from "./table-properties";
@ -66,4 +67,29 @@ describe("TableProperties", () => {
});
});
});
describe("#setShading", () => {
it("sets the shading of the table", () => {
const tp = new TableProperties();
tp.setShading({
fill: "b79c2f",
val: ShadingType.REVERSE_DIAGONAL_STRIPE,
color: "auto",
});
const tree = new Formatter().format(tp);
expect(tree).to.deep.equal({
"w:tblPr": [
{
"w:shd": {
_attr: {
"w:color": "auto",
"w:fill": "b79c2f",
"w:val": "reverseDiagStripe",
},
},
},
],
});
});
});
});

View File

@ -2,6 +2,7 @@ import { expect } from "chai";
import { Formatter } from "export/formatter";
import { Paragraph } from "file/paragraph";
import { HeightRule } from "file/table/table-row/table-row-height";
import { EMPTY_OBJECT } from "file/xml-components";
import { TableCell } from "../table-cell";
@ -10,7 +11,9 @@ import { TableRow } from "./table-row";
describe("TableRow", () => {
describe("#constructor", () => {
it("should create with no cells", () => {
const tableRow = new TableRow([]);
const tableRow = new TableRow({
children: [],
});
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": EMPTY_OBJECT,
@ -18,7 +21,13 @@ describe("TableRow", () => {
});
it("should create with one cell", () => {
const tableRow = new TableRow([new TableCell()]);
const tableRow = new TableRow({
children: [
new TableCell({
children: [],
}),
],
});
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": [
@ -32,46 +41,61 @@ describe("TableRow", () => {
],
});
});
});
describe("#getCell", () => {
it("should get the cell", () => {
const cell = new TableCell();
const tableRow = new TableRow([cell]);
expect(tableRow.getCell(0)).to.equal(cell);
it("should create with cant split", () => {
const tableRow = new TableRow({
children: [],
cantSplit: true,
});
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": [
{
"w:trPr": [
{
"w:cantSplit": {
_attr: {
"w:val": true,
},
},
},
],
},
],
});
});
it("should throw an error if index is out of bounds", () => {
const cell = new TableCell();
const tableRow = new TableRow([cell]);
expect(() => tableRow.getCell(1)).to.throw();
it("should create with table header", () => {
const tableRow = new TableRow({
children: [],
tableHeader: true,
});
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": [
{
"w:trPr": [
{
"w:tblHeader": {
_attr: {
"w:val": true,
},
},
},
],
},
],
});
});
});
describe("#addGridSpan", () => {
it("should merge the cell", () => {
const tableRow = new TableRow([new TableCell(), new TableCell()]);
tableRow.addGridSpan(0, 2);
expect(() => tableRow.getCell(1)).to.throw();
});
});
describe("#mergeCells", () => {
it("should merge the cell", () => {
const tableRow = new TableRow([new TableCell(), new TableCell()]);
tableRow.mergeCells(0, 1);
expect(() => tableRow.getCell(1)).to.throw();
});
});
describe("#setHeight", () => {
it("should set row height", () => {
const tableRow = new TableRow([]);
tableRow.setHeight(100, HeightRule.EXACT);
const tableRow = new TableRow({
children: [],
height: {
height: 100,
rule: HeightRule.EXACT,
},
});
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": [
@ -91,4 +115,71 @@ describe("TableRow", () => {
});
});
});
describe("#addCellToIndex", () => {
it("should add cell to correct index with no initial properties", () => {
const tableRow = new TableRow({
children: [
new TableCell({
children: [new Paragraph("test")],
}),
],
tableHeader: true,
});
tableRow.addCellToIndex(
new TableCell({
children: [],
}),
0,
);
const tree = new Formatter().format(tableRow);
expect(tree).to.deep.equal({
"w:tr": [
{
"w:trPr": [
{
"w:tblHeader": {
_attr: {
"w:val": true,
},
},
},
],
},
{
"w:tc": [
{
"w:p": {},
},
],
},
{
"w:tc": [
{
"w:p": [
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"test",
],
},
],
},
],
},
],
},
],
});
});
});
});

View File

@ -3,56 +3,51 @@ import { XmlComponent } from "file/xml-components";
import { TableCell } from "../table-cell";
import { TableRowProperties } from "./table-row-properties";
export interface ITableRowOptions {
readonly cantSplit?: boolean;
readonly tableHeader?: boolean;
readonly height?: {
readonly height: number;
readonly rule: HeightRule;
};
readonly children: TableCell[];
}
export class TableRow extends XmlComponent {
private readonly properties: TableRowProperties;
constructor(private readonly cells: TableCell[]) {
constructor(private readonly options: ITableRowOptions) {
super("w:tr");
this.properties = new TableRowProperties();
this.root.push(this.properties);
cells.forEach((c) => this.root.push(c));
}
public getCell(index: number): TableCell {
const cell = this.cells[index];
if (!cell) {
throw Error("Index out of bounds when trying to get cell on row");
for (const child of options.children) {
this.root.push(child);
}
return cell;
if (options.cantSplit) {
this.properties.setCantSplit();
}
if (options.tableHeader) {
this.properties.setTableHeader();
}
if (options.height) {
this.properties.setHeight(options.height.height, options.height.rule);
}
}
public addGridSpan(index: number, cellSpan: number): TableCell {
const remainCell = this.cells[index];
remainCell.addGridSpan(cellSpan);
this.cells.splice(index + 1, cellSpan - 1);
this.root.splice(index + 2, cellSpan - 1);
return remainCell;
public get CellCount(): number {
return this.options.children.length;
}
public mergeCells(startIndex: number, endIndex: number): TableCell {
const cellSpan = endIndex - startIndex + 1;
return this.addGridSpan(startIndex, cellSpan);
public get Children(): TableCell[] {
return this.options.children;
}
public setCantSplit(): TableRow {
this.properties.setCantSplit();
return this;
}
public setTableHeader(): TableRow {
this.properties.setTableHeader();
return this;
}
public setHeight(height: number, rule: HeightRule): TableRow {
this.properties.setHeight(height, rule);
return this;
public addCellToIndex(cell: TableCell, index: number): void {
// Offset because properties is also in root.
this.root.splice(index + 1, 0, cell);
}
}

View File

@ -8,8 +8,9 @@ import { Table } from "./table";
// import { WidthType } from "./table-cell";
import { RelativeHorizontalPosition, RelativeVerticalPosition, TableAnchorType } from "./table-properties";
import { EMPTY_OBJECT } from "file/xml-components";
import { TableCell, WidthType } from "./table-cell";
import { TableLayoutType } from "./table-properties/table-layout";
import { TableRow } from "./table-row";
const DEFAULT_TABLE_PROPERTIES = {
"w:tblCellMar": [
@ -118,11 +119,62 @@ describe("Table", () => {
describe("#constructor", () => {
it("creates a table with the correct number of rows and columns", () => {
const table = new Table({
rows: 3,
columns: 2,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
});
const tree = new Formatter().format(table);
const cell = { "w:tc": [{ "w:p": EMPTY_OBJECT }] };
const cell = {
"w:tc": [
{
"w:p": [
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"hello",
],
},
],
},
],
},
],
};
expect(tree).to.deep.equal({
"w:tbl": [
{ "w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS] },
@ -138,8 +190,15 @@ describe("Table", () => {
it("sets the table to fixed width layout", () => {
const table = new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
layout: TableLayoutType.FIXED,
});
const tree = new Formatter().format(table);
@ -151,130 +210,60 @@ describe("Table", () => {
"w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS, { "w:tblLayout": { _attr: { "w:type": "fixed" } } }],
});
});
});
describe("#getRow and Row#getCell", () => {
const table = new Table({
rows: 2,
columns: 2,
});
it("should return the correct row", () => {
table
.getRow(0)
.getCell(0)
.add(new Paragraph("A1"));
table
.getRow(0)
.getCell(1)
.add(new Paragraph("B1"));
table
.getRow(1)
.getCell(0)
.add(new Paragraph("A2"));
table
.getRow(1)
.getCell(1)
.add(new Paragraph("B2"));
const tree = new Formatter().format(table);
const cell = (c) => ({
"w:tc": [
{
"w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, c] }] }],
},
],
});
expect(tree).to.deep.equal({
"w:tbl": [
{ "w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS] },
{
"w:tblGrid": [{ "w:gridCol": { _attr: { "w:w": 100 } } }, { "w:gridCol": { _attr: { "w:w": 100 } } }],
},
{ "w:tr": [cell("A1"), cell("B1")] },
{ "w:tr": [cell("A2"), cell("B2")] },
],
});
});
it("throws an exception if index is out of bounds", () => {
expect(() => table.getCell(9, 9)).to.throw();
});
});
describe("#getColumn", () => {
const table = new Table({
rows: 2,
columns: 2,
});
it("should get correct cell", () => {
const column = table.getColumn(0);
expect(column.getCell(0)).to.equal(table.getCell(0, 0));
expect(column.getCell(1)).to.equal(table.getCell(1, 0));
});
});
describe("#getCell", () => {
it("should returns the correct cell", () => {
it("should set the table to provided width", () => {
const table = new Table({
rows: 2,
columns: 2,
});
table.getCell(0, 0).add(new Paragraph("A1"));
table.getCell(0, 1).add(new Paragraph("B1"));
table.getCell(1, 0).add(new Paragraph("A2"));
table.getCell(1, 1).add(new Paragraph("B2"));
const tree = new Formatter().format(table);
const cell = (c) => ({
"w:tc": [
{
"w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, c] }] }],
},
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
layout: TableLayoutType.FIXED,
});
expect(tree).to.deep.equal({
"w:tbl": [
{ "w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS] },
const tree = new Formatter().format(table);
expect(tree)
.to.have.property("w:tbl")
.which.is.an("array")
.with.has.length.at.least(1);
expect(tree["w:tbl"][0]).to.deep.equal({
"w:tblPr": [
DEFAULT_TABLE_PROPERTIES,
BORDERS,
{
"w:tblGrid": [{ "w:gridCol": { _attr: { "w:w": 100 } } }, { "w:gridCol": { _attr: { "w:w": 100 } } }],
"w:tblW": {
_attr: {
"w:type": "pct",
"w:w": "100%",
},
},
},
{ "w:tr": [cell("A1"), cell("B1")] },
{ "w:tr": [cell("A2"), cell("B2")] },
{ "w:tblLayout": { _attr: { "w:type": "fixed" } } },
],
});
});
});
// describe("#setWidth", () => {
// it("should set the preferred width on the table", () => {
// const table = new Table({rows: 1,columns: 1,}).setWidth(1000, WidthType.PERCENTAGE);
// const tree = new Formatter().format(table);
// expect(tree)
// .to.have.property("w:tbl")
// .which.is.an("array")
// .with.has.length.at.least(1);
// expect(tree["w:tbl"][0]).to.deep.equal({
// "w:tblPr": [DEFAULT_TABLE_PROPERTIES, { "w:tblW": { _attr: { "w:type": "pct", "w:w": "1000%" } } }],
// });
// });
// it("sets the preferred width on the table with a default of AUTO", () => {
// const table = new Table({rows: 1,columns: 1,}).setWidth(1000);
// const tree = new Formatter().format(table);
// expect(tree["w:tbl"][0]).to.deep.equal({
// "w:tblPr": [DEFAULT_TABLE_PROPERTIES, { "w:tblW": { _attr: { "w:type": "auto", "w:w": 1000 } } }],
// });
// });
// });
describe("Cell", () => {
describe("#prepForXml", () => {
it("inserts a paragraph at the end of the cell if it is empty", () => {
const table = new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
});
const tree = new Formatter().format(table);
expect(tree)
@ -282,72 +271,119 @@ describe("Table", () => {
.which.is.an("array");
const row = tree["w:tbl"].find((x) => x["w:tr"]);
expect(row).not.to.be.undefined;
expect(row["w:tr"])
.to.be.an("array")
.which.has.length.at.least(1);
expect(row["w:tr"].find((x) => x["w:tc"])).to.deep.equal({
"w:tc": [{ "w:p": EMPTY_OBJECT }],
});
});
it("inserts a paragraph at the end of the cell even if it has a child table", () => {
const parentTable = new Table({
rows: 1,
columns: 1,
});
parentTable.getCell(0, 0).add(
new Table({
rows: 1,
columns: 1,
}),
);
const tree = new Formatter().format(parentTable);
expect(tree)
.to.have.property("w:tbl")
.which.is.an("array");
const row = tree["w:tbl"].find((x) => x["w:tr"]);
expect(row).not.to.be.undefined;
expect(row["w:tr"])
.to.be.an("array")
.which.has.length.at.least(1);
const cell = row["w:tr"].find((x) => x["w:tc"]);
expect(cell).not.to.be.undefined;
expect(cell["w:tc"][cell["w:tc"].length - 1]).to.deep.equal({
"w:p": EMPTY_OBJECT,
});
});
it("does not insert a paragraph if it already ends with one", () => {
const parentTable = new Table({
rows: 1,
columns: 1,
});
parentTable.getCell(0, 0).add(new Paragraph("Hello"));
const tree = new Formatter().format(parentTable);
expect(tree)
.to.have.property("w:tbl")
.which.is.an("array");
const row = tree["w:tbl"].find((x) => x["w:tr"]);
expect(row).not.to.be.undefined;
expect(row["w:tr"])
.to.be.an("array")
.which.has.length.at.least(1);
expect(row["w:tr"].find((x) => x["w:tc"])).to.deep.equal({
"w:tc": [
{
"w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "Hello"] }] }],
"w:p": [
{
"w:r": [
{
"w:t": [
{
_attr: {
"xml:space": "preserve",
},
},
"hello",
],
},
],
},
],
},
],
});
});
// it("inserts a paragraph at the end of the cell even if it has a child table", () => {
// const table = new Table({
// rows: [
// new TableRow({
// children: [
// new TableCell({
// children: [new Paragraph("hello")],
// }),
// ],
// }),
// ],
// });
// table.getCell(0, 0).add(
// new Table({
// rows: [
// new TableRow({
// children: [
// new TableCell({
// children: [new Paragraph("hello")],
// }),
// ],
// }),
// ],
// }),
// );
// const tree = new Formatter().format(table);
// expect(tree)
// .to.have.property("w:tbl")
// .which.is.an("array");
// const row = tree["w:tbl"].find((x) => x["w:tr"]);
// expect(row).not.to.be.undefined;
// expect(row["w:tr"])
// .to.be.an("array")
// .which.has.length.at.least(1);
// const cell = row["w:tr"].find((x) => x["w:tc"]);
// expect(cell).not.to.be.undefined;
// expect(cell["w:tc"][cell["w:tc"].length - 1]).to.deep.equal({
// "w:p": EMPTY_OBJECT,
// });
// });
// it("does not insert a paragraph if it already ends with one", () => {
// const table = new Table({
// rows: [
// new TableRow({
// children: [
// new TableCell({
// children: [new Paragraph("hello")],
// }),
// ],
// }),
// ],
// });
// table.getCell(0, 0).add(new Paragraph("Hello"));
// const tree = new Formatter().format(table);
// expect(tree)
// .to.have.property("w:tbl")
// .which.is.an("array");
// const row = tree["w:tbl"].find((x) => x["w:tr"]);
// expect(row).not.to.be.undefined;
// expect(row["w:tr"])
// .to.be.an("array")
// .which.has.length.at.least(1);
// expect(row["w:tr"].find((x) => x["w:tc"])).to.deep.equal({
// "w:tc": [
// {
// "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "Hello"] }] }],
// },
// ],
// });
// });
});
});
describe("#float", () => {
it("sets the table float properties", () => {
const table = new Table({
rows: 1,
columns: 1,
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph("hello")],
}),
],
}),
],
float: {
horizontalAnchor: TableAnchorType.MARGIN,
verticalAnchor: TableAnchorType.PAGE,

View File

@ -1,12 +1,11 @@
// http://officeopenxml.com/WPtableGrid.php
import { XmlComponent } from "file/xml-components";
import { TableGrid } from "./grid";
import { TableCell, WidthType } from "./table-cell";
import { TableColumn } from "./table-column";
import { TableCell, VerticalMergeType, WidthType } from "./table-cell";
import { ITableFloatOptions, TableProperties } from "./table-properties";
import { TableLayoutType } from "./table-properties/table-layout";
import { TableRow } from "./table-row";
/*
0-width columns don't get rendered correctly, so we need
to give them some value. A reasonable default would be
@ -18,10 +17,11 @@ import { TableRow } from "./table-row";
algorithm will expand columns to fit its content
*/
export interface ITableOptions {
readonly rows: number;
readonly columns: number;
readonly width?: number;
readonly widthUnitType?: WidthType;
readonly rows: TableRow[];
readonly width?: {
readonly size: number;
readonly type?: WidthType;
};
readonly columnWidths?: number[];
readonly margins?: {
readonly marginUnitType?: WidthType;
@ -36,14 +36,11 @@ export interface ITableOptions {
export class Table extends XmlComponent {
private readonly properties: TableProperties;
private readonly rows: TableRow[];
constructor({
rows,
columns,
width = 100,
widthUnitType = WidthType.AUTO,
columnWidths = Array<number>(columns).fill(100),
width,
columnWidths = Array<number>(Math.max(...rows.map((row) => row.CellCount))).fill(100),
margins: { marginUnitType, top, bottom, right, left } = { marginUnitType: WidthType.AUTO, top: 0, bottom: 0, right: 0, left: 0 },
float,
layout,
@ -52,26 +49,45 @@ export class Table extends XmlComponent {
this.properties = new TableProperties();
this.root.push(this.properties);
this.properties.setBorder();
this.properties.setWidth(width, widthUnitType);
if (width) {
this.properties.setWidth(width.size, width.type);
} else {
this.properties.setWidth(100);
}
this.properties.CellMargin.addBottomMargin(bottom || 0, marginUnitType);
this.properties.CellMargin.addTopMargin(top || 0, marginUnitType);
this.properties.CellMargin.addLeftMargin(left || 0, marginUnitType);
this.properties.CellMargin.addRightMargin(right || 0, marginUnitType);
const grid = new TableGrid(columnWidths);
this.root.push(grid);
this.root.push(new TableGrid(columnWidths));
this.rows = Array(rows)
.fill(0)
.map(() => {
const cells = Array(columns)
.fill(0)
.map(() => new TableCell());
const row = new TableRow(cells);
return row;
for (const row of rows) {
this.root.push(row);
}
for (const row of rows) {
row.Children.forEach((cell, cellIndex) => {
const column = rows.map((r) => r.Children[cellIndex]);
// Row Span has to be added in this method and not the constructor because it needs to know information about the column which happens after Table Cell construction
// Row Span of 1 will crash word as it will add RESTART and not a corresponding CONTINUE
if (cell.options.rowSpan && cell.options.rowSpan > 1) {
const thisCellsColumnIndex = column.indexOf(cell);
const endColumnIndex = thisCellsColumnIndex + (cell.options.rowSpan - 1);
for (let i = thisCellsColumnIndex + 1; i <= endColumnIndex; i++) {
rows[i].addCellToIndex(
new TableCell({
children: [],
verticalMerge: VerticalMergeType.CONTINUE,
}),
i,
);
}
}
});
this.rows.forEach((x) => this.root.push(x));
}
if (float) {
this.properties.setTableFloatProperties(float);
@ -81,24 +97,4 @@ export class Table extends XmlComponent {
this.properties.setLayout(layout);
}
}
public getRow(index: number): TableRow {
const row = this.rows[index];
if (!row) {
throw Error("Index out of bounds when trying to get row on table");
}
return row;
}
public getColumn(index: number): TableColumn {
// This is a convinence method for people who like to work with columns
const cells = this.rows.map((row) => row.getCell(index));
return new TableColumn(cells);
}
public getCell(row: number, col: number): TableCell {
return this.getRow(row).getCell(col);
}
}