From 794870529ecb992ce667bb094185b5770c91c3dc Mon Sep 17 00:00:00 2001 From: singerla Date: Wed, 25 Dec 2024 23:20:13 +0100 Subject: [PATCH] feat(master): remove master and related layouts; #85 --- __tests__/add-slide-master.test.ts | 18 ++- src/automizer.ts | 32 ++++- src/classes/master.ts | 2 +- src/helper/content-tracker.ts | 8 ++ src/helper/modify-presentation-helper.ts | 163 +++++++++++++++++++---- src/helper/xml-helper.ts | 4 +- src/types/xml-types.ts | 2 + 7 files changed, 186 insertions(+), 43 deletions(-) diff --git a/__tests__/add-slide-master.test.ts b/__tests__/add-slide-master.test.ts index c3d81f4..54adec5 100644 --- a/__tests__/add-slide-master.test.ts +++ b/__tests__/add-slide-master.test.ts @@ -1,14 +1,18 @@ import Automizer from '../src/automizer'; -import { ModifyTextHelper } from '../src'; +import { ModifyTextHelper, XmlDocument, XmlHelper } from '../src'; +import { XmlRelationshipHelper } from '../src/helper/xml-relationship-helper'; +import { FileHelper } from '../src/helper/file-helper'; +import { Target } from '../src/types/types'; +import { vd } from '../src/helper/general-helper'; test('Append and modify slideMastes and use slideLayouts', async () => { const automizer = new Automizer({ templateDir: `${__dirname}/pptx-templates`, outputDir: `${__dirname}/pptx-output`, - verbosity: 2, + verbosity: 1, }); - const pres = await automizer + const pres = automizer .loadRoot(`EmptyTemplate.pptx`) .load(`SlideWithNotes.pptx`, 'notes') .load('SlidesWithAdditionalMaster.pptx') @@ -48,9 +52,11 @@ test('Append and modify slideMastes and use slideLayouts', async () => { // You need to pass the index of the desired layout after all // related layouts of all imported masters have been added to rootTemplate. slide.useSlideLayout(26); - }) + }); + + pres.removeMasters(1, 0); - .write(`add-slide-master.test.pptx`); + await pres.write(`add-slide-master.test.pptx`); - expect(pres.masters).toBe(3); + // expect(pres.masters).toBe(3); }); diff --git a/src/automizer.ts b/src/automizer.ts index 4951b19..f3624ed 100644 --- a/src/automizer.ts +++ b/src/automizer.ts @@ -7,24 +7,34 @@ import { PresentationInfo, SourceIdentifier, StatusTracker, + Target, } from './types/types'; import { IPresentationProps } from './interfaces/ipresentation-props'; import { PresTemplate } from './interfaces/pres-template'; import { RootPresTemplate } from './interfaces/root-pres-template'; import { Template } from './classes/template'; -import { ModifyXmlCallback, TemplateInfo } from './types/xml-types'; -import { GeneralHelper, log, Logger } from './helper/general-helper'; +import { + ModifyXmlCallback, + TemplateInfo, + XmlDocument, +} from './types/xml-types'; +import { GeneralHelper, log, Logger, vd } from './helper/general-helper'; import { Master } from './classes/master'; import path from 'path'; import * as fs from 'fs'; import { XmlHelper } from './helper/xml-helper'; import ModifyPresentationHelper from './helper/modify-presentation-helper'; -import { contentTracker as Tracker, ContentTracker } from './helper/content-tracker'; +import { + contentTracker as Tracker, + ContentTracker, +} from './helper/content-tracker'; import JSZip from 'jszip'; import { ISlide } from './interfaces/islide'; import { IMaster } from './interfaces/imaster'; import { ContentTypeExtension } from './enums/content-type-map'; import slugify from 'slugify'; +import { XmlRelationshipHelper } from './helper/xml-relationship-helper'; +import { FileHelper } from './helper/file-helper'; /** * Automizer @@ -389,6 +399,10 @@ export default class Automizer implements IPresentationProps { return this; } + public removeMasters(length: number, from: number) { + this.modify(ModifyPresentationHelper.removeSlideMaster(length, from, this)); + } + /** * Searches this.templates to find template by given name. * @internal @@ -463,10 +477,13 @@ export default class Automizer implements IPresentationProps { } async finalizePresentation() { + const currentMasterId = + await ModifyPresentationHelper.getFirstSlideMasterId(this); + await this.writeMasterSlides(); await this.writeSlides(); await this.writeMediaFiles(); - await this.normalizePresentation(); + await this.normalizePresentation(currentMasterId); await this.applyModifyPresentationCallbacks(); // TODO: refactor content tracker, move this to root template @@ -531,6 +548,7 @@ export default class Automizer implements IPresentationProps { this.rootTemplate.archive, `ppt/presentation.xml`, this.modifyPresentation, + this, ); } @@ -541,9 +559,11 @@ export default class Automizer implements IPresentationProps { * TODO: Use every imported image only once * TODO: Check for lost relations */ - async normalizePresentation(): Promise { + async normalizePresentation(currentMasterId: number): Promise { this.modify(ModifyPresentationHelper.normalizeSlideIds); - this.modify(ModifyPresentationHelper.normalizeSlideMasterIds); + this.modify( + ModifyPresentationHelper.normalizeSlideMasterIds(currentMasterId), + ); if (this.params.cleanup) { if (this.params.removeExistingSlides) { diff --git a/src/classes/master.ts b/src/classes/master.ts index 4360c0f..b558e59 100644 --- a/src/classes/master.ts +++ b/src/classes/master.ts @@ -72,7 +72,7 @@ export class Master extends HasShapes implements IMaster { await this.applyRelModifications(); const info = this.targetTemplate.automizer.params.showIntegrityInfo; - const assert = this.targetTemplate.automizer.params.showIntegrityInfo; + const assert = this.targetTemplate.automizer.params.assertRelatedContents; await this.checkIntegrity(info, assert); await this.cleanSlide(this.targetPath); diff --git a/src/helper/content-tracker.ts b/src/helper/content-tracker.ts index 2e0057c..56688d9 100644 --- a/src/helper/content-tracker.ts +++ b/src/helper/content-tracker.ts @@ -35,6 +35,10 @@ export class ContentTracker { relationTags = contentTrack(); + deleted: TrackedFiles = { + 'ppt/slideMasters': [], + }; + constructor() {} reset(): void { @@ -290,6 +294,10 @@ export class ContentTracker { const relations = this.relations[section]; return relations.filter((rel) => rel.attributes.Target === target); } + + deletedFile(section: string, file: any) { + this.deleted[section].push(file); + } } export const contentTracker = new ContentTracker(); diff --git a/src/helper/modify-presentation-helper.ts b/src/helper/modify-presentation-helper.ts index 84c3a1d..7dbe833 100644 --- a/src/helper/modify-presentation-helper.ts +++ b/src/helper/modify-presentation-helper.ts @@ -3,7 +3,10 @@ import { contentTracker as Tracker } from './content-tracker'; import { FileHelper } from './file-helper'; import IArchive from '../interfaces/iarchive'; import { XmlDocument, XmlElement } from '../types/xml-types'; -import { vd } from './general-helper'; +import { log, vd } from './general-helper'; +import { XmlRelationshipHelper } from './xml-relationship-helper'; +import { Target } from '../types/types'; +import Automizer from '../automizer'; export default class ModifyPresentationHelper { /** @@ -50,37 +53,66 @@ export default class ModifyPresentationHelper { * PowerPoint will complain on any p:sldLayoutId-id lower than its * corresponding slideMaster-id. omg. */ - static normalizeSlideMasterIds = async ( - xml: XmlDocument, - i: number, - archive: IArchive, - ) => { - const slides = ModifyPresentationHelper.getSlideMastersCollection(xml); - let currentId; - await XmlHelper.modifyCollectionAsync( - slides, - async (slide: XmlElement, i) => { - const masterId = i + 1; - if (i === 0) { - currentId = Number(slide.getAttribute('id')); - } - - slide.setAttribute('id', String(currentId)); - currentId++; - - const slideMasterXml = await XmlHelper.getXmlFromArchive( - archive, - `ppt/slideMasters/slideMaster${masterId}.xml`, - ); + static normalizeSlideMasterIds = + (currentId: number) => + async ( + presXml: XmlDocument, + i: number, + archive: IArchive, + pres: Automizer, + ) => { + const slides = + ModifyPresentationHelper.getSlideMastersCollection(presXml); - const slideLayouts = - slideMasterXml.getElementsByTagName('p:sldLayoutId'); - XmlHelper.modifyCollection(slideLayouts, (slideLayout: XmlElement) => { - slideLayout.setAttribute('id', String(currentId)); + const deletedIds = pres.content.deleted['ppt/slideMasters'].map( + (deleted: any) => deleted.targetMasterId, + ); + + await XmlHelper.modifyCollectionAsync( + slides, + async (slide: XmlElement, i) => { + const masterId = i + 1; + if (deletedIds.includes(masterId)) { + return; + } + + const slideMasterXml = await XmlHelper.getXmlFromArchive( + archive, + `ppt/slideMasters/slideMaster${masterId}.xml`, + ); + + slide.setAttribute('id', String(currentId)); currentId++; - }); - }, + + const slideLayouts = + slideMasterXml.getElementsByTagName('p:sldLayoutId'); + + XmlHelper.modifyCollection( + slideLayouts, + (slideLayout: XmlElement) => { + slideLayout.setAttribute('id', String(currentId)); + currentId++; + }, + ); + }, + ); + + deletedIds.forEach((deletedId) => { + const existingMasters = presXml.getElementsByTagName('p:sldMasterId'); + XmlHelper.sliceCollection(existingMasters, 1, deletedId - 1); + }); + + XmlHelper.dump(slides.item(0)); + }; + + static getFirstSlideMasterId = async (pres: Automizer) => { + const presXml = await XmlHelper.getXmlFromArchive( + pres.rootTemplate.archive, + 'ppt/presentation.xml', ); + const slides = ModifyPresentationHelper.getSlideMastersCollection(presXml); + const first = slides.item(0); + return Number(first.getAttribute('id')); }; /** @@ -154,4 +186,77 @@ export default class ModifyPresentationHelper { ); }); } + + static removeSlideMaster = + (length: number, from: number, pres: Automizer) => + async (presXml: XmlDocument) => { + for (let i = 0; i < length; i += 1) { + const targetMasterId = from + i + 1; + const masterToRemove = `slideMaster${targetMasterId}.xml`; + + const layouts = (await new XmlRelationshipHelper().initialize( + pres.rootTemplate.archive, + `${masterToRemove}.rels`, + `ppt/slideMasters/_rels`, + '../slideLayouts/slideLayout', + )) as Target[]; + + const layoutFiles = layouts.map( + (f) => f.file, // path.resolve(`ppt/presentation.xml/${f.file}`), + ); + + const removedLayouts = await FileHelper.removeFromDirectory( + pres.rootTemplate.archive, + 'ppt/slideLayouts/', + (file) => { + return !layoutFiles.includes(file.relativePath); + }, + ); + + const themes = (await new XmlRelationshipHelper().initialize( + pres.rootTemplate.archive, + `${masterToRemove}.rels`, + `ppt/slideMasters/_rels`, + '../theme/theme', + )) as Target[]; + + const themesFiles = themes.map( + (f) => f.file, // path.resolve(`ppt/presentation.xml/${f.file}`), + ); + + // const removedThemes = await FileHelper.removeFromDirectory( + // pres.rootTemplate.archive, + // 'ppt/theme/', + // (file) => { + // return !themesFiles.includes(file.relativePath); + // }, + // ); + + const removedMasters = await FileHelper.removeFromDirectory( + pres.rootTemplate.archive, + 'ppt/slideMasters/', + (file) => { + return file.relativePath === masterToRemove; + }, + ); + + const removedMasterRels = await FileHelper.removeFromDirectory( + pres.rootTemplate.archive, + 'ppt/slideMasters/_rels', + (file) => { + return file.relativePath === `${masterToRemove}.rels`; + }, + ); + + log('removed Layouts:' + removedLayouts.length, 2); + // log('removed Themes:' + removedThemes.length, 2); + log('removed Masters:' + removedMasters.length, 2); + log('removed MasterRels:' + removedMasterRels.length, 2); + + pres.content.deletedFile('ppt/slideMasters', { + masterToRemove, + targetMasterId: targetMasterId, + }); + } + }; } diff --git a/src/helper/xml-helper.ts b/src/helper/xml-helper.ts index ae2bd40..7ae0687 100644 --- a/src/helper/xml-helper.ts +++ b/src/helper/xml-helper.ts @@ -18,19 +18,21 @@ import { ContentTypeExtension, ContentTypeMap, } from '../enums/content-type-map'; +import Automizer from '../automizer'; export class XmlHelper { static async modifyXmlInArchive( archive: IArchive, file: string, callbacks: ModifyXmlCallback[], + parent?: Automizer, ): Promise { const fileProxy = await archive; const xml = await XmlHelper.getXmlFromArchive(fileProxy, file); let i = 0; for (const callback of callbacks) { - await callback(xml, i++, fileProxy); + await callback(xml, i++, fileProxy, parent); } XmlHelper.writeXmlToArchive(await archive, file, xml); diff --git a/src/types/xml-types.ts b/src/types/xml-types.ts index 2ff0707..2f110b7 100644 --- a/src/types/xml-types.ts +++ b/src/types/xml-types.ts @@ -1,5 +1,6 @@ import IArchive from '../interfaces/iarchive'; import { TableData, TableInfo } from './table-types'; +import Automizer from '../automizer'; export type DefaultAttribute = { Extension: string; @@ -94,4 +95,5 @@ export type ModifyXmlCallback = ( xml: XmlDocument | XmlElement, index?: number, archive?: IArchive, + automizer?: Automizer, ) => void;