import { Path } from "src/utils"; import { xml, XmlGeneralNode, XmlNode } from "src/xml"; import { Zip } from "src/zip"; import { Relationship, RelTargetMode } from "./relationship"; /** * A rels file is an xml file that contains the relationship information of a single docx "part". * * See: http://officeopenxml.com/anatomyofOOXML.php */ export class RelsFile { private rels: Record; private relTargets: Record; private nextRelId = 0; private readonly partDir: string; private readonly relsFilePath: string; private readonly zip: Zip; constructor(partPath: string, zip: Zip) { this.zip = zip; this.partDir = partPath && Path.getDirectory(partPath); const partFilename = partPath && Path.getFilename(partPath); this.relsFilePath = Path.combine(this.partDir, '_rels', `${partFilename ?? ''}.rels`); } /** * Returns the rel ID. */ public async add(relTarget: string, relType: string, relTargetMode?: RelTargetMode): Promise { // If relTarget is an internal file it should be relative to the part dir if (this.partDir && relTarget.startsWith(this.partDir)) { relTarget = relTarget.substring(this.partDir.length + 1); } // Parse rels file await this.parseRelsFile(); // Already exists? const relTargetKey = this.getRelTargetKey(relType, relTarget); let relId = this.relTargets[relTargetKey]; if (relId) return relId; // Create rel node relId = this.getNextRelId(); const rel = new Relationship({ id: relId, type: relType, target: relTarget, targetMode: relTargetMode }); // Update lookups this.rels[relId] = rel; this.relTargets[relTargetKey] = relId; // Return return relId; } public async list(): Promise { await this.parseRelsFile(); return Object.values(this.rels); } public absoluteTargetPath(relTarget: string): string { if (this.partDir && relTarget.startsWith(this.partDir)) { return relTarget; } return Path.combine(this.partDir, relTarget); } /** * Save the rels file back to the zip. * Called automatically by the holding `Docx` before exporting. */ public async save(): Promise { // Not change - no need to save if (!this.rels) return; // Create rels xml const root = this.createRootNode(); root.childNodes = Object.values(this.rels).map(rel => rel.toXml()); // Serialize and save const xmlContent = xml.parser.serializeFile(root); this.zip.setFile(this.relsFilePath, xmlContent); } // // Private methods // private getNextRelId(): string { let relId: string; do { this.nextRelId++; relId = 'rId' + this.nextRelId; } while (this.rels[relId]); return relId; } private async parseRelsFile(): Promise { // Already parsed if (this.rels) return; // Parse xml let root: XmlNode; const relsFile = this.zip.getFile(this.relsFilePath); if (relsFile) { const xmlString = await relsFile.getContentText(); root = xml.parser.parse(xmlString); } else { root = this.createRootNode(); } // Parse relationship nodes this.rels = {}; this.relTargets = {}; for (const relNode of root.childNodes) { const genRelNode = relNode as XmlGeneralNode; const attributes = genRelNode.attributes; if (!attributes) continue; const idAttr = attributes['Id']; if (!idAttr) continue; // Store rel const rel = Relationship.fromXml(this.partDir, genRelNode); this.rels[idAttr] = rel; // Create rel target lookup if (rel.type && rel.target) { const relTargetKey = this.getRelTargetKey(rel.type, rel.target); this.relTargets[relTargetKey] = idAttr; } } } private getRelTargetKey(type: string, target: string): string { return `${type} - ${target}`; } private createRootNode(): XmlGeneralNode { const root = xml.create.generalNode('Relationships'); root.attributes = { 'xmlns': 'http://schemas.openxmlformats.org/package/2006/relationships' }; root.childNodes = []; return root; } }