diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..2488a642fd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "option": "watch", + "problemMatcher": [ + "$tsc-watch" + ] + }, + { + "type": "npm", + "script": "ts-node", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/demo/demo16.ts b/demo/demo16.ts index 4b92dc0d7e..6c75218e53 100644 --- a/demo/demo16.ts +++ b/demo/demo16.ts @@ -15,8 +15,12 @@ const footer = doc.createFooter(); footer.createParagraph("Footer on another page"); doc.addSection({ - headerId: header.Header.ReferenceId, - footerId: footer.Footer.ReferenceId, + headers: { + default: header, + }, + footers: { + default: footer, + }, pageNumberStart: 1, pageNumberFormatType: PageNumberFormat.DECIMAL, }); @@ -24,8 +28,12 @@ doc.addSection({ doc.createParagraph("hello"); doc.addSection({ - headerId: header.Header.ReferenceId, - footerId: footer.Footer.ReferenceId, + headers: { + default: header, + }, + footers: { + default: footer, + }, pageNumberStart: 1, pageNumberFormatType: PageNumberFormat.DECIMAL, orientation: PageOrientation.LANDSCAPE, diff --git a/demo/demo23.ts b/demo/demo23.ts index 47ee26a6f9..2e230e0cb7 100644 --- a/demo/demo23.ts +++ b/demo/demo23.ts @@ -8,11 +8,11 @@ const doc = new Document(); const paragraph = new Paragraph("Hello World"); doc.addParagraph(paragraph); -const image = Media.addImage(doc, "./demo/images/image1.jpeg"); -const image2 = Media.addImage(doc, "./demo/images/dog.png"); -const image3 = Media.addImage(doc, "./demo/images/cat.jpg"); -const image4 = Media.addImage(doc, "./demo/images/parrots.bmp"); -const image5 = Media.addImage(doc, "./demo/images/pizza.gif"); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); +const image2 = Media.addImage(doc, fs.readFileSync("./demo/images/dog.png")); +const image3 = Media.addImage(doc, fs.readFileSync("./demo/images/cat.jpg")); +const image4 = Media.addImage(doc, fs.readFileSync("./demo/images/parrots.bmp")); +const image5 = Media.addImage(doc, fs.readFileSync("./demo/images/pizza.gif")); const imageBase64Data = `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`; const image6 = Media.addImage(doc, Buffer.from(imageBase64Data, "base64"), 100, 100); diff --git a/demo/demo24.ts b/demo/demo24.ts index 23828aec65..4901477ced 100644 --- a/demo/demo24.ts +++ b/demo/demo24.ts @@ -8,7 +8,7 @@ const doc = new Document(); const table = doc.createTable(4, 4); table.getCell(2, 2).addContent(new Paragraph("Hello")); -const image = Media.addImage(doc, "./demo/images/image1.jpeg"); +const image = Media.addImage(doc, fs.readFileSync("./demo/images/image1.jpeg")); table.getCell(1, 1).addContent(image.Paragraph); const packer = new Packer(); diff --git a/demo/demo27.ts b/demo/demo27.ts index e04fc07e02..a233cd9afd 100644 --- a/demo/demo27.ts +++ b/demo/demo27.ts @@ -30,5 +30,4 @@ const packer = new Packer(); packer.toBuffer(doc).then((buffer) => { fs.writeFileSync("My Document.docx", buffer); - console.log("Document created successfully at project root!"); }); diff --git a/demo/demo30.ts b/demo/demo30.ts new file mode 100644 index 0000000000..a587e42b7a --- /dev/null +++ b/demo/demo30.ts @@ -0,0 +1,29 @@ +import * as fs from "fs"; +import { Document, ImportDotx, Packer, Paragraph } from "../build"; + +const importDotx = new ImportDotx(); +const filePath = "./demo/dotx/template.dotx"; + +fs.readFile(filePath, (err, data) => { + if (err) { + throw new Error(`Failed to read file ${filePath}.`); + } + + importDotx.extract(data).then((templateDocument) => { + // This any needs fixing + const sectionProps = { + titlePage: templateDocument.titlePageIsDefined, + } as any; + + const doc = new Document(undefined, sectionProps, { + template: templateDocument, + }); + const paragraph = new Paragraph("Hello World"); + doc.addParagraph(paragraph); + + const packer = new Packer(); + packer.toBuffer(doc).then((buffer) => { + fs.writeFileSync("My Document.docx", buffer); + }); + }); +}); diff --git a/demo/dotx/template.dotx b/demo/dotx/template.dotx new file mode 100644 index 0000000000..c29318ad76 Binary files /dev/null and b/demo/dotx/template.dotx differ diff --git a/package.json b/package.json index ee9fa9049a..802c2bfb62 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "types": "./build/index.d.ts", "dependencies": { "@types/image-size": "0.0.29", - "@types/jszip": "^3.1.3", + "@types/jszip": "^3.1.4", "fast-xml-parser": "^3.3.6", "image-size": "^0.6.2", "jszip": "^3.1.5", diff --git a/src/export/formatter.ts b/src/export/formatter.ts index 199c32185f..0a28070d13 100644 --- a/src/export/formatter.ts +++ b/src/export/formatter.ts @@ -2,6 +2,12 @@ import { BaseXmlComponent, IXmlableObject } from "file/xml-components"; export class Formatter { public format(input: BaseXmlComponent): IXmlableObject { - return input.prepForXml(); + const output = input.prepForXml(); + + if (output) { + return output; + } else { + throw Error("XMLComponent did not format correctly"); + } } } diff --git a/src/export/packer/next-compiler.ts b/src/export/packer/next-compiler.ts index 1ee02bf589..f04869cd9e 100644 --- a/src/export/packer/next-compiler.ts +++ b/src/export/packer/next-compiler.ts @@ -59,6 +59,18 @@ export class Compiler { zip.file(`word/media/${data.fileName}`, mediaData); } + for (const header of file.Headers) { + for (const data of header.Media.Array) { + zip.file(`word/media/${data.fileName}`, data.stream); + } + } + + for (const footer of file.Footers) { + for (const data of footer.Media.Array) { + zip.file(`word/media/${data.fileName}`, data.stream); + } + } + return zip; } @@ -128,4 +140,13 @@ export class Compiler { }, }; } + + /* By default docx collapse empty tags. -> . this function mimic it + so comparing (diff) original docx file and the library output is easier + Currently not used, so commenting out */ + // private collapseEmptyTags(xmlData: string): string { + // const regEx = /<(([^ <>]+)[^<>]*)><\/\2>/g; + // const collapsed = xmlData.replace(regEx, "<$1/>"); + // return collapsed; + // } } diff --git a/src/file/document/body/body.spec.ts b/src/file/document/body/body.spec.ts index 31c1e49e32..67b9d9ab21 100644 --- a/src/file/document/body/body.spec.ts +++ b/src/file/document/body/body.spec.ts @@ -16,7 +16,7 @@ describe("Body", () => { expect(formatted) .to.have.property("w:sectPr") .and.to.be.an.instanceof(Array); - expect(formatted["w:sectPr"]).to.have.length(7); + expect(formatted["w:sectPr"]).to.have.length(5); }); }); diff --git a/src/file/document/body/body.ts b/src/file/document/body/body.ts index 941a913bf9..1d19669588 100644 --- a/src/file/document/body/body.ts +++ b/src/file/document/body/body.ts @@ -35,7 +35,7 @@ export class Body extends XmlComponent { this.sections.push(new SectionProperties(params)); } } - public prepForXml(): IXmlableObject { + public prepForXml(): IXmlableObject | undefined { if (this.sections.length === 1) { this.root.push(this.sections[0]); } else if (this.sections.length > 1) { diff --git a/src/file/document/body/section-properties/page-border/page-borders.spec.ts b/src/file/document/body/section-properties/page-border/page-borders.spec.ts index 66abb9cc66..0bf22633ee 100644 --- a/src/file/document/body/section-properties/page-border/page-borders.spec.ts +++ b/src/file/document/body/section-properties/page-border/page-borders.spec.ts @@ -8,9 +8,7 @@ describe("PageBorders", () => { describe("#constructor()", () => { it("should create empty element when no options are passed", () => { const properties = new PageBorders(); - const tree = new Formatter().format(properties); - - expect(tree).to.equal(""); + expect(() => new Formatter().format(properties)).to.throw(); }); it("should create page borders with some configuration", () => { diff --git a/src/file/document/body/section-properties/page-border/page-borders.ts b/src/file/document/body/section-properties/page-border/page-borders.ts index 0af96cfd41..4017fbb7ae 100644 --- a/src/file/document/body/section-properties/page-border/page-borders.ts +++ b/src/file/document/body/section-properties/page-border/page-borders.ts @@ -98,7 +98,9 @@ export class PageBorders extends XmlComponent { } } - public prepForXml(): IXmlableObject { - return this.root.length > 0 ? super.prepForXml() : ""; + public prepForXml(): IXmlableObject | undefined { + if (this.root.length > 0) { + return super.prepForXml(); + } } } diff --git a/src/file/document/body/section-properties/section-properties.spec.ts b/src/file/document/body/section-properties/section-properties.spec.ts index 9a8645e5b0..8e422f425c 100644 --- a/src/file/document/body/section-properties/section-properties.spec.ts +++ b/src/file/document/body/section-properties/section-properties.spec.ts @@ -1,12 +1,17 @@ import { expect } from "chai"; import { Formatter } from "../../../../export/formatter"; -import { FooterReferenceType, PageBorderOffsetFrom, PageNumberFormat } from "./"; +import { FooterWrapper } from "../../../footer-wrapper"; +import { HeaderWrapper } from "../../../header-wrapper"; +import { Media } from "../../../media"; +import { PageBorderOffsetFrom, PageNumberFormat } from "./"; import { SectionProperties } from "./section-properties"; describe("SectionProperties", () => { describe("#constructor()", () => { it("should create section properties with options", () => { + const media = new Media(); + const properties = new SectionProperties({ width: 11906, height: 16838, @@ -20,9 +25,12 @@ describe("SectionProperties", () => { mirror: false, space: 708, linePitch: 360, - headerId: 100, - footerId: 200, - footerType: FooterReferenceType.EVEN, + headers: { + default: new HeaderWrapper(media, 100), + }, + footers: { + even: new FooterWrapper(media, 200), + }, pageNumberStart: 10, pageNumberFormatType: PageNumberFormat.CARDINAL_TEXT, }); @@ -78,9 +86,7 @@ describe("SectionProperties", () => { }); expect(tree["w:sectPr"][2]).to.deep.equal({ "w:cols": [{ _attr: { "w:space": 708 } }] }); expect(tree["w:sectPr"][3]).to.deep.equal({ "w:docGrid": [{ _attr: { "w:linePitch": 360 } }] }); - expect(tree["w:sectPr"][4]).to.deep.equal({ "w:headerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); - expect(tree["w:sectPr"][5]).to.deep.equal({ "w:footerReference": [{ _attr: { "r:id": "rId0", "w:type": "default" } }] }); - expect(tree["w:sectPr"][6]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }); + expect(tree["w:sectPr"][4]).to.deep.equal({ "w:pgNumType": [{ _attr: { "w:fmt": "decimal" } }] }); }); it("should create section properties with changed options", () => { @@ -170,7 +176,8 @@ describe("SectionProperties", () => { }); const tree = new Formatter().format(properties); expect(Object.keys(tree)).to.deep.equal(["w:sectPr"]); - expect(tree["w:sectPr"][7]).to.deep.equal({ + const pgBorders = tree["w:sectPr"].find((item) => item["w:pgBorders"] !== undefined); + expect(pgBorders).to.deep.equal({ "w:pgBorders": [{ _attr: { "w:offsetFrom": "page" } }], }); }); diff --git a/src/file/document/body/section-properties/section-properties.ts b/src/file/document/body/section-properties/section-properties.ts index 5042c8a3ab..ec8a6b6746 100644 --- a/src/file/document/body/section-properties/section-properties.ts +++ b/src/file/document/body/section-properties/section-properties.ts @@ -1,115 +1,170 @@ // http://officeopenxml.com/WPsection.php import { XmlComponent } from "file/xml-components"; -import { FooterReferenceType, IPageBordersOptions, IPageNumberTypeAttributes, PageBorders, PageNumberFormat, PageNumberType } from "./"; +import { FooterWrapper } from "../../../footer-wrapper"; +import { HeaderWrapper } from "../../../header-wrapper"; +import { IPageBordersOptions, IPageNumberTypeAttributes, PageBorders, PageNumberFormat, PageNumberType } from "./"; import { Columns } from "./columns/columns"; import { IColumnsAttributes } from "./columns/columns-attributes"; import { DocumentGrid } from "./doc-grid/doc-grid"; import { IDocGridAttributesProperties } from "./doc-grid/doc-grid-attributes"; -import { FooterReference, IFooterOptions } from "./footer-reference/footer-reference"; -import { HeaderReference, IHeaderOptions } from "./header-reference/header-reference"; -import { HeaderReferenceType } from "./header-reference/header-reference-attributes"; +import { FooterReferenceType } from "./footer-reference"; +import { FooterReference } from "./footer-reference/footer-reference"; +import { HeaderReferenceType } from "./header-reference"; +import { HeaderReference } from "./header-reference/header-reference"; import { PageMargin } from "./page-margin/page-margin"; import { IPageMarginAttributes } from "./page-margin/page-margin-attributes"; import { PageSize } from "./page-size/page-size"; import { IPageSizeAttributes, PageOrientation } from "./page-size/page-size-attributes"; +import { TitlePage } from "./title-page/title-page"; + +export interface IHeaderFooterGroup { + default?: T; + first?: T; + even?: T; +} + +interface IHeadersOptions { + headers?: IHeaderFooterGroup; +} + +interface IFootersOptions { + footers?: IHeaderFooterGroup; +} + +interface ITitlePageOptions { + titlePage?: boolean; +} export type SectionPropertiesOptions = IPageSizeAttributes & IPageMarginAttributes & IColumnsAttributes & IDocGridAttributesProperties & - IHeaderOptions & - IFooterOptions & + IHeadersOptions & + IFootersOptions & IPageNumberTypeAttributes & - IPageBordersOptions; + IPageBordersOptions & + ITitlePageOptions; export class SectionProperties extends XmlComponent { private readonly options: SectionPropertiesOptions; - constructor(options?: SectionPropertiesOptions) { + constructor(options: SectionPropertiesOptions = {}) { super("w:sectPr"); - const defaultOptions = { - width: 11906, - height: 16838, - top: 1440, - right: 1440, - bottom: 1440, - left: 1440, - header: 708, - footer: 708, - gutter: 0, - mirror: false, - space: 708, - linePitch: 360, - orientation: PageOrientation.PORTRAIT, - headerType: HeaderReferenceType.DEFAULT, - headerId: 0, - footerType: FooterReferenceType.DEFAULT, - footerId: 0, - pageNumberStart: undefined, - pageNumberFormatType: PageNumberFormat.DECIMAL, - pageBorders: undefined, - pageBorderTop: undefined, - pageBorderRight: undefined, - pageBorderBottom: undefined, - pageBorderLeft: undefined, - }; + const { + width = 11906, + height = 16838, + top = 1440, + right = 1440, + bottom = 1440, + left = 1440, + header = 708, + footer = 708, + gutter = 0, + mirror = false, + space = 708, + linePitch = 360, + orientation = PageOrientation.PORTRAIT, + headers, + footers, + pageNumberFormatType = PageNumberFormat.DECIMAL, + pageNumberStart, + pageBorders, + pageBorderTop, + pageBorderRight, + pageBorderBottom, + pageBorderLeft, + titlePage = false, + } = options; - const mergedOptions = { - ...defaultOptions, - ...options, - }; + this.options = options; + this.root.push(new PageSize(width, height, orientation)); + this.root.push(new PageMargin(top, right, bottom, left, header, footer, gutter, mirror)); + this.root.push(new Columns(space)); + this.root.push(new DocumentGrid(linePitch)); - this.root.push(new PageSize(mergedOptions.width, mergedOptions.height, mergedOptions.orientation)); - this.root.push( - new PageMargin( - mergedOptions.top, - mergedOptions.right, - mergedOptions.bottom, - mergedOptions.left, - mergedOptions.header, - mergedOptions.footer, - mergedOptions.gutter, - mergedOptions.mirror, - ), - ); - this.root.push(new Columns(mergedOptions.space)); - this.root.push(new DocumentGrid(mergedOptions.linePitch)); + this.addHeaders(headers); + this.addFooters(footers); - this.root.push( - new HeaderReference({ - headerType: mergedOptions.headerType, - headerId: mergedOptions.headerId, - }), - ); - this.root.push( - new FooterReference({ - footerType: mergedOptions.footerType, - footerId: mergedOptions.footerId, - }), - ); + this.root.push(new PageNumberType(pageNumberStart, pageNumberFormatType)); - this.root.push(new PageNumberType(mergedOptions.pageNumberStart, mergedOptions.pageNumberFormatType)); - - if ( - mergedOptions.pageBorders || - mergedOptions.pageBorderTop || - mergedOptions.pageBorderRight || - mergedOptions.pageBorderBottom || - mergedOptions.pageBorderLeft - ) { + if (pageBorders || pageBorderTop || pageBorderRight || pageBorderBottom || pageBorderLeft) { this.root.push( new PageBorders({ - pageBorders: mergedOptions.pageBorders, - pageBorderTop: mergedOptions.pageBorderTop, - pageBorderRight: mergedOptions.pageBorderRight, - pageBorderBottom: mergedOptions.pageBorderBottom, - pageBorderLeft: mergedOptions.pageBorderLeft, + pageBorders: pageBorders, + pageBorderTop: pageBorderTop, + pageBorderRight: pageBorderRight, + pageBorderBottom: pageBorderBottom, + pageBorderLeft: pageBorderLeft, }), ); } - this.options = mergedOptions; + if (titlePage) { + this.root.push(new TitlePage()); + } + } + + private addHeaders(headers?: IHeaderFooterGroup): void { + if (headers) { + if (headers.default) { + this.root.push( + new HeaderReference({ + headerType: HeaderReferenceType.DEFAULT, + headerId: headers.default.Header.ReferenceId, + }), + ); + } + + if (headers.first) { + this.root.push( + new HeaderReference({ + headerType: HeaderReferenceType.FIRST, + headerId: headers.first.Header.ReferenceId, + }), + ); + } + + if (headers.even) { + this.root.push( + new HeaderReference({ + headerType: HeaderReferenceType.EVEN, + headerId: headers.even.Header.ReferenceId, + }), + ); + } + } + } + + private addFooters(footers?: IHeaderFooterGroup): void { + if (footers) { + if (footers.default) { + this.root.push( + new FooterReference({ + footerType: FooterReferenceType.DEFAULT, + footerId: footers.default.Footer.ReferenceId, + }), + ); + } + + if (footers.first) { + this.root.push( + new FooterReference({ + footerType: FooterReferenceType.FIRST, + footerId: footers.first.Footer.ReferenceId, + }), + ); + } + + if (footers.even) { + this.root.push( + new FooterReference({ + footerType: FooterReferenceType.EVEN, + footerId: footers.even.Footer.ReferenceId, + }), + ); + } + } } public get Options(): SectionPropertiesOptions { diff --git a/src/file/file-properties.ts b/src/file/file-properties.ts new file mode 100644 index 0000000000..3ccf8a5176 --- /dev/null +++ b/src/file/file-properties.ts @@ -0,0 +1,11 @@ +import { IDocumentTemplate } from "../import-dotx"; + +export interface IFileProperties { + template?: IDocumentTemplate; +} + +// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432 +/** + * @ignore + */ +export const WORKAROUND = ""; diff --git a/src/file/file.ts b/src/file/file.ts index afa268a5a7..e18b536490 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -2,10 +2,17 @@ import { AppProperties } from "./app-properties/app-properties"; import { ContentTypes } from "./content-types/content-types"; import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { Document } from "./document"; -import { FooterReferenceType, HeaderReference, HeaderReferenceType, SectionPropertiesOptions } from "./document/body/section-properties"; -import { FooterWrapper } from "./footer-wrapper"; +import { + FooterReferenceType, + HeaderReference, + HeaderReferenceType, + IHeaderFooterGroup, + SectionPropertiesOptions, +} from "./document/body/section-properties"; +import { IFileProperties } from "./file-properties"; +import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; import { FootNotes } from "./footnotes"; -import { HeaderWrapper } from "./header-wrapper"; +import { HeaderWrapper, IDocumentHeader } from "./header-wrapper"; import { Image, Media } from "./media"; import { Numbering } from "./numbering"; import { Bookmark, Hyperlink, Paragraph } from "./paragraph"; @@ -18,33 +25,51 @@ import { Table } from "./table"; import { TableOfContents } from "./table-of-contents"; export class File { + private currentRelationshipId: number = 1; + private readonly document: Document; - private styles: Styles; + private readonly headers: IDocumentHeader[] = []; + private readonly footers: IDocumentFooter[] = []; + private readonly docRelationships: Relationships; private readonly coreProperties: CoreProperties; private readonly numbering: Numbering; private readonly media: Media; - private readonly docRelationships: Relationships; private readonly fileRelationships: Relationships; - private readonly headerWrapper: HeaderWrapper[] = []; - private readonly footerWrapper: FooterWrapper[] = []; private readonly footNotes: FootNotes; private readonly settings: Settings; - private readonly contentTypes: ContentTypes; private readonly appProperties: AppProperties; + private styles: Styles; - private currentRelationshipId: number = 1; + constructor( + options: IPropertiesOptions = { + creator: "Un-named", + revision: "1", + lastModifiedBy: "Un-named", + }, + sectionPropertiesOptions: SectionPropertiesOptions = {}, + fileProperties: IFileProperties = {}, + ) { + this.coreProperties = new CoreProperties(options); + this.numbering = new Numbering(); + this.docRelationships = new Relationships(); + this.media = new Media(); + this.fileRelationships = new Relationships(); + this.appProperties = new AppProperties(); + this.footNotes = new FootNotes(); + this.contentTypes = new ContentTypes(); - constructor(options?: IPropertiesOptions, sectionPropertiesOptions?: SectionPropertiesOptions) { - if (!options) { - options = { - creator: "Un-named", - revision: "1", - lastModifiedBy: "Un-named", - }; + if (fileProperties.template) { + this.currentRelationshipId = fileProperties.template.currentRelationshipId + 1; } - if (options.externalStyles) { + // set up styles + if (fileProperties.template && options.externalStyles) { + throw Error("can not use both template and external styles"); + } + if (fileProperties.template) { + this.styles = fileProperties.template.styles; + } else if (options.externalStyles) { const stylesFactory = new ExternalStylesFactory(); this.styles = stylesFactory.newInstance(options.externalStyles); } else { @@ -52,61 +77,30 @@ export class File { this.styles = stylesFactory.newInstance(); } - this.coreProperties = new CoreProperties(options); - this.numbering = new Numbering(); - this.docRelationships = new Relationships(); - this.docRelationships.createRelationship( - this.currentRelationshipId++, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", - "styles.xml", - ); - this.docRelationships.createRelationship( - this.currentRelationshipId++, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", - "numbering.xml", - ); - this.contentTypes = new ContentTypes(); + this.addDefaultRelationships(); - this.docRelationships.createRelationship( - this.currentRelationshipId++, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", - "footnotes.xml", - ); - this.media = new Media(); - - const header = this.createHeader(); - const footer = this.createFooter(); - - this.fileRelationships = new Relationships(); - this.fileRelationships.createRelationship( - 1, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", - "word/document.xml", - ); - this.fileRelationships.createRelationship( - 2, - "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", - "docProps/core.xml", - ); - this.fileRelationships.createRelationship( - 3, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", - "docProps/app.xml", - ); - this.appProperties = new AppProperties(); - - this.footNotes = new FootNotes(); - if (!sectionPropertiesOptions) { - sectionPropertiesOptions = { - footerType: FooterReferenceType.DEFAULT, - headerType: HeaderReferenceType.DEFAULT, - headerId: header.Header.ReferenceId, - footerId: footer.Footer.ReferenceId, - }; + if (fileProperties.template && fileProperties.template.headers) { + for (const templateHeader of fileProperties.template.headers) { + this.addHeaderToDocument(templateHeader.header, templateHeader.type); + } } else { - sectionPropertiesOptions.headerId = header.Header.ReferenceId; - sectionPropertiesOptions.footerId = footer.Footer.ReferenceId; + this.createHeader(); } + + if (fileProperties.template && fileProperties.template.footers) { + for (const templateFooter of fileProperties.template.footers) { + this.addFooterToDocument(templateFooter.footer, templateFooter.type); + } + } else { + this.createFooter(); + } + + sectionPropertiesOptions = { + ...sectionPropertiesOptions, + headers: this.groupHeaders(this.headers, sectionPropertiesOptions.headers), + footers: this.groupFooters(this.footers, sectionPropertiesOptions.footers), + }; + this.document = new Document(sectionPropertiesOptions); this.settings = new Settings(); } @@ -115,8 +109,9 @@ export class File { this.document.addTableOfContents(toc); } - public addParagraph(paragraph: Paragraph): void { + public addParagraph(paragraph: Paragraph): File { this.document.addParagraph(paragraph); + return this; } public createParagraph(text?: string): Paragraph { @@ -179,25 +174,13 @@ export class File { public createHeader(): HeaderWrapper { const header = new HeaderWrapper(this.media, this.currentRelationshipId++); - this.headerWrapper.push(header); - this.docRelationships.createRelationship( - header.Header.ReferenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", - `header${this.headerWrapper.length}.xml`, - ); - this.contentTypes.addHeader(this.headerWrapper.length); + this.addHeaderToDocument(header); return header; } public createFooter(): FooterWrapper { const footer = new FooterWrapper(this.media, this.currentRelationshipId++); - this.footerWrapper.push(footer); - this.docRelationships.createRelationship( - footer.Footer.ReferenceId, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", - `footer${this.footerWrapper.length}.xml`, - ); - this.contentTypes.addFooter(this.footerWrapper.length); + this.addFooterToDocument(footer); return footer; } @@ -214,6 +197,152 @@ export class File { return headerWrapper; } + public getFooterByReferenceNumber(refId: number): FooterWrapper { + const entry = this.footers.map((item) => item.footer).find((h) => h.Footer.ReferenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no footer with given reference id ${refId}`); + } + + public getHeaderByReferenceNumber(refId: number): HeaderWrapper { + const entry = this.headers.map((item) => item.header).find((h) => h.Header.ReferenceId === refId); + if (entry) { + return entry; + } + throw new Error(`There is no header with given reference id ${refId}`); + } + + public verifyUpdateFields(): void { + if (this.document.getTablesOfContents().length) { + this.settings.addUpdateFields(); + } + } + + private addHeaderToDocument(header: HeaderWrapper, type: HeaderReferenceType = HeaderReferenceType.DEFAULT): void { + this.headers.push({ header, type }); + this.docRelationships.createRelationship( + header.Header.ReferenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header", + `header${this.headers.length}.xml`, + ); + this.contentTypes.addHeader(this.headers.length); + } + + private addFooterToDocument(footer: FooterWrapper, type: FooterReferenceType = FooterReferenceType.DEFAULT): void { + this.footers.push({ footer, type }); + this.docRelationships.createRelationship( + footer.Footer.ReferenceId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer", + `footer${this.footers.length}.xml`, + ); + this.contentTypes.addFooter(this.footers.length); + } + + private addDefaultRelationships(): void { + this.fileRelationships.createRelationship( + 1, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", + "word/document.xml", + ); + this.fileRelationships.createRelationship( + 2, + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", + "docProps/core.xml", + ); + this.fileRelationships.createRelationship( + 3, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", + "docProps/app.xml", + ); + + this.docRelationships.createRelationship( + this.currentRelationshipId++, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", + "styles.xml", + ); + this.docRelationships.createRelationship( + this.currentRelationshipId++, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering", + "numbering.xml", + ); + this.docRelationships.createRelationship( + this.currentRelationshipId++, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes", + "footnotes.xml", + ); + } + + private groupHeaders(headers: IDocumentHeader[], group: IHeaderFooterGroup = {}): IHeaderFooterGroup { + let newGroup = group; + + for (const header of headers) { + switch (header.type) { + case HeaderReferenceType.DEFAULT: + newGroup = { + ...newGroup, + default: header.header, + }; + break; + case HeaderReferenceType.FIRST: + newGroup = { + ...newGroup, + first: header.header, + }; + break; + case HeaderReferenceType.EVEN: + newGroup = { + ...newGroup, + even: header.header, + }; + break; + default: + newGroup = { + ...newGroup, + default: header.header, + }; + break; + } + } + + return newGroup; + } + + private groupFooters(footers: IDocumentFooter[], group: IHeaderFooterGroup = {}): IHeaderFooterGroup { + let newGroup = group; + + for (const footer of footers) { + switch (footer.type) { + case FooterReferenceType.DEFAULT: + newGroup = { + ...newGroup, + default: footer.footer, + }; + break; + case FooterReferenceType.FIRST: + newGroup = { + ...newGroup, + first: footer.footer, + }; + break; + case FooterReferenceType.EVEN: + newGroup = { + ...newGroup, + even: footer.footer, + }; + break; + default: + newGroup = { + ...newGroup, + default: footer.footer, + }; + break; + } + } + + return newGroup; + } + public get Document(): Document { return this.document; } @@ -247,35 +376,19 @@ export class File { } public get Header(): HeaderWrapper { - return this.headerWrapper[0]; + return this.headers[0].header; } public get Headers(): HeaderWrapper[] { - return this.headerWrapper; - } - - public HeaderByRefNumber(refId: number): HeaderWrapper { - const entry = this.headerWrapper.find((h) => h.Header.ReferenceId === refId); - if (entry) { - return entry; - } - throw new Error(`There is no header with given reference id ${refId}`); + return this.headers.map((item) => item.header); } public get Footer(): FooterWrapper { - return this.footerWrapper[0]; + return this.footers[0].footer; } public get Footers(): FooterWrapper[] { - return this.footerWrapper; - } - - public FooterByRefNumber(refId: number): FooterWrapper { - const entry = this.footerWrapper.find((h) => h.Footer.ReferenceId === refId); - if (entry) { - return entry; - } - throw new Error(`There is no footer with given reference id ${refId}`); + return this.footers.map((item) => item.footer); } public get ContentTypes(): ContentTypes { @@ -293,10 +406,4 @@ export class File { public get Settings(): Settings { return this.settings; } - - public verifyUpdateFields(): void { - if (this.document.getTablesOfContents().length) { - this.settings.addUpdateFields(); - } - } } diff --git a/src/file/footer-wrapper.ts b/src/file/footer-wrapper.ts index afd673c2b2..2c51cf5072 100644 --- a/src/file/footer-wrapper.ts +++ b/src/file/footer-wrapper.ts @@ -1,16 +1,23 @@ import { XmlComponent } from "file/xml-components"; + +import { FooterReferenceType } from "./document"; import { Footer } from "./footer/footer"; -import { Image, Media } from "./media"; +import { Image, IMediaData, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; +export interface IDocumentFooter { + footer: FooterWrapper; + type: FooterReferenceType; +} + export class FooterWrapper { private readonly footer: Footer; private readonly relationships: Relationships; - constructor(private readonly media: Media, referenceId: number) { - this.footer = new Footer(referenceId); + constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) { + this.footer = new Footer(referenceId, initContent); this.relationships = new Relationships(); } @@ -32,17 +39,33 @@ export class FooterWrapper { return this.footer.createTable(rows, cols); } - public addChildElement(childElement: XmlComponent | string): void { + public addChildElement(childElement: XmlComponent): void { this.footer.addChildElement(childElement); } - public createImage(image: Buffer, width?: number, height?: number): void { - const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height); + public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { + const mediaData = this.media.addMedia(image, refId, width, height); this.relationships.createRelationship( mediaData.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, ); + return mediaData; + } + + public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { + this.relationships.createRelationship( + refId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + target, + targetMode, + ); + } + + public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { + // TODO + // tslint:disable-next-line:no-any + const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } @@ -58,4 +81,8 @@ export class FooterWrapper { public get Relationships(): Relationships { return this.relationships; } + + public get Media(): Media { + return this.media; + } } diff --git a/src/file/footer/footer.ts b/src/file/footer/footer.ts index 532e662c25..0aa5938d0b 100644 --- a/src/file/footer/footer.ts +++ b/src/file/footer/footer.ts @@ -1,14 +1,14 @@ // http://officeopenxml.com/WPfooters.php -import { XmlComponent } from "file/xml-components"; +import { InitializableXmlComponent, XmlComponent } from "file/xml-components"; import { Paragraph } from "../paragraph"; import { Table } from "../table"; import { FooterAttributes } from "./footer-attributes"; -export class Footer extends XmlComponent { +export class Footer extends InitializableXmlComponent { private readonly refId: number; - constructor(referenceNumber: number) { - super("w:ftr"); + constructor(referenceNumber: number, initContent?: XmlComponent) { + super("w:ftr", initContent); this.refId = referenceNumber; this.root.push( new FooterAttributes({ diff --git a/src/file/header-wrapper.ts b/src/file/header-wrapper.ts index 79a216610a..f4a403ce27 100644 --- a/src/file/header-wrapper.ts +++ b/src/file/header-wrapper.ts @@ -1,16 +1,23 @@ import { XmlComponent } from "file/xml-components"; + +import { HeaderReferenceType } from "./document"; import { Header } from "./header/header"; -import { Image, Media } from "./media"; +import { Image, IMediaData, Media } from "./media"; import { ImageParagraph, Paragraph } from "./paragraph"; import { Relationships } from "./relationships"; import { Table } from "./table"; +export interface IDocumentHeader { + header: HeaderWrapper; + type: HeaderReferenceType; +} + export class HeaderWrapper { private readonly header: Header; private readonly relationships: Relationships; - constructor(private readonly media: Media, referenceId: number) { - this.header = new Header(referenceId); + constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) { + this.header = new Header(referenceId, initContent); this.relationships = new Relationships(); } @@ -36,13 +43,29 @@ export class HeaderWrapper { this.header.addChildElement(childElement); } - public createImage(image: Buffer, width?: number, height?: number): void { - const mediaData = this.media.addMedia(image, this.relationships.RelationshipCount, width, height); + public addImageRelationship(image: Buffer, refId: number, width?: number, height?: number): IMediaData { + const mediaData = this.media.addMedia(image, refId, width, height); this.relationships.createRelationship( mediaData.referenceId, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", `media/${mediaData.fileName}`, ); + return mediaData; + } + + public addHyperlinkRelationship(target: string, refId: number, targetMode?: "External" | undefined): void { + this.relationships.createRelationship( + refId, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + target, + targetMode, + ); + } + + public createImage(image: Buffer | string | Uint8Array | ArrayBuffer, width?: number, height?: number): void { + // TODO + // tslint:disable-next-line:no-any + const mediaData = this.addImageRelationship(image as any, this.relationships.RelationshipCount, width, height); this.addImage(new Image(new ImageParagraph(mediaData))); } @@ -58,4 +81,8 @@ export class HeaderWrapper { public get Relationships(): Relationships { return this.relationships; } + + public get Media(): Media { + return this.media; + } } diff --git a/src/file/header/header-attributes.ts b/src/file/header/header-attributes.ts index e47271841c..3e46b4ca94 100644 --- a/src/file/header/header-attributes.ts +++ b/src/file/header/header-attributes.ts @@ -23,6 +23,17 @@ export interface IHeaderAttributesProperties { dcmitype?: string; xsi?: string; type?: string; + cx?: string; + cx1?: string; + cx2?: string; + cx3?: string; + cx4?: string; + cx5?: string; + cx6?: string; + cx7?: string; + cx8?: string; + w16cid: string; + w16se: string; } export class HeaderAttributes extends XmlAttributeComponent { @@ -49,5 +60,16 @@ export class HeaderAttributes extends XmlAttributeComponent this.root.push(x)); this.concreteNumbering.forEach((x) => this.root.push(x)); return super.prepForXml(); diff --git a/src/file/styles/external-styles-factory.spec.ts b/src/file/styles/external-styles-factory.spec.ts index 295c9644f6..5bc7925e8d 100644 --- a/src/file/styles/external-styles-factory.spec.ts +++ b/src/file/styles/external-styles-factory.spec.ts @@ -61,8 +61,6 @@ describe("External styles factory", () => { it("should parse other child elements of w:styles", () => { // tslint:disable-next-line:no-any const importedStyle = new ExternalStylesFactory().newInstance(externalStyles) as any; - - expect(importedStyle.root.length).to.equal(5); expect(importedStyle.root[1]).to.eql({ deleted: false, root: [ diff --git a/src/file/table/table-cell-margin.ts b/src/file/table/table-cell-margin.ts index 198dc89cbd..2b8fd32049 100644 --- a/src/file/table/table-cell-margin.ts +++ b/src/file/table/table-cell-margin.ts @@ -21,8 +21,10 @@ export class TableCellMargin extends XmlComponent { super("w:tblCellMar"); } - public prepForXml(): IXmlableObject { - return this.root.length > 0 ? super.prepForXml() : ""; + public prepForXml(): IXmlableObject | undefined { + if (this.root.length > 0) { + return super.prepForXml(); + } } public addTopMargin(value: number, type: WidthType = WidthType.DXA): void { diff --git a/src/file/table/table-cell.spec.ts b/src/file/table/table-cell.spec.ts index 7a3072f186..4e761d2e07 100644 --- a/src/file/table/table-cell.spec.ts +++ b/src/file/table/table-cell.spec.ts @@ -8,8 +8,7 @@ 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(""); + expect(() => new Formatter().format(tb)).to.throw(); }); }); diff --git a/src/file/table/table-cell.ts b/src/file/table/table-cell.ts index 19e3ccc428..046cc8d8aa 100644 --- a/src/file/table/table-cell.ts +++ b/src/file/table/table-cell.ts @@ -29,8 +29,10 @@ export class TableCellBorders extends XmlComponent { super("w:tcBorders"); } - public prepForXml(): IXmlableObject { - return this.root.length > 0 ? super.prepForXml() : ""; + public prepForXml(): IXmlableObject | undefined { + if (this.root.length > 0) { + return super.prepForXml(); + } } public addTopBorder(style: BorderStyle, size: number, color: string): TableCellBorders { diff --git a/src/file/table/table.ts b/src/file/table/table.ts index f6201acedc..3220bce537 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -145,9 +145,13 @@ export class TableCell extends XmlComponent { return this; } - public prepForXml(): IXmlableObject { + public prepForXml(): IXmlableObject | undefined { // Cells must end with a paragraph const retval = super.prepForXml(); + if (!retval) { + return undefined; + } + const content = retval["w:tc"]; if (!content[content.length - 1]["w:p"]) { content.push(new Paragraph().prepForXml()); diff --git a/src/file/xml-components/base.ts b/src/file/xml-components/base.ts index a5f24f3824..d65e5c6f3c 100644 --- a/src/file/xml-components/base.ts +++ b/src/file/xml-components/base.ts @@ -8,7 +8,7 @@ export abstract class BaseXmlComponent { this.rootKey = rootKey; } - public abstract prepForXml(): IXmlableObject; + public abstract prepForXml(): IXmlableObject | undefined; public get IsDeleted(): boolean { return this.deleted; diff --git a/src/file/xml-components/imported-xml-component.ts b/src/file/xml-components/imported-xml-component.ts index 23c4137900..eb318a3d64 100644 --- a/src/file/xml-components/imported-xml-component.ts +++ b/src/file/xml-components/imported-xml-component.ts @@ -1,7 +1,7 @@ -/* tslint:disable */ -import { XmlComponent, IXmlableObject } from "."; +// tslint:disable:no-any import * as fastXmlParser from "fast-xml-parser"; import { flatMap } from "lodash"; +import { IXmlableObject, XmlComponent } from "."; export const parseOptions = { ignoreAttributes: false, @@ -54,8 +54,27 @@ export function convertToXmlComponent(elementName: string, element: any): Import * Represents imported xml component from xml file. */ export class ImportedXmlComponent extends XmlComponent { - private _attr: any; + /** + * Converts the xml string to a XmlComponent tree. + * + * @param importedContent xml content of the imported component + */ + public 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; + } + + // tslint:disable-next-line:variable-name + private readonly _attr: any; + + // tslint:disable-next-line:variable-name constructor(rootKey: string, _attr?: any) { super(rootKey); if (_attr) { @@ -89,8 +108,12 @@ export class ImportedXmlComponent extends XmlComponent { * ] * } */ - prepForXml(): IXmlableObject { + public prepForXml(): IXmlableObject | undefined { const result = super.prepForXml(); + if (!result) { + return undefined; + } + if (!!this._attr) { if (!Array.isArray(result[this.rootKey])) { result[this.rootKey] = [result[this.rootKey]]; @@ -100,33 +123,17 @@ export class ImportedXmlComponent extends XmlComponent { return result; } - push(xmlComponent: XmlComponent) { + public push(xmlComponent: XmlComponent): void { 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; - } } /** * Used for the attributes of root element that is being imported. */ export class ImportedRootElementAttributes extends XmlComponent { - constructor(private _attr: any) { + // tslint:disable-next-line:variable-name + constructor(private readonly _attr: any) { super(""); } diff --git a/src/file/xml-components/index.ts b/src/file/xml-components/index.ts index 917933869e..66e9641bfd 100644 --- a/src/file/xml-components/index.ts +++ b/src/file/xml-components/index.ts @@ -3,3 +3,4 @@ export * from "./attributes"; export * from "./default-attributes"; export * from "./imported-xml-component"; export * from "./xmlable-object"; +export * from "./initializable-xml-component"; diff --git a/src/file/xml-components/initializable-xml-component.ts b/src/file/xml-components/initializable-xml-component.ts new file mode 100644 index 0000000000..5d461c007a --- /dev/null +++ b/src/file/xml-components/initializable-xml-component.ts @@ -0,0 +1,11 @@ +import { XmlComponent } from "file/xml-components"; + +export abstract class InitializableXmlComponent extends XmlComponent { + constructor(rootKey: string, initComponent?: InitializableXmlComponent) { + super(rootKey); + + if (initComponent) { + this.root = initComponent.root; + } + } +} diff --git a/src/file/xml-components/xml-component.spec.ts b/src/file/xml-components/xml-component.spec.ts index 8b4f983388..10e2f44dc4 100644 --- a/src/file/xml-components/xml-component.spec.ts +++ b/src/file/xml-components/xml-component.spec.ts @@ -26,6 +26,11 @@ describe("XmlComponent", () => { xmlComponent.addChildElement(child); const xml = xmlComponent.prepForXml(); + + if (!xml) { + return; + } + assert.equal(xml["w:test"].length, 0); }); }); diff --git a/src/file/xml-components/xml-component.ts b/src/file/xml-components/xml-component.ts index fae843db29..acbe51cb04 100644 --- a/src/file/xml-components/xml-component.ts +++ b/src/file/xml-components/xml-component.ts @@ -7,10 +7,10 @@ export abstract class XmlComponent extends BaseXmlComponent { constructor(rootKey: string) { super(rootKey); - this.root = new Array(); + this.root = new Array(); } - public prepForXml(): IXmlableObject { + public prepForXml(): IXmlableObject | undefined { const children = this.root .filter((c) => { if (c instanceof BaseXmlComponent) { @@ -24,7 +24,7 @@ export abstract class XmlComponent extends BaseXmlComponent { } return comp; }) - .filter((comp) => comp); // Exclude null, undefined, and empty strings + .filter((comp) => comp !== undefined); // Exclude undefined return { [this.rootKey]: children, }; diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts new file mode 100644 index 0000000000..1d87c71e23 --- /dev/null +++ b/src/import-dotx/import-dotx.ts @@ -0,0 +1,209 @@ +import * as fastXmlParser from "fast-xml-parser"; +import * as JSZip from "jszip"; + +import { FooterReferenceType } from "file/document/body/section-properties/footer-reference"; +import { HeaderReferenceType } from "file/document/body/section-properties/header-reference"; +import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper"; +import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper"; +import { Media } from "file/media"; +import { Styles } from "file/styles"; +import { ExternalStylesFactory } from "file/styles/external-styles-factory"; +import { convertToXmlComponent, ImportedXmlComponent, parseOptions } from "file/xml-components"; + +const importParseOptions = { + ...parseOptions, + textNodeName: "", + trimValues: false, +}; + +const schemeToType = { + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer": "footer", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image": "image", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink": "hyperlink", +}; + +interface IDocumentRefs { + headers: Array<{ id: number; type: HeaderReferenceType }>; + footers: Array<{ id: number; type: FooterReferenceType }>; +} + +interface IRelationshipFileInfo { + id: number; + target: string; + type: "header" | "footer" | "image" | "hyperlink"; +} + +// Document Template +// https://fileinfo.com/extension/dotx +export interface IDocumentTemplate { + currentRelationshipId: number; + headers: IDocumentHeader[]; + footers: IDocumentFooter[]; + styles: Styles; + titlePageIsDefined: boolean; +} + +export class ImportDotx { + private currentRelationshipId: number; + + constructor() { + this.currentRelationshipId = 1; + } + + public async extract(data: Buffer): Promise { + const zipContent = await JSZip.loadAsync(data); + + const stylesContent = await zipContent.files["word/styles.xml"].async("text"); + const stylesFactory = new ExternalStylesFactory(); + const styles = stylesFactory.newInstance(stylesContent); + + const documentContent = zipContent.files["word/document.xml"]; + const documentRefs: IDocumentRefs = this.extractDocumentRefs(await documentContent.async("text")); + const titlePageIsDefined = this.titlePageIsDefined(await documentContent.async("text")); + + const relationshipContent = zipContent.files["word/_rels/document.xml.rels"]; + const documentRelationships: IRelationshipFileInfo[] = this.findReferenceFiles(await relationshipContent.async("text")); + + const media = new Media(); + + const headers: IDocumentHeader[] = []; + for (const headerRef of documentRefs.headers) { + const headerKey = "w:hdr"; + const relationshipFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id); + if (!relationshipFileInfo) { + throw new Error(`Can not find target file for id ${headerRef.id}`); + } + + const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text"); + const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + + const importedComp = convertToXmlComponent(headerKey, xmlObj[headerKey]) as ImportedXmlComponent; + + const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp); + await this.addRelationToWrapper(relationshipFileInfo, zipContent, header); + headers.push({ type: headerRef.type, header }); + } + + const footers: IDocumentFooter[] = []; + for (const footerRef of documentRefs.footers) { + const footerKey = "w:ftr"; + const relationshipFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id); + if (!relationshipFileInfo) { + throw new Error(`Can not find target file for id ${footerRef.id}`); + } + const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text"); + const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const importedComp = convertToXmlComponent(footerKey, xmlObj[footerKey]) as ImportedXmlComponent; + + const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp); + await this.addRelationToWrapper(relationshipFileInfo, zipContent, footer); + footers.push({ type: footerRef.type, footer }); + } + + const templateDocument: IDocumentTemplate = { + headers, + footers, + currentRelationshipId: this.currentRelationshipId, + styles, + titlePageIsDefined, + }; + return templateDocument; + } + + public async addRelationToWrapper( + relationhipFile: IRelationshipFileInfo, + zipContent: JSZip, + wrapper: HeaderWrapper | FooterWrapper, + ): Promise { + let wrapperImagesReferences: IRelationshipFileInfo[] = []; + let hyperLinkReferences: IRelationshipFileInfo[] = []; + const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`]; + if (refFile) { + const xmlRef = await refFile.async("text"); + wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "image"); + hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === "hyperlink"); + } + for (const r of wrapperImagesReferences) { + const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer"); + wrapper.addImageRelationship(buffer, r.id); + } + for (const r of hyperLinkReferences) { + wrapper.addHyperlinkRelationship(r.target, r.id, "External"); + } + } + + public findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { + const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const relationshipXmlArray = Array.isArray(xmlObj.Relationships.Relationship) + ? xmlObj.Relationships.Relationship + : [xmlObj.Relationships.Relationship]; + const relationships: IRelationshipFileInfo[] = relationshipXmlArray + .map((item) => { + return { + id: this.parseRefId(item._attr.Id), + type: schemeToType[item._attr.Type], + target: item._attr.Target, + }; + }) + .filter((item) => item.type !== null); + return relationships; + } + + public extractDocumentRefs(xmlData: string): IDocumentRefs { + interface IAttributedXML { + _attr: object; + } + const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; + + const headerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:headerReference"]; + let headersXmlArray: IAttributedXML[]; + if (headerProps === undefined) { + headersXmlArray = []; + } else if (Array.isArray(headerProps)) { + headersXmlArray = headerProps; + } else { + headersXmlArray = [headerProps]; + } + const headers = headersXmlArray.map((item) => { + return { + type: item._attr["w:type"], + id: this.parseRefId(item._attr["r:id"]), + }; + }); + + const footerProps: undefined | IAttributedXML | IAttributedXML[] = sectionProp["w:footerReference"]; + let footersXmlArray: IAttributedXML[]; + if (footerProps === undefined) { + footersXmlArray = []; + } else if (Array.isArray(footerProps)) { + footersXmlArray = footerProps; + } else { + footersXmlArray = [footerProps]; + } + + const footers = footersXmlArray.map((item) => { + return { + type: item._attr["w:type"], + id: this.parseRefId(item._attr["r:id"]), + }; + }); + + return { headers, footers }; + } + + public titlePageIsDefined(xmlData: string): boolean { + const xmlObj = fastXmlParser.parse(xmlData, importParseOptions); + const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; + return sectionProp["w:titlePg"] !== undefined; + } + + public parseRefId(str: string): number { + const match = /^rId(\d+)$/.exec(str); + if (match === null) { + throw new Error("Invalid ref id"); + } + return parseInt(match[1], 10); + } +} diff --git a/src/import-dotx/index.ts b/src/import-dotx/index.ts new file mode 100644 index 0000000000..2b12220d83 --- /dev/null +++ b/src/import-dotx/index.ts @@ -0,0 +1 @@ +export * from "./import-dotx"; diff --git a/src/index.ts b/src/index.ts index b6d3ecaf25..1e35c87d30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,4 @@ export { File as Document } from "./file"; export * from "./file"; export * from "./export"; +export * from "./import-dotx";