Skip to content

Commit 8fec90e

Browse files
committed
refactor(master): enhanced placeholder matching for layout merge mode; allow multi modifyElement calls
1 parent 4a67c00 commit 8fec90e

17 files changed

+675
-702
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Automizer from '../src/automizer';
2+
import { ModifyShapeHelper, ModifyTextHelper } from '../src';
3+
4+
test('Import layout from another template and merge with auto-mapping placeholders', async () => {
5+
const automizer = new Automizer({
6+
templateDir: `${__dirname}/pptx-templates`,
7+
outputDir: `${__dirname}/pptx-output`,
8+
autoImportSlideMasters: false,
9+
cleanupPlaceholders: false,
10+
});
11+
12+
const pres = automizer
13+
.loadRoot(`RootTemplate.pptx`)
14+
.load(`SlidesWithAdditionalMaster.pptx`)
15+
.load(`EmptySlidePlaceholders.pptx`)
16+
.addMaster(`SlidesWithAdditionalMaster.pptx`, 2);
17+
18+
pres.addSlide('EmptySlidePlaceholders.pptx', 2, async (slide) => {
19+
slide.mergeIntoSlideLayout('Titel und Inhalt');
20+
21+
slide.addElement(
22+
'EmptySlidePlaceholders.pptx',
23+
2,
24+
'@TitleNoPlaceholder',
25+
ModifyTextHelper.setText('Test orig add'),
26+
);
27+
slide.modifyElement('@TitleNoPlaceholder', [
28+
ModifyShapeHelper.roundedCorners(1400),
29+
]);
30+
slide.modifyElement('@TitleNoPlaceholder', [
31+
ModifyTextHelper.setText('Test 1235'),
32+
]);
33+
});
34+
35+
await pres.write(`add-slide-master-merge-layout.test.pptx`);
36+
});
6.02 KB
Binary file not shown.
565 Bytes
Binary file not shown.

src/classes/has-shapes.ts

