Skip to content

Commit 2d8a587

Browse files
committed
feat(text): enhance replaceText to support multi-paragraph / multi-textRuns
1 parent 6c86585 commit 2d8a587

File tree

6 files changed

+270
-78
lines changed

6 files changed

+270
-78
lines changed

src/dev.ts

Lines changed: 48 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,88 +9,61 @@ const run = async () => {
99
// Step 1: Create a pptx with images and a chart inside.
1010
// The chart is modified by pptx-automizer
1111

12-
const writer = new Automizer({
12+
const automizer = new Automizer({
1313
templateDir,
1414
outputDir,
15-
verbosity: 0,
16-
removeExistingSlides: true
15+
verbosity: 2,
16+
removeExistingSlides: true,
1717
});
1818

19-
const pres = writer
20-
.loadRoot(`RootTemplate.pptx`)
21-
.load(`EmptySlide.pptx`, 'empty')
22-
.load(`SlideWithImages.pptx`, 'images')
23-
.load(`ChartBarsStacked.pptx`, 'charts');
24-
25-
const dataSmaller = {
26-
series: [{ label: 'series s1' }, { label: 'series s2' }],
27-
categories: [
28-
{ label: 'item test r1', values: [10, null] },
29-
{ label: 'item test r2', values: [12, 18] },
30-
],
31-
};
19+
const pres = automizer.loadRoot(`RootTemplate.pptx`).load(`TextReplace.pptx`);
3220

3321
await pres
34-
.addSlide('empty', 1, (slide) => {
35-
slide.addElement('images', 1, 'Grafik 5');
36-
})
37-
.addSlide('charts', 1, (slide) => {
38-
slide.modifyElement('BarsStacked', [modify.setChartData(dataSmaller)]);
22+
.addSlide('TextReplace.pptx', 1, (slide) => {
23+
slide.modifyElement('setText', modify.setMultiText([
24+
{
25+
paragraph: {
26+
bullet: true,
27+
level: 1,
28+
marginLeft: -187325,
29+
indent: 541338,
30+
alignment: 'left'
31+
},
32+
textRuns: [
33+
{
34+
text: 'test ',
35+
style: {
36+
color: {
37+
type: 'srgbClr',
38+
value: 'CCCCCC'
39+
}
40+
}
41+
},
42+
{
43+
text: 'test 2',
44+
style: {
45+
size: 700,
46+
color: {
47+
type: 'srgbClr',
48+
value: 'FF0000'
49+
}
50+
}
51+
},
52+
{
53+
text: 'test 3',
54+
style: {
55+
size: 1200,
56+
color: {
57+
type: 'srgbClr',
58+
value: '00FF00'
59+
}
60+
}
61+
}
62+
]
63+
}
64+
]));
3965
})
40-
.write(`modify-automizer-generated-file.tmp.test.pptx`);
41-
42-
// Step 2: Create a copy of the generated file in templateDir
43-
// and load it as a normal template
44-
45-
await fs.promises.copyFile(
46-
`${outputDir}/modify-automizer-generated-file.tmp.test.pptx`,
47-
`${templateDir}/PptxAutomizerGeneratedFile.pptx`,
48-
);
49-
50-
const reader = new Automizer({
51-
templateDir,
52-
outputDir,
53-
// This will display all log() output
54-
verbosity: 2
55-
});
56-
57-
58-
const dataSmallerMod = {
59-
series: [{ label: 'series s3' }, { label: 'series s4' }],
60-
categories: [
61-
{ label: 'item test r3', values: [22, 45] },
62-
{ label: 'item test r4', values: [23, 46] },
63-
{ label: 'item test r5', values: [24, 47] },
64-
],
65-
};
66-
67-
const pres2 = reader
68-
.loadRoot(`RootTemplate.pptx`)
69-
.load(`EmptySlide.pptx`, 'empty')
70-
.load(`SlideWithImages.pptx`, 'images')
71-
.load(`PptxAutomizerGeneratedFile.pptx`, 'generated')
72-
73-
const presInfo = await pres2.getInfo();
74-
const slides = presInfo
75-
.slidesByTemplate(`generated`)
76-
77-
console.log(`The re-imported file PptxAutomizerGeneratedFile.pptx seems to have ${slides.length} slides`)
78-
// But only 2 slides were expected with removeExistingSlides: true
79-
80-
const result2 = await pres2
81-
.addSlide('empty', 1, (slide) => {
82-
slide.addElement('images', 1, 'Grafik 5');
83-
})
84-
.addSlide('generated', 2, (slide) => {
85-
slide.addElement('images', 1, 'Grafik 5');
86-
})
87-
.addSlide('generated', 3, (slide) => {
88-
slide.modifyElement('BarsStacked', [modify.setChartData(dataSmallerMod)]);
89-
})
90-
.write(`modify-automizer-generated-file.test.pptx`);
91-
92-
vd(result2)
93-
66+
.write(`modify-multi-text.test.pptx`);
9467
};
9568

9669
run().catch((error) => {

src/helper/modify-color-helper.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Color, ImageStyle } from '../types/modify-types';
22
import XmlElements from './xml-elements';
33
import { XmlHelper } from './xml-helper';
44
import { XmlElement } from '../types/xml-types';
5+
import { vd } from './general-helper';
56

67
export default class ModifyColorHelper {
78
/**
@@ -21,11 +22,16 @@ export default class ModifyColorHelper {
2122
const solidFill = new XmlElements(element, {
2223
color: color,
2324
}).solidFill();
24-
element.appendChild(solidFill);
25+
26+
if (element.firstChild && index && index === 0) {
27+
element.insertBefore(solidFill, element.firstChild);
28+
} else {
29+
element.appendChild(solidFill);
30+
}
2531
return;
2632
}
2733

28-
let targetIndex = !index
34+
const targetIndex = !index
2935
? 0
3036
: index === 'last'
3137
? solidFills.length - 1

src/helper/modify-text-helper.ts

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import ModifyXmlHelper from './modify-xml-helper';
44
import { XmlElement } from '../types/xml-types';
55
import { vd } from './general-helper';
66
import XmlElements from './xml-elements';
7+
import { XmlHelper } from './xml-helper';
8+
import {MultiTextParagraph} from '../interfaces/imulti-text';
79

810
export default class ModifyTextHelper {
911
/**
@@ -34,8 +36,168 @@ export default class ModifyTextHelper {
3436
}
3537
};
3638

39+
40+
static setMultiText =
41+
(paragraphs: MultiTextParagraph[]) =>
42+
(element: XmlElement): void => {
43+
// Find or create the txBody element
44+
let txBody = element.getElementsByTagName('p:txBody')[0];
45+
if (!txBody) {
46+
txBody = element.ownerDocument.createElement('p:txBody');
47+
element.appendChild(txBody);
48+
49+
// Create required bodyPr and lstStyle elements
50+
const bodyPr = element.ownerDocument.createElement('a:bodyPr');
51+
txBody.appendChild(bodyPr);
52+
53+
const lstStyle = element.ownerDocument.createElement('a:lstStyle');
54+
txBody.appendChild(lstStyle);
55+
}
56+
57+
// Clear existing paragraphs
58+
const existingParagraphs = txBody.getElementsByTagName('a:p');
59+
const length = existingParagraphs.length;
60+
for (let i = length - 1; i >= 0; i--) {
61+
const paragraph = existingParagraphs[i];
62+
paragraph.parentNode.removeChild(paragraph);
63+
}
64+
65+
// Process each paragraph
66+
paragraphs.forEach(para => {
67+
// Create a new paragraph element
68+
const p = element.ownerDocument.createElement('a:p');
69+
txBody.appendChild(p);
70+
71+
// Create paragraph properties
72+
const pPr = element.ownerDocument.createElement('a:pPr');
73+
p.appendChild(pPr);
74+
75+
// Apply paragraph styling
76+
if (para.paragraph) {
77+
// Set bullet level
78+
if (para.paragraph.level !== undefined) {
79+
pPr.setAttribute('lvl', String(para.paragraph.level));
80+
}
81+
82+
// Set bullet configuration
83+
if (para.paragraph.bullet) {
84+
const buChar = element.ownerDocument.createElement('a:buChar');
85+
buChar.setAttribute('char', '•'); // Default bullet character
86+
pPr.appendChild(buChar);
87+
} else if (para.paragraph.level === 0 || para.paragraph.bullet === false) {
88+
const buNone = element.ownerDocument.createElement('a:buNone');
89+
pPr.appendChild(buNone);
90+
}
91+
92+
// Set alignment
93+
if (para.paragraph.alignment) {
94+
const algn = element.ownerDocument.createElement('a:algn');
95+
algn.setAttribute('val', para.paragraph.alignment);
96+
pPr.appendChild(algn);
97+
}
98+
99+
// Set custom indentation
100+
if (para.paragraph.indent !== undefined) {
101+
pPr.setAttribute('indent', String(para.paragraph.indent));
102+
}
103+
104+
// Set left margin
105+
if (para.paragraph.marginLeft !== undefined) {
106+
pPr.setAttribute('marL', String(para.paragraph.marginLeft));
107+
}
108+
109+
// Set line spacing
110+
if (para.paragraph.lineSpacing !== undefined) {
111+
const lnSpc = element.ownerDocument.createElement('a:lnSpc');
112+
const spcPts = element.ownerDocument.createElement('a:spcPts');
113+
spcPts.setAttribute('val', String(Math.round(para.paragraph.lineSpacing * 100))); // Convert to 100ths of a point
114+
lnSpc.appendChild(spcPts);
115+
pPr.appendChild(lnSpc);
116+
}
117+
118+
// Set space before paragraph
119+
if (para.paragraph.spaceBefore !== undefined) {
120+
const spcBef = element.ownerDocument.createElement('a:spcBef');
121+
const spcPts = element.ownerDocument.createElement('a:spcPts');
122+
spcPts.setAttribute('val', String(Math.round(para.paragraph.spaceBefore * 100))); // Convert to 100ths of a point
123+
spcBef.appendChild(spcPts);
124+
pPr.appendChild(spcBef);
125+
}
126+
127+
// Set space after paragraph
128+
if (para.paragraph.spaceAfter !== undefined) {
129+
const spcAft = element.ownerDocument.createElement('a:spcAft');
130+
const spcPts = element.ownerDocument.createElement('a:spcPts');
131+
spcPts.setAttribute('val', String(Math.round(para.paragraph.spaceAfter * 100))); // Convert to 100ths of a point
132+
spcAft.appendChild(spcPts);
133+
pPr.appendChild(spcAft);
134+
}
135+
}
136+
137+
// Handle text runs - if textRuns array exists, use it; otherwise, create a single run with paragraph text
138+
if (para.textRuns && para.textRuns.length > 0) {
139+
// Process each text run in the paragraph
140+
para.textRuns.forEach(run => {
141+
// Create text run
142+
const r = element.ownerDocument.createElement('a:r');
143+
p.appendChild(r);
144+
145+
// Create text properties element
146+
const rPr = element.ownerDocument.createElement('a:rPr');
147+
r.appendChild(rPr);
148+
149+
// Apply text styling if specified
150+
if (run.style) {
151+
ModifyTextHelper.style(run.style)(rPr);
152+
}
153+
154+
// Create text element
155+
const t = element.ownerDocument.createElement('a:t');
156+
r.appendChild(t);
157+
158+
// Handle empty strings with xml:space="preserve"
159+
if (run.text === '') {
160+
t.setAttribute('xml:space', 'preserve');
161+
}
162+
163+
// Set text content
164+
const textNode = element.ownerDocument.createTextNode(run.text || '');
165+
t.appendChild(textNode);
166+
});
167+
} else if (para.text !== undefined) {
168+
// Create a single text run for the paragraph text
169+
const r = element.ownerDocument.createElement('a:r');
170+
p.appendChild(r);
171+
172+
// Create text properties element
173+
const rPr = element.ownerDocument.createElement('a:rPr');
174+
r.appendChild(rPr);
175+
176+
// Apply text styling
177+
if (para.style) {
178+
ModifyTextHelper.style(para.style)(rPr);
179+
}
180+
181+
// Create text element
182+
const t = element.ownerDocument.createElement('a:t');
183+
r.appendChild(t);
184+
185+
// Handle empty strings with xml:space="preserve"
186+
if (para.text === '') {
187+
t.setAttribute('xml:space', 'preserve');
188+
}
189+
190+
// Set text content
191+
const textNode = element.ownerDocument.createTextNode(String(para.text || ''));
192+
t.appendChild(textNode);
193+
}
194+
});
195+
};
196+
197+
37198
static setBulletList =
38-
(list) => (element: XmlElement): void => {
199+
(list) =>
200+
(element: XmlElement): void => {
39201
const xmlElements = new XmlElements(element);
40202
xmlElements.addBulletList(list);
41203
};
@@ -105,4 +267,26 @@ export default class ModifyTextHelper {
105267
(element: XmlElement): void => {
106268
ModifyXmlHelper.booleanAttribute('i', isItalics)(element);
107269
};
270+
271+
static htmlToText = (html: string) => {
272+
html =
273+
'<p><span style="font-size: 24px;">Testing layouts and exporting them.</span></p>\n' +
274+
'<ul>\n' +
275+
'<li>level 1 - 1</li>\n' +
276+
'<li>level 1 - 2</li>\n' +
277+
'<ul>\n' +
278+
'<li>level 1-2-1 <em>italics</em></li>\n' +
279+
'</ul>\n' +
280+
'<li>level 1 - 3</li>\n' +
281+
'<ul>\n' +
282+
'<li>level 1 - 3 - 1</li>\n' +
283+
'</ul>\n' +
284+
'</ul>\n' +
285+
'<p>Testing testing testing</p>\n' +
286+
'<p><strong>bold text</strong></p>\n';
287+
288+
return (element: XmlElement): void => {
289+
XmlHelper.dump(element);
290+
};
291+
};
108292
}

src/helper/xml/prstGeom.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const prstGeom = `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
2+
<a:prstGeom prst="roundRect" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
3+
<a:avLst>
4+
<a:gd name="adj" fmla="val 6653"/>
5+
</a:avLst>
6+
</a:prstGeom>
7+
`;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const setAttribute = ModifyHelper.setAttribute;
5151

5252
const setSolidFill = ModifyShapeHelper.setSolidFill;
5353
const setText = ModifyShapeHelper.setText;
54+
const setMultiText = ModifyTextHelper.setMultiText;
5455
const setBulletList = ModifyShapeHelper.setBulletList;
5556
const replaceText = ModifyShapeHelper.replaceText;
5657
const setPosition = ModifyShapeHelper.setPosition;
@@ -127,6 +128,7 @@ export const modify = {
127128
setAttribute,
128129
setSolidFill,
129130
setText,
131+
setMultiText,
130132
setBulletList,
131133
replaceText,
132134
setPosition,

0 commit comments

Comments
 (0)