diff --git a/.github/workflows/demos.yml b/.github/workflows/demos.yml index 93fb6c3b66..7f7aa5ddaf 100644 --- a/.github/workflows/demos.yml +++ b/.github/workflows/demos.yml @@ -301,15 +301,6 @@ jobs: with: xml-file: build/extracted-doc/word/document.xml xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd - - name: Run Demo - run: npm run ts-node -- ./demo/30-template-document.ts - - name: Extract Word Document - run: npm run extract - - name: Validate XML - uses: ChristophWurst/xmllint-action@v1 - with: - xml-file: build/extracted-doc/word/document.xml - xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd - name: Run Demo run: npm run ts-node -- ./demo/31-tables.ts - name: Extract Word Document diff --git a/.nycrc b/.nycrc index 6c47fc4974..19699770ea 100644 --- a/.nycrc +++ b/.nycrc @@ -1,9 +1,9 @@ { "check-coverage": true, - "statements": 99.79, - "branches": 98.17, + "statements": 99.87, + "branches": 98.2, "functions": 100, - "lines": 99.78, + "lines": 99.86, "include": [ "src/**/*.ts" ], diff --git a/demo/30-template-document.ts b/demo/30-template-document.ts deleted file mode 100644 index 938502786a..0000000000 --- a/demo/30-template-document.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Example on how to use a template document -// Import from 'docx' rather than '../build' if you install from npm -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) => { - const doc = new Document( - { - sections: [ - { - properties: { - titlePage: templateDocument.titlePageIsDefined, - }, - children: [new Paragraph("Hello World")], - }, - ], - }, - { - template: templateDocument, - }, - ); - - Packer.toBuffer(doc).then((buffer) => { - fs.writeFileSync("My Document.docx", buffer); - }); - }); -}); diff --git a/src/file/file-properties.ts b/src/file/file-properties.ts deleted file mode 100644 index 4273be8798..0000000000 --- a/src/file/file-properties.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IDocumentTemplate } from "../import-dotx"; - -export interface IFileProperties { - readonly 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.spec.ts b/src/file/file.spec.ts index bc9f554af6..287de447a4 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -1,12 +1,11 @@ import { expect } from "chai"; import { Formatter } from "@export/formatter"; -import { sectionMarginDefaults, sectionPageSizeDefaults } from "./document"; +import { sectionMarginDefaults, sectionPageSizeDefaults } from "./document"; import { File } from "./file"; import { Footer, Header } from "./header"; import { Paragraph } from "./paragraph"; -import { Media } from "./media"; const PAGE_SIZE_DEFAULTS = { "w:h": sectionPageSizeDefaults.HEIGHT, @@ -433,29 +432,6 @@ describe("File", () => { }); }); - describe("#templates", () => { - // Test will be deprecated when import-dotx and templates are deprecated - it("should work with template", () => { - const doc = new File( - { - sections: [], - }, - { - template: { - currentRelationshipId: 1, - headers: [], - footers: [], - styles: "", - titlePageIsDefined: true, - media: new Media(), - }, - }, - ); - - expect(doc).to.not.be.undefined; - }); - }); - describe("#externalStyles", () => { it("should work with external styles", () => { const doc = new File({ diff --git a/src/file/file.ts b/src/file/file.ts index 1fa9da3c9c..c01c7aa82d 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -4,7 +4,6 @@ import { CoreProperties, IPropertiesOptions } from "./core-properties"; import { CustomProperties } from "./custom-properties"; import { DocumentWrapper } from "./document-wrapper"; import { HeaderFooterReferenceType, ISectionPropertiesOptions } from "./document/body/section-properties"; -import { IFileProperties } from "./file-properties"; import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; import { FootnotesWrapper } from "./footnotes-wrapper"; import { Footer, Header } from "./header"; @@ -55,7 +54,7 @@ export class File { private readonly styles: Styles; private readonly comments: Comments; - public constructor(options: IPropertiesOptions, fileProperties: IFileProperties = {}) { + public constructor(options: IPropertiesOptions) { this.coreProperties = new CoreProperties({ ...options, creator: options.creator ?? "Un-named", @@ -80,20 +79,9 @@ export class File { updateFields: options.features?.updateFields, }); - this.media = fileProperties.template && fileProperties.template.media ? fileProperties.template.media : new Media(); + this.media = new Media(); - if (fileProperties.template) { - this.currentRelationshipId = fileProperties.template.currentRelationshipId + 1; - } - - // set up styles - if (fileProperties.template && options.externalStyles) { - throw Error("can not use both template and external styles"); - } - if (fileProperties.template && fileProperties.template.styles) { - const stylesFactory = new ExternalStylesFactory(); - this.styles = stylesFactory.newInstance(fileProperties.template.styles); - } else if (options.externalStyles) { + if (options.externalStyles) { const stylesFactory = new ExternalStylesFactory(); this.styles = stylesFactory.newInstance(options.externalStyles); } else if (options.styles) { @@ -110,18 +98,6 @@ export class File { this.addDefaultRelationships(); - if (fileProperties.template && fileProperties.template.headers) { - for (const templateHeader of fileProperties.template.headers) { - this.addHeaderToDocument(templateHeader.header, templateHeader.type); - } - } - - if (fileProperties.template && fileProperties.template.footers) { - for (const templateFooter of fileProperties.template.footers) { - this.addFooterToDocument(templateFooter.footer, templateFooter.type); - } - } - for (const section of options.sections) { this.addSection(section); } diff --git a/src/file/index.ts b/src/file/index.ts index 23fdcbc4e4..4bdac87ef9 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -1,7 +1,6 @@ export * from "./paragraph"; export * from "./table"; export * from "./file"; -export * from "./file-properties"; export * from "./numbering"; export * from "./media"; export * from "./drawing"; diff --git a/src/file/media/media.spec.ts b/src/file/media/media.spec.ts index 9886fa811f..e400b347bf 100644 --- a/src/file/media/media.spec.ts +++ b/src/file/media/media.spec.ts @@ -15,96 +15,30 @@ describe("Media", () => { (convenienceFunctions.uniqueId as SinonStub).restore(); }); - describe("#addMedia", () => { - it("should add media", () => { - const image = new Media().addMedia("", { - width: 100, - height: 100, - }); - expect(image.fileName).to.equal("test.png"); - expect(image.transformation).to.deep.equal({ - pixels: { - x: 100, - y: 100, - }, - flip: undefined, - emus: { - x: 952500, - y: 952500, - }, - rotation: undefined, - }); - }); - - it("should return UInt8Array if atob is present", () => { - // eslint-disable-next-line functional/immutable-data - global.atob = () => "atob result"; - - const image = new Media().addMedia("", { - width: 100, - height: 100, - }); - expect(image.stream).to.be.an.instanceof(Uint8Array); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data - (global as any).atob = undefined; - }); - - it("should use data as is if its not a string", () => { - // eslint-disable-next-line functional/immutable-data - global.atob = () => "atob result"; - - const image = new Media().addMedia(Buffer.from(""), { - width: 100, - height: 100, - }); - expect(image.stream).to.be.an.instanceof(Uint8Array); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data - (global as any).atob = undefined; - }); - }); - - describe("#addImage", () => { - it("should add media", () => { + describe("#Array", () => { + it("Get images as array", () => { const media = new Media(); - media.addMedia("", { - width: 100, - height: 100, - }); media.addImage("test2.png", { stream: Buffer.from(""), - fileName: "", + fileName: "test.png", transformation: { pixels: { - x: Math.round(1), - y: Math.round(1), + x: Math.round(100), + y: Math.round(100), + }, + flip: { + vertical: true, + horizontal: true, }, emus: { x: Math.round(1 * 9525), y: Math.round(1 * 9525), }, + rotation: 90, }, }); - expect(media.Array).to.be.lengthOf(2); - }); - }); - - describe("#Array", () => { - it("Get images as array", () => { - const media = new Media(); - media.addMedia("", { - width: 100, - height: 100, - flip: { - vertical: true, - horizontal: true, - }, - rotation: 90, - }); - const array = media.Array; expect(array).to.be.an.instanceof(Array); expect(array.length).to.equal(1); @@ -121,10 +55,10 @@ describe("Media", () => { horizontal: true, }, emus: { - x: 952500, - y: 952500, + x: 9525, + y: 9525, }, - rotation: 5400000, + rotation: 90, }); }); }); diff --git a/src/file/media/media.ts b/src/file/media/media.ts index 3f9ef91938..6478738025 100644 --- a/src/file/media/media.ts +++ b/src/file/media/media.ts @@ -1,5 +1,3 @@ -import { uniqueId } from "@util/convenience-functions"; - import { IMediaData } from "./data"; export interface IMediaTransformation { @@ -20,34 +18,6 @@ export class Media { this.map = new Map(); } - // TODO: Unused - public addMedia(data: Buffer | string | Uint8Array | ArrayBuffer, transformation: IMediaTransformation): IMediaData { - const key = `${uniqueId()}.png`; - - const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data; - - const imageData: IMediaData = { - stream: newData, - fileName: key, - transformation: { - pixels: { - x: Math.round(transformation.width), - y: Math.round(transformation.height), - }, - emus: { - x: Math.round(transformation.width * 9525), - y: Math.round(transformation.height * 9525), - }, - flip: transformation.flip, - rotation: transformation.rotation ? transformation.rotation * 60000 : undefined, - }, - }; - - this.map.set(key, imageData); - - return imageData; - } - public addImage(key: string, mediaData: IMediaData): void { this.map.set(key, mediaData); } @@ -55,24 +25,4 @@ export class Media { public get Array(): readonly IMediaData[] { return Array.from(this.map.values()); } - - private convertDataURIToBinary(dataURI: string): Uint8Array { - // https://gist.github.com/borismus/1032746 - // https://github.com/mafintosh/base64-to-uint8array - const BASE64_MARKER = ";base64,"; - - const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length; - - if (typeof atob === "function") { - return new Uint8Array( - atob(dataURI.substring(base64Index)) - .split("") - .map((c) => c.charCodeAt(0)), - ); - } else { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires - const b = require("buf" + "fer"); - return new b.Buffer(dataURI, "base64"); - } - } } diff --git a/src/import-dotx/import-dotx.spec.ts b/src/import-dotx/import-dotx.spec.ts deleted file mode 100644 index c0101174cc..0000000000 --- a/src/import-dotx/import-dotx.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from "chai"; - -import { ImportDotx } from "./import-dotx"; - -describe("ImportDotx", () => { - describe("#constructor", () => { - it("should create", () => { - const file = new ImportDotx(); - - expect(file).to.deep.equal({}); - }); - }); - - // describe("#extract", () => { - // it("should create", async () => { - // const file = new ImportDotx(); - // const filePath = "./demo/dotx/template.dotx"; - - // const templateDocument = await file.extract(data); - - // await file.extract(data); - - // expect(templateDocument).to.be.equal({ currentRelationshipId: 1 }); - // }); - // }); -}); diff --git a/src/import-dotx/import-dotx.ts b/src/import-dotx/import-dotx.ts deleted file mode 100644 index d1c6142b76..0000000000 --- a/src/import-dotx/import-dotx.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* eslint-disable */ -// This will be deprecated soon -import * as JSZip from "jszip"; -import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js"; - -import { HeaderFooterReferenceType } from "@file/document/body/section-properties"; -import { FooterWrapper, IDocumentFooter } from "@file/footer-wrapper"; -import { HeaderWrapper, IDocumentHeader } from "@file/header-wrapper"; -import { Media } from "@file/media"; -import { TargetModeType } from "@file/relationships/relationship/relationship"; -import { convertToXmlComponent, ImportedXmlComponent } from "@file/xml-components"; - -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 { - readonly headers: { readonly id: number; readonly type: HeaderFooterReferenceType }[]; - readonly footers: { readonly id: number; readonly type: HeaderFooterReferenceType }[]; -} - -enum RelationshipType { - HEADER = "header", - FOOTER = "footer", - IMAGE = "image", - HYPERLINK = "hyperlink", -} - -interface IRelationshipFileInfo { - readonly id: number; - readonly target: string; - readonly type: RelationshipType; -} - -// Document Template -// https://fileinfo.com/extension/dotx -export interface IDocumentTemplate { - readonly currentRelationshipId: number; - readonly headers: IDocumentHeader[]; - readonly footers: IDocumentFooter[]; - readonly styles: string; - readonly titlePageIsDefined: boolean; - readonly media: Media; -} - -export class ImportDotx { - public async extract( - data: Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream, - ): Promise { - const zipContent = await JSZip.loadAsync(data); - - const documentContent = await zipContent.files["word/document.xml"].async("text"); - const relationshipContent = await zipContent.files["word/_rels/document.xml.rels"].async("text"); - - const documentRefs = this.extractDocumentRefs(documentContent); - const documentRelationships = this.findReferenceFiles(relationshipContent); - - const media = new Media(); - - const templateDocument: IDocumentTemplate = { - headers: await this.createHeaders(zipContent, documentRefs, documentRelationships, media, 0), - footers: await this.createFooters(zipContent, documentRefs, documentRelationships, media, documentRefs.headers.length), - currentRelationshipId: documentRefs.footers.length + documentRefs.headers.length, - styles: await zipContent.files["word/styles.xml"].async("text"), - titlePageIsDefined: this.checkIfTitlePageIsDefined(documentContent), - media: media, - }; - - return templateDocument; - } - - private async createFooters( - zipContent: JSZip, - documentRefs: IDocumentRefs, - documentRelationships: IRelationshipFileInfo[], - media: Media, - startingRelationshipId: number, - ): Promise { - const result = documentRefs.footers - .map(async (reference, i) => { - const relationshipFileInfo = documentRelationships.find((rel) => rel.id === reference.id); - - if (relationshipFileInfo === null || !relationshipFileInfo) { - throw new Error(`Can not find target file for id ${reference.id}`); - } - - const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text"); - const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; - - if (!xmlObj.elements) { - return undefined; - } - - const xmlElement = xmlObj.elements.reduce((acc, current) => (current.name === "w:ftr" ? current : acc)); - - const importedComp = convertToXmlComponent(xmlElement) as ImportedXmlComponent; - const wrapper = new FooterWrapper(media, startingRelationshipId + i, importedComp); - await this.addRelationshipToWrapper(relationshipFileInfo, zipContent, wrapper, media); - - return { type: reference.type, footer: wrapper }; - }) - .filter((x) => !!x) as Promise[]; - - return Promise.all(result); - } - - private async createHeaders( - zipContent: JSZip, - documentRefs: IDocumentRefs, - documentRelationships: IRelationshipFileInfo[], - media: Media, - startingRelationshipId: number, - ): Promise { - const result = documentRefs.headers - .map(async (reference, i) => { - const relationshipFileInfo = documentRelationships.find((rel) => rel.id === reference.id); - - if (relationshipFileInfo === null || !relationshipFileInfo) { - throw new Error(`Can not find target file for id ${reference.id}`); - } - - const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text"); - const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement; - - if (!xmlObj.elements) { - return undefined; - } - - const xmlElement = xmlObj.elements.reduce((acc, current) => (current.name === "w:hdr" ? current : acc)); - - const importedComp = convertToXmlComponent(xmlElement) as ImportedXmlComponent; - const wrapper = new HeaderWrapper(media, startingRelationshipId + i, importedComp); - await this.addRelationshipToWrapper(relationshipFileInfo, zipContent, wrapper, media); - - return { type: reference.type, header: wrapper }; - }) - .filter((x) => !!x) as Promise[]; - - return Promise.all(result); - } - - private async addRelationshipToWrapper( - relationshipFile: IRelationshipFileInfo, - zipContent: JSZip, - wrapper: HeaderWrapper | FooterWrapper, - media: Media, - ): Promise { - const refFile = zipContent.files[`word/_rels/${relationshipFile.target}.rels`]; - - if (!refFile) { - return; - } - - const xmlRef = await refFile.async("text"); - const wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.IMAGE); - const hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.HYPERLINK); - - for (const r of wrapperImagesReferences) { - const bufferType = JSZip.support.arraybuffer ? "arraybuffer" : "nodebuffer"; - const buffer = await zipContent.files[`word/${r.target}`].async(bufferType); - const mediaData = media.addMedia(buffer, { - width: 100, - height: 100, - }); - - wrapper.Relationships.createRelationship( - r.id, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", - `media/${mediaData.fileName}`, - ); - } - - for (const r of hyperLinkReferences) { - wrapper.Relationships.createRelationship( - r.id, - "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", - r.target, - TargetModeType.EXTERNAL, - ); - } - } - - private findReferenceFiles(xmlData: string): IRelationshipFileInfo[] { - const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; - const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship) - ? xmlObj.Relationships.Relationship - : [xmlObj.Relationships.Relationship]; - const relationships: IRelationshipFileInfo[] = relationXmlArray - .map((item: XMLElementCompact) => { - if (item._attributes === undefined) { - throw Error("relationship element has no attributes"); - } - return { - id: this.parseRefId(item._attributes.Id as string), - type: schemeToType[item._attributes.Type as string], - target: item._attributes.Target as string, - }; - }) - .filter((item) => item.type !== null); - return relationships; - } - - private extractDocumentRefs(xmlData: string): IDocumentRefs { - const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; - const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; - - const headerProps: XMLElementCompact = sectionProp["w:headerReference"]; - let headersXmlArray: XMLElementCompact[]; - if (headerProps === undefined) { - headersXmlArray = []; - } else if (Array.isArray(headerProps)) { - headersXmlArray = headerProps; - } else { - headersXmlArray = [headerProps]; - } - const headers = headersXmlArray.map((item) => { - if (item._attributes === undefined) { - throw Error("header reference element has no attributes"); - } - return { - type: item._attributes["w:type"] as HeaderFooterReferenceType, - id: this.parseRefId(item._attributes["r:id"] as string), - }; - }); - - const footerProps: XMLElementCompact = sectionProp["w:footerReference"]; - let footersXmlArray: XMLElementCompact[]; - if (footerProps === undefined) { - footersXmlArray = []; - } else if (Array.isArray(footerProps)) { - footersXmlArray = footerProps; - } else { - footersXmlArray = [footerProps]; - } - - const footers = footersXmlArray.map((item) => { - if (item._attributes === undefined) { - throw Error("footer reference element has no attributes"); - } - return { - type: item._attributes["w:type"] as HeaderFooterReferenceType, - id: this.parseRefId(item._attributes["r:id"] as string), - }; - }); - - return { headers, footers }; - } - - private checkIfTitlePageIsDefined(xmlData: string): boolean { - const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact; - const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"]; - - return sectionProp["w:titlePg"] !== undefined; - } - - private 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 deleted file mode 100644 index 2b12220d83..0000000000 --- a/src/import-dotx/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./import-dotx"; diff --git a/src/index.ts b/src/index.ts index 40b344721b..7598917d72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,5 @@ export { File as Document } from "./file"; export * from "./file"; export * from "./export"; -export * from "./import-dotx"; export * from "./util"; export * from "./patcher";