Merge branch 'master' of https://github.com/h4buli/docx into feat/h4buli-update

# Conflicts:
#	package.json
#	src/file/numbering/numbering.ts
This commit is contained in:
Dolan Miu
2018-05-06 02:57:15 +01:00
23 changed files with 877 additions and 75 deletions

View File

@ -45,6 +45,7 @@
"@types/image-size": "0.0.29",
"@types/request-promise": "^4.1.41",
"archiver": "^2.1.1",
"fast-xml-parser": "^3.3.6",
"image-size": "^0.6.2",
"request": "^2.83.0",
"request-promise": "^4.2.2",

View File

@ -10,6 +10,7 @@ export interface IPropertiesOptions {
description?: string;
lastModifiedBy?: string;
revision?: string;
externalStyles?: string;
}
export class CoreProperties extends XmlComponent {

View File

@ -11,7 +11,9 @@ import { Paragraph, PictureRun } from "./paragraph";
import { Relationships } from "./relationships";
import { Styles } from "./styles";
import { DefaultStylesFactory } from "./styles/factory";
import { ExternalStylesFactory } from "./styles/external-styles-factory";
import { Table } from "./table";
import { IMediaData } from "index";
export class File {
private readonly document: Document;
@ -28,8 +30,6 @@ export class File {
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
this.document = new Document(sectionPropertiesOptions);
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
if (!options) {
options = {
@ -39,6 +39,14 @@ export class File {
};
}
if (options.externalStyles) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else {
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
}
this.coreProperties = new CoreProperties(options);
this.numbering = new Numbering();
this.docRelationships = new Relationships();
@ -111,6 +119,16 @@ export class File {
return this.document.createDrawing(mediaData);
}
public createImageData(imageName: string, data: Buffer, width?: number, height?: number): IMediaData {
const mediaData = this.media.addMediaWithData(imageName, data, this.docRelationships.RelationshipCount, width, height);
this.docRelationships.createRelationship(
mediaData.referenceId,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
return mediaData;
}
public get Document(): Document {
return this.document;
}

View File

@ -5,3 +5,4 @@ export * from "./numbering";
export * from "./media";
export * from "./drawing";
export * from "./styles";
export * from "./xml-components";

View File

@ -13,8 +13,8 @@ export interface IMediaDataDimensions {
export interface IMediaData {
referenceId: number;
stream: fs.ReadStream;
path: string;
stream: fs.ReadStream | Buffer;
path?: string;
fileName: string;
dimensions: IMediaDataDimensions;
}

View File

@ -11,23 +11,10 @@ export class Media {
this.map = new Map<string, IMediaData>();
}
public getMedia(key: string): IMediaData {
const data = this.map.get(key);
if (data === undefined) {
throw new Error(`Cannot find image with the key ${key}`);
}
return data;
}
public addMedia(filePath: string, relationshipsCount: number): IMediaData {
const key = path.basename(filePath);
const dimensions = sizeOf(filePath);
private createMedia(key: string, relationshipsCount, dimensions, data: fs.ReadStream | Buffer, filePath?: string, ) {
const imageData = {
referenceId: this.map.size + relationshipsCount + 1,
stream: fs.createReadStream(filePath),
stream: data,
path: filePath,
fileName: key,
dimensions: {
@ -45,6 +32,36 @@ export class Media {
return imageData;
}
public getMedia(key: string): IMediaData {
const data = this.map.get(key);
if (data === undefined) {
throw new Error(`Cannot find image with the key ${key}`);
}
return data;
}
public addMedia(filePath: string, relationshipsCount: number): IMediaData {
const key = path.basename(filePath);
const dimensions = sizeOf(filePath);
return this.createMedia(key, relationshipsCount, dimensions, fs.createReadStream(filePath), filePath);
}
public addMediaWithData(fileName: string, data: Buffer, relationshipsCount: number, width?, height?): IMediaData {
const key = fileName;
let dimensions;
if (width && height) {
dimensions = {
width: width,
height: height
}
} else {
dimensions = sizeOf(data);
}
return this.createMedia(key, relationshipsCount, dimensions, data);
}
public get array(): IMediaData[] {
const array = new Array<IMediaData>();

View File

@ -1 +1,2 @@
export * from "./numbering";
export * from "./abstract-numbering";

View File

@ -1,12 +1,14 @@
import { XmlComponent } from "file/xml-components";
import { XmlComponent, IXmlableObject } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { Indent } from "../paragraph/formatting";
import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num";
export class Numbering extends XmlComponent {
private nextId: number;
private abstractNumbering: Array<XmlComponent> = [];
private concreteNumbering: Array<XmlComponent> = [];
constructor() {
super("w:numbering");
this.root.push(
@ -32,39 +34,23 @@ export class Numbering extends XmlComponent {
);
this.nextId = 0;
const abstractNumbering = this.createAbstractNumbering();
abstractNumbering.createLevel(0, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 720, hanging: 360 }));
abstractNumbering.createLevel(1, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 1440, hanging: 360 }));
abstractNumbering.createLevel(2, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 2160, hanging: 360 }));
abstractNumbering.createLevel(3, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 2880, hanging: 360 }));
abstractNumbering.createLevel(4, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 3600, hanging: 360 }));
abstractNumbering.createLevel(5, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 4320, hanging: 360 }));
abstractNumbering.createLevel(6, "bullet", "\u25CF", "left").addParagraphProperty(new Indent({ left: 5040, hanging: 360 }));
abstractNumbering.createLevel(7, "bullet", "\u25CB", "left").addParagraphProperty(new Indent({ left: 5760, hanging: 360 }));
abstractNumbering.createLevel(8, "bullet", "\u25A0", "left").addParagraphProperty(new Indent({ left: 6480, hanging: 360 }));
this.createConcreteNumbering(abstractNumbering);
}
public createAbstractNumbering(): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++);
this.root.push(num);
this.abstractNumbering.push(num);
return num;
}
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id);
this.root.push(num);
this.concreteNumbering.push(num);
return num;
}
public prepForXml(): IXmlableObject {
this.abstractNumbering.forEach(x => this.root.push(x));
this.concreteNumbering.forEach(x => this.root.push(x));
return super.prepForXml();
}
}

