diff --git a/src/file/drawing/anchor/anchor-attributes.ts b/src/file/drawing/anchor/anchor-attributes.ts new file mode 100644 index 0000000000..cfd8ad3144 --- /dev/null +++ b/src/file/drawing/anchor/anchor-attributes.ts @@ -0,0 +1,26 @@ +import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +export interface IAnchorAttributes extends IDistance { + allowOverlap?: "0" | "1"; + behindDoc?: "0" | "1"; + layoutInCell?: "0" | "1"; + locked?: "0" | "1"; + relativeHeight?: number; + simplePos?: "0" | "1"; +} + +export class AnchorAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + allowOverlap: "allowOverlap", + behindDoc: "behindDoc", + layoutInCell: "layoutInCell", + locked: "locked", + relativeHeight: "relativeHeight", + simplePos: "simplePos", + }; +} diff --git a/src/file/drawing/anchor/anchor.spec.ts b/src/file/drawing/anchor/anchor.spec.ts new file mode 100644 index 0000000000..58adefd174 --- /dev/null +++ b/src/file/drawing/anchor/anchor.spec.ts @@ -0,0 +1,118 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { IDrawingOptions, TextWrapStyle } from ".././"; +import { Anchor } from "./"; + +function createDrawing(drawingOptions: IDrawingOptions): Anchor { + return new Anchor( + 1, + { + pixels: { + x: 100, + y: 100, + }, + emus: { + x: 100 * 9525, + y: 100 * 9525, + }, + }, + drawingOptions, + ); +} + +describe("Anchor", () => { + let anchor: Anchor; + + describe("#constructor()", () => { + it("should create a Drawing with correct root key", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.rootKey, "wp:anchor"); + assert.equal(newJson.root.length, 10); + }); + + it("should create a Drawing with all default options", () => { + anchor = createDrawing({}); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + const anchorAttributes = newJson.root[0].root; + assert.include(anchorAttributes, { + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", + allowOverlap: "1", + behindDoc: "0", + locked: "0", + layoutInCell: "1", + relativeHeight: 952500, + }); + + // 1: simple pos + assert.equal(newJson.root[1].rootKey, "wp:simplePos"); + + // 2: horizontal position + const horizontalPosition = newJson.root[2]; + assert.equal(horizontalPosition.rootKey, "wp:positionH"); + assert.include(horizontalPosition.root[0].root, { + relativeFrom: "column", + }); + assert.equal(horizontalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(horizontalPosition.root[1].root[0], 0); + + // 3: vertical position + const verticalPosition = newJson.root[3]; + assert.equal(verticalPosition.rootKey, "wp:positionV"); + assert.include(verticalPosition.root[0].root, { + relativeFrom: "paragraph", + }); + assert.equal(verticalPosition.root[1].rootKey, "wp:posOffset"); + assert.include(verticalPosition.root[1].root[0], 0); + + // 4: extent + const extent = newJson.root[4]; + assert.equal(extent.rootKey, "wp:extent"); + assert.include(extent.root[0].root, { + cx: 952500, + cy: 952500, + }); + + // 5: effect extent + const effectExtent = newJson.root[5]; + assert.equal(effectExtent.rootKey, "wp:effectExtent"); + + // 6 text wrap: none + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapNone"); + + // 7: doc properties + const docProperties = newJson.root[7]; + assert.equal(docProperties.rootKey, "wp:docPr"); + + // 8: graphic frame properties + const graphicFrame = newJson.root[8]; + assert.equal(graphicFrame.rootKey, "wp:cNvGraphicFramePr"); + + // 9: graphic + const graphic = newJson.root[9]; + assert.equal(graphic.rootKey, "a:graphic"); + }); + + it("should create a Drawing with text wrapping", () => { + anchor = createDrawing({ + textWrapping: { + textWrapStyle: TextWrapStyle.SQUARE, + }, + }); + const newJson = Utility.jsonify(anchor); + assert.equal(newJson.root.length, 10); + + // 6 text wrap: square + const textWrap = newJson.root[6]; + assert.equal(textWrap.rootKey, "wp:wrapSquare"); + }); + }); +}); diff --git a/src/file/drawing/anchor/anchor.ts b/src/file/drawing/anchor/anchor.ts new file mode 100644 index 0000000000..7dbdfca0bc --- /dev/null +++ b/src/file/drawing/anchor/anchor.ts @@ -0,0 +1,88 @@ +// http://officeopenxml.com/drwPicFloating.php +import { IMediaDataDimensions } from "file/media"; +import { XmlComponent } from "file/xml-components"; +import { IDrawingOptions } from "../drawing"; +import { + HorizontalPosition, + HorizontalPositionRelativeFrom, + IFloating, + SimplePos, + VerticalPosition, + VerticalPositionRelativeFrom, +} from "../floating"; +import { Graphic } from "../inline/graphic"; +import { TextWrapStyle, WrapNone, WrapSquare, WrapTight, WrapTopAndBottom } from "../text-wrap"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { AnchorAttributes } from "./anchor-attributes"; + +const defaultOptions: IFloating = { + allowOverlap: true, + behindDocument: false, + lockAnchor: false, + layoutInCell: true, + verticalPosition: { + relative: VerticalPositionRelativeFrom.PARAGRAPH, + offset: 0, + }, + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.COLUMN, + offset: 0, + }, +}; + +export class Anchor extends XmlComponent { + constructor(referenceId: number, dimensions: IMediaDataDimensions, drawingOptions: IDrawingOptions) { + super("wp:anchor"); + + const floating = { + ...defaultOptions, + ...drawingOptions.floating, + }; + this.root.push( + new AnchorAttributes({ + distT: 0, + distB: 0, + distL: 0, + distR: 0, + simplePos: "0", // note: word doesn't fully support - so we use 0 + allowOverlap: floating.allowOverlap === true ? "1" : "0", + behindDoc: floating.behindDocument === true ? "1" : "0", + locked: floating.lockAnchor === true ? "1" : "0", + layoutInCell: floating.layoutInCell === true ? "1" : "0", + relativeHeight: dimensions.emus.y, + }), + ); + + this.root.push(new SimplePos()); + this.root.push(new HorizontalPosition(floating.horizontalPosition)); + this.root.push(new VerticalPosition(floating.verticalPosition)); + this.root.push(new Extent(dimensions.emus.x, dimensions.emus.y)); + this.root.push(new EffectExtent()); + + if (drawingOptions.textWrapping != null) { + switch (drawingOptions.textWrapping.textWrapStyle) { + case TextWrapStyle.SQUARE: + this.root.push(new WrapSquare(drawingOptions.textWrapping)); + break; + case TextWrapStyle.TIGHT: + this.root.push(new WrapTight(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.TOP_AND_BOTTOM: + this.root.push(new WrapTopAndBottom(drawingOptions.textWrapping.distanceFromText)); + break; + case TextWrapStyle.NONE: + default: + this.root.push(new WrapNone()); + } + } else { + this.root.push(new WrapNone()); + } + + this.root.push(new DocProperties()); + this.root.push(new GraphicFrameProperties()); + this.root.push(new Graphic(referenceId, dimensions.emus.x, dimensions.emus.y)); + } +} diff --git a/src/file/drawing/anchor/index.ts b/src/file/drawing/anchor/index.ts new file mode 100644 index 0000000000..57faf47fc0 --- /dev/null +++ b/src/file/drawing/anchor/index.ts @@ -0,0 +1,2 @@ +export * from "./anchor"; +export * from "./anchor-attributes"; diff --git a/src/file/drawing/inline/doc-properties/doc-properties-attributes.ts b/src/file/drawing/doc-properties/doc-properties-attributes.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties-attributes.ts rename to src/file/drawing/doc-properties/doc-properties-attributes.ts diff --git a/src/file/drawing/inline/doc-properties/doc-properties.ts b/src/file/drawing/doc-properties/doc-properties.ts similarity index 100% rename from src/file/drawing/inline/doc-properties/doc-properties.ts rename to src/file/drawing/doc-properties/doc-properties.ts diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index 9b113da0bf..cf3f926315 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -2,14 +2,12 @@ import { assert } from "chai"; import * as fs from "fs"; import { Utility } from "../../tests/utility"; -import { Drawing } from "./"; +import { Drawing, IDrawingOptions, PlacementPosition } from "./"; -describe("Drawing", () => { - let currentBreak: Drawing; - - beforeEach(() => { - const path = "./demo/images/image1.jpeg"; - currentBreak = new Drawing({ +function createDrawing(drawingOptions?: IDrawingOptions): Drawing { + const path = "./demo/images/image1.jpeg"; + return new Drawing( + { fileName: "test.jpg", referenceId: 1, stream: fs.createReadStream(path), @@ -24,14 +22,33 @@ describe("Drawing", () => { y: 100 * 9525, }, }, - }); - }); + }, + drawingOptions, + ); +} + +describe("Drawing", () => { + let currentBreak: Drawing; describe("#constructor()", () => { it("should create a Drawing with correct root key", () => { + currentBreak = createDrawing(); const newJson = Utility.jsonify(currentBreak); assert.equal(newJson.rootKey, "w:drawing"); - // console.log(JSON.stringify(newJson, null, 2)); + }); + + it("should create a drawing with inline element when there are no options passed", () => { + currentBreak = createDrawing(); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:inline"); + }); + + it("should create a drawing with anchor element when there options are passed", () => { + currentBreak = createDrawing({ + position: PlacementPosition.FLOATING, + }); + const newJson = Utility.jsonify(currentBreak); + assert.equal(newJson.root[0].rootKey, "wp:anchor"); }); }); }); diff --git a/src/file/drawing/drawing.ts b/src/file/drawing/drawing.ts index 61c93592df..4c3db93b3a 100644 --- a/src/file/drawing/drawing.ts +++ b/src/file/drawing/drawing.ts @@ -1,20 +1,53 @@ import { IMediaData } from "file/media"; import { XmlComponent } from "file/xml-components"; +import { Anchor } from "./anchor"; +import { IFloating } from "./floating"; import { Inline } from "./inline"; +import { ITextWrapping } from "./text-wrap"; + +export enum PlacementPosition { + INLINE, + FLOATING, +} + +export interface IDistance { + distT?: number; + distB?: number; + distL?: number; + distR?: number; +} + +export interface IDrawingOptions { + position?: PlacementPosition; + textWrapping?: ITextWrapping; + floating?: IFloating; +} + +const defaultDrawingOptions: IDrawingOptions = { + position: PlacementPosition.INLINE, +}; export class Drawing extends XmlComponent { private inline: Inline; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super("w:drawing"); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.inline = new Inline(imageData.referenceId, imageData.dimensions); + const mergedOptions = { + ...defaultDrawingOptions, + ...drawingOptions, + }; - this.root.push(this.inline); + if (mergedOptions.position === PlacementPosition.INLINE) { + this.inline = new Inline(imageData.referenceId, imageData.dimensions); + this.root.push(this.inline); + } else if (mergedOptions.position === PlacementPosition.FLOATING) { + this.root.push(new Anchor(imageData.referenceId, imageData.dimensions, mergedOptions)); + } } public scale(factorX: number, factorY: number): void { diff --git a/src/file/drawing/inline/effect-extent/effect-extent-attributes.ts b/src/file/drawing/effect-extent/effect-extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent-attributes.ts rename to src/file/drawing/effect-extent/effect-extent-attributes.ts diff --git a/src/file/drawing/inline/effect-extent/effect-extent.ts b/src/file/drawing/effect-extent/effect-extent.ts similarity index 100% rename from src/file/drawing/inline/effect-extent/effect-extent.ts rename to src/file/drawing/effect-extent/effect-extent.ts diff --git a/src/file/drawing/inline/extent/extent-attributes.ts b/src/file/drawing/extent/extent-attributes.ts similarity index 100% rename from src/file/drawing/inline/extent/extent-attributes.ts rename to src/file/drawing/extent/extent-attributes.ts diff --git a/src/file/drawing/inline/extent/extent.ts b/src/file/drawing/extent/extent.ts similarity index 100% rename from src/file/drawing/inline/extent/extent.ts rename to src/file/drawing/extent/extent.ts diff --git a/src/file/drawing/floating/align.spec.ts b/src/file/drawing/floating/align.spec.ts new file mode 100644 index 0000000000..5ec77d6fd0 --- /dev/null +++ b/src/file/drawing/floating/align.spec.ts @@ -0,0 +1,15 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign } from "."; +import { Utility } from "../../../tests/utility"; +import { Align } from "./align"; + +describe("Align", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new Align(VerticalPositionAlign.CENTER)); + assert.equal(newJson.rootKey, "wp:align"); + assert.include(newJson.root[0], VerticalPositionAlign.CENTER); + }); + }); +}); diff --git a/src/file/drawing/floating/align.ts b/src/file/drawing/floating/align.ts new file mode 100644 index 0000000000..2ffa4ac52b --- /dev/null +++ b/src/file/drawing/floating/align.ts @@ -0,0 +1,10 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; +import { HorizontalPositionAlign, VerticalPositionAlign } from "./floating-position"; + +export class Align extends XmlComponent { + constructor(value: HorizontalPositionAlign | VerticalPositionAlign) { + super("wp:align"); + this.root.push(value); + } +} diff --git a/src/file/drawing/floating/floating-position.ts b/src/file/drawing/floating/floating-position.ts new file mode 100644 index 0000000000..7039846bc7 --- /dev/null +++ b/src/file/drawing/floating/floating-position.ts @@ -0,0 +1,60 @@ +// http://officeopenxml.com/drwPicFloating-position.php + +export enum HorizontalPositionRelativeFrom { + CHARACTER = "character", + COLUMN = "column", + INSIDE_MARGIN = "insideMargin", + LEFT_MARGIN = "leftMargin", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + RIGHT_MARGIN = "rightMargin", +} + +export enum VerticalPositionRelativeFrom { + BOTTOM_MARGIN = "bottomMargin", + INSIDE_MARGIN = "insideMargin", + LINE = "line", + MARGIN = "margin", + OUTSIDE_MARGIN = "outsideMargin", + PAGE = "page", + PARAGRAPH = "paragraph", + TOP_MARGIN = "topMargin", +} + +export enum HorizontalPositionAlign { + CENTER = "center", + INSIDE = "inside", + LEFT = "left", + OUTSIDE = "outside", + RIGHT = "right", +} + +export enum VerticalPositionAlign { + BOTTOM = "bottom", + CENTER = "center", + INSIDE = "inside", + OUTSIDE = "outside", + TOP = "top", +} + +export interface IHorizontalPositionOptions { + relative: HorizontalPositionRelativeFrom; + align?: HorizontalPositionAlign; + offset?: number; +} + +export interface IVerticalPositionOptions { + relative: VerticalPositionRelativeFrom; + align?: VerticalPositionAlign; + offset?: number; +} + +export interface IFloating { + horizontalPosition: IHorizontalPositionOptions; + verticalPosition: IVerticalPositionOptions; + allowOverlap?: boolean; + lockAnchor?: boolean; + behindDocument?: boolean; + layoutInCell?: boolean; +} diff --git a/src/file/drawing/floating/horizontal-position.spec.ts b/src/file/drawing/floating/horizontal-position.spec.ts new file mode 100644 index 0000000000..1b139b47be --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { HorizontalPositionAlign, HorizontalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { HorizontalPosition } from "./horizontal-position"; + +describe("HorizontalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + align: HorizontalPositionAlign.CENTER, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "center"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new HorizontalPosition({ + relative: HorizontalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionH"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/horizontal-position.ts b/src/file/drawing/floating/horizontal-position.ts new file mode 100644 index 0000000000..f0725aa857 --- /dev/null +++ b/src/file/drawing/floating/horizontal-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { HorizontalPositionRelativeFrom, IHorizontalPositionOptions } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IHorizontalPositionAttributes { + relativeFrom: HorizontalPositionRelativeFrom; +} + +class HorizontalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class HorizontalPosition extends XmlComponent { + constructor(horizontalPosition: IHorizontalPositionOptions) { + super("wp:positionH"); + + this.root.push( + new HorizontalPositionAttributes({ + relativeFrom: horizontalPosition.relative, + }), + ); + + if (horizontalPosition.align) { + this.root.push(new Align(horizontalPosition.align)); + } else if (horizontalPosition.offset !== undefined) { + this.root.push(new PositionOffset(horizontalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/floating/index.ts b/src/file/drawing/floating/index.ts new file mode 100644 index 0000000000..80061d16e1 --- /dev/null +++ b/src/file/drawing/floating/index.ts @@ -0,0 +1,4 @@ +export * from "./floating-position"; +export * from "./simple-pos"; +export * from "./horizontal-position"; +export * from "./vertical-position"; diff --git a/src/file/drawing/floating/position-offset.spec.ts b/src/file/drawing/floating/position-offset.spec.ts new file mode 100644 index 0000000000..74aebaebc2 --- /dev/null +++ b/src/file/drawing/floating/position-offset.spec.ts @@ -0,0 +1,14 @@ +import { assert } from "chai"; + +import { Utility } from "../../../tests/utility"; +import { PositionOffset } from "./position-offset"; + +describe("PositionOffset", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new PositionOffset(50)); + assert.equal(newJson.rootKey, "wp:posOffset"); + assert.equal(newJson.root[0], 50); + }); + }); +}); diff --git a/src/file/drawing/floating/position-offset.ts b/src/file/drawing/floating/position-offset.ts new file mode 100644 index 0000000000..4d3aa96b07 --- /dev/null +++ b/src/file/drawing/floating/position-offset.ts @@ -0,0 +1,9 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlComponent } from "file/xml-components"; + +export class PositionOffset extends XmlComponent { + constructor(offsetValue: number) { + super("wp:posOffset"); + this.root.push(offsetValue.toString()); + } +} diff --git a/src/file/drawing/floating/simple-pos.spec.ts b/src/file/drawing/floating/simple-pos.spec.ts new file mode 100644 index 0000000000..a86739b7b0 --- /dev/null +++ b/src/file/drawing/floating/simple-pos.spec.ts @@ -0,0 +1,17 @@ +import { assert } from "chai"; + +import { SimplePos } from "./simple-pos"; +import { Utility } from "../../../tests/utility"; + +describe("SimplePos", () => { + describe("#constructor()", () => { + it("should create a element with correct root key", () => { + const newJson = Utility.jsonify(new SimplePos()); + assert.equal(newJson.rootKey, "wp:simplePos"); + assert.include(newJson.root[0].root, { + x: 0, + y: 0, + }); + }); + }); +}); diff --git a/src/file/drawing/floating/simple-pos.ts b/src/file/drawing/floating/simple-pos.ts new file mode 100644 index 0000000000..6330f6660a --- /dev/null +++ b/src/file/drawing/floating/simple-pos.ts @@ -0,0 +1,28 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; + +interface ISimplePosAttributes { + x: number; + y: number; +} + +class SimplePosAttributes extends XmlAttributeComponent { + protected xmlKeys = { + x: "x", + y: "y", + }; +} + +export class SimplePos extends XmlComponent { + constructor() { + super("wp:simplePos"); + + // NOTE: It's not fully supported in Microsoft Word, but this element is needed anyway + this.root.push( + new SimplePosAttributes({ + x: 0, + y: 0, + }), + ); + } +} diff --git a/src/file/drawing/floating/vertical-position.spec.ts b/src/file/drawing/floating/vertical-position.spec.ts new file mode 100644 index 0000000000..a9d7ed65f8 --- /dev/null +++ b/src/file/drawing/floating/vertical-position.spec.ts @@ -0,0 +1,41 @@ +import { assert } from "chai"; + +import { VerticalPositionAlign, VerticalPositionRelativeFrom } from "."; +import { Utility } from "../../../tests/utility"; +import { VerticalPosition } from "./vertical-position"; + +describe("VerticalPosition", () => { + describe("#constructor()", () => { + it("should create a element with position align", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + align: VerticalPositionAlign.INSIDE, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:align"); + assert.include(newJson.root[1].root, "inside"); + }); + + it("should create a element with offset", () => { + const newJson = Utility.jsonify( + new VerticalPosition({ + relative: VerticalPositionRelativeFrom.MARGIN, + offset: 40, + }), + ); + assert.equal(newJson.rootKey, "wp:positionV"); + assert.include(newJson.root[0].root, { + relativeFrom: "margin", + }); + + assert.equal(newJson.root[1].rootKey, "wp:posOffset"); + assert.include(newJson.root[1].root[0], 40); + }); + }); +}); diff --git a/src/file/drawing/floating/vertical-position.ts b/src/file/drawing/floating/vertical-position.ts new file mode 100644 index 0000000000..10b6d6028f --- /dev/null +++ b/src/file/drawing/floating/vertical-position.ts @@ -0,0 +1,35 @@ +// http://officeopenxml.com/drwPicFloating-position.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { Align } from "./align"; +import { IVerticalPositionOptions, VerticalPositionRelativeFrom } from "./floating-position"; +import { PositionOffset } from "./position-offset"; + +interface IVerticalPositionAttributes { + relativeFrom: VerticalPositionRelativeFrom; +} + +class VerticalPositionAttributes extends XmlAttributeComponent { + protected xmlKeys = { + relativeFrom: "relativeFrom", + }; +} + +export class VerticalPosition extends XmlComponent { + constructor(verticalPosition: IVerticalPositionOptions) { + super("wp:positionV"); + + this.root.push( + new VerticalPositionAttributes({ + relativeFrom: verticalPosition.relative, + }), + ); + + if (verticalPosition.align) { + this.root.push(new Align(verticalPosition.align)); + } else if (verticalPosition.offset !== undefined) { + this.root.push(new PositionOffset(verticalPosition.offset)); + } else { + throw new Error("There is no configuration provided for floating position (Align or offset)"); + } + } +} diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-lock-attributes.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts b/src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts rename to src/file/drawing/graphic-frame/graphic-frame-locks/graphic-frame-locks.ts diff --git a/src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts b/src/file/drawing/graphic-frame/graphic-frame-properties.ts similarity index 100% rename from src/file/drawing/inline/graphic-frame/graphic-frame-properties.ts rename to src/file/drawing/graphic-frame/graphic-frame-properties.ts diff --git a/src/file/drawing/index.ts b/src/file/drawing/index.ts index ba96e11de9..8a1a62a201 100644 --- a/src/file/drawing/index.ts +++ b/src/file/drawing/index.ts @@ -1 +1,3 @@ -export { Drawing } from "./drawing"; +export * from "./drawing"; +export * from "./text-wrap"; +export * from "./floating"; diff --git a/src/file/drawing/inline/inline-attributes.ts b/src/file/drawing/inline/inline-attributes.ts index 1a4ef74e3c..5f7489188c 100644 --- a/src/file/drawing/inline/inline-attributes.ts +++ b/src/file/drawing/inline/inline-attributes.ts @@ -1,11 +1,8 @@ import { XmlAttributeComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; -export interface IInlineAttributes { - distT?: number; - distB?: number; - distL?: number; - distR?: number; -} +// tslint:disable-next-line:no-empty-interface +export interface IInlineAttributes extends IDistance {} export class InlineAttributes extends XmlAttributeComponent { protected xmlKeys = { diff --git a/src/file/drawing/inline/inline.ts b/src/file/drawing/inline/inline.ts index 0205eb3090..6e5be2ba13 100644 --- a/src/file/drawing/inline/inline.ts +++ b/src/file/drawing/inline/inline.ts @@ -1,11 +1,11 @@ // http://officeopenxml.com/drwPicInline.php import { IMediaDataDimensions } from "file/media"; import { XmlComponent } from "file/xml-components"; -import { DocProperties } from "./doc-properties/doc-properties"; -import { EffectExtent } from "./effect-extent/effect-extent"; -import { Extent } from "./extent/extent"; -import { Graphic } from "./graphic"; -import { GraphicFrameProperties } from "./graphic-frame/graphic-frame-properties"; +import { DocProperties } from "./../doc-properties/doc-properties"; +import { EffectExtent } from "./../effect-extent/effect-extent"; +import { Extent } from "./../extent/extent"; +import { GraphicFrameProperties } from "./../graphic-frame/graphic-frame-properties"; +import { Graphic } from "./../inline/graphic"; import { InlineAttributes } from "./inline-attributes"; export class Inline extends XmlComponent { diff --git a/src/file/drawing/text-wrap/index.ts b/src/file/drawing/text-wrap/index.ts new file mode 100644 index 0000000000..ce8c0bbd13 --- /dev/null +++ b/src/file/drawing/text-wrap/index.ts @@ -0,0 +1,5 @@ +export * from "./text-wrapping"; +export * from "./wrap-none"; +export * from "./wrap-square"; +export * from "./wrap-tight"; +export * from "./wrap-top-and-bottom"; diff --git a/src/file/drawing/text-wrap/text-wrapping.ts b/src/file/drawing/text-wrap/text-wrapping.ts new file mode 100644 index 0000000000..7fc14a52fd --- /dev/null +++ b/src/file/drawing/text-wrap/text-wrapping.ts @@ -0,0 +1,22 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { IDistance } from "../drawing"; + +export enum TextWrapStyle { + NONE, + SQUARE, + TIGHT, + TOP_AND_BOTTOM, +} + +export enum WrapTextOption { + BOTH_SIDES = "bothSides", + LEFT = "left", + RIGHT = "right", + LARGEST = "largest", +} + +export interface ITextWrapping { + textWrapStyle: TextWrapStyle; + wrapTextOption?: WrapTextOption; + distanceFromText?: IDistance; +} diff --git a/src/file/drawing/text-wrap/wrap-none.ts b/src/file/drawing/text-wrap/wrap-none.ts new file mode 100644 index 0000000000..0ac4c632f0 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-none.ts @@ -0,0 +1,8 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlComponent } from "file/xml-components"; + +export class WrapNone extends XmlComponent { + constructor() { + super("wp:wrapNone"); + } +} diff --git a/src/file/drawing/text-wrap/wrap-square.ts b/src/file/drawing/text-wrap/wrap-square.ts new file mode 100644 index 0000000000..08ed108209 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-square.ts @@ -0,0 +1,31 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { ITextWrapping, WrapTextOption } from "."; +import { IDistance } from "../drawing"; + +interface IWrapSquareAttributes extends IDistance { + wrapText?: WrapTextOption; +} + +class WrapSquareAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + distL: "distL", + distR: "distR", + wrapText: "wrapText", + }; +} + +export class WrapSquare extends XmlComponent { + constructor(textWrapping: ITextWrapping) { + super("wp:wrapSquare"); + + this.root.push( + new WrapSquareAttributes({ + wrapText: textWrapping.wrapTextOption || WrapTextOption.BOTH_SIDES, + ...textWrapping.distanceFromText, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-tight.ts b/src/file/drawing/text-wrap/wrap-tight.ts new file mode 100644 index 0000000000..cda9a20194 --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-tight.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTightAttributes { + distT?: number; + distB?: number; +} + +class WrapTightAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTight extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTight"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTightAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/drawing/text-wrap/wrap-top-and-bottom.ts b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts new file mode 100644 index 0000000000..bf6a5c3cae --- /dev/null +++ b/src/file/drawing/text-wrap/wrap-top-and-bottom.ts @@ -0,0 +1,33 @@ +// http://officeopenxml.com/drwPicFloating-textWrap.php +import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { IDistance } from "../drawing"; + +interface IWrapTopAndBottomAttributes { + distT?: number; + distB?: number; +} + +class WrapTopAndBottomAttributes extends XmlAttributeComponent { + protected xmlKeys = { + distT: "distT", + distB: "distB", + }; +} + +export class WrapTopAndBottom extends XmlComponent { + constructor(distanceFromText?: IDistance) { + super("wp:wrapTopAndBottom"); + + distanceFromText = distanceFromText || { + distT: 0, + distB: 0, + }; + + this.root.push( + new WrapTopAndBottomAttributes({ + distT: distanceFromText.distT, + distB: distanceFromText.distB, + }), + ); + } +} diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index bb6efbe25a..eaf0b1c410 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -1,3 +1,4 @@ +import { XmlComponent } from "file/xml-components"; import { Footer } from "./footer/footer"; import { IMediaData, Media } from "./media"; import { Paragraph } from "./paragraph"; @@ -35,6 +36,10 @@ export class FooterWrapper { this.footer.addDrawing(imageData); } + public addChildElement(childElement: XmlComponent | string): void { + this.footer.addChildElement(childElement); + } + public createImage(image: string): void { const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount); this.relationships.createRelationship( diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 5a0c249b42..1d5cccd9cc 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -1,3 +1,4 @@ +import { XmlComponent } from "file/xml-components"; import { Header } from "./header/header"; import { IMediaData, Media } from "./media"; import { Paragraph } from "./paragraph"; @@ -85,6 +86,10 @@ export class HeaderWrapper { this.header.addDrawing(imageData); } + public addChildElement(childElement: XmlComponent | string): void { + this.header.addChildElement(childElement); + } + public createImage(image: string): void { const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount); this.relationships.createRelationship( diff --git a/src/file/paragraph/run/picture-run.ts b/src/file/paragraph/run/picture-run.ts index 1b256ea20d..6a07b3bd9e 100644 --- a/src/file/paragraph/run/picture-run.ts +++ b/src/file/paragraph/run/picture-run.ts @@ -1,18 +1,19 @@ import { Drawing } from "../../drawing"; +import { IDrawingOptions } from "../../drawing/drawing"; import { IMediaData } from "../../media/data"; import { Run } from "../run"; export class PictureRun extends Run { private drawing: Drawing; - constructor(imageData: IMediaData) { + constructor(imageData: IMediaData, drawingOptions?: IDrawingOptions) { super(); if (imageData === undefined) { throw new Error("imageData cannot be undefined"); } - this.drawing = new Drawing(imageData); + this.drawing = new Drawing(imageData, drawingOptions); this.root.push(this.drawing); } diff --git a/src/file/styles/external-styles-factory.ts b/src/file/styles/external-styles-factory.ts index f1bc9c68c6..2ebd6323bd 100644 --- a/src/file/styles/external-styles-factory.ts +++ b/src/file/styles/external-styles-factory.ts @@ -1,13 +1,6 @@ import * as fastXmlParser from "fast-xml-parser"; - +import { convertToXmlComponent, ImportedRootElementAttributes, ImportedXmlComponent, parseOptions } from "file/xml-components"; import { Styles } from "./"; -import { ImportedRootElementAttributes, ImportedXmlComponent } from "./../../file/xml-components"; - -const parseOptions = { - ignoreAttributes: false, - attributeNamePrefix: "", - attrNodeName: "_attr", -}; export class ExternalStylesFactory { /** @@ -45,20 +38,7 @@ export class ExternalStylesFactory { }); // convert the styles one by one - xmlStyles["w:style"].map((style) => this.convertElement("w:style", style)).forEach(importedStyle.push.bind(importedStyle)); - + xmlStyles["w:style"].map((style) => convertToXmlComponent("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; - } } diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts index d7b638ba9f..e941d3f0ec 100644 --- a/src/file/xml-components/imported-xml-component.spec.ts +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -1,5 +1,52 @@ import { expect } from "chai"; -import { ImportedXmlComponent } from "./"; +import { ImportedXmlComponent, convertToXmlComponent } from "./"; + +const xmlString = ` + + + some value + + + Text 1 + + + Text 2 + + + `; + +const importedXmlElement = { + "w:p": { + _attr: { "w:one": "value 1", "w:two": "value 2" }, + "w:rPr": { "w:noProof": "some value" }, + "w:r": [{ _attr: { active: "true" }, "w:t": "Text 1" }, { _attr: { active: "true" }, "w:t": "Text 2" }], + }, +}; + +const convertedXmlElement = { + deleted: false, + rootKey: "w:p", + root: [ + { + deleted: false, + rootKey: "w:rPr", + root: [{ deleted: false, rootKey: "w:noProof", root: ["some value"] }], + }, + { + deleted: false, + rootKey: "w:r", + root: [{ deleted: false, rootKey: "w:t", root: ["Text 1"] }], + _attr: { active: "true" }, + }, + { + deleted: false, + rootKey: "w:r", + root: [{ deleted: false, rootKey: "w:t", root: ["Text 2"] }], + _attr: { active: "true" }, + }, + ], + _attr: { "w:one": "value 1", "w:two": "value 2" }, +}; describe("ImportedXmlComponent", () => { let importedXmlComponent: ImportedXmlComponent; @@ -31,4 +78,16 @@ describe("ImportedXmlComponent", () => { }); }); }); + + it("should create XmlComponent from xml string", () => { + const converted = ImportedXmlComponent.fromXmlString(xmlString); + expect(converted).to.eql(convertedXmlElement); + }); + + describe("convertToXmlComponent", () => { + it("should convert to xml component", () => { + const converted = convertToXmlComponent("w:p", importedXmlElement["w:p"]); + expect(converted).to.eql(convertedXmlElement); + }); + }); }); diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts index 853c84b462..23c4137900 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -1,6 +1,54 @@ -// tslint:disable:no-any -// tslint:disable:variable-name -import { IXmlableObject, XmlComponent } from "./"; +/* tslint:disable */ +import { XmlComponent, IXmlableObject } from "."; +import * as fastXmlParser from "fast-xml-parser"; +import { flatMap } from "lodash"; + +export const parseOptions = { + ignoreAttributes: false, + attributeNamePrefix: "", + attrNodeName: "_attr", +}; + +/** + * Converts the given xml element (in json format) into XmlComponent. + * Note: If element is array, them it will return ImportedXmlComponent[]. Example for given: + * element = [ + * { w:t: "val 1"}, + * { w:t: "val 2"} + * ] + * will return + * [ + * ImportedXmlComponent { rootKey: "w:t", root: [ "val 1" ]}, + * ImportedXmlComponent { rootKey: "w:t", root: [ "val 2" ]} + * ] + * + * @param elementName name (rootKey) of the XmlComponent + * @param element the xml element in json presentation + */ +export function convertToXmlComponent(elementName: string, element: any): ImportedXmlComponent | ImportedXmlComponent[] { + const xmlElement = new ImportedXmlComponent(elementName, element._attr); + if (Array.isArray(element)) { + const out: any[] = []; + element.forEach((itemInArray) => { + out.push(convertToXmlComponent(elementName, itemInArray)); + }); + return flatMap(out); + } else if (typeof element === "object") { + Object.keys(element) + .filter((key) => key !== "_attr") + .map((item) => convertToXmlComponent(item, element[item])) + .forEach((converted) => { + if (Array.isArray(converted)) { + converted.forEach(xmlElement.push.bind(xmlElement)); + } else { + xmlElement.push(converted); + } + }); + } else if (element !== "") { + xmlElement.push(element); + } + return xmlElement; +} /** * Represents imported xml component from xml file. @@ -8,11 +56,10 @@ import { IXmlableObject, XmlComponent } from "./"; export class ImportedXmlComponent extends XmlComponent { private _attr: any; - constructor(rootKey: string, attr?: any) { + constructor(rootKey: string, _attr?: any) { super(rootKey); - - if (attr) { - this._attr = attr; + if (_attr) { + this._attr = _attr; } } @@ -42,7 +89,7 @@ export class ImportedXmlComponent extends XmlComponent { * ] * } */ - public prepForXml(): IXmlableObject { + prepForXml(): IXmlableObject { const result = super.prepForXml(); if (!!this._attr) { if (!Array.isArray(result[this.rootKey])) { @@ -53,9 +100,26 @@ export class ImportedXmlComponent extends XmlComponent { return result; } - public push(xmlComponent: XmlComponent): void { + push(xmlComponent: XmlComponent) { this.root.push(xmlComponent); } + + /** + * Converts the xml string to a XmlComponent tree. + * + * @param importedContent xml content of the imported component + */ + static fromXmlString(importedContent: string): ImportedXmlComponent { + const imported = fastXmlParser.parse(importedContent, parseOptions); + const elementName = Object.keys(imported)[0]; + + const converted = convertToXmlComponent(elementName, imported[elementName]); + + if (Array.isArray(converted) && converted.length > 1) { + throw new Error("Invalid conversion, input must be one element."); + } + return Array.isArray(converted) ? converted[0] : converted; + } } /**