Merge pull request #70 from dolanmiu/feat/h4buli-update

H4 update
This commit is contained in:
Dolan
2018-05-16 19:51:40 +01:00
committed by GitHub
25 changed files with 923 additions and 40 deletions

File diff suppressed because one or more lines are too long

26
demo/demo13.js Normal file
View File

@ -0,0 +1,26 @@
// This example shows 3 styles
const fs = require('fs');
const docx = require('../build');
const styles = fs.readFileSync('./demo/assets/custom-styles.xml', 'utf-8');
const doc = new docx.Document({
title: 'Title',
externalStyles: styles
});
doc.createParagraph('Cool Heading Text').heading1();
let paragraph = new docx.Paragraph('This is a custom named style from the template "MyFancyStyle"');
paragraph.style('MyFancyStyle');
doc.addParagraph(paragraph);
doc.createParagraph('Some normal text')
doc.createParagraph('MyFancyStyle again').style('MyFancyStyle');
paragraph.style('MyFancyStyle');
doc.addParagraph(paragraph);
var exporter = new docx.LocalPacker(doc);
exporter.pack('My Document');
console.log('Document created successfully at project root!');

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { IMediaData } from "file/media";
import { AppProperties } from "./app-properties/app-properties"; import { AppProperties } from "./app-properties/app-properties";
import { ContentTypes } from "./content-types/content-types"; import { ContentTypes } from "./content-types/content-types";
import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CoreProperties, IPropertiesOptions } from "./core-properties";
@ -10,6 +11,7 @@ import { Numbering } from "./numbering";
import { Hyperlink, Paragraph, PictureRun } from "./paragraph"; import { Hyperlink, Paragraph, PictureRun } from "./paragraph";
import { Relationships } from "./relationships"; import { Relationships } from "./relationships";
import { Styles } from "./styles"; import { Styles } from "./styles";
import { ExternalStylesFactory } from "./styles/external-styles-factory";
import { DefaultStylesFactory } from "./styles/factory"; import { DefaultStylesFactory } from "./styles/factory";
import { Table } from "./table"; import { Table } from "./table";
@ -28,8 +30,6 @@ export class File {
constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) {
this.document = new Document(sectionPropertiesOptions); this.document = new Document(sectionPropertiesOptions);
const stylesFactory = new DefaultStylesFactory();
this.styles = stylesFactory.newInstance();
if (!options) { if (!options) {
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.coreProperties = new CoreProperties(options);
this.numbering = new Numbering(); this.numbering = new Numbering();
this.docRelationships = new Relationships(); this.docRelationships = new Relationships();
@ -111,6 +119,16 @@ export class File {
return this.document.createDrawing(mediaData); 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 createHyperlink(link: string, text?: string): Hyperlink { public createHyperlink(link: string, text?: string): Hyperlink {
text = text === undefined ? link : text; text = text === undefined ? link : text;
const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount); const hyperlink = new Hyperlink(text, this.docRelationships.RelationshipCount);

View File

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

View File

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

View File

@ -24,10 +24,34 @@ export class Media {
public addMedia(filePath: string, relationshipsCount: number): IMediaData { public addMedia(filePath: string, relationshipsCount: number): IMediaData {
const key = path.basename(filePath); const key = path.basename(filePath);
const dimensions = sizeOf(filePath); const dimensions = sizeOf(filePath);
return this.createMedia(key, relationshipsCount, dimensions, fs.createReadStream(filePath), filePath);
}
public addMediaWithData(fileName: string, data: Buffer, relationshipsCount: number, width?: number, height?: number): 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);
}
private createMedia(
key: string,
relationshipsCount: number,
dimensions: { width: number; height: number },
data: fs.ReadStream | Buffer,
filePath?: string,
): IMediaData {
const imageData = { const imageData = {
referenceId: this.map.size + relationshipsCount + 1, referenceId: this.map.size + relationshipsCount + 1,
stream: fs.createReadStream(filePath), stream: data,
path: filePath, path: filePath,
fileName: key, fileName: key,
dimensions: { dimensions: {

View File

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

View File

@ -1,12 +1,15 @@
import { XmlComponent } from "file/xml-components"; import { Indent } from "file/paragraph";
import { IXmlableObject, XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes"; import { DocumentAttributes } from "../document/document-attributes";
import { Indent } from "../paragraph/formatting";
import { AbstractNumbering } from "./abstract-numbering"; import { AbstractNumbering } from "./abstract-numbering";
import { Num } from "./num"; import { Num } from "./num";
export class Numbering extends XmlComponent { export class Numbering extends XmlComponent {
private nextId: number; private nextId: number;
private abstractNumbering: XmlComponent[] = [];
private concreteNumbering: XmlComponent[] = [];
constructor() { constructor() {
super("w:numbering"); super("w:numbering");
this.root.push( this.root.push(
@ -58,13 +61,19 @@ export class Numbering extends XmlComponent {
public createAbstractNumbering(): AbstractNumbering { public createAbstractNumbering(): AbstractNumbering {
const num = new AbstractNumbering(this.nextId++); const num = new AbstractNumbering(this.nextId++);
this.root.push(num); this.abstractNumbering.push(num);
return num; return num;
} }
public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num { public createConcreteNumbering(abstractNumbering: AbstractNumbering): Num {
const num = new Num(this.nextId++, abstractNumbering.id); const num = new Num(this.nextId++, abstractNumbering.id);
this.root.push(num); this.concreteNumbering.push(num);
return 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

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

View File

@ -0,0 +1,159 @@
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 * as fastXmlParser from "fast-xml-parser";
import { Styles } from "./";
import { ImportedRootElementAttributes, ImportedXmlComponent } 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;
}
// tslint:disable-next-line:no-any
public 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,7 @@
import { DocumentAttributes } from "../document/document-attributes";
import { Color, Italics, Size } from "../paragraph/run/formatting"; import { Color, Italics, Size } from "../paragraph/run/formatting";
import { Styles } from "./"; import { Styles } from "./";
// import { DocumentDefaults } from "./defaults";
import { import {
Heading1Style, Heading1Style,
Heading2Style, Heading2Style,
@ -16,7 +16,15 @@ import {
export class DefaultStylesFactory { export class DefaultStylesFactory {
public newInstance(): Styles { 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(); styles.createDocumentDefaults();
const titleStyle = new TitleStyle(); const titleStyle = new TitleStyle();

View File

@ -1,21 +1,13 @@
import { XmlComponent } from "file/xml-components"; import { BaseXmlComponent, XmlComponent } from "file/xml-components";
import { DocumentAttributes } from "../document/document-attributes";
import { DocumentDefaults } from "./defaults"; import { DocumentDefaults } from "./defaults";
import { ParagraphStyle } from "./style"; import { ParagraphStyle } from "./style";
export class Styles extends XmlComponent { export class Styles extends XmlComponent {
constructor() { constructor(initialStyles?: BaseXmlComponent) {
super("w:styles"); super("w:styles");
this.root.push( if (initialStyles) {
new DocumentAttributes({ this.root.push(initialStyles);
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",
}),
);
} }
public push(style: XmlComponent): Styles { public push(style: XmlComponent): Styles {

View File

@ -1 +1,2 @@
export * from "./table"; 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,207 @@
import { IXmlableObject, XmlAttributeComponent, XmlComponent } 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 {
public setProperties(style: BorderStyle, size: number, color: string): BaseTableCellBorder {
const attrs = new CellBorderAttributes({
style: style,
size: size,
color: color,
});
this.root.push(attrs);
return this;
}
}
export class TableCellBorders extends XmlComponent {
constructor() {
super("w:tcBorders");
}
public prepForXml(): IXmlableObject {
return this.root.length > 0 ? super.prepForXml() : "";
}
public addTopBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
const top = new BaseTableCellBorder("w:top");
top.setProperties(style, size, color);
this.root.push(top);
return this;
}
public addStartBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
const start = new BaseTableCellBorder("w:start");
start.setProperties(style, size, color);
this.root.push(start);
return this;
}
public addBottomBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
const bottom = new BaseTableCellBorder("w:bottom");
bottom.setProperties(style, size, color);
this.root.push(bottom);
return this;
}
public addEndBorder(style: BorderStyle, size: number, color: string): TableCellBorders {
const end = new BaseTableCellBorder("w:end");
end.setProperties(style, size, color);
this.root.push(end);
return this;
}
}
/**
* 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

@ -1,3 +1,4 @@
import { GridSpan, TableCellBorders, TableCellWidth, VAlign, VerticalAlign, VMerge, VMergeType, WidthType } from "file/table/table-cell";
import { IXmlableObject, XmlComponent } from "file/xml-components"; import { IXmlableObject, XmlComponent } from "file/xml-components";
import { Paragraph } from "../paragraph"; import { Paragraph } from "../paragraph";
import { TableGrid } from "./grid"; import { TableGrid } from "./grid";
@ -8,27 +9,32 @@ export class Table extends XmlComponent {
private readonly rows: TableRow[]; private readonly rows: TableRow[];
private readonly grid: TableGrid; private readonly grid: TableGrid;
constructor(rows: number, cols: number) { constructor(rows: number, cols: number, colSizes?: number[]) {
super("w:tbl"); super("w:tbl");
this.properties = new TableProperties(); this.properties = new TableProperties();
this.root.push(this.properties); this.root.push(this.properties);
this.properties.setBorder(); this.properties.setBorder();
const gridCols: number[] = []; if (colSizes && colSizes.length > 0) {
for (let i = 0; i < cols; i++) { this.grid = new TableGrid(colSizes);
/* } else {
0-width columns don't get rendered correctly, so we need const gridCols: number[] = [];
to give them some value. A reasonable default would be for (let i = 0; i < cols; i++) {
~6in / numCols, but if we do that it becomes very hard /*
to resize the table using setWidth, unless the layout 0-width columns don't get rendered correctly, so we need
algorithm is set to 'fixed'. Instead, the approach here to give them some value. A reasonable default would be
means even in 'auto' layout, setting a width on the ~6in / numCols, but if we do that it becomes very hard
table will make it look reasonable, as the layout to resize the table using setWidth, unless the layout
algorithm will expand columns to fit its content algorithm is set to 'fixed'. Instead, the approach here
*/ means even in 'auto' layout, setting a width on the
gridCols.push(1); 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.root.push(this.grid);
this.rows = []; this.rows = [];
@ -112,10 +118,45 @@ export class TableCell extends XmlComponent {
this.addContent(para); this.addContent(para);
return para; return para;
} }
get cellProperties(): TableCellProperties {
return this.properties;
}
} }
export class TableCellProperties extends XmlComponent { export class TableCellProperties extends XmlComponent {
private cellBorder: TableCellBorders;
constructor() { constructor() {
super("w:tcPr"); super("w:tcPr");
this.cellBorder = new TableCellBorders();
this.root.push(this.cellBorder);
}
get borders(): TableCellBorders {
return this.cellBorder;
}
public addGridSpan(cellSpan: number): TableCellProperties {
this.root.push(new GridSpan(cellSpan));
return this;
}
public addVerticalMerge(type: VMergeType): TableCellProperties {
this.root.push(new VMerge(type));
return this;
}
public setVerticalAlign(vAlignType: VerticalAlign): TableCellProperties {
this.root.push(new VAlign(vAlignType));
return this;
}
public setWidth(width: string | number, type: WidthType): TableCellProperties {
this.root.push(new TableCellWidth(width, type));
return this;
} }
} }

View File

@ -2,10 +2,15 @@ import { IXmlableObject } from "./xmlable-object";
export abstract class BaseXmlComponent { export abstract class BaseXmlComponent {
protected rootKey: string; protected rootKey: string;
protected deleted: boolean = false;
constructor(rootKey: string) { constructor(rootKey: string) {
this.rootKey = rootKey; this.rootKey = rootKey;
} }
public abstract prepForXml(): IXmlableObject; public abstract prepForXml(): IXmlableObject;
public get isDeleted(): boolean {
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,74 @@
// tslint:disable:no-any
// tslint:disable:variable-name
import { IXmlableObject, XmlComponent } 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"
* }
* }
* ]
* }
* ]
* }
*/
public 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;
}
public push(xmlComponent: XmlComponent): void {
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 "./xml-component";
export * from "./attributes"; export * from "./attributes";
export * from "./default-attributes"; export * from "./default-attributes";
export * from "./imported-xml-component";
export * from "./xmlable-object"; export * from "./xmlable-object";

View File

@ -18,4 +18,15 @@ describe("XmlComponent", () => {
assert.equal(newJson.rootKey, "w:test"); 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 { public prepForXml(): IXmlableObject {
const children = this.root const children = this.root
.filter((c) => {
if (c instanceof BaseXmlComponent) {
return !c.isDeleted;
}
return true;
})
.map((comp) => { .map((comp) => {
if (comp instanceof BaseXmlComponent) { if (comp instanceof BaseXmlComponent) {
return comp.prepForXml(); return comp.prepForXml();
@ -23,4 +29,15 @@ export abstract class XmlComponent extends BaseXmlComponent {
[this.rootKey]: children, [this.rootKey]: children,
}; };
} }
// TODO: Unused method
public addChildElement(child: XmlComponent | string): XmlComponent {
this.root.push(child);
return this;
}
public delete(): void {
this.deleted = true;
}
} }