View File

@ -141,6 +141,11 @@ export class Paragraph extends XmlComponent {
return this;
}
public setCustomNumbering(numberId: number, indentLevel: number): Paragraph {
this.properties.push(new NumberProperties(numberId, indentLevel));
return this;
}
public style(styleId: string): Paragraph {
this.properties.push(new Style(styleId));
return this;

View File

@ -0,0 +1,160 @@
import { expect } from "chai";
import { ExternalStylesFactory } from "./external-styles-factory";
describe("External styles factory", () => {
let externalStyles;
beforeEach(() => {
externalStyles = `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:mc="first" xmlns:r="second">
<w:docDefaults>
</w:docDefaults>
<w:latentStyles w:defLockedState="1" w:defUIPriority="99">
</w:latentStyles>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
<w:name w:val="Normal"/>
<w:qFormat/>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
<w:basedOn w:val="Normal"/>
<w:pPr>
<w:keepNext/>
<w:keepLines/>
<w:pBdr>
<w:bottom w:val="single" w:sz="4" w:space="1" w:color="auto"/>
</w:pBdr>
</w:pPr>
</w:style>
</w:styles>`;
});
describe("#parse", () => {
it("should parse w:styles attributes", () => {
const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
expect(importedStyle.rootKey).to.equal("w:styles");
expect(importedStyle.root[0]._attr).to.eql({
"xmlns:mc": "first",
"xmlns:r": "second",
});
});
it("should parse other child elements of w:styles", () => {
const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
expect(importedStyle.root.length).to.equal(5);
expect(importedStyle.root[1]).to.eql({
deleted: false,
root: [],
rootKey: "w:docDefaults",
});
expect(importedStyle.root[2]).to.eql({
_attr: {
"w:defLockedState": "1",
"w:defUIPriority": "99",
},
deleted: false,
root: [],
rootKey: "w:latentStyles",
});
});
it("should parse styles elements", () => {
const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any;
expect(importedStyle.root.length).to.equal(5);
expect(importedStyle.root[3]).to.eql({
_attr: {
"w:default": "1",
"w:styleId": "Normal",
"w:type": "paragraph",
},
deleted: false,
root: [
{
_attr: {
"w:val": "Normal",
},
deleted: false,
root: [],
rootKey: "w:name",
},
{
deleted: false,
root: [],
rootKey: "w:qFormat",
},
],
rootKey: "w:style",
});
expect(importedStyle.root[4]).to.eql({
_attr: {
"w:styleId": "Heading1",
"w:type": "paragraph",
},
deleted: false,
root: [
{
_attr: {
"w:val": "heading 1",
},
deleted: false,
root: [],
rootKey: "w:name",
},
{
_attr: {
"w:val": "Normal",
},
deleted: false,
root: [],
rootKey: "w:basedOn",
},
{
deleted: false,
root: [
{
deleted: false,
root: [],
rootKey: "w:keepNext",
},
{
deleted: false,
root: [],
rootKey: "w:keepLines",
},
{
deleted: false,
root: [
{
_attr: {
"w:color": "auto",
"w:space": "1",
"w:sz": "4",
"w:val": "single",
},
deleted: false,
root: [],
rootKey: "w:bottom",
},
],
rootKey: "w:pBdr",
},
],
rootKey: "w:pPr",
},
],
rootKey: "w:style",
});
});
});
});

View File

@ -0,0 +1,64 @@
import { Styles } from "./";
import * as fastXmlParser from "fast-xml-parser";
import { ImportedXmlComponent, ImportedRootElementAttributes } from "./../../file/xml-components";
const parseOptions = {
ignoreAttributes: false,
attributeNamePrefix: "",
attrNodeName: "_attr",
};
export class ExternalStylesFactory {
/**
* Creates new Style based on the given styles.
* Parses the styles and convert them to XmlComponent.
* Example content from styles.xml:
* <?xml version="1.0">
* <w:styles xmlns:mc="some schema" ...>
*
* <w:style w:type="paragraph" w:styleId="Heading1">
* <w:name w:val="heading 1"/>
* .....
* </w:style>
*
* <w:style w:type="paragraph" w:styleId="Heading2">
* <w:name w:val="heading 2"/>
* .....
* </w:style>
*
* <w:docDefaults>Or any other element will be parsed to</w:docDefaults>
*
* </w:styles>
* @param externalStyles context from styles.xml
*/
public newInstance(externalStyles: string): Styles {
const xmlStyles = fastXmlParser.parse(externalStyles, parseOptions)["w:styles"];
// create styles with attributes from the parsed xml
const importedStyle = new Styles(new ImportedRootElementAttributes(xmlStyles._attr));
// convert other elements (not styles definitions, but default styles and so on ...)
Object.keys(xmlStyles)
.filter((element) => element !== "_attr" && element !== "w:style")
.forEach((element) => {
importedStyle.push(new ImportedXmlComponent(element, xmlStyles[element]._attr));
});
// convert the styles one by one
xmlStyles["w:style"]
.map((style) => this.convertElement("w:style", style))
.forEach(importedStyle.push.bind(importedStyle));
return importedStyle;
}
convertElement(elementName: string, element: any): ImportedXmlComponent {
const xmlElement = new ImportedXmlComponent(elementName, element._attr);
if (typeof element === "object") {
Object.keys(element)
.filter((key) => key !== "_attr")
.map((item) => this.convertElement(item, element[item]))
.forEach(xmlElement.push.bind(xmlElement));
}
return xmlElement;
}
}

View File

@ -1,7 +1,8 @@
import { Color, Italics, Size } from "../paragraph/run/formatting";
import { Styles } from "./";
// import { DocumentDefaults } from "./defaults";
import { DocumentAttributes } from "../document/document-attributes";
import {
Heading1Style,
Heading2Style,
@ -15,7 +16,15 @@ import {
export class DefaultStylesFactory {
public newInstance(): Styles {
const styles = new Styles();
const documentAttributes = new DocumentAttributes({
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
Ignorable: "w14 w15",
});
const styles = new Styles(documentAttributes);
styles.createDocumentDefaults();
const titleStyle = new TitleStyle();

View File

@ -1,21 +1,13 @@
import { XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { XmlComponent, BaseXmlComponent } from "file/xml-components";
import { DocumentDefaults } from "./defaults";
import { ParagraphStyle } from "./style";
export class Styles extends XmlComponent {
constructor() {
constructor(_initialStyles?: BaseXmlComponent) {
super("w:styles");
this.root.push(
new DocumentAttributes({
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
Ignorable: "w14 w15",
}),
);
if (_initialStyles) {
this.root.push(_initialStyles);
}
}
public push(style: XmlComponent): Styles {

View File

@ -1 +1,2 @@
export * from "./table";
export * from './table-cell';

View File

@ -0,0 +1,181 @@
import { expect } from "chai";
import { TableCellBorders, BorderStyle, TableCellWidth, WidthType } from "./table-cell";
import { Formatter } from "../../export/formatter";
describe("TableCellBorders", () => {
describe("#prepForXml", () => {
it("should not add empty borders element if there are no borders defined", () => {
const tb = new TableCellBorders();
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal("");
});
});
describe("#addingBorders", () => {
it("should add top border", () => {
const tb = new TableCellBorders();
tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF");
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal({
"w:tcBorders": [
{
"w:top": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 1,
"w:val": "dotted",
},
},
],
},
],
});
});
it("should add start(left) border", () => {
const tb = new TableCellBorders();
tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF");
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal({
"w:tcBorders": [
{
"w:start": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 2,
"w:val": "single",
},
},
],
},
],
});
});
it("should add bottom border", () => {
const tb = new TableCellBorders();
tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF");
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal({
"w:tcBorders": [
{
"w:bottom": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 1,
"w:val": "double",
},
},
],
},
],
});
});
it("should add end(right) border", () => {
const tb = new TableCellBorders();
tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF");
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal({
"w:tcBorders": [
{
"w:end": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 3,
"w:val": "thick",
},
},
],
},
],
});
});
it("should add multiple borders", () => {
const tb = new TableCellBorders();
tb.addTopBorder(BorderStyle.DOTTED, 1, "FF00FF");
tb.addEndBorder(BorderStyle.THICK, 3, "FF00FF");
tb.addBottomBorder(BorderStyle.DOUBLE, 1, "FF00FF");
tb.addStartBorder(BorderStyle.SINGLE, 2, "FF00FF");
const tree = new Formatter().format(tb);
expect(tree).to.deep.equal({
"w:tcBorders": [
{
"w:top": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 1,
"w:val": "dotted",
},
},
],
},
{
"w:end": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 3,
"w:val": "thick",
},
},
],
},
{
"w:bottom": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 1,
"w:val": "double",
},
},
],
},
{
"w:start": [
{
_attr: {
"w:color": "FF00FF",
"w:sz": 2,
"w:val": "single",
},
},
],
},
],
});
});
});
});
describe("TableCellWidth", () => {
describe("#constructor", () => {
it("should create object", () => {
const tcWidth = new TableCellWidth(100, WidthType.DXA);
const tree = new Formatter().format(tcWidth);
expect(tree).to.deep.equal({
"w:tcW": [
{
"_attr": {
"w:type": "dxa",
"w:w": 100
}
}
]
});
});
});
});