Lines changed: 106 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
import { ContentTracker } from '../helper/content-tracker';
2424
import {
2525
ElementInfo,
26+
ModifyXmlCallback,
27+
PlaceholderInfo,
2628
RelationshipAttribute,
2729
SlideListAttribute,
2830
TemplateSlideInfo,
@@ -39,8 +41,9 @@ import { XmlSlideHelper } from '../helper/xml-slide-helper';
3941
import { OLEObject } from '../shapes/ole';
4042
import { Hyperlink } from '../shapes/hyperlink';
4143
import { HyperlinkProcessor } from '../helper/hyperlink-processor';
42-
import { vd } from '../helper/general-helper';
4344
import { Diagram } from '../shapes/diagram';
45+
import { ISlide } from '../interfaces/islide';
46+
import { GeneralHelper, vd } from '../helper/general-helper';
4447

4548
export default class HasShapes {
4649
/**
@@ -83,6 +86,11 @@ export default class HasShapes {
8386
* @internal
8487
*/
8588
targetPath: string;
89+
/**
90+
* Preparations of root template slide
91+
* @internal
92+
*/
93+
preparations: SlideModificationCallback[];
8694
/**
8795
* Modifications of root template slide
8896
* @internal
@@ -92,7 +100,7 @@ export default class HasShapes {
92100
* Modifications of slide relations
93101
* @internal
94102
*/
95-
relModifications: SlideModificationCallback[];
103+
relModifications: ModifyXmlCallback[];
96104
/**
97105
* Import elements of slide
98106
* @internal
@@ -144,6 +152,7 @@ export default class HasShapes {
144152
];
145153
targetType: ShapeTargetType;
146154
params: AutomizerParams;
155+
presentation: IPresentationProps;
147156

148157
cleanupPlaceholders = false;
149158

@@ -153,11 +162,14 @@ export default class HasShapes {
153162
}) {
154163
this.sourceTemplate = params.template;
155164

165+
this.preparations = [];
156166
this.modifications = [];
157167
this.relModifications = [];
158168
this.importElements = [];
159169
this.generateElements = [];
160170

171+
this.presentation = params.presentation;
172+
161173
this.status = params.presentation.status;
162174
this.content = params.presentation.content;
163175

@@ -180,17 +192,17 @@ export default class HasShapes {
180192
/**
181193
* Asynchronously retrieves all elements from the slide.
182194
* @param filterTags Use an array of strings to filter parent tags (e.g. 'sp')
183-
* @param slideInfo Use placeholder position from layout as fallback
195+
* @param layoutPlaceholders
184196
* @returns {Promise<ElementInfo[]>} A promise that resolves to an array of ElementInfo objects.
185197
*/
186198
async getAllElements(
187199
filterTags?: string[],
188-
slideInfo?: TemplateSlideInfo,
200+
layoutPlaceholders?: PlaceholderInfo[],
189201
): Promise<ElementInfo[]> {
190202
const xmlSlideHelper = await this.getSlideHelper();
191203

192204
// Get all ElementInfo objects
193-
return xmlSlideHelper.getAllElements(filterTags, slideInfo);
205+
return xmlSlideHelper.getAllElements(filterTags, layoutPlaceholders);
194206
}
195207

196208
/**
@@ -219,30 +231,48 @@ export default class HasShapes {
219231
* @returns {Promise<XmlSlideHelper>} An instance of XmlSlideHelper.
220232
*/
221233
async getSlideHelper(): Promise<XmlSlideHelper> {
234+
return this.getSlideHelperInstance(
235+
this.sourceTemplate.archive,
236+
this.sourcePath,
237+
this.sourceNumber,
238+
);
239+
}
240+
241+
async getSlideHelperInstance(
242+
archive: IArchive,
243+
path: string,
244+
number: number,
245+
): Promise<XmlSlideHelper> {
222246
try {
223247
// Retrieve the slide XML data
224-
const slideXml = await XmlHelper.getXmlFromArchive(
225-
this.sourceTemplate.archive,
226-
this.sourcePath,
227-
);
248+
const slideXml = await XmlHelper.getXmlFromArchive(archive, path);
228249

229250
const sourceLayoutId = await XmlRelationshipHelper.getSlideLayoutNumber(
230-
this.sourceTemplate.archive,
231-
this.sourceNumber,
251+
archive,
252+
number,
232253
);
233254

234255
// Initialize the XmlSlideHelper
235256
return new XmlSlideHelper(slideXml, {
236-
sourceArchive: this.sourceTemplate.archive,
237-
slideNumber: this.sourceNumber,
238-
sourceLayoutId
257+
sourceArchive: archive,
258+
slideNumber: number,
259+
sourceLayoutId,
239260
});
240261
} catch (error) {
241262
// Log the error message
242263
throw new Error(error.message);
243264
}
244265
}
245266

267+
/**
268+
* Push preparations list
269+
* @internal
270+
* @param callback
271+
*/
272+
prepare(callback: SlideModificationCallback): void {
273+
this.preparations.push(callback);
274+
}
275+
246276
/**
247277
* Push modifications list
248278
* @internal
@@ -257,7 +287,7 @@ export default class HasShapes {
257287
* @internal
258288
* @param callback
259289
*/
260-
modifyRelations(callback: SlideModificationCallback): void {
290+
modifyRelations(callback: ModifyXmlCallback): void {
261291
this.relModifications.push(callback);
262292
}
263293

@@ -369,55 +399,6 @@ export default class HasShapes {
369399
});
370400
}
371401

372-
/**
373-
* Checks if an element with the same selector has already been imported or modified.
374-
* This function helps to apply placeholder modifications properly.
375-
*
376-
* @param {FindElementSelector} selector - The selector used to identify an element.
377-
* Can be a string or an object with name and optional creationId/nameIdx.
378-
* @returns {ImportElement|undefined} The existing element if found, otherwise undefined.
379-
*/
380-
getAlreadyModifiedElement(
381-
selector: FindElementSelector,
382-
): ImportElement | undefined {
383-
// Search through previously imported/modified elements
384-
return this.importElements.find((element) => {
385-
// Skip comparison if either selector is not an object
386-
if (
387-
typeof selector !== 'object' ||
388-
typeof element.selector !== 'object'
389-
) {
390-
return false;
391-
}
392-
393-
// Case 1: Element without creationId - match by name and nameIdx
394-
if (!selector.creationId) {
395-
return (
396-
selector.name === element.selector.name &&
397-
selector.nameIdx === element.selector.nameIdx
398-
);
399-
}
400-
401-
// Case 2: Element with creationId - match by name and normalized creationId
402-
if (selector.creationId && element.selector?.creationId) {
403-
// Normalize creationIds by removing curly braces
404-
const normalizedSelectorId = selector.creationId.replace(/{|}/g, '');
405-
const normalizedElementId = element.selector.creationId.replace(
406-
/{|}/g,
407-
'',
408-
);
409-
410-
return (
411-
selector.name === element.selector.name &&
412-
normalizedSelectorId === normalizedElementId
413-
);
414-
}
415-
416-
// No match found for this element
417-
return false;
418-
});
419-
}
420-
421402
/**
422403
* ToDo: Implement creationIds as well for slideMasters
423404
*
@@ -456,13 +437,19 @@ export default class HasShapes {
456437
}
457438

458439
/**
459-
* Imported selected elements
440+
* Imported selected elements while merging multiple element modifications
460441
* @internal
461442
*/
462443
async importedSelectedElements(): Promise<void> {
444+
await this.getUniqueImportedElements();
445+
463446
for (const element of this.importElements) {
464-
const info = await this.getElementInfo(element);
447+
if (!element.info) {
448+
// Element has already been modified, skipping...
449+
continue;
450+
}
465451

452+
const info = element.info;
466453
switch (info?.type) {
467454
case ElementType.Chart:
468455
await new Chart(info, this.targetType)[info.mode](
@@ -515,6 +502,35 @@ export default class HasShapes {
515502
}
516503
}
517504

505+
/**
506+
* Processes and updates the list of imported elements by ensuring their uniqueness based on a generated hash.
507+
* If duplicate elements are found, their callbacks are merged.
508+
*
509+
* @return {Promise<void>} Resolves when the process of identifying and updating unique imported elements is complete.
510+
*/
511+
async getUniqueImportedElements(): Promise<void> {
512+
for (const element of this.importElements) {
513+
const info = await this.getElementInfo(element);
514+
const eleHash =
515+
516+
XmlHelper.createHashFromXmlElement(info.sourceElement, element);
517+
518+
const alreadyImported = this.importElements.find(
519+
(ele) => ele.info?.hash === eleHash,
520+
);
521+
if (alreadyImported) {
522+
alreadyImported.callback = GeneralHelper.arrayify(
523+
alreadyImported.callback,
524+
);
525+
const pushCallbacks = GeneralHelper.arrayify(element.callback);
526+
alreadyImported.callback.push(...pushCallbacks);
527+
} else {
528+
info.hash = eleHash;
529+
element.info = info;
530+
}
531+
}
532+
}
533+
518534
/**
519535
* Gets element info
520536
* @internal
@@ -898,7 +914,10 @@ export default class HasShapes {
898914
).modifyOnAddedSlide(this.targetTemplate, this.targetNumber);
899915
}
900916

901-
const diagrams = await Diagram.getAllOnSlide(this.sourceArchive, this.relsPath);
917+
const diagrams = await Diagram.getAllOnSlide(
918+
this.sourceArchive,
919+
this.relsPath,
920+
);
902921
for (const diagram of diagrams) {
903922
await new Diagram(
904923
{
@@ -1082,7 +1101,25 @@ export default class HasShapes {
10821101
}
10831102

10841103
/**
1085-
* Applys modifications
1104+
* Applys slide preparation callbacks
1105+
* Will be executed before any shape modifications callback
1106+
* @internal
1107+
* @returns modifications
1108+
*/
1109+
async applyPreparations(): Promise<void> {
1110+
for (const modification of this.preparations) {
1111+
const xml = await XmlHelper.getXmlFromArchive(
1112+
this.targetArchive,
1113+
this.targetPath,
1114+
);
1115+
await modification(xml, this);
1116+
XmlHelper.writeXmlToArchive(this.targetArchive, this.targetPath, xml);
1117+
}
1118+
}
1119+
1120+
/**
1121+
* Applys slide modification callbacks
1122+
* Will be executed after all shape modifications callbacks
10861123
* @internal
10871124
* @returns modifications
10881125
*/
@@ -1092,7 +1129,7 @@ export default class HasShapes {
10921129
this.targetArchive,
10931130
this.targetPath,
10941131
);
1095-
modification(xml);
1132+
await modification(xml, this);
10961133
XmlHelper.writeXmlToArchive(this.targetArchive, this.targetPath, xml);
10971134
}
10981135
}

src/classes/shape.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,6 @@ export class Shape {
193193
}
194194
});
195195

196-
// if(cb && typeof cb === 'function') {
197-
// XmlHelper.dump(targetSlideXml)
198-
// }
199-
200196
XmlHelper.writeXmlToArchive(
201197
this.targetArchive,
202198
this.targetSlideFile,

0 commit comments

Comments
 (0)