@ -1,7 +1,9 @@
|
||||
import { Paragraph } from "../paragraph";
|
||||
import { Table } from "../table";
|
||||
import { XmlComponent } from "../xml-components";
|
||||
import { Body } from "./body";
|
||||
import { DocumentAttributes } from "./document-attributes";
|
||||
|
||||
export class Document extends XmlComponent {
|
||||
private body: Body;
|
||||
|
||||
@ -39,4 +41,15 @@ export class Document extends XmlComponent {
|
||||
this.addParagraph(para);
|
||||
return para;
|
||||
}
|
||||
|
||||
public addTable(table: Table): void {
|
||||
this.body.push(table);
|
||||
}
|
||||
|
||||
public createTable(rows: number, cols: number): Table {
|
||||
const table = new Table(rows, cols);
|
||||
this.addTable(table);
|
||||
return table;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,3 +2,4 @@ export { Document } from "./document";
|
||||
export { Paragraph } from "./paragraph";
|
||||
export { Run } from "./run";
|
||||
export { TextRun } from "./run/text-run";
|
||||
export { Table } from "./table";
|
||||
|
@ -1,3 +0,0 @@
|
||||
export class Table {
|
||||
|
||||
}
|
21
ts/docx/table/grid.ts
Normal file
21
ts/docx/table/grid.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { XmlAttributeComponent, XmlComponent } from "../xml-components";
|
||||
|
||||
export class TableGrid extends XmlComponent {
|
||||
constructor(cols: number[]) {
|
||||
super("w:tblGrid");
|
||||
cols.forEach((col) => this.root.push(new GridCol(col)));
|
||||
}
|
||||
}
|
||||
|
||||
class GridColAttributes extends XmlAttributeComponent<{w: number}> {
|
||||
protected xmlKeys = {w: "w:w"};
|
||||
}
|
||||
|
||||
export class GridCol extends XmlComponent {
|
||||
constructor(width?: number) {
|
||||
super("w:gridCol");
|
||||
if (width !== undefined) {
|
||||
this.root.push(new GridColAttributes({w: width}));
|
||||
}
|
||||
}
|
||||
}
|
123
ts/docx/table/index.ts
Normal file
123
ts/docx/table/index.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { Paragraph } from "../paragraph";
|
||||
import { XmlComponent } from "../xml-components";
|
||||
|
||||
import { TableGrid } from "./grid";
|
||||
import { TableProperties, widthTypes } from "./properties";
|
||||
|
||||
export class Table extends XmlComponent {
|
||||
private properties: TableProperties;
|
||||
private rows: TableRow[];
|
||||
private grid: TableGrid;
|
||||
|
||||
constructor(rows: number, cols: number) {
|
||||
super("w:tbl");
|
||||
this.properties = new TableProperties();
|
||||
this.root.push(this.properties);
|
||||
|
||||
const gridCols: number[] = [];
|
||||
for (let i = 0; i < cols; i++) {
|
||||
/*
|
||||
0-width columns don't get rendered correctly, so we need
|
||||
to give them some value. A reasonable default would be
|
||||
~6in / numCols, but if we do that it becomes very hard
|
||||
to resize the table using setWidth, unless the layout
|
||||
algorithm is set to 'fixed'. Instead, the approach here
|
||||
means even in 'auto' layout, setting a width on the
|
||||
table will make it look reasonable, as the layout
|
||||
algorithm will expand columns to fit its content
|
||||
*/
|
||||
gridCols.push(1);
|
||||
}
|
||||
this.grid = new TableGrid(gridCols);
|
||||
this.root.push(this.grid);
|
||||
|
||||
this.rows = [];
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const cells: TableCell[] = [];
|
||||
for (let j = 0; j < cols; j++) {
|
||||
cells.push(new TableCell());
|
||||
}
|
||||
const row = new TableRow(cells);
|
||||
this.rows.push(row);
|
||||
this.root.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
public getRow(ix: number): TableRow {
|
||||
return this.rows[ix];
|
||||
}
|
||||
|
||||
public getCell(row: number, col: number): TableCell {
|
||||
return this.getRow(row).getCell(col);
|
||||
}
|
||||
|
||||
public setWidth(type: widthTypes, width: number | string): Table {
|
||||
this.properties.setWidth(type, width);
|
||||
return this;
|
||||
}
|
||||
|
||||
public fixedWidthLayout(): Table {
|
||||
this.properties.fixedWidthLayout();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class TableRow extends XmlComponent {
|
||||
private properties: TableRowProperties;
|
||||
private cells: TableCell[];
|
||||
|
||||
constructor(cells: TableCell[]) {
|
||||
super("w:tr");
|
||||
this.properties = new TableRowProperties();
|
||||
this.root.push(this.properties);
|
||||
this.cells = cells;
|
||||
cells.forEach((c) => this.root.push(c));
|
||||
}
|
||||
|
||||
public getCell(ix: number): TableCell {
|
||||
return this.cells[ix];
|
||||
}
|
||||
}
|
||||
|
||||
class TableRowProperties extends XmlComponent {
|
||||
constructor() {
|
||||
super("w:trPr");
|
||||
}
|
||||
}
|
||||
|
||||
class TableCell extends XmlComponent {
|
||||
private properties: TableCellProperties;
|
||||
|
||||
constructor() {
|
||||
super("w:tc");
|
||||
this.properties = new TableCellProperties();
|
||||
this.root.push(this.properties);
|
||||
}
|
||||
|
||||
public addContent(content: Paragraph | Table): TableCell {
|
||||
this.root.push(content);
|
||||
return this;
|
||||
}
|
||||
|
||||
public prepForXml(): object {
|
||||
// Cells must end with a paragraph
|
||||
const retval = super.prepForXml();
|
||||
const content = retval["w:tc"];
|
||||
if (!content[content.length - 1]["w:p"]) {
|
||||
content.push(new Paragraph().prepForXml());
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
public createParagraph(text?: string): Paragraph {
|
||||
const para = new Paragraph(text);
|
||||
this.addContent(para);
|
||||
return para;
|
||||
}
|
||||
}
|
||||
|
||||
class TableCellProperties extends XmlComponent {
|
||||
constructor() {
|
||||
super("w:tcPr");
|
||||
}
|
||||
}
|
48
ts/docx/table/properties.ts
Normal file
48
ts/docx/table/properties.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { XmlAttributeComponent, XmlComponent } from "../xml-components";
|
||||
|
||||
export type widthTypes = "dxa" | "pct" | "nil" | "auto";
|
||||
|
||||
export class TableProperties extends XmlComponent {
|
||||
constructor() {
|
||||
super("w:tblPr");
|
||||
}
|
||||
|
||||
public setWidth(type: widthTypes, w: number | string): TableProperties {
|
||||
this.root.push(new PreferredTableWidth(type, w));
|
||||
return this;
|
||||
}
|
||||
|
||||
public fixedWidthLayout(): TableProperties {
|
||||
this.root.push(new TableLayout("fixed"));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface ITableWidth {
|
||||
type: widthTypes;
|
||||
w: number | string;
|
||||
}
|
||||
|
||||
class TableWidthAttributes extends XmlAttributeComponent<ITableWidth> {
|
||||
protected xmlKeys = {type: "w:type", w: "w:w"};
|
||||
}
|
||||
|
||||
class PreferredTableWidth extends XmlComponent {
|
||||
constructor(type: widthTypes, w: number | string) {
|
||||
super("w:tblW");
|
||||
this.root.push(new TableWidthAttributes({type, w}));
|
||||
}
|
||||
}
|
||||
|
||||
type tableLayout = "autofit" | "fixed";
|
||||
|
||||
class TableLayoutAttributes extends XmlAttributeComponent<{type: tableLayout}> {
|
||||
protected xmlKeys = {type: "w:type"};
|
||||
}
|
||||
|
||||
class TableLayout extends XmlComponent {
|
||||
constructor(type: tableLayout) {
|
||||
super("w:tblLayout");
|
||||
this.root.push(new TableLayoutAttributes({type}));
|
||||
}
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"outDir": "../build-tests",
|
||||
"sourceRoot": "./",
|
||||
"rootDir": "./",
|
||||
"module": "commonjs"
|
||||
}
|
||||
|
@ -46,4 +46,28 @@ describe("Document", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#createTable", () => {
|
||||
it("should create a new table and append it to body", () => {
|
||||
const table = document.createTable(2, 3);
|
||||
expect(table).to.be.an.instanceof(docx.Table);
|
||||
const body = new Formatter().format(document)["w:document"][1]["w:body"];
|
||||
expect(body).to.be.an("array").which.has.length.at.least(1);
|
||||
expect(body[0]).to.have.property("w:tbl");
|
||||
});
|
||||
|
||||
it("should create a table with the correct dimensions", () => {
|
||||
document.createTable(2, 3);
|
||||
const body = new Formatter().format(document)["w:document"][1]["w:body"];
|
||||
expect(body).to.be.an("array").which.has.length.at.least(1);
|
||||
expect(body[0]).to.have.property("w:tbl").which.includes({
|
||||
"w:tblGrid": [
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
],
|
||||
});
|
||||
expect(body[0]["w:tbl"].filter((x) => x["w:tr"])).to.have.length(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
37
ts/tests/docx/table/testGrid.ts
Normal file
37
ts/tests/docx/table/testGrid.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { expect } from "chai";
|
||||
import { GridCol, TableGrid } from "../../../docx/table/grid";
|
||||
import { Formatter } from "../../../export/formatter";
|
||||
|
||||
describe("GridCol", () => {
|
||||
describe("#constructor", () => {
|
||||
it("sets the width attribute to the value given", () => {
|
||||
const grid = new GridCol(1234);
|
||||
const tree = new Formatter().format(grid);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:gridCol": [{_attr: {"w:w": 1234}}],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not set a width attribute if not given", () => {
|
||||
const grid = new GridCol();
|
||||
const tree = new Formatter().format(grid);
|
||||
expect(tree).to.deep.equal({"w:gridCol": []});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TableGrid", () => {
|
||||
describe("#constructor", () => {
|
||||
it("creates a column for each width given", () => {
|
||||
const grid = new TableGrid([1234, 321, 123]);
|
||||
const tree = new Formatter().format(grid);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tblGrid": [
|
||||
{"w:gridCol": [{_attr: {"w:w": 1234}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 321}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 123}}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
37
ts/tests/docx/table/testProperties.ts
Normal file
37
ts/tests/docx/table/testProperties.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { expect } from "chai";
|
||||
import { TableProperties } from "../../../docx/table/properties";
|
||||
import { Formatter } from "../../../export/formatter";
|
||||
|
||||
describe("TableProperties", () => {
|
||||
describe("#constructor", () => {
|
||||
it("creates an initially empty property object", () => {
|
||||
const tp = new TableProperties();
|
||||
const tree = new Formatter().format(tp);
|
||||
expect(tree).to.deep.equal({"w:tblPr": []});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setWidth", () => {
|
||||
it("adds a table width property", () => {
|
||||
const tp = new TableProperties().setWidth("dxa", 1234);
|
||||
const tree = new Formatter().format(tp);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tblPr": [
|
||||
{"w:tblW": [{_attr: {"w:type": "dxa", "w:w": 1234}}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fixedWidthLayout", () => {
|
||||
it("sets the table to fixed width layout", () => {
|
||||
const tp = new TableProperties().fixedWidthLayout();
|
||||
const tree = new Formatter().format(tp);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tblPr": [
|
||||
{"w:tblLayout": [{_attr: {"w:type": "fixed"}}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
185
ts/tests/docx/table/testTable.ts
Normal file
185
ts/tests/docx/table/testTable.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { expect } from "chai";
|
||||
import { Paragraph } from "../../../docx/paragraph";
|
||||
import { Table } from "../../../docx/table";
|
||||
import { Formatter } from "../../../export/formatter";
|
||||
|
||||
describe("Table", () => {
|
||||
describe("#constructor", () => {
|
||||
it("creates a table with the correct number of rows and columns", () => {
|
||||
const table = new Table(3, 2);
|
||||
const tree = new Formatter().format(table);
|
||||
const cell = {"w:tc": [{"w:tcPr": []}, {"w:p": [{"w:pPr": []}]}]};
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tbl": [
|
||||
{"w:tblPr": []},
|
||||
{"w:tblGrid": [
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
]},
|
||||
{"w:tr": [{"w:trPr": []}, cell, cell]},
|
||||
{"w:tr": [{"w:trPr": []}, cell, cell]},
|
||||
{"w:tr": [{"w:trPr": []}, cell, cell]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getRow and Row#getCell", () => {
|
||||
it("returns the correct row", () => {
|
||||
const table = new Table(2, 2);
|
||||
table.getRow(0).getCell(0).addContent(new Paragraph("A1"));
|
||||
table.getRow(0).getCell(1).addContent(new Paragraph("B1"));
|
||||
table.getRow(1).getCell(0).addContent(new Paragraph("A2"));
|
||||
table.getRow(1).getCell(1).addContent(new Paragraph("B2"));
|
||||
const tree = new Formatter().format(table);
|
||||
const cell = (c) => ({"w:tc": [
|
||||
{"w:tcPr": []},
|
||||
{"w:p": [
|
||||
{"w:pPr": []},
|
||||
{"w:r": [{"w:rPr": []}, {"w:t": [c]}]},
|
||||
]},
|
||||
]});
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tbl": [
|
||||
{"w:tblPr": []},
|
||||
{"w:tblGrid": [
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
]},
|
||||
{"w:tr": [{"w:trPr": []}, cell("A1"), cell("B1")]},
|
||||
{"w:tr": [{"w:trPr": []}, cell("A2"), cell("B2")]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getCell", () => {
|
||||
it("returns the correct cell", () => {
|
||||
const table = new Table(2, 2);
|
||||
table.getCell(0, 0).addContent(new Paragraph("A1"));
|
||||
table.getCell(0, 1).addContent(new Paragraph("B1"));
|
||||
table.getCell(1, 0).addContent(new Paragraph("A2"));
|
||||
table.getCell(1, 1).addContent(new Paragraph("B2"));
|
||||
const tree = new Formatter().format(table);
|
||||
const cell = (c) => ({"w:tc": [
|
||||
{"w:tcPr": []},
|
||||
{"w:p": [
|
||||
{"w:pPr": []},
|
||||
{"w:r": [{"w:rPr": []}, {"w:t": [c]}]},
|
||||
]},
|
||||
]});
|
||||
expect(tree).to.deep.equal({
|
||||
"w:tbl": [
|
||||
{"w:tblPr": []},
|
||||
{"w:tblGrid": [
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
{"w:gridCol": [{_attr: {"w:w": 1}}]},
|
||||
]},
|
||||
{"w:tr": [{"w:trPr": []}, cell("A1"), cell("B1")]},
|
||||
{"w:tr": [{"w:trPr": []}, cell("A2"), cell("B2")]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setWidth", () => {
|
||||
it("sets the preferred width on the table", () => {
|
||||
const table = new Table(2, 2).setWidth("pct", 1000);
|
||||
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": [
|
||||
{"w:tblW": [{_attr: {"w:type": "pct", "w:w": 1000}}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#fixedWidthLayout", () => {
|
||||
it("sets the table to fixed width layout", () => {
|
||||
const table = new Table(2, 2).fixedWidthLayout();
|
||||
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": [
|
||||
{"w:tblLayout": [{_attr: {"w:type": "fixed"}}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cell", () => {
|
||||
describe("#prepForXml", () => {
|
||||
it("inserts a paragraph at the end of the cell if it is empty", () => {
|
||||
const table = new Table(1, 1);
|
||||
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:tcPr": []},
|
||||
{"w:p": [{"w:pPr": []}]},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("inserts a paragraph at the end of the cell even if it has a child table", () => {
|
||||
const parentTable = new Table(1, 1);
|
||||
parentTable.getCell(0, 0).addContent(new Table(1, 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": [{"w:pPr": []}],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not insert a paragraph if it already ends with one", () => {
|
||||
const parentTable = new Table(1, 1);
|
||||
parentTable.getCell(0, 0).addContent(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:tcPr": []},
|
||||
{"w:p": [
|
||||
{"w:pPr": []},
|
||||
{"w:r": [{"w:rPr": []}, {"w:t": ["Hello"]}]},
|
||||
]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#createParagraph", () => {
|
||||
it("inserts a new paragraph in the cell", () => {
|
||||
const table = new Table(1, 1);
|
||||
const para = table.getCell(0, 0).createParagraph("Test paragraph");
|
||||
expect(para).to.be.an.instanceof(Paragraph);
|
||||
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:tcPr": []},
|
||||
{"w:p": [
|
||||
{"w:pPr": []},
|
||||
{"w:r": [{"w:rPr": []}, {"w:t": ["Test paragraph"]}]},
|
||||
]},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user