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
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { ChartWorkbookUpdater } = require('./ChartWorkbookUpdater.js');
|
|
3
|
-
|
|
4
|
-
const logger = createLogger('ChartCacheGenerator');
|
|
1
|
+
const { ChartWorkbookUpdater } = require('./ChartWorkbookUpdater.js')
|
|
5
2
|
|
|
6
3
|
class ChartCacheGenerator {
|
|
7
4
|
/**
|
|
@@ -10,8 +7,8 @@ class ChartCacheGenerator {
|
|
|
10
7
|
static generateStrCache(values) {
|
|
11
8
|
const ptEntries = values
|
|
12
9
|
.map((val, i) => `<c:pt idx="${i}"><c:v>${this.#escapeXml(String(val))}</c:v></c:pt>`)
|
|
13
|
-
.join('')
|
|
14
|
-
return `<c:strCache><c:ptCount val="${values.length}"/>${ptEntries}</c:strCache
|
|
10
|
+
.join('')
|
|
11
|
+
return `<c:strCache><c:ptCount val="${values.length}"/>${ptEntries}</c:strCache>`
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
@@ -20,8 +17,8 @@ class ChartCacheGenerator {
|
|
|
20
17
|
static generateNumCache(values) {
|
|
21
18
|
const ptEntries = values
|
|
22
19
|
.map((val, i) => `<c:pt idx="${i}"><c:v>${Number(val) || 0}</c:v></c:pt>`)
|
|
23
|
-
.join('')
|
|
24
|
-
return `<c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="${values.length}"/>${ptEntries}</c:numCache
|
|
20
|
+
.join('')
|
|
21
|
+
return `<c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="${values.length}"/>${ptEntries}</c:numCache>`
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
/**
|
|
@@ -32,117 +29,137 @@ class ChartCacheGenerator {
|
|
|
32
29
|
* @param {string} sheetName - Target worksheet name.
|
|
33
30
|
*/
|
|
34
31
|
static updateCategories(xml, categories, sheetName = 'Sheet1') {
|
|
35
|
-
const count = categories.length
|
|
32
|
+
const count = categories.length
|
|
36
33
|
|
|
37
34
|
// Formula for categories: Sheet1!$A$2:$A$N
|
|
38
|
-
const formula = ChartWorkbookUpdater.getFormulaRange(sheetName, 2, 0, count + 1, 0)
|
|
39
|
-
const newStrCache = this.generateStrCache(categories)
|
|
35
|
+
const formula = ChartWorkbookUpdater.getFormulaRange(sheetName, 2, 0, count + 1, 0)
|
|
36
|
+
const newStrCache = this.generateStrCache(categories)
|
|
40
37
|
|
|
41
38
|
// Replace the entire <c:cat> block to ensure correct formula and cache
|
|
42
|
-
const catPattern = /(<c:cat>)([\s\S]*?)(<\/c:cat>)/g
|
|
39
|
+
const catPattern = /(<c:cat>)([\s\S]*?)(<\/c:cat>)/g
|
|
43
40
|
|
|
44
41
|
return xml.replace(catPattern, (match, open, content, close) => {
|
|
45
42
|
// Reconstruct the cat block
|
|
46
43
|
// Try to determine if it used strRef or numRef originally
|
|
47
|
-
let refTag = content.includes('<c:numRef>') ? 'numRef' : 'strRef'
|
|
44
|
+
let refTag = content.includes('<c:numRef>') ? 'numRef' : 'strRef'
|
|
48
45
|
// But typically categories are strings. Let's use strRef.
|
|
49
|
-
refTag = 'strRef'
|
|
46
|
+
refTag = 'strRef'
|
|
50
47
|
|
|
51
|
-
return `${open}<c:${refTag}><c:f>${formula}</c:f>${newStrCache}</c:${refTag}>${close}
|
|
52
|
-
})
|
|
48
|
+
return `${open}<c:${refTag}><c:f>${formula}</c:f>${newStrCache}</c:${refTag}>${close}`
|
|
49
|
+
})
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
/**
|
|
56
53
|
* Updates series names and values in chart XML.
|
|
57
54
|
*/
|
|
58
55
|
static updateSeries(xml, series, categoriesLength, sheetName = 'Sheet1') {
|
|
59
|
-
let updated = xml
|
|
60
|
-
const serPattern = /(<c:ser>)([\s\S]*?)(<\/c:ser>)/g
|
|
61
|
-
const serMatches = [...updated.matchAll(serPattern)]
|
|
56
|
+
let updated = xml
|
|
57
|
+
const serPattern = /(<c:ser>)([\s\S]*?)(<\/c:ser>)/g
|
|
58
|
+
const serMatches = [...updated.matchAll(serPattern)]
|
|
62
59
|
|
|
63
|
-
if (serMatches.length === 0) return xml
|
|
60
|
+
if (serMatches.length === 0) return xml
|
|
64
61
|
|
|
65
|
-
let serIndex = 0
|
|
62
|
+
let serIndex = 0
|
|
66
63
|
updated = updated.replace(serPattern, (match, open, content, close) => {
|
|
67
64
|
if (serIndex >= series.length) {
|
|
68
65
|
// If there are more series templates than data, we could drop them,
|
|
69
66
|
// but replacing with an empty string might break the XML layout if we're not careful.
|
|
70
67
|
// Actually, removing extra series is requested: "Allow removing old series".
|
|
71
|
-
return ''
|
|
68
|
+
return ''
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
const serData = series[serIndex]
|
|
75
|
-
const colIndex = serIndex + 1
|
|
76
|
-
serIndex
|
|
71
|
+
const serData = series[serIndex]
|
|
72
|
+
const colIndex = serIndex + 1 // Series data starts in column B (1)
|
|
73
|
+
serIndex++
|
|
77
74
|
|
|
78
|
-
let updatedContent = content
|
|
75
|
+
let updatedContent = content
|
|
79
76
|
|
|
80
77
|
// 1. Update Series Name (c:tx)
|
|
81
78
|
if (serData.name !== undefined) {
|
|
82
|
-
const nameFormula = ChartWorkbookUpdater.getFormulaSingleCell(sheetName, 1, colIndex)
|
|
83
|
-
const nameCache = `<c:strCache><c:ptCount val="1"/><c:pt idx="0"><c:v>${this.#escapeXml(serData.name)}</c:v></c:pt></c:strCache
|
|
79
|
+
const nameFormula = ChartWorkbookUpdater.getFormulaSingleCell(sheetName, 1, colIndex)
|
|
80
|
+
const nameCache = `<c:strCache><c:ptCount val="1"/><c:pt idx="0"><c:v>${this.#escapeXml(serData.name)}</c:v></c:pt></c:strCache>`
|
|
84
81
|
|
|
85
|
-
const txPattern = /(<c:tx>)([\s\S]*?)(<\/c:tx>)
|
|
82
|
+
const txPattern = /(<c:tx>)([\s\S]*?)(<\/c:tx>)/
|
|
86
83
|
if (txPattern.test(updatedContent)) {
|
|
87
84
|
updatedContent = updatedContent.replace(txPattern, (match, p1, p2, p3) => {
|
|
88
|
-
return `${p1}<c:strRef><c:f>${nameFormula}</c:f>${nameCache}</c:strRef>${p3}
|
|
89
|
-
})
|
|
85
|
+
return `${p1}<c:strRef><c:f>${nameFormula}</c:f>${nameCache}</c:strRef>${p3}`
|
|
86
|
+
})
|
|
90
87
|
} else {
|
|
91
88
|
// Some charts don't have <c:tx>, we prepend it after <c:order> or <c:idx>
|
|
92
|
-
const insertAfter = /(<c:order[^>]*>)
|
|
89
|
+
const insertAfter = /(<c:order[^>]*>)/
|
|
93
90
|
if (insertAfter.test(updatedContent)) {
|
|
94
91
|
updatedContent = updatedContent.replace(insertAfter, (match, p1) => {
|
|
95
|
-
return `${p1}<c:tx><c:strRef><c:f>${nameFormula}</c:f>${nameCache}</c:strRef></c:tx
|
|
96
|
-
})
|
|
92
|
+
return `${p1}<c:tx><c:strRef><c:f>${nameFormula}</c:f>${nameCache}</c:strRef></c:tx>`
|
|
93
|
+
})
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
// 2. Update Series Values (c:val)
|
|
102
99
|
if (serData.values !== undefined) {
|
|
103
|
-
const valuesCount = categoriesLength || serData.values.length
|
|
104
|
-
const valFormula = ChartWorkbookUpdater.getFormulaRange(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
const valuesCount = categoriesLength || serData.values.length
|
|
101
|
+
const valFormula = ChartWorkbookUpdater.getFormulaRange(
|
|
102
|
+
sheetName,
|
|
103
|
+
2,
|
|
104
|
+
colIndex,
|
|
105
|
+
valuesCount + 1,
|
|
106
|
+
colIndex
|
|
107
|
+
)
|
|
108
|
+
const valCache = this.generateNumCache(serData.values)
|
|
109
|
+
|
|
110
|
+
const valPattern = /(<c:val>)([\s\S]*?)(<\/c:val>)/
|
|
108
111
|
if (valPattern.test(updatedContent)) {
|
|
109
112
|
updatedContent = updatedContent.replace(valPattern, (match, p1, p2, p3) => {
|
|
110
|
-
return `${p1}<c:numRef><c:f>${valFormula}</c:f>${valCache}</c:numRef>${p3}
|
|
111
|
-
})
|
|
113
|
+
return `${p1}<c:numRef><c:f>${valFormula}</c:f>${valCache}</c:numRef>${p3}`
|
|
114
|
+
})
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
return `${open}${updatedContent}${close}
|
|
116
|
-
})
|
|
118
|
+
return `${open}${updatedContent}${close}`
|
|
119
|
+
})
|
|
117
120
|
|
|
118
|
-
return updated
|
|
121
|
+
return updated
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/**
|
|
122
125
|
* Clones a series template to support dynamic series addition.
|
|
123
126
|
*/
|
|
124
127
|
static appendDynamicSeries(xml, targetCount) {
|
|
125
|
-
const serPattern = /(<c:ser>)([\s\S]*?)(<\/c:ser>)/g
|
|
126
|
-
const matches = [...xml.matchAll(serPattern)]
|
|
127
|
-
if (matches.length === 0 || matches.length >= targetCount) return xml
|
|
128
|
+
const serPattern = /(<c:ser>)([\s\S]*?)(<\/c:ser>)/g
|
|
129
|
+
const matches = [...xml.matchAll(serPattern)]
|
|
130
|
+
if (matches.length === 0 || matches.length >= targetCount) return xml
|
|
128
131
|
|
|
129
132
|
// Use the last series as a template to clone
|
|
130
|
-
const templateMatch = matches[matches.length - 1]
|
|
131
|
-
const template = templateMatch[0]
|
|
133
|
+
const templateMatch = matches[matches.length - 1]
|
|
134
|
+
const template = templateMatch[0]
|
|
132
135
|
|
|
133
136
|
// Find the end of the last series
|
|
134
|
-
const lastIndex = templateMatch.index + template.length
|
|
137
|
+
const lastIndex = templateMatch.index + template.length
|
|
135
138
|
|
|
136
|
-
let newSeriesBlocks = ''
|
|
139
|
+
let newSeriesBlocks = ''
|
|
137
140
|
for (let i = matches.length; i < targetCount; i++) {
|
|
138
|
-
let clone = template
|
|
141
|
+
let clone = template
|
|
139
142
|
// Update c:idx and c:order
|
|
140
|
-
clone = clone.replace(/(<c:idx val=")\d+("\/>)/g, `$1${i}$2`)
|
|
141
|
-
clone = clone.replace(/(<c:order val=")\d+("\/>)/g, `$1${i}$2`)
|
|
142
|
-
newSeriesBlocks += clone
|
|
143
|
+
clone = clone.replace(/(<c:idx val=")\d+("\/>)/g, `$1${i}$2`)
|
|
144
|
+
clone = clone.replace(/(<c:order val=")\d+("\/>)/g, `$1${i}$2`)
|
|
145
|
+
newSeriesBlocks += clone
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
return xml.substring(0, lastIndex) + newSeriesBlocks + xml.substring(lastIndex)
|
|
148
|
+
return xml.substring(0, lastIndex) + newSeriesBlocks + xml.substring(lastIndex)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Updates the chart title in chart XML.
|
|
153
|
+
*/
|
|
154
|
+
static updateTitle(xml, title) {
|
|
155
|
+
const titleBlock = `<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${this.#escapeXml(title)}</a:t></a:r></a:p></c:rich></c:tx><c:layout/></c:title>`
|
|
156
|
+
if (xml.includes('<c:title>')) {
|
|
157
|
+
const fullTitlePattern = /(<c:title>[\s\S]*?<\/c:title>)/
|
|
158
|
+
return xml.replace(fullTitlePattern, titleBlock)
|
|
159
|
+
} else {
|
|
160
|
+
const chartPattern = /(<c:chart>)/
|
|
161
|
+
return xml.replace(chartPattern, `$1${titleBlock}`)
|
|
162
|
+
}
|
|
146
163
|
}
|
|
147
164
|
|
|
148
165
|
static #escapeXml(str) {
|
|
@@ -151,8 +168,8 @@ class ChartCacheGenerator {
|
|
|
151
168
|
.replace(/</g, '<')
|
|
152
169
|
.replace(/>/g, '>')
|
|
153
170
|
.replace(/"/g, '"')
|
|
154
|
-
.replace(/'/g, ''')
|
|
171
|
+
.replace(/'/g, ''')
|
|
155
172
|
}
|
|
156
173
|
}
|
|
157
174
|
|
|
158
|
-
module.exports = { ChartCacheGenerator }
|
|
175
|
+
module.exports = { ChartCacheGenerator }
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
const { createLogger } = require('../../utils/logger.js');
|
|
2
|
-
|
|
3
|
-
const logger = createLogger('ChartParser');
|
|
4
|
-
|
|
5
1
|
class ChartParser {
|
|
6
2
|
/**
|
|
7
3
|
* Finds a chart's relationship ID and type in a slide's XML based on shape name/id.
|
|
@@ -15,14 +11,14 @@ class ChartParser {
|
|
|
15
11
|
const shapeNamePattern = new RegExp(
|
|
16
12
|
`<p:cNvPr[^>]*name="${chartId}"[^>]*>(?:.*?)<c:chart[^>]*r:id="(rId\\d+)"`,
|
|
17
13
|
's'
|
|
18
|
-
)
|
|
19
|
-
const rIdMatch = shapeNamePattern.exec(slideXml)
|
|
14
|
+
)
|
|
15
|
+
const rIdMatch = shapeNamePattern.exec(slideXml)
|
|
20
16
|
if (rIdMatch) {
|
|
21
|
-
return { rId: rIdMatch[1] }
|
|
17
|
+
return { rId: rIdMatch[1] }
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
// Strategy 2: Find all chart graphicFrames and we will match later in manager
|
|
25
|
-
return null
|
|
21
|
+
return null
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
/**
|
|
@@ -33,13 +29,13 @@ class ChartParser {
|
|
|
33
29
|
*/
|
|
34
30
|
static parseChartData(xml) {
|
|
35
31
|
// This could be used for validation and extracting current chart cache
|
|
36
|
-
const ptCountMatch = xml.match(/<c:ptCount val="(\d+)"\/>/)
|
|
37
|
-
const pointCount = ptCountMatch ? parseInt(ptCountMatch[1], 10) : 0
|
|
32
|
+
const ptCountMatch = xml.match(/<c:ptCount val="(\d+)"\/>/)
|
|
33
|
+
const pointCount = ptCountMatch ? parseInt(ptCountMatch[1], 10) : 0
|
|
38
34
|
|
|
39
35
|
return {
|
|
40
|
-
pointCount
|
|
41
|
-
}
|
|
36
|
+
pointCount,
|
|
37
|
+
}
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
module.exports = { ChartParser }
|
|
41
|
+
module.exports = { ChartParser }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { REL_TYPES } = require('../RelationshipManager.js')
|
|
1
|
+
const { REL_TYPES } = require('../RelationshipManager.js')
|
|
2
2
|
|
|
3
3
|
class ChartRelationshipManager {
|
|
4
4
|
/**
|
|
@@ -10,26 +10,28 @@ class ChartRelationshipManager {
|
|
|
10
10
|
* @returns {Object} validation issues
|
|
11
11
|
*/
|
|
12
12
|
static validateChartRelationships(relationshipManager, zipManager, chartZipPath) {
|
|
13
|
-
const issues = { errors: [], warnings: [] }
|
|
14
|
-
const rels = relationshipManager.getRelationships(chartZipPath)
|
|
13
|
+
const issues = { errors: [], warnings: [] }
|
|
14
|
+
const rels = relationshipManager.getRelationships(chartZipPath)
|
|
15
15
|
|
|
16
|
-
let hasWorkbook = false
|
|
16
|
+
let hasWorkbook = false
|
|
17
17
|
for (const rel of rels) {
|
|
18
18
|
if (rel.type === REL_TYPES.PACKAGE) {
|
|
19
|
-
hasWorkbook = true
|
|
20
|
-
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
19
|
+
hasWorkbook = true
|
|
20
|
+
const xlsxPath = relationshipManager.resolveTarget(chartZipPath, rel.target)
|
|
21
21
|
if (!zipManager.hasFile(xlsxPath)) {
|
|
22
|
-
issues.errors.push(`Embedded workbook missing: ${xlsxPath}`)
|
|
22
|
+
issues.errors.push(`Embedded workbook missing: ${xlsxPath}`)
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (!hasWorkbook) {
|
|
28
|
-
issues.warnings.push(
|
|
28
|
+
issues.warnings.push(
|
|
29
|
+
`Chart ${chartZipPath} has no embedded workbook relationship. Live editing in PowerPoint may fail.`
|
|
30
|
+
)
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
return issues
|
|
33
|
+
return issues
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
module.exports = { ChartRelationshipManager }
|
|
37
|
+
module.exports = { ChartRelationshipManager }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const JSZip = require('jszip')
|
|
2
|
-
const { createLogger } = require('../../utils/logger.js')
|
|
1
|
+
const JSZip = require('jszip')
|
|
2
|
+
const { createLogger } = require('../../utils/logger.js')
|
|
3
3
|
|
|
4
|
-
const logger = createLogger('ChartWorkbookUpdater')
|
|
4
|
+
const logger = createLogger('ChartWorkbookUpdater')
|
|
5
5
|
|
|
6
6
|
class ChartWorkbookUpdater {
|
|
7
7
|
/**
|
|
@@ -12,83 +12,86 @@ class ChartWorkbookUpdater {
|
|
|
12
12
|
* @returns {Promise<Buffer>} - The updated XLSX buffer.
|
|
13
13
|
*/
|
|
14
14
|
static async updateWorkbook(workbookData, data) {
|
|
15
|
-
if (!workbookData) return null
|
|
15
|
+
if (!workbookData) return null
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
|
-
const zip = await JSZip.loadAsync(workbookData)
|
|
18
|
+
const zip = await JSZip.loadAsync(workbookData)
|
|
19
19
|
|
|
20
20
|
// Look for sheet1.xml
|
|
21
|
-
const sheetPath = 'xl/worksheets/sheet1.xml'
|
|
21
|
+
const sheetPath = 'xl/worksheets/sheet1.xml'
|
|
22
22
|
if (!zip.file(sheetPath)) {
|
|
23
|
-
logger.warn('sheet1.xml not found in embedded workbook, trying to find first sheet')
|
|
23
|
+
logger.warn('sheet1.xml not found in embedded workbook, trying to find first sheet')
|
|
24
24
|
// fallback to finding the first sheet
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const newSheetXml = this.#generateSheetXml(data)
|
|
28
|
-
zip.file(sheetPath, newSheetXml)
|
|
27
|
+
const newSheetXml = this.#generateSheetXml(data)
|
|
28
|
+
zip.file(sheetPath, newSheetXml)
|
|
29
29
|
|
|
30
30
|
// Clean up any existing Excel tables, as our new sheet data might not align with them
|
|
31
|
-
const tableFiles = Object.keys(zip.files).filter(f => f.startsWith('xl/tables/'))
|
|
32
|
-
tableFiles.forEach(f => zip.remove(f))
|
|
31
|
+
const tableFiles = Object.keys(zip.files).filter(f => f.startsWith('xl/tables/'))
|
|
32
|
+
tableFiles.forEach(f => zip.remove(f))
|
|
33
33
|
|
|
34
|
-
const sheetRels = Object.keys(zip.files).filter(f => f.startsWith('xl/worksheets/_rels/'))
|
|
35
|
-
sheetRels.forEach(f => zip.remove(f))
|
|
34
|
+
const sheetRels = Object.keys(zip.files).filter(f => f.startsWith('xl/worksheets/_rels/'))
|
|
35
|
+
sheetRels.forEach(f => zip.remove(f))
|
|
36
36
|
|
|
37
|
-
const contentTypesFile = zip.file('[Content_Types].xml')
|
|
37
|
+
const contentTypesFile = zip.file('[Content_Types].xml')
|
|
38
38
|
if (contentTypesFile) {
|
|
39
|
-
const contentTypesXml = await contentTypesFile.async('text')
|
|
40
|
-
const updatedContentTypes = contentTypesXml.replace(
|
|
41
|
-
|
|
39
|
+
const contentTypesXml = await contentTypesFile.async('text')
|
|
40
|
+
const updatedContentTypes = contentTypesXml.replace(
|
|
41
|
+
/<Override[^>]*PartName="\/xl\/tables\/[^"]*"[^>]*\/>/g,
|
|
42
|
+
''
|
|
43
|
+
)
|
|
44
|
+
zip.file('[Content_Types].xml', updatedContentTypes)
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
return await zip.generateAsync({
|
|
45
48
|
type: 'nodebuffer',
|
|
46
49
|
compression: 'DEFLATE',
|
|
47
|
-
compressionOptions: { level: 6 }
|
|
48
|
-
})
|
|
50
|
+
compressionOptions: { level: 6 },
|
|
51
|
+
})
|
|
49
52
|
} catch (err) {
|
|
50
|
-
console.error('Failed to update embedded workbook', err)
|
|
51
|
-
logger.error('Failed to update embedded workbook', err)
|
|
52
|
-
return workbookData
|
|
53
|
+
console.error('Failed to update embedded workbook', err)
|
|
54
|
+
logger.error('Failed to update embedded workbook', err)
|
|
55
|
+
return workbookData // Return original if failed
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
static #generateSheetXml(data) {
|
|
57
|
-
const { categories = [], series = [] } = data
|
|
60
|
+
const { categories = [], series = [] } = data
|
|
58
61
|
|
|
59
62
|
// Column count = 1 (categories) + series.length
|
|
60
|
-
const numCols = 1 + series.length
|
|
61
|
-
const numRows = 1 + categories.length
|
|
63
|
+
const numCols = 1 + series.length
|
|
64
|
+
const numRows = 1 + categories.length // Row 1 = headers
|
|
62
65
|
|
|
63
|
-
const lastColLetter = this.getColumnLetter(numCols - 1)
|
|
64
|
-
const dimensionRef = `A1:${lastColLetter}${numRows}
|
|
66
|
+
const lastColLetter = this.getColumnLetter(numCols - 1)
|
|
67
|
+
const dimensionRef = `A1:${lastColLetter}${numRows}`
|
|
65
68
|
|
|
66
|
-
let sheetData = '<sheetData>'
|
|
69
|
+
let sheetData = '<sheetData>'
|
|
67
70
|
|
|
68
71
|
// Row 1: Headers (empty cell A1, then series names)
|
|
69
|
-
sheetData += '<row r="1">'
|
|
70
|
-
sheetData += `<c r="A1" t="inlineStr"><is><t></t></is></c
|
|
72
|
+
sheetData += '<row r="1">'
|
|
73
|
+
sheetData += `<c r="A1" t="inlineStr"><is><t></t></is></c>`
|
|
71
74
|
series.forEach((ser, i) => {
|
|
72
|
-
const colLetter = this.getColumnLetter(i + 1)
|
|
73
|
-
sheetData += `<c r="${colLetter}1" t="inlineStr"><is><t>${this.#escapeXml(ser.name || '')}</t></is></c
|
|
74
|
-
})
|
|
75
|
-
sheetData += '</row>'
|
|
75
|
+
const colLetter = this.getColumnLetter(i + 1)
|
|
76
|
+
sheetData += `<c r="${colLetter}1" t="inlineStr"><is><t>${this.#escapeXml(ser.name || '')}</t></is></c>`
|
|
77
|
+
})
|
|
78
|
+
sheetData += '</row>'
|
|
76
79
|
|
|
77
80
|
// Rows 2..N: Data (category name in A, then values)
|
|
78
81
|
categories.forEach((cat, rowIndex) => {
|
|
79
|
-
const r = rowIndex + 2
|
|
80
|
-
sheetData += `<row r="${r}"
|
|
81
|
-
sheetData += `<c r="A${r}" t="inlineStr"><is><t>${this.#escapeXml(String(cat))}</t></is></c
|
|
82
|
+
const r = rowIndex + 2 // +1 for 1-based, +1 for header row
|
|
83
|
+
sheetData += `<row r="${r}">`
|
|
84
|
+
sheetData += `<c r="A${r}" t="inlineStr"><is><t>${this.#escapeXml(String(cat))}</t></is></c>`
|
|
82
85
|
|
|
83
86
|
series.forEach((ser, colIndex) => {
|
|
84
|
-
const colLetter = this.getColumnLetter(colIndex + 1)
|
|
85
|
-
const val = ser.values && ser.values[rowIndex] !== undefined ? ser.values[rowIndex] : 0
|
|
86
|
-
sheetData += `<c r="${colLetter}${r}"><v>${Number(val)}</v></c
|
|
87
|
-
})
|
|
88
|
-
sheetData += '</row>'
|
|
89
|
-
})
|
|
87
|
+
const colLetter = this.getColumnLetter(colIndex + 1)
|
|
88
|
+
const val = ser.values && ser.values[rowIndex] !== undefined ? ser.values[rowIndex] : 0
|
|
89
|
+
sheetData += `<c r="${colLetter}${r}"><v>${Number(val)}</v></c>`
|
|
90
|
+
})
|
|
91
|
+
sheetData += '</row>'
|
|
92
|
+
})
|
|
90
93
|
|
|
91
|
-
sheetData += '</sheetData>'
|
|
94
|
+
sheetData += '</sheetData>'
|
|
92
95
|
|
|
93
96
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
94
97
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
@@ -96,27 +99,27 @@ class ChartWorkbookUpdater {
|
|
|
96
99
|
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
|
|
97
100
|
<sheetFormatPr defaultRowHeight="15"/>
|
|
98
101
|
${sheetData}
|
|
99
|
-
</worksheet
|
|
102
|
+
</worksheet>`
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
static getColumnLetter(colIndex) {
|
|
103
|
-
let letter = ''
|
|
106
|
+
let letter = ''
|
|
104
107
|
while (colIndex >= 0) {
|
|
105
|
-
letter = String.fromCharCode(65 + (colIndex % 26)) + letter
|
|
106
|
-
colIndex = Math.floor(colIndex / 26) - 1
|
|
108
|
+
letter = String.fromCharCode(65 + (colIndex % 26)) + letter
|
|
109
|
+
colIndex = Math.floor(colIndex / 26) - 1
|
|
107
110
|
}
|
|
108
|
-
return letter
|
|
111
|
+
return letter
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
static getFormulaRange(sheetName, startRow, startCol, endRow, endCol) {
|
|
112
|
-
const startLetter = this.getColumnLetter(startCol)
|
|
113
|
-
const endLetter = this.getColumnLetter(endCol)
|
|
114
|
-
return `${sheetName}!$${startLetter}$${startRow}:$${endLetter}$${endRow}
|
|
115
|
+
const startLetter = this.getColumnLetter(startCol)
|
|
116
|
+
const endLetter = this.getColumnLetter(endCol)
|
|
117
|
+
return `${sheetName}!$${startLetter}$${startRow}:$${endLetter}$${endRow}`
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
static getFormulaSingleCell(sheetName, row, col) {
|
|
118
|
-
const letter = this.getColumnLetter(col)
|
|
119
|
-
return `${sheetName}!$${letter}$${row}
|
|
121
|
+
const letter = this.getColumnLetter(col)
|
|
122
|
+
return `${sheetName}!$${letter}$${row}`
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
static #escapeXml(str) {
|
|
@@ -125,8 +128,8 @@ class ChartWorkbookUpdater {
|
|
|
125
128
|
.replace(/</g, '<')
|
|
126
129
|
.replace(/>/g, '>')
|
|
127
130
|
.replace(/"/g, '"')
|
|
128
|
-
.replace(/'/g, ''')
|
|
131
|
+
.replace(/'/g, ''')
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
module.exports = { ChartWorkbookUpdater }
|
|
135
|
+
module.exports = { ChartWorkbookUpdater }
|