View File

@ -0,0 +1,197 @@
import { XmlComponent, XmlAttributeComponent, IXmlableObject } from "file/xml-components";
export enum BorderStyle {
SINGLE = "single",
DASH_DOT_STROKED = "dashDotStroked",
DASHED = "dashed",
DASH_SMALL_GAP = "dashSmallGap",
DOT_DASH = "dotDash",
DOT_DOT_DASH = "dotDotDash",
DOTTED = "dotted",
DOUBLE = "double",
DOUBLE_WAVE = "doubleWave",
INSET = "inset",
NIL = "nil",
NONE = "none",
OUTSET = "outset",
THICK = "thick",
THICK_THIN_LARGE_GAP = "thickThinLargeGap",
THICK_THIN_MEDIUM_GAP = "thickThinMediumGap",
THICK_THIN_SMALL_GAP = "thickThinSmallGap",
THIN_THICK_LARGE_GAP = "thinThickLargeGap",
THIN_THICK_MEDIUM_GAP = "thinThickMediumGap",
THIN_THICK_SMALL_GAP = "thinThickSmallGap",
THIN_THICK_THIN_LARGE_GAP = "thinThickThinLargeGap",
THIN_THICK_THIN_MEDIUM_GAP = "thinThickThinMediumGap",
THIN_THICK_THIN_SMALL_GAP = "thinThickThinSmallGap",
THREE_D_EMBOSS = "threeDEmboss",
THREE_D_ENGRAVE = "threeDEngrave",
TRIPLE = "triple",
WAVE = "wave",
}
interface ICellBorder {
style: BorderStyle;
size: number;
color: string;
}
class CellBorderAttributes extends XmlAttributeComponent<ICellBorder> {
protected xmlKeys = { style: "w:val", size: "w:sz", color: "w:color" };
}
class BaseTableCellBorder extends XmlComponent {
setProperties(style: BorderStyle, size: number, color: string) {
let attrs = new CellBorderAttributes({
style: style,
size: size,
color: color,
});
this.root.push(attrs);
}
}
export class TableCellBorders extends XmlComponent {
constructor() {
super("w:tcBorders");
}
public prepForXml(): IXmlableObject {
return this.root.length > 0 ? super.prepForXml() : "";
}
addTopBorder(style: BorderStyle, size: number, color: string) {
const top = new BaseTableCellBorder("w:top");
top.setProperties(style, size, color);
this.root.push(top);
}
addStartBorder(style: BorderStyle, size: number, color: string) {
const start = new BaseTableCellBorder("w:start");
start.setProperties(style, size, color);
this.root.push(start);
}
addBottomBorder(style: BorderStyle, size: number, color: string) {
const bottom = new BaseTableCellBorder("w:bottom");
bottom.setProperties(style, size, color);
this.root.push(bottom);
}
addEndBorder(style: BorderStyle, size: number, color: string) {
const end = new BaseTableCellBorder("w:end");
end.setProperties(style, size, color);
this.root.push(end);
}
}
/**
* Attributes fot the GridSpan element.
*/
class GridSpanAttributes extends XmlAttributeComponent<{ val: number }> {
protected xmlKeys = { val: "w:val" };
}
/**
* GridSpan element. Should be used in a table cell. Pass the number of columns that this cell need to span.
*/
export class GridSpan extends XmlComponent {
constructor(value: number) {
super("w:gridSpan");
this.root.push(
new GridSpanAttributes({
val: value,
}),
);
}
}
/**
* Vertical merge types.
*/
export enum VMergeType {
/**
* Cell that is merged with upper one.
*/
CONTINUE = "continue",
/**
* Cell that is starting the vertical merge.
*/
RESTART = "restart",
}
class VMergeAttributes extends XmlAttributeComponent<{ val: VMergeType }> {
protected xmlKeys = { val: "w:val" };
}
/**
* Vertical merge element. Should be used in a table cell.
*/
export class VMerge extends XmlComponent {
constructor(value: VMergeType) {
super("w:vMerge");
this.root.push(
new VMergeAttributes({
val: value,
}),
);
}
}
export enum VerticalAlign {
BOTTOM = "bottom",
CENTER = "center",
TOP = "top",
}
class VAlignAttributes extends XmlAttributeComponent<{ val: VerticalAlign }> {
protected xmlKeys = { val: "w:val" };
}
/**
* Vertical align element.
*/
export class VAlign extends XmlComponent {
constructor(value: VerticalAlign) {
super("w:vAlign");
this.root.push(
new VAlignAttributes({
val: value,
}),
);
}
}
export enum WidthType {
/** Auto. */
AUTO = "auto",
/** Value is in twentieths of a point */
DXA = "dxa",
/** No (empty) value. */
NIL = "nil",
/** Value is in percentage. */
PERCENTAGE = "pct",
}
class TableCellWidthAttributes extends XmlAttributeComponent<{ type: WidthType; width: string | number }> {
protected xmlKeys = { width: "w:w", type: "w:type" };
}
/**
* Table cell width element.
*/
export class TableCellWidth extends XmlComponent {
constructor(value: string | number, type: WidthType) {
super("w:tcW");
this.root.push(
new TableCellWidthAttributes({
width: value,
type: type,
}),
);
}
}

