diff --git a/.nycrc b/.nycrc index 25c38c14ad..c06a0f3774 100644 --- a/.nycrc +++ b/.nycrc @@ -1,9 +1,9 @@ { "check-coverage": true, - "lines": 96.81, - "functions": 93.80, - "branches": 92.63, - "statements": 96.80, + "lines": 97.77, + "functions": 93.89, + "branches": 94.55, + "statements": 97.75, "include": [ "src/**/*.ts" ], diff --git a/demo/22-right-to-left-text.ts b/demo/22-right-to-left-text.ts index 2c3984fb46..4ee344391c 100644 --- a/demo/22-right-to-left-text.ts +++ b/demo/22-right-to-left-text.ts @@ -1,7 +1,7 @@ // This demo shows right to left for special languages // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { Document, Packer, Paragraph, TextRun } from "../build"; +import { Document, Packer, Paragraph, Table, TableCell, TableRow, TextRun } from "../build"; const doc = new Document(); @@ -36,6 +36,31 @@ doc.addSection({ }), ], }), + new Table({ + visuallyRightToLeft: true, + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("שלום עולם")], + }), + new TableCell({ + children: [], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [], + }), + new TableCell({ + children: [new Paragraph("שלום עולם")], + }), + ], + }), + ], + }), ], }); diff --git a/docs/usage/tables.md b/docs/usage/tables.md index 8d10b55ca3..637efa2977 100644 --- a/docs/usage/tables.md +++ b/docs/usage/tables.md @@ -326,6 +326,17 @@ const cell = new TableCell({ }); ``` +### Visual Right to Left Table + +It is possible to reverse how the cells of the table are displayed. The table direction. More info here: https://superuser.com/questions/996912/how-to-change-a-table-direction-in-microsoft-word + +```ts +const table = new Table({ + visuallyRightToLeft: true, +}); +``` + + ## Examples [Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/4-basic-table.ts ':include') diff --git a/src/file/paragraph/formatting/bidirectional.spec.ts b/src/file/paragraph/formatting/bidirectional.spec.ts new file mode 100644 index 0000000000..a197ada54e --- /dev/null +++ b/src/file/paragraph/formatting/bidirectional.spec.ts @@ -0,0 +1,15 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; + +import { Bidirectional } from "./bidirectional"; + +describe("Bidirectional", () => { + it("should create", () => { + const bidirectional = new Bidirectional(); + const tree = new Formatter().format(bidirectional); + expect(tree).to.deep.equal({ + "w:bidi": {}, + }); + }); +}); diff --git a/src/file/table/table-properties/table-cell-margin.spec.ts b/src/file/table/table-properties/table-cell-margin.spec.ts index 2a89db7a4a..814888adc0 100644 --- a/src/file/table/table-properties/table-cell-margin.spec.ts +++ b/src/file/table/table-properties/table-cell-margin.spec.ts @@ -8,22 +8,31 @@ import { TableCellMargin } from "./table-cell-margin"; describe("TableCellMargin", () => { describe("#constructor", () => { it("should throw an error if theres no child elements", () => { - const cellMargin = new TableCellMargin(); + const cellMargin = new TableCellMargin({}); expect(() => new Formatter().format(cellMargin)).to.throw(); }); }); describe("#addTopMargin", () => { it("should add a table cell top margin", () => { - const cellMargin = new TableCellMargin(); - cellMargin.addTopMargin(1234, WidthType.DXA); + const cellMargin = new TableCellMargin({ + top: { + value: 1234, + type: 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 cellMargin = new TableCellMargin({ + top: { + value: 1234, + }, + }); + const tree = new Formatter().format(cellMargin); expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:top": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }); }); @@ -31,15 +40,22 @@ describe("TableCellMargin", () => { describe("#addLeftMargin", () => { it("should add a table cell left margin", () => { - const cellMargin = new TableCellMargin(); - cellMargin.addLeftMargin(1234, WidthType.DXA); + const cellMargin = new TableCellMargin({ + left: { + value: 1234, + type: 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 cellMargin = new TableCellMargin({ + left: { + value: 1234, + }, + }); const tree = new Formatter().format(cellMargin); expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:left": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }); }); @@ -47,15 +63,24 @@ describe("TableCellMargin", () => { describe("#addBottomMargin", () => { it("should add a table cell bottom margin", () => { - const cellMargin = new TableCellMargin(); - cellMargin.addBottomMargin(1234, WidthType.DXA); + const cellMargin = new TableCellMargin({ + bottom: { + value: 1234, + type: 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 cellMargin = new TableCellMargin({ + bottom: { + value: 1234, + }, + }); + const tree = new Formatter().format(cellMargin); expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:bottom": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }); }); @@ -63,15 +88,24 @@ describe("TableCellMargin", () => { describe("#addRightMargin", () => { it("should add a table cell right margin", () => { - const cellMargin = new TableCellMargin(); - cellMargin.addRightMargin(1234, WidthType.DXA); + const cellMargin = new TableCellMargin({ + right: { + value: 1234, + type: 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 cellMargin = new TableCellMargin({ + right: { + value: 1234, + }, + }); + const tree = new Formatter().format(cellMargin); expect(tree).to.deep.equal({ "w:tblCellMar": [{ "w:right": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }); }); diff --git a/src/file/table/table-properties/table-cell-margin.ts b/src/file/table/table-properties/table-cell-margin.ts index ebebb87bc3..6889b6f580 100644 --- a/src/file/table/table-properties/table-cell-margin.ts +++ b/src/file/table/table-properties/table-cell-margin.ts @@ -6,7 +6,23 @@ class TableCellMarginAttributes extends XmlAttributeComponent<{ readonly type: W protected readonly xmlKeys = { value: "w:w", type: "w:type" }; } +interface IBaseTableCellMarginOptions { + readonly value: number; + readonly type?: WidthType; +} + class BaseTableCellMargin extends XmlComponent { + constructor(rootKey: string, options: IBaseTableCellMarginOptions) { + super(rootKey); + + this.root.push( + new TableCellMarginAttributes({ + type: options.type ?? WidthType.DXA, + value: options.value, + }), + ); + } + public setProperties(value: number, type: WidthType = WidthType.DXA): void { this.root.push( new TableCellMarginAttributes({ @@ -17,36 +33,31 @@ class BaseTableCellMargin extends XmlComponent { } } +export interface ITableCellMarginOptions { + readonly top?: IBaseTableCellMarginOptions; + readonly bottom?: IBaseTableCellMarginOptions; + readonly left?: IBaseTableCellMarginOptions; + readonly right?: IBaseTableCellMarginOptions; +} + export class TableCellMargin extends IgnoreIfEmptyXmlComponent { - constructor() { + constructor(options: ITableCellMarginOptions) { super("w:tblCellMar"); - } - public addTopMargin(value: number, type: WidthType = WidthType.DXA): void { - const top = new BaseTableCellMargin("w:top"); + if (options.bottom) { + this.root.push(new BaseTableCellMargin("w:bottom", options.bottom)); + } - top.setProperties(value, type); - this.root.push(top); - } + if (options.top) { + this.root.push(new BaseTableCellMargin("w:top", options.top)); + } - public addLeftMargin(value: number, type: WidthType = WidthType.DXA): void { - const left = new BaseTableCellMargin("w:left"); + if (options.left) { + this.root.push(new BaseTableCellMargin("w:left", options.left)); + } - left.setProperties(value, type); - this.root.push(left); - } - - public addBottomMargin(value: number, type: WidthType = WidthType.DXA): void { - const bottom = new BaseTableCellMargin("w:bottom"); - - bottom.setProperties(value, type); - this.root.push(bottom); - } - - public addRightMargin(value: number, type: WidthType = WidthType.DXA): void { - const right = new BaseTableCellMargin("w:right"); - - right.setProperties(value, type); - this.root.push(right); + if (options.right) { + this.root.push(new BaseTableCellMargin("w:right", options.right)); + } } } diff --git a/src/file/table/table-properties/table-properties.spec.ts b/src/file/table/table-properties/table-properties.spec.ts index 1c10e09b6a..77205e70db 100644 --- a/src/file/table/table-properties/table-properties.spec.ts +++ b/src/file/table/table-properties/table-properties.spec.ts @@ -11,7 +11,7 @@ import { TableProperties } from "./table-properties"; describe("TableProperties", () => { describe("#constructor", () => { it("creates an initially empty property object", () => { - const tp = new TableProperties(); + const tp = new TableProperties({}); // The TableProperties is ignorable if there are no attributes, // which results in prepForXml returning undefined, which causes // the formatter to throw an error if that is the only object it @@ -22,7 +22,12 @@ describe("TableProperties", () => { describe("#setWidth", () => { it("should add a table width property", () => { - const tp = new TableProperties().setWidth(1234, WidthType.DXA); + const tp = new TableProperties({ + width: { + size: 1234, + type: WidthType.DXA, + }, + }); const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblW": { _attr: { "w:type": "dxa", "w:w": 1234 } } }], @@ -30,7 +35,12 @@ describe("TableProperties", () => { }); it("should add a table width property with default of AUTO", () => { - const tp = new TableProperties().setWidth(1234); + const tp = new TableProperties({ + width: { + size: 1234, + }, + }); + const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblW": { _attr: { "w:type": "auto", "w:w": 1234 } } }], @@ -40,8 +50,10 @@ describe("TableProperties", () => { describe("#setLayout", () => { it("sets the table to fixed width layout", () => { - const tp = new TableProperties(); - tp.setLayout(TableLayoutType.FIXED); + const tp = new TableProperties({ + layout: TableLayoutType.FIXED, + }); + const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblLayout": { _attr: { "w:type": "fixed" } } }], @@ -51,8 +63,15 @@ describe("TableProperties", () => { describe("#cellMargin", () => { it("adds a table cell top margin", () => { - const tp = new TableProperties(); - tp.CellMargin.addTopMargin(1234, WidthType.DXA); + const tp = new TableProperties({ + cellMargin: { + top: { + value: 1234, + type: WidthType.DXA, + }, + }, + }); + const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblCellMar": [{ "w:top": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }], @@ -60,8 +79,15 @@ describe("TableProperties", () => { }); it("adds a table cell left margin", () => { - const tp = new TableProperties(); - tp.CellMargin.addLeftMargin(1234, WidthType.DXA); + const tp = new TableProperties({ + cellMargin: { + left: { + value: 1234, + type: WidthType.DXA, + }, + }, + }); + const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [{ "w:tblCellMar": [{ "w:left": { _attr: { "w:type": "dxa", "w:w": 1234 } } }] }], @@ -71,12 +97,14 @@ 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 tp = new TableProperties({ + shading: { + fill: "b79c2f", + val: ShadingType.REVERSE_DIAGONAL_STRIPE, + color: "auto", + }, }); + const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [ @@ -95,9 +123,10 @@ describe("TableProperties", () => { }); describe("#setAlignment", () => { - it("sets the shading of the table", () => { - const tp = new TableProperties(); - tp.setAlignment(AlignmentType.CENTER); + it("sets the alignment of the table", () => { + const tp = new TableProperties({ + alignment: AlignmentType.CENTER, + }); const tree = new Formatter().format(tp); expect(tree).to.deep.equal({ "w:tblPr": [ @@ -112,4 +141,20 @@ describe("TableProperties", () => { }); }); }); + + describe("#Set Virtual Right to Left", () => { + it("sets the alignment of the table", () => { + const tp = new TableProperties({ + visuallyRightToLeft: true, + }); + const tree = new Formatter().format(tp); + expect(tree).to.deep.equal({ + "w:tblPr": [ + { + "w:bidiVisual": {}, + }, + ], + }); + }); + }); }); diff --git a/src/file/table/table-properties/table-properties.ts b/src/file/table/table-properties/table-properties.ts index 8a7d000d5b..de210a90ae 100644 --- a/src/file/table/table-properties/table-properties.ts +++ b/src/file/table/table-properties/table-properties.ts @@ -5,51 +5,58 @@ import { Alignment, AlignmentType } from "../../paragraph"; import { ITableShadingAttributesProperties, TableShading } from "../shading"; import { WidthType } from "../table-cell"; import { ITableBordersOptions, TableBorders } from "./table-borders"; -import { TableCellMargin } from "./table-cell-margin"; +import { ITableCellMarginOptions, TableCellMargin } from "./table-cell-margin"; import { ITableFloatOptions, TableFloatProperties } from "./table-float-properties"; import { TableLayout, TableLayoutType } from "./table-layout"; import { PreferredTableWidth } from "./table-width"; +import { VisuallyRightToLeft } from "./visually-right-to-left"; + +export interface ITablePropertiesOptions { + readonly width?: { + readonly size: number; + readonly type?: WidthType; + }; + readonly layout?: TableLayoutType; + readonly borders?: ITableBordersOptions; + readonly float?: ITableFloatOptions; + readonly shading?: ITableShadingAttributesProperties; + readonly alignment?: AlignmentType; + readonly cellMargin?: ITableCellMarginOptions; + readonly visuallyRightToLeft?: boolean; +} export class TableProperties extends IgnoreIfEmptyXmlComponent { - private readonly cellMargin: TableCellMargin; - - constructor() { + constructor(options: ITablePropertiesOptions) { super("w:tblPr"); - this.cellMargin = new TableCellMargin(); - this.root.push(this.cellMargin); - } + this.root.push(new TableCellMargin(options.cellMargin || {})); - public setWidth(width: number, type: WidthType = WidthType.AUTO): TableProperties { - this.root.push(new PreferredTableWidth(type, width)); - return this; - } + if (options.borders) { + this.root.push(new TableBorders(options.borders)); + } - public setLayout(type: TableLayoutType): void { - this.root.push(new TableLayout(type)); - } + if (options.width) { + this.root.push(new PreferredTableWidth(options.width.type, options.width.size)); + } - public setBorder(borderOptions: ITableBordersOptions): TableProperties { - this.root.push(new TableBorders(borderOptions)); - return this; - } + if (options.float) { + this.root.push(new TableFloatProperties(options.float)); + } - public get CellMargin(): TableCellMargin { - return this.cellMargin; - } + if (options.layout) { + this.root.push(new TableLayout(options.layout)); + } - public setTableFloatProperties(tableFloatOptions: ITableFloatOptions): TableProperties { - this.root.push(new TableFloatProperties(tableFloatOptions)); - return this; - } + if (options.alignment) { + this.root.push(new Alignment(options.alignment)); + } - public setShading(attrs: ITableShadingAttributesProperties): TableProperties { - this.root.push(new TableShading(attrs)); + if (options.shading) { + this.root.push(new TableShading(options.shading)); + } - return this; - } - - public setAlignment(type: AlignmentType): void { - this.root.push(new Alignment(type)); + if (options.visuallyRightToLeft) { + this.root.push(new VisuallyRightToLeft()); + } } } diff --git a/src/file/table/table-properties/table-width.ts b/src/file/table/table-properties/table-width.ts index a463e958df..44468c744d 100644 --- a/src/file/table/table-properties/table-width.ts +++ b/src/file/table/table-properties/table-width.ts @@ -13,7 +13,7 @@ class TableWidthAttributes extends XmlAttributeComponent { } export class PreferredTableWidth extends XmlComponent { - constructor(type: WidthType, w: number) { + constructor(type: WidthType = WidthType.AUTO, w: number) { super("w:tblW"); const width: number | string = type === WidthType.PERCENTAGE ? `${w}%` : w; this.root.push(new TableWidthAttributes({ type: type, w: width })); diff --git a/src/file/table/table-properties/visually-right-to-left.spec.ts b/src/file/table/table-properties/visually-right-to-left.spec.ts new file mode 100644 index 0000000000..792c90194b --- /dev/null +++ b/src/file/table/table-properties/visually-right-to-left.spec.ts @@ -0,0 +1,14 @@ +import { expect } from "chai"; + +import { Formatter } from "export/formatter"; +import { VisuallyRightToLeft } from "./visually-right-to-left"; + +describe("VisuallyRightToLeft", () => { + it("should create", () => { + const visuallyRightToLeft = new VisuallyRightToLeft(); + const tree = new Formatter().format(visuallyRightToLeft); + expect(tree).to.deep.equal({ + "w:bidiVisual": {}, + }); + }); +}); diff --git a/src/file/table/table-properties/visually-right-to-left.ts b/src/file/table/table-properties/visually-right-to-left.ts new file mode 100644 index 0000000000..c0598a5a26 --- /dev/null +++ b/src/file/table/table-properties/visually-right-to-left.ts @@ -0,0 +1,8 @@ +// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_bidiVisual_topic_ID0EOXIQ.html +import { XmlComponent } from "file/xml-components"; + +export class VisuallyRightToLeft extends XmlComponent { + constructor() { + super("w:bidiVisual"); + } +} diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 69b3c0f775..3aadf0b28e 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -36,11 +36,10 @@ export interface ITableOptions { readonly layout?: TableLayoutType; readonly borders?: ITableBordersOptions; readonly alignment?: AlignmentType; + readonly visuallyRightToLeft?: boolean; } export class Table extends XmlComponent { - private readonly properties: TableProperties; - constructor({ rows, width, @@ -50,27 +49,38 @@ export class Table extends XmlComponent { layout, borders, alignment, + visuallyRightToLeft, }: ITableOptions) { super("w:tbl"); - this.properties = new TableProperties(); - this.root.push(this.properties); - if (borders) { - this.properties.setBorder(borders); - } else { - this.properties.setBorder({}); - } - - 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); + this.root.push( + new TableProperties({ + borders: borders ?? {}, + width: width ?? { size: 100 }, + float, + layout, + alignment, + cellMargin: { + bottom: { + value: bottom || 0, + type: marginUnitType, + }, + top: { + value: top || 0, + type: marginUnitType, + }, + left: { + value: left || 0, + type: marginUnitType, + }, + right: { + value: right || 0, + type: marginUnitType, + }, + }, + visuallyRightToLeft, + }), + ); this.root.push(new TableGrid(columnWidths)); @@ -101,17 +111,5 @@ export class Table extends XmlComponent { columnIndex += cell.options.columnSpan || 1; }); }); - - if (float) { - this.properties.setTableFloatProperties(float); - } - - if (layout) { - this.properties.setLayout(layout); - } - - if (alignment) { - this.properties.setAlignment(alignment); - } } }