diff --git a/.cspell.json b/.cspell.json index 2395fa64d5..1a5972c29c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -47,7 +47,9 @@ "/xmlKeys = {[^}]+}/g", "/\\.to\\.deep\\.equal\\({[^)]+}\\)/g", "\\.to\\.include\\.members\\(\\[[^\\]]+]\\)", - "/new [a-zA-Z]+\\({[^£]+}\\)/g" + "/new [a-zA-Z]+\\({[^£]+}\\)/g", + "/ { + describe("#createHyperlinkClick", () => { + it("should create a Hyperlink Click component", () => { + const tree = new Formatter().format(createHyperlinkClick("1", false)); + + expect(tree).to.deep.equal({ + "a:hlinkClick": { + _attr: { + "r:id": "rId1", + }, + }, + }); + }); + + it("should create a Hyperlink Click component with xmlns:a", () => { + const tree = new Formatter().format(createHyperlinkClick("1", true)); + + expect(tree).to.deep.equal({ + "a:hlinkClick": { + _attr: { + "r:id": "rId1", + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + }); + }); + }); + + describe("#createHyperlinkHover", () => { + it("should create a Hyperlink Hover component", () => { + const tree = new Formatter().format(createHyperlinkHover("1", false)); + + expect(tree).to.deep.equal({ + "a:hlinkHover": { + _attr: { + "r:id": "rId1", + }, + }, + }); + }); + + it("should create a Hyperlink Hover component with xmlns:a", () => { + const tree = new Formatter().format(createHyperlinkHover("1", true)); + + expect(tree).to.deep.equal({ + "a:hlinkHover": { + _attr: { + "r:id": "rId1", + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + }); + }); + }); +}); diff --git a/src/file/drawing/doc-properties/doc-properties-children.ts b/src/file/drawing/doc-properties/doc-properties-children.ts new file mode 100644 index 0000000000..0809f581e5 --- /dev/null +++ b/src/file/drawing/doc-properties/doc-properties-children.ts @@ -0,0 +1,57 @@ +// +// +// +// +// + +import { BuilderElement, XmlComponent } from "@file/xml-components"; + +// +// +// +// +// +// +// +// +// + +// TODO: Implement the rest of the attributes + +export const createHyperlinkClick = (linkId: string, hasXmlNs: boolean): XmlComponent => + new BuilderElement({ + name: "a:hlinkClick", + attributes: { + ...(hasXmlNs + ? { + xmlns: { + key: "xmlns:a", + value: "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + } + : {}), + id: { + key: "r:id", + value: `rId${linkId}`, + }, + }, + }); + +export const createHyperlinkHover = (linkId: string, hasXmlNs: boolean): XmlComponent => + new BuilderElement({ + name: "a:hlinkHover", + attributes: { + ...(hasXmlNs + ? { + xmlns: { + key: "xmlns:a", + value: "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + } + : {}), + id: { + key: "r:id", + value: `rId${linkId}`, + }, + }, + }); diff --git a/src/file/drawing/doc-properties/doc-properties.ts b/src/file/drawing/doc-properties/doc-properties.ts index 0724cd9513..ab5145cd5d 100644 --- a/src/file/drawing/doc-properties/doc-properties.ts +++ b/src/file/drawing/doc-properties/doc-properties.ts @@ -1,19 +1,22 @@ -import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; +// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_docPr_topic_ID0ES32OB.html +import { IContext, IXmlableObject, NextAttributeComponent, XmlComponent } from "@file/xml-components"; +import { ConcreteHyperlink } from "@file/paragraph"; + import { uniqueNumericId } from "@util/convenience-functions"; -class DocPropertiesAttributes extends XmlAttributeComponent<{ - readonly id?: number; - readonly name?: string; - readonly description?: string; - readonly title?: string; -}> { - protected readonly xmlKeys = { - id: "id", - name: "name", - description: "descr", - title: "title", - }; -} +import { createHyperlinkClick } from "./doc-properties-children"; + +// +// +// +// +// +// +// +// +// +// +// export interface DocPropertiesOptions { readonly name: string; @@ -26,12 +29,39 @@ export class DocProperties extends XmlComponent { super("wp:docPr"); this.root.push( - new DocPropertiesAttributes({ - id: uniqueNumericId(), - name, - description, - title, + new NextAttributeComponent({ + id: { + key: "id", + value: uniqueNumericId(), + }, + name: { + key: "name", + value: name, + }, + description: { + key: "descr", + value: description, + }, + title: { + key: "title", + value: title, + }, }), ); } + + public prepForXml(context: IContext): IXmlableObject | undefined { + for (let i = context.stack.length - 1; i >= 0; i--) { + const element = context.stack[i]; + if (!(element instanceof ConcreteHyperlink)) { + continue; + } + + this.root.push(createHyperlinkClick(element.linkId, true)); + + break; + } + + return super.prepForXml(context); + } } diff --git a/src/file/drawing/drawing.spec.ts b/src/file/drawing/drawing.spec.ts index 4b01e84a81..d445a01055 100644 --- a/src/file/drawing/drawing.spec.ts +++ b/src/file/drawing/drawing.spec.ts @@ -1,9 +1,11 @@ import { expect } from "chai"; import { SinonStub, stub } from "sinon"; +import { IContext } from "@file/xml-components"; import { Formatter } from "@export/formatter"; import * as convenienceFunctions from "@util/convenience-functions"; +import { ConcreteHyperlink, TextRun } from "../"; import { Drawing, IDrawingOptions } from "./drawing"; const imageBase64Data = `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`; @@ -450,5 +452,257 @@ describe("Drawing", () => { ], }); }); + + it("should create a drawing with a hyperlink", () => { + currentBreak = createDrawing({ + floating: { + horizontalPosition: { + offset: 0, + }, + verticalPosition: { + offset: 0, + }, + }, + }); + const tree = new Formatter().format(currentBreak, { + stack: [new ConcreteHyperlink([new TextRun("Test")], "1")], + } as unknown as IContext); + expect(tree).to.deep.equal({ + "w:drawing": [ + { + "wp:anchor": [ + { + _attr: { + allowOverlap: "1", + behindDoc: "0", + distB: 0, + distL: 0, + distR: 0, + distT: 0, + layoutInCell: "1", + locked: "0", + relativeHeight: 952500, + simplePos: "0", + }, + }, + { + "wp:simplePos": { + _attr: { + x: 0, + y: 0, + }, + }, + }, + { + "wp:positionH": [ + { + _attr: { + relativeFrom: "page", + }, + }, + { + "wp:posOffset": ["0"], + }, + ], + }, + { + "wp:positionV": [ + { + _attr: { + relativeFrom: "page", + }, + }, + { + "wp:posOffset": ["0"], + }, + ], + }, + { + "wp:extent": { + _attr: { + cx: 952500, + cy: 952500, + }, + }, + }, + { + "wp:effectExtent": { + _attr: { + b: 0, + l: 0, + r: 0, + t: 0, + }, + }, + }, + { + "wp:wrapNone": {}, + }, + { + "wp:docPr": [ + { + _attr: { + descr: "", + id: 0, + name: "", + title: "", + }, + }, + { + "a:hlinkClick": { + _attr: { + "r:id": "rId1", + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + }, + ], + }, + { + "wp:cNvGraphicFramePr": [ + { + "a:graphicFrameLocks": { + _attr: { + // tslint:disable-next-line:object-literal-key-quotes + noChangeAspect: 1, + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + }, + ], + }, + { + "a:graphic": [ + { + _attr: { + "xmlns:a": "http://schemas.openxmlformats.org/drawingml/2006/main", + }, + }, + { + "a:graphicData": [ + { + _attr: { + uri: "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:pic": [ + { + _attr: { + "xmlns:pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + }, + }, + { + "pic:nvPicPr": [ + { + "pic:cNvPr": [ + { + _attr: { + descr: "", + id: 0, + name: "", + }, + }, + { + "a:hlinkClick": { + _attr: { + "r:id": "rId1", + }, + }, + }, + ], + }, + { + "pic:cNvPicPr": [ + { + "a:picLocks": { + _attr: { + noChangeArrowheads: 1, + noChangeAspect: 1, + }, + }, + }, + ], + }, + ], + }, + { + "pic:blipFill": [ + { + "a:blip": { + _attr: { + // tslint:disable-next-line:object-literal-key-quotes + cstate: "none", + "r:embed": "rId{test.jpg}", + }, + }, + }, + { + "a:srcRect": {}, + }, + { + "a:stretch": [ + { + "a:fillRect": {}, + }, + ], + }, + ], + }, + { + "pic:spPr": [ + { + _attr: { + bwMode: "auto", + }, + }, + { + "a:xfrm": [ + { + _attr: {}, + }, + { + "a:off": { + _attr: { + x: 0, + y: 0, + }, + }, + }, + { + "a:ext": { + _attr: { + cx: 952500, + cy: 952500, + }, + }, + }, + ], + }, + { + "a:prstGeom": [ + { + _attr: { + prst: "rect", + }, + }, + { + "a:avLst": {}, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }); + }); }); }); diff --git a/src/file/drawing/inline/graphic/graphic-data/pic/non-visual-pic-properties/non-visual-properties/non-visual-properties.ts b/src/file/drawing/inline/graphic/graphic-data/pic/non-visual-pic-properties/non-visual-properties/non-visual-properties.ts index 0b2b5a249c..31842356c0 100644 --- a/src/file/drawing/inline/graphic/graphic-data/pic/non-visual-pic-properties/non-visual-properties/non-visual-properties.ts +++ b/src/file/drawing/inline/graphic/graphic-data/pic/non-visual-pic-properties/non-visual-properties/non-visual-properties.ts @@ -1,6 +1,21 @@ -import { XmlComponent } from "@file/xml-components"; +import { IContext, IXmlableObject, XmlComponent } from "@file/xml-components"; +import { createHyperlinkClick } from "@file/drawing/doc-properties/doc-properties-children"; +import { ConcreteHyperlink } from "@file/paragraph"; + import { NonVisualPropertiesAttributes } from "./non-visual-properties-attributes"; +// +// +// +// +// +// +// +// +// +// +// + export class NonVisualProperties extends XmlComponent { public constructor() { super("pic:cNvPr"); @@ -13,4 +28,19 @@ export class NonVisualProperties extends XmlComponent { }), ); } + + public prepForXml(context: IContext): IXmlableObject | undefined { + for (let i = context.stack.length - 1; i >= 0; i--) { + const element = context.stack[i]; + if (!(element instanceof ConcreteHyperlink)) { + continue; + } + + this.root.push(createHyperlinkClick(element.linkId, false)); + + break; + } + + return super.prepForXml(context); + } } diff --git a/src/file/paragraph/paragraph.spec.ts b/src/file/paragraph/paragraph.spec.ts index c8b4135288..b74359de86 100644 --- a/src/file/paragraph/paragraph.spec.ts +++ b/src/file/paragraph/paragraph.spec.ts @@ -954,6 +954,7 @@ describe("Paragraph", () => { paragraph.prepForXml({ viewWrapper: viewWrapperMock, file: file, + stack: [], }); const tree = new Formatter().format(paragraph); expect(tree).to.deep.equal({ diff --git a/src/file/paragraph/properties.spec.ts b/src/file/paragraph/properties.spec.ts index 0890d65f5b..4c4fe41d5b 100644 --- a/src/file/paragraph/properties.spec.ts +++ b/src/file/paragraph/properties.spec.ts @@ -31,6 +31,7 @@ describe("ParagraphProperties", () => { } as File, // tslint:disable-next-line: no-object-literal-type-assertion viewWrapper: new DocumentWrapper({ background: {} }), + stack: [], }); expect(tree).to.deep.equal({ diff --git a/src/file/paragraph/run/image-run.spec.ts b/src/file/paragraph/run/image-run.spec.ts index 37bd72d753..edc4aa5910 100644 --- a/src/file/paragraph/run/image-run.spec.ts +++ b/src/file/paragraph/run/image-run.spec.ts @@ -47,6 +47,7 @@ describe("ImageRun", () => { }, } as unknown as File, viewWrapper: {} as unknown as IViewWrapper, + stack: [], }); expect(tree).to.deep.equal({ "w:r": [ @@ -298,6 +299,7 @@ describe("ImageRun", () => { }, } as unknown as File, viewWrapper: {} as unknown as IViewWrapper, + stack: [], }); expect(tree).to.deep.equal({ "w:r": [ @@ -552,6 +554,7 @@ describe("ImageRun", () => { }, } as unknown as File, viewWrapper: {} as unknown as IViewWrapper, + stack: [], }); expect(tree).to.deep.equal({ @@ -810,6 +813,7 @@ describe("ImageRun", () => { }, } as unknown as File, viewWrapper: {} as unknown as IViewWrapper, + stack: [], }); expect(tree).to.deep.equal({ diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts index 410ff07feb..4bd1043b90 100644 --- a/src/file/xml-components/base.ts +++ b/src/file/xml-components/base.ts @@ -5,6 +5,8 @@ import { IXmlableObject } from "./xmlable-object"; export interface IContext { readonly file: File; readonly viewWrapper: IViewWrapper; + // eslint-disable-next-line functional/prefer-readonly-type + readonly stack: IXmlableObject[]; } export abstract class BaseXmlComponent { diff --git a/src/file/xml-components/default-attributes.ts b/src/file/xml-components/default-attributes.ts index 4d1bbab4de..62d5cb60b4 100644 --- a/src/file/xml-components/default-attributes.ts +++ b/src/file/xml-components/default-attributes.ts @@ -34,7 +34,7 @@ export class NextAttributeComponent extends BaseXmlComp public prepForXml(_: IContext): IXmlableObject { const attrs = Object.values<{ readonly key: string; readonly value: string | boolean | number }>(this.root) - .filter(({ value }) => !!value) + .filter(({ value }) => value !== undefined) .reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {} as IXmlAttribute); return { _attr: attrs }; } diff --git a/src/file/xml-components/imported-xml-component.spec.ts b/src/file/xml-components/imported-xml-component.spec.ts index ae0440d662..d9a14227c2 100644 --- a/src/file/xml-components/imported-xml-component.spec.ts +++ b/src/file/xml-components/imported-xml-component.spec.ts @@ -62,7 +62,7 @@ describe("ImportedXmlComponent", () => { describe("#prepForXml()", () => { it("should transform for xml", () => { // tslint:disable-next-line: no-object-literal-type-assertion - const converted = importedXmlComponent.prepForXml({} as IContext); + const converted = importedXmlComponent.prepForXml({ stack: [] } as unknown as IContext); expect(converted).to.deep.equal({ "w:test": [ { diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index 878931224b..93bdbf19fb 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -12,7 +12,15 @@ export abstract class XmlComponent extends BaseXmlComponent { this.root = new Array(); } + // This method is called by the formatter to get the XML representation of this component. + // It is called recursively for all child components. + // It is a serializer to be used in the xml library. + // https://www.npmjs.com/package/xml + // Child components can override this method to customize the XML representation, or execute side effects. public prepForXml(context: IContext): IXmlableObject | undefined { + // Mutating the stack is required for performance reasons + // eslint-disable-next-line functional/immutable-data + context.stack.push(this); const children = this.root .map((comp) => { if (comp instanceof BaseXmlComponent) { @@ -21,6 +29,9 @@ export abstract class XmlComponent extends BaseXmlComponent { return comp; }) .filter((comp) => comp !== undefined); // Exclude undefined + + // eslint-disable-next-line functional/immutable-data + context.stack.pop(); // If we only have a single IXmlableObject in our children array and it // represents our attributes, use the object itself as our children to // avoid an unneeded XML close element. diff --git a/tsconfig.mjs.json b/tsconfig.mjs.json new file mode 100644 index 0000000000..192ad7f8a5 --- /dev/null +++ b/tsconfig.mjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "outDir": "dist/mjs", + "target": "esnext" + } +}