View File

@ -2,33 +2,39 @@ import { IXmlableObject, XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph";
import { TableGrid } from "./grid";
import { TableProperties, WidthTypes } from "./properties";
import { TableCellBorders, GridSpan, VMerge, VMergeType, VerticalAlign, VAlign, TableCellWidth, WidthType } from "file/table/table-cell";
export class Table extends XmlComponent {
private readonly properties: TableProperties;
private readonly rows: TableRow[];
private readonly grid: TableGrid;
constructor(rows: number, cols: number) {
constructor(rows: number, cols: number, colSizes?: number[]) {
super("w:tbl");
this.properties = new TableProperties();
this.root.push(this.properties);
this.properties.setBorder();
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);
if (colSizes && colSizes.length > 0) {
this.grid = new TableGrid(colSizes);
} else {
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.grid = new TableGrid(gridCols);
this.root.push(this.grid);
this.rows = [];
@ -112,10 +118,36 @@ export class TableCell extends XmlComponent {
this.addContent(para);
return para;
}
get cellProperties() {
return this.properties;
}
}
export class TableCellProperties extends XmlComponent {
private cellBorder: TableCellBorders;
constructor() {
super("w:tcPr");
this.cellBorder = new TableCellBorders();
this.root.push(this.cellBorder);
}
get borders() {
return this.cellBorder;
}
addGridSpan(cellSpan: number) {
this.root.push(new GridSpan(cellSpan));
}
addVerticalMerge(type: VMergeType) {
this.root.push(new VMerge(type));
}
setVerticalAlign(vAlignType: VerticalAlign) {
this.root.push(new VAlign(vAlignType));
}
setWidth(width: string | number, type: WidthType) {
this.root.push(new TableCellWidth(width, type));
}
}

View File

@ -2,10 +2,15 @@ import { IXmlableObject } from "./xmlable-object";
export abstract class BaseXmlComponent {
protected rootKey: string;
protected deleted: boolean = false;
constructor(rootKey: string) {
this.rootKey = rootKey;
}
public abstract prepForXml(): IXmlableObject;
get isDeleted() {
return this.deleted;
}
}

View File

@ -0,0 +1,34 @@
import { expect } from "chai";
import { ImportedXmlComponent } from "./";
describe("ImportedXmlComponent", () => {
let importedXmlComponent: ImportedXmlComponent;
beforeEach(() => {
const attributes = {
someAttr: "1",
otherAttr: "2",
};
importedXmlComponent = new ImportedXmlComponent("w:test", attributes);
importedXmlComponent.push(new ImportedXmlComponent("w:child"));
});
describe("#prepForXml()", () => {
it("should transform for xml", () => {
const converted = importedXmlComponent.prepForXml();
expect(converted).to.eql({
"w:test": [
{
_attr: {
someAttr: "1",
otherAttr: "2",
},
},
{
"w:child": [],
},
],
});
});
});
});

View File

@ -0,0 +1,71 @@
import { XmlComponent, IXmlableObject } from ".";
/**
* Represents imported xml component from xml file.
*/
export class ImportedXmlComponent extends XmlComponent {
private _attr: any;
constructor(rootKey: string, _attr?: any) {
super(rootKey);
if (_attr) {
this._attr = _attr;
}
}
/**
* Transforms the object so it can be converted to xml. Example:
* <w:someKey someAttr="1" otherAttr="11">
* <w:child childAttr="2">
* </w:child>
* </w:someKey>
* {
* 'w:someKey': [
* {
* _attr: {
* someAttr: "1",
* otherAttr: "11"
* }
* },
* {
* 'w:child': [
* {
* _attr: {
* childAttr: "2"
* }
* }
* ]
* }
* ]
* }
*/
prepForXml(): IXmlableObject {
const result = super.prepForXml();
if (!!this._attr) {
if (!Array.isArray(result[this.rootKey])) {
result[this.rootKey] = [result[this.rootKey]];
}
result[this.rootKey].unshift({ _attr: this._attr });
}
return result;
}
push(xmlComponent: XmlComponent) {
this.root.push(xmlComponent);
}
}
/**
* Used for the attributes of root element that is being imported.
*/
export class ImportedRootElementAttributes extends XmlComponent {
constructor(private _attr: any) {
super("");
}
public prepForXml(): IXmlableObject {
return {
_attr: this._attr,
};
}
}

View File

@ -1,4 +1,5 @@
export * from "./xml-component";
export * from "./attributes";
export * from "./default-attributes";
export * from './imported-xml-component';
export * from "./xmlable-object";

View File

@ -18,4 +18,15 @@ describe("XmlComponent", () => {
assert.equal(newJson.rootKey, "w:test");
});
});
describe("#prepForXml()", () => {
it("should skip deleted elements", () => {
const child = new TestComponent("w:test1");
child.delete();
xmlComponent.addChildElement(child);
const xml = xmlComponent.prepForXml();
assert.equal(xml['w:test'].length, 0);
});
});
});

View File

@ -12,6 +12,12 @@ export abstract class XmlComponent extends BaseXmlComponent {
public prepForXml(): IXmlableObject {
const children = this.root
.filter(c => {
if (c instanceof BaseXmlComponent) {
return !c.isDeleted;
}
return true;
})
.map((comp) => {
if (comp instanceof BaseXmlComponent) {
return comp.prepForXml();
@ -23,4 +29,12 @@ export abstract class XmlComponent extends BaseXmlComponent {
[this.rootKey]: children,
};
}
public addChildElement(child: XmlComponent | string) {
this.root.push(child);
}
public delete() {
this.deleted = true;
}
}