node-pptx-templater 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +336 -281
- package/package.json +1 -1
- package/src/cli/commands/build.js +30 -31
- package/src/cli/commands/debug.js +23 -23
- package/src/cli/commands/extract.js +21 -21
- package/src/cli/commands/inspect.js +23 -23
- package/src/cli/commands/validate.js +17 -17
- package/src/cli/index.js +39 -36
- package/src/core/OutputWriter.js +79 -78
- package/src/core/PPTXTemplater.js +856 -273
- package/src/core/TemplateEngine.js +67 -71
- package/src/core/ValidationEngine.js +246 -0
- package/src/index.js +30 -17
- package/src/managers/ChartManager.js +195 -70
- package/src/managers/ContentTypesManager.js +49 -45
- package/src/managers/HyperlinkManager.js +146 -142
- package/src/managers/ImageManager.js +336 -0
- package/src/managers/MediaManager.js +62 -81
- package/src/managers/RelationshipManager.js +99 -95
- package/src/managers/ShapeManager.js +340 -0
- package/src/managers/SlideManager.js +408 -311
- package/src/managers/TableManager.js +979 -262
- package/src/managers/TextManager.js +197 -0
- package/src/managers/ZipManager.js +69 -69
- package/src/managers/charts/ChartCacheGenerator.js +75 -58
- package/src/managers/charts/ChartParser.js +9 -13
- package/src/managers/charts/ChartRelationshipManager.js +12 -10
- package/src/managers/charts/ChartWorkbookUpdater.js +59 -56
- package/src/parsers/XMLParser.js +47 -50
- package/src/templates/blankPptx.js +3 -2
- package/src/templates/slideTemplate.js +28 -34
- package/src/utils/contentTypesHelper.js +40 -54
- package/src/utils/errors.js +18 -18
- package/src/utils/idUtils.js +16 -14
- package/src/utils/logger.js +18 -16
- package/src/utils/relationshipUtils.js +19 -20
- package/src/utils/xmlUtils.js +26 -26
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
* This "text normalization" approach correctly handles fragmented placeholders.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
const { createLogger } = require('../utils/logger.js')
|
|
29
|
+
const { createLogger } = require('../utils/logger.js')
|
|
30
30
|
|
|
31
|
-
const logger = createLogger('TemplateEngine')
|
|
31
|
+
const logger = createLogger('TemplateEngine')
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Default placeholder pattern: {{key}}
|
|
35
35
|
* Can be overridden per-call.
|
|
36
36
|
*/
|
|
37
|
-
const DEFAULT_PLACEHOLDER_PATTERN = /\{\{([^{}]+)\}\}/g
|
|
37
|
+
const DEFAULT_PLACEHOLDER_PATTERN = /\{\{([^{}]+)\}\}/g
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* @class TemplateEngine
|
|
@@ -44,13 +44,13 @@ const DEFAULT_PLACEHOLDER_PATTERN = /\{\{([^{}]+)\}\}/g;
|
|
|
44
44
|
*/
|
|
45
45
|
class TemplateEngine {
|
|
46
46
|
/** @private @type {XMLParser} */
|
|
47
|
-
#xmlParser
|
|
47
|
+
#xmlParser
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* @param {XMLParser} xmlParser
|
|
51
51
|
*/
|
|
52
52
|
constructor(xmlParser) {
|
|
53
|
-
this.#xmlParser = xmlParser
|
|
53
|
+
this.#xmlParser = xmlParser
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -68,28 +68,28 @@ class TemplateEngine {
|
|
|
68
68
|
* '{{date}}': '2026-01-01'
|
|
69
69
|
* });
|
|
70
70
|
*/
|
|
71
|
-
replaceTextInXml(slideXml, replacements
|
|
71
|
+
replaceTextInXml(slideXml, replacements) {
|
|
72
72
|
if (!replacements || Object.keys(replacements).length === 0) {
|
|
73
|
-
return slideXml
|
|
73
|
+
return slideXml
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
logger.debug(`Replacing ${Object.keys(replacements).length} placeholder(s)`)
|
|
76
|
+
logger.debug(`Replacing ${Object.keys(replacements).length} placeholder(s)`)
|
|
77
77
|
|
|
78
78
|
// Step 1: Process paragraph by paragraph to handle fragmented runs
|
|
79
|
-
let updated = this.#processParagraphs(slideXml, replacements)
|
|
79
|
+
let updated = this.#processParagraphs(slideXml, replacements)
|
|
80
80
|
|
|
81
81
|
// Step 2: Simple direct replacement for any remaining unfragmented placeholders
|
|
82
82
|
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
83
|
-
const escaped = this.#escapeXml(String(value))
|
|
84
|
-
const placeholderEscaped = this.#escapeXml(placeholder)
|
|
83
|
+
const escaped = this.#escapeXml(String(value))
|
|
84
|
+
const placeholderEscaped = this.#escapeXml(placeholder)
|
|
85
85
|
|
|
86
86
|
// Replace the XML-escaped form (e.g., {{name}} as {{name}})
|
|
87
|
-
updated = updated.split(placeholderEscaped).join(escaped)
|
|
87
|
+
updated = updated.split(placeholderEscaped).join(escaped)
|
|
88
88
|
// Replace the plain form (in case it's not escaped in the XML)
|
|
89
|
-
updated = updated.split(placeholder).join(escaped)
|
|
89
|
+
updated = updated.split(placeholder).join(escaped)
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
return updated
|
|
92
|
+
return updated
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
@@ -103,30 +103,28 @@ class TemplateEngine {
|
|
|
103
103
|
*/
|
|
104
104
|
#processParagraphs(slideXml, replacements) {
|
|
105
105
|
// Find all <a:p>...</a:p> paragraphs
|
|
106
|
-
let updated = slideXml
|
|
107
|
-
let offset = 0
|
|
106
|
+
let updated = slideXml
|
|
107
|
+
let offset = 0
|
|
108
108
|
|
|
109
|
-
const paragraphPattern = /<a:p>([\s\S]*?)<\/a:p>/g
|
|
110
|
-
let match
|
|
109
|
+
const paragraphPattern = /<a:p>([\s\S]*?)<\/a:p>/g
|
|
110
|
+
let match
|
|
111
111
|
|
|
112
112
|
while ((match = paragraphPattern.exec(slideXml)) !== null) {
|
|
113
|
-
const paragraphXml = match[0]
|
|
114
|
-
const processedParagraph = this.#processParagraph(paragraphXml, replacements)
|
|
113
|
+
const paragraphXml = match[0]
|
|
114
|
+
const processedParagraph = this.#processParagraph(paragraphXml, replacements)
|
|
115
115
|
|
|
116
116
|
if (processedParagraph !== paragraphXml) {
|
|
117
|
-
// Replace in the updated string (adjust for offset changes)
|
|
118
|
-
const start = updated.indexOf(paragraphXml, match.index + offset - (match.index));
|
|
119
|
-
|
|
120
117
|
// More reliable: replace from the beginning of the current search area
|
|
121
|
-
updated =
|
|
118
|
+
updated =
|
|
119
|
+
updated.substring(0, match.index + offset) +
|
|
122
120
|
processedParagraph +
|
|
123
|
-
updated.substring(match.index + offset + paragraphXml.length)
|
|
121
|
+
updated.substring(match.index + offset + paragraphXml.length)
|
|
124
122
|
|
|
125
|
-
offset += processedParagraph.length - paragraphXml.length
|
|
123
|
+
offset += processedParagraph.length - paragraphXml.length
|
|
126
124
|
}
|
|
127
125
|
}
|
|
128
126
|
|
|
129
|
-
return updated
|
|
127
|
+
return updated
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
/**
|
|
@@ -139,32 +137,32 @@ class TemplateEngine {
|
|
|
139
137
|
*/
|
|
140
138
|
#processParagraph(paragraphXml, replacements) {
|
|
141
139
|
// Extract all text runs from this paragraph
|
|
142
|
-
const runs = this.#extractRuns(paragraphXml)
|
|
140
|
+
const runs = this.#extractRuns(paragraphXml)
|
|
143
141
|
|
|
144
|
-
if (runs.length === 0) return paragraphXml
|
|
142
|
+
if (runs.length === 0) return paragraphXml
|
|
145
143
|
|
|
146
144
|
// Combine text from all runs
|
|
147
|
-
const combinedText = runs.map(r => r.text).join('')
|
|
145
|
+
const combinedText = runs.map(r => r.text).join('')
|
|
148
146
|
|
|
149
147
|
// Check if any placeholder appears in the combined text
|
|
150
|
-
let hasPlaceholder = false
|
|
148
|
+
let hasPlaceholder = false
|
|
151
149
|
for (const placeholder of Object.keys(replacements)) {
|
|
152
150
|
if (combinedText.includes(placeholder)) {
|
|
153
|
-
hasPlaceholder = true
|
|
154
|
-
break
|
|
151
|
+
hasPlaceholder = true
|
|
152
|
+
break
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
if (!hasPlaceholder) return paragraphXml
|
|
156
|
+
if (!hasPlaceholder) return paragraphXml
|
|
159
157
|
|
|
160
158
|
// Perform replacement on combined text
|
|
161
|
-
let replacedText = combinedText
|
|
159
|
+
let replacedText = combinedText
|
|
162
160
|
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
163
|
-
replacedText = replacedText.split(placeholder).join(String(value))
|
|
161
|
+
replacedText = replacedText.split(placeholder).join(String(value))
|
|
164
162
|
}
|
|
165
163
|
|
|
166
164
|
// Rebuild the paragraph: merge all runs into a single run using first run's format
|
|
167
|
-
return this.#mergeRunsWithText(paragraphXml, runs, replacedText)
|
|
165
|
+
return this.#mergeRunsWithText(paragraphXml, runs, replacedText)
|
|
168
166
|
}
|
|
169
167
|
|
|
170
168
|
/**
|
|
@@ -176,25 +174,25 @@ class TemplateEngine {
|
|
|
176
174
|
* @returns {Array<{xml: string, text: string, start: number, end: number}>}
|
|
177
175
|
*/
|
|
178
176
|
#extractRuns(paragraphXml) {
|
|
179
|
-
const runs = []
|
|
180
|
-
const runPattern = /(<a:r(?:\s[^>]*)?>)([\s\S]*?)(<\/a:r>)/g
|
|
181
|
-
let match
|
|
177
|
+
const runs = []
|
|
178
|
+
const runPattern = /(<a:r(?:\s[^>]*)?>)([\s\S]*?)(<\/a:r>)/g
|
|
179
|
+
let match
|
|
182
180
|
|
|
183
181
|
while ((match = runPattern.exec(paragraphXml)) !== null) {
|
|
184
|
-
const runXml = match[0]
|
|
182
|
+
const runXml = match[0]
|
|
185
183
|
// Extract text from <a:t>...</a:t> within this run
|
|
186
|
-
const tMatch = /<a:t>([\s\S]*?)<\/a:t>/.exec(runXml)
|
|
187
|
-
const text = tMatch ? this.#unescapeXml(tMatch[1]) : ''
|
|
184
|
+
const tMatch = /<a:t>([\s\S]*?)<\/a:t>/.exec(runXml)
|
|
185
|
+
const text = tMatch ? this.#unescapeXml(tMatch[1]) : ''
|
|
188
186
|
|
|
189
187
|
runs.push({
|
|
190
188
|
xml: runXml,
|
|
191
189
|
text,
|
|
192
190
|
start: match.index,
|
|
193
191
|
end: match.index + runXml.length,
|
|
194
|
-
})
|
|
192
|
+
})
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
return runs
|
|
195
|
+
return runs
|
|
198
196
|
}
|
|
199
197
|
|
|
200
198
|
/**
|
|
@@ -208,25 +206,23 @@ class TemplateEngine {
|
|
|
208
206
|
* @returns {string} Updated paragraph XML.
|
|
209
207
|
*/
|
|
210
208
|
#mergeRunsWithText(paragraphXml, runs, newText) {
|
|
211
|
-
if (runs.length === 0) return paragraphXml
|
|
209
|
+
if (runs.length === 0) return paragraphXml
|
|
212
210
|
|
|
213
211
|
// Use the first run as the format template
|
|
214
|
-
const firstRun = runs[0]
|
|
212
|
+
const firstRun = runs[0]
|
|
215
213
|
|
|
216
214
|
// Build the replacement run: first run's format + new text
|
|
217
|
-
const mergedRunXml = this.#setRunText(firstRun.xml, this.#escapeXml(newText))
|
|
215
|
+
const mergedRunXml = this.#setRunText(firstRun.xml, this.#escapeXml(newText))
|
|
218
216
|
|
|
219
217
|
// Build new paragraph:
|
|
220
218
|
// Keep everything before first run, insert merged run, remove the rest,
|
|
221
219
|
// keep everything after last run
|
|
222
|
-
const firstRunStart = firstRun.start
|
|
223
|
-
const lastRunEnd = runs[runs.length - 1].end
|
|
220
|
+
const firstRunStart = firstRun.start
|
|
221
|
+
const lastRunEnd = runs[runs.length - 1].end
|
|
224
222
|
|
|
225
223
|
return (
|
|
226
|
-
paragraphXml.substring(0, firstRunStart) +
|
|
227
|
-
|
|
228
|
-
paragraphXml.substring(lastRunEnd)
|
|
229
|
-
);
|
|
224
|
+
paragraphXml.substring(0, firstRunStart) + mergedRunXml + paragraphXml.substring(lastRunEnd)
|
|
225
|
+
)
|
|
230
226
|
}
|
|
231
227
|
|
|
232
228
|
/**
|
|
@@ -238,12 +234,12 @@ class TemplateEngine {
|
|
|
238
234
|
* @returns {string} Updated run XML.
|
|
239
235
|
*/
|
|
240
236
|
#setRunText(runXml, text) {
|
|
241
|
-
const tPattern = /(<a:t>)([\s\S]*?)(<\/a:t>)
|
|
237
|
+
const tPattern = /(<a:t>)([\s\S]*?)(<\/a:t>)/
|
|
242
238
|
if (tPattern.test(runXml)) {
|
|
243
|
-
return runXml.replace(tPattern, `$1${text}$3`)
|
|
239
|
+
return runXml.replace(tPattern, `$1${text}$3`)
|
|
244
240
|
}
|
|
245
241
|
// If no <a:t>, add one before </a:r>
|
|
246
|
-
return runXml.replace('</a:r>', `<a:t>${text}</a:t></a:r>`)
|
|
242
|
+
return runXml.replace('</a:r>', `<a:t>${text}</a:t></a:r>`)
|
|
247
243
|
}
|
|
248
244
|
|
|
249
245
|
/**
|
|
@@ -254,7 +250,7 @@ class TemplateEngine {
|
|
|
254
250
|
* @returns {boolean}
|
|
255
251
|
*/
|
|
256
252
|
containsPlaceholders(text, replacements) {
|
|
257
|
-
return Object.keys(replacements).some(p => text.includes(p))
|
|
253
|
+
return Object.keys(replacements).some(p => text.includes(p))
|
|
258
254
|
}
|
|
259
255
|
|
|
260
256
|
/**
|
|
@@ -269,24 +265,24 @@ class TemplateEngine {
|
|
|
269
265
|
* // → ['{{title}}', '{{date}}', '{{company}}']
|
|
270
266
|
*/
|
|
271
267
|
extractPlaceholders(xml, pattern = DEFAULT_PLACEHOLDER_PATTERN) {
|
|
272
|
-
const placeholders = new Set()
|
|
273
|
-
const textPattern = /<a:t>([\s\S]*?)<\/a:t>/g
|
|
274
|
-
let match
|
|
268
|
+
const placeholders = new Set()
|
|
269
|
+
const textPattern = /<a:t>([\s\S]*?)<\/a:t>/g
|
|
270
|
+
let match
|
|
275
271
|
|
|
276
272
|
// Extract text content first, then find placeholders
|
|
277
|
-
const allText = []
|
|
273
|
+
const allText = []
|
|
278
274
|
while ((match = textPattern.exec(xml)) !== null) {
|
|
279
|
-
allText.push(match[1])
|
|
275
|
+
allText.push(match[1])
|
|
280
276
|
}
|
|
281
277
|
|
|
282
|
-
const combined = allText.join('')
|
|
283
|
-
const plPattern = new RegExp(pattern.source, 'g')
|
|
284
|
-
let plMatch
|
|
278
|
+
const combined = allText.join('')
|
|
279
|
+
const plPattern = new RegExp(pattern.source, 'g')
|
|
280
|
+
let plMatch
|
|
285
281
|
while ((plMatch = plPattern.exec(combined)) !== null) {
|
|
286
|
-
placeholders.add(plMatch[0])
|
|
282
|
+
placeholders.add(plMatch[0])
|
|
287
283
|
}
|
|
288
284
|
|
|
289
|
-
return Array.from(placeholders)
|
|
285
|
+
return Array.from(placeholders)
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
/**
|
|
@@ -301,7 +297,7 @@ class TemplateEngine {
|
|
|
301
297
|
.replace(/</g, '<')
|
|
302
298
|
.replace(/>/g, '>')
|
|
303
299
|
.replace(/"/g, '"')
|
|
304
|
-
.replace(/'/g, ''')
|
|
300
|
+
.replace(/'/g, ''')
|
|
305
301
|
}
|
|
306
302
|
|
|
307
303
|
/**
|
|
@@ -316,8 +312,8 @@ class TemplateEngine {
|
|
|
316
312
|
.replace(/</g, '<')
|
|
317
313
|
.replace(/>/g, '>')
|
|
318
314
|
.replace(/"/g, '"')
|
|
319
|
-
.replace(/'/g, "'")
|
|
315
|
+
.replace(/'/g, "'")
|
|
320
316
|
}
|
|
321
317
|
}
|
|
322
318
|
|
|
323
|
-
module.exports = { TemplateEngine }
|
|
319
|
+
module.exports = { TemplateEngine }
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview ValidationEngine - Complete validation and checking engine for PowerPoint presentations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @class ValidationEngine
|
|
7
|
+
* @description Runs audits and verification checks on PPTX files.
|
|
8
|
+
*/
|
|
9
|
+
class ValidationEngine {
|
|
10
|
+
/**
|
|
11
|
+
* Validates the entire presentation.
|
|
12
|
+
*
|
|
13
|
+
* @param {PPTXTemplater} ppt - The presentation templater instance.
|
|
14
|
+
* @returns {Promise<Object>} Structured report.
|
|
15
|
+
*/
|
|
16
|
+
static async validatePresentation(ppt) {
|
|
17
|
+
const errors = []
|
|
18
|
+
const warnings = []
|
|
19
|
+
|
|
20
|
+
// 1. Validate slides and references
|
|
21
|
+
const slides = ppt.slideManager.getAllSlideInfo()
|
|
22
|
+
for (const slide of slides) {
|
|
23
|
+
// Validate slide relationships
|
|
24
|
+
const relResult = this.validateRelationships(ppt, slide.zipPath)
|
|
25
|
+
errors.push(...relResult.errors.map(e => `Slide ${slide.index} relationship error: ${e}`))
|
|
26
|
+
warnings.push(
|
|
27
|
+
...relResult.warnings.map(w => `Slide ${slide.index} relationship warning: ${w}`)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// Validate slide XML and elements
|
|
31
|
+
const slideResult = await this.validateSlide(ppt, slide.index)
|
|
32
|
+
errors.push(...slideResult.errors)
|
|
33
|
+
warnings.push(...slideResult.warnings)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Validate presentation level relationships
|
|
37
|
+
const presRelResult = this.validateRelationships(ppt, 'ppt/presentation.xml')
|
|
38
|
+
errors.push(...presRelResult.errors.map(e => `Presentation relationship error: ${e}`))
|
|
39
|
+
warnings.push(...presRelResult.warnings.map(w => `Presentation relationship warning: ${w}`))
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
valid: errors.length === 0,
|
|
43
|
+
errors,
|
|
44
|
+
warnings,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validates a single slide's XML and elements (like tables, shapes, charts).
|
|
50
|
+
*
|
|
51
|
+
* @param {PPTXTemplater} ppt
|
|
52
|
+
* @param {number} slideIndex
|
|
53
|
+
* @returns {Promise<Object>}
|
|
54
|
+
*/
|
|
55
|
+
static async validateSlide(ppt, slideIndex) {
|
|
56
|
+
const errors = []
|
|
57
|
+
const warnings = []
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const slideXml = await ppt.slideManager.getSlideXmlAsync(slideIndex)
|
|
61
|
+
|
|
62
|
+
// Verify well-formed XML
|
|
63
|
+
const xmlCheck = ppt.xmlParser.validate(slideXml)
|
|
64
|
+
if (!xmlCheck.valid) {
|
|
65
|
+
errors.push(`Slide ${slideIndex} XML syntax error: ${xmlCheck.error}`)
|
|
66
|
+
return { valid: false, errors, warnings }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Verify tables on the slide
|
|
70
|
+
const tables = ppt.tableManager.inspectTables(slideIndex, ppt.slideManager)
|
|
71
|
+
for (const table of tables) {
|
|
72
|
+
const tableResult = await this.validateTable(ppt, slideIndex, table.id)
|
|
73
|
+
errors.push(
|
|
74
|
+
...tableResult.errors.map(e => `Slide ${slideIndex} Table "${table.name}": ${e}`)
|
|
75
|
+
)
|
|
76
|
+
warnings.push(
|
|
77
|
+
...tableResult.warnings.map(w => `Slide ${slideIndex} Table "${table.name}": ${w}`)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Verify charts on the slide
|
|
82
|
+
const charts = ppt.chartManager.getChartsInSlide(
|
|
83
|
+
slideIndex,
|
|
84
|
+
ppt.slideManager,
|
|
85
|
+
ppt.relationshipManager
|
|
86
|
+
)
|
|
87
|
+
for (const chart of charts) {
|
|
88
|
+
if (!ppt.zipManager.hasFile(chart.zipPath)) {
|
|
89
|
+
errors.push(
|
|
90
|
+
`Slide ${slideIndex} referenced chart file does not exist at ${chart.zipPath}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Verify image references on the slide
|
|
96
|
+
const images = ppt.imageManager.getImages(
|
|
97
|
+
slideIndex,
|
|
98
|
+
ppt.slideManager,
|
|
99
|
+
ppt.relationshipManager
|
|
100
|
+
)
|
|
101
|
+
for (const image of images) {
|
|
102
|
+
if (image.targetPath && !ppt.zipManager.hasFile(image.targetPath)) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`Slide ${slideIndex} referenced image file does not exist at ${image.targetPath}`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
errors.push(`Slide ${slideIndex} validation error: ${err.message}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
valid: errors.length === 0,
|
|
114
|
+
errors,
|
|
115
|
+
warnings,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validates a table's XML and OpenXML conformity.
|
|
121
|
+
*
|
|
122
|
+
* @param {PPTXTemplater} ppt
|
|
123
|
+
* @param {number} slideIndex
|
|
124
|
+
* @param {string} tableId
|
|
125
|
+
* @returns {Promise<Object>}
|
|
126
|
+
*/
|
|
127
|
+
static async validateTable(ppt, slideIndex, tableId) {
|
|
128
|
+
const errors = []
|
|
129
|
+
const warnings = []
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const slideXml = await ppt.slideManager.getSlideXmlAsync(slideIndex)
|
|
133
|
+
const slideObj = ppt.xmlParser.parse(slideXml, `slide${slideIndex}.xml`)
|
|
134
|
+
|
|
135
|
+
const spTree = slideObj?.['p:sld']?.['p:cSld']?.['p:spTree']
|
|
136
|
+
if (!spTree) {
|
|
137
|
+
errors.push('Slide shape tree not found')
|
|
138
|
+
return { valid: false, errors, warnings }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let frames = spTree['p:graphicFrame'] || []
|
|
142
|
+
if (!Array.isArray(frames)) frames = [frames]
|
|
143
|
+
|
|
144
|
+
let tbl = null
|
|
145
|
+
for (const frame of frames) {
|
|
146
|
+
const t = frame?.['a:graphic']?.['a:graphicData']?.['a:tbl']
|
|
147
|
+
if (!t) continue
|
|
148
|
+
const cNvPr = frame?.['p:nvGraphicFramePr']?.['p:cNvPr']
|
|
149
|
+
if (cNvPr && (cNvPr['@_name'] === tableId || String(cNvPr['@_id']) === tableId)) {
|
|
150
|
+
tbl = t
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!tbl) {
|
|
156
|
+
errors.push(`Table "${tableId}" not found in slide object`)
|
|
157
|
+
return { valid: false, errors, warnings }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check cols against gridcols
|
|
161
|
+
const cols = tbl['a:tblGrid']?.['a:gridCol'] || []
|
|
162
|
+
const trs = tbl['a:tr'] || []
|
|
163
|
+
|
|
164
|
+
if (cols.length === 0) {
|
|
165
|
+
errors.push('Table column definitions (tblGrid) are missing')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check rowIds duplicate values
|
|
169
|
+
const rowIds = new Set()
|
|
170
|
+
trs.forEach((tr, rIdx) => {
|
|
171
|
+
const tcs = tr['a:tc'] || []
|
|
172
|
+
if (tcs.length !== cols.length) {
|
|
173
|
+
warnings.push(
|
|
174
|
+
`Row ${rIdx} cell count (${tcs.length}) does not match grid columns count (${cols.length})`
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for rowId
|
|
179
|
+
const ext = tr['a:extLst']?.['a:ext']
|
|
180
|
+
const exts = Array.isArray(ext) ? ext : [ext]
|
|
181
|
+
for (const e of exts) {
|
|
182
|
+
if (e?.['a16:rowId']) {
|
|
183
|
+
const val = e['a16:rowId']['@_val']
|
|
184
|
+
if (val) {
|
|
185
|
+
if (rowIds.has(val)) {
|
|
186
|
+
errors.push(`Duplicate a16:rowId "${val}" found at row index ${rIdx}`)
|
|
187
|
+
}
|
|
188
|
+
rowIds.add(val)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
} catch (err) {
|
|
194
|
+
errors.push(`Table validation error: ${err.message}`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
valid: errors.length === 0,
|
|
199
|
+
errors,
|
|
200
|
+
warnings,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validates relationship mappings for a specific part.
|
|
206
|
+
*
|
|
207
|
+
* @param {PPTXTemplater} ppt
|
|
208
|
+
* @param {string} partPath
|
|
209
|
+
* @returns {Object}
|
|
210
|
+
*/
|
|
211
|
+
static validateRelationships(ppt, partPath) {
|
|
212
|
+
const errors = []
|
|
213
|
+
const warnings = []
|
|
214
|
+
|
|
215
|
+
const relsPath = ppt.relationshipManager.getRelsPath(partPath)
|
|
216
|
+
if (!ppt.zipManager.hasFile(relsPath)) {
|
|
217
|
+
warnings.push(`Relationship file missing at ${relsPath}`)
|
|
218
|
+
return { valid: true, errors, warnings }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const rels = ppt.relationshipManager.getRelationships(partPath)
|
|
222
|
+
const relIds = new Set()
|
|
223
|
+
|
|
224
|
+
for (const rel of rels) {
|
|
225
|
+
if (relIds.has(rel.id)) {
|
|
226
|
+
errors.push(`Duplicate relationship ID "${rel.id}" inside ${relsPath}`)
|
|
227
|
+
}
|
|
228
|
+
relIds.add(rel.id)
|
|
229
|
+
|
|
230
|
+
if (rel.targetMode !== 'External') {
|
|
231
|
+
const resolved = ppt.relationshipManager.resolveTarget(partPath, rel.target)
|
|
232
|
+
if (!ppt.zipManager.hasFile(resolved)) {
|
|
233
|
+
errors.push(`Relationship ${rel.id} points to non-existent file: ${resolved}`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
valid: errors.length === 0,
|
|
240
|
+
errors,
|
|
241
|
+
warnings,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { ValidationEngine }
|
package/src/index.js
CHANGED
|
@@ -24,23 +24,32 @@
|
|
|
24
24
|
* └─────────────────────────────────────────────────────────────┘
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
const { PPTXTemplater } = require('./core/PPTXTemplater.js')
|
|
28
|
-
const { ZipManager } = require('./managers/ZipManager.js')
|
|
29
|
-
const { XMLParser } = require('./parsers/XMLParser.js')
|
|
30
|
-
const { SlideManager } = require('./managers/SlideManager.js')
|
|
31
|
-
const { ChartManager } = require('./managers/ChartManager.js')
|
|
32
|
-
const { TableManager } = require('./managers/TableManager.js')
|
|
33
|
-
const {
|
|
34
|
-
const {
|
|
35
|
-
const {
|
|
36
|
-
const {
|
|
37
|
-
const {
|
|
27
|
+
const { PPTXTemplater } = require('./core/PPTXTemplater.js')
|
|
28
|
+
const { ZipManager } = require('./managers/ZipManager.js')
|
|
29
|
+
const { XMLParser } = require('./parsers/XMLParser.js')
|
|
30
|
+
const { SlideManager } = require('./managers/SlideManager.js')
|
|
31
|
+
const { ChartManager } = require('./managers/ChartManager.js')
|
|
32
|
+
const { TableManager } = require('./managers/TableManager.js')
|
|
33
|
+
const { ShapeManager } = require('./managers/ShapeManager.js')
|
|
34
|
+
const { ImageManager } = require('./managers/ImageManager.js')
|
|
35
|
+
const { TextManager } = require('./managers/TextManager.js')
|
|
36
|
+
const { HyperlinkManager } = require('./managers/HyperlinkManager.js')
|
|
37
|
+
const { MediaManager } = require('./managers/MediaManager.js')
|
|
38
|
+
const { RelationshipManager } = require('./managers/RelationshipManager.js')
|
|
39
|
+
const { OutputWriter } = require('./core/OutputWriter.js')
|
|
40
|
+
const { TemplateEngine } = require('./core/TemplateEngine.js')
|
|
41
|
+
const { ValidationEngine } = require('./core/ValidationEngine.js')
|
|
38
42
|
|
|
39
43
|
// Utility exports
|
|
40
|
-
const { generateRelationshipId, parseRelationshipId } = require('./utils/relationshipUtils.js')
|
|
41
|
-
const { validateXML, repairXML } = require('./utils/xmlUtils.js')
|
|
42
|
-
const { createLogger } = require('./utils/logger.js')
|
|
43
|
-
const {
|
|
44
|
+
const { generateRelationshipId, parseRelationshipId } = require('./utils/relationshipUtils.js')
|
|
45
|
+
const { validateXML, repairXML } = require('./utils/xmlUtils.js')
|
|
46
|
+
const { createLogger } = require('./utils/logger.js')
|
|
47
|
+
const {
|
|
48
|
+
PPTXError,
|
|
49
|
+
SlideNotFoundError,
|
|
50
|
+
ChartNotFoundError,
|
|
51
|
+
TableNotFoundError,
|
|
52
|
+
} = require('./utils/errors.js')
|
|
44
53
|
|
|
45
54
|
module.exports = {
|
|
46
55
|
PPTXTemplater,
|
|
@@ -49,11 +58,15 @@ module.exports = {
|
|
|
49
58
|
SlideManager,
|
|
50
59
|
ChartManager,
|
|
51
60
|
TableManager,
|
|
61
|
+
ShapeManager,
|
|
62
|
+
ImageManager,
|
|
63
|
+
TextManager,
|
|
52
64
|
HyperlinkManager,
|
|
53
65
|
MediaManager,
|
|
54
66
|
RelationshipManager,
|
|
55
67
|
OutputWriter,
|
|
56
68
|
TemplateEngine,
|
|
69
|
+
ValidationEngine,
|
|
57
70
|
generateRelationshipId,
|
|
58
71
|
parseRelationshipId,
|
|
59
72
|
validateXML,
|
|
@@ -62,5 +75,5 @@ module.exports = {
|
|
|
62
75
|
PPTXError,
|
|
63
76
|
SlideNotFoundError,
|
|
64
77
|
ChartNotFoundError,
|
|
65
|
-
TableNotFoundError
|
|
66
|
-
}
|
|
78
|
+
TableNotFoundError,
|
|
79
|
+
}
|