2018-09-06 08:30:23 +01:00
|
|
|
import * as JSZip from "jszip";
|
2018-10-17 09:15:32 +03:00
|
|
|
import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js";
|
2018-10-16 11:28:25 +03:00
|
|
|
|
2018-09-06 08:30:23 +01:00
|
|
|
import { FooterReferenceType } from "file/document/body/section-properties/footer-reference";
|
|
|
|
import { HeaderReferenceType } from "file/document/body/section-properties/header-reference";
|
2018-09-17 20:06:51 +01:00
|
|
|
import { FooterWrapper, IDocumentFooter } from "file/footer-wrapper";
|
|
|
|
import { HeaderWrapper, IDocumentHeader } from "file/header-wrapper";
|
2018-10-23 00:41:18 +01:00
|
|
|
import { Media } from "file/media";
|
2018-12-05 00:05:11 +00:00
|
|
|
import { TargetModeType } from "file/relationships/relationship/relationship";
|
2018-09-26 14:33:05 +03:00
|
|
|
import { Styles } from "file/styles";
|
|
|
|
import { ExternalStylesFactory } from "file/styles/external-styles-factory";
|
2018-12-05 00:05:11 +00:00
|
|
|
import { convertToXmlComponent, ImportedXmlComponent } from "file/xml-components";
|
2018-09-26 14:33:05 +03:00
|
|
|
|
2018-09-04 17:16:31 +03:00
|
|
|
const schemeToType = {
|
2018-09-06 08:30:23 +01:00
|
|
|
"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",
|
2018-10-02 16:17:26 +03:00
|
|
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink": "hyperlink",
|
2018-09-06 08:30:23 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
interface IDocumentRefs {
|
2018-11-02 02:51:57 +00:00
|
|
|
readonly headers: Array<{ readonly id: number; readonly type: HeaderReferenceType }>;
|
|
|
|
readonly footers: Array<{ readonly id: number; readonly type: FooterReferenceType }>;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
enum RelationshipType {
|
|
|
|
HEADER = "header",
|
|
|
|
FOOTER = "footer",
|
|
|
|
IMAGE = "image",
|
|
|
|
HYPERLINK = "hyperlink",
|
|
|
|
}
|
|
|
|
|
2018-09-17 20:06:51 +01:00
|
|
|
interface IRelationshipFileInfo {
|
2018-11-02 02:51:57 +00:00
|
|
|
readonly id: number;
|
|
|
|
readonly target: string;
|
2018-12-05 00:05:11 +00:00
|
|
|
readonly type: RelationshipType;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-09-19 23:04:34 +01:00
|
|
|
// Document Template
|
|
|
|
// https://fileinfo.com/extension/dotx
|
|
|
|
export interface IDocumentTemplate {
|
2018-11-02 02:51:57 +00:00
|
|
|
readonly currentRelationshipId: number;
|
|
|
|
readonly headers: IDocumentHeader[];
|
|
|
|
readonly footers: IDocumentFooter[];
|
|
|
|
readonly styles: Styles;
|
|
|
|
readonly titlePageIsDefined: boolean;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
2018-08-29 18:36:48 +03:00
|
|
|
|
2018-09-24 22:00:08 +01:00
|
|
|
export class ImportDotx {
|
2018-11-02 02:51:57 +00:00
|
|
|
// tslint:disable-next-line:readonly-keyword
|
2018-09-06 08:30:23 +01:00
|
|
|
private currentRelationshipId: number;
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-08-29 18:36:48 +03:00
|
|
|
constructor() {
|
2018-09-06 08:30:23 +01:00
|
|
|
this.currentRelationshipId = 1;
|
2018-08-29 18:36:48 +03:00
|
|
|
}
|
|
|
|
|
2018-09-19 23:04:34 +01:00
|
|
|
public async extract(data: Buffer): Promise<IDocumentTemplate> {
|
2018-09-06 08:30:23 +01:00
|
|
|
const zipContent = await JSZip.loadAsync(data);
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-09-26 14:33:05 +03:00
|
|
|
const stylesContent = await zipContent.files["word/styles.xml"].async("text");
|
2018-12-18 23:37:21 +00:00
|
|
|
const documentContent = await zipContent.files["word/document.xml"].async("text");
|
|
|
|
const relationshipContent = await zipContent.files["word/_rels/document.xml.rels"].async("text");
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
const stylesFactory = new ExternalStylesFactory();
|
|
|
|
const documentRefs = this.extractDocumentRefs(documentContent);
|
|
|
|
const documentRelationships = this.findReferenceFiles(relationshipContent);
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-10-23 00:31:51 +01:00
|
|
|
const media = new Media();
|
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
const templateDocument: IDocumentTemplate = {
|
|
|
|
headers: await this.createHeaders(zipContent, documentRefs, documentRelationships, media),
|
|
|
|
footers: await this.createFooters(zipContent, documentRefs, documentRelationships, media),
|
|
|
|
currentRelationshipId: this.currentRelationshipId,
|
|
|
|
styles: stylesFactory.newInstance(stylesContent),
|
|
|
|
titlePageIsDefined: this.checkIfTitlePageIsDefined(documentContent),
|
|
|
|
};
|
2018-09-06 08:30:23 +01:00
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
return templateDocument;
|
|
|
|
}
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
private async createFooters(
|
|
|
|
zipContent: JSZip,
|
|
|
|
documentRefs: IDocumentRefs,
|
|
|
|
documentRelationships: IRelationshipFileInfo[],
|
|
|
|
media: Media,
|
|
|
|
): Promise<IDocumentFooter[]> {
|
2018-09-17 20:06:51 +01:00
|
|
|
const footers: IDocumentFooter[] = [];
|
2018-12-18 23:37:21 +00:00
|
|
|
|
2018-09-06 08:30:23 +01:00
|
|
|
for (const footerRef of documentRefs.footers) {
|
2018-09-17 20:06:51 +01:00
|
|
|
const relationFileInfo = documentRelationships.find((rel) => rel.id === footerRef.id);
|
2018-12-18 23:37:21 +00:00
|
|
|
|
2018-09-06 08:30:23 +01:00
|
|
|
if (relationFileInfo === null || !relationFileInfo) {
|
|
|
|
throw new Error(`Can not find target file for id ${footerRef.id}`);
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
2018-12-18 23:37:21 +00:00
|
|
|
|
2018-10-02 16:17:26 +03:00
|
|
|
const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text");
|
2018-10-17 09:15:32 +03:00
|
|
|
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement;
|
|
|
|
let footerXmlElement: XMLElement | undefined;
|
|
|
|
for (const xmlElm of xmlObj.elements || []) {
|
|
|
|
if (xmlElm.name === "w:ftr") {
|
|
|
|
footerXmlElement = xmlElm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (footerXmlElement === undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const importedComp = convertToXmlComponent(footerXmlElement) as ImportedXmlComponent;
|
2018-10-23 00:31:51 +01:00
|
|
|
const footer = new FooterWrapper(media, this.currentRelationshipId++, importedComp);
|
2018-12-05 00:05:11 +00:00
|
|
|
await this.addRelationshipToWrapper(relationFileInfo, zipContent, footer, media);
|
2018-09-06 08:30:23 +01:00
|
|
|
footers.push({ type: footerRef.type, footer });
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
return footers;
|
|
|
|
}
|
2018-12-05 19:47:56 +00:00
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
private async createHeaders(
|
|
|
|
zipContent: JSZip,
|
|
|
|
documentRefs: IDocumentRefs,
|
|
|
|
documentRelationships: IRelationshipFileInfo[],
|
|
|
|
media: Media,
|
|
|
|
): Promise<IDocumentHeader[]> {
|
|
|
|
const headers: IDocumentHeader[] = [];
|
|
|
|
|
|
|
|
for (const headerRef of documentRefs.headers) {
|
|
|
|
const relationFileInfo = documentRelationships.find((rel) => rel.id === headerRef.id);
|
|
|
|
if (relationFileInfo === null || !relationFileInfo) {
|
|
|
|
throw new Error(`Can not find target file for id ${headerRef.id}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const xmlData = await zipContent.files[`word/${relationFileInfo.target}`].async("text");
|
|
|
|
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement;
|
|
|
|
let headerXmlElement: XMLElement | undefined;
|
|
|
|
for (const xmlElm of xmlObj.elements || []) {
|
|
|
|
if (xmlElm.name === "w:hdr") {
|
|
|
|
headerXmlElement = xmlElm;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (headerXmlElement === undefined) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const importedComp = convertToXmlComponent(headerXmlElement) as ImportedXmlComponent;
|
|
|
|
const header = new HeaderWrapper(media, this.currentRelationshipId++, importedComp);
|
|
|
|
// await this.addMedia(zipContent, media, documentRefs, documentRelationships);
|
|
|
|
await this.addRelationshipToWrapper(relationFileInfo, zipContent, header, media);
|
|
|
|
headers.push({ type: headerRef.type, header });
|
|
|
|
}
|
|
|
|
|
|
|
|
return headers;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
private async addRelationshipToWrapper(
|
2018-10-23 00:31:51 +01:00
|
|
|
relationhipFile: IRelationshipFileInfo,
|
2018-09-07 21:48:59 +01:00
|
|
|
zipContent: JSZip,
|
|
|
|
wrapper: HeaderWrapper | FooterWrapper,
|
2018-12-05 00:05:11 +00:00
|
|
|
media: Media,
|
2018-09-07 21:48:59 +01:00
|
|
|
): Promise<void> {
|
2018-10-23 00:31:51 +01:00
|
|
|
const refFile = zipContent.files[`word/_rels/${relationhipFile.target}.rels`];
|
2018-12-05 00:05:11 +00:00
|
|
|
|
|
|
|
if (!refFile) {
|
|
|
|
return;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
2018-12-05 00:05:11 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2018-09-06 08:30:23 +01:00
|
|
|
for (const r of wrapperImagesReferences) {
|
2018-10-02 16:17:26 +03:00
|
|
|
const buffer = await zipContent.files[`word/${r.target}`].async("nodebuffer");
|
2018-12-05 00:05:11 +00:00
|
|
|
const mediaData = media.addMedia(buffer, r.id);
|
2018-12-18 23:37:21 +00:00
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
wrapper.Relationships.createRelationship(
|
|
|
|
mediaData.referenceId,
|
|
|
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
|
|
`media/${mediaData.fileName}`,
|
|
|
|
);
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
2018-12-05 00:05:11 +00:00
|
|
|
|
2018-10-02 16:17:26 +03:00
|
|
|
for (const r of hyperLinkReferences) {
|
2018-12-05 00:05:11 +00:00
|
|
|
wrapper.Relationships.createRelationship(
|
|
|
|
r.id,
|
|
|
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
|
|
r.target,
|
|
|
|
TargetModeType.EXTERNAL,
|
|
|
|
);
|
2018-10-02 16:17:26 +03:00
|
|
|
}
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
private findReferenceFiles(xmlData: string): IRelationshipFileInfo[] {
|
2018-10-17 09:15:32 +03:00
|
|
|
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
|
2018-09-06 08:30:23 +01:00
|
|
|
const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship)
|
|
|
|
? xmlObj.Relationships.Relationship
|
|
|
|
: [xmlObj.Relationships.Relationship];
|
2018-10-31 22:09:36 +00:00
|
|
|
const relationships: IRelationshipFileInfo[] = relationXmlArray
|
2018-10-17 09:15:32 +03:00
|
|
|
.map((item: XMLElementCompact) => {
|
|
|
|
if (item._attributes === undefined) {
|
|
|
|
throw Error("relationship element has no attributes");
|
|
|
|
}
|
2018-09-04 17:16:31 +03:00
|
|
|
return {
|
2018-10-17 09:15:32 +03:00
|
|
|
id: this.parseRefId(item._attributes.Id as string),
|
|
|
|
type: schemeToType[item._attributes.Type as string],
|
|
|
|
target: item._attributes.Target as string,
|
2018-09-06 08:30:23 +01:00
|
|
|
};
|
2018-09-04 17:16:31 +03:00
|
|
|
})
|
2018-09-06 08:30:23 +01:00
|
|
|
.filter((item) => item.type !== null);
|
2018-10-23 00:31:51 +01:00
|
|
|
return relationships;
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
private extractDocumentRefs(xmlData: string): IDocumentRefs {
|
2018-10-17 09:15:32 +03:00
|
|
|
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
|
2018-09-06 08:30:23 +01:00
|
|
|
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
|
|
|
|
|
2018-10-17 09:15:32 +03:00
|
|
|
const headerProps: XMLElementCompact = sectionProp["w:headerReference"];
|
|
|
|
let headersXmlArray: XMLElementCompact[];
|
2018-10-02 16:02:28 +03:00
|
|
|
if (headerProps === undefined) {
|
|
|
|
headersXmlArray = [];
|
|
|
|
} else if (Array.isArray(headerProps)) {
|
|
|
|
headersXmlArray = headerProps;
|
|
|
|
} else {
|
|
|
|
headersXmlArray = [headerProps];
|
|
|
|
}
|
2018-09-06 08:30:23 +01:00
|
|
|
const headers = headersXmlArray.map((item) => {
|
2018-10-17 09:15:32 +03:00
|
|
|
if (item._attributes === undefined) {
|
|
|
|
throw Error("header referecne element has no attributes");
|
|
|
|
}
|
2018-09-06 08:30:23 +01:00
|
|
|
return {
|
2018-10-17 09:15:32 +03:00
|
|
|
type: item._attributes["w:type"] as HeaderReferenceType,
|
|
|
|
id: this.parseRefId(item._attributes["r:id"] as string),
|
2018-09-06 08:30:23 +01:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2018-10-17 09:15:32 +03:00
|
|
|
const footerProps: XMLElementCompact = sectionProp["w:footerReference"];
|
|
|
|
let footersXmlArray: XMLElementCompact[];
|
2018-10-02 16:02:28 +03:00
|
|
|
if (footerProps === undefined) {
|
|
|
|
footersXmlArray = [];
|
|
|
|
} else if (Array.isArray(footerProps)) {
|
|
|
|
footersXmlArray = footerProps;
|
|
|
|
} else {
|
|
|
|
footersXmlArray = [footerProps];
|
|
|
|
}
|
|
|
|
|
2018-09-06 08:30:23 +01:00
|
|
|
const footers = footersXmlArray.map((item) => {
|
2018-10-17 09:15:32 +03:00
|
|
|
if (item._attributes === undefined) {
|
|
|
|
throw Error("footer referecne element has no attributes");
|
|
|
|
}
|
2018-09-06 08:30:23 +01:00
|
|
|
return {
|
2018-10-17 09:15:32 +03:00
|
|
|
type: item._attributes["w:type"] as FooterReferenceType,
|
|
|
|
id: this.parseRefId(item._attributes["r:id"] as string),
|
2018-09-06 08:30:23 +01:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
return { headers, footers };
|
2018-08-29 18:36:48 +03:00
|
|
|
}
|
2018-09-04 17:16:31 +03:00
|
|
|
|
2018-12-18 23:37:21 +00:00
|
|
|
private checkIfTitlePageIsDefined(xmlData: string): boolean {
|
2018-10-17 09:15:32 +03:00
|
|
|
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
|
2018-10-02 17:52:55 +03:00
|
|
|
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
|
2018-12-16 01:56:42 +00:00
|
|
|
|
2018-10-02 17:52:55 +03:00
|
|
|
return sectionProp["w:titlePg"] !== undefined;
|
|
|
|
}
|
|
|
|
|
2018-12-05 00:05:11 +00:00
|
|
|
private parseRefId(str: string): number {
|
2018-09-06 08:30:23 +01:00
|
|
|
const match = /^rId(\d+)$/.exec(str);
|
|
|
|
if (match === null) {
|
|
|
|
throw new Error("Invalid ref id");
|
2018-09-04 17:16:31 +03:00
|
|
|
}
|
2018-09-06 08:30:23 +01:00
|
|
|
return parseInt(match[1], 10);
|
|
|
|
}
|
2018-08-29 18:36:48 +03:00
|
|
|
}
|